Current section
Files
Jump to
Current section
Files
c_src/git_checkout.hpp
//-----------------------------------------------------------------------------
// This code was derived from
// https://github.com/libgit2/libgit2/blob/main/examples/checkout.c
//-----------------------------------------------------------------------------
// libgit2 "checkout" - perform git checkouts
//-----------------------------------------------------------------------------
// Written by the libgit2 contributors
//
// To the extent possible under law, the author(s) have dedicated all copyright
// and related and neighboring rights to this software to the public domain
// worldwide. This software is distributed without any warranty.
//
// You should have received a copy of the CC0 Public Domain Dedication along
// with this software. If not, see
// <http://creativecommons.org/publicdomain/zero/1.0/>.
//-----------------------------------------------------------------------------
#pragma once
#include <git2/common.h>
// Define the printf format specifier to use for size_t output
#if defined(_MSC_VER) || defined(__MINGW32__)
# define PRIuZ "Iu"
# define PRIxZ "Ix"
# define PRIdZ "Id"
#else
# define PRIuZ "zu"
# define PRIxZ "zx"
# define PRIdZ "zd"
#endif
struct checkout_options {
checkout_options() : force(false), verbose(false), perf(false) {}
bool force; /// force the checkout to happen
bool verbose; /// show checkout progress
bool perf; /// show performance data
};
struct checkout_stats {
int total_steps;
int stat_calls;
int mkdir_calls;
int chmod_calls;
};
int resolve_refish(git_annotated_commit** commit, git_repository* repo, const char* refish)
{
assert(commit != NULL);
SmartPtr<git_reference> ref(git_reference_free);
int err = git_reference_dwim(&ref, repo, refish);
if (err == GIT_OK) {
git_annotated_commit_from_ref(commit, repo, ref);
return GIT_OK;
}
SmartPtr<git_object> obj(git_object_free);
err = git_revparse_single(&obj, repo, refish);
if (err == GIT_OK)
err = git_annotated_commit_lookup(commit, repo, git_object_id(obj));
return err;
}
// This corresponds to `git switch --guess`: if a given ref does
// not exist, git will by default try to guess the reference by
// seeing whether any remote has a branch called <ref>. If there
// is a single remote only that has it, then it is assumed to be
// the desired reference and a local branch is created for it.
//
// The following is a simplified implementation. It will not try
// to check whether the ref is unique across all remotes.
static int guess_refish(git_annotated_commit** out, git_repository* repo, const char* ref)
{
GitStrArray remotes;
int err;
if ((err = git_remote_list(&remotes, repo)) < 0)
return err;
SmartPtr<git_reference> remote_ref(git_reference_free);
for (size_t i = 0; i < remotes.size(); i++) {
auto refname = std::format("refs/remotes/{}/{}", remotes[i], ref);
if ((err = git_reference_lookup(&remote_ref, repo, refname.c_str())) < 0 && err != GIT_ENOTFOUND)
break;
}
if (!remote_ref)
return GIT_ENOTFOUND;
return git_annotated_commit_from_ref(out, repo, remote_ref);
}
// Implementation of checkout logic
ERL_NIF_TERM lg2_checkout(ErlNifEnv* env, git_repository* repo, std::string const& rev, ERL_NIF_TERM opts)
{
checkout_options o;
int err = 0;
// Parse options
{
ERL_NIF_TERM opt;
while (enif_get_list_cell(env, opts, &opt, &opts)) {
if (enif_is_identical(opt, ATOM_VERBOSE)) o.verbose = true;
else if (enif_is_identical(opt, ATOM_PERF)) o.perf = true;
else if (enif_is_identical(opt, ATOM_FORCE)) o.force = true;
else [[unlikely]]
return raise_badarg_exception(env, opt);
}
}
// Make sure we're not about to checkout while something else is going on
auto state = git_repository_state(repo);
if (state != GIT_REPOSITORY_STATE_NONE)
return make_error(env, "Repository is in unexpected state " + std::to_string(state));
SmartPtr<git_annotated_commit> target(git_annotated_commit_free);
err = resolve_refish(&target, repo, rev.c_str()) != GIT_OK
&& guess_refish (&target, repo, rev.c_str()) != GIT_OK;
if (err)
return make_git_error(env, "Failed to resolve " + rev);
checkout_stats stats{};
// This function is called to report progression, ie. it's called once with
// a NULL path and the number of total steps, then for each subsequent path,
// the current completed_step value.
auto save_checkout_progress =
[](const char* path, size_t completed_steps, size_t total_steps, void* payload)
{
auto stats = static_cast<checkout_stats*>(payload);
if (payload)
stats->total_steps = total_steps;
};
// This function is called when the checkout completes, and is used to report the
// number of syscalls performed.
auto save_perf_data =
[](const git_checkout_perfdata* perfdata, void* payload)
{
auto stats = static_cast<checkout_stats*>(payload);
if (stats) {
stats->stat_calls = perfdata->stat_calls;
stats->mkdir_calls = perfdata->mkdir_calls;
stats->chmod_calls = perfdata->chmod_calls;
}
};
// This is the main "checkout <branch>" logic, responsible for performing
// a branch-based checkout.
git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;
checkout_opts.checkout_strategy = o.force ? GIT_CHECKOUT_FORCE : GIT_CHECKOUT_SAFE;
if (o.verbose) {
checkout_opts.progress_cb = save_checkout_progress;
checkout_opts.progress_payload = &stats;
if (o.perf) {
checkout_opts.perfdata_cb = save_perf_data;
checkout_opts.perfdata_payload = &stats;
}
}
SmartPtr<git_commit> target_commit(git_commit_free);
/// Grab the commit we're interested to move to
if (git_commit_lookup(&target_commit, repo, git_annotated_commit_id(target.get())) < 0)
return make_git_error(env, "Failed to lookup commit");
// Perform the checkout so the workdir corresponds to what target_commit
// contains.
//
// Note that it's okay to pass a git_commit here, because it will be
// peeled to a tree.
if (git_checkout_tree(repo, target_commit.template cast<const git_object*>(), &checkout_opts) < 0)
return make_git_error(env, "Failed to checkout tree");
// Now that the checkout has completed, we have to update HEAD.
//
// Depending on the "origin" of target (ie. it's an OID or a branch name),
// we might need to detach HEAD.
auto annotated_ref = git_annotated_commit_ref(target);
if (!annotated_ref)
err = git_repository_set_head_detached_from_annotated(repo, target) != GIT_OK;
else {
const char* target_head;
SmartPtr<git_reference> ref(git_reference_free);
SmartPtr<git_reference> branch(git_reference_free);
if (git_reference_lookup(&ref, repo, annotated_ref) != GIT_OK)
return make_git_error(env, "Failed to lookup annotated HEAD reference");
if (!git_reference_is_remote(ref))
target_head = annotated_ref;
else if (git_branch_create_from_annotated(&branch, repo, rev.c_str(), target, 0) < 0)
return make_git_error(env, "Failed to update HEAD reference from remote branch");
else
target_head = git_reference_name(branch);
err = git_repository_set_head(repo, target_head) != GIT_OK;
}
if (err)
return make_git_error(env, "Failed to update HEAD reference");
if (!o.verbose)
return ATOM_OK;
ERL_NIF_TERM keys[] = {ATOM_TOTAL_STEPS, ATOM_STAT_CALLS, ATOM_MKDIR_CALLS, ATOM_CHMOD_CALLS};
ERL_NIF_TERM vals[] = {
enif_make_int(env, stats.total_steps),
enif_make_int(env, stats.stat_calls),
enif_make_int(env, stats.mkdir_calls),
enif_make_int(env, stats.chmod_calls),
};
ERL_NIF_TERM map;
return enif_make_map_from_arrays(env, keys, vals, 4, &map) ? map : enif_raise_exception(env, ATOM_ENOMEM);
}