mirror of
https://git.proxmox.com/git/libgit2
synced 2025-05-10 00:15:45 +00:00

1. internal iterators now return GIT_ITEROVER when you go past the last item in the iteration. 2. git_iterator_advance will "advance" to the first item in the iteration if it is called immediately after creating the iterator, which allows a simpler idiom for basic iteration. 3. if git_iterator_advance encounters an error reading data (e.g. a missing tree or an unreadable file), it returns the error but also attempts to advance past the invalid data to prevent an infinite loop. Updated all tests and internal usage of iterators to account for these new behaviors.
2155 lines
56 KiB
C
2155 lines
56 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 "merge_file.h"
|
|
#include "blob.h"
|
|
#include "hashsig.h"
|
|
#include "oid.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/sys/index.h"
|
|
|
|
#define GIT_MERGE_INDEX_ENTRY_EXISTS(X) ((X).mode != 0)
|
|
|
|
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 git_merge_base_many(git_oid *out, git_repository *repo, const git_oid input_array[], size_t length)
|
|
{
|
|
git_revwalk *walk;
|
|
git_vector list;
|
|
git_commit_list *result = NULL;
|
|
int error = -1;
|
|
unsigned int i;
|
|
git_commit_list_node *commit;
|
|
|
|
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 %u.", length);
|
|
return -1;
|
|
}
|
|
|
|
if (git_vector_init(&list, length - 1, NULL) < 0)
|
|
return -1;
|
|
|
|
if (git_revwalk_new(&walk, repo) < 0)
|
|
goto cleanup;
|
|
|
|
for (i = 1; i < length; i++) {
|
|
commit = git_revwalk__commit_lookup(walk, &input_array[i]);
|
|
if (commit == NULL)
|
|
goto cleanup;
|
|
|
|
git_vector_insert(&list, commit);
|
|
}
|
|
|
|
commit = git_revwalk__commit_lookup(walk, &input_array[0]);
|
|
if (commit == NULL)
|
|
goto cleanup;
|
|
|
|
if (git_merge__bases_many(&result, walk, commit, &list) < 0)
|
|
goto cleanup;
|
|
|
|
if (!result) {
|
|
giterr_set(GITERR_MERGE, "No merge base found");
|
|
error = GIT_ENOTFOUND;
|
|
goto cleanup;
|
|
}
|
|
|
|
git_oid_cpy(out, &result->item->oid);
|
|
|
|
error = 0;
|
|
|
|
cleanup:
|
|
git_commit_list_free(&result);
|
|
git_revwalk_free(walk);
|
|
git_vector_free(&list);
|
|
return error;
|
|
}
|
|
|
|
int git_merge_base(git_oid *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;
|
|
}
|
|
|
|
git_oid_cpy(out, &result->item->oid);
|
|
git_commit_list_free(&result);
|
|
git_revwalk_free(walk);
|
|
|
|
return 0;
|
|
|
|
on_error:
|
|
git_revwalk_free(walk);
|
|
return -1;
|
|
}
|
|
|
|
static int interesting(git_pqueue *list)
|
|
{
|
|
unsigned int i;
|
|
/* element 0 isn't used - we need to start at 1 */
|
|
for (i = 1; i < list->size; i++) {
|
|
git_commit_list_node *commit = list->d[i];
|
|
if ((commit->flags & STALE) == 0)
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
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;
|
|
git_pqueue list;
|
|
|
|
/* 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_pqueue_init(&list, twos->length * 2, git_commit_list_time_cmp) < 0)
|
|
return -1;
|
|
|
|
if (git_commit_list_parse(walk, one) < 0)
|
|
return -1;
|
|
|
|
one->flags |= PARENT1;
|
|
if (git_pqueue_insert(&list, one) < 0)
|
|
return -1;
|
|
|
|
git_vector_foreach(twos, i, two) {
|
|
git_commit_list_parse(walk, two);
|
|
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;
|
|
int flags;
|
|
|
|
commit = git_pqueue_pop(&list);
|
|
|
|
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);
|
|
|
|
/* filter out any stale commits in the results */
|
|
tmp = result;
|
|
result = NULL;
|
|
|
|
while (tmp) {
|
|
struct git_commit_list *next = tmp->next;
|
|
if (!(tmp->item->flags & STALE))
|
|
if (git_commit_list_insert_by_date(tmp->item, &result) == NULL)
|
|
return -1;
|
|
|
|
git__free(tmp);
|
|
tmp = next;
|
|
}
|
|
|
|
*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 (cb(&oid, payload) < 0) {
|
|
error = GIT_EUSER;
|
|
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->oid, &b->oid)) == 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.oid, &conflict->our_entry.oid) != 0);
|
|
theirs_changed = (git_oid__cmp(&conflict->ancestor_entry.oid, &conflict->their_entry.oid) != 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.oid, &conflict->their_entry.oid) != 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,
|
|
unsigned int automerge_flags)
|
|
{
|
|
git_merge_file_input ancestor = GIT_MERGE_FILE_INPUT_INIT,
|
|
ours = GIT_MERGE_FILE_INPUT_INIT,
|
|
theirs = GIT_MERGE_FILE_INPUT_INIT;
|
|
git_merge_file_result result = GIT_MERGE_FILE_RESULT_INIT;
|
|
git_index_entry *index_entry;
|
|
git_odb *odb = NULL;
|
|
git_oid automerge_oid;
|
|
int error = 0;
|
|
|
|
assert(resolved && diff_list && conflict);
|
|
|
|
*resolved = 0;
|
|
|
|
if (automerge_flags == GIT_MERGE_AUTOMERGE_NONE)
|
|
return 0;
|
|
|
|
/* Reject D/F conflicts */
|
|
if (conflict->type == GIT_MERGE_DIFF_DIRECTORY_FILE)
|
|
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;
|
|
|
|
if ((error = git_repository_odb(&odb, diff_list->repo)) < 0 ||
|
|
(error = git_merge_file_input_from_index_entry(&ancestor, diff_list->repo, &conflict->ancestor_entry)) < 0 ||
|
|
(error = git_merge_file_input_from_index_entry(&ours, diff_list->repo, &conflict->our_entry)) < 0 ||
|
|
(error = git_merge_file_input_from_index_entry(&theirs, diff_list->repo, &conflict->their_entry)) < 0 ||
|
|
(error = git_merge_files(&result, &ancestor, &ours, &theirs, automerge_flags)) < 0 ||
|
|
!result.automergeable ||
|
|
(error = git_odb_write(&automerge_oid, odb, result.data, result.len, GIT_OBJ_BLOB)) < 0)
|
|
goto done;
|
|
|
|
if ((index_entry = git_pool_malloc(&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->oid, &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_input_free(&ancestor);
|
|
git_merge_file_input_free(&ours);
|
|
git_merge_file_input_free(&theirs);
|
|
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,
|
|
unsigned int automerge_flags)
|
|
{
|
|
int resolved = 0;
|
|
int error = 0;
|
|
|
|
*out = 0;
|
|
|
|
if ((error = merge_conflict_resolve_trivial(&resolved, diff_list, conflict)) < 0)
|
|
goto done;
|
|
|
|
if (automerge_flags != GIT_MERGE_AUTOMERGE_NONE) {
|
|
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, automerge_flags)) < 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_tree_opts *opts)
|
|
{
|
|
GIT_UNUSED(repo);
|
|
GIT_UNUSED(a_idx);
|
|
GIT_UNUSED(b_idx);
|
|
GIT_UNUSED(cache);
|
|
GIT_UNUSED(opts);
|
|
|
|
if (git_oid__cmp(&a->oid, &b->oid) == 0)
|
|
return 100;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int index_entry_similarity_calc(
|
|
void **out,
|
|
git_repository *repo,
|
|
git_index_entry *entry,
|
|
const git_merge_tree_opts *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->oid)) < 0)
|
|
return error;
|
|
|
|
git_oid_cpy(&diff_file.oid, &entry->oid);
|
|
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_tree_opts *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_tree_opts *),
|
|
void **cache,
|
|
const git_merge_tree_opts *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_tree_opts *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_tree_opts *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)
|
|
{
|
|
git_merge_diff *conflict = conflicts->contents[idx];
|
|
|
|
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_tree_opts *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_TREE_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) {
|
|
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);
|
|
|
|
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(
|
|
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->oid, &other->oid) ||
|
|
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_malloc(pool, sizeof(git_merge_diff))) == NULL)
|
|
return NULL;
|
|
|
|
if (index_entry_dup(&conflict->ancestor_entry, pool, entries[TREE_IDX_ANCESTOR]) < 0 ||
|
|
index_entry_dup(&conflict->our_entry, pool, entries[TREE_IDX_OURS]) < 0 ||
|
|
index_entry_dup(&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_index_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_index_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(entry, &diff_list->pool, tree_items[0])) >= 0)
|
|
error = git_vector_insert(&diff_list->staged, entry);
|
|
|
|
return error;
|
|
}
|
|
|
|
int git_merge_diff_list__find_differences(
|
|
git_merge_diff_list *diff_list,
|
|
const git_tree *ancestor_tree,
|
|
const git_tree *our_tree,
|
|
const git_tree *their_tree)
|
|
{
|
|
git_iterator *iterators[3] = {0};
|
|
const git_index_entry *items[3] = {0}, *best_cur_item, *cur_items[3];
|
|
git_vector_cmp entry_compare = git_index_entry__cmp;
|
|
struct merge_diff_df_data df_data = {0};
|
|
int cur_item_modified;
|
|
size_t i, j;
|
|
int error = 0;
|
|
|
|
assert(diff_list && our_tree && their_tree);
|
|
|
|
if ((error = git_iterator_for_tree(&iterators[TREE_IDX_ANCESTOR], (git_tree *)ancestor_tree, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0 ||
|
|
(error = git_iterator_for_tree(&iterators[TREE_IDX_OURS], (git_tree *)our_tree, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0 ||
|
|
(error = git_iterator_for_tree(&iterators[TREE_IDX_THEIRS], (git_tree *)their_tree, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0)
|
|
goto done;
|
|
|
|
/* Set up the iterators */
|
|
for (i = 0; i < 3; i++) {
|
|
error = git_iterator_current(&items[i], iterators[i]);
|
|
if (error < 0 && error != GIT_ITEROVER)
|
|
goto done;
|
|
}
|
|
|
|
while (true) {
|
|
for (i = 0; i < 3; i++)
|
|
cur_items[i] = NULL;
|
|
|
|
best_cur_item = NULL;
|
|
cur_item_modified = 0;
|
|
|
|
/* Find the next path(s) to consume from each iterator */
|
|
for (i = 0; i < 3; i++) {
|
|
if (items[i] == NULL) {
|
|
cur_item_modified = 1;
|
|
continue;
|
|
}
|
|
|
|
if (best_cur_item == NULL) {
|
|
best_cur_item = items[i];
|
|
cur_items[i] = items[i];
|
|
} else {
|
|
int path_diff = entry_compare(items[i], best_cur_item);
|
|
|
|
if (path_diff < 0) {
|
|
/*
|
|
* Found an item that sorts before our current item, make
|
|
* our current item this one.
|
|
*/
|
|
for (j = 0; j < i; j++)
|
|
cur_items[j] = NULL;
|
|
|
|
cur_item_modified = 1;
|
|
best_cur_item = items[i];
|
|
cur_items[i] = items[i];
|
|
} else if (path_diff > 0) {
|
|
/* No entry for the current item, this is modified */
|
|
cur_item_modified = 1;
|
|
} else if (path_diff == 0) {
|
|
cur_items[i] = items[i];
|
|
|
|
if (!cur_item_modified)
|
|
cur_item_modified = index_entry_cmp(best_cur_item, items[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (best_cur_item == NULL)
|
|
break;
|
|
|
|
if (cur_item_modified)
|
|
error = merge_index_insert_conflict(diff_list, &df_data, cur_items);
|
|
else
|
|
error = merge_index_insert_unmodified(diff_list, cur_items);
|
|
if (error < 0)
|
|
goto done;
|
|
|
|
/* Advance each iterator that participated */
|
|
for (i = 0; i < 3; i++) {
|
|
if (cur_items[i] == NULL)
|
|
continue;
|
|
|
|
error = git_iterator_advance(&items[i], iterators[i]);
|
|
if (error < 0 && error != GIT_ITEROVER)
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
done:
|
|
for (i = 0; i < 3; i++)
|
|
git_iterator_free(iterators[i]);
|
|
|
|
if (error == GIT_ITEROVER)
|
|
error = 0;
|
|
|
|
return error;
|
|
}
|
|
|
|
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;
|
|
|
|
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_pool_init(&diff_list->pool, 1, 0) < 0)
|
|
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_tree_normalize_opts(
|
|
git_repository *repo,
|
|
git_merge_tree_opts *opts,
|
|
const git_merge_tree_opts *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_tree_opts));
|
|
else {
|
|
git_merge_tree_opts init = GIT_MERGE_TREE_OPTS_INIT;
|
|
memcpy(opts, &init, sizeof(init));
|
|
|
|
opts->flags = GIT_MERGE_TREE_FIND_RENAMES;
|
|
opts->rename_threshold = GIT_MERGE_TREE_RENAME_THRESHOLD;
|
|
}
|
|
|
|
if (!opts->target_limit) {
|
|
int32_t limit = 0;
|
|
|
|
opts->target_limit = GIT_MERGE_TREE_TARGET_LIMIT;
|
|
|
|
if (git_config_get_int32(&limit, cfg, "merge.renameLimit") < 0) {
|
|
giterr_clear();
|
|
|
|
if (git_config_get_int32(&limit, cfg, "diff.renameLimit") < 0)
|
|
giterr_clear();
|
|
}
|
|
|
|
if (limit > 0)
|
|
opts->target_limit = 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;
|
|
|
|
if (opts->flags & GIT_DIFF_FIND_IGNORE_WHITESPACE)
|
|
opts->metric->payload = (void *)GIT_HASHSIG_IGNORE_WHITESPACE;
|
|
else if (opts->flags & GIT_DIFF_FIND_DONT_IGNORE_WHITESPACE)
|
|
opts->metric->payload = (void *)GIT_HASHSIG_NORMAL;
|
|
else
|
|
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->oid;
|
|
|
|
return git_index_reuc_add(index, entry->path,
|
|
mode[0], oid[0], mode[1], oid[1], mode[2], oid[2]);
|
|
}
|
|
|
|
int index_from_diff_list(git_index **out, git_merge_diff_list *diff_list)
|
|
{
|
|
git_index *index;
|
|
size_t i;
|
|
git_index_entry *entry;
|
|
git_merge_diff *conflict;
|
|
int error = 0;
|
|
|
|
*out = NULL;
|
|
|
|
if ((error = git_index_new(&index)) < 0)
|
|
return error;
|
|
|
|
git_vector_foreach(&diff_list->staged, i, entry) {
|
|
if ((error = git_index_add(index, entry)) < 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;
|
|
}
|
|
}
|
|
|
|
/* 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)
|
|
goto on_error;
|
|
|
|
if (ours != NULL &&
|
|
(error = merge_index_insert_reuc(index, TREE_IDX_OURS, ours)) < 0)
|
|
goto on_error;
|
|
|
|
if (theirs != NULL &&
|
|
(error = merge_index_insert_reuc(index, TREE_IDX_THEIRS, theirs)) < 0)
|
|
goto on_error;
|
|
}
|
|
|
|
*out = index;
|
|
return 0;
|
|
|
|
on_error:
|
|
git_index_free(index);
|
|
|
|
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_tree_opts *given_opts)
|
|
{
|
|
git_merge_diff_list *diff_list;
|
|
git_merge_tree_opts opts;
|
|
git_merge_diff *conflict;
|
|
git_vector changes;
|
|
size_t i;
|
|
int error = 0;
|
|
|
|
assert(out && repo && our_tree && their_tree);
|
|
|
|
*out = NULL;
|
|
|
|
if ((error = merge_tree_normalize_opts(repo, &opts, given_opts)) < 0)
|
|
return error;
|
|
|
|
diff_list = git_merge_diff_list__alloc(repo);
|
|
GITERR_CHECK_ALLOC(diff_list);
|
|
|
|
if ((error = git_merge_diff_list__find_differences(diff_list, ancestor_tree, our_tree, their_tree)) < 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, opts.automerge_flags)) < 0)
|
|
goto done;
|
|
|
|
if (!resolved)
|
|
git_vector_insert(&diff_list->conflicts, conflict);
|
|
}
|
|
|
|
if (!given_opts || !given_opts->metric)
|
|
git__free(opts.metric);
|
|
|
|
error = index_from_diff_list(out, diff_list);
|
|
|
|
done:
|
|
git_merge_diff_list__free(diff_list);
|
|
|
|
return error;
|
|
}
|
|
|
|
/* Merge setup / cleanup */
|
|
|
|
static int write_orig_head(
|
|
git_repository *repo,
|
|
const git_merge_head *our_head)
|
|
{
|
|
git_filebuf file = GIT_FILEBUF_INIT;
|
|
git_buf file_path = GIT_BUF_INIT;
|
|
char orig_oid_str[GIT_OID_HEXSZ + 1];
|
|
int error = 0;
|
|
|
|
assert(repo && our_head);
|
|
|
|
git_oid_tostr(orig_oid_str, GIT_OID_HEXSZ+1, &our_head->oid);
|
|
|
|
if ((error = git_buf_joinpath(&file_path, repo->path_repository, GIT_ORIG_HEAD_FILE)) == 0 &&
|
|
(error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_FORCE)) == 0 &&
|
|
(error = git_filebuf_printf(&file, "%s\n", orig_oid_str)) == 0)
|
|
error = git_filebuf_commit(&file, 0666);
|
|
|
|
if (error < 0)
|
|
git_filebuf_cleanup(&file);
|
|
|
|
git_buf_free(&file_path);
|
|
|
|
return error;
|
|
}
|
|
|
|
static int write_merge_head(
|
|
git_repository *repo,
|
|
const git_merge_head *heads[],
|
|
size_t heads_len)
|
|
{
|
|
git_filebuf file = GIT_FILEBUF_INIT;
|
|
git_buf file_path = GIT_BUF_INIT;
|
|
char merge_oid_str[GIT_OID_HEXSZ + 1];
|
|
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)) < 0)
|
|
goto cleanup;
|
|
|
|
for (i = 0; i < heads_len; i++) {
|
|
git_oid_tostr(merge_oid_str, GIT_OID_HEXSZ+1, &heads[i]->oid);
|
|
|
|
if ((error = git_filebuf_printf(&file, "%s\n", merge_oid_str)) < 0)
|
|
goto cleanup;
|
|
}
|
|
|
|
error = git_filebuf_commit(&file, 0666);
|
|
|
|
cleanup:
|
|
if (error < 0)
|
|
git_filebuf_cleanup(&file);
|
|
|
|
git_buf_free(&file_path);
|
|
|
|
return error;
|
|
}
|
|
|
|
static int write_merge_mode(git_repository *repo, unsigned int flags)
|
|
{
|
|
git_filebuf file = GIT_FILEBUF_INIT;
|
|
git_buf file_path = GIT_BUF_INIT;
|
|
int error = 0;
|
|
|
|
/* For future expansion */
|
|
GIT_UNUSED(flags);
|
|
|
|
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)) < 0)
|
|
goto cleanup;
|
|
|
|
error = git_filebuf_commit(&file, 0666);
|
|
|
|
cleanup:
|
|
if (error < 0)
|
|
git_filebuf_cleanup(&file);
|
|
|
|
git_buf_free(&file_path);
|
|
|
|
return error;
|
|
}
|
|
|
|
struct merge_msg_entry {
|
|
const git_merge_head *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_merge_head *heads[],
|
|
size_t heads_len)
|
|
{
|
|
git_filebuf file = GIT_FILEBUF_INIT;
|
|
git_buf file_path = GIT_BUF_INIT;
|
|
char oid_str[GIT_OID_HEXSZ + 1];
|
|
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)
|
|
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)) < 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;
|
|
|
|
git_oid_fmt(oid_str, &entries[i].merge_head->oid);
|
|
oid_str[GIT_OID_HEXSZ] = '\0';
|
|
|
|
if ((error = git_filebuf_printf(&file, "%scommit '%s'", (i > 0) ? "; " : "", oid_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;
|
|
|
|
git_oid_fmt(oid_str, &entries[i].merge_head->oid);
|
|
oid_str[GIT_OID_HEXSZ] = '\0';
|
|
|
|
if ((error = git_filebuf_printf(&file, "; commit '%s'", oid_str)) < 0)
|
|
goto cleanup;
|
|
}
|
|
|
|
if ((error = git_filebuf_printf(&file, "\n")) < 0 ||
|
|
(error = git_filebuf_commit(&file, 0666)) < 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_merge_head *our_head,
|
|
const git_merge_head *heads[],
|
|
size_t heads_len,
|
|
unsigned int flags)
|
|
{
|
|
int error = 0;
|
|
|
|
assert (repo && our_head && heads);
|
|
|
|
if ((error = write_orig_head(repo, our_head)) == 0 &&
|
|
(error = write_merge_head(repo, heads, heads_len)) == 0 &&
|
|
(error = write_merge_mode(repo, flags)) == 0) {
|
|
error = write_merge_msg(repo, heads, heads_len);
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
int git_repository_merge_cleanup(git_repository *repo)
|
|
{
|
|
int error = 0;
|
|
git_buf merge_head_path = GIT_BUF_INIT,
|
|
merge_mode_path = GIT_BUF_INIT,
|
|
merge_msg_path = GIT_BUF_INIT;
|
|
|
|
assert(repo);
|
|
|
|
if (git_buf_joinpath(&merge_head_path, repo->path_repository, GIT_MERGE_HEAD_FILE) < 0 ||
|
|
git_buf_joinpath(&merge_mode_path, repo->path_repository, GIT_MERGE_MODE_FILE) < 0 ||
|
|
git_buf_joinpath(&merge_msg_path, repo->path_repository, GIT_MERGE_MSG_FILE) < 0)
|
|
return -1;
|
|
|
|
if (git_path_isfile(merge_head_path.ptr)) {
|
|
if ((error = p_unlink(merge_head_path.ptr)) < 0)
|
|
goto cleanup;
|
|
}
|
|
|
|
if (git_path_isfile(merge_mode_path.ptr))
|
|
(void)p_unlink(merge_mode_path.ptr);
|
|
|
|
if (git_path_isfile(merge_msg_path.ptr))
|
|
(void)p_unlink(merge_msg_path.ptr);
|
|
|
|
cleanup:
|
|
git_buf_free(&merge_msg_path);
|
|
git_buf_free(&merge_mode_path);
|
|
git_buf_free(&merge_head_path);
|
|
|
|
return error;
|
|
}
|
|
|
|
/* Merge heads are the input to merge */
|
|
|
|
static int merge_head_init(
|
|
git_merge_head **out,
|
|
git_repository *repo,
|
|
const char *ref_name,
|
|
const char *remote_url,
|
|
const git_oid *oid)
|
|
{
|
|
git_merge_head *head;
|
|
int error = 0;
|
|
|
|
assert(out && oid);
|
|
|
|
*out = NULL;
|
|
|
|
head = git__calloc(1, sizeof(git_merge_head));
|
|
GITERR_CHECK_ALLOC(head);
|
|
|
|
if (ref_name) {
|
|
head->ref_name = git__strdup(ref_name);
|
|
GITERR_CHECK_ALLOC(head->ref_name);
|
|
}
|
|
|
|
if (remote_url) {
|
|
head->remote_url = git__strdup(remote_url);
|
|
GITERR_CHECK_ALLOC(head->remote_url);
|
|
}
|
|
|
|
git_oid_cpy(&head->oid, oid);
|
|
|
|
if ((error = git_commit_lookup(&head->commit, repo, &head->oid)) < 0) {
|
|
git_merge_head_free(head);
|
|
return error;
|
|
}
|
|
|
|
*out = head;
|
|
return error;
|
|
}
|
|
|
|
int git_merge_head_from_ref(
|
|
git_merge_head **out,
|
|
git_repository *repo,
|
|
git_reference *ref)
|
|
{
|
|
git_reference *resolved;
|
|
int error = 0;
|
|
|
|
assert(out && repo && ref);
|
|
|
|
*out = NULL;
|
|
|
|
if ((error = git_reference_resolve(&resolved, ref)) < 0)
|
|
return error;
|
|
|
|
error = merge_head_init(out, repo, git_reference_name(ref), NULL,
|
|
git_reference_target(resolved));
|
|
|
|
git_reference_free(resolved);
|
|
return error;
|
|
}
|
|
|
|
int git_merge_head_from_oid(
|
|
git_merge_head **out,
|
|
git_repository *repo,
|
|
const git_oid *oid)
|
|
{
|
|
assert(out && repo && oid);
|
|
|
|
return merge_head_init(out, repo, NULL, NULL, oid);
|
|
}
|
|
|
|
int git_merge_head_from_fetchhead(
|
|
git_merge_head **out,
|
|
git_repository *repo,
|
|
const char *branch_name,
|
|
const char *remote_url,
|
|
const git_oid *oid)
|
|
{
|
|
assert(repo && branch_name && remote_url && oid);
|
|
|
|
return merge_head_init(out, repo, branch_name, remote_url, oid);
|
|
}
|
|
|
|
void git_merge_head_free(git_merge_head *head)
|
|
{
|
|
if (head == NULL)
|
|
return;
|
|
|
|
if (head->commit != NULL)
|
|
git_object_free((git_object *)head->commit);
|
|
|
|
if (head->ref_name != NULL)
|
|
git__free(head->ref_name);
|
|
|
|
if (head->remote_url != NULL)
|
|
git__free(head->remote_url);
|
|
|
|
git__free(head);
|
|
}
|