diff options
author | Edward Thomson <ethomson@edwardthomson.com> | 2015-12-23 10:23:08 -0600 |
---|---|---|
committer | Edward Thomson <ethomson@github.com> | 2016-03-17 11:02:26 -0400 |
commit | 3f04219fcdcbc6369270eaf2d878d4fe7064254d (patch) | |
tree | 38163746ebb430e787d7a82bdec44cac5d82df4c | |
parent | 7a74590d8f952971088a90c584945ceefe1bf90e (diff) | |
download | libgit2-3f04219fcdcbc6369270eaf2d878d4fe7064254d.tar.gz |
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.
-rw-r--r-- | include/git2/sys/merge.h | 230 | ||||
-rw-r--r-- | src/merge.c | 190 | ||||
-rw-r--r-- | src/merge.h | 99 | ||||
-rw-r--r-- | src/merge_driver.c | 403 | ||||
-rw-r--r-- | src/merge_file.c | 60 | ||||
-rw-r--r-- | tests/merge/driver.c | 208 |
6 files changed, 1061 insertions, 129 deletions
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( - int *resolved, - git_merge_diff_list *diff_list, - const git_merge_diff *conflict, - const git_merge_file_options *file_opts) +static bool merge_conflict_can_resolve_contents( + const git_merge_diff *conflict) { - const git_index_entry *ancestor = NULL, *ours = NULL, *theirs = NULL; - git_merge_file_result result = {0}; - git_index_entry *index_entry; - git_odb *odb = NULL; - git_oid automerge_oid; - int error = 0; - - assert(resolved && diff_list && conflict); - - *resolved = 0; - if (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) || !GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry)) - return 0; + return false; /* Reject D/F conflicts */ if (conflict->type == GIT_MERGE_DIFF_DIRECTORY_FILE) - return 0; + return false; /* Reject submodules. */ if (S_ISGITLINK(conflict->ancestor_entry.mode) || S_ISGITLINK(conflict->our_entry.mode) || S_ISGITLINK(conflict->their_entry.mode)) - return 0; + 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 0; + 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 0; + 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) +{ + git_merge_driver_source source = {0}; + git_merge_file_result result = {0}; + git_merge_driver *driver; + git_index_entry *merge_result; + git_odb *odb = NULL; + void *data; + int error = 0; + + assert(resolved && diff_list && conflict); + + *resolved = 0; + + if (!merge_conflict_can_resolve_contents(conflict)) 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); + } + + if (error < 0) { + if (error == GIT_EMERGECONFLICT) + error = 0; - index_entry->file_size = result.len; - index_entry->mode = result.mode; - git_oid_cpy(&index_entry->id, &automerge_oid); + goto done; + } - git_vector_insert(&diff_list->staged, index_entry); + 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(); +} + |