summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarlos Martín Nieto <carlosmn@github.com>2016-03-17 18:01:37 +0100
committerCarlos Martín Nieto <carlosmn@github.com>2016-03-17 18:01:37 +0100
commite18df18bea14e17f648209b733359564a5836d8a (patch)
treea0dc5b65ab27376cab6a2edc8f3700823c37543a
parente02acbb3c6076cc9acbcf8de7a258eee406b2921 (diff)
parentd953c4505e09756b4b4f72b431a51867281643ca (diff)
downloadlibgit2-e18df18bea14e17f648209b733359564a5836d8a.tar.gz
Merge pull request #3564 from ethomson/merge_drivers
Custom merge drivers and proper gitattributes `merge` handling
-rw-r--r--CHANGELOG.md8
-rw-r--r--include/git2/merge.h11
-rw-r--r--include/git2/sys/merge.h177
-rw-r--r--src/global.c2
-rw-r--r--src/merge.c223
-rw-r--r--src/merge.h63
-rw-r--r--src/merge_driver.c396
-rw-r--r--src/merge_driver.h60
-rw-r--r--src/merge_file.c60
-rw-r--r--tests/merge/driver.c388
-rw-r--r--tests/merge/workdir/simple.c36
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;