diff options
author | Carlos Martín Nieto <carlosmn@github.com> | 2016-03-17 18:01:37 +0100 |
---|---|---|
committer | Carlos Martín Nieto <carlosmn@github.com> | 2016-03-17 18:01:37 +0100 |
commit | e18df18bea14e17f648209b733359564a5836d8a (patch) | |
tree | a0dc5b65ab27376cab6a2edc8f3700823c37543a | |
parent | e02acbb3c6076cc9acbcf8de7a258eee406b2921 (diff) | |
parent | d953c4505e09756b4b4f72b431a51867281643ca (diff) | |
download | libgit2-e18df18bea14e17f648209b733359564a5836d8a.tar.gz |
Merge pull request #3564 from ethomson/merge_drivers
Custom merge drivers and proper gitattributes `merge` handling
-rw-r--r-- | CHANGELOG.md | 8 | ||||
-rw-r--r-- | include/git2/merge.h | 11 | ||||
-rw-r--r-- | include/git2/sys/merge.h | 177 | ||||
-rw-r--r-- | src/global.c | 2 | ||||
-rw-r--r-- | src/merge.c | 223 | ||||
-rw-r--r-- | src/merge.h | 63 | ||||
-rw-r--r-- | src/merge_driver.c | 396 | ||||
-rw-r--r-- | src/merge_driver.h | 60 | ||||
-rw-r--r-- | src/merge_file.c | 60 | ||||
-rw-r--r-- | tests/merge/driver.c | 388 | ||||
-rw-r--r-- | tests/merge/workdir/simple.c | 36 |
11 files changed, 1309 insertions, 115 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/include/git2/sys/merge.h b/include/git2/sys/merge.h new file mode 100644 index 000000000..031941042 --- /dev/null +++ b/include/git2/sys/merge.h @@ -0,0 +1,177 @@ +/* + * 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); + + +/** + * 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 + * 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 + * 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 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 `filter_name` contains the name of the filter that was invoked, as + * specified by the file's attributes. + * + * The `src` contains the data about the file to be merged. + */ +typedef int (*git_merge_driver_apply_fn)( + 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 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. + */ +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 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; +}; + +#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/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 d2f92ccce..a0f2405ff 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" @@ -50,18 +51,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, @@ -273,7 +262,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; @@ -810,76 +799,158 @@ 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, + const char *name, + git_merge_driver *driver, + git_merge_diff_list *diff_list, + git_merge_driver_source *src) +{ + 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, &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; + + 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: + 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_options *merge_opts, + const git_merge_file_options *file_opts) +{ + 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; + const char *name; + bool fallback = false; + int error; + + 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.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; - 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 and use + * the builtin driver. + */ + 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(&name, &driver, &source)) < 0) + goto done; + + if (driver == NULL) + fallback = true; + } - if ((index_entry = git_pool_mallocz(&diff_list->pool, sizeof(git_index_entry))) == NULL) - GITERR_CHECK_ALLOC(index_entry); + if (driver) { + error = merge_conflict_invoke_driver(&merge_result, name, driver, + diff_list, &source); - index_entry->path = git_pool_strdup(&diff_list->pool, result.path); - GITERR_CHECK_ALLOC(index_entry->path); + if (error == GIT_PASSTHROUGH) + fallback = true; + } - index_entry->file_size = result.len; - index_entry->mode = result.mode; - git_oid_cpy(&index_entry->id, &automerge_oid); + if (fallback) { + error = merge_conflict_invoke_driver(&merge_result, "text", + &git_merge_driver__text.base, diff_list, &source); + } - git_vector_insert(&diff_list->staged, index_entry); + if (error < 0) { + if (error == GIT_EMERGECONFLICT) + error = 0; + + goto done; + } + + git_vector_insert(&diff_list->staged, merge_result); git_vector_insert(&diff_list->resolved, (git_merge_diff *)conflict); *resolved = 1; @@ -895,6 +966,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; @@ -902,16 +974,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_automerge(&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; @@ -1627,6 +1703,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); @@ -1644,6 +1721,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); @@ -1666,7 +1759,9 @@ static int merge_normalize_opts( opts->metric->payload = (void *)GIT_HASHSIG_SMART_WHITESPACE; } - return 0; +done: + git_config_entry_free(entry); + return error; } @@ -1878,7 +1973,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) { @@ -1899,6 +1994,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); @@ -2111,14 +2208,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.h b/src/merge.h index bd839be49..f8cac161f 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,19 @@ #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, +}; + + /** Types of changes when files are merged from branch to branch. */ typedef enum { /* No conflict - a change only occurs in one branch. */ @@ -70,7 +84,6 @@ typedef enum { GIT_MERGE_DIFF_DF_CHILD = (1 << 11), } git_merge_diff_type_t; - typedef struct { git_repository *repo; git_pool pool; @@ -152,4 +165,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..cc039dbb5 --- /dev/null +++ b/src/merge_driver.c @@ -0,0 +1,396 @@ +/* + * 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 "merge_driver.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_rwlock lock; + 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; + +static void git_merge_driver_global_shutdown(void); + + +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) +{ + git_merge_driver__builtin *driver = (git_merge_driver__builtin *)self; + git_merge_file_options file_opts = GIT_MERGE_FILE_OPTIONS_INIT; + git_merge_file_result result = {0}; + int error; + + GIT_UNUSED(filter_name); + + if (src->file_opts) + memcpy(&file_opts, src->file_opts, sizeof(git_merge_file_options)); + + 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) + 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_binary_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) +{ + GIT_UNUSED(self); + GIT_UNUSED(path_out); + GIT_UNUSED(mode_out); + GIT_UNUSED(merged_out); + GIT_UNUSED(filter_name); + 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); +} + +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__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, + merge_driver_binary_apply +}; + +/* 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; + + 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_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.base)) < 0 || + (error = merge_driver_registry_insert( + merge_driver_name__union, &git_merge_driver__union.base)) < 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 = 0; + + if (git_rwlock_wrlock(&merge_driver_registry.lock) < 0) { + giterr_set(GITERR_OS, "failed to lock merge driver registry"); + return -1; + } + + 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); + entry->initialized = false; + } + + git__free(entry); + +done: + git_rwlock_wrunlock(&merge_driver_registry.lock); + return error; +} + +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 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.base; + else if (name == merge_driver_name__binary) + return &git_merge_driver__binary; + + if (git_rwlock_rdlock(&merge_driver_registry.lock) < 0) { + giterr_set(GITERR_OS, "failed to lock merge driver registry"); + return NULL; + } + + entry = merge_driver_registry_lookup(&pos, name); + + git_rwlock_rdunlock(&merge_driver_registry.lock); + + if (entry == NULL) { + giterr_set(GITERR_MERGE, "cannot use an unregistered filter"); + return NULL; + } + + if (!entry->initialized) { + if (entry->driver->initialize && + (error = entry->driver->initialize(entry->driver)) < 0) + return NULL; + + entry->initialized = 1; + } + + return entry->driver; +} + +static int merge_driver_name_for_path( + const char **out, + git_repository *repo, + const char *path, + const char *default_driver) +{ + 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; + + /* unset: do not merge ("binary") */ + else if (GIT_ATTR_FALSE(value)) + *out = merge_driver_name__binary; + + else if (GIT_ATTR_UNSPECIFIED(value) && default_driver) + *out = default_driver; + + else if (GIT_ATTR_UNSPECIFIED(value)) + *out = merge_driver_name__text; + + 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( + const char **name_out, + git_merge_driver **driver_out, + const git_merge_driver_source *src) +{ + const char *path, *driver_name; + 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, src->default_driver)) < 0) + return error; + + *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 new file mode 100644 index 000000000..bde27502c --- /dev/null +++ b/src/merge_driver.h @@ -0,0 +1,60 @@ +/* + * 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; +}; + +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( + char **name_out, + git_merge_driver **driver_out, + git_repository *repo, + const char *path); + +/* 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__builtin git_merge_driver__text; + +/* Merge driver for union-style merging */ +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; + +#endif 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..c75a00742 --- /dev/null +++ b/tests/merge/driver.c @@ -0,0 +1,388 @@ +#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" + +#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); + +void test_merge_driver__initialize(void) +{ + git_config *cfg; + + 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)); + + 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_apply( + git_merge_driver *s, + 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(src); + + *path_out = "applied.txt"; + *mode_out = GIT_FILEMODE_BLOB; + + return git_buf_printf(merged_out, "This is the `%s` driver.\n", + filter_name); +} + +static struct test_merge_driver test_driver_custom = { + { + GIT_MERGE_DRIVER_VERSION, + test_driver_init, + test_driver_shutdown, + test_driver_apply, + }, + 0, + 0, +}; + +static struct test_merge_driver test_driver_wildcard = { + { + GIT_MERGE_DRIVER_VERSION, + test_driver_init, + test_driver_shutdown, + test_driver_apply, + }, + 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; + + 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); +} + +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(); +} + +static int defer_driver_apply( + git_merge_driver *s, + 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(path_out); + GIT_UNUSED(mode_out); + GIT_UNUSED(merged_out); + GIT_UNUSED(filter_name); + 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, + defer_driver_apply, + }, + 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"); +} + +static int conflict_driver_apply( + git_merge_driver *s, + 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(path_out); + GIT_UNUSED(mode_out); + GIT_UNUSED(merged_out); + GIT_UNUSED(filter_name); + 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, + conflict_driver_apply, + }, + 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"); +} + +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_apply.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"); +} + +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")); +} + +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(); +} + 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; |