1026 lines
34 KiB
C++
1026 lines
34 KiB
C++
#include "git_library.h"
|
|
#include "global_defines.h"
|
|
#include "local_git_repository.h"
|
|
|
|
#include <QDir>
|
|
#include <QDebug>
|
|
|
|
#include <git2.h>
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
// see https://libgit2.org/libgit2/ex/HEAD/checkout.html
|
|
|
|
/* Define t/he printf format specifier to use for size_t output */
|
|
#if defined(_MSC_VER) || defined(__MINGW32__)
|
|
# define PRIuZ "Illu"
|
|
# define PRIxZ "Illx"
|
|
# define PRIdZ "Illd"
|
|
#else
|
|
# define PRIuZ "zu"
|
|
# define PRIxZ "zx"
|
|
# define PRIdZ "zd"
|
|
#endif
|
|
|
|
typedef struct {
|
|
unsigned int force : 1;
|
|
unsigned int progress : 1;
|
|
unsigned int perf : 1;
|
|
} checkout_options;
|
|
|
|
int cred_acquire_cb(git_credential **out,
|
|
const char *url,
|
|
const char *username_from_url,
|
|
unsigned int allowed_types,
|
|
void *payload)
|
|
{
|
|
Q_UNUSED(url);
|
|
Q_UNUSED(username_from_url);
|
|
Q_UNUSED(payload);
|
|
int error = -1;
|
|
|
|
if (allowed_types & GIT_CREDENTIAL_USERPASS_PLAINTEXT) {
|
|
error = git_credential_userpass_plaintext_new(out,
|
|
GitLibrary::userName().toStdString().c_str(),
|
|
GitLibrary::userPassword().toStdString().c_str());
|
|
} else {
|
|
fprintf(stderr, "GIT_CREDENTIAL_USERPASS_PLAINTEXT not supported by protocol (allowed_types=%08x)\n", allowed_types);
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
static bool check(int error, const char *message, const char *extra) {
|
|
const git_error *lg2err;
|
|
const char *lg2msg = "", *lg2spacer = "";
|
|
|
|
if (!error) {
|
|
return true;
|
|
}
|
|
|
|
if ((lg2err = git_error_last()) != NULL && lg2err->message != NULL) {
|
|
lg2msg = lg2err->message;
|
|
lg2spacer = " - ";
|
|
}
|
|
|
|
if (extra)
|
|
fprintf(stderr, "%s '%s' [%d]%s%s\n",
|
|
message, extra, error, lg2spacer, lg2msg);
|
|
else
|
|
fprintf(stderr, "%s [%d]%s%s\n",
|
|
message, error, lg2spacer, lg2msg);
|
|
|
|
// exit(1);
|
|
return false;
|
|
}
|
|
|
|
static int resolve_refish(git_annotated_commit **commit, git_repository *repo, const char *refish) {
|
|
git_reference *ref;
|
|
git_object *obj;
|
|
int err = 0;
|
|
|
|
assert(commit != NULL);
|
|
|
|
err = git_reference_dwim(&ref, repo, refish);
|
|
if (err == GIT_OK) {
|
|
git_annotated_commit_from_ref(commit, repo, ref);
|
|
git_reference_free(ref);
|
|
return 0;
|
|
}
|
|
|
|
err = git_revparse_single(&obj, repo, refish);
|
|
if (err == GIT_OK) {
|
|
err = git_annotated_commit_lookup(commit, repo, git_object_id(obj));
|
|
git_object_free(obj);
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
static int perform_checkout_ref(git_repository *repo,
|
|
git_annotated_commit *target,
|
|
const char *target_ref, checkout_options *opts) {
|
|
git_checkout_options checkout_opts;
|
|
git_reference *ref = NULL, *branch = NULL;
|
|
git_commit *target_commit = NULL;
|
|
int err;
|
|
|
|
if ((err = git_checkout_options_init(&checkout_opts, GIT_CHECKOUT_OPTIONS_VERSION)) < 0) {
|
|
fprintf(stderr, "error checkout options init %s\n", git_error_last()->message);
|
|
return err;
|
|
}
|
|
|
|
// Setup our checkout options from the parsed options
|
|
|
|
checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE;
|
|
|
|
if (opts->force) {
|
|
// checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE;
|
|
}
|
|
|
|
if (opts->progress) {
|
|
// checkout_opts.progress_cb = print_checkout_progress;
|
|
}
|
|
|
|
if (opts->perf) {
|
|
// checkout_opts.perfdata_cb = print_perf_data;
|
|
}
|
|
|
|
// Grab the commit we're interested to move to
|
|
|
|
err = git_commit_lookup(&target_commit, repo, git_annotated_commit_id(target));
|
|
if (err != 0) {
|
|
fprintf(stderr, "failed to lookup commit: %s\n", git_error_last()->message);
|
|
goto cleanup;
|
|
}
|
|
|
|
// 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.
|
|
|
|
err = git_checkout_tree(repo, (const git_object *)target_commit, &checkout_opts);
|
|
if (err != 0) {
|
|
fprintf(stderr, "failed to checkout tree: %s\n", git_error_last()->message);
|
|
goto cleanup;
|
|
}
|
|
|
|
// 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.
|
|
|
|
if (git_annotated_commit_ref(target)) {
|
|
const char *target_head;
|
|
|
|
if ((err = git_reference_lookup(&ref, repo, git_annotated_commit_ref(target))) < 0)
|
|
goto error;
|
|
|
|
if (git_reference_is_remote(ref)) {
|
|
if ((err = git_branch_create_from_annotated(&branch, repo, target_ref, target, 0)) < 0)
|
|
goto error;
|
|
target_head = git_reference_name(branch);
|
|
} else {
|
|
target_head = git_annotated_commit_ref(target);
|
|
}
|
|
|
|
err = git_repository_set_head(repo, target_head);
|
|
} else {
|
|
err = git_repository_set_head_detached_from_annotated(repo, target);
|
|
}
|
|
|
|
error:
|
|
if (err != 0) {
|
|
fprintf(stderr, "failed to update HEAD reference: %s\n", git_error_last()->message);
|
|
goto cleanup;
|
|
}
|
|
|
|
cleanup:
|
|
git_commit_free(target_commit);
|
|
git_reference_free(branch);
|
|
git_reference_free(ref);
|
|
|
|
return err;
|
|
}
|
|
|
|
static int guess_refish(git_annotated_commit **out, git_repository *repo, const char *ref) {
|
|
git_strarray remotes = { NULL, 0 };
|
|
git_reference *remote_ref = NULL;
|
|
int error;
|
|
size_t i;
|
|
|
|
if ((error = git_remote_list(&remotes, repo)) < 0) {
|
|
fprintf(stderr, "error git_remote_list %s\n", git_error_last()->message);
|
|
goto out;
|
|
}
|
|
|
|
for (i = 0; i < remotes.count; i++) {
|
|
char *refname = NULL;
|
|
size_t reflen;
|
|
|
|
reflen = snprintf(refname, 0, "refs/remotes/%s/%s", remotes.strings[i], ref);
|
|
if ((refname = (char *)malloc(reflen + 1)) == NULL) {
|
|
error = -1;
|
|
fprintf(stderr, "%s:%d malloc failed\n", __func__, __LINE__);
|
|
goto next;
|
|
}
|
|
snprintf(refname, reflen + 1, "refs/remotes/%s/%s", remotes.strings[i], ref);
|
|
|
|
if ((error = git_reference_lookup(&remote_ref, repo, refname)) < 0) {
|
|
fprintf(stderr, "git reference lookup %s\n", git_error_last()->message);
|
|
goto next;
|
|
}
|
|
|
|
break;
|
|
next:
|
|
free(refname);
|
|
refname = NULL;
|
|
if (error < 0 && error != GIT_ENOTFOUND) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!remote_ref) {
|
|
error = GIT_ENOTFOUND;
|
|
goto out;
|
|
}
|
|
|
|
if ((error = git_annotated_commit_from_ref(out, repo, remote_ref)) < 0)
|
|
goto out;
|
|
|
|
out:
|
|
git_reference_free(remote_ref);
|
|
git_strarray_dispose(&remotes);
|
|
return error;
|
|
}
|
|
|
|
//
|
|
// git fetch
|
|
//
|
|
|
|
static int progress_cb(const char *str, int len, void *data) {
|
|
(void)data;
|
|
printf("remote: %.*s", len, str);
|
|
fflush(stdout); /* We don't have the \n to force the flush */
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* This function gets called for each remote-tracking branch that gets
|
|
* updated. The message we output depends on whether it's a new one or
|
|
* an update.
|
|
*/
|
|
static int update_cb(const char *refname, const git_oid *a, const git_oid *b, void *data) {
|
|
char a_str[GIT_OID_SHA1_HEXSIZE+1], b_str[GIT_OID_SHA1_HEXSIZE+1];
|
|
(void)data;
|
|
|
|
git_oid_fmt(b_str, b);
|
|
b_str[GIT_OID_SHA1_HEXSIZE] = '\0';
|
|
|
|
if (git_oid_is_zero(a)) {
|
|
printf("[new] %.20s %s\n", b_str, refname);
|
|
} else {
|
|
git_oid_fmt(a_str, a);
|
|
a_str[GIT_OID_SHA1_HEXSIZE] = '\0';
|
|
printf("[updated] %.10s..%.10s %s\n", a_str, b_str, refname);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* This gets called during the download and indexing. Here we show
|
|
* processed and total objects in the pack and the amount of received
|
|
* data. Most frontends will probably want to show a percentage and
|
|
* the download rate.
|
|
*/
|
|
static int transfer_progress_cb(const git_indexer_progress *stats, void *payload) {
|
|
(void)payload;
|
|
|
|
if (stats->received_objects == stats->total_objects) {
|
|
printf("Resolving deltas %u/%u\r",
|
|
stats->indexed_deltas, stats->total_deltas);
|
|
} else if (stats->total_objects > 0) {
|
|
printf("Received %u/%u objects (%u) in %" PRIuZ " bytes\r",
|
|
stats->received_objects, stats->total_objects,
|
|
stats->indexed_objects, stats->received_bytes);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int fetch(git_repository *repo, char const *repoName) {
|
|
git_remote *remote = NULL;
|
|
const git_indexer_progress *stats;
|
|
git_fetch_options fetch_opts;
|
|
//git_fetch_options fetch_opts = GIT_FETCH_OPTIONS_INIT;
|
|
|
|
git_fetch_options_init(&fetch_opts, GIT_FETCH_OPTIONS_VERSION);
|
|
|
|
/* Figure out whether it's a named remote or a URL */
|
|
printf("Fetching %s for repo %p\n", repoName, repo);
|
|
if (git_remote_lookup(&remote, repo, repoName) < 0)
|
|
if (git_remote_create_anonymous(&remote, repo, repoName) < 0)
|
|
goto on_error;
|
|
|
|
/* Set up the callbacks (only update_tips for now) */
|
|
fetch_opts.callbacks.update_tips = &update_cb;
|
|
fetch_opts.callbacks.sideband_progress = &progress_cb;
|
|
fetch_opts.callbacks.transfer_progress = transfer_progress_cb;
|
|
fetch_opts.callbacks.credentials = cred_acquire_cb;
|
|
|
|
/**
|
|
* Perform the fetch with the configured refspecs from the
|
|
* config. Update the reflog for the updated references with
|
|
* "fetch".
|
|
*/
|
|
if (git_remote_fetch(remote, NULL, &fetch_opts, "fetch") < 0)
|
|
goto on_error;
|
|
|
|
/**
|
|
* If there are local objects (we got a thin pack), then tell
|
|
* the user how many objects we saved from having to cross the
|
|
* network.
|
|
*/
|
|
stats = git_remote_stats(remote);
|
|
if (stats->local_objects > 0) {
|
|
printf("\rReceived %u/%u objects in %" PRIuZ " bytes (used %u local objects)\n",
|
|
stats->indexed_objects, stats->total_objects, stats->received_bytes, stats->local_objects);
|
|
} else{
|
|
printf("\rReceived %u/%u objects in %" PRIuZ "bytes\n",
|
|
stats->indexed_objects, stats->total_objects, stats->received_bytes);
|
|
}
|
|
|
|
git_remote_free(remote);
|
|
|
|
return 0;
|
|
|
|
on_error:
|
|
git_remote_free(remote);
|
|
return -1;
|
|
}
|
|
|
|
//
|
|
// git merge
|
|
//
|
|
struct merge_options {
|
|
const char **heads;
|
|
size_t heads_count;
|
|
|
|
git_annotated_commit **annotated;
|
|
size_t annotated_count;
|
|
|
|
unsigned int no_commit : 1;
|
|
};
|
|
|
|
static void merge_options_init(struct merge_options *opts) {
|
|
memset(opts, 0, sizeof(*opts));
|
|
|
|
opts->heads = NULL;
|
|
opts->heads_count = 0;
|
|
opts->annotated = NULL;
|
|
opts->annotated_count = 0;
|
|
}
|
|
|
|
static void parse_options(const char **repo_path, struct merge_options *opts) {
|
|
#if 0
|
|
struct args_info args = ARGS_INFO_INIT;
|
|
|
|
for (args.pos = 1; args.pos < argc; ++args.pos) {
|
|
const char *curr = argv[args.pos];
|
|
|
|
if (curr[0] != '-') {
|
|
opts_add_refish(opts, curr);
|
|
} else if (!strcmp(curr, "--no-commit")) {
|
|
opts->no_commit = 1;
|
|
} else if (match_str_arg(repo_path, &args, "--git-dir")) {
|
|
continue;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static void opts_add_refish(struct merge_options *opts, const char *refish) {
|
|
size_t sz;
|
|
|
|
assert(opts != NULL);
|
|
|
|
sz = ++opts->heads_count * sizeof(opts->heads[0]);
|
|
if ((opts->heads = (const char **)realloc((void *) opts->heads, sz)) != 0) {
|
|
opts->heads[opts->heads_count - 1] = refish;
|
|
}
|
|
}
|
|
|
|
static int resolve_heads(git_repository *repo, struct merge_options *opts) {
|
|
git_annotated_commit **annotated = (git_annotated_commit **)calloc(opts->heads_count, sizeof(git_annotated_commit *));
|
|
size_t annotated_count = 0, i;
|
|
int err = 0;
|
|
|
|
for (i = 0; i < opts->heads_count; i++) {
|
|
err = resolve_refish(&annotated[annotated_count++], repo, opts->heads[i]);
|
|
if (err != 0) {
|
|
fprintf(stderr, "failed to resolve refish %s: %s\n", opts->heads[i], git_error_last()->message);
|
|
annotated_count--;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (annotated_count != opts->heads_count) {
|
|
fprintf(stderr, "unable to parse some refish\n");
|
|
free(annotated);
|
|
return -1;
|
|
}
|
|
|
|
opts->annotated = annotated;
|
|
opts->annotated_count = annotated_count;
|
|
return 0;
|
|
}
|
|
|
|
static int perform_fastforward(git_repository *repo, const git_oid *target_oid, int is_unborn) {
|
|
// git_checkout_options ff_checkout_options = GIT_CHECKOUT_OPTIONS_INIT;
|
|
git_checkout_options ff_checkout_options;
|
|
|
|
// #define GIT_CHECKOUT_OPTIONS_INIT {GIT_CHECKOUT_OPTIONS_VERSION, GIT_CHECKOUT_SAFE}
|
|
|
|
git_checkout_options_init(&ff_checkout_options, GIT_CHECKOUT_OPTIONS_VERSION);
|
|
ff_checkout_options.checkout_strategy = GIT_CHECKOUT_SAFE;
|
|
|
|
git_reference *target_ref;
|
|
git_reference *new_target_ref;
|
|
git_object *target = NULL;
|
|
int err = 0;
|
|
|
|
if (is_unborn) {
|
|
const char *symbolic_ref;
|
|
git_reference *head_ref;
|
|
|
|
/* HEAD reference is unborn, lookup manually so we don't try to resolve it */
|
|
err = git_reference_lookup(&head_ref, repo, "HEAD");
|
|
if (err != 0) {
|
|
qCritical() << HEADER << "failed to lookup HEAD ref";
|
|
return -1;
|
|
}
|
|
|
|
/* Grab the reference HEAD should be pointing to */
|
|
symbolic_ref = git_reference_symbolic_target(head_ref);
|
|
|
|
/* Create our master reference on the target OID */
|
|
err = git_reference_create(&target_ref, repo, symbolic_ref, target_oid, 0, NULL);
|
|
if (err != 0) {
|
|
qCritical() << HEADER << "failed to create master reference";
|
|
return -1;
|
|
}
|
|
|
|
git_reference_free(head_ref);
|
|
} else {
|
|
/* HEAD exists, just lookup and resolve */
|
|
err = git_repository_head(&target_ref, repo);
|
|
if (err != 0) {
|
|
qCritical() << HEADER << "failed to get HEAD reference";
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/* Lookup the target object */
|
|
err = git_object_lookup(&target, repo, target_oid, GIT_OBJECT_COMMIT);
|
|
if (err != 0) {
|
|
qCritical() << HEADER << QString("failed to lookup OID %s").arg(git_oid_tostr_s(target_oid));
|
|
return -1;
|
|
}
|
|
|
|
/* Checkout the result so the workdir is in the expected state */
|
|
ff_checkout_options.checkout_strategy = GIT_CHECKOUT_SAFE;
|
|
err = git_checkout_tree(repo, target, &ff_checkout_options);
|
|
if (err != 0) {
|
|
qCritical() << HEADER << "failed to checkout HEAD reference";
|
|
return -1;
|
|
}
|
|
|
|
/* Move the target reference to the target OID */
|
|
err = git_reference_set_target(&new_target_ref, target_ref, target_oid, NULL);
|
|
if (err != 0) {
|
|
qCritical() << HEADER << "failed to move HEAD reference";
|
|
return -1;
|
|
}
|
|
|
|
git_reference_free(target_ref);
|
|
git_reference_free(new_target_ref);
|
|
git_object_free(target);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void output_conflicts(git_index *index) {
|
|
git_index_conflict_iterator *conflicts;
|
|
const git_index_entry *ancestor;
|
|
const git_index_entry *our;
|
|
const git_index_entry *their;
|
|
int err = 0;
|
|
|
|
check(git_index_conflict_iterator_new(&conflicts, index), "failed to create conflict iterator", NULL);
|
|
|
|
while ((err = git_index_conflict_next(&ancestor, &our, &their, conflicts)) == 0) {
|
|
qCritical() << HEADER
|
|
<< QString("conflict: a:%s o:%s t:%s")
|
|
.arg(ancestor ? ancestor->path : "NULL")
|
|
.arg(our->path ? our->path : "NULL")
|
|
.arg(their->path ? their->path : "NULL");
|
|
}
|
|
|
|
if (err != GIT_ITEROVER) {
|
|
qCritical() << HEADER << "error iterating conflicts";
|
|
}
|
|
|
|
git_index_conflict_iterator_free(conflicts);
|
|
}
|
|
|
|
static int create_merge_commit(git_repository *repo, git_index *index, struct merge_options *opts) {
|
|
git_oid tree_oid, commit_oid;
|
|
git_tree *tree;
|
|
git_signature *sign;
|
|
git_reference *merge_ref = NULL;
|
|
git_annotated_commit *merge_commit;
|
|
git_reference *head_ref;
|
|
git_commit **parents = (git_commit **)calloc(opts->annotated_count + 1, sizeof(git_commit *));
|
|
|
|
if (!parents) {
|
|
qCritical() << HEADER << "calloc failed";
|
|
return -1;
|
|
}
|
|
|
|
const char *msg_target = NULL;
|
|
size_t msglen = 0;
|
|
char *msg;
|
|
size_t i;
|
|
int err;
|
|
|
|
/* Grab our needed references */
|
|
check(git_repository_head(&head_ref, repo), "failed to get repo HEAD", NULL);
|
|
if (resolve_refish(&merge_commit, repo, opts->heads[0])) {
|
|
qCritical() << HEADER << QString("failed to resolve refish %s").arg(opts->heads[0]);
|
|
free(parents);
|
|
return -1;
|
|
}
|
|
|
|
/* Maybe that's a ref, so DWIM it */
|
|
err = git_reference_dwim(&merge_ref, repo, opts->heads[0]);
|
|
check(err, "failed to DWIM reference", git_error_last()->message);
|
|
|
|
/* Grab a signature */
|
|
check(git_signature_now(&sign, "Me", "me@example.com"), "failed to create signature", NULL);
|
|
|
|
#define MERGE_COMMIT_MSG "Merge %s '%s'"
|
|
/* Prepare a standard merge commit message */
|
|
if (merge_ref != NULL) {
|
|
check(git_branch_name(&msg_target, merge_ref), "failed to get branch name of merged ref", NULL);
|
|
} else {
|
|
msg_target = git_oid_tostr_s(git_annotated_commit_id(merge_commit));
|
|
}
|
|
|
|
msglen = snprintf(NULL, 0, MERGE_COMMIT_MSG, (merge_ref ? "branch" : "commit"), msg_target);
|
|
if (msglen > 0) {
|
|
msglen++;
|
|
}
|
|
|
|
if ((msg = (char *)malloc(msglen)) == nullptr) {
|
|
qCritical() << HEADER << 'malloc failed';
|
|
goto cleanup;
|
|
}
|
|
|
|
err = snprintf(msg, msglen, MERGE_COMMIT_MSG, (merge_ref ? "branch" : "commit"), msg_target);
|
|
|
|
/* This is only to silence the compiler */
|
|
if (err < 0) {
|
|
goto cleanup;
|
|
}
|
|
|
|
/* Setup our parent commits */
|
|
err = git_reference_peel((git_object **)&parents[0], head_ref, GIT_OBJECT_COMMIT);
|
|
check(err, "failed to peel head reference", NULL);
|
|
for (i = 0; i < opts->annotated_count; i++) {
|
|
git_commit_lookup(&parents[i + 1], repo, git_annotated_commit_id(opts->annotated[i]));
|
|
}
|
|
|
|
/* Prepare our commit tree */
|
|
check(git_index_write_tree(&tree_oid, index), "failed to write merged tree", NULL);
|
|
check(git_tree_lookup(&tree, repo, &tree_oid), "failed to lookup tree", NULL);
|
|
|
|
/* Commit time ! */
|
|
err = git_commit_create(&commit_oid,
|
|
repo, git_reference_name(head_ref),
|
|
sign, sign,
|
|
NULL, msg,
|
|
tree,
|
|
opts->annotated_count + 1, (const git_commit **)parents);
|
|
|
|
check(err, "failed to create commit", NULL);
|
|
|
|
/* We're done merging, cleanup the repository state */
|
|
git_repository_state_cleanup(repo);
|
|
|
|
cleanup:
|
|
free(parents);
|
|
return err;
|
|
}
|
|
|
|
static int merge(git_repository *repo) {
|
|
struct merge_options opts;
|
|
git_index *index;
|
|
git_repository_state_t state;
|
|
git_merge_analysis_t analysis;
|
|
git_merge_preference_t preference;
|
|
const char *path = ".";
|
|
int err = 0;
|
|
|
|
merge_options_init(&opts);
|
|
parse_options(&path, &opts);
|
|
|
|
state = (git_repository_state_t)git_repository_state(repo);
|
|
if (state != GIT_REPOSITORY_STATE_NONE) {
|
|
fprintf(stderr, "repository is in unexpected state %d\n", state);
|
|
goto cleanup;
|
|
}
|
|
|
|
err = resolve_heads(repo, &opts);
|
|
if (err != 0)
|
|
goto cleanup;
|
|
|
|
err = git_merge_analysis(&analysis, &preference,
|
|
repo,
|
|
(const git_annotated_commit **)opts.annotated,
|
|
opts.annotated_count);
|
|
check(err, "merge analysis failed", NULL);
|
|
|
|
if (analysis & GIT_MERGE_ANALYSIS_UP_TO_DATE) {
|
|
printf("Already up-to-date\n");
|
|
return 0;
|
|
} else if (analysis & GIT_MERGE_ANALYSIS_UNBORN ||
|
|
(analysis & GIT_MERGE_ANALYSIS_FASTFORWARD &&
|
|
!(preference & GIT_MERGE_PREFERENCE_NO_FASTFORWARD))) {
|
|
const git_oid *target_oid;
|
|
if (analysis & GIT_MERGE_ANALYSIS_UNBORN) {
|
|
printf("Unborn\n");
|
|
} else {
|
|
printf("Fast-forward\n");
|
|
}
|
|
|
|
/* Since this is a fast-forward, there can be only one merge head */
|
|
target_oid = git_annotated_commit_id(*opts.annotated);
|
|
assert(opts.annotated_count == 1);
|
|
|
|
return perform_fastforward(repo, target_oid, (analysis & GIT_MERGE_ANALYSIS_UNBORN));
|
|
} else if (analysis & GIT_MERGE_ANALYSIS_NORMAL) {
|
|
//git_merge_options merge_opts = GIT_MERGE_OPTIONS_INIT;
|
|
git_merge_options merge_opts;
|
|
|
|
git_merge_options_init(&merge_opts, GIT_MERGE_OPTIONS_VERSION);
|
|
|
|
// TODO: check this...
|
|
// merge_opts.file_flags = GIT_MERGE_FIND_RENAMES;
|
|
|
|
// GIT_MERGE_OPTIONS_VERSION, GIT_MERGE_FIND_RENAMES }
|
|
|
|
//git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;
|
|
git_checkout_options checkout_opts;
|
|
|
|
git_checkout_options_init(&checkout_opts, GIT_CHECKOUT_OPTIONS_VERSION);
|
|
checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE;
|
|
|
|
merge_opts.flags = 0;
|
|
merge_opts.file_flags = GIT_MERGE_FILE_STYLE_DIFF3;
|
|
|
|
checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE|GIT_CHECKOUT_ALLOW_CONFLICTS;
|
|
|
|
if (preference & GIT_MERGE_PREFERENCE_FASTFORWARD_ONLY) {
|
|
printf("Fast-forward is preferred, but only a merge is possible\n");
|
|
return -1;
|
|
}
|
|
|
|
err = git_merge(repo,
|
|
(const git_annotated_commit **)opts.annotated, opts.annotated_count,
|
|
&merge_opts, &checkout_opts);
|
|
check(err, "merge failed", NULL);
|
|
}
|
|
|
|
/* If we get here, we actually performed the merge above */
|
|
|
|
check(git_repository_index(&index, repo), "failed to get repository index", NULL);
|
|
|
|
if (git_index_has_conflicts(index)) {
|
|
/* Handle conflicts */
|
|
output_conflicts(index);
|
|
} else if (!opts.no_commit) {
|
|
create_merge_commit(repo, index, &opts);
|
|
printf("Merge made\n");
|
|
}
|
|
|
|
cleanup:
|
|
free((char **)opts.heads);
|
|
free(opts.annotated);
|
|
|
|
return 0;
|
|
}
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
|
|
QString GitLibrary::m_userName;
|
|
QString GitLibrary::m_userPassword;
|
|
|
|
int GitLibrary::Init() {
|
|
return git_libgit2_init();
|
|
}
|
|
|
|
int GitLibrary::Shutdown() {
|
|
return git_libgit2_shutdown();
|
|
}
|
|
|
|
CWD::CWD(QString const &localRepoName) {
|
|
m_prev = QDir::current();
|
|
m_valid = false;
|
|
|
|
if (!localRepoName.startsWith("customer_")) {
|
|
qCritical() << HEADER
|
|
<< "localRepoName not starting with customer_*";
|
|
return;
|
|
}
|
|
|
|
QString const &p = QDir::cleanPath(LocalGitRepository::GetReposRootDirectory()
|
|
+ QDir::separator() + localRepoName);
|
|
|
|
if (!QDir(p).exists()) {
|
|
if (!QDir().mkpath(p)) {
|
|
qCritical() << HEADER << "ERROR cannot mkdir" << p;
|
|
}
|
|
}
|
|
|
|
m_valid = QDir(p).exists();
|
|
|
|
if (m_valid) {
|
|
QDir::setCurrent(p);
|
|
qCritical() << HEADER << "setCurrent working directory" << p;
|
|
m_localRepoDir.setPath(p);
|
|
m_localRepoGitDir.setPath(p + QDir::separator() + ".git");
|
|
} else {
|
|
qCritical() << HEADER << "ERROR p" << p << "still not exists";
|
|
}
|
|
}
|
|
|
|
CWD::~CWD() {
|
|
QDir::setCurrent(m_prev.absolutePath());
|
|
qCritical() << HEADER << "setCurrent working directory" << m_prev.absolutePath();
|
|
}
|
|
|
|
int GitLibrary::CheckoutRepository(char const *url,
|
|
char const *localRepoName,
|
|
char const *branchName) {
|
|
int err = 0;
|
|
checkout_options opts;
|
|
git_repository_state_t state = GIT_REPOSITORY_STATE_NONE;
|
|
git_annotated_commit *checkout_target = NULL;
|
|
git_reference *branch_ref = NULL;
|
|
|
|
CWD cwd(localRepoName);
|
|
QString const &localRepoPath = cwd.localRepoDir().absolutePath();
|
|
|
|
if (!cwd.localRepoGitDir().exists()) {
|
|
|
|
if (!url) {
|
|
qCritical() << HEADER << "url" << url << "not given";
|
|
return -1;
|
|
}
|
|
|
|
qCritical() << HEADER
|
|
<< "localRepoGirDir" << cwd.localRepoGitDir().absolutePath()
|
|
<< "does not exist. cloning...";
|
|
|
|
if ((err = CloneRepository(url, localRepoName)) < 0) { // a "clone" checkouts "master"
|
|
return err;
|
|
}
|
|
}
|
|
|
|
qCritical() << HEADER
|
|
<< "checkout repository:" << localRepoPath
|
|
<< " branch:" << branchName << "...";
|
|
|
|
git_repository *repo = LocalGitRepository::GetInstance(localRepoName)->GetGitRepository();
|
|
if (!repo) { // no clone
|
|
std::string const &s = localRepoPath.toStdString();
|
|
char const *r = s.c_str();
|
|
if ((err = git_repository_open(&repo, r)) != 0) {
|
|
qCritical() << HEADER << "repository" << localRepoPath
|
|
<< "cannot be opened:" << QString(git_error_last()->message);
|
|
return err;
|
|
}
|
|
|
|
qCritical() << HEADER << "repository" << localRepoPath << "opened";
|
|
LocalGitRepository::GetInstance(localRepoName)->SetGitRepository(repo);
|
|
}
|
|
|
|
// Make sure we're not about to checkout while something else is going on
|
|
if ((state = (git_repository_state_t)git_repository_state(repo)) != GIT_REPOSITORY_STATE_NONE) {
|
|
qCritical() << __func__ << ":" << __LINE__ << "repository" << localRepoPath
|
|
<< "is in unexpected state" << (int)state;
|
|
err = -1;
|
|
} else {
|
|
if ((err = resolve_refish(&checkout_target, repo, branchName)) < 0 &&
|
|
(err = guess_refish(&checkout_target, repo, branchName)) < 0) {
|
|
qCritical() << __func__ << ":" << __LINE__ << "ERROR failed to resolve"
|
|
<< QString(branchName) << QString(git_error_last()->message);
|
|
} else {
|
|
if ((err = perform_checkout_ref(repo, checkout_target, branchName, &opts)) == 0) {
|
|
if ((err = git_branch_lookup(&branch_ref, repo, branchName, GIT_BRANCH_LOCAL)) != 0) {
|
|
if (err == GIT_ENOTFOUND) {
|
|
qCritical() << __func__ << ":" << __LINE__
|
|
<< "ERROR local branch" << branchName << "not found";
|
|
} else {
|
|
qCritical() << __func__ << ":" << __LINE__
|
|
<< "ERROR local branch" << branchName << "not found"
|
|
<< QString(git_error_last()->message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (branch_ref) {
|
|
git_reference_free(branch_ref);
|
|
}
|
|
|
|
if (checkout_target) {
|
|
git_annotated_commit_free(checkout_target);
|
|
}
|
|
|
|
if (err == 0) {
|
|
qCritical() << HEADER
|
|
<< "checkout repository" << localRepoPath
|
|
<< "and branch" << branchName << "done";
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
int GitLibrary::CloneRepository(char const *url, char const *localRepoName) {
|
|
CWD cwd(localRepoName);
|
|
int res = 0;
|
|
|
|
qCritical() << HEADER;
|
|
qCritical() << " url:" << url;
|
|
qCritical() << "repo:" << localRepoName;
|
|
|
|
git_clone_options opts;
|
|
if ((res = git_clone_options_init(&opts, GIT_CLONE_OPTIONS_VERSION)) == 0) {
|
|
opts.checkout_branch = "master";
|
|
git_repository *repo = LocalGitRepository::GetInstance(localRepoName)->GetGitRepository();
|
|
if (!repo) {
|
|
if ((res = git_clone(&repo, url, ".", &opts)) < 0) {
|
|
git_error const *error = git_error_last();
|
|
qCritical() << HEADER;
|
|
qCritical() << " error:" << error->message;
|
|
qCritical() << "localRepoDir:" << cwd.localRepoDir().absolutePath();
|
|
}
|
|
LocalGitRepository::GetInstance(localRepoName)->SetGitRepository(repo);
|
|
}
|
|
} else {
|
|
git_error const *error = git_error_last();
|
|
qCritical() << HEADER << error->message;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
int GitLibrary::CommitFile(char const *localRepoName, char const *branchName,
|
|
char const *fName, char const *commitMessage) {
|
|
CWD cwd(localRepoName);
|
|
int error = CheckoutRepository(nullptr, localRepoName, branchName);
|
|
if (error == 0) {
|
|
git_repository *repo = LocalGitRepository::GetInstance(localRepoName)->GetGitRepository();
|
|
|
|
git_oid commit_oid,tree_oid;
|
|
git_tree *tree = NULL;
|
|
git_index *index = NULL;
|
|
git_object *parent = NULL;
|
|
git_reference *ref = NULL;
|
|
git_signature *signature = NULL;
|
|
git_repository_state_t state = GIT_REPOSITORY_STATE_NONE;
|
|
|
|
if (git_repository_head_unborn(repo) == 1) {
|
|
qCritical() << HEADER << "HEAD unborn. Create first commit";
|
|
return -1;
|
|
}
|
|
|
|
// Make sure we're not about to checkout while something else is going on
|
|
if ((state = (git_repository_state_t)git_repository_state(repo)) != GIT_REPOSITORY_STATE_NONE) {
|
|
qCritical() << HEADER;
|
|
qCritical() << "ERROR repository" << cwd.localRepoDir().absolutePath()
|
|
<< "is in unexpected state" << (int)state;
|
|
error = -1;
|
|
} else {
|
|
error = git_revparse_ext(&parent, &ref, repo, "HEAD");
|
|
if (error == GIT_ENOTFOUND) {
|
|
qCritical() << HEADER << "HEAD not found. Creating first commit";
|
|
error = 0;
|
|
} else if (error != 0) {
|
|
const git_error *err = git_error_last();
|
|
if (err) {
|
|
qCritical() << HEADER << QString("ERROR %d: %s").arg(err->klass).arg(err->message);
|
|
} else {
|
|
qCritical() << HEADER << QString("ERROR no detailed error %d").arg(error);
|
|
}
|
|
}
|
|
|
|
if (error == 0) {
|
|
if (check(git_repository_index(&index, repo), "Could not open repository index", NULL) &&
|
|
check(git_index_write_tree(&tree_oid, index), "Could not write tree", NULL) &&
|
|
check(git_index_write(index), "Could not write index", NULL) &&
|
|
check(git_tree_lookup(&tree, repo, &tree_oid), "Error looking up tree", NULL) &&
|
|
check(git_signature_default(&signature, repo), "Error creating signature", NULL) &&
|
|
check(git_commit_create_v(&commit_oid, repo, "HEAD", signature, signature,
|
|
NULL, commitMessage, tree, parent ? 1 : 0, parent),
|
|
"Error creating commit", NULL)) {
|
|
qCritical() << HEADER;
|
|
qCritical() << "localRepoName" << QString::fromUtf8(localRepoName, strlen(localRepoName));
|
|
qCritical() << " branchName" << QString::fromUtf8(branchName, strlen(branchName));
|
|
qCritical() << " fName" << QString::fromUtf8(fName, strlen(fName));
|
|
qCritical() << "commitMessage" << QString::fromUtf8(commitMessage, strlen(commitMessage));
|
|
qCritical() << " git commit" << "OK";
|
|
}
|
|
|
|
if (index) {
|
|
git_index_free(index);
|
|
}
|
|
if (signature) {
|
|
git_signature_free(signature);
|
|
}
|
|
if (tree) {
|
|
git_tree_free(tree);
|
|
}
|
|
if (parent) {
|
|
git_object_free(parent);
|
|
}
|
|
if (ref) {
|
|
git_reference_free(ref);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
int GitLibrary::PushRepository(char const *localRepoName, char const *branchName,
|
|
char const *userName, char const *userPassword) {
|
|
CWD cwd(localRepoName);
|
|
int error = CheckoutRepository(nullptr, localRepoName, branchName);
|
|
if (error == 0) {
|
|
|
|
git_repository_state_t state = GIT_REPOSITORY_STATE_NONE;
|
|
git_push_options options;
|
|
git_remote_callbacks callbacks;
|
|
git_remote* remote = NULL;
|
|
char refspec_[] = "refs/heads/master";
|
|
char *refspec = refspec_;
|
|
const git_strarray refspecs = {
|
|
&refspec,
|
|
1
|
|
};
|
|
|
|
// std::lock_guard<std::mutex> lock(m);
|
|
|
|
m_userName = QString::fromUtf8(userName, strlen(userName));
|
|
m_userPassword = QString::fromUtf8(userPassword, strlen(userPassword));
|
|
|
|
git_repository *repo = LocalGitRepository::GetInstance(localRepoName)->GetGitRepository();
|
|
|
|
// Make sure we're not about to checkout while something else is going on
|
|
if ((state = (git_repository_state_t)git_repository_state(repo)) != GIT_REPOSITORY_STATE_NONE) {
|
|
qCritical() << HEADER;
|
|
qCritical() << "ERROR repository" << cwd.localRepoDir().absolutePath()
|
|
<< "is in unexpected state" << (int)state;
|
|
} else {
|
|
if (check(git_remote_lookup(&remote, repo, "origin" ), "Unable to lookup remote", NULL)) {
|
|
if (check(git_remote_init_callbacks(&callbacks, GIT_REMOTE_CALLBACKS_VERSION),
|
|
"Error initializing remote callbacks", NULL)) {
|
|
callbacks.credentials = cred_acquire_cb;
|
|
if (check(git_push_options_init(&options, GIT_PUSH_OPTIONS_VERSION ),
|
|
"Error initializing push", NULL)) {
|
|
options.callbacks = callbacks;
|
|
if (check(git_remote_push(remote, &refspecs, &options), "Error pushing", NULL)) {
|
|
qCritical() << HEADER;
|
|
qCritical() << QString("pushed %s (branch: %s").arg(localRepoName).arg(branchName);
|
|
error = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (remote) {
|
|
git_remote_free(remote);
|
|
}
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
int GitLibrary::PullRepository(char const *localRepoName, char const *remoteRepoName) {
|
|
// "git pull" is a basically a "git fetch" followed by a "git merge"
|
|
git_repository *repo = nullptr;
|
|
int error = 0;
|
|
|
|
if ((error = git_repository_open(&repo, localRepoName)) != 0) {
|
|
// error
|
|
}
|
|
// remoteRepoName typically "origin"
|
|
if ((error = fetch(repo, remoteRepoName)) != 0) {
|
|
// error
|
|
}
|
|
if ((error = merge(repo)) != 0) {
|
|
// error
|
|
}
|
|
|
|
if (repo) {
|
|
git_repository_free(repo);
|
|
}
|
|
|
|
return error;
|
|
}
|