Commit a27f31d8 by Carlos Martín Nieto

Merge pull request #3513 from ethomson/merge_recursive

Recursive Merge
parents e0ab1ca0 5b9c63c3
......@@ -34,6 +34,14 @@ v0.23 + 1
### Breaking API changes
* The `git_merge_tree_flag_t` is now `git_merge_flag_t`. Subsequently,
its members are no longer prefixed with `GIT_MERGE_TREE_FLAG` but are
now prefixed with `GIT_MERGE_FLAG`, and the `tree_flags` field of the
`git_merge_options` structure is now named `flags`.
* The `git_merge_file_flags_t` enum is now `git_merge_file_flag_t` for
consistency with other enum type names.
* `git_cert` descendent types now have a proper `parent` member
* It is the responsibility of the refdb backend to decide what to do
......
......@@ -62,8 +62,8 @@ GIT_EXTERN(int) git_merge_file_init_input(
unsigned int version);
/**
* Flags for `git_merge_tree` options. A combination of these flags can be
* passed in via the `tree_flags` value in the `git_merge_options`.
* Flags for `git_merge` options. A combination of these flags can be
* passed in via the `flags` value in the `git_merge_options`.
*/
typedef enum {
/**
......@@ -71,20 +71,28 @@ typedef enum {
* side or the common ancestor and the "theirs" side. This will enable
* the ability to merge between a modified and renamed file.
*/
GIT_MERGE_TREE_FIND_RENAMES = (1 << 0),
GIT_MERGE_FIND_RENAMES = (1 << 0),
/**
* If a conflict occurs, exit immediately instead of attempting to
* continue resolving conflicts. The merge operation will fail with
* GIT_EMERGECONFLICT and no index will be returned.
*/
GIT_MERGE_TREE_FAIL_ON_CONFLICT = (1 << 1),
GIT_MERGE_FAIL_ON_CONFLICT = (1 << 1),
/**
* Do not write the REUC extension on the generated index
*/
GIT_MERGE_TREE_SKIP_REUC = (1 << 2),
} git_merge_tree_flag_t;
GIT_MERGE_SKIP_REUC = (1 << 2),
/**
* If the commits being merged have multiple merge bases, do not build
* a recursive merge base (by merging the multiple merge bases),
* instead simply use the first base. This flag provides a similar
* merge base to `git-merge-resolve`.
*/
GIT_MERGE_NO_RECURSIVE = (1 << 3),
} git_merge_flag_t;
/**
* Merge file favor options for `git_merge_options` instruct the file-level
......@@ -152,7 +160,7 @@ typedef enum {
/** Take extra time to find minimal diff */
GIT_MERGE_FILE_DIFF_MINIMAL = (1 << 7),
} git_merge_file_flags_t;
} git_merge_file_flag_t;
/**
* Options for merging a file
......@@ -181,8 +189,8 @@ typedef struct {
/** The file to favor in region conflicts. */
git_merge_file_favor_t favor;
/** see `git_merge_file_flags_t` above */
unsigned int flags;
/** see `git_merge_file_flag_t` above */
git_merge_file_flag_t flags;
} git_merge_file_options;
#define GIT_MERGE_FILE_OPTIONS_VERSION 1
......@@ -232,11 +240,13 @@ typedef struct {
*/
typedef struct {
unsigned int version;
git_merge_tree_flag_t tree_flags;
/** See `git_merge_flag_t` above */
git_merge_flag_t flags;
/**
* Similarity to consider a file renamed (default 50). If
* `GIT_MERGE_TREE_FIND_RENAMES` is enabled, added files will be compared
* `GIT_MERGE_FIND_RENAMES` is enabled, added files will be compared
* with deleted files to determine their similarity. Files that are
* more similar than the rename threshold (percentage-wise) will be
* treated as a rename.
......@@ -255,11 +265,19 @@ typedef struct {
/** Pluggable similarity metric; pass NULL to use internal metric */
git_diff_similarity_metric *metric;
/**
* Maximum number of times to merge common ancestors to build a
* virtual merge base when faced with criss-cross merges. When this
* limit is reached, the next ancestor will simply be used instead of
* attempting to merge it. The default is unlimited.
*/
unsigned int recursion_limit;
/** Flags for handling conflicting content. */
git_merge_file_favor_t file_favor;
/** see `git_merge_file_flags_t` above */
unsigned int file_flags;
/** see `git_merge_file_flag_t` above */
git_merge_file_flag_t file_flags;
} git_merge_options;
#define GIT_MERGE_OPTIONS_VERSION 1
......
......@@ -7,12 +7,16 @@
#include "common.h"
#include "annotated_commit.h"
#include "refs.h"
#include "cache.h"
#include "git2/commit.h"
#include "git2/refs.h"
#include "git2/repository.h"
#include "git2/annotated_commit.h"
#include "git2/revparse.h"
#include "git2/tree.h"
#include "git2/index.h"
static int annotated_commit_init(
git_annotated_commit **out,
......@@ -22,14 +26,17 @@ static int annotated_commit_init(
const char *remote_url)
{
git_annotated_commit *annotated_commit;
git_commit *commit = NULL;
int error = 0;
assert(out && id);
*out = NULL;
annotated_commit = git__calloc(1, sizeof(git_annotated_commit));
GITERR_CHECK_ALLOC(annotated_commit);
if ((error = git_commit_lookup(&commit, repo, id)) < 0 ||
(error = git_annotated_commit_from_commit(&annotated_commit,
commit)) < 0)
goto done;
if (ref_name) {
annotated_commit->ref_name = git__strdup(ref_name);
......@@ -41,15 +48,10 @@ static int annotated_commit_init(
GITERR_CHECK_ALLOC(annotated_commit->remote_url);
}
git_oid_fmt(annotated_commit->id_str, id);
annotated_commit->id_str[GIT_OID_HEXSZ] = '\0';
if ((error = git_commit_lookup(&annotated_commit->commit, repo, id)) < 0) {
git_annotated_commit_free(annotated_commit);
return error;
}
*out = annotated_commit;
done:
git_commit_free(commit);
return error;
}
......@@ -75,6 +77,51 @@ int git_annotated_commit_from_ref(
return error;
}
int git_annotated_commit_from_head(
git_annotated_commit **out,
git_repository *repo)
{
git_reference *head;
int error;
assert(out && repo);
*out = NULL;
if ((error = git_reference_lookup(&head, repo, GIT_HEAD_FILE)) < 0)
return -1;
error = git_annotated_commit_from_ref(out, repo, head);
git_reference_free(head);
return error;
}
int git_annotated_commit_from_commit(
git_annotated_commit **out,
git_commit *commit)
{
git_annotated_commit *annotated_commit;
assert(out && commit);
*out = NULL;
annotated_commit = git__calloc(1, sizeof(git_annotated_commit));
GITERR_CHECK_ALLOC(annotated_commit);
annotated_commit->type = GIT_ANNOTATED_COMMIT_REAL;
git_cached_obj_incref(commit);
annotated_commit->commit = commit;
git_oid_fmt(annotated_commit->id_str, git_commit_id(commit));
annotated_commit->id_str[GIT_OID_HEXSZ] = '\0';
*out = annotated_commit;
return 0;
}
int git_annotated_commit_lookup(
git_annotated_commit **out,
git_repository *repo,
......@@ -136,14 +183,20 @@ void git_annotated_commit_free(git_annotated_commit *annotated_commit)
if (annotated_commit == NULL)
return;
if (annotated_commit->commit != NULL)
switch (annotated_commit->type) {
case GIT_ANNOTATED_COMMIT_REAL:
git_commit_free(annotated_commit->commit);
if (annotated_commit->ref_name != NULL)
git_tree_free(annotated_commit->tree);
git__free(annotated_commit->ref_name);
if (annotated_commit->remote_url != NULL)
git__free(annotated_commit->remote_url);
break;
case GIT_ANNOTATED_COMMIT_VIRTUAL:
git_index_free(annotated_commit->index);
git_array_clear(annotated_commit->parents);
break;
default:
abort();
}
git__free(annotated_commit);
}
......@@ -7,11 +7,31 @@
#ifndef INCLUDE_annotated_commit_h__
#define INCLUDE_annotated_commit_h__
#include "oidarray.h"
#include "git2/oid.h"
/** Internal structure for merge inputs */
typedef enum {
GIT_ANNOTATED_COMMIT_REAL = 1,
GIT_ANNOTATED_COMMIT_VIRTUAL = 2,
} git_annotated_commit_t;
/**
* Internal structure for merge inputs. An annotated commit is generally
* "real" and backed by an actual commit in the repository, but merge will
* internally create "virtual" commits that are in-memory intermediate
* commits backed by an index.
*/
struct git_annotated_commit {
git_annotated_commit_t type;
/* real commit */
git_commit *commit;
git_tree *tree;
/* virtual commit structure */
git_index *index;
git_array_oid_t parents;
char *ref_name;
char *remote_url;
......@@ -19,4 +39,9 @@ struct git_annotated_commit {
char id_str[GIT_OID_HEXSZ+1];
};
extern int git_annotated_commit_from_head(git_annotated_commit **out,
git_repository *repo);
extern int git_annotated_commit_from_commit(git_annotated_commit **out,
git_commit *commit);
#endif
......@@ -27,6 +27,8 @@
#include "config.h"
#include "oidarray.h"
#include "annotated_commit.h"
#include "commit.h"
#include "oidarray.h"
#include "git2/types.h"
#include "git2/repository.h"
......@@ -47,6 +49,19 @@
#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,
......@@ -799,11 +814,9 @@ static int merge_conflict_resolve_automerge(
int *resolved,
git_merge_diff_list *diff_list,
const git_merge_diff *conflict,
unsigned int merge_file_favor,
unsigned int file_flags)
const git_merge_file_options *file_opts)
{
const git_index_entry *ancestor = NULL, *ours = NULL, *theirs = NULL;
git_merge_file_options opts = GIT_MERGE_FILE_OPTIONS_INIT;
git_merge_file_result result = {0};
git_index_entry *index_entry;
git_odb *odb = NULL;
......@@ -850,12 +863,9 @@ static int merge_conflict_resolve_automerge(
theirs = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry) ?
&conflict->their_entry : NULL;
opts.favor = merge_file_favor;
opts.flags = file_flags;
if ((error = git_repository_odb(&odb, diff_list->repo)) < 0 ||
(error = git_merge_file_from_index(&result, diff_list->repo, ancestor, ours, theirs, &opts)) < 0 ||
!result.automergeable ||
(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;
......@@ -885,8 +895,7 @@ static int merge_conflict_resolve(
int *out,
git_merge_diff_list *diff_list,
const git_merge_diff *conflict,
unsigned int merge_file_favor,
unsigned int file_flags)
const git_merge_file_options *file_opts)
{
int resolved = 0;
int error = 0;
......@@ -902,8 +911,7 @@ static int merge_conflict_resolve(
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,
merge_file_favor, file_flags)) < 0)
if (!resolved && (error = merge_conflict_resolve_automerge(&resolved, diff_list, conflict, file_opts)) < 0)
goto done;
*out = resolved;
......@@ -1296,7 +1304,7 @@ int git_merge_diff_list__find_renames(
assert(diff_list && opts);
if ((opts->tree_flags & GIT_MERGE_TREE_FIND_RENAMES) == 0)
if ((opts->flags & GIT_MERGE_FIND_RENAMES) == 0)
return 0;
similarity_ours = git__calloc(diff_list->conflicts.length,
......@@ -1632,8 +1640,8 @@ static int merge_normalize_opts(
git_merge_options init = GIT_MERGE_OPTIONS_INIT;
memcpy(opts, &init, sizeof(init));
opts->tree_flags = GIT_MERGE_TREE_FIND_RENAMES;
opts->rename_threshold = GIT_MERGE_TREE_RENAME_THRESHOLD;
opts->flags = GIT_MERGE_FIND_RENAMES;
opts->rename_threshold = GIT_MERGE_DEFAULT_RENAME_THRESHOLD;
}
if (!opts->target_limit) {
......@@ -1643,7 +1651,7 @@ static int merge_normalize_opts(
limit = git_config__get_int_force(cfg, "diff.renamelimit", 0);
opts->target_limit = (limit <= 0) ?
GIT_MERGE_TREE_TARGET_LIMIT : (unsigned int)limit;
GIT_MERGE_DEFAULT_TARGET_LIMIT : (unsigned int)limit;
}
/* assign the internal metric with whitespace flag as payload */
......@@ -1827,6 +1835,7 @@ int git_merge__iterators(
*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;
......@@ -1842,6 +1851,17 @@ int git_merge__iterators(
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);
......@@ -1860,11 +1880,12 @@ int git_merge__iterators(
git_vector_foreach(&changes, i, conflict) {
int resolved = 0;
if ((error = merge_conflict_resolve(&resolved, diff_list, conflict, opts.file_favor, opts.file_flags)) < 0)
if ((error = merge_conflict_resolve(
&resolved, diff_list, conflict, &file_opts)) < 0)
goto done;
if (!resolved) {
if ((opts.tree_flags & GIT_MERGE_TREE_FAIL_ON_CONFLICT)) {
if ((opts.flags & GIT_MERGE_FAIL_ON_CONFLICT)) {
giterr_set(GITERR_MERGE, "merge conflicts exist");
error = GIT_EMERGECONFLICT;
goto done;
......@@ -1875,7 +1896,7 @@ int git_merge__iterators(
}
error = index_from_diff_list(out, diff_list,
(opts.tree_flags & GIT_MERGE_TREE_SKIP_REUC));
(opts.flags & GIT_MERGE_SKIP_REUC));
done:
if (!given_opts || !given_opts->metric)
......@@ -1922,6 +1943,207 @@ done:
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,
......@@ -1930,30 +2152,19 @@ int git_merge_commits(
const git_commit *their_commit,
const git_merge_options *opts)
{
git_oid ancestor_oid;
git_commit *ancestor_commit = NULL;
git_tree *our_tree = NULL, *their_tree = NULL, *ancestor_tree = NULL;
git_annotated_commit *ours = NULL, *theirs = NULL, *base = NULL;
int error = 0;
if ((error = git_merge_base(&ancestor_oid, repo, git_commit_id(our_commit), git_commit_id(their_commit))) < 0 &&
error == GIT_ENOTFOUND)
giterr_clear();
else if (error < 0 ||
(error = git_commit_lookup(&ancestor_commit, repo, &ancestor_oid)) < 0 ||
(error = git_commit_tree(&ancestor_tree, ancestor_commit)) < 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;
if ((error = git_commit_tree(&our_tree, our_commit)) < 0 ||
(error = git_commit_tree(&their_tree, their_commit)) < 0 ||
(error = git_merge_trees(out, repo, ancestor_tree, our_tree, their_tree, opts)) < 0)
goto done;
error = merge_annotated_commits(out, &base, repo, ours, theirs, 0, opts);
done:
git_commit_free(ancestor_commit);
git_tree_free(our_tree);
git_tree_free(their_tree);
git_tree_free(ancestor_tree);
git_annotated_commit_free(ours);
git_annotated_commit_free(theirs);
git_annotated_commit_free(base);
return error;
}
......@@ -2387,49 +2598,50 @@ const char *merge_their_label(const char *branchname)
}
static int merge_normalize_checkout_opts(
git_checkout_options *out,
git_repository *repo,
git_checkout_options *checkout_opts,
const git_checkout_options *given_checkout_opts,
const git_annotated_commit *ancestor_head,
unsigned int checkout_strategy,
git_annotated_commit *ancestor,
const git_annotated_commit *our_head,
size_t their_heads_len,
const git_annotated_commit **their_heads)
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(checkout_opts, given_checkout_opts, sizeof(git_checkout_options));
else {
git_checkout_options default_checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;
default_checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE;
memcpy(out, given_checkout_opts, sizeof(git_checkout_options));
else
memcpy(out, &default_checkout_opts, sizeof(git_checkout_options));
memcpy(checkout_opts, &default_checkout_opts, sizeof(git_checkout_options));
}
out->checkout_strategy = checkout_strategy;
/* TODO: for multiple ancestors in merge-recursive, this is "merged common ancestors" */
if (!checkout_opts->ancestor_label) {
if (ancestor_head && ancestor_head->commit)
checkout_opts->ancestor_label = git_commit_summary(ancestor_head->commit);
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
checkout_opts->ancestor_label = "ancestor";
out->ancestor_label = "empty base";
}
if (!checkout_opts->our_label) {
if (!out->our_label) {
if (our_head && our_head->ref_name)
checkout_opts->our_label = our_head->ref_name;
out->our_label = our_head->ref_name;
else
checkout_opts->our_label = "ours";
out->our_label = "ours";
}
if (!checkout_opts->their_label) {
if (!out->their_label) {
if (their_heads_len == 1 && their_heads[0]->ref_name)
checkout_opts->their_label = merge_their_label(their_heads[0]->ref_name);
out->their_label = merge_their_label(their_heads[0]->ref_name);
else if (their_heads_len == 1)
checkout_opts->their_label = their_heads[0]->id_str;
out->their_label = their_heads[0]->id_str;
else
checkout_opts->their_label = "theirs";
out->their_label = "theirs";
}
return error;
......@@ -2782,11 +2994,10 @@ int git_merge(
{
git_reference *our_ref = NULL;
git_checkout_options checkout_opts;
git_annotated_commit *ancestor_head = NULL, *our_head = NULL;
git_tree *ancestor_tree = NULL, *our_tree = NULL, **their_trees = NULL;
git_annotated_commit *our_head = NULL, *base = NULL;
git_index *index = NULL;
git_indexwriter indexwriter = GIT_INDEXWRITER_INIT;
size_t i;
unsigned int checkout_strategy;
int error = 0;
assert(repo && their_heads);
......@@ -2796,61 +3007,49 @@ int git_merge(
return -1;
}
their_trees = git__calloc(their_heads_len, sizeof(git_tree *));
GITERR_CHECK_ALLOC(their_trees);
if ((error = merge_heads(&ancestor_head, &our_head, repo, their_heads, their_heads_len)) < 0 ||
(error = merge_normalize_checkout_opts(repo, &checkout_opts, given_checkout_opts,
ancestor_head, our_head, their_heads_len, their_heads)) < 0 ||
(error = git_indexwriter_init_for_operation(&indexwriter, repo, &checkout_opts.checkout_strategy)) < 0)
goto on_error;
if ((error = git_repository__ensure_not_bare(repo, "merge")) < 0)
goto done;
/* Write the merge files to the repository. */
if ((error = git_merge__setup(repo, our_head, their_heads, their_heads_len)) < 0)
goto on_error;
checkout_strategy = given_checkout_opts ?
given_checkout_opts->checkout_strategy :
GIT_CHECKOUT_SAFE;
if (ancestor_head != NULL &&
(error = git_commit_tree(&ancestor_tree, ancestor_head->commit)) < 0)
goto on_error;
if ((error = git_commit_tree(&our_tree, our_head->commit)) < 0)
goto on_error;
if ((error = git_indexwriter_init_for_operation(&indexwriter, repo,
&checkout_strategy)) < 0)
goto done;
for (i = 0; i < their_heads_len; i++) {
if ((error = git_commit_tree(&their_trees[i], their_heads[i]->commit)) < 0)
goto on_error;
}
/* 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: recursive, octopus, etc... */
/* TODO: octopus */
if ((error = git_merge_trees(&index, repo, ancestor_tree, our_tree, their_trees[0], merge_opts)) < 0 ||
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 ||
(error = git_checkout_index(repo, index, &checkout_opts)) < 0 ||
(error = git_indexwriter_commit(&indexwriter)) < 0)
goto on_error;
(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;
on_error:
merge_state_cleanup(repo);
error = git_indexwriter_commit(&indexwriter);
done:
git_indexwriter_cleanup(&indexwriter);
if (error < 0)
merge_state_cleanup(repo);
git_indexwriter_cleanup(&indexwriter);
git_index_free(index);
git_tree_free(ancestor_tree);
git_tree_free(our_tree);
for (i = 0; i < their_heads_len; i++)
git_tree_free(their_trees[i]);
git__free(their_trees);
git_annotated_commit_free(our_head);
git_annotated_commit_free(ancestor_head);
git_annotated_commit_free(base);
git_reference_free(our_ref);
return error;
......
......@@ -19,8 +19,8 @@
#define GIT_MERGE_MODE_FILE "MERGE_MODE"
#define GIT_MERGE_FILE_MODE 0666
#define GIT_MERGE_TREE_RENAME_THRESHOLD 50
#define GIT_MERGE_TREE_TARGET_LIMIT 1000
#define GIT_MERGE_DEFAULT_RENAME_THRESHOLD 50
#define GIT_MERGE_DEFAULT_TARGET_LIMIT 1000
/** Types of changes when files are merged from branch to branch. */
typedef enum {
......
......@@ -300,7 +300,7 @@ void test_cherrypick_workdir__rename(void)
{ 0100644, "28d9eb4208074ad1cc84e71ccc908b34573f05d2", 0, "file3.txt.renamed" },
};
opts.merge_opts.tree_flags |= GIT_MERGE_TREE_FIND_RENAMES;
opts.merge_opts.flags |= GIT_MERGE_FIND_RENAMES;
opts.merge_opts.rename_threshold = 50;
git_oid_fromstr(&head_oid, "cfc4f0999a8367568e049af4f72e452d40828a15");
......@@ -335,7 +335,7 @@ void test_cherrypick_workdir__both_renamed(void)
{ 0100644, "28d9eb4208074ad1cc84e71ccc908b34573f05d2", 2, "file3.txt.renamed_on_branch" },
};
opts.merge_opts.tree_flags |= GIT_MERGE_TREE_FIND_RENAMES;
opts.merge_opts.flags |= GIT_MERGE_FIND_RENAMES;
opts.merge_opts.rename_threshold = 50;
git_oid_fromstr(&head_oid, "44cd2ed2052c9c68f9a439d208e9614dc2a55c70");
......
#define AUTOMERGEABLE_MERGED_FILE \
"this file is changed in master\n" \
"this file is automergeable\n" \
"this file is automergeable\n" \
"this file is automergeable\n" \
"this file is automergeable\n" \
"this file is automergeable\n" \
"this file is automergeable\n" \
"this file is automergeable\n" \
"this file is changed in branch\n"
#define AUTOMERGEABLE_MERGED_FILE_CRLF \
"this file is changed in master\r\n" \
"this file is automergeable\r\n" \
"this file is automergeable\r\n" \
"this file is automergeable\r\n" \
"this file is automergeable\r\n" \
"this file is automergeable\r\n" \
"this file is automergeable\r\n" \
"this file is automergeable\r\n" \
"this file is changed in branch\r\n"
#define CONFLICTING_MERGE_FILE \
"<<<<<<< HEAD\n" \
"this file is changed in master and branch\n" \
"=======\n" \
"this file is changed in branch and master\n" \
">>>>>>> 7cb63eed597130ba4abb87b3e544b85021905520\n"
#define CONFLICTING_DIFF3_FILE \
"<<<<<<< HEAD\n" \
"this file is changed in master and branch\n" \
"||||||| initial\n" \
"this file is a conflict\n" \
"=======\n" \
"this file is changed in branch and master\n" \
">>>>>>> 7cb63eed597130ba4abb87b3e544b85021905520\n"
#define CONFLICTING_UNION_FILE \
"this file is changed in master and branch\n" \
"this file is changed in branch and master\n"
#define CONFLICTING_RECURSIVE_F1_TO_F2 \
"VEAL SOUP.\n" \
"\n" \
"<<<<<<< HEAD\n" \
"PUT INTO A POT THREE QUARTS OF WATER, three onions cut small, ONE\n" \
"=======\n" \
"PUT INTO A POT THREE QUARTS OF WATER, three onions cut not too small, one\n" \
">>>>>>> branchF-2\n" \
"spoonful of black pepper pounded, and two of salt, with two or three\n" \
"slices of lean ham; let it boil steadily two hours; skim it\n" \
"occasionally, then put into it a shin of veal, let it boil two hours\n" \
"longer; take out the slices of ham, and skim off the grease if any\n" \
"should rise, take a gill of good cream, mix with it two table-spoonsful\n" \
"of flour very nicely, and the yelks of two eggs beaten well, strain this\n" \
"mixture, and add some chopped parsley; pour some soup on by degrees,\n" \
"stir it well, and pour it into the pot, continuing to stir until it has\n" \
"boiled two or three minutes to take off the raw taste of the eggs. If\n" \
"the cream be not perfectly sweet, and the eggs quite new, the thickening\n" \
"will curdle in the soup. For a change you may put a dozen ripe tomatos\n" \
"in, first taking off their skins, by letting them stand a few minutes in\n" \
"hot water, when they may be easily peeled. When made in this way you\n" \
"must thicken it with the flour only. Any part of the veal may be used,\n" \
"but the shin or knuckle is the nicest.\n" \
"\n" \
"<<<<<<< HEAD\n" \
"This certainly is a mighty fine recipe.\n" \
"=======\n" \
"This is a mighty fine recipe!\n" \
">>>>>>> branchF-2\n"
#define CONFLICTING_RECURSIVE_H1_TO_H2_WITH_DIFF3 \
"VEAL SOUP.\n" \
"\n" \
"<<<<<<< HEAD\n" \
"put into a pot three quarts of water, three onions cut small, one\n" \
"||||||| merged common ancestors\n" \
"<<<<<<< Temporary merge branch 1\n" \
"Put into a pot three quarts of water, THREE ONIONS CUT SMALL, one\n" \
"||||||| merged common ancestors\n" \
"Put into a pot three quarts of water, three onions cut small, one\n" \
"=======\n" \
"PUT INTO A POT three quarts of water, three onions cut small, one\n" \
">>>>>>> Temporary merge branch 2\n" \
"=======\n" \
"Put Into A Pot Three Quarts of Water, Three Onions Cut Small, One\n" \
">>>>>>> branchH-2\n" \
"spoonful of black pepper pounded, and two of salt, with two or three\n" \
"slices of lean ham; let it boil steadily two hours; skim it\n" \
"occasionally, then put into it a shin of veal, let it boil two hours\n" \
"longer; take out the slices of ham, and skim off the grease if any\n" \
"should rise, take a gill of good cream, mix with it two table-spoonsful\n" \
"of flour very nicely, and the yelks of two eggs beaten well, strain this\n" \
"mixture, and add some chopped parsley; pour some soup on by degrees,\n" \
"stir it well, and pour it into the pot, continuing to stir until it has\n" \
"boiled two or three minutes to take off the raw taste of the eggs. If\n" \
"the cream be not perfectly sweet, and the eggs quite new, the thickening\n" \
"will curdle in the soup. For a change you may put a dozen ripe tomatos\n" \
"in, first taking off their skins, by letting them stand a few minutes in\n" \
"hot water, when they may be easily peeled. When made in this way you\n" \
"must thicken it with the flour only. Any part of the veal may be used,\n" \
"but the shin or knuckle is the nicest.\n"
......@@ -4,6 +4,7 @@
#include "buffer.h"
#include "merge.h"
#include "merge_helpers.h"
#include "conflict_data.h"
#include "refs.h"
#include "fileops.h"
#include "diff_xdiff.h"
......
......@@ -4,6 +4,7 @@
#include "tree.h"
#include "merge_helpers.h"
#include "merge.h"
#include "index.h"
#include "git2/merge.h"
#include "git2/sys/index.h"
#include "git2/annotated_commit.h"
......@@ -239,7 +240,7 @@ int merge_test_index(git_index *index, const struct merge_index_entry expected[]
const git_index_entry *index_entry;
/*
dump_index_entries(&index->entries);
merge__dump_index_entries(&index->entries);
*/
if (git_index_entrycount(index) != expected_len)
......
......@@ -4,49 +4,6 @@
#include "merge.h"
#include "git2/merge.h"
#define AUTOMERGEABLE_MERGED_FILE \
"this file is changed in master\n" \
"this file is automergeable\n" \
"this file is automergeable\n" \
"this file is automergeable\n" \
"this file is automergeable\n" \
"this file is automergeable\n" \
"this file is automergeable\n" \
"this file is automergeable\n" \
"this file is changed in branch\n"
#define AUTOMERGEABLE_MERGED_FILE_CRLF \
"this file is changed in master\r\n" \
"this file is automergeable\r\n" \
"this file is automergeable\r\n" \
"this file is automergeable\r\n" \
"this file is automergeable\r\n" \
"this file is automergeable\r\n" \
"this file is automergeable\r\n" \
"this file is automergeable\r\n" \
"this file is changed in branch\r\n"
#define CONFLICTING_MERGE_FILE \
"<<<<<<< HEAD\n" \
"this file is changed in master and branch\n" \
"=======\n" \
"this file is changed in branch and master\n" \
">>>>>>> 7cb63eed597130ba4abb87b3e544b85021905520\n"
#define CONFLICTING_DIFF3_FILE \
"<<<<<<< HEAD\n" \
"this file is changed in master and branch\n" \
"||||||| initial\n" \
"this file is a conflict\n" \
"=======\n" \
"this file is changed in branch and master\n" \
">>>>>>> 7cb63eed597130ba4abb87b3e544b85021905520\n"
#define CONFLICTING_UNION_FILE \
"this file is changed in master and branch\n" \
"this file is changed in branch and master\n"
struct merge_index_entry {
uint16_t mode;
char oid_str[GIT_OID_HEXSZ+1];
......
......@@ -3,8 +3,9 @@
#include "git2/merge.h"
#include "buffer.h"
#include "merge.h"
#include "../merge_helpers.h"
#include "fileops.h"
#include "../merge_helpers.h"
#include "../conflict_data.h"
static git_repository *repo;
......
......@@ -3,6 +3,7 @@
#include "git2/merge.h"
#include "merge.h"
#include "../merge_helpers.h"
#include "../conflict_data.h"
static git_repository *repo;
......@@ -134,7 +135,7 @@ void test_merge_trees_commits__fail_on_conflict(void)
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
opts.tree_flags |= GIT_MERGE_TREE_FAIL_ON_CONFLICT;
opts.flags |= GIT_MERGE_FAIL_ON_CONFLICT;
cl_git_fail_with(GIT_EMERGECONFLICT,
merge_trees_from_branches(&index, repo, "df_side1", "df_side2", &opts));
......
#include "clar_libgit2.h"
#include "git2/repository.h"
#include "git2/merge.h"
#include "merge.h"
#include "../merge_helpers.h"
static git_repository *repo;
#define TEST_REPO_PATH "merge-recursive"
void test_merge_trees_recursive__initialize(void)
{
repo = cl_git_sandbox_init(TEST_REPO_PATH);
}
void test_merge_trees_recursive__cleanup(void)
{
cl_git_sandbox_cleanup();
}
void test_merge_trees_recursive__one_base_commit(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "dea7215f259b2cced87d1bda6c72f8b4ce37a2ff", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" },
{ 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" },
{ 0100644, "94d2c01087f48213bd157222d54edfefd77c9bba", 0, "veal.txt" },
};
cl_git_pass(merge_commits_from_branches(&index, repo, "branchA-1", "branchA-2", &opts));
cl_assert(merge_test_index(index, merge_index_entries, 6));
git_index_free(index);
}
void test_merge_trees_recursive__one_base_commit_norecursive(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "dea7215f259b2cced87d1bda6c72f8b4ce37a2ff", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" },
{ 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" },
{ 0100644, "94d2c01087f48213bd157222d54edfefd77c9bba", 0, "veal.txt" },
};
opts.flags |= GIT_MERGE_NO_RECURSIVE;
cl_git_pass(merge_commits_from_branches(&index, repo, "branchA-1", "branchA-2", &opts));
cl_assert(merge_test_index(index, merge_index_entries, 6));
git_index_free(index);
}
void test_merge_trees_recursive__two_base_commits(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" },
{ 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" },
{ 0100644, "666ffdfcf1eaa5641fa31064bf2607327e843c09", 0, "veal.txt" },
};
cl_git_pass(merge_commits_from_branches(&index, repo, "branchB-1", "branchB-2", &opts));
cl_assert(merge_test_index(index, merge_index_entries, 6));
git_index_free(index);
}
void test_merge_trees_recursive__two_base_commits_norecursive(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" },
{ 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" },
{ 0100644, "cb49ad76147f5f9439cbd6133708b76142660660", 1, "veal.txt" },
{ 0100644, "b2a81ead9e722af0099fccfb478cea88eea749a2", 2, "veal.txt" },
{ 0100644, "4e21d2d63357bde5027d1625f5ec6b430cdeb143", 3, "veal.txt" },
};
opts.flags |= GIT_MERGE_NO_RECURSIVE;
cl_git_pass(merge_commits_from_branches(&index, repo, "branchB-1", "branchB-2", &opts));
cl_assert(merge_test_index(index, merge_index_entries, 8));
git_index_free(index);
}
void test_merge_trees_recursive__two_levels_of_multiple_bases(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" },
{ 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" },
{ 0100644, "15faa0c9991f2d65686e844651faa2ff9827887b", 0, "veal.txt" },
};
cl_git_pass(merge_commits_from_branches(&index, repo, "branchC-1", "branchC-2", &opts));
cl_assert(merge_test_index(index, merge_index_entries, 6));
git_index_free(index);
}
void test_merge_trees_recursive__two_levels_of_multiple_bases_norecursive(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" },
{ 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" },
{ 0100644, "b2a81ead9e722af0099fccfb478cea88eea749a2", 1, "veal.txt" },
{ 0100644, "898d12687fb35be271c27c795a6b32c8b51da79e", 2, "veal.txt" },
{ 0100644, "68a2e1ee61a23a4728fe6b35580fbbbf729df370", 3, "veal.txt" },
};
opts.flags |= GIT_MERGE_NO_RECURSIVE;
cl_git_pass(merge_commits_from_branches(&index, repo, "branchC-1", "branchC-2", &opts));
cl_assert(merge_test_index(index, merge_index_entries, 8));
git_index_free(index);
}
void test_merge_trees_recursive__three_levels_of_multiple_bases(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" },
{ 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" },
{ 0100644, "d55e5dc038c52f1a36548625bcb666cbc06db9e6", 0, "veal.txt" },
};
cl_git_pass(merge_commits_from_branches(&index, repo, "branchD-2", "branchD-1", &opts));
cl_assert(merge_test_index(index, merge_index_entries, 6));
git_index_free(index);
}
void test_merge_trees_recursive__three_levels_of_multiple_bases_norecursive(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" },
{ 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" },
{ 0100644, "898d12687fb35be271c27c795a6b32c8b51da79e", 1, "veal.txt" },
{ 0100644, "f1b44c04989a3a1c14b036cfadfa328d53a7bc5e", 2, "veal.txt" },
{ 0100644, "5e8747f5200fac0f945a07daf6163ca9cb1a8da9", 3, "veal.txt" },
};
opts.flags |= GIT_MERGE_NO_RECURSIVE;
cl_git_pass(merge_commits_from_branches(&index, repo, "branchD-2", "branchD-1", &opts));
cl_assert(merge_test_index(index, merge_index_entries, 8));
git_index_free(index);
}
void test_merge_trees_recursive__three_base_commits(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "c4f7269b07c76d02755d75ccaf05c0b4c36cdc6c", 0, "gravy.txt" },
{ 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" },
{ 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" },
};
cl_git_pass(merge_commits_from_branches(&index, repo, "branchE-1", "branchE-2", &opts));
cl_assert(merge_test_index(index, merge_index_entries, 6));
git_index_free(index);
}
void test_merge_trees_recursive__three_base_commits_norecursive(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "9e12bce04446d097ae1782967a5888c2e2a0d35b", 1, "gravy.txt" },
{ 0100644, "d8dd349b78f19a4ebe3357bacb8138f00bf5ed41", 2, "gravy.txt" },
{ 0100644, "e50fbbd701458757bdfe9815f58ed717c588d1b5", 3, "gravy.txt" },
{ 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" },
{ 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" },
};
opts.flags |= GIT_MERGE_NO_RECURSIVE;
cl_git_pass(merge_commits_from_branches(&index, repo, "branchE-1", "branchE-2", &opts));
cl_assert(merge_test_index(index, merge_index_entries, 8));
git_index_free(index);
}
void test_merge_trees_recursive__conflict(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" },
{ 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" },
{ 0100644, "fa567f568ed72157c0c617438d077695b99d9aac", 1, "veal.txt" },
{ 0100644, "21950d5e4e4d1a871b4dfcf72ecb6b9c162c434e", 2, "veal.txt" },
{ 0100644, "3855170cef875708da06ab9ad7fc6a73b531cda1", 3, "veal.txt" },
};
cl_git_pass(merge_commits_from_branches(&index, repo, "branchF-1", "branchF-2", &opts));
cl_assert(merge_test_index(index, merge_index_entries, 8));
git_index_free(index);
}
/*
* Branch G-1 and G-2 have three common ancestors (815b5a1, ad2ace9, 483065d).
* The merge-base of the first two has two common ancestors (723181f, a34e5a1)
* which themselves have two common ancestors (8f35f30, 3a3f5a6), which
* finally has a common ancestor of 7c7bf85. This virtual merge base will
* be computed and merged with 483065d which also has a common ancestor of
* 7c7bf85.
*/
void test_merge_trees_recursive__oh_so_many_levels_of_recursion(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" },
{ 0100644, "7c7e08f9559d9e1551b91e1cf68f1d0066109add", 0, "oyster.txt" },
{ 0100644, "898d12687fb35be271c27c795a6b32c8b51da79e", 0, "veal.txt" },
};
cl_git_pass(merge_commits_from_branches(&index, repo, "branchG-1", "branchG-2", &opts));
cl_assert(merge_test_index(index, merge_index_entries, 6));
git_index_free(index);
}
/* Branch H-1 and H-2 have two common ancestors (aa9e263, 6ef31d3). The two
* ancestors themselves conflict.
*/
void test_merge_trees_recursive__conflicting_merge_base(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" },
{ 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" },
{ 0100644, "3a66812fed1e03ea4a6a7ee28d8a57aec1ca6537", 1, "veal.txt" },
{ 0100644, "d604c75019c282144bdbbf3fd3462ba74b240efc", 2, "veal.txt" },
{ 0100644, "37a5054a9f9b4628e3924c5cb8f2147c6e2a3efc", 3, "veal.txt" },
};
cl_git_pass(merge_commits_from_branches(&index, repo, "branchH-1", "branchH-2", &opts));
cl_assert(merge_test_index(index, merge_index_entries, 8));
git_index_free(index);
}
/* Branch H-1 and H-2 have two common ancestors (aa9e263, 6ef31d3). The two
* ancestors themselves conflict. The generated common ancestor file will
* have diff3 style conflicts inside it.
*/
void test_merge_trees_recursive__conflicting_merge_base_with_diff3(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" },
{ 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" },
{ 0100644, "cd17a91513f3aee9e44114d1ede67932dd41d2fc", 1, "veal.txt" },
{ 0100644, "d604c75019c282144bdbbf3fd3462ba74b240efc", 2, "veal.txt" },
{ 0100644, "37a5054a9f9b4628e3924c5cb8f2147c6e2a3efc", 3, "veal.txt" },
};
opts.file_flags |= GIT_MERGE_FILE_STYLE_DIFF3;
cl_git_pass(merge_commits_from_branches(&index, repo, "branchH-1", "branchH-2", &opts));
cl_assert(merge_test_index(index, merge_index_entries, 8));
git_index_free(index);
}
/* Branch I-1 and I-2 have two common ancestors (aa9e263, 6ef31d3). The two
* ancestors themselves conflict, but when each was merged, the conflicts were
* resolved identically, thus merging I-1 into I-2 does not conflict.
*/
void test_merge_trees_recursive__conflicting_merge_base_since_resolved(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" },
{ 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" },
{ 0100644, "a02d4fd126e0cc8fb46ee48cf38bad36d44f2dbc", 0, "veal.txt" },
};
cl_git_pass(merge_commits_from_branches(&index, repo, "branchI-1", "branchI-2", &opts));
cl_assert(merge_test_index(index, merge_index_entries, 6));
git_index_free(index);
}
/* There are multiple levels of criss-cross merges, and multiple recursive
* merges would create a common ancestor that allows the merge to complete
* successfully. Test that we can build a single virtual base, then stop,
* which will produce a conflicting merge.
*/
void test_merge_trees_recursive__recursionlimit(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "ce7e553c6feb6e5f3bd67e3c3be04182fe3094b4", 1, "gravy.txt" },
{ 0100644, "d8dd349b78f19a4ebe3357bacb8138f00bf5ed41", 2, "gravy.txt" },
{ 0100644, "e50fbbd701458757bdfe9815f58ed717c588d1b5", 3, "gravy.txt" },
{ 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" },
{ 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" },
};
opts.recursion_limit = 1;
cl_git_pass(merge_commits_from_branches(&index, repo, "branchE-1", "branchE-2", &opts));
cl_assert(merge_test_index(index, merge_index_entries, 8));
git_index_free(index);
}
......@@ -47,7 +47,7 @@ static void test_find_differences(
git_iterator_options iter_opts = GIT_ITERATOR_OPTIONS_INIT;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
opts.tree_flags |= GIT_MERGE_TREE_FIND_RENAMES;
opts.flags |= GIT_MERGE_FIND_RENAMES;
opts.target_limit = 1000;
opts.rename_threshold = 50;
......
#include "clar_libgit2.h"
#include "git2/repository.h"
#include "git2/merge.h"
#include "merge.h"
#include "../merge_helpers.h"
#include "../conflict_data.h"
static git_repository *repo;
#define TEST_REPO_PATH "merge-recursive"
void test_merge_workdir_recursive__initialize(void)
{
repo = cl_git_sandbox_init(TEST_REPO_PATH);
}
void test_merge_workdir_recursive__cleanup(void)
{
cl_git_sandbox_cleanup();
}
void test_merge_workdir_recursive__writes_conflict_with_virtual_base(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
git_buf conflicting_buf = GIT_BUF_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" },
{ 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" },
{ 0100644, "fa567f568ed72157c0c617438d077695b99d9aac", 1, "veal.txt" },
{ 0100644, "21950d5e4e4d1a871b4dfcf72ecb6b9c162c434e", 2, "veal.txt" },
{ 0100644, "3855170cef875708da06ab9ad7fc6a73b531cda1", 3, "veal.txt" },
};
cl_git_pass(merge_branches(repo, GIT_REFS_HEADS_DIR "branchF-1", GIT_REFS_HEADS_DIR "branchF-2", &opts, NULL));
cl_git_pass(git_repository_index(&index, repo));
cl_assert(merge_test_index(index, merge_index_entries, 8));
cl_git_pass(git_futils_readbuffer(&conflicting_buf, "merge-recursive/veal.txt"));
cl_assert_equal_s(CONFLICTING_RECURSIVE_F1_TO_F2, conflicting_buf.ptr);
git_index_free(index);
git_buf_free(&conflicting_buf);
}
void test_merge_workdir_recursive__conflicting_merge_base_with_diff3(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;
git_buf conflicting_buf = GIT_BUF_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" },
{ 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" },
{ 0100644, "cd17a91513f3aee9e44114d1ede67932dd41d2fc", 1, "veal.txt" },
{ 0100644, "d604c75019c282144bdbbf3fd3462ba74b240efc", 2, "veal.txt" },
{ 0100644, "37a5054a9f9b4628e3924c5cb8f2147c6e2a3efc", 3, "veal.txt" },
};
opts.file_flags |= GIT_MERGE_FILE_STYLE_DIFF3;
checkout_opts.checkout_strategy |= GIT_CHECKOUT_CONFLICT_STYLE_DIFF3;
cl_git_pass(merge_branches(repo, GIT_REFS_HEADS_DIR "branchH-1", GIT_REFS_HEADS_DIR "branchH-2", &opts, &checkout_opts));
cl_git_pass(git_repository_index(&index, repo));
cl_assert(merge_test_index(index, merge_index_entries, 8));
cl_git_pass(git_futils_readbuffer(&conflicting_buf, "merge-recursive/veal.txt"));
cl_assert_equal_s(CONFLICTING_RECURSIVE_H1_TO_H2_WITH_DIFF3, conflicting_buf.ptr);
git_index_free(index);
git_buf_free(&conflicting_buf);
}
......@@ -63,7 +63,7 @@ void test_merge_workdir_renames__renames(void)
{ 0100644, "b69fe837e4cecfd4c9a40cdca7c138468687df07", 0, "7-both-renamed.txt~rename_conflict_theirs" },
};
merge_opts.tree_flags |= GIT_MERGE_TREE_FIND_RENAMES;
merge_opts.flags |= GIT_MERGE_FIND_RENAMES;
merge_opts.rename_threshold = 50;
cl_git_pass(merge_branches(repo, GIT_REFS_HEADS_DIR BRANCH_RENAME_OURS, GIT_REFS_HEADS_DIR BRANCH_RENAME_THEIRS, &merge_opts, NULL));
......@@ -99,7 +99,7 @@ void test_merge_workdir_renames__ours(void)
{ 0100644, "b42712cfe99a1a500b2a51fe984e0b8a7702ba11", 0, "7-both-renamed.txt" },
};
merge_opts.tree_flags |= GIT_MERGE_TREE_FIND_RENAMES;
merge_opts.flags |= GIT_MERGE_FIND_RENAMES;
merge_opts.rename_threshold = 50;
checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_USE_OURS;
......@@ -147,7 +147,7 @@ void test_merge_workdir_renames__similar(void)
{ 0100644, "b69fe837e4cecfd4c9a40cdca7c138468687df07", 0, "7-both-renamed.txt~rename_conflict_theirs" },
};
merge_opts.tree_flags |= GIT_MERGE_TREE_FIND_RENAMES;
merge_opts.flags |= GIT_MERGE_FIND_RENAMES;
merge_opts.rename_threshold = 50;
cl_git_pass(merge_branches(repo, GIT_REFS_HEADS_DIR BRANCH_RENAME_OURS, GIT_REFS_HEADS_DIR BRANCH_RENAME_THEIRS, &merge_opts, NULL));
......
......@@ -4,6 +4,7 @@
#include "buffer.h"
#include "merge.h"
#include "../merge_helpers.h"
#include "../conflict_data.h"
#include "refs.h"
#include "fileops.h"
......
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
7c7bf85e978f1d18c0566f702d2cb7766b9c8d4f refs/heads/master
ASPARAGUS SOUP.
Take four large bunches of asparagus, scrape it nicely, cut off one inch
of the tops, and lay them in water, chop the stalks and put them on the
fire with a piece of bacon, a large onion cut up, and pepper and salt;
add two quarts of water, boil them till the stalks are quite soft, then
pulp them through a sieve, and strain the water to it, which must be put
back in the pot; put into it a chicken cut up, with the tops of
asparagus which had been laid by, boil it until these last articles are
sufficiently done, thicken with flour, butter and milk, and serve it up.
BEEF SOUP.
Take the hind shin of beef, cut off all the flesh off the leg-bone,
which must be taken away entirely, or the soup will be greasy. Wash the
meat clean and lay it in a pot, sprinkle over it one small
table-spoonful of pounded black pepper, and two of salt; three onions
the size of a hen's egg, cut small, six small carrots scraped and cut
up, two small turnips pared and cut into dice; pour on three quarts of
water, cover the pot close, and keep it gently and steadily boiling five
hours, which will leave about three pints of clear soup; do not let the
pot boil over, but take off the scum carefully, as it rises. When it has
boiled four hours, put in a small bundle of thyme and parsley, and a
pint of celery cut small, or a tea-spoonful of celery seed pounded.
These latter ingredients would lose their delicate flavour if boiled too
much. Just before you take it up, brown it in the following manner: put
a small table-spoonful of nice brown sugar into an iron skillet, set it
on the fire and stir it till it melts and looks very dark, pour into it
a ladle full of the soup, a little at a time; stirring it all the while.
Strain this browning and mix it well with the soup; take out the bundle
of thyme and parsley, put the nicest pieces of meat in your tureen, and
pour on the soup and vegetables; put in some toasted bread cut in dice,
and serve it up.
SOUP WITH BOUILLI.
Take the nicest part of the thick brisket of beef, about eight pounds,
put it into a pot with every thing directed for the other soup; make it
exactly in the same way, only put it on an hour sooner, that you may
have time to prepare the bouilli; after it has boiled five hours, take
out the beef, cover up the soup and set it near the fire that it may
keep hot. Take the skin off the beef, have the yelk of an egg well
beaten, dip a feather in it and wash the top of your beef, sprinkle over
it the crumb of stale bread finely grated, put it in a Dutch oven
previously heated, put the top on with coals enough to brown, but not
burn the beef; let it stand nearly an hour, and prepare your gravy
thus:--Take a sufficient quantity of soup and the vegetables boiled in
it; add to it a table-spoonful of red wine, and two of mushroom catsup,
thicken with a little bit of butter and a little brown flour; make it
very hot, pour it in your dish, and put the beef on it. Garnish it with
green pickle, cut in thin slices, serve up the soup in a tureen with
bits of toasted bread.
GRAVY SOUP.
Get eight pounds of coarse lean beef--wash it clean and lay it in your
pot, put in the same ingredients as for the shin soup, with the same
quantity of water, and follow the process directed for that. Strain the
soup through a sieve, and serve it up clear, with nothing more than
toasted bread in it; two table-spoonsful of mushroom catsup will add a
fine flavour to the soup.
OYSTER SOUP!
Wash and drain two quarts of oysters, put them on with three quarts of
water, three onions chopped up, two or three slices of lean ham, pepper
and salt; boil it till reduced one-half, strain it through a sieve,
return the liquid into the pot, put in one quart of fresh oysters, boil
it till they are sufficiently done, and thicken the soup with four
spoonsful of flour, two gills of rich cream, and the yelks of six new
laid eggs beaten well; boil it a few minutes after the thickening is put
in. Take care that it does not curdle, and that the flour is not in
lumps; serve it up with the last oysters that were put in. If the
flavour of thyme be agreeable, you may put in a little, but take care
that it does not boil in it long enough to discolour the soup.
VEAL SOUP.
PUT INTO A POT THREE QUARTS OF WATER, 3 onions cut small, ONE
spoonful of black pepper pounded, and two of salt, with two or three
slices of lean ham; let it boil steadily two hours; skim it
occasionally, then put into it a shin of veal, let it boil two hours
longer; take out the slices of ham, and skim off the grease if any
should rise, take a gill of good cream, mix with it two table-spoonsful
of flour very nicely, and the yelks of two eggs beaten well, strain this
mixture, and add some chopped parsley; pour some soup on by degrees,
stir it well, and pour it into the pot, continuing to stir until it has
boiled two or three minutes to take off the raw taste of the eggs. If
the cream be not perfectly sweet, and the eggs quite new, the thickening
will curdle in the soup. For a change you may put a dozen ripe tomatos
in, first taking off their skins, by letting them stand a few minutes in
hot water, when they may be easily peeled. When made in this way you
must thicken it with the flour only. Any part of the veal may be used,
but the shin or knuckle is the nicest.
This is a mighty fine recipe!
......@@ -410,7 +410,7 @@ void test_revert_workdir__rename_1_of_2(void)
{ 0100644, "0f5bfcf58c558d865da6be0281d7795993646cee", 2, "file6.txt" },
};
opts.merge_opts.tree_flags |= GIT_MERGE_TREE_FIND_RENAMES;
opts.merge_opts.flags |= GIT_MERGE_FIND_RENAMES;
opts.merge_opts.rename_threshold = 50;
git_oid_fromstr(&head_oid, "cef56612d71a6af8d8015691e4865f7fece905b5");
......@@ -444,7 +444,7 @@ void test_revert_workdir__rename(void)
{ "file4.txt", "file5.txt", "" },
};
opts.merge_opts.tree_flags |= GIT_MERGE_TREE_FIND_RENAMES;
opts.merge_opts.flags |= GIT_MERGE_FIND_RENAMES;
opts.merge_opts.rename_threshold = 50;
git_oid_fromstr(&head_oid, "55568c8de5322ff9a95d72747a239cdb64a19965");
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment