mirror of
https://git.proxmox.com/git/libgit2
synced 2025-05-21 18:03:54 +00:00

Instead of calling `git_index_add` in a loop, use the new `git_index_fill` internal API to fill the index with the initial staged entries. The new `fill` helper assumes that all the entries will be unique and valid, so it can append them at the end of the entries vector and only sort it once at the end. It performs no validation checks. This prevents the quadratic behavior caused by having to sort the entries list once after every insertion.
3076 lines
79 KiB
C
3076 lines
79 KiB
C
/*
|
|
* Copyright (C) the libgit2 contributors. All rights reserved.
|
|
*
|
|
* This file is part of libgit2, distributed under the GNU GPL v2 with
|
|
* a Linking Exception. For full terms see the included COPYING file.
|
|
*/
|
|
|
|
#include "common.h"
|
|
#include "posix.h"
|
|
#include "buffer.h"
|
|
#include "repository.h"
|
|
#include "revwalk.h"
|
|
#include "commit_list.h"
|
|
#include "merge.h"
|
|
#include "path.h"
|
|
#include "refs.h"
|
|
#include "object.h"
|
|
#include "iterator.h"
|
|
#include "refs.h"
|
|
#include "diff.h"
|
|
#include "checkout.h"
|
|
#include "tree.h"
|
|
#include "blob.h"
|
|
#include "oid.h"
|
|
#include "index.h"
|
|
#include "filebuf.h"
|
|
#include "config.h"
|
|
#include "oidarray.h"
|
|
#include "annotated_commit.h"
|
|
#include "commit.h"
|
|
#include "oidarray.h"
|
|
|
|
#include "git2/types.h"
|
|
#include "git2/repository.h"
|
|
#include "git2/object.h"
|
|
#include "git2/commit.h"
|
|
#include "git2/merge.h"
|
|
#include "git2/refs.h"
|
|
#include "git2/reset.h"
|
|
#include "git2/checkout.h"
|
|
#include "git2/signature.h"
|
|
#include "git2/config.h"
|
|
#include "git2/tree.h"
|
|
#include "git2/oidarray.h"
|
|
#include "git2/annotated_commit.h"
|
|
#include "git2/sys/index.h"
|
|
#include "git2/sys/hashsig.h"
|
|
|
|
#define GIT_MERGE_INDEX_ENTRY_EXISTS(X) ((X).mode != 0)
|
|
#define GIT_MERGE_INDEX_ENTRY_ISFILE(X) S_ISREG((X).mode)
|
|
|
|
|
|
/** Internal merge flags. */
|
|
enum {
|
|
/** The merge is for a virtual base in a recursive merge. */
|
|
GIT_MERGE__VIRTUAL_BASE = (1 << 31),
|
|
};
|
|
|
|
enum {
|
|
/** Accept the conflict file, staging it as the merge result. */
|
|
GIT_MERGE_FILE_FAVOR__CONFLICTED = 4,
|
|
};
|
|
|
|
|
|
typedef enum {
|
|
TREE_IDX_ANCESTOR = 0,
|
|
TREE_IDX_OURS = 1,
|
|
TREE_IDX_THEIRS = 2
|
|
} merge_tree_index_t;
|
|
|
|
/* Tracks D/F conflicts */
|
|
struct merge_diff_df_data {
|
|
const char *df_path;
|
|
const char *prev_path;
|
|
git_merge_diff *prev_conflict;
|
|
};
|
|
|
|
/* Merge base computation */
|
|
|
|
int merge_bases_many(git_commit_list **out, git_revwalk **walk_out, git_repository *repo, size_t length, const git_oid input_array[])
|
|
{
|
|
git_revwalk *walk = NULL;
|
|
git_vector list;
|
|
git_commit_list *result = NULL;
|
|
git_commit_list_node *commit;
|
|
int error = -1;
|
|
unsigned int i;
|
|
|
|
if (length < 2) {
|
|
giterr_set(GITERR_INVALID, "At least two commits are required to find an ancestor. Provided 'length' was %" PRIuZ ".", length);
|
|
return -1;
|
|
}
|
|
|
|
if (git_vector_init(&list, length - 1, NULL) < 0)
|
|
return -1;
|
|
|
|
if (git_revwalk_new(&walk, repo) < 0)
|
|
goto on_error;
|
|
|
|
for (i = 1; i < length; i++) {
|
|
commit = git_revwalk__commit_lookup(walk, &input_array[i]);
|
|
if (commit == NULL)
|
|
goto on_error;
|
|
|
|
git_vector_insert(&list, commit);
|
|
}
|
|
|
|
commit = git_revwalk__commit_lookup(walk, &input_array[0]);
|
|
if (commit == NULL)
|
|
goto on_error;
|
|
|
|
if (git_merge__bases_many(&result, walk, commit, &list) < 0)
|
|
goto on_error;
|
|
|
|
if (!result) {
|
|
giterr_set(GITERR_MERGE, "No merge base found");
|
|
error = GIT_ENOTFOUND;
|
|
goto on_error;
|
|
}
|
|
|
|
*out = result;
|
|
*walk_out = walk;
|
|
|
|
git_vector_free(&list);
|
|
return 0;
|
|
|
|
on_error:
|
|
git_vector_free(&list);
|
|
git_revwalk_free(walk);
|
|
return error;
|
|
}
|
|
|
|
int git_merge_base_many(git_oid *out, git_repository *repo, size_t length, const git_oid input_array[])
|
|
{
|
|
git_revwalk *walk;
|
|
git_commit_list *result = NULL;
|
|
int error = 0;
|
|
|
|
assert(out && repo && input_array);
|
|
|
|
if ((error = merge_bases_many(&result, &walk, repo, length, input_array)) < 0)
|
|
return error;
|
|
|
|
git_oid_cpy(out, &result->item->oid);
|
|
|
|
git_commit_list_free(&result);
|
|
git_revwalk_free(walk);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int git_merge_bases_many(git_oidarray *out, git_repository *repo, size_t length, const git_oid input_array[])
|
|
{
|
|
git_revwalk *walk;
|
|
git_commit_list *list, *result = NULL;
|
|
int error = 0;
|
|
git_array_oid_t array;
|
|
|
|
assert(out && repo && input_array);
|
|
|
|
if ((error = merge_bases_many(&result, &walk, repo, length, input_array)) < 0)
|
|
return error;
|
|
|
|
git_array_init(array);
|
|
|
|
list = result;
|
|
while (list) {
|
|
git_oid *id = git_array_alloc(array);
|
|
if (id == NULL) {
|
|
error = -1;
|
|
goto cleanup;
|
|
}
|
|
|
|
git_oid_cpy(id, &list->item->oid);
|
|
list = list->next;
|
|
}
|
|
|
|
git_oidarray__from_array(out, &array);
|
|
|
|
cleanup:
|
|
git_commit_list_free(&result);
|
|
git_revwalk_free(walk);
|
|
|
|
return error;
|
|
}
|
|
|
|
int git_merge_base_octopus(git_oid *out, git_repository *repo, size_t length, const git_oid input_array[])
|
|
{
|
|
git_oid result;
|
|
unsigned int i;
|
|
int error = -1;
|
|
|
|
assert(out && repo && input_array);
|
|
|
|
if (length < 2) {
|
|
giterr_set(GITERR_INVALID, "At least two commits are required to find an ancestor. Provided 'length' was %" PRIuZ ".", length);
|
|
return -1;
|
|
}
|
|
|
|
result = input_array[0];
|
|
for (i = 1; i < length; i++) {
|
|
error = git_merge_base(&result, repo, &result, &input_array[i]);
|
|
if (error < 0)
|
|
return error;
|
|
}
|
|
|
|
*out = result;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int merge_bases(git_commit_list **out, git_revwalk **walk_out, git_repository *repo, const git_oid *one, const git_oid *two)
|
|
{
|
|
git_revwalk *walk;
|
|
git_vector list;
|
|
git_commit_list *result = NULL;
|
|
git_commit_list_node *commit;
|
|
void *contents[1];
|
|
|
|
if (git_revwalk_new(&walk, repo) < 0)
|
|
return -1;
|
|
|
|
commit = git_revwalk__commit_lookup(walk, two);
|
|
if (commit == NULL)
|
|
goto on_error;
|
|
|
|
/* This is just one value, so we can do it on the stack */
|
|
memset(&list, 0x0, sizeof(git_vector));
|
|
contents[0] = commit;
|
|
list.length = 1;
|
|
list.contents = contents;
|
|
|
|
commit = git_revwalk__commit_lookup(walk, one);
|
|
if (commit == NULL)
|
|
goto on_error;
|
|
|
|
if (git_merge__bases_many(&result, walk, commit, &list) < 0)
|
|
goto on_error;
|
|
|
|
if (!result) {
|
|
git_revwalk_free(walk);
|
|
giterr_set(GITERR_MERGE, "No merge base found");
|
|
return GIT_ENOTFOUND;
|
|
}
|
|
|
|
*out = result;
|
|
*walk_out = walk;
|
|
|
|
return 0;
|
|
|
|
on_error:
|
|
git_revwalk_free(walk);
|
|
return -1;
|
|
|
|
}
|
|
|
|
int git_merge_base(git_oid *out, git_repository *repo, const git_oid *one, const git_oid *two)
|
|
{
|
|
int error;
|
|
git_revwalk *walk;
|
|
git_commit_list *result;
|
|
|
|
if ((error = merge_bases(&result, &walk, repo, one, two)) < 0)
|
|
return error;
|
|
|
|
git_oid_cpy(out, &result->item->oid);
|
|
git_commit_list_free(&result);
|
|
git_revwalk_free(walk);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int git_merge_bases(git_oidarray *out, git_repository *repo, const git_oid *one, const git_oid *two)
|
|
{
|
|
int error;
|
|
git_revwalk *walk;
|
|
git_commit_list *result, *list;
|
|
git_array_oid_t array;
|
|
|
|
git_array_init(array);
|
|
|
|
if ((error = merge_bases(&result, &walk, repo, one, two)) < 0)
|
|
return error;
|
|
|
|
list = result;
|
|
while (list) {
|
|
git_oid *id = git_array_alloc(array);
|
|
if (id == NULL)
|
|
goto on_error;
|
|
|
|
git_oid_cpy(id, &list->item->oid);
|
|
list = list->next;
|
|
}
|
|
|
|
git_oidarray__from_array(out, &array);
|
|
git_commit_list_free(&result);
|
|
git_revwalk_free(walk);
|
|
|
|
return 0;
|
|
|
|
on_error:
|
|
git_commit_list_free(&result);
|
|
git_revwalk_free(walk);
|
|
return -1;
|
|
}
|
|
|
|
static int interesting(git_pqueue *list)
|
|
{
|
|
size_t i;
|
|
|
|
for (i = 0; i < git_pqueue_size(list); i++) {
|
|
git_commit_list_node *commit = git_pqueue_get(list, i);
|
|
if ((commit->flags & STALE) == 0)
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void clear_commit_marks_1(git_commit_list **plist,
|
|
git_commit_list_node *commit, unsigned int mark)
|
|
{
|
|
while (commit) {
|
|
unsigned int i;
|
|
|
|
if (!(mark & commit->flags))
|
|
return;
|
|
|
|
commit->flags &= ~mark;
|
|
|
|
for (i = 1; i < commit->out_degree; i++) {
|
|
git_commit_list_node *p = commit->parents[i];
|
|
git_commit_list_insert(p, plist);
|
|
}
|
|
|
|
commit = commit->out_degree ? commit->parents[0] : NULL;
|
|
}
|
|
}
|
|
|
|
static void clear_commit_marks_many(git_vector *commits, unsigned int mark)
|
|
{
|
|
git_commit_list *list = NULL;
|
|
git_commit_list_node *c;
|
|
unsigned int i;
|
|
|
|
git_vector_foreach(commits, i, c) {
|
|
git_commit_list_insert(c, &list);
|
|
}
|
|
|
|
while (list)
|
|
clear_commit_marks_1(&list, git_commit_list_pop(&list), mark);
|
|
}
|
|
|
|
static void clear_commit_marks(git_commit_list_node *commit, unsigned int mark)
|
|
{
|
|
git_commit_list *list = NULL;
|
|
git_commit_list_insert(commit, &list);
|
|
while (list)
|
|
clear_commit_marks_1(&list, git_commit_list_pop(&list), mark);
|
|
}
|
|
|
|
static int paint_down_to_common(
|
|
git_commit_list **out, git_revwalk *walk, git_commit_list_node *one, git_vector *twos)
|
|
{
|
|
git_pqueue list;
|
|
git_commit_list *result = NULL;
|
|
git_commit_list_node *two;
|
|
|
|
int error;
|
|
unsigned int i;
|
|
|
|
if (git_pqueue_init(&list, 0, twos->length * 2, git_commit_list_time_cmp) < 0)
|
|
return -1;
|
|
|
|
one->flags |= PARENT1;
|
|
if (git_pqueue_insert(&list, one) < 0)
|
|
return -1;
|
|
|
|
git_vector_foreach(twos, i, two) {
|
|
if (git_commit_list_parse(walk, two) < 0)
|
|
return -1;
|
|
|
|
two->flags |= PARENT2;
|
|
|
|
if (git_pqueue_insert(&list, two) < 0)
|
|
return -1;
|
|
}
|
|
|
|
/* as long as there are non-STALE commits */
|
|
while (interesting(&list)) {
|
|
git_commit_list_node *commit = git_pqueue_pop(&list);
|
|
int flags;
|
|
|
|
if (commit == NULL)
|
|
break;
|
|
|
|
flags = commit->flags & (PARENT1 | PARENT2 | STALE);
|
|
if (flags == (PARENT1 | PARENT2)) {
|
|
if (!(commit->flags & RESULT)) {
|
|
commit->flags |= RESULT;
|
|
if (git_commit_list_insert(commit, &result) == NULL)
|
|
return -1;
|
|
}
|
|
/* we mark the parents of a merge stale */
|
|
flags |= STALE;
|
|
}
|
|
|
|
for (i = 0; i < commit->out_degree; i++) {
|
|
git_commit_list_node *p = commit->parents[i];
|
|
if ((p->flags & flags) == flags)
|
|
continue;
|
|
|
|
if ((error = git_commit_list_parse(walk, p)) < 0)
|
|
return error;
|
|
|
|
p->flags |= flags;
|
|
if (git_pqueue_insert(&list, p) < 0)
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
git_pqueue_free(&list);
|
|
*out = result;
|
|
return 0;
|
|
}
|
|
|
|
static int remove_redundant(git_revwalk *walk, git_vector *commits)
|
|
{
|
|
git_vector work = GIT_VECTOR_INIT;
|
|
unsigned char *redundant;
|
|
unsigned int *filled_index;
|
|
unsigned int i, j;
|
|
int error = 0;
|
|
|
|
redundant = git__calloc(commits->length, 1);
|
|
GITERR_CHECK_ALLOC(redundant);
|
|
filled_index = git__calloc((commits->length - 1), sizeof(unsigned int));
|
|
GITERR_CHECK_ALLOC(filled_index);
|
|
|
|
for (i = 0; i < commits->length; ++i) {
|
|
if ((error = git_commit_list_parse(walk, commits->contents[i])) < 0)
|
|
goto done;
|
|
}
|
|
|
|
for (i = 0; i < commits->length; ++i) {
|
|
git_commit_list *common = NULL;
|
|
git_commit_list_node *commit = commits->contents[i];
|
|
|
|
if (redundant[i])
|
|
continue;
|
|
|
|
git_vector_clear(&work);
|
|
|
|
for (j = 0; j < commits->length; j++) {
|
|
if (i == j || redundant[j])
|
|
continue;
|
|
|
|
filled_index[work.length] = j;
|
|
if ((error = git_vector_insert(&work, commits->contents[j])) < 0)
|
|
goto done;
|
|
}
|
|
|
|
error = paint_down_to_common(&common, walk, commit, &work);
|
|
if (error < 0)
|
|
goto done;
|
|
|
|
if (commit->flags & PARENT2)
|
|
redundant[i] = 1;
|
|
|
|
for (j = 0; j < work.length; j++) {
|
|
git_commit_list_node *w = work.contents[j];
|
|
if (w->flags & PARENT1)
|
|
redundant[filled_index[j]] = 1;
|
|
}
|
|
|
|
clear_commit_marks(commit, ALL_FLAGS);
|
|
clear_commit_marks_many(&work, ALL_FLAGS);
|
|
|
|
git_commit_list_free(&common);
|
|
}
|
|
|
|
for (i = 0; i < commits->length; ++i) {
|
|
if (redundant[i])
|
|
commits->contents[i] = NULL;
|
|
}
|
|
|
|
done:
|
|
git__free(redundant);
|
|
git__free(filled_index);
|
|
git_vector_free(&work);
|
|
return error;
|
|
}
|
|
|
|
int git_merge__bases_many(git_commit_list **out, git_revwalk *walk, git_commit_list_node *one, git_vector *twos)
|
|
{
|
|
int error;
|
|
unsigned int i;
|
|
git_commit_list_node *two;
|
|
git_commit_list *result = NULL, *tmp = NULL;
|
|
|
|
/* If there's only the one commit, there can be no merge bases */
|
|
if (twos->length == 0) {
|
|
*out = NULL;
|
|
return 0;
|
|
}
|
|
|
|
/* if the commit is repeated, we have a our merge base already */
|
|
git_vector_foreach(twos, i, two) {
|
|
if (one == two)
|
|
return git_commit_list_insert(one, out) ? 0 : -1;
|
|
}
|
|
|
|
if (git_commit_list_parse(walk, one) < 0)
|
|
return -1;
|
|
|
|
error = paint_down_to_common(&result, walk, one, twos);
|
|
if (error < 0)
|
|
return error;
|
|
|
|
/* filter out any stale commits in the results */
|
|
tmp = result;
|
|
result = NULL;
|
|
|
|
while (tmp) {
|
|
git_commit_list_node *c = git_commit_list_pop(&tmp);
|
|
if (!(c->flags & STALE))
|
|
if (git_commit_list_insert_by_date(c, &result) == NULL)
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* more than one merge base -- see if there are redundant merge
|
|
* bases and remove them
|
|
*/
|
|
if (result && result->next) {
|
|
git_vector redundant = GIT_VECTOR_INIT;
|
|
|
|
while (result)
|
|
git_vector_insert(&redundant, git_commit_list_pop(&result));
|
|
|
|
clear_commit_marks(one, ALL_FLAGS);
|
|
clear_commit_marks_many(twos, ALL_FLAGS);
|
|
|
|
if ((error = remove_redundant(walk, &redundant)) < 0) {
|
|
git_vector_free(&redundant);
|
|
return error;
|
|
}
|
|
|
|
git_vector_foreach(&redundant, i, two) {
|
|
if (two != NULL)
|
|
git_commit_list_insert_by_date(two, &result);
|
|
}
|
|
|
|
git_vector_free(&redundant);
|
|
}
|
|
|
|
*out = result;
|
|
return 0;
|
|
}
|
|
|
|
int git_repository_mergehead_foreach(
|
|
git_repository *repo,
|
|
git_repository_mergehead_foreach_cb cb,
|
|
void *payload)
|
|
{
|
|
git_buf merge_head_path = GIT_BUF_INIT, merge_head_file = GIT_BUF_INIT;
|
|
char *buffer, *line;
|
|
size_t line_num = 1;
|
|
git_oid oid;
|
|
int error = 0;
|
|
|
|
assert(repo && cb);
|
|
|
|
if ((error = git_buf_joinpath(&merge_head_path, repo->path_repository,
|
|
GIT_MERGE_HEAD_FILE)) < 0)
|
|
return error;
|
|
|
|
if ((error = git_futils_readbuffer(&merge_head_file,
|
|
git_buf_cstr(&merge_head_path))) < 0)
|
|
goto cleanup;
|
|
|
|
buffer = merge_head_file.ptr;
|
|
|
|
while ((line = git__strsep(&buffer, "\n")) != NULL) {
|
|
if (strlen(line) != GIT_OID_HEXSZ) {
|
|
giterr_set(GITERR_INVALID, "Unable to parse OID - invalid length");
|
|
error = -1;
|
|
goto cleanup;
|
|
}
|
|
|
|
if ((error = git_oid_fromstr(&oid, line)) < 0)
|
|
goto cleanup;
|
|
|
|
if ((error = cb(&oid, payload)) != 0) {
|
|
giterr_set_after_callback(error);
|
|
goto cleanup;
|
|
}
|
|
|
|
++line_num;
|
|
}
|
|
|
|
if (*buffer) {
|
|
giterr_set(GITERR_MERGE, "No EOL at line %d", line_num);
|
|
error = -1;
|
|
goto cleanup;
|
|
}
|
|
|
|
cleanup:
|
|
git_buf_free(&merge_head_path);
|
|
git_buf_free(&merge_head_file);
|
|
|
|
return error;
|
|
}
|
|
|
|
GIT_INLINE(int) index_entry_cmp(const git_index_entry *a, const git_index_entry *b)
|
|
{
|
|
int value = 0;
|
|
|
|
if (a->path == NULL)
|
|
return (b->path == NULL) ? 0 : 1;
|
|
|
|
if ((value = a->mode - b->mode) == 0 &&
|
|
(value = git_oid__cmp(&a->id, &b->id)) == 0)
|
|
value = strcmp(a->path, b->path);
|
|
|
|
return value;
|
|
}
|
|
|
|
/* Conflict resolution */
|
|
|
|
static int merge_conflict_resolve_trivial(
|
|
int *resolved,
|
|
git_merge_diff_list *diff_list,
|
|
const git_merge_diff *conflict)
|
|
{
|
|
int ours_empty, theirs_empty;
|
|
int ours_changed, theirs_changed, ours_theirs_differ;
|
|
git_index_entry const *result = NULL;
|
|
int error = 0;
|
|
|
|
assert(resolved && diff_list && conflict);
|
|
|
|
*resolved = 0;
|
|
|
|
if (conflict->type == GIT_MERGE_DIFF_DIRECTORY_FILE ||
|
|
conflict->type == GIT_MERGE_DIFF_RENAMED_ADDED)
|
|
return 0;
|
|
|
|
if (conflict->our_status == GIT_DELTA_RENAMED ||
|
|
conflict->their_status == GIT_DELTA_RENAMED)
|
|
return 0;
|
|
|
|
ours_empty = !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry);
|
|
theirs_empty = !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry);
|
|
|
|
ours_changed = (conflict->our_status != GIT_DELTA_UNMODIFIED);
|
|
theirs_changed = (conflict->their_status != GIT_DELTA_UNMODIFIED);
|
|
ours_theirs_differ = ours_changed && theirs_changed &&
|
|
index_entry_cmp(&conflict->our_entry, &conflict->their_entry);
|
|
|
|
/*
|
|
* Note: with only one ancestor, some cases are not distinct:
|
|
*
|
|
* 16: ancest:anc1/anc2, head:anc1, remote:anc2 = result:no merge
|
|
* 3: ancest:(empty)^, head:head, remote:(empty) = result:no merge
|
|
* 2: ancest:(empty)^, head:(empty), remote:remote = result:no merge
|
|
*
|
|
* Note that the two cases that take D/F conflicts into account
|
|
* specifically do not need to be explicitly tested, as D/F conflicts
|
|
* would fail the *empty* test:
|
|
*
|
|
* 3ALT: ancest:(empty)+, head:head, remote:*empty* = result:head
|
|
* 2ALT: ancest:(empty)+, head:*empty*, remote:remote = result:remote
|
|
*
|
|
* Note that many of these cases need not be explicitly tested, as
|
|
* they simply degrade to "all different" cases (eg, 11):
|
|
*
|
|
* 4: ancest:(empty)^, head:head, remote:remote = result:no merge
|
|
* 7: ancest:ancest+, head:(empty), remote:remote = result:no merge
|
|
* 9: ancest:ancest+, head:head, remote:(empty) = result:no merge
|
|
* 11: ancest:ancest+, head:head, remote:remote = result:no merge
|
|
*/
|
|
|
|
/* 5ALT: ancest:*, head:head, remote:head = result:head */
|
|
if (ours_changed && !ours_empty && !ours_theirs_differ)
|
|
result = &conflict->our_entry;
|
|
/* 6: ancest:ancest+, head:(empty), remote:(empty) = result:no merge */
|
|
else if (ours_changed && ours_empty && theirs_empty)
|
|
*resolved = 0;
|
|
/* 8: ancest:ancest^, head:(empty), remote:ancest = result:no merge */
|
|
else if (ours_empty && !theirs_changed)
|
|
*resolved = 0;
|
|
/* 10: ancest:ancest^, head:ancest, remote:(empty) = result:no merge */
|
|
else if (!ours_changed && theirs_empty)
|
|
*resolved = 0;
|
|
/* 13: ancest:ancest+, head:head, remote:ancest = result:head */
|
|
else if (ours_changed && !theirs_changed)
|
|
result = &conflict->our_entry;
|
|
/* 14: ancest:ancest+, head:ancest, remote:remote = result:remote */
|
|
else if (!ours_changed && theirs_changed)
|
|
result = &conflict->their_entry;
|
|
else
|
|
*resolved = 0;
|
|
|
|
if (result != NULL &&
|
|
GIT_MERGE_INDEX_ENTRY_EXISTS(*result) &&
|
|
(error = git_vector_insert(&diff_list->staged, (void *)result)) >= 0)
|
|
*resolved = 1;
|
|
|
|
/* Note: trivial resolution does not update the REUC. */
|
|
|
|
return error;
|
|
}
|
|
|
|
static int merge_conflict_resolve_one_removed(
|
|
int *resolved,
|
|
git_merge_diff_list *diff_list,
|
|
const git_merge_diff *conflict)
|
|
{
|
|
int ours_empty, theirs_empty;
|
|
int ours_changed, theirs_changed;
|
|
int error = 0;
|
|
|
|
assert(resolved && diff_list && conflict);
|
|
|
|
*resolved = 0;
|
|
|
|
if (conflict->type == GIT_MERGE_DIFF_DIRECTORY_FILE ||
|
|
conflict->type == GIT_MERGE_DIFF_RENAMED_ADDED)
|
|
return 0;
|
|
|
|
ours_empty = !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry);
|
|
theirs_empty = !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry);
|
|
|
|
ours_changed = (conflict->our_status != GIT_DELTA_UNMODIFIED);
|
|
theirs_changed = (conflict->their_status != GIT_DELTA_UNMODIFIED);
|
|
|
|
/* Removed in both */
|
|
if (ours_changed && ours_empty && theirs_empty)
|
|
*resolved = 1;
|
|
/* Removed in ours */
|
|
else if (ours_empty && !theirs_changed)
|
|
*resolved = 1;
|
|
/* Removed in theirs */
|
|
else if (!ours_changed && theirs_empty)
|
|
*resolved = 1;
|
|
|
|
if (*resolved)
|
|
git_vector_insert(&diff_list->resolved, (git_merge_diff *)conflict);
|
|
|
|
return error;
|
|
}
|
|
|
|
static int merge_conflict_resolve_one_renamed(
|
|
int *resolved,
|
|
git_merge_diff_list *diff_list,
|
|
const git_merge_diff *conflict)
|
|
{
|
|
int ours_renamed, theirs_renamed;
|
|
int ours_changed, theirs_changed;
|
|
git_index_entry *merged;
|
|
int error = 0;
|
|
|
|
assert(resolved && diff_list && conflict);
|
|
|
|
*resolved = 0;
|
|
|
|
if (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) ||
|
|
!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry))
|
|
return 0;
|
|
|
|
ours_renamed = (conflict->our_status == GIT_DELTA_RENAMED);
|
|
theirs_renamed = (conflict->their_status == GIT_DELTA_RENAMED);
|
|
|
|
if (!ours_renamed && !theirs_renamed)
|
|
return 0;
|
|
|
|
/* Reject one file in a 2->1 conflict */
|
|
if (conflict->type == GIT_MERGE_DIFF_BOTH_RENAMED_2_TO_1 ||
|
|
conflict->type == GIT_MERGE_DIFF_BOTH_RENAMED_1_TO_2 ||
|
|
conflict->type == GIT_MERGE_DIFF_RENAMED_ADDED)
|
|
return 0;
|
|
|
|
ours_changed = (git_oid__cmp(&conflict->ancestor_entry.id, &conflict->our_entry.id) != 0);
|
|
theirs_changed = (git_oid__cmp(&conflict->ancestor_entry.id, &conflict->their_entry.id) != 0);
|
|
|
|
/* if both are modified (and not to a common target) require a merge */
|
|
if (ours_changed && theirs_changed &&
|
|
git_oid__cmp(&conflict->our_entry.id, &conflict->their_entry.id) != 0)
|
|
return 0;
|
|
|
|
if ((merged = git_pool_malloc(&diff_list->pool, sizeof(git_index_entry))) == NULL)
|
|
return -1;
|
|
|
|
if (ours_changed)
|
|
memcpy(merged, &conflict->our_entry, sizeof(git_index_entry));
|
|
else
|
|
memcpy(merged, &conflict->their_entry, sizeof(git_index_entry));
|
|
|
|
if (ours_renamed)
|
|
merged->path = conflict->our_entry.path;
|
|
else
|
|
merged->path = conflict->their_entry.path;
|
|
|
|
*resolved = 1;
|
|
|
|
git_vector_insert(&diff_list->staged, merged);
|
|
git_vector_insert(&diff_list->resolved, (git_merge_diff *)conflict);
|
|
|
|
return error;
|
|
}
|
|
|
|
static int merge_conflict_resolve_automerge(
|
|
int *resolved,
|
|
git_merge_diff_list *diff_list,
|
|
const git_merge_diff *conflict,
|
|
const git_merge_file_options *file_opts)
|
|
{
|
|
const git_index_entry *ancestor = NULL, *ours = NULL, *theirs = NULL;
|
|
git_merge_file_result result = {0};
|
|
git_index_entry *index_entry;
|
|
git_odb *odb = NULL;
|
|
git_oid automerge_oid;
|
|
int error = 0;
|
|
|
|
assert(resolved && diff_list && conflict);
|
|
|
|
*resolved = 0;
|
|
|
|
if (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) ||
|
|
!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry))
|
|
return 0;
|
|
|
|
/* Reject D/F conflicts */
|
|
if (conflict->type == GIT_MERGE_DIFF_DIRECTORY_FILE)
|
|
return 0;
|
|
|
|
/* Reject submodules. */
|
|
if (S_ISGITLINK(conflict->ancestor_entry.mode) ||
|
|
S_ISGITLINK(conflict->our_entry.mode) ||
|
|
S_ISGITLINK(conflict->their_entry.mode))
|
|
return 0;
|
|
|
|
/* Reject link/file conflicts. */
|
|
if ((S_ISLNK(conflict->ancestor_entry.mode) ^ S_ISLNK(conflict->our_entry.mode)) ||
|
|
(S_ISLNK(conflict->ancestor_entry.mode) ^ S_ISLNK(conflict->their_entry.mode)))
|
|
return 0;
|
|
|
|
/* Reject name conflicts */
|
|
if (conflict->type == GIT_MERGE_DIFF_BOTH_RENAMED_2_TO_1 ||
|
|
conflict->type == GIT_MERGE_DIFF_RENAMED_ADDED)
|
|
return 0;
|
|
|
|
if ((conflict->our_status & GIT_DELTA_RENAMED) == GIT_DELTA_RENAMED &&
|
|
(conflict->their_status & GIT_DELTA_RENAMED) == GIT_DELTA_RENAMED &&
|
|
strcmp(conflict->ancestor_entry.path, conflict->their_entry.path) != 0)
|
|
return 0;
|
|
|
|
ancestor = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry) ?
|
|
&conflict->ancestor_entry : NULL;
|
|
ours = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) ?
|
|
&conflict->our_entry : NULL;
|
|
theirs = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry) ?
|
|
&conflict->their_entry : NULL;
|
|
|
|
if ((error = git_repository_odb(&odb, diff_list->repo)) < 0 ||
|
|
(error = git_merge_file_from_index(&result, diff_list->repo, ancestor, ours, theirs, file_opts)) < 0 ||
|
|
(!result.automergeable && !(file_opts->flags & GIT_MERGE_FILE_FAVOR__CONFLICTED)) ||
|
|
(error = git_odb_write(&automerge_oid, odb, result.ptr, result.len, GIT_OBJ_BLOB)) < 0)
|
|
goto done;
|
|
|
|
if ((index_entry = git_pool_mallocz(&diff_list->pool, sizeof(git_index_entry))) == NULL)
|
|
GITERR_CHECK_ALLOC(index_entry);
|
|
|
|
index_entry->path = git_pool_strdup(&diff_list->pool, result.path);
|
|
GITERR_CHECK_ALLOC(index_entry->path);
|
|
|
|
index_entry->file_size = result.len;
|
|
index_entry->mode = result.mode;
|
|
git_oid_cpy(&index_entry->id, &automerge_oid);
|
|
|
|
git_vector_insert(&diff_list->staged, index_entry);
|
|
git_vector_insert(&diff_list->resolved, (git_merge_diff *)conflict);
|
|
|
|
*resolved = 1;
|
|
|
|
done:
|
|
git_merge_file_result_free(&result);
|
|
git_odb_free(odb);
|
|
|
|
return error;
|
|
}
|
|
|
|
static int merge_conflict_resolve(
|
|
int *out,
|
|
git_merge_diff_list *diff_list,
|
|
const git_merge_diff *conflict,
|
|
const git_merge_file_options *file_opts)
|
|
{
|
|
int resolved = 0;
|
|
int error = 0;
|
|
|
|
*out = 0;
|
|
|
|
if ((error = merge_conflict_resolve_trivial(&resolved, diff_list, conflict)) < 0)
|
|
goto done;
|
|
|
|
if (!resolved && (error = merge_conflict_resolve_one_removed(&resolved, diff_list, conflict)) < 0)
|
|
goto done;
|
|
|
|
if (!resolved && (error = merge_conflict_resolve_one_renamed(&resolved, diff_list, conflict)) < 0)
|
|
goto done;
|
|
|
|
if (!resolved && (error = merge_conflict_resolve_automerge(&resolved, diff_list, conflict, file_opts)) < 0)
|
|
goto done;
|
|
|
|
*out = resolved;
|
|
|
|
done:
|
|
return error;
|
|
}
|
|
|
|
/* Rename detection and coalescing */
|
|
|
|
struct merge_diff_similarity {
|
|
unsigned char similarity;
|
|
size_t other_idx;
|
|
};
|
|
|
|
static int index_entry_similarity_exact(
|
|
git_repository *repo,
|
|
git_index_entry *a,
|
|
size_t a_idx,
|
|
git_index_entry *b,
|
|
size_t b_idx,
|
|
void **cache,
|
|
const git_merge_options *opts)
|
|
{
|
|
GIT_UNUSED(repo);
|
|
GIT_UNUSED(a_idx);
|
|
GIT_UNUSED(b_idx);
|
|
GIT_UNUSED(cache);
|
|
GIT_UNUSED(opts);
|
|
|
|
if (git_oid__cmp(&a->id, &b->id) == 0)
|
|
return 100;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int index_entry_similarity_calc(
|
|
void **out,
|
|
git_repository *repo,
|
|
git_index_entry *entry,
|
|
const git_merge_options *opts)
|
|
{
|
|
git_blob *blob;
|
|
git_diff_file diff_file = {{{0}}};
|
|
git_off_t blobsize;
|
|
int error;
|
|
|
|
*out = NULL;
|
|
|
|
if ((error = git_blob_lookup(&blob, repo, &entry->id)) < 0)
|
|
return error;
|
|
|
|
git_oid_cpy(&diff_file.id, &entry->id);
|
|
diff_file.path = entry->path;
|
|
diff_file.size = entry->file_size;
|
|
diff_file.mode = entry->mode;
|
|
diff_file.flags = 0;
|
|
|
|
blobsize = git_blob_rawsize(blob);
|
|
|
|
/* file too big for rename processing */
|
|
if (!git__is_sizet(blobsize))
|
|
return 0;
|
|
|
|
error = opts->metric->buffer_signature(out, &diff_file,
|
|
git_blob_rawcontent(blob), (size_t)blobsize,
|
|
opts->metric->payload);
|
|
|
|
git_blob_free(blob);
|
|
|
|
return error;
|
|
}
|
|
|
|
static int index_entry_similarity_inexact(
|
|
git_repository *repo,
|
|
git_index_entry *a,
|
|
size_t a_idx,
|
|
git_index_entry *b,
|
|
size_t b_idx,
|
|
void **cache,
|
|
const git_merge_options *opts)
|
|
{
|
|
int score = 0;
|
|
int error = 0;
|
|
|
|
if (GIT_MODE_TYPE(a->mode) != GIT_MODE_TYPE(b->mode))
|
|
return 0;
|
|
|
|
/* update signature cache if needed */
|
|
if (!cache[a_idx] && (error = index_entry_similarity_calc(&cache[a_idx], repo, a, opts)) < 0)
|
|
return error;
|
|
if (!cache[b_idx] && (error = index_entry_similarity_calc(&cache[b_idx], repo, b, opts)) < 0)
|
|
return error;
|
|
|
|
/* some metrics may not wish to process this file (too big / too small) */
|
|
if (!cache[a_idx] || !cache[b_idx])
|
|
return 0;
|
|
|
|
/* compare signatures */
|
|
if (opts->metric->similarity(
|
|
&score, cache[a_idx], cache[b_idx], opts->metric->payload) < 0)
|
|
return -1;
|
|
|
|
/* clip score */
|
|
if (score < 0)
|
|
score = 0;
|
|
else if (score > 100)
|
|
score = 100;
|
|
|
|
return score;
|
|
}
|
|
|
|
static int merge_diff_mark_similarity(
|
|
git_repository *repo,
|
|
git_merge_diff_list *diff_list,
|
|
struct merge_diff_similarity *similarity_ours,
|
|
struct merge_diff_similarity *similarity_theirs,
|
|
int (*similarity_fn)(git_repository *, git_index_entry *, size_t, git_index_entry *, size_t, void **, const git_merge_options *),
|
|
void **cache,
|
|
const git_merge_options *opts)
|
|
{
|
|
size_t i, j;
|
|
git_merge_diff *conflict_src, *conflict_tgt;
|
|
int similarity;
|
|
|
|
git_vector_foreach(&diff_list->conflicts, i, conflict_src) {
|
|
/* Items can be the source of a rename iff they have an item in the
|
|
* ancestor slot and lack an item in the ours or theirs slot. */
|
|
if (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_src->ancestor_entry) ||
|
|
(GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_src->our_entry) &&
|
|
GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_src->their_entry)))
|
|
continue;
|
|
|
|
git_vector_foreach(&diff_list->conflicts, j, conflict_tgt) {
|
|
size_t our_idx = diff_list->conflicts.length + j;
|
|
size_t their_idx = (diff_list->conflicts.length * 2) + j;
|
|
|
|
if (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_tgt->ancestor_entry))
|
|
continue;
|
|
|
|
if (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_tgt->our_entry) &&
|
|
!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_src->our_entry)) {
|
|
similarity = similarity_fn(repo, &conflict_src->ancestor_entry, i, &conflict_tgt->our_entry, our_idx, cache, opts);
|
|
|
|
if (similarity == GIT_EBUFS)
|
|
continue;
|
|
else if (similarity < 0)
|
|
return similarity;
|
|
|
|
if (similarity > similarity_ours[i].similarity &&
|
|
similarity > similarity_ours[j].similarity) {
|
|
/* Clear previous best similarity */
|
|
if (similarity_ours[i].similarity > 0)
|
|
similarity_ours[similarity_ours[i].other_idx].similarity = 0;
|
|
|
|
if (similarity_ours[j].similarity > 0)
|
|
similarity_ours[similarity_ours[j].other_idx].similarity = 0;
|
|
|
|
similarity_ours[i].similarity = similarity;
|
|
similarity_ours[i].other_idx = j;
|
|
|
|
similarity_ours[j].similarity = similarity;
|
|
similarity_ours[j].other_idx = i;
|
|
}
|
|
}
|
|
|
|
if (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_tgt->their_entry) &&
|
|
!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict_src->their_entry)) {
|
|
similarity = similarity_fn(repo, &conflict_src->ancestor_entry, i, &conflict_tgt->their_entry, their_idx, cache, opts);
|
|
|
|
if (similarity > similarity_theirs[i].similarity &&
|
|
similarity > similarity_theirs[j].similarity) {
|
|
/* Clear previous best similarity */
|
|
if (similarity_theirs[i].similarity > 0)
|
|
similarity_theirs[similarity_theirs[i].other_idx].similarity = 0;
|
|
|
|
if (similarity_theirs[j].similarity > 0)
|
|
similarity_theirs[similarity_theirs[j].other_idx].similarity = 0;
|
|
|
|
similarity_theirs[i].similarity = similarity;
|
|
similarity_theirs[i].other_idx = j;
|
|
|
|
similarity_theirs[j].similarity = similarity;
|
|
similarity_theirs[j].other_idx = i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Rename conflicts:
|
|
*
|
|
* Ancestor Ours Theirs
|
|
*
|
|
* 0a A A A No rename
|
|
* b A A* A No rename (ours was rewritten)
|
|
* c A A A* No rename (theirs rewritten)
|
|
* 1a A A B[A] Rename or rename/edit
|
|
* b A B[A] A (automergeable)
|
|
* 2 A B[A] B[A] Both renamed (automergeable)
|
|
* 3a A B[A] Rename/delete
|
|
* b A B[A] (same)
|
|
* 4a A B[A] B Rename/add [B~ours B~theirs]
|
|
* b A B B[A] (same)
|
|
* 5 A B[A] C[A] Both renamed ("1 -> 2")
|
|
* 6 A C[A] Both renamed ("2 -> 1")
|
|
* B C[B] [C~ours C~theirs] (automergeable)
|
|
*/
|
|
static void merge_diff_mark_rename_conflict(
|
|
git_merge_diff_list *diff_list,
|
|
struct merge_diff_similarity *similarity_ours,
|
|
bool ours_renamed,
|
|
size_t ours_source_idx,
|
|
struct merge_diff_similarity *similarity_theirs,
|
|
bool theirs_renamed,
|
|
size_t theirs_source_idx,
|
|
git_merge_diff *target,
|
|
const git_merge_options *opts)
|
|
{
|
|
git_merge_diff *ours_source = NULL, *theirs_source = NULL;
|
|
|
|
if (ours_renamed)
|
|
ours_source = diff_list->conflicts.contents[ours_source_idx];
|
|
|
|
if (theirs_renamed)
|
|
theirs_source = diff_list->conflicts.contents[theirs_source_idx];
|
|
|
|
/* Detect 2->1 conflicts */
|
|
if (ours_renamed && theirs_renamed) {
|
|
/* Both renamed to the same target name. */
|
|
if (ours_source_idx == theirs_source_idx)
|
|
ours_source->type = GIT_MERGE_DIFF_BOTH_RENAMED;
|
|
else {
|
|
ours_source->type = GIT_MERGE_DIFF_BOTH_RENAMED_2_TO_1;
|
|
theirs_source->type = GIT_MERGE_DIFF_BOTH_RENAMED_2_TO_1;
|
|
}
|
|
} else if (ours_renamed) {
|
|
/* If our source was also renamed in theirs, this is a 1->2 */
|
|
if (similarity_theirs[ours_source_idx].similarity >= opts->rename_threshold)
|
|
ours_source->type = GIT_MERGE_DIFF_BOTH_RENAMED_1_TO_2;
|
|
|
|
else if (GIT_MERGE_INDEX_ENTRY_EXISTS(target->their_entry)) {
|
|
ours_source->type = GIT_MERGE_DIFF_RENAMED_ADDED;
|
|
target->type = GIT_MERGE_DIFF_RENAMED_ADDED;
|
|
}
|
|
|
|
else if (!GIT_MERGE_INDEX_ENTRY_EXISTS(ours_source->their_entry))
|
|
ours_source->type = GIT_MERGE_DIFF_RENAMED_DELETED;
|
|
|
|
else if (ours_source->type == GIT_MERGE_DIFF_MODIFIED_DELETED)
|
|
ours_source->type = GIT_MERGE_DIFF_RENAMED_MODIFIED;
|
|
} else if (theirs_renamed) {
|
|
/* If their source was also renamed in ours, this is a 1->2 */
|
|
if (similarity_ours[theirs_source_idx].similarity >= opts->rename_threshold)
|
|
theirs_source->type = GIT_MERGE_DIFF_BOTH_RENAMED_1_TO_2;
|
|
|
|
else if (GIT_MERGE_INDEX_ENTRY_EXISTS(target->our_entry)) {
|
|
theirs_source->type = GIT_MERGE_DIFF_RENAMED_ADDED;
|
|
target->type = GIT_MERGE_DIFF_RENAMED_ADDED;
|
|
}
|
|
|
|
else if (!GIT_MERGE_INDEX_ENTRY_EXISTS(theirs_source->our_entry))
|
|
theirs_source->type = GIT_MERGE_DIFF_RENAMED_DELETED;
|
|
|
|
else if (theirs_source->type == GIT_MERGE_DIFF_MODIFIED_DELETED)
|
|
theirs_source->type = GIT_MERGE_DIFF_RENAMED_MODIFIED;
|
|
}
|
|
}
|
|
|
|
GIT_INLINE(void) merge_diff_coalesce_rename(
|
|
git_index_entry *source_entry,
|
|
git_delta_t *source_status,
|
|
git_index_entry *target_entry,
|
|
git_delta_t *target_status)
|
|
{
|
|
/* Coalesce the rename target into the rename source. */
|
|
memcpy(source_entry, target_entry, sizeof(git_index_entry));
|
|
*source_status = GIT_DELTA_RENAMED;
|
|
|
|
memset(target_entry, 0x0, sizeof(git_index_entry));
|
|
*target_status = GIT_DELTA_UNMODIFIED;
|
|
}
|
|
|
|
static void merge_diff_list_coalesce_renames(
|
|
git_merge_diff_list *diff_list,
|
|
struct merge_diff_similarity *similarity_ours,
|
|
struct merge_diff_similarity *similarity_theirs,
|
|
const git_merge_options *opts)
|
|
{
|
|
size_t i;
|
|
bool ours_renamed = 0, theirs_renamed = 0;
|
|
size_t ours_source_idx = 0, theirs_source_idx = 0;
|
|
git_merge_diff *ours_source, *theirs_source, *target;
|
|
|
|
for (i = 0; i < diff_list->conflicts.length; i++) {
|
|
target = diff_list->conflicts.contents[i];
|
|
|
|
ours_renamed = 0;
|
|
theirs_renamed = 0;
|
|
|
|
if (GIT_MERGE_INDEX_ENTRY_EXISTS(target->our_entry) &&
|
|
similarity_ours[i].similarity >= opts->rename_threshold) {
|
|
ours_source_idx = similarity_ours[i].other_idx;
|
|
|
|
ours_source = diff_list->conflicts.contents[ours_source_idx];
|
|
|
|
merge_diff_coalesce_rename(
|
|
&ours_source->our_entry,
|
|
&ours_source->our_status,
|
|
&target->our_entry,
|
|
&target->our_status);
|
|
|
|
similarity_ours[ours_source_idx].similarity = 0;
|
|
similarity_ours[i].similarity = 0;
|
|
|
|
ours_renamed = 1;
|
|
}
|
|
|
|
/* insufficient to determine direction */
|
|
if (GIT_MERGE_INDEX_ENTRY_EXISTS(target->their_entry) &&
|
|
similarity_theirs[i].similarity >= opts->rename_threshold) {
|
|
theirs_source_idx = similarity_theirs[i].other_idx;
|
|
|
|
theirs_source = diff_list->conflicts.contents[theirs_source_idx];
|
|
|
|
merge_diff_coalesce_rename(
|
|
&theirs_source->their_entry,
|
|
&theirs_source->their_status,
|
|
&target->their_entry,
|
|
&target->their_status);
|
|
|
|
similarity_theirs[theirs_source_idx].similarity = 0;
|
|
similarity_theirs[i].similarity = 0;
|
|
|
|
theirs_renamed = 1;
|
|
}
|
|
|
|
merge_diff_mark_rename_conflict(diff_list,
|
|
similarity_ours, ours_renamed, ours_source_idx,
|
|
similarity_theirs, theirs_renamed, theirs_source_idx,
|
|
target, opts);
|
|
}
|
|
}
|
|
|
|
static int merge_diff_empty(const git_vector *conflicts, size_t idx, void *p)
|
|
{
|
|
git_merge_diff *conflict = conflicts->contents[idx];
|
|
|
|
GIT_UNUSED(p);
|
|
|
|
return (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry) &&
|
|
!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) &&
|
|
!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry));
|
|
}
|
|
|
|
static void merge_diff_list_count_candidates(
|
|
git_merge_diff_list *diff_list,
|
|
size_t *src_count,
|
|
size_t *tgt_count)
|
|
{
|
|
git_merge_diff *entry;
|
|
size_t i;
|
|
|
|
*src_count = 0;
|
|
*tgt_count = 0;
|
|
|
|
git_vector_foreach(&diff_list->conflicts, i, entry) {
|
|
if (GIT_MERGE_INDEX_ENTRY_EXISTS(entry->ancestor_entry) &&
|
|
(!GIT_MERGE_INDEX_ENTRY_EXISTS(entry->our_entry) ||
|
|
!GIT_MERGE_INDEX_ENTRY_EXISTS(entry->their_entry)))
|
|
(*src_count)++;
|
|
else if (!GIT_MERGE_INDEX_ENTRY_EXISTS(entry->ancestor_entry))
|
|
(*tgt_count)++;
|
|
}
|
|
}
|
|
|
|
int git_merge_diff_list__find_renames(
|
|
git_repository *repo,
|
|
git_merge_diff_list *diff_list,
|
|
const git_merge_options *opts)
|
|
{
|
|
struct merge_diff_similarity *similarity_ours, *similarity_theirs;
|
|
void **cache = NULL;
|
|
size_t cache_size = 0;
|
|
size_t src_count, tgt_count, i;
|
|
int error = 0;
|
|
|
|
assert(diff_list && opts);
|
|
|
|
if ((opts->flags & GIT_MERGE_FIND_RENAMES) == 0)
|
|
return 0;
|
|
|
|
similarity_ours = git__calloc(diff_list->conflicts.length,
|
|
sizeof(struct merge_diff_similarity));
|
|
GITERR_CHECK_ALLOC(similarity_ours);
|
|
|
|
similarity_theirs = git__calloc(diff_list->conflicts.length,
|
|
sizeof(struct merge_diff_similarity));
|
|
GITERR_CHECK_ALLOC(similarity_theirs);
|
|
|
|
/* Calculate similarity between items that were deleted from the ancestor
|
|
* and added in the other branch.
|
|
*/
|
|
if ((error = merge_diff_mark_similarity(repo, diff_list, similarity_ours,
|
|
similarity_theirs, index_entry_similarity_exact, NULL, opts)) < 0)
|
|
goto done;
|
|
|
|
if (diff_list->conflicts.length <= opts->target_limit) {
|
|
GITERR_CHECK_ALLOC_MULTIPLY(&cache_size, diff_list->conflicts.length, 3);
|
|
cache = git__calloc(cache_size, sizeof(void *));
|
|
GITERR_CHECK_ALLOC(cache);
|
|
|
|
merge_diff_list_count_candidates(diff_list, &src_count, &tgt_count);
|
|
|
|
if (src_count > opts->target_limit || tgt_count > opts->target_limit) {
|
|
/* TODO: report! */
|
|
} else {
|
|
if ((error = merge_diff_mark_similarity(
|
|
repo, diff_list, similarity_ours, similarity_theirs,
|
|
index_entry_similarity_inexact, cache, opts)) < 0)
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
/* For entries that are appropriately similar, merge the new name's entry
|
|
* into the old name.
|
|
*/
|
|
merge_diff_list_coalesce_renames(diff_list, similarity_ours, similarity_theirs, opts);
|
|
|
|
/* And remove any entries that were merged and are now empty. */
|
|
git_vector_remove_matching(&diff_list->conflicts, merge_diff_empty, NULL);
|
|
|
|
done:
|
|
if (cache != NULL) {
|
|
for (i = 0; i < cache_size; ++i) {
|
|
if (cache[i] != NULL)
|
|
opts->metric->free_signature(cache[i], opts->metric->payload);
|
|
}
|
|
|
|
git__free(cache);
|
|
}
|
|
|
|
git__free(similarity_ours);
|
|
git__free(similarity_theirs);
|
|
|
|
return error;
|
|
}
|
|
|
|
/* Directory/file conflict handling */
|
|
|
|
GIT_INLINE(const char *) merge_diff_path(
|
|
const git_merge_diff *conflict)
|
|
{
|
|
if (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry))
|
|
return conflict->ancestor_entry.path;
|
|
else if (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry))
|
|
return conflict->our_entry.path;
|
|
else if (GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry))
|
|
return conflict->their_entry.path;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
GIT_INLINE(bool) merge_diff_any_side_added_or_modified(
|
|
const git_merge_diff *conflict)
|
|
{
|
|
if (conflict->our_status == GIT_DELTA_ADDED ||
|
|
conflict->our_status == GIT_DELTA_MODIFIED ||
|
|
conflict->their_status == GIT_DELTA_ADDED ||
|
|
conflict->their_status == GIT_DELTA_MODIFIED)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
GIT_INLINE(bool) path_is_prefixed(const char *parent, const char *child)
|
|
{
|
|
size_t child_len = strlen(child);
|
|
size_t parent_len = strlen(parent);
|
|
|
|
if (child_len < parent_len ||
|
|
strncmp(parent, child, parent_len) != 0)
|
|
return 0;
|
|
|
|
return (child[parent_len] == '/');
|
|
}
|
|
|
|
GIT_INLINE(int) merge_diff_detect_df_conflict(
|
|
struct merge_diff_df_data *df_data,
|
|
git_merge_diff *conflict)
|
|
{
|
|
const char *cur_path = merge_diff_path(conflict);
|
|
|
|
/* Determine if this is a D/F conflict or the child of one */
|
|
if (df_data->df_path &&
|
|
path_is_prefixed(df_data->df_path, cur_path))
|
|
conflict->type = GIT_MERGE_DIFF_DF_CHILD;
|
|
else if(df_data->df_path)
|
|
df_data->df_path = NULL;
|
|
else if (df_data->prev_path &&
|
|
merge_diff_any_side_added_or_modified(df_data->prev_conflict) &&
|
|
merge_diff_any_side_added_or_modified(conflict) &&
|
|
path_is_prefixed(df_data->prev_path, cur_path)) {
|
|
conflict->type = GIT_MERGE_DIFF_DF_CHILD;
|
|
|
|
df_data->prev_conflict->type = GIT_MERGE_DIFF_DIRECTORY_FILE;
|
|
df_data->df_path = df_data->prev_path;
|
|
}
|
|
|
|
df_data->prev_path = cur_path;
|
|
df_data->prev_conflict = conflict;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Conflict handling */
|
|
|
|
GIT_INLINE(int) merge_diff_detect_type(
|
|
git_merge_diff *conflict)
|
|
{
|
|
if (conflict->our_status == GIT_DELTA_ADDED &&
|
|
conflict->their_status == GIT_DELTA_ADDED)
|
|
conflict->type = GIT_MERGE_DIFF_BOTH_ADDED;
|
|
else if (conflict->our_status == GIT_DELTA_MODIFIED &&
|
|
conflict->their_status == GIT_DELTA_MODIFIED)
|
|
conflict->type = GIT_MERGE_DIFF_BOTH_MODIFIED;
|
|
else if (conflict->our_status == GIT_DELTA_DELETED &&
|
|
conflict->their_status == GIT_DELTA_DELETED)
|
|
conflict->type = GIT_MERGE_DIFF_BOTH_DELETED;
|
|
else if (conflict->our_status == GIT_DELTA_MODIFIED &&
|
|
conflict->their_status == GIT_DELTA_DELETED)
|
|
conflict->type = GIT_MERGE_DIFF_MODIFIED_DELETED;
|
|
else if (conflict->our_status == GIT_DELTA_DELETED &&
|
|
conflict->their_status == GIT_DELTA_MODIFIED)
|
|
conflict->type = GIT_MERGE_DIFF_MODIFIED_DELETED;
|
|
else
|
|
conflict->type = GIT_MERGE_DIFF_NONE;
|
|
|
|
return 0;
|
|
}
|
|
|
|
GIT_INLINE(int) index_entry_dup_pool(
|
|
git_index_entry *out,
|
|
git_pool *pool,
|
|
const git_index_entry *src)
|
|
{
|
|
if (src != NULL) {
|
|
memcpy(out, src, sizeof(git_index_entry));
|
|
if ((out->path = git_pool_strdup(pool, src->path)) == NULL)
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
GIT_INLINE(int) merge_delta_type_from_index_entries(
|
|
const git_index_entry *ancestor,
|
|
const git_index_entry *other)
|
|
{
|
|
if (ancestor == NULL && other == NULL)
|
|
return GIT_DELTA_UNMODIFIED;
|
|
else if (ancestor == NULL && other != NULL)
|
|
return GIT_DELTA_ADDED;
|
|
else if (ancestor != NULL && other == NULL)
|
|
return GIT_DELTA_DELETED;
|
|
else if (S_ISDIR(ancestor->mode) ^ S_ISDIR(other->mode))
|
|
return GIT_DELTA_TYPECHANGE;
|
|
else if(S_ISLNK(ancestor->mode) ^ S_ISLNK(other->mode))
|
|
return GIT_DELTA_TYPECHANGE;
|
|
else if (git_oid__cmp(&ancestor->id, &other->id) ||
|
|
ancestor->mode != other->mode)
|
|
return GIT_DELTA_MODIFIED;
|
|
|
|
return GIT_DELTA_UNMODIFIED;
|
|
}
|
|
|
|
static git_merge_diff *merge_diff_from_index_entries(
|
|
git_merge_diff_list *diff_list,
|
|
const git_index_entry **entries)
|
|
{
|
|
git_merge_diff *conflict;
|
|
git_pool *pool = &diff_list->pool;
|
|
|
|
if ((conflict = git_pool_mallocz(pool, sizeof(git_merge_diff))) == NULL)
|
|
return NULL;
|
|
|
|
if (index_entry_dup_pool(&conflict->ancestor_entry, pool, entries[TREE_IDX_ANCESTOR]) < 0 ||
|
|
index_entry_dup_pool(&conflict->our_entry, pool, entries[TREE_IDX_OURS]) < 0 ||
|
|
index_entry_dup_pool(&conflict->their_entry, pool, entries[TREE_IDX_THEIRS]) < 0)
|
|
return NULL;
|
|
|
|
conflict->our_status = merge_delta_type_from_index_entries(
|
|
entries[TREE_IDX_ANCESTOR], entries[TREE_IDX_OURS]);
|
|
conflict->their_status = merge_delta_type_from_index_entries(
|
|
entries[TREE_IDX_ANCESTOR], entries[TREE_IDX_THEIRS]);
|
|
|
|
return conflict;
|
|
}
|
|
|
|
/* Merge trees */
|
|
|
|
static int merge_diff_list_insert_conflict(
|
|
git_merge_diff_list *diff_list,
|
|
struct merge_diff_df_data *merge_df_data,
|
|
const git_index_entry *tree_items[3])
|
|
{
|
|
git_merge_diff *conflict;
|
|
|
|
if ((conflict = merge_diff_from_index_entries(diff_list, tree_items)) == NULL ||
|
|
merge_diff_detect_type(conflict) < 0 ||
|
|
merge_diff_detect_df_conflict(merge_df_data, conflict) < 0 ||
|
|
git_vector_insert(&diff_list->conflicts, conflict) < 0)
|
|
return -1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int merge_diff_list_insert_unmodified(
|
|
git_merge_diff_list *diff_list,
|
|
const git_index_entry *tree_items[3])
|
|
{
|
|
int error = 0;
|
|
git_index_entry *entry;
|
|
|
|
entry = git_pool_malloc(&diff_list->pool, sizeof(git_index_entry));
|
|
GITERR_CHECK_ALLOC(entry);
|
|
|
|
if ((error = index_entry_dup_pool(entry, &diff_list->pool, tree_items[0])) >= 0)
|
|
error = git_vector_insert(&diff_list->staged, entry);
|
|
|
|
return error;
|
|
}
|
|
|
|
struct merge_diff_find_data {
|
|
git_merge_diff_list *diff_list;
|
|
struct merge_diff_df_data df_data;
|
|
};
|
|
|
|
static int queue_difference(const git_index_entry **entries, void *data)
|
|
{
|
|
struct merge_diff_find_data *find_data = data;
|
|
bool item_modified = false;
|
|
size_t i;
|
|
|
|
if (!entries[0] || !entries[1] || !entries[2]) {
|
|
item_modified = true;
|
|
} else {
|
|
for (i = 1; i < 3; i++) {
|
|
if (index_entry_cmp(entries[0], entries[i]) != 0) {
|
|
item_modified = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return item_modified ?
|
|
merge_diff_list_insert_conflict(
|
|
find_data->diff_list, &find_data->df_data, entries) :
|
|
merge_diff_list_insert_unmodified(find_data->diff_list, entries);
|
|
}
|
|
|
|
int git_merge_diff_list__find_differences(
|
|
git_merge_diff_list *diff_list,
|
|
git_iterator *ancestor_iter,
|
|
git_iterator *our_iter,
|
|
git_iterator *their_iter)
|
|
{
|
|
git_iterator *iterators[3] = { ancestor_iter, our_iter, their_iter };
|
|
struct merge_diff_find_data find_data = { diff_list };
|
|
|
|
return git_iterator_walk(iterators, 3, queue_difference, &find_data);
|
|
}
|
|
|
|
git_merge_diff_list *git_merge_diff_list__alloc(git_repository *repo)
|
|
{
|
|
git_merge_diff_list *diff_list = git__calloc(1, sizeof(git_merge_diff_list));
|
|
|
|
if (diff_list == NULL)
|
|
return NULL;
|
|
|
|
diff_list->repo = repo;
|
|
|
|
git_pool_init(&diff_list->pool, 1);
|
|
|
|
if (git_vector_init(&diff_list->staged, 0, NULL) < 0 ||
|
|
git_vector_init(&diff_list->conflicts, 0, NULL) < 0 ||
|
|
git_vector_init(&diff_list->resolved, 0, NULL) < 0) {
|
|
git_merge_diff_list__free(diff_list);
|
|
return NULL;
|
|
}
|
|
|
|
return diff_list;
|
|
}
|
|
|
|
void git_merge_diff_list__free(git_merge_diff_list *diff_list)
|
|
{
|
|
if (!diff_list)
|
|
return;
|
|
|
|
git_vector_free(&diff_list->staged);
|
|
git_vector_free(&diff_list->conflicts);
|
|
git_vector_free(&diff_list->resolved);
|
|
git_pool_clear(&diff_list->pool);
|
|
git__free(diff_list);
|
|
}
|
|
|
|
static int merge_normalize_opts(
|
|
git_repository *repo,
|
|
git_merge_options *opts,
|
|
const git_merge_options *given)
|
|
{
|
|
git_config *cfg = NULL;
|
|
int error = 0;
|
|
|
|
assert(repo && opts);
|
|
|
|
if ((error = git_repository_config__weakptr(&cfg, repo)) < 0)
|
|
return error;
|
|
|
|
if (given != NULL)
|
|
memcpy(opts, given, sizeof(git_merge_options));
|
|
else {
|
|
git_merge_options init = GIT_MERGE_OPTIONS_INIT;
|
|
memcpy(opts, &init, sizeof(init));
|
|
|
|
opts->flags = GIT_MERGE_FIND_RENAMES;
|
|
opts->rename_threshold = GIT_MERGE_DEFAULT_RENAME_THRESHOLD;
|
|
}
|
|
|
|
if (!opts->target_limit) {
|
|
int limit = git_config__get_int_force(cfg, "merge.renamelimit", 0);
|
|
|
|
if (!limit)
|
|
limit = git_config__get_int_force(cfg, "diff.renamelimit", 0);
|
|
|
|
opts->target_limit = (limit <= 0) ?
|
|
GIT_MERGE_DEFAULT_TARGET_LIMIT : (unsigned int)limit;
|
|
}
|
|
|
|
/* assign the internal metric with whitespace flag as payload */
|
|
if (!opts->metric) {
|
|
opts->metric = git__malloc(sizeof(git_diff_similarity_metric));
|
|
GITERR_CHECK_ALLOC(opts->metric);
|
|
|
|
opts->metric->file_signature = git_diff_find_similar__hashsig_for_file;
|
|
opts->metric->buffer_signature = git_diff_find_similar__hashsig_for_buf;
|
|
opts->metric->free_signature = git_diff_find_similar__hashsig_free;
|
|
opts->metric->similarity = git_diff_find_similar__calc_similarity;
|
|
opts->metric->payload = (void *)GIT_HASHSIG_SMART_WHITESPACE;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int merge_index_insert_reuc(
|
|
git_index *index,
|
|
size_t idx,
|
|
const git_index_entry *entry)
|
|
{
|
|
const git_index_reuc_entry *reuc;
|
|
int mode[3] = { 0, 0, 0 };
|
|
git_oid const *oid[3] = { NULL, NULL, NULL };
|
|
size_t i;
|
|
|
|
if (!GIT_MERGE_INDEX_ENTRY_EXISTS(*entry))
|
|
return 0;
|
|
|
|
if ((reuc = git_index_reuc_get_bypath(index, entry->path)) != NULL) {
|
|
for (i = 0; i < 3; i++) {
|
|
mode[i] = reuc->mode[i];
|
|
oid[i] = &reuc->oid[i];
|
|
}
|
|
}
|
|
|
|
mode[idx] = entry->mode;
|
|
oid[idx] = &entry->id;
|
|
|
|
return git_index_reuc_add(index, entry->path,
|
|
mode[0], oid[0], mode[1], oid[1], mode[2], oid[2]);
|
|
}
|
|
|
|
static int index_update_reuc(git_index *index, git_merge_diff_list *diff_list)
|
|
{
|
|
int error;
|
|
size_t i;
|
|
git_merge_diff *conflict;
|
|
|
|
/* Add each entry in the resolved conflict to the REUC independently, since
|
|
* the paths may differ due to renames. */
|
|
git_vector_foreach(&diff_list->resolved, i, conflict) {
|
|
const git_index_entry *ancestor =
|
|
GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry) ?
|
|
&conflict->ancestor_entry : NULL;
|
|
|
|
const git_index_entry *ours =
|
|
GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) ?
|
|
&conflict->our_entry : NULL;
|
|
|
|
const git_index_entry *theirs =
|
|
GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry) ?
|
|
&conflict->their_entry : NULL;
|
|
|
|
if (ancestor != NULL &&
|
|
(error = merge_index_insert_reuc(index, TREE_IDX_ANCESTOR, ancestor)) < 0)
|
|
return error;
|
|
|
|
if (ours != NULL &&
|
|
(error = merge_index_insert_reuc(index, TREE_IDX_OURS, ours)) < 0)
|
|
return error;
|
|
|
|
if (theirs != NULL &&
|
|
(error = merge_index_insert_reuc(index, TREE_IDX_THEIRS, theirs)) < 0)
|
|
return error;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int index_from_diff_list(git_index **out,
|
|
git_merge_diff_list *diff_list, bool skip_reuc)
|
|
{
|
|
git_index *index;
|
|
size_t i;
|
|
git_merge_diff *conflict;
|
|
int error = 0;
|
|
|
|
*out = NULL;
|
|
|
|
if ((error = git_index_new(&index)) < 0)
|
|
return error;
|
|
|
|
if ((error = git_index__fill(index, &diff_list->staged)) < 0)
|
|
goto on_error;
|
|
|
|
git_vector_foreach(&diff_list->conflicts, i, conflict) {
|
|
const git_index_entry *ancestor =
|
|
GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry) ?
|
|
&conflict->ancestor_entry : NULL;
|
|
|
|
const git_index_entry *ours =
|
|
GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) ?
|
|
&conflict->our_entry : NULL;
|
|
|
|
const git_index_entry *theirs =
|
|
GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry) ?
|
|
&conflict->their_entry : NULL;
|
|
|
|
if ((error = git_index_conflict_add(index, ancestor, ours, theirs)) < 0)
|
|
goto on_error;
|
|
}
|
|
|
|
/* Add each rename entry to the rename portion of the index. */
|
|
git_vector_foreach(&diff_list->conflicts, i, conflict) {
|
|
const char *ancestor_path, *our_path, *their_path;
|
|
|
|
if (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry))
|
|
continue;
|
|
|
|
ancestor_path = conflict->ancestor_entry.path;
|
|
|
|
our_path =
|
|
GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) ?
|
|
conflict->our_entry.path : NULL;
|
|
|
|
their_path =
|
|
GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry) ?
|
|
conflict->their_entry.path : NULL;
|
|
|
|
if ((our_path && strcmp(ancestor_path, our_path) != 0) ||
|
|
(their_path && strcmp(ancestor_path, their_path) != 0)) {
|
|
if ((error = git_index_name_add(index, ancestor_path, our_path, their_path)) < 0)
|
|
goto on_error;
|
|
}
|
|
}
|
|
|
|
if (!skip_reuc) {
|
|
if ((error = index_update_reuc(index, diff_list)) < 0)
|
|
goto on_error;
|
|
}
|
|
|
|
*out = index;
|
|
return 0;
|
|
|
|
on_error:
|
|
git_index_free(index);
|
|
return error;
|
|
}
|
|
|
|
static git_iterator *iterator_given_or_empty(git_iterator **empty, git_iterator *given)
|
|
{
|
|
git_iterator_options opts = GIT_ITERATOR_OPTIONS_INIT;
|
|
|
|
if (given)
|
|
return given;
|
|
|
|
opts.flags = GIT_ITERATOR_DONT_IGNORE_CASE;
|
|
|
|
if (git_iterator_for_nothing(empty, &opts) < 0)
|
|
return NULL;
|
|
|
|
return *empty;
|
|
}
|
|
|
|
int git_merge__iterators(
|
|
git_index **out,
|
|
git_repository *repo,
|
|
git_iterator *ancestor_iter,
|
|
git_iterator *our_iter,
|
|
git_iterator *theirs_iter,
|
|
const git_merge_options *given_opts)
|
|
{
|
|
git_iterator *empty_ancestor = NULL,
|
|
*empty_ours = NULL,
|
|
*empty_theirs = NULL;
|
|
git_merge_diff_list *diff_list;
|
|
git_merge_options opts;
|
|
git_merge_file_options file_opts = GIT_MERGE_FILE_OPTIONS_INIT;
|
|
git_merge_diff *conflict;
|
|
git_vector changes;
|
|
size_t i;
|
|
int error = 0;
|
|
|
|
assert(out && repo);
|
|
|
|
*out = NULL;
|
|
|
|
GITERR_CHECK_VERSION(
|
|
given_opts, GIT_MERGE_OPTIONS_VERSION, "git_merge_options");
|
|
|
|
if ((error = merge_normalize_opts(repo, &opts, given_opts)) < 0)
|
|
return error;
|
|
|
|
file_opts.favor = opts.file_favor;
|
|
file_opts.flags = opts.file_flags;
|
|
|
|
/* use the git-inspired labels when virtual base building */
|
|
if (opts.flags & GIT_MERGE__VIRTUAL_BASE) {
|
|
file_opts.ancestor_label = "merged common ancestors";
|
|
file_opts.our_label = "Temporary merge branch 1";
|
|
file_opts.their_label = "Temporary merge branch 2";
|
|
file_opts.flags |= GIT_MERGE_FILE_FAVOR__CONFLICTED;
|
|
}
|
|
|
|
diff_list = git_merge_diff_list__alloc(repo);
|
|
GITERR_CHECK_ALLOC(diff_list);
|
|
|
|
ancestor_iter = iterator_given_or_empty(&empty_ancestor, ancestor_iter);
|
|
our_iter = iterator_given_or_empty(&empty_ours, our_iter);
|
|
theirs_iter = iterator_given_or_empty(&empty_theirs, theirs_iter);
|
|
|
|
if ((error = git_merge_diff_list__find_differences(
|
|
diff_list, ancestor_iter, our_iter, theirs_iter)) < 0 ||
|
|
(error = git_merge_diff_list__find_renames(repo, diff_list, &opts)) < 0)
|
|
goto done;
|
|
|
|
memcpy(&changes, &diff_list->conflicts, sizeof(git_vector));
|
|
git_vector_clear(&diff_list->conflicts);
|
|
|
|
git_vector_foreach(&changes, i, conflict) {
|
|
int resolved = 0;
|
|
|
|
if ((error = merge_conflict_resolve(
|
|
&resolved, diff_list, conflict, &file_opts)) < 0)
|
|
goto done;
|
|
|
|
if (!resolved) {
|
|
if ((opts.flags & GIT_MERGE_FAIL_ON_CONFLICT)) {
|
|
giterr_set(GITERR_MERGE, "merge conflicts exist");
|
|
error = GIT_EMERGECONFLICT;
|
|
goto done;
|
|
}
|
|
|
|
git_vector_insert(&diff_list->conflicts, conflict);
|
|
}
|
|
}
|
|
|
|
error = index_from_diff_list(out, diff_list,
|
|
(opts.flags & GIT_MERGE_SKIP_REUC));
|
|
|
|
done:
|
|
if (!given_opts || !given_opts->metric)
|
|
git__free(opts.metric);
|
|
|
|
git_merge_diff_list__free(diff_list);
|
|
git_iterator_free(empty_ancestor);
|
|
git_iterator_free(empty_ours);
|
|
git_iterator_free(empty_theirs);
|
|
|
|
return error;
|
|
}
|
|
|
|
int git_merge_trees(
|
|
git_index **out,
|
|
git_repository *repo,
|
|
const git_tree *ancestor_tree,
|
|
const git_tree *our_tree,
|
|
const git_tree *their_tree,
|
|
const git_merge_options *merge_opts)
|
|
{
|
|
git_iterator *ancestor_iter = NULL, *our_iter = NULL, *their_iter = NULL;
|
|
git_iterator_options iter_opts = GIT_ITERATOR_OPTIONS_INIT;
|
|
int error;
|
|
|
|
iter_opts.flags = GIT_ITERATOR_DONT_IGNORE_CASE;
|
|
|
|
if ((error = git_iterator_for_tree(
|
|
&ancestor_iter, (git_tree *)ancestor_tree, &iter_opts)) < 0 ||
|
|
(error = git_iterator_for_tree(
|
|
&our_iter, (git_tree *)our_tree, &iter_opts)) < 0 ||
|
|
(error = git_iterator_for_tree(
|
|
&their_iter, (git_tree *)their_tree, &iter_opts)) < 0)
|
|
goto done;
|
|
|
|
error = git_merge__iterators(
|
|
out, repo, ancestor_iter, our_iter, their_iter, merge_opts);
|
|
|
|
done:
|
|
git_iterator_free(ancestor_iter);
|
|
git_iterator_free(our_iter);
|
|
git_iterator_free(their_iter);
|
|
|
|
return error;
|
|
}
|
|
|
|
static int merge_annotated_commits(
|
|
git_index **index_out,
|
|
git_annotated_commit **base_out,
|
|
git_repository *repo,
|
|
git_annotated_commit *our_commit,
|
|
git_annotated_commit *their_commit,
|
|
size_t recursion_level,
|
|
const git_merge_options *opts);
|
|
|
|
GIT_INLINE(int) insert_head_ids(
|
|
git_array_oid_t *ids,
|
|
const git_annotated_commit *annotated_commit)
|
|
{
|
|
git_oid *id;
|
|
size_t i;
|
|
|
|
if (annotated_commit->type == GIT_ANNOTATED_COMMIT_REAL) {
|
|
id = git_array_alloc(*ids);
|
|
GITERR_CHECK_ALLOC(id);
|
|
|
|
git_oid_cpy(id, git_commit_id(annotated_commit->commit));
|
|
} else {
|
|
for (i = 0; i < annotated_commit->parents.size; i++) {
|
|
id = git_array_alloc(*ids);
|
|
GITERR_CHECK_ALLOC(id);
|
|
|
|
git_oid_cpy(id, &annotated_commit->parents.ptr[i]);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int create_virtual_base(
|
|
git_annotated_commit **out,
|
|
git_repository *repo,
|
|
git_annotated_commit *one,
|
|
git_annotated_commit *two,
|
|
const git_merge_options *opts,
|
|
size_t recursion_level)
|
|
{
|
|
git_annotated_commit *result = NULL;
|
|
git_index *index = NULL;
|
|
git_merge_options virtual_opts = GIT_MERGE_OPTIONS_INIT;
|
|
|
|
result = git__calloc(1, sizeof(git_annotated_commit));
|
|
GITERR_CHECK_ALLOC(result);
|
|
|
|
/* Conflicts in the merge base creation do not propagate to conflicts
|
|
* in the result; the conflicted base will act as the common ancestor.
|
|
*/
|
|
if (opts)
|
|
memcpy(&virtual_opts, opts, sizeof(git_merge_options));
|
|
|
|
virtual_opts.flags &= ~GIT_MERGE_FAIL_ON_CONFLICT;
|
|
virtual_opts.flags |= GIT_MERGE__VIRTUAL_BASE;
|
|
|
|
if ((merge_annotated_commits(&index, NULL, repo, one, two,
|
|
recursion_level + 1, &virtual_opts)) < 0)
|
|
return -1;
|
|
|
|
result->type = GIT_ANNOTATED_COMMIT_VIRTUAL;
|
|
result->index = index;
|
|
|
|
insert_head_ids(&result->parents, one);
|
|
insert_head_ids(&result->parents, two);
|
|
|
|
*out = result;
|
|
return 0;
|
|
}
|
|
|
|
static int compute_base(
|
|
git_annotated_commit **out,
|
|
git_repository *repo,
|
|
const git_annotated_commit *one,
|
|
const git_annotated_commit *two,
|
|
const git_merge_options *given_opts,
|
|
size_t recursion_level)
|
|
{
|
|
git_array_oid_t head_ids = GIT_ARRAY_INIT;
|
|
git_oidarray bases = {0};
|
|
git_annotated_commit *base = NULL, *other = NULL, *new_base = NULL;
|
|
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
|
|
size_t i;
|
|
int error;
|
|
|
|
*out = NULL;
|
|
|
|
if (given_opts)
|
|
memcpy(&opts, given_opts, sizeof(git_merge_options));
|
|
|
|
if ((error = insert_head_ids(&head_ids, one)) < 0 ||
|
|
(error = insert_head_ids(&head_ids, two)) < 0)
|
|
goto done;
|
|
|
|
if ((error = git_merge_bases_many(&bases, repo,
|
|
head_ids.size, head_ids.ptr)) < 0 ||
|
|
(error = git_annotated_commit_lookup(&base, repo, &bases.ids[0])) < 0 ||
|
|
(opts.flags & GIT_MERGE_NO_RECURSIVE))
|
|
goto done;
|
|
|
|
for (i = 1; i < bases.count; i++) {
|
|
recursion_level++;
|
|
|
|
if (opts.recursion_limit && recursion_level > opts.recursion_limit)
|
|
break;
|
|
|
|
if ((error = git_annotated_commit_lookup(&other, repo,
|
|
&bases.ids[i])) < 0 ||
|
|
(error = create_virtual_base(&new_base, repo, base, other, &opts,
|
|
recursion_level)) < 0)
|
|
goto done;
|
|
|
|
git_annotated_commit_free(base);
|
|
git_annotated_commit_free(other);
|
|
|
|
base = new_base;
|
|
new_base = NULL;
|
|
other = NULL;
|
|
}
|
|
|
|
done:
|
|
if (error == 0)
|
|
*out = base;
|
|
else
|
|
git_annotated_commit_free(base);
|
|
|
|
git_annotated_commit_free(other);
|
|
git_annotated_commit_free(new_base);
|
|
git_oidarray_free(&bases);
|
|
git_array_clear(head_ids);
|
|
return error;
|
|
}
|
|
|
|
static int iterator_for_annotated_commit(
|
|
git_iterator **out,
|
|
git_annotated_commit *commit)
|
|
{
|
|
git_iterator_options opts = GIT_ITERATOR_OPTIONS_INIT;
|
|
int error;
|
|
|
|
opts.flags = GIT_ITERATOR_DONT_IGNORE_CASE;
|
|
|
|
if (commit == NULL) {
|
|
error = git_iterator_for_nothing(out, &opts);
|
|
} else if (commit->type == GIT_ANNOTATED_COMMIT_VIRTUAL) {
|
|
error = git_iterator_for_index(out, commit->index, &opts);
|
|
} else {
|
|
if (!commit->tree &&
|
|
(error = git_commit_tree(&commit->tree, commit->commit)) < 0)
|
|
goto done;
|
|
|
|
error = git_iterator_for_tree(out, commit->tree, &opts);
|
|
}
|
|
|
|
done:
|
|
return error;
|
|
}
|
|
|
|
static int merge_annotated_commits(
|
|
git_index **index_out,
|
|
git_annotated_commit **base_out,
|
|
git_repository *repo,
|
|
git_annotated_commit *ours,
|
|
git_annotated_commit *theirs,
|
|
size_t recursion_level,
|
|
const git_merge_options *opts)
|
|
{
|
|
git_annotated_commit *base = NULL;
|
|
git_iterator *base_iter = NULL, *our_iter = NULL, *their_iter = NULL;
|
|
int error;
|
|
|
|
if ((error = compute_base(&base, repo, ours, theirs, opts,
|
|
recursion_level)) < 0) {
|
|
|
|
if (error != GIT_ENOTFOUND)
|
|
goto done;
|
|
|
|
giterr_clear();
|
|
}
|
|
|
|
if ((error = iterator_for_annotated_commit(&base_iter, base)) < 0 ||
|
|
(error = iterator_for_annotated_commit(&our_iter, ours)) < 0 ||
|
|
(error = iterator_for_annotated_commit(&their_iter, theirs)) < 0 ||
|
|
(error = git_merge__iterators(index_out, repo, base_iter, our_iter,
|
|
their_iter, opts)) < 0)
|
|
goto done;
|
|
|
|
if (base_out) {
|
|
*base_out = base;
|
|
base = NULL;
|
|
}
|
|
|
|
done:
|
|
git_annotated_commit_free(base);
|
|
git_iterator_free(base_iter);
|
|
git_iterator_free(our_iter);
|
|
git_iterator_free(their_iter);
|
|
return error;
|
|
}
|
|
|
|
|
|
int git_merge_commits(
|
|
git_index **out,
|
|
git_repository *repo,
|
|
const git_commit *our_commit,
|
|
const git_commit *their_commit,
|
|
const git_merge_options *opts)
|
|
{
|
|
git_annotated_commit *ours = NULL, *theirs = NULL, *base = NULL;
|
|
int error = 0;
|
|
|
|
if ((error = git_annotated_commit_from_commit(&ours, (git_commit *)our_commit)) < 0 ||
|
|
(error = git_annotated_commit_from_commit(&theirs, (git_commit *)their_commit)) < 0)
|
|
goto done;
|
|
|
|
error = merge_annotated_commits(out, &base, repo, ours, theirs, 0, opts);
|
|
|
|
done:
|
|
git_annotated_commit_free(ours);
|
|
git_annotated_commit_free(theirs);
|
|
git_annotated_commit_free(base);
|
|
return error;
|
|
}
|
|
|
|
/* Merge setup / cleanup */
|
|
|
|
static int write_merge_head(
|
|
git_repository *repo,
|
|
const git_annotated_commit *heads[],
|
|
size_t heads_len)
|
|
{
|
|
git_filebuf file = GIT_FILEBUF_INIT;
|
|
git_buf file_path = GIT_BUF_INIT;
|
|
size_t i;
|
|
int error = 0;
|
|
|
|
assert(repo && heads);
|
|
|
|
if ((error = git_buf_joinpath(&file_path, repo->path_repository, GIT_MERGE_HEAD_FILE)) < 0 ||
|
|
(error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_FORCE, GIT_MERGE_FILE_MODE)) < 0)
|
|
goto cleanup;
|
|
|
|
for (i = 0; i < heads_len; i++) {
|
|
if ((error = git_filebuf_printf(&file, "%s\n", heads[i]->id_str)) < 0)
|
|
goto cleanup;
|
|
}
|
|
|
|
error = git_filebuf_commit(&file);
|
|
|
|
cleanup:
|
|
if (error < 0)
|
|
git_filebuf_cleanup(&file);
|
|
|
|
git_buf_free(&file_path);
|
|
|
|
return error;
|
|
}
|
|
|
|
static int write_merge_mode(git_repository *repo)
|
|
{
|
|
git_filebuf file = GIT_FILEBUF_INIT;
|
|
git_buf file_path = GIT_BUF_INIT;
|
|
int error = 0;
|
|
|
|
assert(repo);
|
|
|
|
if ((error = git_buf_joinpath(&file_path, repo->path_repository, GIT_MERGE_MODE_FILE)) < 0 ||
|
|
(error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_FORCE, GIT_MERGE_FILE_MODE)) < 0)
|
|
goto cleanup;
|
|
|
|
if ((error = git_filebuf_write(&file, "no-ff", 5)) < 0)
|
|
goto cleanup;
|
|
|
|
error = git_filebuf_commit(&file);
|
|
|
|
cleanup:
|
|
if (error < 0)
|
|
git_filebuf_cleanup(&file);
|
|
|
|
git_buf_free(&file_path);
|
|
|
|
return error;
|
|
}
|
|
|
|
struct merge_msg_entry {
|
|
const git_annotated_commit *merge_head;
|
|
bool written;
|
|
};
|
|
|
|
static int msg_entry_is_branch(
|
|
const struct merge_msg_entry *entry,
|
|
git_vector *entries)
|
|
{
|
|
GIT_UNUSED(entries);
|
|
|
|
return (entry->written == 0 &&
|
|
entry->merge_head->remote_url == NULL &&
|
|
entry->merge_head->ref_name != NULL &&
|
|
git__strncmp(GIT_REFS_HEADS_DIR, entry->merge_head->ref_name, strlen(GIT_REFS_HEADS_DIR)) == 0);
|
|
}
|
|
|
|
static int msg_entry_is_tracking(
|
|
const struct merge_msg_entry *entry,
|
|
git_vector *entries)
|
|
{
|
|
GIT_UNUSED(entries);
|
|
|
|
return (entry->written == 0 &&
|
|
entry->merge_head->remote_url == NULL &&
|
|
entry->merge_head->ref_name != NULL &&
|
|
git__strncmp(GIT_REFS_REMOTES_DIR, entry->merge_head->ref_name, strlen(GIT_REFS_REMOTES_DIR)) == 0);
|
|
}
|
|
|
|
static int msg_entry_is_tag(
|
|
const struct merge_msg_entry *entry,
|
|
git_vector *entries)
|
|
{
|
|
GIT_UNUSED(entries);
|
|
|
|
return (entry->written == 0 &&
|
|
entry->merge_head->remote_url == NULL &&
|
|
entry->merge_head->ref_name != NULL &&
|
|
git__strncmp(GIT_REFS_TAGS_DIR, entry->merge_head->ref_name, strlen(GIT_REFS_TAGS_DIR)) == 0);
|
|
}
|
|
|
|
static int msg_entry_is_remote(
|
|
const struct merge_msg_entry *entry,
|
|
git_vector *entries)
|
|
{
|
|
if (entry->written == 0 &&
|
|
entry->merge_head->remote_url != NULL &&
|
|
entry->merge_head->ref_name != NULL &&
|
|
git__strncmp(GIT_REFS_HEADS_DIR, entry->merge_head->ref_name, strlen(GIT_REFS_HEADS_DIR)) == 0)
|
|
{
|
|
struct merge_msg_entry *existing;
|
|
|
|
/* Match only branches from the same remote */
|
|
if (entries->length == 0)
|
|
return 1;
|
|
|
|
existing = git_vector_get(entries, 0);
|
|
|
|
return (git__strcmp(existing->merge_head->remote_url,
|
|
entry->merge_head->remote_url) == 0);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int msg_entry_is_oid(
|
|
const struct merge_msg_entry *merge_msg_entry)
|
|
{
|
|
return (merge_msg_entry->written == 0 &&
|
|
merge_msg_entry->merge_head->ref_name == NULL &&
|
|
merge_msg_entry->merge_head->remote_url == NULL);
|
|
}
|
|
|
|
static int merge_msg_entry_written(
|
|
const struct merge_msg_entry *merge_msg_entry)
|
|
{
|
|
return (merge_msg_entry->written == 1);
|
|
}
|
|
|
|
static int merge_msg_entries(
|
|
git_vector *v,
|
|
const struct merge_msg_entry *entries,
|
|
size_t len,
|
|
int (*match)(const struct merge_msg_entry *entry, git_vector *entries))
|
|
{
|
|
size_t i;
|
|
int matches, total = 0;
|
|
|
|
git_vector_clear(v);
|
|
|
|
for (i = 0; i < len; i++) {
|
|
if ((matches = match(&entries[i], v)) < 0)
|
|
return matches;
|
|
else if (!matches)
|
|
continue;
|
|
|
|
git_vector_insert(v, (struct merge_msg_entry *)&entries[i]);
|
|
total++;
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
static int merge_msg_write_entries(
|
|
git_filebuf *file,
|
|
git_vector *entries,
|
|
const char *item_name,
|
|
const char *item_plural_name,
|
|
size_t ref_name_skip,
|
|
const char *source,
|
|
char sep)
|
|
{
|
|
struct merge_msg_entry *entry;
|
|
size_t i;
|
|
int error = 0;
|
|
|
|
if (entries->length == 0)
|
|
return 0;
|
|
|
|
if (sep && (error = git_filebuf_printf(file, "%c ", sep)) < 0)
|
|
goto done;
|
|
|
|
if ((error = git_filebuf_printf(file, "%s ",
|
|
(entries->length == 1) ? item_name : item_plural_name)) < 0)
|
|
goto done;
|
|
|
|
git_vector_foreach(entries, i, entry) {
|
|
if (i > 0 &&
|
|
(error = git_filebuf_printf(file, "%s", (i == entries->length - 1) ? " and " : ", ")) < 0)
|
|
goto done;
|
|
|
|
if ((error = git_filebuf_printf(file, "'%s'", entry->merge_head->ref_name + ref_name_skip)) < 0)
|
|
goto done;
|
|
|
|
entry->written = 1;
|
|
}
|
|
|
|
if (source)
|
|
error = git_filebuf_printf(file, " of %s", source);
|
|
|
|
done:
|
|
return error;
|
|
}
|
|
|
|
static int merge_msg_write_branches(
|
|
git_filebuf *file,
|
|
git_vector *entries,
|
|
char sep)
|
|
{
|
|
return merge_msg_write_entries(file, entries,
|
|
"branch", "branches", strlen(GIT_REFS_HEADS_DIR), NULL, sep);
|
|
}
|
|
|
|
static int merge_msg_write_tracking(
|
|
git_filebuf *file,
|
|
git_vector *entries,
|
|
char sep)
|
|
{
|
|
return merge_msg_write_entries(file, entries,
|
|
"remote-tracking branch", "remote-tracking branches", 0, NULL, sep);
|
|
}
|
|
|
|
static int merge_msg_write_tags(
|
|
git_filebuf *file,
|
|
git_vector *entries,
|
|
char sep)
|
|
{
|
|
return merge_msg_write_entries(file, entries,
|
|
"tag", "tags", strlen(GIT_REFS_TAGS_DIR), NULL, sep);
|
|
}
|
|
|
|
static int merge_msg_write_remotes(
|
|
git_filebuf *file,
|
|
git_vector *entries,
|
|
char sep)
|
|
{
|
|
const char *source;
|
|
|
|
if (entries->length == 0)
|
|
return 0;
|
|
|
|
source = ((struct merge_msg_entry *)entries->contents[0])->merge_head->remote_url;
|
|
|
|
return merge_msg_write_entries(file, entries,
|
|
"branch", "branches", strlen(GIT_REFS_HEADS_DIR), source, sep);
|
|
}
|
|
|
|
static int write_merge_msg(
|
|
git_repository *repo,
|
|
const git_annotated_commit *heads[],
|
|
size_t heads_len)
|
|
{
|
|
git_filebuf file = GIT_FILEBUF_INIT;
|
|
git_buf file_path = GIT_BUF_INIT;
|
|
struct merge_msg_entry *entries;
|
|
git_vector matching = GIT_VECTOR_INIT;
|
|
size_t i;
|
|
char sep = 0;
|
|
int error = 0;
|
|
|
|
assert(repo && heads);
|
|
|
|
entries = git__calloc(heads_len, sizeof(struct merge_msg_entry));
|
|
GITERR_CHECK_ALLOC(entries);
|
|
|
|
if (git_vector_init(&matching, heads_len, NULL) < 0) {
|
|
git__free(entries);
|
|
return -1;
|
|
}
|
|
|
|
for (i = 0; i < heads_len; i++)
|
|
entries[i].merge_head = heads[i];
|
|
|
|
if ((error = git_buf_joinpath(&file_path, repo->path_repository, GIT_MERGE_MSG_FILE)) < 0 ||
|
|
(error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_FORCE, GIT_MERGE_FILE_MODE)) < 0 ||
|
|
(error = git_filebuf_write(&file, "Merge ", 6)) < 0)
|
|
goto cleanup;
|
|
|
|
/*
|
|
* This is to emulate the format of MERGE_MSG by core git.
|
|
*
|
|
* Core git will write all the commits specified by OID, in the order
|
|
* provided, until the first named branch or tag is reached, at which
|
|
* point all branches will be written in the order provided, then all
|
|
* tags, then all remote tracking branches and finally all commits that
|
|
* were specified by OID that were not already written.
|
|
*
|
|
* Yes. Really.
|
|
*/
|
|
for (i = 0; i < heads_len; i++) {
|
|
if (!msg_entry_is_oid(&entries[i]))
|
|
break;
|
|
|
|
if ((error = git_filebuf_printf(&file,
|
|
"%scommit '%s'", (i > 0) ? "; " : "",
|
|
entries[i].merge_head->id_str)) < 0)
|
|
goto cleanup;
|
|
|
|
entries[i].written = 1;
|
|
}
|
|
|
|
if (i)
|
|
sep = ';';
|
|
|
|
if ((error = merge_msg_entries(&matching, entries, heads_len, msg_entry_is_branch)) < 0 ||
|
|
(error = merge_msg_write_branches(&file, &matching, sep)) < 0)
|
|
goto cleanup;
|
|
|
|
if (matching.length)
|
|
sep =',';
|
|
|
|
if ((error = merge_msg_entries(&matching, entries, heads_len, msg_entry_is_tracking)) < 0 ||
|
|
(error = merge_msg_write_tracking(&file, &matching, sep)) < 0)
|
|
goto cleanup;
|
|
|
|
if (matching.length)
|
|
sep =',';
|
|
|
|
if ((error = merge_msg_entries(&matching, entries, heads_len, msg_entry_is_tag)) < 0 ||
|
|
(error = merge_msg_write_tags(&file, &matching, sep)) < 0)
|
|
goto cleanup;
|
|
|
|
if (matching.length)
|
|
sep =',';
|
|
|
|
/* We should never be called with multiple remote branches, but handle
|
|
* it in case we are... */
|
|
while ((error = merge_msg_entries(&matching, entries, heads_len, msg_entry_is_remote)) > 0) {
|
|
if ((error = merge_msg_write_remotes(&file, &matching, sep)) < 0)
|
|
goto cleanup;
|
|
|
|
if (matching.length)
|
|
sep =',';
|
|
}
|
|
|
|
if (error < 0)
|
|
goto cleanup;
|
|
|
|
for (i = 0; i < heads_len; i++) {
|
|
if (merge_msg_entry_written(&entries[i]))
|
|
continue;
|
|
|
|
if ((error = git_filebuf_printf(&file, "; commit '%s'",
|
|
entries[i].merge_head->id_str)) < 0)
|
|
goto cleanup;
|
|
}
|
|
|
|
if ((error = git_filebuf_printf(&file, "\n")) < 0 ||
|
|
(error = git_filebuf_commit(&file)) < 0)
|
|
goto cleanup;
|
|
|
|
cleanup:
|
|
if (error < 0)
|
|
git_filebuf_cleanup(&file);
|
|
|
|
git_buf_free(&file_path);
|
|
|
|
git_vector_free(&matching);
|
|
git__free(entries);
|
|
|
|
return error;
|
|
}
|
|
|
|
int git_merge__setup(
|
|
git_repository *repo,
|
|
const git_annotated_commit *our_head,
|
|
const git_annotated_commit *heads[],
|
|
size_t heads_len)
|
|
{
|
|
int error = 0;
|
|
|
|
assert (repo && our_head && heads);
|
|
|
|
if ((error = git_repository__set_orig_head(repo, git_annotated_commit_id(our_head))) == 0 &&
|
|
(error = write_merge_head(repo, heads, heads_len)) == 0 &&
|
|
(error = write_merge_mode(repo)) == 0) {
|
|
error = write_merge_msg(repo, heads, heads_len);
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
/* Merge branches */
|
|
|
|
static int merge_ancestor_head(
|
|
git_annotated_commit **ancestor_head,
|
|
git_repository *repo,
|
|
const git_annotated_commit *our_head,
|
|
const git_annotated_commit **their_heads,
|
|
size_t their_heads_len)
|
|
{
|
|
git_oid *oids, ancestor_oid;
|
|
size_t i, alloc_len;
|
|
int error = 0;
|
|
|
|
assert(repo && our_head && their_heads);
|
|
|
|
GITERR_CHECK_ALLOC_ADD(&alloc_len, their_heads_len, 1);
|
|
oids = git__calloc(alloc_len, sizeof(git_oid));
|
|
GITERR_CHECK_ALLOC(oids);
|
|
|
|
git_oid_cpy(&oids[0], git_commit_id(our_head->commit));
|
|
|
|
for (i = 0; i < their_heads_len; i++)
|
|
git_oid_cpy(&oids[i + 1], git_annotated_commit_id(their_heads[i]));
|
|
|
|
if ((error = git_merge_base_many(&ancestor_oid, repo, their_heads_len + 1, oids)) < 0)
|
|
goto on_error;
|
|
|
|
error = git_annotated_commit_lookup(ancestor_head, repo, &ancestor_oid);
|
|
|
|
on_error:
|
|
git__free(oids);
|
|
return error;
|
|
}
|
|
|
|
const char *merge_their_label(const char *branchname)
|
|
{
|
|
const char *slash;
|
|
|
|
if ((slash = strrchr(branchname, '/')) == NULL)
|
|
return branchname;
|
|
|
|
if (*(slash+1) == '\0')
|
|
return "theirs";
|
|
|
|
return slash+1;
|
|
}
|
|
|
|
static int merge_normalize_checkout_opts(
|
|
git_checkout_options *out,
|
|
git_repository *repo,
|
|
const git_checkout_options *given_checkout_opts,
|
|
unsigned int checkout_strategy,
|
|
git_annotated_commit *ancestor,
|
|
const git_annotated_commit *our_head,
|
|
const git_annotated_commit **their_heads,
|
|
size_t their_heads_len)
|
|
{
|
|
git_checkout_options default_checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;
|
|
int error = 0;
|
|
|
|
GIT_UNUSED(repo);
|
|
|
|
if (given_checkout_opts != NULL)
|
|
memcpy(out, given_checkout_opts, sizeof(git_checkout_options));
|
|
else
|
|
memcpy(out, &default_checkout_opts, sizeof(git_checkout_options));
|
|
|
|
out->checkout_strategy = checkout_strategy;
|
|
|
|
if (!out->ancestor_label) {
|
|
if (ancestor && ancestor->type == GIT_ANNOTATED_COMMIT_REAL)
|
|
out->ancestor_label = git_commit_summary(ancestor->commit);
|
|
else if (ancestor)
|
|
out->ancestor_label = "merged common ancestors";
|
|
else
|
|
out->ancestor_label = "empty base";
|
|
}
|
|
|
|
if (!out->our_label) {
|
|
if (our_head && our_head->ref_name)
|
|
out->our_label = our_head->ref_name;
|
|
else
|
|
out->our_label = "ours";
|
|
}
|
|
|
|
if (!out->their_label) {
|
|
if (their_heads_len == 1 && their_heads[0]->ref_name)
|
|
out->their_label = merge_their_label(their_heads[0]->ref_name);
|
|
else if (their_heads_len == 1)
|
|
out->their_label = their_heads[0]->id_str;
|
|
else
|
|
out->their_label = "theirs";
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
static int merge_check_index(size_t *conflicts, git_repository *repo, git_index *index_new, git_vector *merged_paths)
|
|
{
|
|
git_tree *head_tree = NULL;
|
|
git_index *index_repo = NULL;
|
|
git_iterator *iter_repo = NULL, *iter_new = NULL;
|
|
git_iterator_options iter_opts = GIT_ITERATOR_OPTIONS_INIT;
|
|
git_diff *staged_diff_list = NULL, *index_diff_list = NULL;
|
|
git_diff_delta *delta;
|
|
git_diff_options opts = GIT_DIFF_OPTIONS_INIT;
|
|
git_vector staged_paths = GIT_VECTOR_INIT;
|
|
size_t i;
|
|
int error = 0;
|
|
|
|
GIT_UNUSED(merged_paths);
|
|
|
|
*conflicts = 0;
|
|
|
|
/* No staged changes may exist unless the change staged is identical to
|
|
* the result of the merge. This allows one to apply to merge manually,
|
|
* then run merge. Any other staged change would be overwritten by
|
|
* a reset merge.
|
|
*/
|
|
if ((error = git_repository_head_tree(&head_tree, repo)) < 0 ||
|
|
(error = git_repository_index(&index_repo, repo)) < 0 ||
|
|
(error = git_diff_tree_to_index(&staged_diff_list, repo, head_tree, index_repo, &opts)) < 0)
|
|
goto done;
|
|
|
|
if (staged_diff_list->deltas.length == 0)
|
|
goto done;
|
|
|
|
git_vector_foreach(&staged_diff_list->deltas, i, delta) {
|
|
if ((error = git_vector_insert(&staged_paths, (char *)delta->new_file.path)) < 0)
|
|
goto done;
|
|
}
|
|
|
|
iter_opts.flags = GIT_ITERATOR_DONT_IGNORE_CASE;
|
|
iter_opts.pathlist.strings = (char **)staged_paths.contents;
|
|
iter_opts.pathlist.count = staged_paths.length;
|
|
|
|
if ((error = git_iterator_for_index(&iter_repo, index_repo, &iter_opts)) < 0 ||
|
|
(error = git_iterator_for_index(&iter_new, index_new, &iter_opts)) < 0 ||
|
|
(error = git_diff__from_iterators(&index_diff_list, repo, iter_repo, iter_new, &opts)) < 0)
|
|
goto done;
|
|
|
|
*conflicts = index_diff_list->deltas.length;
|
|
|
|
done:
|
|
git_tree_free(head_tree);
|
|
git_index_free(index_repo);
|
|
git_iterator_free(iter_repo);
|
|
git_iterator_free(iter_new);
|
|
git_diff_free(staged_diff_list);
|
|
git_diff_free(index_diff_list);
|
|
git_vector_free(&staged_paths);
|
|
|
|
return error;
|
|
}
|
|
|
|
static int merge_check_workdir(size_t *conflicts, git_repository *repo, git_index *index_new, git_vector *merged_paths)
|
|
{
|
|
git_diff *wd_diff_list = NULL;
|
|
git_diff_options opts = GIT_DIFF_OPTIONS_INIT;
|
|
int error = 0;
|
|
|
|
GIT_UNUSED(index_new);
|
|
|
|
*conflicts = 0;
|
|
|
|
/* We need to have merged at least 1 file for the possibility to exist to
|
|
* have conflicts with the workdir. Passing 0 as the pathspec count paramter
|
|
* will consider all files in the working directory, that is, we may detect
|
|
* a conflict if there were untracked files in the workdir prior to starting
|
|
* the merge. This typically happens when cherry-picking a commmit whose
|
|
* changes have already been applied.
|
|
*/
|
|
if (merged_paths->length == 0)
|
|
return 0;
|
|
|
|
opts.flags |= GIT_DIFF_INCLUDE_UNTRACKED;
|
|
|
|
/* Workdir changes may exist iff they do not conflict with changes that
|
|
* will be applied by the merge (including conflicts). Ensure that there
|
|
* are no changes in the workdir to these paths.
|
|
*/
|
|
opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH;
|
|
opts.pathspec.count = merged_paths->length;
|
|
opts.pathspec.strings = (char **)merged_paths->contents;
|
|
|
|
if ((error = git_diff_index_to_workdir(&wd_diff_list, repo, NULL, &opts)) < 0)
|
|
goto done;
|
|
|
|
*conflicts = wd_diff_list->deltas.length;
|
|
|
|
done:
|
|
git_diff_free(wd_diff_list);
|
|
|
|
return error;
|
|
}
|
|
|
|
int git_merge__check_result(git_repository *repo, git_index *index_new)
|
|
{
|
|
git_tree *head_tree = NULL;
|
|
git_iterator *iter_head = NULL, *iter_new = NULL;
|
|
git_iterator_options iter_opts = GIT_ITERATOR_OPTIONS_INIT;
|
|
git_diff *merged_list = NULL;
|
|
git_diff_options opts = GIT_DIFF_OPTIONS_INIT;
|
|
git_diff_delta *delta;
|
|
git_vector paths = GIT_VECTOR_INIT;
|
|
size_t i, index_conflicts = 0, wd_conflicts = 0, conflicts;
|
|
const git_index_entry *e;
|
|
int error = 0;
|
|
|
|
iter_opts.flags = GIT_ITERATOR_DONT_IGNORE_CASE;
|
|
|
|
if ((error = git_repository_head_tree(&head_tree, repo)) < 0 ||
|
|
(error = git_iterator_for_tree(&iter_head, head_tree, &iter_opts)) < 0 ||
|
|
(error = git_iterator_for_index(&iter_new, index_new, &iter_opts)) < 0 ||
|
|
(error = git_diff__from_iterators(&merged_list, repo, iter_head, iter_new, &opts)) < 0)
|
|
goto done;
|
|
|
|
git_vector_foreach(&merged_list->deltas, i, delta) {
|
|
if ((error = git_vector_insert(&paths, (char *)delta->new_file.path)) < 0)
|
|
goto done;
|
|
}
|
|
|
|
for (i = 0; i < git_index_entrycount(index_new); i++) {
|
|
e = git_index_get_byindex(index_new, i);
|
|
|
|
if (git_index_entry_is_conflict(e) &&
|
|
(git_vector_last(&paths) == NULL ||
|
|
strcmp(git_vector_last(&paths), e->path) != 0)) {
|
|
|
|
if ((error = git_vector_insert(&paths, (char *)e->path)) < 0)
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
/* Make sure the index and workdir state do not prevent merging */
|
|
if ((error = merge_check_index(&index_conflicts, repo, index_new, &paths)) < 0 ||
|
|
(error = merge_check_workdir(&wd_conflicts, repo, index_new, &paths)) < 0)
|
|
goto done;
|
|
|
|
if ((conflicts = index_conflicts + wd_conflicts) > 0) {
|
|
giterr_set(GITERR_MERGE, "%" PRIuZ " uncommitted change%s would be overwritten by merge",
|
|
conflicts, (conflicts != 1) ? "s" : "");
|
|
error = GIT_ECONFLICT;
|
|
}
|
|
|
|
done:
|
|
git_vector_free(&paths);
|
|
git_tree_free(head_tree);
|
|
git_iterator_free(iter_head);
|
|
git_iterator_free(iter_new);
|
|
git_diff_free(merged_list);
|
|
|
|
return error;
|
|
}
|
|
|
|
int git_merge__append_conflicts_to_merge_msg(
|
|
git_repository *repo,
|
|
git_index *index)
|
|
{
|
|
git_filebuf file = GIT_FILEBUF_INIT;
|
|
git_buf file_path = GIT_BUF_INIT;
|
|
const char *last = NULL;
|
|
size_t i;
|
|
int error;
|
|
|
|
if (!git_index_has_conflicts(index))
|
|
return 0;
|
|
|
|
if ((error = git_buf_joinpath(&file_path, repo->path_repository, GIT_MERGE_MSG_FILE)) < 0 ||
|
|
(error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_APPEND, GIT_MERGE_FILE_MODE)) < 0)
|
|
goto cleanup;
|
|
|
|
git_filebuf_printf(&file, "\nConflicts:\n");
|
|
|
|
for (i = 0; i < git_index_entrycount(index); i++) {
|
|
const git_index_entry *e = git_index_get_byindex(index, i);
|
|
|
|
if (!git_index_entry_is_conflict(e))
|
|
continue;
|
|
|
|
if (last == NULL || strcmp(e->path, last) != 0)
|
|
git_filebuf_printf(&file, "\t%s\n", e->path);
|
|
|
|
last = e->path;
|
|
}
|
|
|
|
error = git_filebuf_commit(&file);
|
|
|
|
cleanup:
|
|
if (error < 0)
|
|
git_filebuf_cleanup(&file);
|
|
|
|
git_buf_free(&file_path);
|
|
|
|
return error;
|
|
}
|
|
|
|
static int merge_state_cleanup(git_repository *repo)
|
|
{
|
|
const char *state_files[] = {
|
|
GIT_MERGE_HEAD_FILE,
|
|
GIT_MERGE_MODE_FILE,
|
|
GIT_MERGE_MSG_FILE,
|
|
};
|
|
|
|
return git_repository__cleanup_files(repo, state_files, ARRAY_SIZE(state_files));
|
|
}
|
|
|
|
static int merge_heads(
|
|
git_annotated_commit **ancestor_head_out,
|
|
git_annotated_commit **our_head_out,
|
|
git_repository *repo,
|
|
const git_annotated_commit **their_heads,
|
|
size_t their_heads_len)
|
|
{
|
|
git_annotated_commit *ancestor_head = NULL, *our_head = NULL;
|
|
git_reference *our_ref = NULL;
|
|
int error = 0;
|
|
|
|
*ancestor_head_out = NULL;
|
|
*our_head_out = NULL;
|
|
|
|
if ((error = git_repository__ensure_not_bare(repo, "merge")) < 0)
|
|
goto done;
|
|
|
|
if ((error = git_reference_lookup(&our_ref, repo, GIT_HEAD_FILE)) < 0 ||
|
|
(error = git_annotated_commit_from_ref(&our_head, repo, our_ref)) < 0)
|
|
goto done;
|
|
|
|
if ((error = merge_ancestor_head(&ancestor_head, repo, our_head, their_heads, their_heads_len)) < 0) {
|
|
if (error != GIT_ENOTFOUND)
|
|
goto done;
|
|
|
|
giterr_clear();
|
|
error = 0;
|
|
}
|
|
|
|
*ancestor_head_out = ancestor_head;
|
|
*our_head_out = our_head;
|
|
|
|
done:
|
|
if (error < 0) {
|
|
git_annotated_commit_free(ancestor_head);
|
|
git_annotated_commit_free(our_head);
|
|
}
|
|
|
|
git_reference_free(our_ref);
|
|
|
|
return error;
|
|
}
|
|
|
|
static int merge_preference(git_merge_preference_t *out, git_repository *repo)
|
|
{
|
|
git_config *config;
|
|
const char *value;
|
|
int bool_value, error = 0;
|
|
|
|
*out = GIT_MERGE_PREFERENCE_NONE;
|
|
|
|
if ((error = git_repository_config_snapshot(&config, repo)) < 0)
|
|
goto done;
|
|
|
|
if ((error = git_config_get_string(&value, config, "merge.ff")) < 0) {
|
|
if (error == GIT_ENOTFOUND) {
|
|
giterr_clear();
|
|
error = 0;
|
|
}
|
|
|
|
goto done;
|
|
}
|
|
|
|
if (git_config_parse_bool(&bool_value, value) == 0) {
|
|
if (!bool_value)
|
|
*out |= GIT_MERGE_PREFERENCE_NO_FASTFORWARD;
|
|
} else {
|
|
if (strcasecmp(value, "only") == 0)
|
|
*out |= GIT_MERGE_PREFERENCE_FASTFORWARD_ONLY;
|
|
}
|
|
|
|
done:
|
|
git_config_free(config);
|
|
return error;
|
|
}
|
|
|
|
int git_merge_analysis(
|
|
git_merge_analysis_t *analysis_out,
|
|
git_merge_preference_t *preference_out,
|
|
git_repository *repo,
|
|
const git_annotated_commit **their_heads,
|
|
size_t their_heads_len)
|
|
{
|
|
git_annotated_commit *ancestor_head = NULL, *our_head = NULL;
|
|
int error = 0;
|
|
|
|
assert(analysis_out && preference_out && repo && their_heads);
|
|
|
|
if (their_heads_len != 1) {
|
|
giterr_set(GITERR_MERGE, "Can only merge a single branch");
|
|
error = -1;
|
|
goto done;
|
|
}
|
|
|
|
*analysis_out = GIT_MERGE_ANALYSIS_NONE;
|
|
|
|
if ((error = merge_preference(preference_out, repo)) < 0)
|
|
goto done;
|
|
|
|
if (git_repository_head_unborn(repo)) {
|
|
*analysis_out |= GIT_MERGE_ANALYSIS_FASTFORWARD | GIT_MERGE_ANALYSIS_UNBORN;
|
|
goto done;
|
|
}
|
|
|
|
if ((error = merge_heads(&ancestor_head, &our_head, repo, their_heads, their_heads_len)) < 0)
|
|
goto done;
|
|
|
|
/* We're up-to-date if we're trying to merge our own common ancestor. */
|
|
if (ancestor_head && git_oid_equal(
|
|
git_annotated_commit_id(ancestor_head), git_annotated_commit_id(their_heads[0])))
|
|
*analysis_out |= GIT_MERGE_ANALYSIS_UP_TO_DATE;
|
|
|
|
/* We're fastforwardable if we're our own common ancestor. */
|
|
else if (ancestor_head && git_oid_equal(
|
|
git_annotated_commit_id(ancestor_head), git_annotated_commit_id(our_head)))
|
|
*analysis_out |= GIT_MERGE_ANALYSIS_FASTFORWARD | GIT_MERGE_ANALYSIS_NORMAL;
|
|
|
|
/* Otherwise, just a normal merge is possible. */
|
|
else
|
|
*analysis_out |= GIT_MERGE_ANALYSIS_NORMAL;
|
|
|
|
done:
|
|
git_annotated_commit_free(ancestor_head);
|
|
git_annotated_commit_free(our_head);
|
|
return error;
|
|
}
|
|
|
|
int git_merge(
|
|
git_repository *repo,
|
|
const git_annotated_commit **their_heads,
|
|
size_t their_heads_len,
|
|
const git_merge_options *merge_opts,
|
|
const git_checkout_options *given_checkout_opts)
|
|
{
|
|
git_reference *our_ref = NULL;
|
|
git_checkout_options checkout_opts;
|
|
git_annotated_commit *our_head = NULL, *base = NULL;
|
|
git_index *index = NULL;
|
|
git_indexwriter indexwriter = GIT_INDEXWRITER_INIT;
|
|
unsigned int checkout_strategy;
|
|
int error = 0;
|
|
|
|
assert(repo && their_heads);
|
|
|
|
if (their_heads_len != 1) {
|
|
giterr_set(GITERR_MERGE, "Can only merge a single branch");
|
|
return -1;
|
|
}
|
|
|
|
if ((error = git_repository__ensure_not_bare(repo, "merge")) < 0)
|
|
goto done;
|
|
|
|
checkout_strategy = given_checkout_opts ?
|
|
given_checkout_opts->checkout_strategy :
|
|
GIT_CHECKOUT_SAFE;
|
|
|
|
if ((error = git_indexwriter_init_for_operation(&indexwriter, repo,
|
|
&checkout_strategy)) < 0)
|
|
goto done;
|
|
|
|
/* Write the merge setup files to the repository. */
|
|
if ((error = git_annotated_commit_from_head(&our_head, repo)) < 0 ||
|
|
(error = git_merge__setup(repo, our_head, their_heads,
|
|
their_heads_len)) < 0)
|
|
goto done;
|
|
|
|
/* TODO: octopus */
|
|
|
|
if ((error = merge_annotated_commits(&index, &base, repo, our_head,
|
|
(git_annotated_commit *)their_heads[0], 0, merge_opts)) < 0 ||
|
|
(error = git_merge__check_result(repo, index)) < 0 ||
|
|
(error = git_merge__append_conflicts_to_merge_msg(repo, index)) < 0)
|
|
goto done;
|
|
|
|
/* check out the merge results */
|
|
|
|
if ((error = merge_normalize_checkout_opts(&checkout_opts, repo,
|
|
given_checkout_opts, checkout_strategy,
|
|
base, our_head, their_heads, their_heads_len)) < 0 ||
|
|
(error = git_checkout_index(repo, index, &checkout_opts)) < 0)
|
|
goto done;
|
|
|
|
error = git_indexwriter_commit(&indexwriter);
|
|
|
|
done:
|
|
if (error < 0)
|
|
merge_state_cleanup(repo);
|
|
|
|
git_indexwriter_cleanup(&indexwriter);
|
|
git_index_free(index);
|
|
git_annotated_commit_free(our_head);
|
|
git_annotated_commit_free(base);
|
|
git_reference_free(our_ref);
|
|
|
|
return error;
|
|
}
|
|
|
|
int git_merge_init_options(git_merge_options *opts, unsigned int version)
|
|
{
|
|
GIT_INIT_STRUCTURE_FROM_TEMPLATE(
|
|
opts, version, git_merge_options, GIT_MERGE_OPTIONS_INIT);
|
|
return 0;
|
|
}
|
|
|
|
int git_merge_file_init_input(git_merge_file_input *input, unsigned int version)
|
|
{
|
|
GIT_INIT_STRUCTURE_FROM_TEMPLATE(
|
|
input, version, git_merge_file_input, GIT_MERGE_FILE_INPUT_INIT);
|
|
return 0;
|
|
}
|
|
|
|
int git_merge_file_init_options(
|
|
git_merge_file_options *opts, unsigned int version)
|
|
{
|
|
GIT_INIT_STRUCTURE_FROM_TEMPLATE(
|
|
opts, version, git_merge_file_options, GIT_MERGE_FILE_OPTIONS_INIT);
|
|
return 0;
|
|
}
|