From f8787098fbc8944afd684ed0e26221e36da2d6f8 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 31 Oct 2015 18:50:13 +0100 Subject: [PATCH 01/14] Support union merges via .gitattributes file --- src/merge.c | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/merge.c b/src/merge.c index d2f92ccce..767564bf0 100644 --- a/src/merge.c +++ b/src/merge.c @@ -1819,6 +1819,28 @@ static git_iterator *iterator_given_or_empty(git_iterator **empty, git_iterator return *empty; } +static int lookup_file_favor( + git_merge_file_favor_t *file_favor, + git_repository *repo, + const char *path) +{ + int error = 0; + const char *value = NULL; + + if (path) { + if ((error = git_attr_get(&value, repo, 0, path, "merge")) < 0) + goto done; + + if (*file_favor == GIT_MERGE_FILE_FAVOR_NORMAL && + value && strcmp(value, "union") == 0) { + *file_favor |= GIT_MERGE_FILE_FAVOR_UNION; + } + } + +done: + return error; +} + int git_merge__iterators( git_index **out, git_repository *repo, @@ -1877,6 +1899,10 @@ int git_merge__iterators( git_vector_foreach(&changes, i, conflict) { int resolved = 0; + /* Check for merge options in .gitattributes */ + if ((error = lookup_file_favor(&opts.file_favor, repo, conflict->our_entry.path) < 0)) + goto done; + if ((error = merge_conflict_resolve( &resolved, diff_list, conflict, &file_opts)) < 0) goto done; From 7a74590d8f952971088a90c584945ceefe1bf90e Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 3 Dec 2015 09:57:56 -0800 Subject: [PATCH 02/14] Fix rebase bug and include test for merge=union --- src/merge.c | 2 +- tests/merge/workdir/simple.c | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/merge.c b/src/merge.c index 767564bf0..1c2375442 100644 --- a/src/merge.c +++ b/src/merge.c @@ -1900,7 +1900,7 @@ int git_merge__iterators( int resolved = 0; /* Check for merge options in .gitattributes */ - if ((error = lookup_file_favor(&opts.file_favor, repo, conflict->our_entry.path) < 0)) + if ((error = lookup_file_favor(&file_opts.favor, repo, conflict->our_entry.path) < 0)) goto done; if ((error = merge_conflict_resolve( diff --git a/tests/merge/workdir/simple.c b/tests/merge/workdir/simple.c index 3cdd15b5a..964532e46 100644 --- a/tests/merge/workdir/simple.c +++ b/tests/merge/workdir/simple.c @@ -330,6 +330,42 @@ void test_merge_workdir_simple__union(void) cl_assert(merge_test_reuc(repo_index, merge_reuc_entries, 4)); } +void test_merge_workdir_simple__gitattributes_union(void) +{ + git_buf conflicting_buf = GIT_BUF_INIT; + + struct merge_index_entry merge_index_entries[] = { + ADDED_IN_MASTER_INDEX_ENTRY, + AUTOMERGEABLE_INDEX_ENTRY, + CHANGED_IN_BRANCH_INDEX_ENTRY, + CHANGED_IN_MASTER_INDEX_ENTRY, + + { 0100644, "72cdb057b340205164478565e91eb71647e66891", 0, "conflicting.txt" }, + + UNCHANGED_INDEX_ENTRY, + }; + + struct merge_reuc_entry merge_reuc_entries[] = { + AUTOMERGEABLE_REUC_ENTRY, + CONFLICTING_REUC_ENTRY, + REMOVED_IN_BRANCH_REUC_ENTRY, + REMOVED_IN_MASTER_REUC_ENTRY + }; + + set_core_autocrlf_to(repo, false); + cl_git_mkfile(TEST_REPO_PATH "/.gitattributes", "conflicting.txt merge=union\n"); + + merge_simple_branch(GIT_MERGE_FILE_FAVOR_NORMAL, 0); + + cl_git_pass(git_futils_readbuffer(&conflicting_buf, + TEST_REPO_PATH "/conflicting.txt")); + cl_assert(strcmp(conflicting_buf.ptr, CONFLICTING_UNION_FILE) == 0); + git_buf_free(&conflicting_buf); + + cl_assert(merge_test_index(repo_index, merge_index_entries, 6)); + cl_assert(merge_test_reuc(repo_index, merge_reuc_entries, 4)); +} + void test_merge_workdir_simple__diff3_from_config(void) { git_config *config; From 3f04219fcdcbc6369270eaf2d878d4fe7064254d Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 23 Dec 2015 10:23:08 -0600 Subject: [PATCH 03/14] merge driver: introduce custom merge drivers Consumers can now register custom merged drivers with `git_merge_driver_register`. This allows consumers to support the merge drivers, as configured in `.gitattributes`. Consumers will be asked to perform the file-level merge when a custom driver is configured. --- include/git2/sys/merge.h | 230 ++++++++++++++++++++++ src/merge.c | 206 +++++++++++--------- src/merge.h | 99 +++++++++- src/merge_driver.c | 403 +++++++++++++++++++++++++++++++++++++++ src/merge_file.c | 60 ++---- tests/merge/driver.c | 208 ++++++++++++++++++++ 6 files changed, 1069 insertions(+), 137 deletions(-) create mode 100644 include/git2/sys/merge.h create mode 100644 src/merge_driver.c create mode 100644 tests/merge/driver.c diff --git a/include/git2/sys/merge.h b/include/git2/sys/merge.h new file mode 100644 index 000000000..a9f8ca8c2 --- /dev/null +++ b/include/git2/sys/merge.h @@ -0,0 +1,230 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_sys_git_merge_h__ +#define INCLUDE_sys_git_merge_h__ + +/** + * @file git2/sys/merge.h + * @brief Git merge driver backend and plugin routines + * @defgroup git_backend Git custom backend APIs + * @ingroup Git + * @{ + */ +GIT_BEGIN_DECL + +typedef struct git_merge_driver git_merge_driver; + +/** + * Look up a merge driver by name + * + * @param name The name of the merge driver + * @return Pointer to the merge driver object or NULL if not found + */ +GIT_EXTERN(git_merge_driver *) git_merge_driver_lookup(const char *name); + +#define GIT_MERGE_DRIVER_TEXT "text" +#define GIT_MERGE_DRIVER_BINARY "binary" +#define GIT_MERGE_DRIVER_UNION "union" + +/** + * A merge driver source represents the file to be merged + */ +typedef struct git_merge_driver_source git_merge_driver_source; + +/** Get the repository that the source data is coming from. */ +GIT_EXTERN(git_repository *) git_merge_driver_source_repo( + const git_merge_driver_source *src); + +/** Gets the ancestor of the file to merge. */ +GIT_EXTERN(git_index_entry *) git_merge_driver_source_ancestor( + const git_merge_driver_source *src); + +/** Gets the ours side of the file to merge. */ +GIT_EXTERN(git_index_entry *) git_merge_driver_source_ours( + const git_merge_driver_source *src); + +/** Gets the theirs side of the file to merge. */ +GIT_EXTERN(git_index_entry *) git_merge_driver_source_theirs( + const git_merge_driver_source *src); + +/** Gets the merge file options that the merge was invoked with */ +GIT_EXTERN(git_merge_file_options *) git_merge_driver_source_file_options( + const git_merge_driver_source *src); + + +/* + * struct git_merge_driver + * + * The merge driver lifecycle: + * - initialize - first use of merge driver + * - shutdown - merge driver removed/unregistered from system + * - check - considering using merge driver for file + * - apply - apply merge driver to the file + * - cleanup - done with file + */ + +/** + * Initialize callback on merge driver + * + * Specified as `driver.initialize`, this is an optional callback invoked + * before a merge driver is first used. It will be called once at most. + * + * If non-NULL, the merge driver's `initialize` callback will be invoked + * right before the first use of the driver, so you can defer expensive + * initialization operations (in case libgit2 is being used in a way that + * doesn't need the merge driver). + */ +typedef int (*git_merge_driver_init_fn)(git_merge_driver *self); + +/** + * Shutdown callback on merge driver + * + * Specified as `driver.shutdown`, this is an optional callback invoked + * when the merge driver is unregistered or when libgit2 is shutting down. + * It will be called once at most and should release resources as needed. + * This may be called even if the `initialize` callback was not made. + * + * Typically this function will free the `git_merge_driver` object itself. + */ +typedef void (*git_merge_driver_shutdown_fn)(git_merge_driver *self); + +/** + * Callback to decide if a given conflict can be resolved with this merge + * driver. + * + * Specified as `driver.check`, this is an optional callback that checks + * if the given conflict can be resolved with this merge driver. + * + * It should return 0 if the merge driver should be applied (i.e. success), + * `GIT_PASSTHROUGH` if the driver is not available, which is the equivalent + * of an unregistered or nonexistent merge driver. In this case, the default + * (`text`) driver will be run. This is useful if you register a wildcard + * merge driver but are not interested in handling the requested file (and + * should just fallback). The driver can also return `GIT_EMERGECONFLICT` + * if the driver is not able to produce a merge result, and the file will + * remain conflicted. Any other errors will fail and return to the caller. + * + * The `name` will be set to the name of the driver as configured in the + * attributes. + * + * The `src` contains the data about the file to be merged. + * + * The `payload` will be a pointer to a reference payload for the driver. + * This will start as NULL, but `check` can assign to this pointer for + * later use by the `apply` callback. Note that the value should be heap + * allocated (not stack), so that it doesn't go away before the `apply` + * callback can use it. If a driver allocates and assigns a value to the + * `payload`, it will need a `cleanup` callback to free the payload. + */ +typedef int (*git_merge_driver_check_fn)( + git_merge_driver *self, + void **payload, + const char *name, + const git_merge_driver_source *src); + +/** + * Callback to actually perform the merge. + * + * Specified as `driver.apply`, this is the callback that actually does the + * merge. If it can successfully perform a merge, it should populate + * `path_out` with a pointer to the filename to accept, `mode_out` with + * the resultant mode, and `merged_out` with the buffer of the merged file + * and then return 0. If the driver returns `GIT_PASSTHROUGH`, then the + * default merge driver should instead be run. It can also return + * `GIT_EMERGECONFLICT` if the driver is not able to produce a merge result, + * and the file will remain conflicted. Any other errors will fail and + * return to the caller. + * + * The `src` contains the data about the file to be merged. + * + * The `payload` value will refer to any payload that was set by the + * `check` callback. It may be read from or written to as needed. + */ +typedef int (*git_merge_driver_apply_fn)( + git_merge_driver *self, + void **payload, + const char **path_out, + uint32_t *mode_out, + git_buf *merged_out, + const git_merge_driver_source *src); + +/** + * Callback to clean up after merge has been performed. + * + * Specified as `driver.cleanup`, this is an optional callback invoked + * after the driver has been run. If the `check` or `apply` callbacks + * allocated a `payload` to keep per-source merge driver state, use this + * callback to free that payload and release resources as required. + */ +typedef void (*git_merge_driver_cleanup_fn)( + git_merge_driver *self, + void *payload); + +/** + * Merge driver structure used to register custom merge drivers. + * + * To associate extra data with a driver, allocate extra data and put the + * `git_merge_driver` struct at the start of your data buffer, then cast + * the `self` pointer to your larger structure when your callback is invoked. + * + * `version` should be set to GIT_MERGE_DRIVER_VERSION + * + * The `initialize`, `shutdown`, `check`, `apply`, and `cleanup` + * callbacks are all documented above with the respective function pointer + * typedefs. + */ +struct git_merge_driver { + unsigned int version; + + git_merge_driver_init_fn initialize; + git_merge_driver_shutdown_fn shutdown; + git_merge_driver_check_fn check; + git_merge_driver_apply_fn apply; + git_merge_driver_cleanup_fn cleanup; +}; + +#define GIT_MERGE_DRIVER_VERSION 1 + +/** + * Register a merge driver under a given name. + * + * As mentioned elsewhere, the initialize callback will not be invoked + * immediately. It is deferred until the driver is used in some way. + * + * Currently the merge driver registry is not thread safe, so any + * registering or deregistering of merge drivers must be done outside of + * any possible usage of the drivers (i.e. during application setup or + * shutdown). + * + * @param name The name of this driver to match an attribute. Attempting + * to register with an in-use name will return GIT_EEXISTS. + * @param driver The merge driver definition. This pointer will be stored + * as is by libgit2 so it must be a durable allocation (either + * static or on the heap). + * @return 0 on successful registry, error code <0 on failure + */ +GIT_EXTERN(int) git_merge_driver_register( + const char *name, git_merge_driver *driver); + +/** + * Remove the merge driver with the given name. + * + * Attempting to remove the builtin libgit2 merge drivers is not permitted + * and will return an error. + * + * Currently the merge driver registry is not thread safe, so any + * registering or deregistering of drivers must be done outside of any + * possible usage of the drivers (i.e. during application setup or shutdown). + * + * @param name The name under which the merge driver was registered + * @return 0 on success, error code <0 on failure + */ +GIT_EXTERN(int) git_merge_driver_unregister(const char *name); + +/** @} */ +GIT_END_DECL +#endif diff --git a/src/merge.c b/src/merge.c index 1c2375442..0bc64ac13 100644 --- a/src/merge.c +++ b/src/merge.c @@ -50,18 +50,6 @@ #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, @@ -810,76 +798,148 @@ static int merge_conflict_resolve_one_renamed( return error; } -static int merge_conflict_resolve_automerge( +static bool merge_conflict_can_resolve_contents( + const git_merge_diff *conflict) +{ + if (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) || + !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry)) + return false; + + /* Reject D/F conflicts */ + if (conflict->type == GIT_MERGE_DIFF_DIRECTORY_FILE) + return false; + + /* Reject submodules. */ + if (S_ISGITLINK(conflict->ancestor_entry.mode) || + S_ISGITLINK(conflict->our_entry.mode) || + S_ISGITLINK(conflict->their_entry.mode)) + return false; + + /* Reject link/file conflicts. */ + if ((S_ISLNK(conflict->ancestor_entry.mode) ^ + S_ISLNK(conflict->our_entry.mode)) || + (S_ISLNK(conflict->ancestor_entry.mode) ^ + S_ISLNK(conflict->their_entry.mode))) + return false; + + /* Reject name conflicts */ + if (conflict->type == GIT_MERGE_DIFF_BOTH_RENAMED_2_TO_1 || + conflict->type == GIT_MERGE_DIFF_RENAMED_ADDED) + return false; + + if ((conflict->our_status & GIT_DELTA_RENAMED) == GIT_DELTA_RENAMED && + (conflict->their_status & GIT_DELTA_RENAMED) == GIT_DELTA_RENAMED && + strcmp(conflict->ancestor_entry.path, conflict->their_entry.path) != 0) + return false; + + return true; +} + +static int merge_conflict_invoke_driver( + git_index_entry **out, + git_merge_driver *driver, + void *data, + git_merge_diff_list *diff_list, + git_merge_driver_source *source) +{ + git_index_entry *result; + git_buf buf = GIT_BUF_INIT; + const char *path; + uint32_t mode; + git_odb *odb = NULL; + git_oid oid; + int error; + + *out = NULL; + + if ((error = driver->apply(driver, &data, &path, &mode, &buf, source)) < 0) + goto done; + + if ((error = git_repository_odb(&odb, source->repo)) < 0 || + (error = git_odb_write(&oid, odb, buf.ptr, buf.size, GIT_OBJ_BLOB)) < 0) + goto done; + + result = git_pool_mallocz(&diff_list->pool, sizeof(git_index_entry)); + GITERR_CHECK_ALLOC(result); + + git_oid_cpy(&result->id, &oid); + result->mode = mode; + result->file_size = buf.size; + + result->path = git_pool_strdup(&diff_list->pool, path); + GITERR_CHECK_ALLOC(result->path); + + *out = result; + +done: + if (driver->cleanup) + driver->cleanup(driver, data); + + git_buf_free(&buf); + git_odb_free(odb); + + return error; +} + +static int merge_conflict_resolve_contents( int *resolved, git_merge_diff_list *diff_list, const git_merge_diff *conflict, const git_merge_file_options *file_opts) { - const git_index_entry *ancestor = NULL, *ours = NULL, *theirs = NULL; + git_merge_driver_source source = {0}; git_merge_file_result result = {0}; - git_index_entry *index_entry; + git_merge_driver *driver; + git_index_entry *merge_result; git_odb *odb = NULL; - git_oid automerge_oid; + void *data; int error = 0; assert(resolved && diff_list && conflict); *resolved = 0; - if (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) || - !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry)) + if (!merge_conflict_can_resolve_contents(conflict)) return 0; - /* Reject D/F conflicts */ - if (conflict->type == GIT_MERGE_DIFF_DIRECTORY_FILE) - return 0; - - /* Reject submodules. */ - if (S_ISGITLINK(conflict->ancestor_entry.mode) || - S_ISGITLINK(conflict->our_entry.mode) || - S_ISGITLINK(conflict->their_entry.mode)) - return 0; - - /* Reject link/file conflicts. */ - if ((S_ISLNK(conflict->ancestor_entry.mode) ^ S_ISLNK(conflict->our_entry.mode)) || - (S_ISLNK(conflict->ancestor_entry.mode) ^ S_ISLNK(conflict->their_entry.mode))) - return 0; - - /* Reject name conflicts */ - if (conflict->type == GIT_MERGE_DIFF_BOTH_RENAMED_2_TO_1 || - conflict->type == GIT_MERGE_DIFF_RENAMED_ADDED) - return 0; - - if ((conflict->our_status & GIT_DELTA_RENAMED) == GIT_DELTA_RENAMED && - (conflict->their_status & GIT_DELTA_RENAMED) == GIT_DELTA_RENAMED && - strcmp(conflict->ancestor_entry.path, conflict->their_entry.path) != 0) - return 0; - - ancestor = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry) ? + source.repo = diff_list->repo; + source.file_opts = file_opts; + source.ancestor = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry) ? &conflict->ancestor_entry : NULL; - ours = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) ? + source.ours = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) ? &conflict->our_entry : NULL; - theirs = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry) ? + source.theirs = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry) ? &conflict->their_entry : NULL; - if ((error = git_repository_odb(&odb, diff_list->repo)) < 0 || - (error = git_merge_file_from_index(&result, diff_list->repo, ancestor, ours, theirs, file_opts)) < 0 || - (!result.automergeable && !(file_opts->flags & GIT_MERGE_FILE_FAVOR__CONFLICTED)) || - (error = git_odb_write(&automerge_oid, odb, result.ptr, result.len, GIT_OBJ_BLOB)) < 0) - goto done; + if (file_opts->favor != GIT_MERGE_FILE_FAVOR_NORMAL) { + /* if the user requested a particular type of resolution (via the + * favor flag) then let that override the gitattributes. + */ + driver = &git_merge_driver__normal; + data = (void *)file_opts->favor; + } else { + /* find the merge driver for this file */ + if ((error = git_merge_driver_for_source(&driver, &data, &source)) < 0) + goto done; + } - if ((index_entry = git_pool_mallocz(&diff_list->pool, sizeof(git_index_entry))) == NULL) - GITERR_CHECK_ALLOC(index_entry); + error = merge_conflict_invoke_driver(&merge_result, + driver, data, diff_list, &source); - index_entry->path = git_pool_strdup(&diff_list->pool, result.path); - GITERR_CHECK_ALLOC(index_entry->path); + if (error == GIT_PASSTHROUGH) { + data = NULL; + error = merge_conflict_invoke_driver(&merge_result, + &git_merge_driver__text, data, diff_list, &source); + } - index_entry->file_size = result.len; - index_entry->mode = result.mode; - git_oid_cpy(&index_entry->id, &automerge_oid); + if (error < 0) { + if (error == GIT_EMERGECONFLICT) + error = 0; - git_vector_insert(&diff_list->staged, index_entry); + goto done; + } + + git_vector_insert(&diff_list->staged, merge_result); git_vector_insert(&diff_list->resolved, (git_merge_diff *)conflict); *resolved = 1; @@ -911,7 +971,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, file_opts)) < 0) + if (!resolved && (error = merge_conflict_resolve_contents(&resolved, diff_list, conflict, file_opts)) < 0) goto done; *out = resolved; @@ -1819,28 +1879,6 @@ static git_iterator *iterator_given_or_empty(git_iterator **empty, git_iterator return *empty; } -static int lookup_file_favor( - git_merge_file_favor_t *file_favor, - git_repository *repo, - const char *path) -{ - int error = 0; - const char *value = NULL; - - if (path) { - if ((error = git_attr_get(&value, repo, 0, path, "merge")) < 0) - goto done; - - if (*file_favor == GIT_MERGE_FILE_FAVOR_NORMAL && - value && strcmp(value, "union") == 0) { - *file_favor |= GIT_MERGE_FILE_FAVOR_UNION; - } - } - -done: - return error; -} - int git_merge__iterators( git_index **out, git_repository *repo, @@ -1899,10 +1937,6 @@ int git_merge__iterators( git_vector_foreach(&changes, i, conflict) { int resolved = 0; - /* Check for merge options in .gitattributes */ - if ((error = lookup_file_favor(&file_opts.favor, repo, conflict->our_entry.path) < 0)) - goto done; - if ((error = merge_conflict_resolve( &resolved, diff_list, conflict, &file_opts)) < 0) goto done; diff --git a/src/merge.h b/src/merge.h index bd839be49..10d77f3d8 100644 --- a/src/merge.h +++ b/src/merge.h @@ -12,8 +12,9 @@ #include "pool.h" #include "iterator.h" -#include "git2/merge.h" #include "git2/types.h" +#include "git2/merge.h" +#include "git2/sys/merge.h" #define GIT_MERGE_MSG_FILE "MERGE_MSG" #define GIT_MERGE_MODE_FILE "MERGE_MODE" @@ -22,6 +23,49 @@ #define GIT_MERGE_DEFAULT_RENAME_THRESHOLD 50 #define GIT_MERGE_DEFAULT_TARGET_LIMIT 1000 + +/** 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, +}; + + +/* Merge drivers */ + +struct git_merge_driver_source { + git_repository *repo; + const git_merge_file_options *file_opts; + + const git_index_entry *ancestor; + const git_index_entry *ours; + const git_index_entry *theirs; +}; + +extern int git_merge_driver_for_path( + char **name_out, + git_merge_driver **driver_out, + git_repository *repo, + const char *path); + +/* Basic (normal) merge driver, takes favor type as the payload argument */ +extern git_merge_driver git_merge_driver__normal; + +/* Merge driver for text files, performs a standard three-way merge */ +extern git_merge_driver git_merge_driver__text; + +/* Merge driver for union-style merging */ +extern git_merge_driver git_merge_driver__union; + +/* Merge driver for unmergeable (binary) files: always produces conflicts */ +extern git_merge_driver git_merge_driver__binary; + + /** Types of changes when files are merged from branch to branch. */ typedef enum { /* No conflict - a change only occurs in one branch. */ @@ -70,7 +114,6 @@ typedef enum { GIT_MERGE_DIFF_DF_CHILD = (1 << 11), } git_merge_diff_type_t; - typedef struct { git_repository *repo; git_pool pool; @@ -132,6 +175,12 @@ int git_merge_diff_list__find_renames(git_repository *repo, git_merge_diff_list void git_merge_diff_list__free(git_merge_diff_list *diff_list); +/* Merge driver configuration */ +int git_merge_driver_for_source( + git_merge_driver **driver_out, + void **data_out, + const git_merge_driver_source *src); + /* Merge metadata setup */ int git_merge__setup( @@ -152,4 +201,50 @@ int git_merge__check_result(git_repository *repo, git_index *index_new); int git_merge__append_conflicts_to_merge_msg(git_repository *repo, git_index *index); +/* Merge files */ + +GIT_INLINE(const char *) git_merge_file__best_path( + const char *ancestor, + const char *ours, + const char *theirs) +{ + if (!ancestor) { + if (ours && theirs && strcmp(ours, theirs) == 0) + return ours; + + return NULL; + } + + if (ours && strcmp(ancestor, ours) == 0) + return theirs; + else if(theirs && strcmp(ancestor, theirs) == 0) + return ours; + + return NULL; +} + +GIT_INLINE(uint32_t) git_merge_file__best_mode( + uint32_t ancestor, uint32_t ours, uint32_t theirs) +{ + /* + * If ancestor didn't exist and either ours or theirs is executable, + * assume executable. Otherwise, if any mode changed from the ancestor, + * use that one. + */ + if (!ancestor) { + if (ours == GIT_FILEMODE_BLOB_EXECUTABLE || + theirs == GIT_FILEMODE_BLOB_EXECUTABLE) + return GIT_FILEMODE_BLOB_EXECUTABLE; + + return GIT_FILEMODE_BLOB; + } else if (ours && theirs) { + if (ancestor == ours) + return theirs; + + return ours; + } + + return 0; +} + #endif diff --git a/src/merge_driver.c b/src/merge_driver.c new file mode 100644 index 000000000..5866e013e --- /dev/null +++ b/src/merge_driver.c @@ -0,0 +1,403 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" +#include "vector.h" +#include "global.h" +#include "merge.h" +#include "git2/merge.h" +#include "git2/sys/merge.h" + +static const char *merge_driver_name__text = "text"; +static const char *merge_driver_name__union = "union"; +static const char *merge_driver_name__binary = "binary"; + +struct merge_driver_registry { + git_vector drivers; +}; + +typedef struct { + git_merge_driver *driver; + int initialized; + char name[GIT_FLEX_ARRAY]; +} git_merge_driver_entry; + +static struct merge_driver_registry *merge_driver_registry = NULL; + +static int merge_driver_apply( + git_merge_driver *self, + void **payload, + const char **path_out, + uint32_t *mode_out, + git_buf *merged_out, + const git_merge_driver_source *src) +{ + git_merge_file_options file_opts = GIT_MERGE_FILE_OPTIONS_INIT; + git_merge_file_result result = {0}; + int error; + + GIT_UNUSED(self); + + if (src->file_opts) + memcpy(&file_opts, src->file_opts, sizeof(git_merge_file_options)); + + file_opts.favor = (git_merge_file_favor_t) *payload; + + if ((error = git_merge_file_from_index(&result, src->repo, + src->ancestor, src->ours, src->theirs, &file_opts)) < 0) + goto done; + + if (!result.automergeable && + !(file_opts.flags & GIT_MERGE_FILE_FAVOR__CONFLICTED)) { + error = GIT_EMERGECONFLICT; + goto done; + } + + *path_out = git_merge_file__best_path( + src->ancestor ? src->ancestor->path : NULL, + src->ours ? src->ours->path : NULL, + src->theirs ? src->theirs->path : NULL); + + *mode_out = git_merge_file__best_mode( + src->ancestor ? src->ancestor->mode : 0, + src->ours ? src->ours->mode : 0, + src->theirs ? src->theirs->mode : 0); + + merged_out->ptr = (char *)result.ptr; + merged_out->size = result.len; + merged_out->asize = result.len; + result.ptr = NULL; + +done: + git_merge_file_result_free(&result); + return error; +} + +static int merge_driver_text_check( + git_merge_driver *self, + void **payload, + const char *name, + const git_merge_driver_source *src) +{ + GIT_UNUSED(self); + GIT_UNUSED(name); + GIT_UNUSED(src); + + *payload = (void *)GIT_MERGE_FILE_FAVOR_NORMAL; + return 0; +} + +static int merge_driver_union_check( + git_merge_driver *self, + void **payload, + const char *name, + const git_merge_driver_source *src) +{ + GIT_UNUSED(self); + GIT_UNUSED(name); + GIT_UNUSED(src); + + *payload = (void *)GIT_MERGE_FILE_FAVOR_UNION; + return 0; +} + +static int merge_driver_binary_apply( + git_merge_driver *self, + void **payload, + const char **path_out, + uint32_t *mode_out, + git_buf *merged_out, + const git_merge_driver_source *src) +{ + GIT_UNUSED(self); + GIT_UNUSED(payload); + GIT_UNUSED(path_out); + GIT_UNUSED(mode_out); + GIT_UNUSED(merged_out); + GIT_UNUSED(src); + + return GIT_EMERGECONFLICT; +} + +static int merge_driver_entry_cmp(const void *a, const void *b) +{ + const git_merge_driver_entry *entry_a = a; + const git_merge_driver_entry *entry_b = b; + + return strcmp(entry_a->name, entry_b->name); +} + +static int merge_driver_entry_search(const void *a, const void *b) +{ + const char *name_a = a; + const git_merge_driver_entry *entry_b = b; + + return strcmp(name_a, entry_b->name); +} + +static void merge_driver_registry_shutdown(void) +{ + struct merge_driver_registry *reg; + git_merge_driver_entry *entry; + size_t i; + + if ((reg = git__swap(merge_driver_registry, NULL)) == NULL) + return; + + git_vector_foreach(®->drivers, i, entry) { + if (entry && entry->driver->shutdown) + entry->driver->shutdown(entry->driver); + + git__free(entry); + } + + git_vector_free(®->drivers); + git__free(reg); +} + +git_merge_driver git_merge_driver__normal = { + GIT_MERGE_DRIVER_VERSION, + NULL, + NULL, + NULL, + merge_driver_apply +}; + +git_merge_driver git_merge_driver__text = { + GIT_MERGE_DRIVER_VERSION, + NULL, + NULL, + merge_driver_text_check, + merge_driver_apply +}; + +git_merge_driver git_merge_driver__union = { + GIT_MERGE_DRIVER_VERSION, + NULL, + NULL, + merge_driver_union_check, + merge_driver_apply +}; + +git_merge_driver git_merge_driver__binary = { + GIT_MERGE_DRIVER_VERSION, + NULL, + NULL, + NULL, + merge_driver_binary_apply +}; + +static int merge_driver_registry_initialize(void) +{ + struct merge_driver_registry *reg; + int error = 0; + + if (merge_driver_registry) + return 0; + + reg = git__calloc(1, sizeof(struct merge_driver_registry)); + GITERR_CHECK_ALLOC(reg); + + if ((error = git_vector_init(®->drivers, 3, merge_driver_entry_cmp)) < 0) + goto done; + + reg = git__compare_and_swap(&merge_driver_registry, NULL, reg); + + if (reg != NULL) + goto done; + + git__on_shutdown(merge_driver_registry_shutdown); + + if ((error = git_merge_driver_register( + merge_driver_name__text, &git_merge_driver__text)) < 0 || + (error = git_merge_driver_register( + merge_driver_name__union, &git_merge_driver__union)) < 0 || + (error = git_merge_driver_register( + merge_driver_name__binary, &git_merge_driver__binary)) < 0) + goto done; + +done: + if (error < 0) + merge_driver_registry_shutdown(); + + return error; +} + +int git_merge_driver_register(const char *name, git_merge_driver *driver) +{ + git_merge_driver_entry *entry; + + assert(name && driver); + + if (merge_driver_registry_initialize() < 0) + return -1; + + entry = git__calloc(1, sizeof(git_merge_driver_entry) + strlen(name) + 1); + GITERR_CHECK_ALLOC(entry); + + strcpy(entry->name, name); + entry->driver = driver; + + return git_vector_insert_sorted( + &merge_driver_registry->drivers, entry, NULL); +} + +int git_merge_driver_unregister(const char *name) +{ + git_merge_driver_entry *entry; + size_t pos; + int error; + + if ((error = git_vector_search2(&pos, &merge_driver_registry->drivers, + merge_driver_entry_search, name)) < 0) + return error; + + entry = git_vector_get(&merge_driver_registry->drivers, pos); + git_vector_remove(&merge_driver_registry->drivers, pos); + + if (entry->initialized && entry->driver->shutdown) { + entry->driver->shutdown(entry->driver); + entry->initialized = false; + } + + git__free(entry); + + return 0; +} + +git_merge_driver *git_merge_driver_lookup(const char *name) +{ + git_merge_driver_entry *entry; + size_t pos; + int error; + + /* If we've decided the merge driver to use internally - and not + * based on user configuration (in merge_driver_name_for_path) + * then we can use a hardcoded name instead of looking it up in + * the vector. + */ + if (name == merge_driver_name__text) + return &git_merge_driver__text; + else if (name == merge_driver_name__binary) + return &git_merge_driver__binary; + + if (merge_driver_registry_initialize() < 0) + return NULL; + + error = git_vector_search2(&pos, &merge_driver_registry->drivers, + merge_driver_entry_search, name); + + if (error == GIT_ENOTFOUND) + return NULL; + + entry = git_vector_get(&merge_driver_registry->drivers, pos); + + if (!entry->initialized) { + if (entry->driver->initialize && + (error = entry->driver->initialize(entry->driver)) < 0) + return NULL; + + entry->initialized = 1; + } + + return entry->driver; +} + +static git_merge_driver *merge_driver_lookup_with_default(const char *name) +{ + git_merge_driver *driver = git_merge_driver_lookup(name); + + if (driver == NULL) + driver = git_merge_driver_lookup("*"); + + if (driver == NULL) + driver = &git_merge_driver__text; + + return driver; +} + +static int merge_driver_name_for_path( + const char **out, + git_repository *repo, + const char *path) +{ + const char *value; + int error; + + *out = NULL; + + if ((error = git_attr_get(&value, repo, 0, path, "merge")) < 0) + return error; + + /* set: use the built-in 3-way merge driver ("text") */ + if (GIT_ATTR_TRUE(value)) { + *out = merge_driver_name__text; + return 0; + } + + /* unset: do not merge ("binary") */ + if (GIT_ATTR_FALSE(value)) { + *out = merge_driver_name__binary; + return 0; + } + + if (GIT_ATTR_UNSPECIFIED(value)) { + /* TODO */ + /* if there's a merge.default configuration value, use it */ + *out = merge_driver_name__text; + return 0; + } + + *out = value; + return 0; +} + +int git_merge_driver_for_source( + git_merge_driver **driver_out, + void **data_out, + const git_merge_driver_source *src) +{ + const char *path, *driver_name; + git_merge_driver *driver; + void *data = NULL; + int error = 0; + + path = git_merge_file__best_path( + src->ancestor ? src->ancestor->path : NULL, + src->ours ? src->ours->path : NULL, + src->theirs ? src->theirs->path : NULL); + + if ((error = merge_driver_name_for_path(&driver_name, src->repo, path)) < 0) + return error; + + driver = merge_driver_lookup_with_default(driver_name); + + if (driver->check) + error = driver->check(driver, &data, driver_name, src); + + if (error == GIT_PASSTHROUGH) + driver = &git_merge_driver__text; + else if (error == GIT_EMERGECONFLICT) + driver = &git_merge_driver__binary; + else + goto done; + + error = 0; + data = NULL; + + if (driver->check) + error = driver->check(driver, &data, driver_name, src); + + /* the text and binary drivers must succeed their check */ + assert(error == 0); + +done: + *driver_out = driver; + *data_out = data; + return error; +} + diff --git a/src/merge_file.c b/src/merge_file.c index 6d4738065..731f4b724 100644 --- a/src/merge_file.c +++ b/src/merge_file.c @@ -11,6 +11,7 @@ #include "fileops.h" #include "index.h" #include "diff_xdiff.h" +#include "merge.h" #include "git2/repository.h" #include "git2/object.h" @@ -26,52 +27,6 @@ #define GIT_MERGE_FILE_SIDE_EXISTS(X) ((X)->mode != 0) -GIT_INLINE(const char *) merge_file_best_path( - const git_merge_file_input *ancestor, - const git_merge_file_input *ours, - const git_merge_file_input *theirs) -{ - if (!ancestor) { - if (ours && theirs && strcmp(ours->path, theirs->path) == 0) - return ours->path; - - return NULL; - } - - 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; -} - -GIT_INLINE(int) merge_file_best_mode( - const git_merge_file_input *ancestor, - const git_merge_file_input *ours, - const git_merge_file_input *theirs) -{ - /* - * If ancestor didn't exist and either ours or theirs is executable, - * assume executable. Otherwise, if any mode changed from the ancestor, - * use that one. - */ - 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; - - return ours->mode; - } - - return 0; -} - int git_merge_file__input_from_index( git_merge_file_input *input_out, git_odb_object **odb_object_out, @@ -177,8 +132,12 @@ static int merge_file__xdiff( goto done; } - if ((path = merge_file_best_path(ancestor, ours, theirs)) != NULL && - (out->path = strdup(path)) == NULL) { + path = git_merge_file__best_path( + ancestor ? ancestor->path : NULL, + ours ? ours->path : NULL, + theirs ? theirs->path : NULL); + + if (path != NULL && (out->path = git__strdup(path)) == NULL) { error = -1; goto done; } @@ -186,7 +145,10 @@ static int merge_file__xdiff( out->automergeable = (xdl_result == 0); out->ptr = (const char *)mmbuffer.ptr; out->len = mmbuffer.size; - out->mode = merge_file_best_mode(ancestor, ours, theirs); + out->mode = git_merge_file__best_mode( + ancestor ? ancestor->mode : 0, + ours ? ours->mode : 0, + theirs ? theirs->mode : 0); done: if (error < 0) diff --git a/tests/merge/driver.c b/tests/merge/driver.c new file mode 100644 index 000000000..34ed914dc --- /dev/null +++ b/tests/merge/driver.c @@ -0,0 +1,208 @@ +#include "clar_libgit2.h" +#include "git2/repository.h" +#include "git2/merge.h" +#include "buffer.h" +#include "merge.h" + +#define TEST_REPO_PATH "merge-resolve" +#define BRANCH_ID "7cb63eed597130ba4abb87b3e544b85021905520" + +static git_repository *repo; +static git_index *repo_index; + +static void test_drivers_register(void); +static void test_drivers_unregister(void); + +void test_merge_driver__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")); + cl_git_pass(git_config_set_bool(cfg, "core.autocrlf", false)); + + test_drivers_register(); + + git_config_free(cfg); +} + +void test_merge_driver__cleanup(void) +{ + test_drivers_unregister(); + + git_index_free(repo_index); + cl_git_sandbox_cleanup(); +} + +struct test_merge_driver { + git_merge_driver base; + int initialized; + int shutdown; +}; + +static int test_driver_init(git_merge_driver *s) +{ + struct test_merge_driver *self = (struct test_merge_driver *)s; + self->initialized = 1; + return 0; +} + +static void test_driver_shutdown(git_merge_driver *s) +{ + struct test_merge_driver *self = (struct test_merge_driver *)s; + self->shutdown = 1; +} + +static int test_driver_check( + git_merge_driver *s, + void **payload, + const char *name, + const git_merge_driver_source *src) +{ + GIT_UNUSED(s); + GIT_UNUSED(src); + + *payload = git__strdup(name); + GITERR_CHECK_ALLOC(*payload); + + return 0; +} + +static int test_driver_apply( + git_merge_driver *s, + void **payload, + const char **path_out, + uint32_t *mode_out, + git_buf *merged_out, + const git_merge_driver_source *src) +{ + GIT_UNUSED(s); + GIT_UNUSED(src); + + *path_out = "applied.txt"; + *mode_out = GIT_FILEMODE_BLOB; + + return git_buf_printf(merged_out, "This is the `%s` driver.\n", + (char *)*payload); +} + +static void test_driver_cleanup(git_merge_driver *s, void *payload) +{ + GIT_UNUSED(s); + + git__free(payload); +} + + +static struct test_merge_driver test_driver_custom = { + { + GIT_MERGE_DRIVER_VERSION, + test_driver_init, + test_driver_shutdown, + test_driver_check, + test_driver_apply, + test_driver_cleanup + }, + 0, + 0, +}; + +static struct test_merge_driver test_driver_wildcard = { + { + GIT_MERGE_DRIVER_VERSION, + test_driver_init, + test_driver_shutdown, + test_driver_check, + test_driver_apply, + test_driver_cleanup + }, + 0, + 0, +}; + +static void test_drivers_register(void) +{ + cl_git_pass(git_merge_driver_register("custom", &test_driver_custom.base)); + cl_git_pass(git_merge_driver_register("*", &test_driver_wildcard.base)); +} + +static void test_drivers_unregister(void) +{ + cl_git_pass(git_merge_driver_unregister("custom")); + cl_git_pass(git_merge_driver_unregister("*")); +} + +static void set_gitattributes_to(const char *driver) +{ + git_buf line = GIT_BUF_INIT; + + cl_git_pass(git_buf_printf(&line, "automergeable.txt merge=%s\n", driver)); + cl_git_mkfile(TEST_REPO_PATH "/.gitattributes", line.ptr); + git_buf_free(&line); +} + +static void merge_branch(void) +{ + git_oid their_id; + git_annotated_commit *their_head; + + cl_git_pass(git_oid_fromstr(&their_id, BRANCH_ID)); + cl_git_pass(git_annotated_commit_lookup(&their_head, repo, &their_id)); + + cl_git_pass(git_merge(repo, (const git_annotated_commit **)&their_head, + 1, NULL, NULL)); + + git_annotated_commit_free(their_head); +} + +void test_merge_driver__custom(void) +{ + const char *expected = "This is the `custom` driver.\n"; + set_gitattributes_to("custom"); + merge_branch(); + + cl_assert_equal_file(expected, strlen(expected), + TEST_REPO_PATH "/applied.txt"); +} + +void test_merge_driver__wildcard(void) +{ + const char *expected = "This is the `foobar` driver.\n"; + set_gitattributes_to("foobar"); + merge_branch(); + + cl_assert_equal_file(expected, strlen(expected), + TEST_REPO_PATH "/applied.txt"); +} + +void test_merge_driver__shutdown_is_called(void) +{ + test_driver_custom.initialized = 0; + test_driver_custom.shutdown = 0; + test_driver_wildcard.initialized = 0; + test_driver_wildcard.shutdown = 0; + + /* run the merge with the custom driver */ + set_gitattributes_to("custom"); + merge_branch(); + + /* unregister the drivers, ensure their shutdown function is called */ + test_drivers_unregister(); + + /* since the `custom` driver was used, it should have been initialized and + * shutdown, but the wildcard driver was not used at all and should not + * have been initialized or shutdown. + */ + cl_assert(test_driver_custom.initialized); + cl_assert(test_driver_custom.shutdown); + cl_assert(!test_driver_wildcard.initialized); + cl_assert(!test_driver_wildcard.shutdown); + + test_drivers_register(); +} + From 59f293146503bf5e615161fe89c305f080755dd1 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 23 Dec 2015 23:44:58 -0600 Subject: [PATCH 04/14] merge driver: test GIT_PASSTHROUGH When a `check` or `apply` callback function returns `GIT_PASSTHROUGH`, move on to the default merge driver. --- tests/merge/driver.c | 95 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/tests/merge/driver.c b/tests/merge/driver.c index 34ed914dc..2597e5721 100644 --- a/tests/merge/driver.c +++ b/tests/merge/driver.c @@ -7,8 +7,11 @@ #define TEST_REPO_PATH "merge-resolve" #define BRANCH_ID "7cb63eed597130ba4abb87b3e544b85021905520" +#define AUTOMERGEABLE_IDSTR "f2e1550a0c9e53d5811175864a29536642ae3821" + static git_repository *repo; static git_index *repo_index; +static git_oid automergeable_id; static void test_drivers_register(void); static void test_drivers_unregister(void); @@ -20,6 +23,8 @@ void test_merge_driver__initialize(void) repo = cl_git_sandbox_init(TEST_REPO_PATH); git_repository_index(&repo_index, repo); + git_oid_fromstr(&automergeable_id, AUTOMERGEABLE_IDSTR); + /* Ensure that the user's merge.conflictstyle doesn't interfere */ cl_git_pass(git_repository_config(&cfg, repo)); @@ -206,3 +211,93 @@ void test_merge_driver__shutdown_is_called(void) test_drivers_register(); } +static int defer_driver_check( + git_merge_driver *s, + void **payload, + const char *name, + const git_merge_driver_source *src) +{ + GIT_UNUSED(s); + GIT_UNUSED(payload); + GIT_UNUSED(name); + GIT_UNUSED(src); + + return GIT_PASSTHROUGH; +} + +static struct test_merge_driver test_driver_defer_check = { + { + GIT_MERGE_DRIVER_VERSION, + test_driver_init, + test_driver_shutdown, + defer_driver_check, + test_driver_apply, + test_driver_cleanup + }, + 0, + 0, +}; + +void test_merge_driver__check_can_defer(void) +{ + const git_index_entry *idx; + + cl_git_pass(git_merge_driver_register("defer", + &test_driver_defer_check.base)); + + set_gitattributes_to("defer"); + merge_branch(); + + cl_assert((idx = git_index_get_bypath(repo_index, "automergeable.txt", 0))); + cl_assert_equal_oid(&automergeable_id, &idx->id); + + git_merge_driver_unregister("defer"); +} + +static int defer_driver_apply( + git_merge_driver *s, + void **payload, + const char **path_out, + uint32_t *mode_out, + git_buf *merged_out, + const git_merge_driver_source *src) +{ + GIT_UNUSED(s); + GIT_UNUSED(payload); + GIT_UNUSED(path_out); + GIT_UNUSED(mode_out); + GIT_UNUSED(merged_out); + GIT_UNUSED(src); + + return GIT_PASSTHROUGH; +} + +static struct test_merge_driver test_driver_defer_apply = { + { + GIT_MERGE_DRIVER_VERSION, + test_driver_init, + test_driver_shutdown, + test_driver_check, + defer_driver_apply, + test_driver_cleanup + }, + 0, + 0, +}; + +void test_merge_driver__apply_can_defer(void) +{ + const git_index_entry *idx; + + cl_git_pass(git_merge_driver_register("defer", + &test_driver_defer_apply.base)); + + set_gitattributes_to("defer"); + merge_branch(); + + cl_assert((idx = git_index_get_bypath(repo_index, "automergeable.txt", 0))); + cl_assert_equal_oid(&automergeable_id, &idx->id); + + git_merge_driver_unregister("defer"); +} + From 7d307c1edc5430dce270302d539d6eab8ed054c7 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 23 Dec 2015 23:52:02 -0600 Subject: [PATCH 05/14] merge driver: test GIT_EMERGECONFLICT When a `check` or `apply` callback function returns `GIT_EMERGECONFLICT` stop and product a conflict. --- tests/merge/driver.c | 90 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/tests/merge/driver.c b/tests/merge/driver.c index 2597e5721..d45178281 100644 --- a/tests/merge/driver.c +++ b/tests/merge/driver.c @@ -301,3 +301,93 @@ void test_merge_driver__apply_can_defer(void) git_merge_driver_unregister("defer"); } +static int conflict_driver_check( + git_merge_driver *s, + void **payload, + const char *name, + const git_merge_driver_source *src) +{ + GIT_UNUSED(s); + GIT_UNUSED(payload); + GIT_UNUSED(name); + GIT_UNUSED(src); + + return GIT_EMERGECONFLICT; +} + +static struct test_merge_driver test_driver_conflict_check = { + { + GIT_MERGE_DRIVER_VERSION, + test_driver_init, + test_driver_shutdown, + conflict_driver_check, + test_driver_apply, + test_driver_cleanup + }, + 0, + 0, +}; + +void test_merge_driver__check_can_conflict(void) +{ + const git_index_entry *ancestor, *ours, *theirs; + + cl_git_pass(git_merge_driver_register("conflict", + &test_driver_conflict_check.base)); + + set_gitattributes_to("conflict"); + merge_branch(); + + cl_git_pass(git_index_conflict_get(&ancestor, &ours, &theirs, + repo_index, "automergeable.txt")); + + git_merge_driver_unregister("conflict"); +} + +static int conflict_driver_apply( + git_merge_driver *s, + void **payload, + const char **path_out, + uint32_t *mode_out, + git_buf *merged_out, + const git_merge_driver_source *src) +{ + GIT_UNUSED(s); + GIT_UNUSED(payload); + GIT_UNUSED(path_out); + GIT_UNUSED(mode_out); + GIT_UNUSED(merged_out); + GIT_UNUSED(src); + + return GIT_EMERGECONFLICT; +} + +static struct test_merge_driver test_driver_conflict_apply = { + { + GIT_MERGE_DRIVER_VERSION, + test_driver_init, + test_driver_shutdown, + test_driver_check, + conflict_driver_apply, + test_driver_cleanup + }, + 0, + 0, +}; + +void test_merge_driver__apply_can_conflict(void) +{ + const git_index_entry *ancestor, *ours, *theirs; + + cl_git_pass(git_merge_driver_register("conflict", + &test_driver_conflict_apply.base)); + + set_gitattributes_to("conflict"); + merge_branch(); + + cl_git_pass(git_index_conflict_get(&ancestor, &ours, &theirs, + repo_index, "automergeable.txt")); + + git_merge_driver_unregister("conflict"); +} + From 30a94ab75687ae52ad4f9081831110b10dbd82ca Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Thu, 24 Dec 2015 22:52:23 -0600 Subject: [PATCH 06/14] merge driver: allow custom default driver Allow merge users to configure a custom default merge driver via `git_merge_options`. Similarly, honor the `merge.default` configuration option. --- CHANGELOG.md | 8 +++++ include/git2/merge.h | 11 ++++++- src/merge.c | 40 +++++++++++++++++++++---- src/merge.h | 1 + src/merge_driver.c | 71 ++++++++++++++++++++++---------------------- 5 files changed, 88 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21f972d2e..0e9ca156a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ v0.24 ### Changes or improvements +* Custom merge drivers can now be registered, which allows callers to + configure callbacks to honor `merge=driver` configuration in + `.gitattributes`. + * Custom filters can now be registered with wildcard attributes, for example `filter=*`. Consumers should examine the attributes parameter of the `check` function for details. @@ -83,6 +87,10 @@ v0.24 ### Breaking API changes +* `git_merge_options` now provides a `default_driver` that can be used + to provide the name of a merge driver to be used to handle files changed + during a merge. + * 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 diff --git a/include/git2/merge.h b/include/git2/merge.h index 560797a0c..c6f6cba6c 100644 --- a/include/git2/merge.h +++ b/include/git2/merge.h @@ -273,7 +273,16 @@ typedef struct { */ unsigned int recursion_limit; - /** Flags for handling conflicting content. */ + /** + * Default merge driver to be used when both sides of a merge have + * changed. The default is the `text` driver. + */ + const char *default_driver; + + /** + * Flags for handling conflicting content, to be used with the standard + * (`text`) merge driver. + */ git_merge_file_favor_t file_favor; /** see `git_merge_file_flag_t` above */ diff --git a/src/merge.c b/src/merge.c index 0bc64ac13..9a6442a91 100644 --- a/src/merge.c +++ b/src/merge.c @@ -885,6 +885,7 @@ static int merge_conflict_resolve_contents( int *resolved, git_merge_diff_list *diff_list, const git_merge_diff *conflict, + const git_merge_options *merge_opts, const git_merge_file_options *file_opts) { git_merge_driver_source source = {0}; @@ -903,6 +904,7 @@ static int merge_conflict_resolve_contents( return 0; source.repo = diff_list->repo; + source.default_driver = merge_opts->default_driver; source.file_opts = file_opts; source.ancestor = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry) ? &conflict->ancestor_entry : NULL; @@ -955,6 +957,7 @@ static int merge_conflict_resolve( int *out, git_merge_diff_list *diff_list, const git_merge_diff *conflict, + const git_merge_options *merge_opts, const git_merge_file_options *file_opts) { int resolved = 0; @@ -962,16 +965,20 @@ static int merge_conflict_resolve( *out = 0; - if ((error = merge_conflict_resolve_trivial(&resolved, diff_list, conflict)) < 0) + if ((error = merge_conflict_resolve_trivial( + &resolved, diff_list, conflict)) < 0) goto done; - if (!resolved && (error = merge_conflict_resolve_one_removed(&resolved, diff_list, conflict)) < 0) + if (!resolved && (error = merge_conflict_resolve_one_removed( + &resolved, diff_list, conflict)) < 0) goto done; - if (!resolved && (error = merge_conflict_resolve_one_renamed(&resolved, diff_list, conflict)) < 0) + if (!resolved && (error = merge_conflict_resolve_one_renamed( + &resolved, diff_list, conflict)) < 0) goto done; - if (!resolved && (error = merge_conflict_resolve_contents(&resolved, diff_list, conflict, file_opts)) < 0) + if (!resolved && (error = merge_conflict_resolve_contents( + &resolved, diff_list, conflict, merge_opts, file_opts)) < 0) goto done; *out = resolved; @@ -1687,6 +1694,7 @@ static int merge_normalize_opts( const git_merge_options *given) { git_config *cfg = NULL; + git_config_entry *entry = NULL; int error = 0; assert(repo && opts); @@ -1704,6 +1712,22 @@ static int merge_normalize_opts( opts->rename_threshold = GIT_MERGE_DEFAULT_RENAME_THRESHOLD; } + if (given && given->default_driver) { + opts->default_driver = git__strdup(given->default_driver); + GITERR_CHECK_ALLOC(opts->default_driver); + } else { + error = git_config_get_entry(&entry, cfg, "merge.default"); + + if (error == 0) { + opts->default_driver = git__strdup(entry->value); + GITERR_CHECK_ALLOC(opts->default_driver); + } else if (error == GIT_ENOTFOUND) { + error = 0; + } else { + goto done; + } + } + if (!opts->target_limit) { int limit = git_config__get_int_force(cfg, "merge.renamelimit", 0); @@ -1726,7 +1750,9 @@ static int merge_normalize_opts( opts->metric->payload = (void *)GIT_HASHSIG_SMART_WHITESPACE; } - return 0; +done: + git_config_entry_free(entry); + return error; } @@ -1938,7 +1964,7 @@ int git_merge__iterators( int resolved = 0; if ((error = merge_conflict_resolve( - &resolved, diff_list, conflict, &file_opts)) < 0) + &resolved, diff_list, conflict, &opts, &file_opts)) < 0) goto done; if (!resolved) { @@ -1959,6 +1985,8 @@ done: if (!given_opts || !given_opts->metric) git__free(opts.metric); + git__free((char *)opts.default_driver); + git_merge_diff_list__free(diff_list); git_iterator_free(empty_ancestor); git_iterator_free(empty_ours); diff --git a/src/merge.h b/src/merge.h index 10d77f3d8..8eaa1ad92 100644 --- a/src/merge.h +++ b/src/merge.h @@ -40,6 +40,7 @@ enum { struct git_merge_driver_source { git_repository *repo; + const char *default_driver; const git_merge_file_options *file_opts; const git_index_entry *ancestor; diff --git a/src/merge_driver.c b/src/merge_driver.c index 5866e013e..791afe0b7 100644 --- a/src/merge_driver.c +++ b/src/merge_driver.c @@ -307,23 +307,11 @@ git_merge_driver *git_merge_driver_lookup(const char *name) return entry->driver; } -static git_merge_driver *merge_driver_lookup_with_default(const char *name) -{ - git_merge_driver *driver = git_merge_driver_lookup(name); - - if (driver == NULL) - driver = git_merge_driver_lookup("*"); - - if (driver == NULL) - driver = &git_merge_driver__text; - - return driver; -} - static int merge_driver_name_for_path( const char **out, git_repository *repo, - const char *path) + const char *path, + const char *default_driver) { const char *value; int error; @@ -334,28 +322,37 @@ static int merge_driver_name_for_path( return error; /* set: use the built-in 3-way merge driver ("text") */ - if (GIT_ATTR_TRUE(value)) { + if (GIT_ATTR_TRUE(value)) *out = merge_driver_name__text; - return 0; - } /* unset: do not merge ("binary") */ - if (GIT_ATTR_FALSE(value)) { + else if (GIT_ATTR_FALSE(value)) *out = merge_driver_name__binary; - return 0; - } - if (GIT_ATTR_UNSPECIFIED(value)) { - /* TODO */ - /* if there's a merge.default configuration value, use it */ + else if (GIT_ATTR_UNSPECIFIED(value) && default_driver) + *out = default_driver; + + else if (GIT_ATTR_UNSPECIFIED(value)) *out = merge_driver_name__text; - return 0; - } - - *out = value; + + else + *out = value; + return 0; } + +GIT_INLINE(git_merge_driver *) merge_driver_lookup_with_wildcard( + const char *name) +{ + git_merge_driver *driver = git_merge_driver_lookup(name); + + if (driver == NULL) + driver = git_merge_driver_lookup("*"); + + return driver; +} + int git_merge_driver_for_source( git_merge_driver **driver_out, void **data_out, @@ -371,20 +368,22 @@ int git_merge_driver_for_source( src->ours ? src->ours->path : NULL, src->theirs ? src->theirs->path : NULL); - if ((error = merge_driver_name_for_path(&driver_name, src->repo, path)) < 0) + if ((error = merge_driver_name_for_path( + &driver_name, src->repo, path, src->default_driver)) < 0) return error; - driver = merge_driver_lookup_with_default(driver_name); + driver = merge_driver_lookup_with_wildcard(driver_name); - if (driver->check) + if (driver && driver->check) { error = driver->check(driver, &data, driver_name, src); - if (error == GIT_PASSTHROUGH) - driver = &git_merge_driver__text; - else if (error == GIT_EMERGECONFLICT) - driver = &git_merge_driver__binary; - else - goto done; + if (error == GIT_PASSTHROUGH) + driver = &git_merge_driver__text; + else if (error == GIT_EMERGECONFLICT) + driver = &git_merge_driver__binary; + else + goto done; + } error = 0; data = NULL; From d3f0875a60c4112f004c21aa0e02db7af9e79597 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Fri, 25 Dec 2015 00:34:39 -0600 Subject: [PATCH 07/14] merge driver: tests for custom default merge drivers --- tests/merge/driver.c | 59 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/merge/driver.c b/tests/merge/driver.c index d45178281..c29f87f8a 100644 --- a/tests/merge/driver.c +++ b/tests/merge/driver.c @@ -391,3 +391,62 @@ void test_merge_driver__apply_can_conflict(void) git_merge_driver_unregister("conflict"); } +void test_merge_driver__default_can_be_specified(void) +{ + git_oid their_id; + git_annotated_commit *their_head; + git_merge_options merge_opts = GIT_MERGE_OPTIONS_INIT; + const char *expected = "This is the `custom` driver.\n"; + + merge_opts.default_driver = "custom"; + + cl_git_pass(git_oid_fromstr(&their_id, BRANCH_ID)); + cl_git_pass(git_annotated_commit_lookup(&their_head, repo, &their_id)); + + cl_git_pass(git_merge(repo, (const git_annotated_commit **)&their_head, + 1, &merge_opts, NULL)); + + git_annotated_commit_free(their_head); + + cl_assert_equal_file(expected, strlen(expected), + TEST_REPO_PATH "/applied.txt"); +} + +void test_merge_driver__honors_builtin_mergedefault(void) +{ + const git_index_entry *ancestor, *ours, *theirs; + + cl_repo_set_string(repo, "merge.default", "binary"); + merge_branch(); + + cl_git_pass(git_index_conflict_get(&ancestor, &ours, &theirs, + repo_index, "automergeable.txt")); +} + +void test_merge_driver__honors_custom_mergedefault(void) +{ + const char *expected = "This is the `custom` driver.\n"; + + cl_repo_set_string(repo, "merge.default", "custom"); + merge_branch(); + + cl_assert_equal_file(expected, strlen(expected), + TEST_REPO_PATH "/applied.txt"); +} + +void test_merge_driver__mergedefault_deferring_falls_back_to_text(void) +{ + const git_index_entry *idx; + + cl_git_pass(git_merge_driver_register("defer", + &test_driver_defer_check.base)); + + cl_repo_set_string(repo, "merge.default", "defer"); + merge_branch(); + + cl_assert((idx = git_index_get_bypath(repo_index, "automergeable.txt", 0))); + cl_assert_equal_oid(&automergeable_id, &idx->id); + + git_merge_driver_unregister("defer"); +} + From 58d33126d44eeff69e375e500d1e77e03e2665a0 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Sat, 26 Dec 2015 19:47:17 +0000 Subject: [PATCH 08/14] merge driver: tests for set and unset merge attribute Ensure that setting the merge attribute forces the built-in default `text` driver and does *not* honor the `merge.default` configuration option. Further ensure that unsetting the merge attribute forces a conflict (the `binary` driver). --- tests/merge/driver.c | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/tests/merge/driver.c b/tests/merge/driver.c index c29f87f8a..26041eca7 100644 --- a/tests/merge/driver.c +++ b/tests/merge/driver.c @@ -146,7 +146,15 @@ static void set_gitattributes_to(const char *driver) { git_buf line = GIT_BUF_INIT; - cl_git_pass(git_buf_printf(&line, "automergeable.txt merge=%s\n", driver)); + if (driver && strcmp(driver, "")) + git_buf_printf(&line, "automergeable.txt merge=%s\n", driver); + else if (driver) + git_buf_printf(&line, "automergeable.txt merge\n"); + else + git_buf_printf(&line, "automergeable.txt -merge\n"); + + cl_assert(!git_buf_oom(&line)); + cl_git_mkfile(TEST_REPO_PATH "/.gitattributes", line.ptr); git_buf_free(&line); } @@ -450,3 +458,30 @@ void test_merge_driver__mergedefault_deferring_falls_back_to_text(void) git_merge_driver_unregister("defer"); } +void test_merge_driver__set_forces_text(void) +{ + const git_index_entry *idx; + + /* `merge` without specifying a driver indicates `text` */ + set_gitattributes_to(""); + cl_repo_set_string(repo, "merge.default", "custom"); + + merge_branch(); + + cl_assert((idx = git_index_get_bypath(repo_index, "automergeable.txt", 0))); + cl_assert_equal_oid(&automergeable_id, &idx->id); +} + +void test_merge_driver__unset_forces_binary(void) +{ + const git_index_entry *ancestor, *ours, *theirs; + + /* `-merge` without specifying a driver indicates `binary` */ + set_gitattributes_to(NULL); + cl_repo_set_string(repo, "merge.default", "custom"); + + merge_branch(); + + cl_git_pass(git_index_conflict_get(&ancestor, &ours, &theirs, + repo_index, "automergeable.txt")); +} From 4662583692e92ea1b0eb06e37ffe9d363740db30 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Sun, 7 Feb 2016 15:19:43 -0800 Subject: [PATCH 09/14] merge driver: correct indentation --- src/merge.c | 26 +++++++++++++------------- src/merge_driver.c | 34 +++++++++++++++++----------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/merge.c b/src/merge.c index 9a6442a91..5ef0f3609 100644 --- a/src/merge.c +++ b/src/merge.c @@ -261,7 +261,7 @@ int git_merge_base(git_oid *out, git_repository *repo, const git_oid *one, const int git_merge_bases(git_oidarray *out, git_repository *repo, const git_oid *one, const git_oid *two) { int error; - git_revwalk *walk; + git_revwalk *walk; git_commit_list *result, *list; git_array_oid_t array; @@ -925,8 +925,8 @@ static int merge_conflict_resolve_contents( goto done; } - error = merge_conflict_invoke_driver(&merge_result, - driver, data, diff_list, &source); + error = merge_conflict_invoke_driver(&merge_result, + driver, data, diff_list, &source); if (error == GIT_PASSTHROUGH) { data = NULL; @@ -934,12 +934,12 @@ static int merge_conflict_resolve_contents( &git_merge_driver__text, data, diff_list, &source); } - if (error < 0) { - if (error == GIT_EMERGECONFLICT) - error = 0; + if (error < 0) { + if (error == GIT_EMERGECONFLICT) + error = 0; - goto done; - } + goto done; + } git_vector_insert(&diff_list->staged, merge_result); git_vector_insert(&diff_list->resolved, (git_merge_diff *)conflict); @@ -2199,14 +2199,14 @@ static int merge_annotated_commits( git_iterator *base_iter = NULL, *our_iter = NULL, *their_iter = NULL; int error; - if ((error = compute_base(&base, repo, ours, theirs, opts, + if ((error = compute_base(&base, repo, ours, theirs, opts, recursion_level)) < 0) { - if (error != GIT_ENOTFOUND) - goto done; + if (error != GIT_ENOTFOUND) + goto done; - giterr_clear(); - } + giterr_clear(); + } if ((error = iterator_for_annotated_commit(&base_iter, base)) < 0 || (error = iterator_for_annotated_commit(&our_iter, ours)) < 0 || diff --git a/src/merge_driver.c b/src/merge_driver.c index 791afe0b7..8795f2f2a 100644 --- a/src/merge_driver.c +++ b/src/merge_driver.c @@ -143,20 +143,20 @@ static void merge_driver_registry_shutdown(void) { struct merge_driver_registry *reg; git_merge_driver_entry *entry; - size_t i; + size_t i; - if ((reg = git__swap(merge_driver_registry, NULL)) == NULL) - return; + if ((reg = git__swap(merge_driver_registry, NULL)) == NULL) + return; - git_vector_foreach(®->drivers, i, entry) { - if (entry && entry->driver->shutdown) - entry->driver->shutdown(entry->driver); + git_vector_foreach(®->drivers, i, entry) { + if (entry && entry->driver->shutdown) + entry->driver->shutdown(entry->driver); - git__free(entry); - } + git__free(entry); + } - git_vector_free(®->drivers); - git__free(reg); + git_vector_free(®->drivers); + git__free(reg); } git_merge_driver git_merge_driver__normal = { @@ -202,15 +202,15 @@ static int merge_driver_registry_initialize(void) reg = git__calloc(1, sizeof(struct merge_driver_registry)); GITERR_CHECK_ALLOC(reg); - if ((error = git_vector_init(®->drivers, 3, merge_driver_entry_cmp)) < 0) + if ((error = git_vector_init(®->drivers, 3, merge_driver_entry_cmp)) < 0) goto done; - reg = git__compare_and_swap(&merge_driver_registry, NULL, reg); + reg = git__compare_and_swap(&merge_driver_registry, NULL, reg); - if (reg != NULL) - goto done; + if (reg != NULL) + goto done; - git__on_shutdown(merge_driver_registry_shutdown); + git__on_shutdown(merge_driver_registry_shutdown); if ((error = git_merge_driver_register( merge_driver_name__text, &git_merge_driver__text)) < 0 || @@ -294,7 +294,7 @@ git_merge_driver *git_merge_driver_lookup(const char *name) if (error == GIT_ENOTFOUND) return NULL; - entry = git_vector_get(&merge_driver_registry->drivers, pos); + entry = git_vector_get(&merge_driver_registry->drivers, pos); if (!entry->initialized) { if (entry->driver->initialize && @@ -304,7 +304,7 @@ git_merge_driver *git_merge_driver_lookup(const char *name) entry->initialized = 1; } - return entry->driver; + return entry->driver; } static int merge_driver_name_for_path( From 7a3ab14feec81fd397427b18fa8137e55546e198 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Sun, 7 Feb 2016 15:58:34 -0800 Subject: [PATCH 10/14] merge driver: get a pointer to favor --- src/merge.c | 2 +- src/merge_driver.c | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/merge.c b/src/merge.c index 5ef0f3609..bf75cf214 100644 --- a/src/merge.c +++ b/src/merge.c @@ -918,7 +918,7 @@ static int merge_conflict_resolve_contents( * favor flag) then let that override the gitattributes. */ driver = &git_merge_driver__normal; - data = (void *)file_opts->favor; + data = (void **)&file_opts->favor; } else { /* find the merge driver for this file */ if ((error = git_merge_driver_for_source(&driver, &data, &source)) < 0) diff --git a/src/merge_driver.c b/src/merge_driver.c index 8795f2f2a..59b5461e2 100644 --- a/src/merge_driver.c +++ b/src/merge_driver.c @@ -28,6 +28,9 @@ typedef struct { static struct merge_driver_registry *merge_driver_registry = NULL; +static git_merge_file_favor_t merge_favor_normal = GIT_MERGE_FILE_FAVOR_NORMAL; +static git_merge_file_favor_t merge_favor_union = GIT_MERGE_FILE_FAVOR_UNION; + static int merge_driver_apply( git_merge_driver *self, void **payload, @@ -37,6 +40,7 @@ static int merge_driver_apply( const git_merge_driver_source *src) { git_merge_file_options file_opts = GIT_MERGE_FILE_OPTIONS_INIT; + git_merge_file_favor_t *favor = (git_merge_file_favor_t *) *payload; git_merge_file_result result = {0}; int error; @@ -45,7 +49,8 @@ static int merge_driver_apply( if (src->file_opts) memcpy(&file_opts, src->file_opts, sizeof(git_merge_file_options)); - file_opts.favor = (git_merge_file_favor_t) *payload; + if (favor) + file_opts.favor = *favor; if ((error = git_merge_file_from_index(&result, src->repo, src->ancestor, src->ours, src->theirs, &file_opts)) < 0) @@ -87,7 +92,7 @@ static int merge_driver_text_check( GIT_UNUSED(name); GIT_UNUSED(src); - *payload = (void *)GIT_MERGE_FILE_FAVOR_NORMAL; + *payload = &merge_favor_normal; return 0; } @@ -101,7 +106,7 @@ static int merge_driver_union_check( GIT_UNUSED(name); GIT_UNUSED(src); - *payload = (void *)GIT_MERGE_FILE_FAVOR_UNION; + *payload = &merge_favor_union; return 0; } From 967e073dca8742d20bc14fd8c62b64b54f9a5326 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Sat, 27 Feb 2016 16:42:02 -0500 Subject: [PATCH 11/14] merge driver: correct global initialization --- src/global.c | 2 + src/merge.c | 1 + src/merge.h | 31 ------- src/merge_driver.c | 208 ++++++++++++++++++++++++++++----------------- src/merge_driver.h | 44 ++++++++++ 5 files changed, 176 insertions(+), 110 deletions(-) create mode 100644 src/merge_driver.h diff --git a/src/global.c b/src/global.c index c725b5184..cbd12ddda 100644 --- a/src/global.c +++ b/src/global.c @@ -9,6 +9,7 @@ #include "hash.h" #include "sysdir.h" #include "filter.h" +#include "merge_driver.h" #include "openssl_stream.h" #include "thread-utils.h" #include "git2/global.h" @@ -59,6 +60,7 @@ static int init_common(void) if ((ret = git_hash_global_init()) == 0 && (ret = git_sysdir_global_init()) == 0 && (ret = git_filter_global_init()) == 0 && + (ret = git_merge_driver_global_init()) == 0 && (ret = git_transport_ssh_global_init()) == 0) ret = git_openssl_stream_global_init(); diff --git a/src/merge.c b/src/merge.c index bf75cf214..742330583 100644 --- a/src/merge.c +++ b/src/merge.c @@ -29,6 +29,7 @@ #include "annotated_commit.h" #include "commit.h" #include "oidarray.h" +#include "merge_driver.h" #include "git2/types.h" #include "git2/repository.h" diff --git a/src/merge.h b/src/merge.h index 8eaa1ad92..68290e7cf 100644 --- a/src/merge.h +++ b/src/merge.h @@ -36,37 +36,6 @@ enum { }; -/* Merge drivers */ - -struct git_merge_driver_source { - git_repository *repo; - const char *default_driver; - const git_merge_file_options *file_opts; - - const git_index_entry *ancestor; - const git_index_entry *ours; - const git_index_entry *theirs; -}; - -extern int git_merge_driver_for_path( - char **name_out, - git_merge_driver **driver_out, - git_repository *repo, - const char *path); - -/* Basic (normal) merge driver, takes favor type as the payload argument */ -extern git_merge_driver git_merge_driver__normal; - -/* Merge driver for text files, performs a standard three-way merge */ -extern git_merge_driver git_merge_driver__text; - -/* Merge driver for union-style merging */ -extern git_merge_driver git_merge_driver__union; - -/* Merge driver for unmergeable (binary) files: always produces conflicts */ -extern git_merge_driver git_merge_driver__binary; - - /** Types of changes when files are merged from branch to branch. */ typedef enum { /* No conflict - a change only occurs in one branch. */ diff --git a/src/merge_driver.c b/src/merge_driver.c index 59b5461e2..e5bb169ed 100644 --- a/src/merge_driver.c +++ b/src/merge_driver.c @@ -9,6 +9,7 @@ #include "vector.h" #include "global.h" #include "merge.h" +#include "merge_driver.h" #include "git2/merge.h" #include "git2/sys/merge.h" @@ -17,6 +18,7 @@ static const char *merge_driver_name__union = "union"; static const char *merge_driver_name__binary = "binary"; struct merge_driver_registry { + git_rwlock lock; git_vector drivers; }; @@ -26,11 +28,14 @@ typedef struct { char name[GIT_FLEX_ARRAY]; } git_merge_driver_entry; -static struct merge_driver_registry *merge_driver_registry = NULL; +static struct merge_driver_registry merge_driver_registry; static git_merge_file_favor_t merge_favor_normal = GIT_MERGE_FILE_FAVOR_NORMAL; static git_merge_file_favor_t merge_favor_union = GIT_MERGE_FILE_FAVOR_UNION; +static void git_merge_driver_global_shutdown(void); + + static int merge_driver_apply( git_merge_driver *self, void **payload, @@ -144,26 +149,6 @@ static int merge_driver_entry_search(const void *a, const void *b) return strcmp(name_a, entry_b->name); } -static void merge_driver_registry_shutdown(void) -{ - struct merge_driver_registry *reg; - git_merge_driver_entry *entry; - size_t i; - - if ((reg = git__swap(merge_driver_registry, NULL)) == NULL) - return; - - git_vector_foreach(®->drivers, i, entry) { - if (entry && entry->driver->shutdown) - entry->driver->shutdown(entry->driver); - - git__free(entry); - } - - git_vector_free(®->drivers); - git__free(reg); -} - git_merge_driver git_merge_driver__normal = { GIT_MERGE_DRIVER_VERSION, NULL, @@ -196,51 +181,12 @@ git_merge_driver git_merge_driver__binary = { merge_driver_binary_apply }; -static int merge_driver_registry_initialize(void) -{ - struct merge_driver_registry *reg; - int error = 0; - - if (merge_driver_registry) - return 0; - - reg = git__calloc(1, sizeof(struct merge_driver_registry)); - GITERR_CHECK_ALLOC(reg); - - if ((error = git_vector_init(®->drivers, 3, merge_driver_entry_cmp)) < 0) - goto done; - - reg = git__compare_and_swap(&merge_driver_registry, NULL, reg); - - if (reg != NULL) - goto done; - - git__on_shutdown(merge_driver_registry_shutdown); - - if ((error = git_merge_driver_register( - merge_driver_name__text, &git_merge_driver__text)) < 0 || - (error = git_merge_driver_register( - merge_driver_name__union, &git_merge_driver__union)) < 0 || - (error = git_merge_driver_register( - merge_driver_name__binary, &git_merge_driver__binary)) < 0) - goto done; - -done: - if (error < 0) - merge_driver_registry_shutdown(); - - return error; -} - -int git_merge_driver_register(const char *name, git_merge_driver *driver) +/* Note: callers must lock the registry before calling this function */ +static int merge_driver_registry_insert( + const char *name, git_merge_driver *driver) { git_merge_driver_entry *entry; - assert(name && driver); - - if (merge_driver_registry_initialize() < 0) - return -1; - entry = git__calloc(1, sizeof(git_merge_driver_entry) + strlen(name) + 1); GITERR_CHECK_ALLOC(entry); @@ -248,21 +194,120 @@ int git_merge_driver_register(const char *name, git_merge_driver *driver) entry->driver = driver; return git_vector_insert_sorted( - &merge_driver_registry->drivers, entry, NULL); + &merge_driver_registry.drivers, entry, NULL); +} + +int git_merge_driver_global_init(void) +{ + int error; + + if (git_rwlock_init(&merge_driver_registry.lock) < 0) + return -1; + + if ((error = git_vector_init(&merge_driver_registry.drivers, 3, + merge_driver_entry_cmp)) < 0) + goto done; + + if ((error = merge_driver_registry_insert( + merge_driver_name__text, &git_merge_driver__text)) < 0 || + (error = merge_driver_registry_insert( + merge_driver_name__union, &git_merge_driver__union)) < 0 || + (error = merge_driver_registry_insert( + merge_driver_name__binary, &git_merge_driver__binary)) < 0) + + git__on_shutdown(git_merge_driver_global_shutdown); + +done: + if (error < 0) + git_vector_free_deep(&merge_driver_registry.drivers); + + return error; +} + +static void git_merge_driver_global_shutdown(void) +{ + git_merge_driver_entry *entry; + size_t i; + + if (git_rwlock_wrlock(&merge_driver_registry.lock) < 0) + return; + + git_vector_foreach(&merge_driver_registry.drivers, i, entry) { + if (entry->driver->shutdown) + entry->driver->shutdown(entry->driver); + + git__free(entry); + } + + git_vector_free(&merge_driver_registry.drivers); + + git_rwlock_wrunlock(&merge_driver_registry.lock); + git_rwlock_free(&merge_driver_registry.lock); +} + +/* Note: callers must lock the registry before calling this function */ +static int merge_driver_registry_find(size_t *pos, const char *name) +{ + return git_vector_search2(pos, &merge_driver_registry.drivers, + merge_driver_entry_search, name); +} + +/* Note: callers must lock the registry before calling this function */ +static git_merge_driver_entry *merge_driver_registry_lookup( + size_t *pos, const char *name) +{ + git_merge_driver_entry *entry = NULL; + + if (!merge_driver_registry_find(pos, name)) + entry = git_vector_get(&merge_driver_registry.drivers, *pos); + + return entry; +} + +int git_merge_driver_register(const char *name, git_merge_driver *driver) +{ + int error; + + assert(name && driver); + + if (git_rwlock_wrlock(&merge_driver_registry.lock) < 0) { + giterr_set(GITERR_OS, "failed to lock merge driver registry"); + return -1; + } + + if (!merge_driver_registry_find(NULL, name)) { + giterr_set(GITERR_MERGE, "attempt to reregister existing driver '%s'", + name); + error = GIT_EEXISTS; + goto done; + } + + error = merge_driver_registry_insert(name, driver); + +done: + git_rwlock_wrunlock(&merge_driver_registry.lock); + return error; } int git_merge_driver_unregister(const char *name) { git_merge_driver_entry *entry; size_t pos; - int error; + int error = 0; - if ((error = git_vector_search2(&pos, &merge_driver_registry->drivers, - merge_driver_entry_search, name)) < 0) - return error; + if (git_rwlock_wrlock(&merge_driver_registry.lock) < 0) { + giterr_set(GITERR_OS, "failed to lock merge driver registry"); + return -1; + } - entry = git_vector_get(&merge_driver_registry->drivers, pos); - git_vector_remove(&merge_driver_registry->drivers, pos); + if ((entry = merge_driver_registry_lookup(&pos, name)) == NULL) { + giterr_set(GITERR_MERGE, "cannot find merge driver '%s' to unregister", + name); + error = GIT_ENOTFOUND; + goto done; + } + + git_vector_remove(&merge_driver_registry.drivers, pos); if (entry->initialized && entry->driver->shutdown) { entry->driver->shutdown(entry->driver); @@ -271,7 +316,9 @@ int git_merge_driver_unregister(const char *name) git__free(entry); - return 0; +done: + git_rwlock_wrunlock(&merge_driver_registry.lock); + return error; } git_merge_driver *git_merge_driver_lookup(const char *name) @@ -282,24 +329,27 @@ git_merge_driver *git_merge_driver_lookup(const char *name) /* If we've decided the merge driver to use internally - and not * based on user configuration (in merge_driver_name_for_path) - * then we can use a hardcoded name instead of looking it up in - * the vector. + * then we can use a hardcoded name to compare instead of bothering + * to take a lock and look it up in the vector. */ if (name == merge_driver_name__text) return &git_merge_driver__text; else if (name == merge_driver_name__binary) return &git_merge_driver__binary; - if (merge_driver_registry_initialize() < 0) + if (git_rwlock_rdlock(&merge_driver_registry.lock) < 0) { + giterr_set(GITERR_OS, "failed to lock merge driver registry"); return NULL; + } - error = git_vector_search2(&pos, &merge_driver_registry->drivers, - merge_driver_entry_search, name); + entry = merge_driver_registry_lookup(&pos, name); - if (error == GIT_ENOTFOUND) + git_rwlock_rdunlock(&merge_driver_registry.lock); + + if (entry == NULL) { + giterr_set(GITERR_MERGE, "cannot use an unregistered filter"); return NULL; - - entry = git_vector_get(&merge_driver_registry->drivers, pos); + } if (!entry->initialized) { if (entry->driver->initialize && diff --git a/src/merge_driver.h b/src/merge_driver.h new file mode 100644 index 000000000..c0f75faf2 --- /dev/null +++ b/src/merge_driver.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_merge_driver_h__ +#define INCLUDE_merge_driver_h__ + +#include "git2/merge.h" +#include "git2/index.h" +#include "git2/sys/merge.h" + +struct git_merge_driver_source { + git_repository *repo; + const char *default_driver; + const git_merge_file_options *file_opts; + + const git_index_entry *ancestor; + const git_index_entry *ours; + const git_index_entry *theirs; +}; + +extern int git_merge_driver_global_init(void); + +extern int git_merge_driver_for_path( + char **name_out, + git_merge_driver **driver_out, + git_repository *repo, + const char *path); + +/* Basic (normal) merge driver, takes favor type as the payload argument */ +extern git_merge_driver git_merge_driver__normal; + +/* Merge driver for text files, performs a standard three-way merge */ +extern git_merge_driver git_merge_driver__text; + +/* Merge driver for union-style merging */ +extern git_merge_driver git_merge_driver__union; + +/* Merge driver for unmergeable (binary) files: always produces conflicts */ +extern git_merge_driver git_merge_driver__binary; + +#endif From 3f7d3df1ecc0ed7c31427aa37a8de62026c7edef Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Sat, 27 Feb 2016 16:57:12 -0500 Subject: [PATCH 12/14] merge driver: improve inline documentation --- include/git2/sys/merge.h | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/include/git2/sys/merge.h b/include/git2/sys/merge.h index a9f8ca8c2..bc9908e36 100644 --- a/include/git2/sys/merge.h +++ b/include/git2/sys/merge.h @@ -56,22 +56,12 @@ GIT_EXTERN(git_merge_file_options *) git_merge_driver_source_file_options( const git_merge_driver_source *src); -/* - * struct git_merge_driver - * - * The merge driver lifecycle: - * - initialize - first use of merge driver - * - shutdown - merge driver removed/unregistered from system - * - check - considering using merge driver for file - * - apply - apply merge driver to the file - * - cleanup - done with file - */ - /** * Initialize callback on merge driver * * Specified as `driver.initialize`, this is an optional callback invoked - * before a merge driver is first used. It will be called once at most. + * before a merge driver is first used. It will be called once at most + * per library lifetime. * * If non-NULL, the merge driver's `initialize` callback will be invoked * right before the first use of the driver, so you can defer expensive @@ -170,20 +160,33 @@ typedef void (*git_merge_driver_cleanup_fn)( * To associate extra data with a driver, allocate extra data and put the * `git_merge_driver` struct at the start of your data buffer, then cast * the `self` pointer to your larger structure when your callback is invoked. - * - * `version` should be set to GIT_MERGE_DRIVER_VERSION - * - * The `initialize`, `shutdown`, `check`, `apply`, and `cleanup` - * callbacks are all documented above with the respective function pointer - * typedefs. */ struct git_merge_driver { + /** The `version` should be set to `GIT_MERGE_DRIVER_VERSION`. */ unsigned int version; + /** Called when the merge driver is first used for any file. */ git_merge_driver_init_fn initialize; + + /** Called when the merge driver is unregistered from the system. */ git_merge_driver_shutdown_fn shutdown; + + /** + * Called to determine whether the merge driver should be invoked + * for a given file. If this function returns `GIT_PASSTHROUGH` + * then the `apply` function will not be invoked and the default + * (`text`) merge driver will instead be run. + */ git_merge_driver_check_fn check; + + /** + * Called to merge the contents of a conflict. If this function + * returns `GIT_PASSTHROUGH` then the default (`text`) merge driver + * will instead be invoked. If this function returns + * `GIT_EMERGECONFLICT` then the file will remain conflicted. git_merge_driver_apply_fn apply; + + /** Called when the system is done filtering for a file. */ git_merge_driver_cleanup_fn cleanup; }; From 6d8b2cdbee00f2c4e97796b52e05dd39bd655138 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Sun, 28 Feb 2016 09:34:11 -0500 Subject: [PATCH 13/14] merge driver: remove `check` callback Since the `apply` callback can defer, the `check` callback is not necessary. Removing the `check` callback further makes the `payload` unnecessary along with the `cleanup` callback. --- include/git2/sys/merge.h | 68 ++------------------ src/merge.c | 37 ++++++----- src/merge.h | 6 -- src/merge_driver.c | 121 +++++++++--------------------------- src/merge_driver.h | 24 +++++-- tests/merge/driver.c | 131 +++------------------------------------ 6 files changed, 81 insertions(+), 306 deletions(-) diff --git a/include/git2/sys/merge.h b/include/git2/sys/merge.h index bc9908e36..031941042 100644 --- a/include/git2/sys/merge.h +++ b/include/git2/sys/merge.h @@ -83,41 +83,7 @@ typedef int (*git_merge_driver_init_fn)(git_merge_driver *self); typedef void (*git_merge_driver_shutdown_fn)(git_merge_driver *self); /** - * Callback to decide if a given conflict can be resolved with this merge - * driver. - * - * Specified as `driver.check`, this is an optional callback that checks - * if the given conflict can be resolved with this merge driver. - * - * It should return 0 if the merge driver should be applied (i.e. success), - * `GIT_PASSTHROUGH` if the driver is not available, which is the equivalent - * of an unregistered or nonexistent merge driver. In this case, the default - * (`text`) driver will be run. This is useful if you register a wildcard - * merge driver but are not interested in handling the requested file (and - * should just fallback). The driver can also return `GIT_EMERGECONFLICT` - * if the driver is not able to produce a merge result, and the file will - * remain conflicted. Any other errors will fail and return to the caller. - * - * The `name` will be set to the name of the driver as configured in the - * attributes. - * - * The `src` contains the data about the file to be merged. - * - * The `payload` will be a pointer to a reference payload for the driver. - * This will start as NULL, but `check` can assign to this pointer for - * later use by the `apply` callback. Note that the value should be heap - * allocated (not stack), so that it doesn't go away before the `apply` - * callback can use it. If a driver allocates and assigns a value to the - * `payload`, it will need a `cleanup` callback to free the payload. - */ -typedef int (*git_merge_driver_check_fn)( - git_merge_driver *self, - void **payload, - const char *name, - const git_merge_driver_source *src); - -/** - * Callback to actually perform the merge. + * Callback to perform the merge. * * Specified as `driver.apply`, this is the callback that actually does the * merge. If it can successfully perform a merge, it should populate @@ -129,31 +95,19 @@ typedef int (*git_merge_driver_check_fn)( * and the file will remain conflicted. Any other errors will fail and * return to the caller. * - * The `src` contains the data about the file to be merged. + * The `filter_name` contains the name of the filter that was invoked, as + * specified by the file's attributes. * - * The `payload` value will refer to any payload that was set by the - * `check` callback. It may be read from or written to as needed. + * The `src` contains the data about the file to be merged. */ typedef int (*git_merge_driver_apply_fn)( git_merge_driver *self, - void **payload, const char **path_out, uint32_t *mode_out, git_buf *merged_out, + const char *filter_name, const git_merge_driver_source *src); -/** - * Callback to clean up after merge has been performed. - * - * Specified as `driver.cleanup`, this is an optional callback invoked - * after the driver has been run. If the `check` or `apply` callbacks - * allocated a `payload` to keep per-source merge driver state, use this - * callback to free that payload and release resources as required. - */ -typedef void (*git_merge_driver_cleanup_fn)( - git_merge_driver *self, - void *payload); - /** * Merge driver structure used to register custom merge drivers. * @@ -171,23 +125,13 @@ struct git_merge_driver { /** Called when the merge driver is unregistered from the system. */ git_merge_driver_shutdown_fn shutdown; - /** - * Called to determine whether the merge driver should be invoked - * for a given file. If this function returns `GIT_PASSTHROUGH` - * then the `apply` function will not be invoked and the default - * (`text`) merge driver will instead be run. - */ - git_merge_driver_check_fn check; - /** * Called to merge the contents of a conflict. If this function * returns `GIT_PASSTHROUGH` then the default (`text`) merge driver * will instead be invoked. If this function returns * `GIT_EMERGECONFLICT` then the file will remain conflicted. + */ git_merge_driver_apply_fn apply; - - /** Called when the system is done filtering for a file. */ - git_merge_driver_cleanup_fn cleanup; }; #define GIT_MERGE_DRIVER_VERSION 1 diff --git a/src/merge.c b/src/merge.c index 742330583..27c0cea95 100644 --- a/src/merge.c +++ b/src/merge.c @@ -838,10 +838,10 @@ static bool merge_conflict_can_resolve_contents( static int merge_conflict_invoke_driver( git_index_entry **out, + const char *name, git_merge_driver *driver, - void *data, git_merge_diff_list *diff_list, - git_merge_driver_source *source) + git_merge_driver_source *src) { git_index_entry *result; git_buf buf = GIT_BUF_INIT; @@ -853,10 +853,8 @@ static int merge_conflict_invoke_driver( *out = NULL; - if ((error = driver->apply(driver, &data, &path, &mode, &buf, source)) < 0) - goto done; - - if ((error = git_repository_odb(&odb, source->repo)) < 0 || + if ((error = driver->apply(driver, &path, &mode, &buf, name, src)) < 0 || + (error = git_repository_odb(&odb, src->repo)) < 0 || (error = git_odb_write(&oid, odb, buf.ptr, buf.size, GIT_OBJ_BLOB)) < 0) goto done; @@ -873,9 +871,6 @@ static int merge_conflict_invoke_driver( *out = result; done: - if (driver->cleanup) - driver->cleanup(driver, data); - git_buf_free(&buf); git_odb_free(odb); @@ -892,9 +887,10 @@ static int merge_conflict_resolve_contents( git_merge_driver_source source = {0}; git_merge_file_result result = {0}; git_merge_driver *driver; + git_merge_driver__builtin builtin = {{0}}; git_index_entry *merge_result; git_odb *odb = NULL; - void *data; + const char *name; int error = 0; assert(resolved && diff_list && conflict); @@ -916,23 +912,26 @@ static int merge_conflict_resolve_contents( if (file_opts->favor != GIT_MERGE_FILE_FAVOR_NORMAL) { /* if the user requested a particular type of resolution (via the - * favor flag) then let that override the gitattributes. + * favor flag) then let that override the gitattributes and use + * the builtin driver. */ - driver = &git_merge_driver__normal; - data = (void **)&file_opts->favor; + name = "text"; + builtin.base.apply = git_merge_driver__builtin_apply; + builtin.favor = file_opts->favor; + + driver = &builtin.base; } else { /* find the merge driver for this file */ - if ((error = git_merge_driver_for_source(&driver, &data, &source)) < 0) + if ((error = git_merge_driver_for_source(&name, &driver, &source)) < 0) goto done; } - error = merge_conflict_invoke_driver(&merge_result, - driver, data, diff_list, &source); + error = merge_conflict_invoke_driver(&merge_result, name, driver, + diff_list, &source); if (error == GIT_PASSTHROUGH) { - data = NULL; - error = merge_conflict_invoke_driver(&merge_result, - &git_merge_driver__text, data, diff_list, &source); + error = merge_conflict_invoke_driver(&merge_result, "text", + &git_merge_driver__text.base, diff_list, &source); } if (error < 0) { diff --git a/src/merge.h b/src/merge.h index 68290e7cf..f8cac161f 100644 --- a/src/merge.h +++ b/src/merge.h @@ -145,12 +145,6 @@ int git_merge_diff_list__find_renames(git_repository *repo, git_merge_diff_list void git_merge_diff_list__free(git_merge_diff_list *diff_list); -/* Merge driver configuration */ -int git_merge_driver_for_source( - git_merge_driver **driver_out, - void **data_out, - const git_merge_driver_source *src); - /* Merge metadata setup */ int git_merge__setup( diff --git a/src/merge_driver.c b/src/merge_driver.c index e5bb169ed..cc039dbb5 100644 --- a/src/merge_driver.c +++ b/src/merge_driver.c @@ -30,32 +30,29 @@ typedef struct { static struct merge_driver_registry merge_driver_registry; -static git_merge_file_favor_t merge_favor_normal = GIT_MERGE_FILE_FAVOR_NORMAL; -static git_merge_file_favor_t merge_favor_union = GIT_MERGE_FILE_FAVOR_UNION; - static void git_merge_driver_global_shutdown(void); -static int merge_driver_apply( +int git_merge_driver__builtin_apply( git_merge_driver *self, - void **payload, const char **path_out, uint32_t *mode_out, git_buf *merged_out, + const char *filter_name, const git_merge_driver_source *src) { + git_merge_driver__builtin *driver = (git_merge_driver__builtin *)self; git_merge_file_options file_opts = GIT_MERGE_FILE_OPTIONS_INIT; - git_merge_file_favor_t *favor = (git_merge_file_favor_t *) *payload; git_merge_file_result result = {0}; int error; - GIT_UNUSED(self); + GIT_UNUSED(filter_name); if (src->file_opts) memcpy(&file_opts, src->file_opts, sizeof(git_merge_file_options)); - if (favor) - file_opts.favor = *favor; + if (driver->favor) + file_opts.favor = driver->favor; if ((error = git_merge_file_from_index(&result, src->repo, src->ancestor, src->ours, src->theirs, &file_opts)) < 0) @@ -87,47 +84,19 @@ done: return error; } -static int merge_driver_text_check( - git_merge_driver *self, - void **payload, - const char *name, - const git_merge_driver_source *src) -{ - GIT_UNUSED(self); - GIT_UNUSED(name); - GIT_UNUSED(src); - - *payload = &merge_favor_normal; - return 0; -} - -static int merge_driver_union_check( - git_merge_driver *self, - void **payload, - const char *name, - const git_merge_driver_source *src) -{ - GIT_UNUSED(self); - GIT_UNUSED(name); - GIT_UNUSED(src); - - *payload = &merge_favor_union; - return 0; -} - static int merge_driver_binary_apply( git_merge_driver *self, - void **payload, const char **path_out, uint32_t *mode_out, git_buf *merged_out, + const char *filter_name, const git_merge_driver_source *src) { GIT_UNUSED(self); - GIT_UNUSED(payload); GIT_UNUSED(path_out); GIT_UNUSED(mode_out); GIT_UNUSED(merged_out); + GIT_UNUSED(filter_name); GIT_UNUSED(src); return GIT_EMERGECONFLICT; @@ -149,35 +118,30 @@ static int merge_driver_entry_search(const void *a, const void *b) return strcmp(name_a, entry_b->name); } -git_merge_driver git_merge_driver__normal = { - GIT_MERGE_DRIVER_VERSION, - NULL, - NULL, - NULL, - merge_driver_apply +git_merge_driver__builtin git_merge_driver__text = { + { + GIT_MERGE_DRIVER_VERSION, + NULL, + NULL, + git_merge_driver__builtin_apply, + }, + GIT_MERGE_FILE_FAVOR_NORMAL }; -git_merge_driver git_merge_driver__text = { - GIT_MERGE_DRIVER_VERSION, - NULL, - NULL, - merge_driver_text_check, - merge_driver_apply -}; - -git_merge_driver git_merge_driver__union = { - GIT_MERGE_DRIVER_VERSION, - NULL, - NULL, - merge_driver_union_check, - merge_driver_apply +git_merge_driver__builtin git_merge_driver__union = { + { + GIT_MERGE_DRIVER_VERSION, + NULL, + NULL, + git_merge_driver__builtin_apply, + }, + GIT_MERGE_FILE_FAVOR_UNION }; git_merge_driver git_merge_driver__binary = { GIT_MERGE_DRIVER_VERSION, NULL, NULL, - NULL, merge_driver_binary_apply }; @@ -209,9 +173,9 @@ int git_merge_driver_global_init(void) goto done; if ((error = merge_driver_registry_insert( - merge_driver_name__text, &git_merge_driver__text)) < 0 || + merge_driver_name__text, &git_merge_driver__text.base)) < 0 || (error = merge_driver_registry_insert( - merge_driver_name__union, &git_merge_driver__union)) < 0 || + merge_driver_name__union, &git_merge_driver__union.base)) < 0 || (error = merge_driver_registry_insert( merge_driver_name__binary, &git_merge_driver__binary)) < 0) @@ -333,7 +297,7 @@ git_merge_driver *git_merge_driver_lookup(const char *name) * to take a lock and look it up in the vector. */ if (name == merge_driver_name__text) - return &git_merge_driver__text; + return &git_merge_driver__text.base; else if (name == merge_driver_name__binary) return &git_merge_driver__binary; @@ -409,13 +373,11 @@ GIT_INLINE(git_merge_driver *) merge_driver_lookup_with_wildcard( } int git_merge_driver_for_source( + const char **name_out, git_merge_driver **driver_out, - void **data_out, const git_merge_driver_source *src) { const char *path, *driver_name; - git_merge_driver *driver; - void *data = NULL; int error = 0; path = git_merge_file__best_path( @@ -427,31 +389,8 @@ int git_merge_driver_for_source( &driver_name, src->repo, path, src->default_driver)) < 0) return error; - driver = merge_driver_lookup_with_wildcard(driver_name); - - if (driver && driver->check) { - error = driver->check(driver, &data, driver_name, src); - - if (error == GIT_PASSTHROUGH) - driver = &git_merge_driver__text; - else if (error == GIT_EMERGECONFLICT) - driver = &git_merge_driver__binary; - else - goto done; - } - - error = 0; - data = NULL; - - if (driver->check) - error = driver->check(driver, &data, driver_name, src); - - /* the text and binary drivers must succeed their check */ - assert(error == 0); - -done: - *driver_out = driver; - *data_out = data; + *name_out = driver_name; + *driver_out = merge_driver_lookup_with_wildcard(driver_name); return error; } diff --git a/src/merge_driver.h b/src/merge_driver.h index c0f75faf2..bde27502c 100644 --- a/src/merge_driver.h +++ b/src/merge_driver.h @@ -21,6 +21,11 @@ struct git_merge_driver_source { const git_index_entry *theirs; }; +typedef struct git_merge_driver__builtin { + git_merge_driver base; + git_merge_file_favor_t favor; +} git_merge_driver__builtin; + extern int git_merge_driver_global_init(void); extern int git_merge_driver_for_path( @@ -29,14 +34,25 @@ extern int git_merge_driver_for_path( git_repository *repo, const char *path); -/* Basic (normal) merge driver, takes favor type as the payload argument */ -extern git_merge_driver git_merge_driver__normal; +/* Merge driver configuration */ +extern int git_merge_driver_for_source( + const char **name_out, + git_merge_driver **driver_out, + const git_merge_driver_source *src); + +extern int git_merge_driver__builtin_apply( + git_merge_driver *self, + const char **path_out, + uint32_t *mode_out, + git_buf *merged_out, + const char *filter_name, + const git_merge_driver_source *src); /* Merge driver for text files, performs a standard three-way merge */ -extern git_merge_driver git_merge_driver__text; +extern git_merge_driver__builtin git_merge_driver__text; /* Merge driver for union-style merging */ -extern git_merge_driver git_merge_driver__union; +extern git_merge_driver__builtin git_merge_driver__union; /* Merge driver for unmergeable (binary) files: always produces conflicts */ extern git_merge_driver git_merge_driver__binary; diff --git a/tests/merge/driver.c b/tests/merge/driver.c index 26041eca7..670c2c482 100644 --- a/tests/merge/driver.c +++ b/tests/merge/driver.c @@ -63,27 +63,12 @@ static void test_driver_shutdown(git_merge_driver *s) self->shutdown = 1; } -static int test_driver_check( - git_merge_driver *s, - void **payload, - const char *name, - const git_merge_driver_source *src) -{ - GIT_UNUSED(s); - GIT_UNUSED(src); - - *payload = git__strdup(name); - GITERR_CHECK_ALLOC(*payload); - - return 0; -} - static int test_driver_apply( git_merge_driver *s, - void **payload, const char **path_out, uint32_t *mode_out, git_buf *merged_out, + const char *filter_name, const git_merge_driver_source *src) { GIT_UNUSED(s); @@ -93,25 +78,15 @@ static int test_driver_apply( *mode_out = GIT_FILEMODE_BLOB; return git_buf_printf(merged_out, "This is the `%s` driver.\n", - (char *)*payload); + filter_name); } -static void test_driver_cleanup(git_merge_driver *s, void *payload) -{ - GIT_UNUSED(s); - - git__free(payload); -} - - static struct test_merge_driver test_driver_custom = { { GIT_MERGE_DRIVER_VERSION, test_driver_init, test_driver_shutdown, - test_driver_check, test_driver_apply, - test_driver_cleanup }, 0, 0, @@ -122,9 +97,7 @@ static struct test_merge_driver test_driver_wildcard = { GIT_MERGE_DRIVER_VERSION, test_driver_init, test_driver_shutdown, - test_driver_check, test_driver_apply, - test_driver_cleanup }, 0, 0, @@ -219,62 +192,19 @@ void test_merge_driver__shutdown_is_called(void) test_drivers_register(); } -static int defer_driver_check( - git_merge_driver *s, - void **payload, - const char *name, - const git_merge_driver_source *src) -{ - GIT_UNUSED(s); - GIT_UNUSED(payload); - GIT_UNUSED(name); - GIT_UNUSED(src); - - return GIT_PASSTHROUGH; -} - -static struct test_merge_driver test_driver_defer_check = { - { - GIT_MERGE_DRIVER_VERSION, - test_driver_init, - test_driver_shutdown, - defer_driver_check, - test_driver_apply, - test_driver_cleanup - }, - 0, - 0, -}; - -void test_merge_driver__check_can_defer(void) -{ - const git_index_entry *idx; - - cl_git_pass(git_merge_driver_register("defer", - &test_driver_defer_check.base)); - - set_gitattributes_to("defer"); - merge_branch(); - - cl_assert((idx = git_index_get_bypath(repo_index, "automergeable.txt", 0))); - cl_assert_equal_oid(&automergeable_id, &idx->id); - - git_merge_driver_unregister("defer"); -} - static int defer_driver_apply( git_merge_driver *s, - void **payload, const char **path_out, uint32_t *mode_out, git_buf *merged_out, + const char *filter_name, const git_merge_driver_source *src) { GIT_UNUSED(s); - GIT_UNUSED(payload); GIT_UNUSED(path_out); GIT_UNUSED(mode_out); GIT_UNUSED(merged_out); + GIT_UNUSED(filter_name); GIT_UNUSED(src); return GIT_PASSTHROUGH; @@ -285,9 +215,7 @@ static struct test_merge_driver test_driver_defer_apply = { GIT_MERGE_DRIVER_VERSION, test_driver_init, test_driver_shutdown, - test_driver_check, defer_driver_apply, - test_driver_cleanup }, 0, 0, @@ -309,62 +237,19 @@ void test_merge_driver__apply_can_defer(void) git_merge_driver_unregister("defer"); } -static int conflict_driver_check( - git_merge_driver *s, - void **payload, - const char *name, - const git_merge_driver_source *src) -{ - GIT_UNUSED(s); - GIT_UNUSED(payload); - GIT_UNUSED(name); - GIT_UNUSED(src); - - return GIT_EMERGECONFLICT; -} - -static struct test_merge_driver test_driver_conflict_check = { - { - GIT_MERGE_DRIVER_VERSION, - test_driver_init, - test_driver_shutdown, - conflict_driver_check, - test_driver_apply, - test_driver_cleanup - }, - 0, - 0, -}; - -void test_merge_driver__check_can_conflict(void) -{ - const git_index_entry *ancestor, *ours, *theirs; - - cl_git_pass(git_merge_driver_register("conflict", - &test_driver_conflict_check.base)); - - set_gitattributes_to("conflict"); - merge_branch(); - - cl_git_pass(git_index_conflict_get(&ancestor, &ours, &theirs, - repo_index, "automergeable.txt")); - - git_merge_driver_unregister("conflict"); -} - static int conflict_driver_apply( git_merge_driver *s, - void **payload, const char **path_out, uint32_t *mode_out, git_buf *merged_out, + const char *filter_name, const git_merge_driver_source *src) { GIT_UNUSED(s); - GIT_UNUSED(payload); GIT_UNUSED(path_out); GIT_UNUSED(mode_out); GIT_UNUSED(merged_out); + GIT_UNUSED(filter_name); GIT_UNUSED(src); return GIT_EMERGECONFLICT; @@ -375,9 +260,7 @@ static struct test_merge_driver test_driver_conflict_apply = { GIT_MERGE_DRIVER_VERSION, test_driver_init, test_driver_shutdown, - test_driver_check, conflict_driver_apply, - test_driver_cleanup }, 0, 0, @@ -447,7 +330,7 @@ void test_merge_driver__mergedefault_deferring_falls_back_to_text(void) const git_index_entry *idx; cl_git_pass(git_merge_driver_register("defer", - &test_driver_defer_check.base)); + &test_driver_defer_apply.base)); cl_repo_set_string(repo, "merge.default", "defer"); merge_branch(); From d953c4505e09756b4b4f72b431a51867281643ca Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Sun, 28 Feb 2016 21:30:00 -0500 Subject: [PATCH 14/14] merge drivers: handle configured but not found driver --- src/merge.c | 17 +++++++++++++---- tests/merge/driver.c | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/merge.c b/src/merge.c index 27c0cea95..a0f2405ff 100644 --- a/src/merge.c +++ b/src/merge.c @@ -891,7 +891,8 @@ static int merge_conflict_resolve_contents( git_index_entry *merge_result; git_odb *odb = NULL; const char *name; - int error = 0; + bool fallback = false; + int error; assert(resolved && diff_list && conflict); @@ -924,12 +925,20 @@ static int merge_conflict_resolve_contents( /* find the merge driver for this file */ if ((error = git_merge_driver_for_source(&name, &driver, &source)) < 0) goto done; + + if (driver == NULL) + fallback = true; } - error = merge_conflict_invoke_driver(&merge_result, name, driver, - diff_list, &source); + if (driver) { + error = merge_conflict_invoke_driver(&merge_result, name, driver, + diff_list, &source); - if (error == GIT_PASSTHROUGH) { + if (error == GIT_PASSTHROUGH) + fallback = true; + } + + if (fallback) { error = merge_conflict_invoke_driver(&merge_result, "text", &git_merge_driver__text.base, diff_list, &source); } diff --git a/tests/merge/driver.c b/tests/merge/driver.c index 670c2c482..c75a00742 100644 --- a/tests/merge/driver.c +++ b/tests/merge/driver.c @@ -368,3 +368,21 @@ void test_merge_driver__unset_forces_binary(void) cl_git_pass(git_index_conflict_get(&ancestor, &ours, &theirs, repo_index, "automergeable.txt")); } + +void test_merge_driver__not_configured_driver_falls_back(void) +{ + const git_index_entry *idx; + + test_drivers_unregister(); + + /* `merge` without specifying a driver indicates `text` */ + set_gitattributes_to("notfound"); + + merge_branch(); + + cl_assert((idx = git_index_get_bypath(repo_index, "automergeable.txt", 0))); + cl_assert_equal_oid(&automergeable_id, &idx->id); + + test_drivers_register(); +} +