From 05d47768caf6fec51fa85cb6275c9ba8324aead6 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Mon, 10 Mar 2014 22:30:41 -0800 Subject: [PATCH] Introduce git_merge_file for consumers --- include/git2/merge.h | 169 ++++++++++++++++++++++ src/checkout.c | 43 +++--- src/index.c | 8 +- src/index.h | 2 + src/merge.c | 55 +++++-- src/merge_file.c | 263 +++++++++++++++++++++++----------- src/merge_file.h | 78 ---------- tests/merge/files.c | 175 ++++++++++++++++++++++ tests/merge/merge_helpers.h | 43 ++++++ tests/merge/trees/automerge.c | 22 --- tests/merge/trees/commits.c | 11 -- tests/merge/workdir/simple.c | 41 ------ tests/structinit/structinit.c | 10 ++ 13 files changed, 640 insertions(+), 280 deletions(-) create mode 100644 tests/merge/files.c diff --git a/include/git2/merge.h b/include/git2/merge.h index cfec32f4d..38e3408cc 100644 --- a/include/git2/merge.h +++ b/include/git2/merge.h @@ -22,6 +22,43 @@ */ GIT_BEGIN_DECL +/** + * The file inputs to `git_merge_file`. Callers should populate the + * `git_merge_file_input` structure with descriptions of the files in + * each side of the conflict for use in producing the merge file. + */ +typedef struct { + unsigned int version; + + /** Pointer to the contents of the file. */ + const char *ptr; + + /** Size of the contents pointed to in `ptr`. */ + size_t size; + + /** File name of the conflicted file, or `NULL` to not merge the path. */ + const char *path; + + /** File mode of the conflicted file, or `0` to not merge the mode. */ + unsigned int mode; +} git_merge_file_input; + +#define GIT_MERGE_FILE_INPUT_VERSION 1 +#define GIT_MERGE_FILE_INPUT_INIT {GIT_MERGE_FILE_INPUT_VERSION} + +/** + * Initializes a `git_merge_file_input` with default values. Equivalent to + * creating an instance with GIT_MERGE_FILE_INPUT_INIT. + * + * @param opts the `git_merge_file_input` instance to initialize. + * @param version the version of the struct; you should pass + * `GIT_MERGE_FILE_INPUT_VERSION` here. + * @return Zero on success; -1 on failure. + */ +GIT_EXTERN(int) git_merge_file_init_input( + git_merge_file_input *opts, + int version); + /** * Flags for `git_merge_tree` options. A combination of these flags can be * passed in via the `flags` value in the `git_merge_tree_opts`. @@ -71,6 +108,86 @@ typedef enum { GIT_MERGE_FILE_FAVOR_UNION = 3, } git_merge_file_favor_t; +typedef enum { + /* Defaults */ + GIT_MERGE_FILE_DEFAULT = 0, + + /* Create standard conflicted merge files */ + GIT_MERGE_FILE_STYLE_MERGE = (1 << 0), + + /* Create diff3-style files */ + GIT_MERGE_FILE_STYLE_DIFF3 = (1 << 1), + + /* Condense non-alphanumeric regions for simplified diff file */ + GIT_MERGE_FILE_SIMPLIFY_ALNUM = (1 << 2), +} git_merge_file_flags_t; + +typedef struct { + unsigned int version; + + /** + * Label for the ancestor file side of the conflict which will be prepended + * to labels in diff3-format merge files. + */ + const char *ancestor_label; + + /** + * Label for our file side of the conflict which will be prepended + * to labels in merge files. + */ + const char *our_label; + + /** + * Label for their file side of the conflict which will be prepended + * to labels in merge files. + */ + const char *their_label; + + /** The file to favor in region conflicts. */ + git_merge_file_favor_t favor; + + /** Merge file flags. */ + git_merge_file_flags_t flags; +} git_merge_file_options; + +#define GIT_MERGE_FILE_OPTIONS_VERSION 1 +#define GIT_MERGE_FILE_OPTIONS_INIT {GIT_MERGE_FILE_OPTIONS_VERSION} + +/** + * Initializes a `git_merge_file_options` with default values. Equivalent to + * creating an instance with GIT_MERGE_FILE_OPTIONS_INIT. + * + * @param opts the `git_merge_file_options` instance to initialize. + * @param version the version of the struct; you should pass + * `GIT_MERGE_FILE_OPTIONS_VERSION` here. + * @return Zero on success; -1 on failure. + */ +GIT_EXTERN(int) git_merge_file_init_options( + git_merge_file_options *opts, + int version); + +typedef struct { + /** + * True if the output was automerged, false if the output contains + * conflict markers. + */ + unsigned int automergeable; + + /** + * The path that the resultant merge file should use, or NULL if a + * filename conflict would occur. + */ + char *path; + + /** The mode that the resultant merge file should use. */ + unsigned int mode; + + /** The contents of the merge. */ + unsigned char *ptr; + + /** The length of the merge contents. */ + size_t len; +} git_merge_file_result; typedef struct { unsigned int version; @@ -268,6 +385,58 @@ GIT_EXTERN(int) git_merge_head_from_id( GIT_EXTERN(void) git_merge_head_free( git_merge_head *head); +/** + * Merge two files as they exist in the in-memory data structures, using + * the given common ancestor as the baseline, producing a + * `git_merge_file_result` that reflects the merge result. The + * `git_merge_file_result` must be freed with `git_merge_file_result_free`. + * + * Note that this function does not reference a repository and any + * configuration must be passed as `git_merge_file_options`. + * + * @param out The git_merge_file_result to be filled in + * @param ancestor The contents of the ancestor file + * @param ours The contents of the file in "our" side + * @param theirs The contents of the file in "their" side + * @param opts The merge file options or `NULL` for defaults + * @return 0 on success or error code + */ +GIT_EXTERN(int) git_merge_file( + git_merge_file_result *out, + const git_merge_file_input *ancestor, + const git_merge_file_input *ours, + const git_merge_file_input *theirs, + const git_merge_file_options *opts); + +/** + * Merge two files as they exist in the index, using the given common + * ancestor as the baseline, producing a `git_merge_file_result` that + * reflects the merge result. The `git_merge_file_result` must be freed with + * `git_merge_file_result_free`. + * + * @param out The git_merge_file_result to be filled in + * @param repo The repository + * @param ancestor The index entry for the ancestor file (stage level 1) + * @param our_path The index entry for our file (stage level 2) + * @param their_path The index entry for their file (stage level 3) + * @param opts The merge file options or NULL + * @return 0 on success or error code + */ +GIT_EXTERN(int) git_merge_file_from_index( + git_merge_file_result *out, + git_repository *repo, + const git_index_entry *ancestor, + const git_index_entry *ours, + const git_index_entry *theirs, + const git_merge_file_options *opts); + +/** + * Frees a `git_merge_file_result`. + * + * @param result The result to free or `NULL` + */ +GIT_EXTERN(void) git_merge_file_result_free(git_merge_file_result *result); + /** * Merge two trees, producing a `git_index` that reflects the result of * the merge. The index may be written as-is to the working directory diff --git a/src/checkout.c b/src/checkout.c index 5dd4ec71c..f882f3593 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -1681,29 +1681,20 @@ static int checkout_write_merge( { git_buf our_label = GIT_BUF_INIT, their_label = GIT_BUF_INIT, path_suffixed = GIT_BUF_INIT, path_workdir = GIT_BUF_INIT; - git_merge_file_options merge_file_opts = GIT_MERGE_FILE_OPTIONS_INIT; - 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_merge_file_options opts = GIT_MERGE_FILE_OPTIONS_INIT; + git_merge_file_result result = {0}; git_filebuf output = GIT_FILEBUF_INIT; int error = 0; if (data->opts.checkout_strategy & GIT_CHECKOUT_CONFLICT_STYLE_DIFF3) - merge_file_opts.style = GIT_MERGE_FILE_STYLE_DIFF3; + opts.flags |= GIT_MERGE_FILE_STYLE_DIFF3; - if ((conflict->ancestor && - (error = git_merge_file_input_from_index_entry( - &ancestor, data->repo, conflict->ancestor)) < 0) || - (error = git_merge_file_input_from_index_entry( - &ours, data->repo, conflict->ours)) < 0 || - (error = git_merge_file_input_from_index_entry( - &theirs, data->repo, conflict->theirs)) < 0) - goto done; - - ancestor.label = data->opts.ancestor_label ? data->opts.ancestor_label : "ancestor"; - ours.label = data->opts.our_label ? data->opts.our_label : "ours"; - theirs.label = data->opts.their_label ? data->opts.their_label : "theirs"; + opts.ancestor_label = data->opts.ancestor_label ? + data->opts.ancestor_label : "ancestor"; + opts.our_label = data->opts.our_label ? + data->opts.our_label : "ours"; + opts.their_label = data->opts.their_label ? + data->opts.their_label : "theirs"; /* If all the paths are identical, decorate the diff3 file with the branch * names. Otherwise, append branch_name:path. @@ -1712,16 +1703,17 @@ static int checkout_write_merge( strcmp(conflict->ours->path, conflict->theirs->path) != 0) { if ((error = conflict_entry_name( - &our_label, ours.label, conflict->ours->path)) < 0 || + &our_label, opts.our_label, conflict->ours->path)) < 0 || (error = conflict_entry_name( - &their_label, theirs.label, conflict->theirs->path)) < 0) + &their_label, opts.their_label, conflict->theirs->path)) < 0) goto done; - ours.label = git_buf_cstr(&our_label); - theirs.label = git_buf_cstr(&their_label); + opts.our_label = git_buf_cstr(&our_label); + opts.their_label = git_buf_cstr(&their_label); } - if ((error = git_merge_files(&result, &ancestor, &ours, &theirs, &merge_file_opts)) < 0) + if ((error = git_merge_file_from_index(&result, data->repo, + conflict->ancestor, conflict->ours, conflict->theirs, &opts)) < 0) goto done; if (result.path == NULL || result.mode == 0) { @@ -1739,7 +1731,7 @@ static int checkout_write_merge( if ((error = git_futils_mkpath2file(path_workdir.ptr, 0755)) < 0 || (error = git_filebuf_open(&output, path_workdir.ptr, GIT_FILEBUF_DO_NOT_BUFFER, result.mode)) < 0 || - (error = git_filebuf_write(&output, result.data, result.len)) < 0 || + (error = git_filebuf_write(&output, result.ptr, result.len)) < 0 || (error = git_filebuf_commit(&output)) < 0) goto done; @@ -1747,9 +1739,6 @@ done: git_buf_free(&our_label); git_buf_free(&their_label); - 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_buf_free(&path_workdir); git_buf_free(&path_suffixed); diff --git a/src/index.c b/src/index.c index 0d7d50668..ea0815e4c 100644 --- a/src/index.c +++ b/src/index.c @@ -300,7 +300,7 @@ static void index_entry_free(git_index_entry *entry) git__free(entry); } -static unsigned int index_create_mode(unsigned int mode) +unsigned int git_index__create_mode(unsigned int mode) { if (S_ISLNK(mode)) return S_IFLNK; @@ -320,9 +320,9 @@ static unsigned int index_merge_mode( if (index->distrust_filemode && S_ISREG(mode)) return (existing && S_ISREG(existing->mode)) ? - existing->mode : index_create_mode(0666); + existing->mode : git_index__create_mode(0666); - return index_create_mode(mode); + return git_index__create_mode(mode); } void git_index__set_ignore_case(git_index *index, bool ignore_case) @@ -619,7 +619,7 @@ void git_index_entry__init_from_stat( entry->dev = st->st_rdev; entry->ino = st->st_ino; entry->mode = (!trust_mode && S_ISREG(st->st_mode)) ? - index_create_mode(0666) : index_create_mode(st->st_mode); + git_index__create_mode(0666) : git_index__create_mode(st->st_mode); entry->uid = st->st_uid; entry->gid = st->st_gid; entry->file_size = st->st_size; diff --git a/src/index.h b/src/index.h index f88d110f7..259a3149f 100644 --- a/src/index.h +++ b/src/index.h @@ -60,4 +60,6 @@ extern int git_index__find( extern void git_index__set_ignore_case(git_index *index, bool ignore_case); +extern unsigned int git_index__create_mode(unsigned int mode); + #endif diff --git a/src/merge.c b/src/merge.c index 0b11c0da3..8a33edb13 100644 --- a/src/merge.c +++ b/src/merge.c @@ -539,11 +539,9 @@ static int merge_conflict_resolve_automerge( const git_merge_diff *conflict, unsigned int merge_file_favor) { - git_merge_file_options merge_file_opts = GIT_MERGE_FILE_OPTIONS_INIT; - 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; + 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; git_oid automerge_oid; @@ -553,7 +551,9 @@ static int merge_conflict_resolve_automerge( *resolved = 0; - merge_file_opts.favor = merge_file_favor; + if (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) || + !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry)) + return 0; /* Reject D/F conflicts */ if (conflict->type == GIT_MERGE_DIFF_DIRECTORY_FILE) @@ -584,13 +584,19 @@ static int merge_conflict_resolve_automerge( if (conflict->binary) return 0; + ancestor = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry) ? + &conflict->ancestor_entry : NULL; + ours = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) ? + &conflict->our_entry : NULL; + theirs = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry) ? + &conflict->their_entry : NULL; + + opts.favor = merge_file_favor; + 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, &merge_file_opts)) < 0 || + (error = git_merge_file_from_index(&result, diff_list->repo, ancestor, ours, theirs, &opts)) < 0 || !result.automergeable || - (error = git_odb_write(&automerge_oid, odb, result.data, result.len, GIT_OBJ_BLOB)) < 0) + (error = git_odb_write(&automerge_oid, odb, result.ptr, result.len, GIT_OBJ_BLOB)) < 0) goto done; if ((index_entry = git_pool_malloc(&diff_list->pool, sizeof(git_index_entry))) == NULL) @@ -609,9 +615,6 @@ static int merge_conflict_resolve_automerge( *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); @@ -2793,3 +2796,27 @@ int git_merge_tree_init_opts(git_merge_tree_opts* opts, int version) return 0; } } + +int git_merge_file_init_input(git_merge_file_input *input, int version) +{ + if (version != GIT_MERGE_FILE_INPUT_VERSION) { + giterr_set(GITERR_INVALID, "Invalid version %d for git_merge_file_input", version); + return -1; + } else { + git_merge_file_input i = GIT_MERGE_FILE_INPUT_INIT; + memcpy(input, &i, sizeof(i)); + return 0; + } +} + +int git_merge_file_init_options(git_merge_file_options *opts, int version) +{ + if (version != GIT_MERGE_FILE_OPTIONS_VERSION) { + giterr_set(GITERR_INVALID, "Invalid version %d for git_merge_file_options", version); + return -1; + } else { + git_merge_file_options o = GIT_MERGE_FILE_OPTIONS_INIT; + memcpy(opts, &o, sizeof(o)); + return 0; + } +} diff --git a/src/merge_file.c b/src/merge_file.c index 986fbf9fe..fc45cbfbf 100644 --- a/src/merge_file.c +++ b/src/merge_file.c @@ -8,6 +8,9 @@ #include "common.h" #include "repository.h" #include "merge_file.h" +#include "posix.h" +#include "fileops.h" +#include "index.h" #include "git2/repository.h" #include "git2/object.h" @@ -22,17 +25,17 @@ GIT_INLINE(const char *) merge_file_best_path( const git_merge_file_input *ours, const git_merge_file_input *theirs) { - if (!GIT_MERGE_FILE_SIDE_EXISTS(ancestor)) { - if (strcmp(ours->path, theirs->path) == 0) + if (!ancestor) { + if (ours && theirs && strcmp(ours->path, theirs->path) == 0) return ours->path; return NULL; } - if (strcmp(ancestor->path, ours->path) == 0) - return theirs->path; - else if(strcmp(ancestor->path, theirs->path) == 0) - return ours->path; + if (ours && strcmp(ancestor->path, ours->path) == 0) + return theirs ? theirs->path : NULL; + else if(theirs && strcmp(ancestor->path, theirs->path) == 0) + return ours ? ours->path : NULL; return NULL; } @@ -47,136 +50,230 @@ GIT_INLINE(int) merge_file_best_mode( * assume executable. Otherwise, if any mode changed from the ancestor, * use that one. */ - if (!GIT_MERGE_FILE_SIDE_EXISTS(ancestor)) { - if (ours->mode == GIT_FILEMODE_BLOB_EXECUTABLE || - theirs->mode == GIT_FILEMODE_BLOB_EXECUTABLE) + if (!ancestor) { + if ((ours && ours->mode == GIT_FILEMODE_BLOB_EXECUTABLE) || + (theirs && theirs->mode == GIT_FILEMODE_BLOB_EXECUTABLE)) return GIT_FILEMODE_BLOB_EXECUTABLE; return GIT_FILEMODE_BLOB; - } + } else if (ours && theirs) { + if (ancestor->mode == ours->mode) + return theirs->mode; - if (ancestor->mode == ours->mode) - return theirs->mode; - else if(ancestor->mode == theirs->mode) return ours->mode; + } return 0; } -int git_merge_file_input_from_index_entry( - git_merge_file_input *input, - git_repository *repo, +int git_merge_file__input_from_index( + git_merge_file_input *input_out, + git_odb_object **odb_object_out, + git_odb *odb, const git_index_entry *entry) { - git_odb *odb = NULL; int error = 0; - assert(input && repo && entry); + assert(input_out && odb_object_out && odb && entry); - if (entry->mode == 0) - return 0; - - if ((error = git_repository_odb(&odb, repo)) < 0 || - (error = git_odb_read(&input->odb_object, odb, &entry->id)) < 0) + if ((error = git_odb_read(odb_object_out, odb, &entry->id)) < 0) goto done; - input->mode = entry->mode; - input->path = git__strdup(entry->path); - input->mmfile.size = git_odb_object_size(input->odb_object); - input->mmfile.ptr = (char *)git_odb_object_data(input->odb_object); - - if (input->label == NULL) - input->label = entry->path; + input_out->path = entry->path; + input_out->mode = entry->mode; + input_out->ptr = (char *)git_odb_object_data(*odb_object_out); + input_out->size = git_odb_object_size(*odb_object_out); done: - git_odb_free(odb); - return error; } -int git_merge_file_input_from_diff_file( - git_merge_file_input *input, - git_repository *repo, - const git_diff_file *file) +static void merge_file_normalize_opts( + git_merge_file_options *out, + const git_merge_file_options *given_opts) { - git_odb *odb = NULL; - int error = 0; - - assert(input && repo && file); - - if (file->mode == 0) - return 0; - - if ((error = git_repository_odb(&odb, repo)) < 0 || - (error = git_odb_read(&input->odb_object, odb, &file->id)) < 0) - goto done; - - input->mode = file->mode; - input->path = git__strdup(file->path); - input->mmfile.size = git_odb_object_size(input->odb_object); - input->mmfile.ptr = (char *)git_odb_object_data(input->odb_object); - - if (input->label == NULL) - input->label = file->path; - -done: - git_odb_free(odb); - - return error; + if (given_opts) + memcpy(out, given_opts, sizeof(git_merge_file_options)); + else { + git_merge_file_options default_opts = GIT_MERGE_FILE_OPTIONS_INIT; + memcpy(out, &default_opts, sizeof(git_merge_file_options)); + } } -int git_merge_files( +static int git_merge_file__from_inputs( git_merge_file_result *out, - git_merge_file_input *ancestor, - git_merge_file_input *ours, - git_merge_file_input *theirs, - git_merge_file_options *opts) + const git_merge_file_input *ancestor, + const git_merge_file_input *ours, + const git_merge_file_input *theirs, + const git_merge_file_options *given_opts) { xmparam_t xmparam; + mmfile_t ancestor_mmfile = {0}, our_mmfile = {0}, their_mmfile = {0}; mmbuffer_t mmbuffer; + git_merge_file_options options = GIT_MERGE_FILE_OPTIONS_INIT; + const char *path; int xdl_result; int error = 0; - assert(out && ancestor && ours && theirs); - memset(out, 0x0, sizeof(git_merge_file_result)); - if (!GIT_MERGE_FILE_SIDE_EXISTS(ours) || !GIT_MERGE_FILE_SIDE_EXISTS(theirs)) - return 0; + merge_file_normalize_opts(&options, given_opts); memset(&xmparam, 0x0, sizeof(xmparam_t)); - xmparam.ancestor = ancestor->label; - xmparam.file1 = ours->label; - xmparam.file2 = theirs->label; - out->path = merge_file_best_path(ancestor, ours, theirs); - out->mode = merge_file_best_mode(ancestor, ours, theirs); + if (ancestor) { + xmparam.ancestor = (options.ancestor_label) ? + options.ancestor_label : ancestor->path; + ancestor_mmfile.ptr = (char *)ancestor->ptr; + ancestor_mmfile.size = ancestor->size; + } - if (opts && opts->favor == GIT_MERGE_FILE_FAVOR_OURS) + xmparam.file1 = (options.our_label) ? + options.our_label : ours->path; + our_mmfile.ptr = (char *)ours->ptr; + our_mmfile.size = ours->size; + + xmparam.file2 = (options.their_label) ? + options.their_label : theirs->path; + their_mmfile.ptr = (char *)theirs->ptr; + their_mmfile.size = theirs->size; + + if (options.favor == GIT_MERGE_FILE_FAVOR_OURS) xmparam.favor = XDL_MERGE_FAVOR_OURS; - else if (opts && opts->favor == GIT_MERGE_FILE_FAVOR_THEIRS) + else if (options.favor == GIT_MERGE_FILE_FAVOR_THEIRS) xmparam.favor = XDL_MERGE_FAVOR_THEIRS; - else if (opts && opts->favor == GIT_MERGE_FILE_FAVOR_UNION) + else if (options.favor == GIT_MERGE_FILE_FAVOR_UNION) xmparam.favor = XDL_MERGE_FAVOR_UNION; - xmparam.level = - (opts && (opts->flags & GIT_MERGE_FILE_SIMPLIFY_ALNUM)) ? + xmparam.level = (options.flags & GIT_MERGE_FILE_SIMPLIFY_ALNUM) ? XDL_MERGE_ZEALOUS_ALNUM : XDL_MERGE_ZEALOUS; - if (opts && opts->style == GIT_MERGE_FILE_STYLE_DIFF3) + if (options.flags & GIT_MERGE_FILE_STYLE_DIFF3) xmparam.style = XDL_MERGE_DIFF3; - if ((xdl_result = xdl_merge(&ancestor->mmfile, &ours->mmfile, - &theirs->mmfile, &xmparam, &mmbuffer)) < 0) { + if ((xdl_result = xdl_merge(&ancestor_mmfile, &our_mmfile, + &their_mmfile, &xmparam, &mmbuffer)) < 0) { giterr_set(GITERR_MERGE, "Failed to merge files."); error = -1; goto done; } + if ((path = merge_file_best_path(ancestor, ours, theirs)) != NULL && + (out->path = strdup(path)) == NULL) { + error = -1; + goto done; + } + out->automergeable = (xdl_result == 0); - out->data = (unsigned char *)mmbuffer.ptr; + out->ptr = (unsigned char *)mmbuffer.ptr; out->len = mmbuffer.size; + out->mode = merge_file_best_mode(ancestor, ours, theirs); done: + if (error < 0) + git_merge_file_result_free(out); + return error; } + +static git_merge_file_input *git_merge_file__normalize_inputs( + git_merge_file_input *out, + const git_merge_file_input *given) +{ + memcpy(out, given, sizeof(git_merge_file_input)); + + if (!out->path) + out->path = "file.txt"; + + if (!out->mode) + out->mode = 0100644; + + return out; +} + +int git_merge_file( + git_merge_file_result *out, + const git_merge_file_input *ancestor, + const git_merge_file_input *ours, + const git_merge_file_input *theirs, + const git_merge_file_options *options) +{ + git_merge_file_input inputs[3] = { {0} }; + + assert(out && ours && theirs); + + memset(out, 0x0, sizeof(git_merge_file_result)); + + if (ancestor) + ancestor = git_merge_file__normalize_inputs(&inputs[0], ancestor); + + ours = git_merge_file__normalize_inputs(&inputs[1], ours); + theirs = git_merge_file__normalize_inputs(&inputs[2], theirs); + + return git_merge_file__from_inputs(out, ancestor, ours, theirs, options); +} + +int git_merge_file_from_index( + git_merge_file_result *out, + git_repository *repo, + const git_index_entry *ancestor, + const git_index_entry *ours, + const git_index_entry *theirs, + const git_merge_file_options *options) +{ + git_merge_file_input inputs[3] = { {0} }, + *ancestor_input = NULL, *our_input = NULL, *their_input = NULL; + git_odb *odb = NULL; + git_odb_object *odb_object[3] = { 0 }; + int error = 0; + + assert(out && repo && ours && theirs); + + memset(out, 0x0, sizeof(git_merge_file_result)); + + if ((error = git_repository_odb(&odb, repo)) < 0) + goto done; + + if (ancestor) { + if ((error = git_merge_file__input_from_index( + &inputs[0], &odb_object[0], odb, ancestor)) < 0) + goto done; + + ancestor_input = &inputs[0]; + } + + if ((error = git_merge_file__input_from_index( + &inputs[1], &odb_object[1], odb, ours)) < 0) + goto done; + + our_input = &inputs[1]; + + if ((error = git_merge_file__input_from_index( + &inputs[2], &odb_object[2], odb, theirs)) < 0) + goto done; + + their_input = &inputs[2]; + + if ((error = git_merge_file__from_inputs(out, + ancestor_input, our_input, their_input, options)) < 0) + goto done; + +done: + git_odb_object_free(odb_object[0]); + git_odb_object_free(odb_object[1]); + git_odb_object_free(odb_object[2]); + git_odb_free(odb); + + return error; +} + +void git_merge_file_result_free(git_merge_file_result *result) +{ + if (result == NULL) + return; + + git__free(result->path); + + /* xdiff uses malloc() not git_malloc, so we use free(), not git_free() */ + free(result->ptr); +} diff --git a/src/merge_file.h b/src/merge_file.h index 332be490b..263391ee3 100644 --- a/src/merge_file.h +++ b/src/merge_file.h @@ -11,82 +11,4 @@ #include "git2/merge.h" -typedef struct { - const char *label; - char *path; - unsigned int mode; - mmfile_t mmfile; - - git_odb_object *odb_object; -} git_merge_file_input; - -#define GIT_MERGE_FILE_INPUT_INIT {0} - -typedef struct { - bool automergeable; - - const char *path; - int mode; - - unsigned char *data; - size_t len; -} git_merge_file_result; - -#define GIT_MERGE_FILE_RESULT_INIT {0} - -typedef enum { - /* Condense non-alphanumeric regions for simplified diff file */ - GIT_MERGE_FILE_SIMPLIFY_ALNUM = (1 << 0), -} git_merge_file_flags_t; - -typedef enum { - /* Create standard conflicted merge files */ - GIT_MERGE_FILE_STYLE_MERGE = 0, - - /* Create diff3-style files */ - GIT_MERGE_FILE_STYLE_DIFF3 = 1, -} git_merge_file_style_t; - -typedef struct { - git_merge_file_favor_t favor; - git_merge_file_flags_t flags; - git_merge_file_style_t style; -} git_merge_file_options; - -#define GIT_MERGE_FILE_OPTIONS_INIT {0} - -int git_merge_file_input_from_index_entry( - git_merge_file_input *input, - git_repository *repo, - const git_index_entry *entry); - -int git_merge_file_input_from_diff_file( - git_merge_file_input *input, - git_repository *repo, - const git_diff_file *file); - -int git_merge_files( - git_merge_file_result *out, - git_merge_file_input *ancestor, - git_merge_file_input *ours, - git_merge_file_input *theirs, - git_merge_file_options *opts); - -GIT_INLINE(void) git_merge_file_input_free(git_merge_file_input *input) -{ - assert(input); - git__free(input->path); - git_odb_object_free(input->odb_object); -} - -GIT_INLINE(void) git_merge_file_result_free(git_merge_file_result *filediff) -{ - if (filediff == NULL) - return; - - /* xdiff uses malloc() not git_malloc, so we use free(), not git_free() */ - if (filediff->data != NULL) - free(filediff->data); -} - #endif diff --git a/tests/merge/files.c b/tests/merge/files.c new file mode 100644 index 000000000..c377471e2 --- /dev/null +++ b/tests/merge/files.c @@ -0,0 +1,175 @@ +#include "clar_libgit2.h" +#include "git2/repository.h" +#include "git2/merge.h" +#include "buffer.h" +#include "merge.h" +#include "merge_helpers.h" +#include "refs.h" +#include "fileops.h" + +#define TEST_REPO_PATH "merge-resolve" +#define TEST_INDEX_PATH TEST_REPO_PATH "/.git/index" + +static git_repository *repo; +static git_index *repo_index; + +// Fixture setup and teardown +void test_merge_files__initialize(void) +{ + git_config *cfg; + + repo = cl_git_sandbox_init(TEST_REPO_PATH); + git_repository_index(&repo_index, repo); + + /* Ensure that the user's merge.conflictstyle doesn't interfere */ + cl_git_pass(git_repository_config(&cfg, repo)); + cl_git_pass(git_config_set_string(cfg, "merge.conflictstyle", "merge")); + git_config_free(cfg); +} + +void test_merge_files__cleanup(void) +{ + git_index_free(repo_index); + cl_git_sandbox_cleanup(); +} + +void test_merge_files__automerge_from_bufs(void) +{ + 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 = {0}; + const char *expected = "Zero\n1\n2\n3\n4\n5\n6\n7\n8\n9\nTen\n"; + + ancestor.ptr = "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n"; + ancestor.size = strlen(ancestor.ptr); + ancestor.path = "testfile.txt"; + ancestor.mode = 0100755; + + ours.ptr = "Zero\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n"; + ours.size = strlen(ours.ptr); + ours.path = "testfile.txt"; + ours.mode = 0100755; + + theirs.ptr = "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\nTen\n"; + theirs.size = strlen(theirs.ptr); + theirs.path = "testfile.txt"; + theirs.mode = 0100755; + + cl_git_pass(git_merge_file(&result, &ancestor, &ours, &theirs, 0)); + + cl_assert_equal_i(1, result.automergeable); + + cl_assert_equal_s("testfile.txt", result.path); + cl_assert_equal_i(0100755, result.mode); + + cl_assert_equal_i(strlen(expected), result.len); + cl_assert_equal_strn(expected, result.ptr, result.len); + + git_merge_file_result_free(&result); +} + +void test_merge_files__automerge_use_best_path_and_mode(void) +{ + 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 = {0}; + const char *expected = "Zero\n1\n2\n3\n4\n5\n6\n7\n8\n9\nTen\n"; + + ancestor.ptr = "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n"; + ancestor.size = strlen(ancestor.ptr); + ancestor.path = "testfile.txt"; + ancestor.mode = 0100755; + + ours.ptr = "Zero\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n"; + ours.size = strlen(ours.ptr); + ours.path = "testfile.txt"; + ours.mode = 0100644; + + theirs.ptr = "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\nTen\n"; + theirs.size = strlen(theirs.ptr); + theirs.path = "theirs.txt"; + theirs.mode = 0100755; + + cl_git_pass(git_merge_file(&result, &ancestor, &ours, &theirs, 0)); + + cl_assert_equal_i(1, result.automergeable); + + cl_assert_equal_s("theirs.txt", result.path); + cl_assert_equal_i(0100644, result.mode); + + cl_assert_equal_i(strlen(expected), result.len); + cl_assert_equal_strn(expected, result.ptr, result.len); + + git_merge_file_result_free(&result); +} + +void test_merge_files__conflict_from_bufs(void) +{ + 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 = {0}; + + const char *expected = "<<<<<<< testfile.txt\nAloha!\nOurs.\n=======\nHi!\nTheirs.\n>>>>>>> theirs.txt\n"; + size_t expected_len = strlen(expected); + + ancestor.ptr = "Hello!\nAncestor!\n"; + ancestor.size = strlen(ancestor.ptr); + ancestor.path = "testfile.txt"; + ancestor.mode = 0100755; + + ours.ptr = "Aloha!\nOurs.\n"; + ours.size = strlen(ours.ptr); + ours.path = "testfile.txt"; + ours.mode = 0100644; + + theirs.ptr = "Hi!\nTheirs.\n"; + theirs.size = strlen(theirs.ptr); + theirs.path = "theirs.txt"; + theirs.mode = 0100755; + + cl_git_pass(git_merge_file(&result, &ancestor, &ours, &theirs, NULL)); + + cl_assert_equal_i(0, result.automergeable); + + cl_assert_equal_s("theirs.txt", result.path); + cl_assert_equal_i(0100644, result.mode); + + cl_assert_equal_i(expected_len, result.len); + cl_assert_equal_strn(expected, result.ptr, expected_len); + + git_merge_file_result_free(&result); +} + +void test_merge_files__automerge_from_index(void) +{ + git_merge_file_result result = {0}; + git_index_entry ancestor, ours, theirs; + + git_oid_fromstr(&ancestor.id, "6212c31dab5e482247d7977e4f0dd3601decf13b"); + ancestor.path = "automergeable.txt"; + ancestor.mode = 0100644; + + git_oid_fromstr(&ours.id, "ee3fa1b8c00aff7fe02065fdb50864bb0d932ccf"); + ours.path = "automergeable.txt"; + ours.mode = 0100755; + + git_oid_fromstr(&theirs.id, "058541fc37114bfc1dddf6bd6bffc7fae5c2e6fe"); + theirs.path = "newname.txt"; + theirs.mode = 0100644; + + cl_git_pass(git_merge_file_from_index(&result, repo, + &ancestor, &ours, &theirs, 0)); + + cl_assert_equal_i(1, result.automergeable); + + cl_assert_equal_s("newname.txt", result.path); + cl_assert_equal_i(0100755, result.mode); + + cl_assert_equal_i(strlen(AUTOMERGEABLE_MERGED_FILE), result.len); + cl_assert_equal_strn(AUTOMERGEABLE_MERGED_FILE, result.ptr, result.len); + + git_merge_file_result_free(&result); +} diff --git a/tests/merge/merge_helpers.h b/tests/merge/merge_helpers.h index 3f53abc7c..63d1cb7a9 100644 --- a/tests/merge/merge_helpers.h +++ b/tests/merge/merge_helpers.h @@ -4,6 +4,49 @@ #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[41]; diff --git a/tests/merge/trees/automerge.c b/tests/merge/trees/automerge.c index bd710e6d8..79069d81d 100644 --- a/tests/merge/trees/automerge.c +++ b/tests/merge/trees/automerge.c @@ -54,28 +54,6 @@ static git_repository *repo; "", \ "5c3b68a71fc4fa5d362fd3875e53137c6a5ab7a5" } -#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" - // Fixture setup and teardown void test_merge_trees_automerge__initialize(void) { diff --git a/tests/merge/trees/commits.c b/tests/merge/trees/commits.c index eeb30dae5..08caf804f 100644 --- a/tests/merge/trees/commits.c +++ b/tests/merge/trees/commits.c @@ -8,17 +8,6 @@ static git_repository *repo; #define TEST_REPO_PATH "merge-resolve" -#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" - void test_merge_trees_commits__initialize(void) { repo = cl_git_sandbox_init(TEST_REPO_PATH); diff --git a/tests/merge/workdir/simple.c b/tests/merge/workdir/simple.c index a9a63651c..07c60c5fa 100644 --- a/tests/merge/workdir/simple.c +++ b/tests/merge/workdir/simple.c @@ -71,47 +71,6 @@ static git_index *repo_index; "", \ "5c3b68a71fc4fa5d362fd3875e53137c6a5ab7a5" } -#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" // Fixture setup and teardown void test_merge_workdir_simple__initialize(void) diff --git a/tests/structinit/structinit.c b/tests/structinit/structinit.c index 1df970d49..61fe8c786 100644 --- a/tests/structinit/structinit.c +++ b/tests/structinit/structinit.c @@ -65,6 +65,16 @@ void test_structinit_structinit__compare(void) git_diff_find_options, GIT_DIFF_FIND_OPTIONS_VERSION, \ GIT_DIFF_FIND_OPTIONS_INIT, git_diff_find_init_options); + /* merge_file_input */ + CHECK_MACRO_FUNC_INIT_EQUAL( \ + git_merge_file_input, GIT_MERGE_FILE_INPUT_VERSION, \ + GIT_MERGE_FILE_INPUT_INIT, git_merge_file_init_input); + + /* merge_file */ + CHECK_MACRO_FUNC_INIT_EQUAL( \ + git_merge_file_options, GIT_MERGE_FILE_OPTIONS_VERSION, \ + GIT_MERGE_FILE_OPTIONS_INIT, git_merge_file_init_options); + /* merge_tree */ CHECK_MACRO_FUNC_INIT_EQUAL( \ git_merge_tree_opts, GIT_MERGE_TREE_OPTS_VERSION, \