summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEdward Thomson <ethomson@edwardthomson.com>2015-12-23 10:23:08 -0600
committerEdward Thomson <ethomson@github.com>2016-03-17 11:02:26 -0400
commit3f04219fcdcbc6369270eaf2d878d4fe7064254d (patch)
tree38163746ebb430e787d7a82bdec44cac5d82df4c
parent7a74590d8f952971088a90c584945ceefe1bf90e (diff)
downloadlibgit2-3f04219fcdcbc6369270eaf2d878d4fe7064254d.tar.gz
merge driver: introduce custom merge drivers
Consumers can now register custom merged drivers with `git_merge_driver_register`. This allows consumers to support the merge drivers, as configured in `.gitattributes`. Consumers will be asked to perform the file-level merge when a custom driver is configured.
-rw-r--r--include/git2/sys/merge.h230
-rw-r--r--src/merge.c190
-rw-r--r--src/merge.h99
-rw-r--r--src/merge_driver.c403
-rw-r--r--src/merge_file.c60
-rw-r--r--tests/merge/driver.c208
6 files changed, 1061 insertions, 129 deletions
diff --git a/include/git2/sys/merge.h b/include/git2/sys/merge.h
new file mode 100644
index 000000000..a9f8ca8c2
--- /dev/null
+++ b/include/git2/sys/merge.h
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+#ifndef INCLUDE_sys_git_merge_h__
+#define INCLUDE_sys_git_merge_h__
+
+/**
+ * @file git2/sys/merge.h
+ * @brief Git merge driver backend and plugin routines
+ * @defgroup git_backend Git custom backend APIs
+ * @ingroup Git
+ * @{
+ */
+GIT_BEGIN_DECL
+
+typedef struct git_merge_driver git_merge_driver;
+
+/**
+ * Look up a merge driver by name
+ *
+ * @param name The name of the merge driver
+ * @return Pointer to the merge driver object or NULL if not found
+ */
+GIT_EXTERN(git_merge_driver *) git_merge_driver_lookup(const char *name);
+
+#define GIT_MERGE_DRIVER_TEXT "text"
+#define GIT_MERGE_DRIVER_BINARY "binary"
+#define GIT_MERGE_DRIVER_UNION "union"
+
+/**
+ * A merge driver source represents the file to be merged
+ */
+typedef struct git_merge_driver_source git_merge_driver_source;
+
+/** Get the repository that the source data is coming from. */
+GIT_EXTERN(git_repository *) git_merge_driver_source_repo(
+ const git_merge_driver_source *src);
+
+/** Gets the ancestor of the file to merge. */
+GIT_EXTERN(git_index_entry *) git_merge_driver_source_ancestor(
+ const git_merge_driver_source *src);
+
+/** Gets the ours side of the file to merge. */
+GIT_EXTERN(git_index_entry *) git_merge_driver_source_ours(
+ const git_merge_driver_source *src);
+
+/** Gets the theirs side of the file to merge. */
+GIT_EXTERN(git_index_entry *) git_merge_driver_source_theirs(
+ const git_merge_driver_source *src);
+
+/** Gets the merge file options that the merge was invoked with */
+GIT_EXTERN(git_merge_file_options *) git_merge_driver_source_file_options(
+ const git_merge_driver_source *src);
+
+
+/*
+ * struct git_merge_driver
+ *
+ * The merge driver lifecycle:
+ * - initialize - first use of merge driver
+ * - shutdown - merge driver removed/unregistered from system
+ * - check - considering using merge driver for file
+ * - apply - apply merge driver to the file
+ * - cleanup - done with file
+ */
+
+/**
+ * Initialize callback on merge driver
+ *
+ * Specified as `driver.initialize`, this is an optional callback invoked
+ * before a merge driver is first used. It will be called once at most.
+ *
+ * If non-NULL, the merge driver's `initialize` callback will be invoked
+ * right before the first use of the driver, so you can defer expensive
+ * initialization operations (in case libgit2 is being used in a way that
+ * doesn't need the merge driver).
+ */
+typedef int (*git_merge_driver_init_fn)(git_merge_driver *self);
+
+/**
+ * Shutdown callback on merge driver
+ *
+ * Specified as `driver.shutdown`, this is an optional callback invoked
+ * when the merge driver is unregistered or when libgit2 is shutting down.
+ * It will be called once at most and should release resources as needed.
+ * This may be called even if the `initialize` callback was not made.
+ *
+ * Typically this function will free the `git_merge_driver` object itself.
+ */
+typedef void (*git_merge_driver_shutdown_fn)(git_merge_driver *self);
+
+/**
+ * Callback to decide if a given conflict can be resolved with this merge
+ * driver.
+ *
+ * Specified as `driver.check`, this is an optional callback that checks
+ * if the given conflict can be resolved with this merge driver.
+ *
+ * It should return 0 if the merge driver should be applied (i.e. success),
+ * `GIT_PASSTHROUGH` if the driver is not available, which is the equivalent
+ * of an unregistered or nonexistent merge driver. In this case, the default
+ * (`text`) driver will be run. This is useful if you register a wildcard
+ * merge driver but are not interested in handling the requested file (and
+ * should just fallback). The driver can also return `GIT_EMERGECONFLICT`
+ * if the driver is not able to produce a merge result, and the file will
+ * remain conflicted. Any other errors will fail and return to the caller.
+ *
+ * The `name` will be set to the name of the driver as configured in the
+ * attributes.
+ *
+ * The `src` contains the data about the file to be merged.
+ *
+ * The `payload` will be a pointer to a reference payload for the driver.
+ * This will start as NULL, but `check` can assign to this pointer for
+ * later use by the `apply` callback. Note that the value should be heap
+ * allocated (not stack), so that it doesn't go away before the `apply`
+ * callback can use it. If a driver allocates and assigns a value to the
+ * `payload`, it will need a `cleanup` callback to free the payload.
+ */
+typedef int (*git_merge_driver_check_fn)(
+ git_merge_driver *self,
+ void **payload,
+ const char *name,
+ const git_merge_driver_source *src);
+
+/**
+ * Callback to actually perform the merge.
+ *
+ * Specified as `driver.apply`, this is the callback that actually does the
+ * merge. If it can successfully perform a merge, it should populate
+ * `path_out` with a pointer to the filename to accept, `mode_out` with
+ * the resultant mode, and `merged_out` with the buffer of the merged file
+ * and then return 0. If the driver returns `GIT_PASSTHROUGH`, then the
+ * default merge driver should instead be run. It can also return
+ * `GIT_EMERGECONFLICT` if the driver is not able to produce a merge result,
+ * and the file will remain conflicted. Any other errors will fail and
+ * return to the caller.
+ *
+ * The `src` contains the data about the file to be merged.
+ *
+ * The `payload` value will refer to any payload that was set by the
+ * `check` callback. It may be read from or written to as needed.
+ */
+typedef int (*git_merge_driver_apply_fn)(
+ git_merge_driver *self,
+ void **payload,
+ const char **path_out,
+ uint32_t *mode_out,
+ git_buf *merged_out,
+ const git_merge_driver_source *src);
+
+/**
+ * Callback to clean up after merge has been performed.
+ *
+ * Specified as `driver.cleanup`, this is an optional callback invoked
+ * after the driver has been run. If the `check` or `apply` callbacks
+ * allocated a `payload` to keep per-source merge driver state, use this
+ * callback to free that payload and release resources as required.
+ */
+typedef void (*git_merge_driver_cleanup_fn)(
+ git_merge_driver *self,
+ void *payload);
+
+/**
+ * Merge driver structure used to register custom merge drivers.
+ *
+ * To associate extra data with a driver, allocate extra data and put the
+ * `git_merge_driver` struct at the start of your data buffer, then cast
+ * the `self` pointer to your larger structure when your callback is invoked.
+ *
+ * `version` should be set to GIT_MERGE_DRIVER_VERSION
+ *
+ * The `initialize`, `shutdown`, `check`, `apply`, and `cleanup`
+ * callbacks are all documented above with the respective function pointer
+ * typedefs.
+ */
+struct git_merge_driver {
+ unsigned int version;
+
+ git_merge_driver_init_fn initialize;
+ git_merge_driver_shutdown_fn shutdown;
+ git_merge_driver_check_fn check;
+ git_merge_driver_apply_fn apply;
+ git_merge_driver_cleanup_fn cleanup;
+};
+
+#define GIT_MERGE_DRIVER_VERSION 1
+
+/**
+ * Register a merge driver under a given name.
+ *
+ * As mentioned elsewhere, the initialize callback will not be invoked
+ * immediately. It is deferred until the driver is used in some way.
+ *
+ * Currently the merge driver registry is not thread safe, so any
+ * registering or deregistering of merge drivers must be done outside of
+ * any possible usage of the drivers (i.e. during application setup or
+ * shutdown).
+ *
+ * @param name The name of this driver to match an attribute. Attempting
+ * to register with an in-use name will return GIT_EEXISTS.
+ * @param driver The merge driver definition. This pointer will be stored
+ * as is by libgit2 so it must be a durable allocation (either
+ * static or on the heap).
+ * @return 0 on successful registry, error code <0 on failure
+ */
+GIT_EXTERN(int) git_merge_driver_register(
+ const char *name, git_merge_driver *driver);
+
+/**
+ * Remove the merge driver with the given name.
+ *
+ * Attempting to remove the builtin libgit2 merge drivers is not permitted
+ * and will return an error.
+ *
+ * Currently the merge driver registry is not thread safe, so any
+ * registering or deregistering of drivers must be done outside of any
+ * possible usage of the drivers (i.e. during application setup or shutdown).
+ *
+ * @param name The name under which the merge driver was registered
+ * @return 0 on success, error code <0 on failure
+ */
+GIT_EXTERN(int) git_merge_driver_unregister(const char *name);
+
+/** @} */
+GIT_END_DECL
+#endif
diff --git a/src/merge.c b/src/merge.c
index 1c2375442..0bc64ac13 100644
--- a/src/merge.c
+++ b/src/merge.c
@@ -50,18 +50,6 @@
#define GIT_MERGE_INDEX_ENTRY_ISFILE(X) S_ISREG((X).mode)
-/** Internal merge flags. */
-enum {
- /** The merge is for a virtual base in a recursive merge. */
- GIT_MERGE__VIRTUAL_BASE = (1 << 31),
-};
-
-enum {
- /** Accept the conflict file, staging it as the merge result. */
- GIT_MERGE_FILE_FAVOR__CONFLICTED = 4,
-};
-
-
typedef enum {
TREE_IDX_ANCESTOR = 0,
TREE_IDX_OURS = 1,
@@ -810,76 +798,148 @@ static int merge_conflict_resolve_one_renamed(
return error;
}
-static int merge_conflict_resolve_automerge(
- int *resolved,
- git_merge_diff_list *diff_list,
- const git_merge_diff *conflict,
- const git_merge_file_options *file_opts)
+static bool merge_conflict_can_resolve_contents(
+ const git_merge_diff *conflict)
{
- const git_index_entry *ancestor = NULL, *ours = NULL, *theirs = NULL;
- git_merge_file_result result = {0};
- git_index_entry *index_entry;
- git_odb *odb = NULL;
- git_oid automerge_oid;
- int error = 0;
-
- assert(resolved && diff_list && conflict);
-
- *resolved = 0;
-
if (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) ||
!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry))
- return 0;
+ return false;
/* Reject D/F conflicts */
if (conflict->type == GIT_MERGE_DIFF_DIRECTORY_FILE)
- return 0;
+ return false;
/* Reject submodules. */
if (S_ISGITLINK(conflict->ancestor_entry.mode) ||
S_ISGITLINK(conflict->our_entry.mode) ||
S_ISGITLINK(conflict->their_entry.mode))
- return 0;
+ return false;
/* Reject link/file conflicts. */
- if ((S_ISLNK(conflict->ancestor_entry.mode) ^ S_ISLNK(conflict->our_entry.mode)) ||
- (S_ISLNK(conflict->ancestor_entry.mode) ^ S_ISLNK(conflict->their_entry.mode)))
- return 0;
+ if ((S_ISLNK(conflict->ancestor_entry.mode) ^
+ S_ISLNK(conflict->our_entry.mode)) ||
+ (S_ISLNK(conflict->ancestor_entry.mode) ^
+ S_ISLNK(conflict->their_entry.mode)))
+ return false;
/* Reject name conflicts */
if (conflict->type == GIT_MERGE_DIFF_BOTH_RENAMED_2_TO_1 ||
conflict->type == GIT_MERGE_DIFF_RENAMED_ADDED)
- return 0;
+ return false;
if ((conflict->our_status & GIT_DELTA_RENAMED) == GIT_DELTA_RENAMED &&
(conflict->their_status & GIT_DELTA_RENAMED) == GIT_DELTA_RENAMED &&
strcmp(conflict->ancestor_entry.path, conflict->their_entry.path) != 0)
+ return false;
+
+ return true;
+}
+
+static int merge_conflict_invoke_driver(
+ git_index_entry **out,
+ git_merge_driver *driver,
+ void *data,
+ git_merge_diff_list *diff_list,
+ git_merge_driver_source *source)
+{
+ git_index_entry *result;
+ git_buf buf = GIT_BUF_INIT;
+ const char *path;
+ uint32_t mode;
+ git_odb *odb = NULL;
+ git_oid oid;
+ int error;
+
+ *out = NULL;
+
+ if ((error = driver->apply(driver, &data, &path, &mode, &buf, source)) < 0)
+ goto done;
+
+ if ((error = git_repository_odb(&odb, source->repo)) < 0 ||
+ (error = git_odb_write(&oid, odb, buf.ptr, buf.size, GIT_OBJ_BLOB)) < 0)
+ goto done;
+
+ result = git_pool_mallocz(&diff_list->pool, sizeof(git_index_entry));
+ GITERR_CHECK_ALLOC(result);
+
+ git_oid_cpy(&result->id, &oid);
+ result->mode = mode;
+ result->file_size = buf.size;
+
+ result->path = git_pool_strdup(&diff_list->pool, path);
+ GITERR_CHECK_ALLOC(result->path);
+
+ *out = result;
+
+done:
+ if (driver->cleanup)
+ driver->cleanup(driver, data);
+
+ git_buf_free(&buf);
+ git_odb_free(odb);
+
+ return error;
+}
+
+static int merge_conflict_resolve_contents(
+ int *resolved,
+ git_merge_diff_list *diff_list,
+ const git_merge_diff *conflict,
+ const git_merge_file_options *file_opts)
+{
+ git_merge_driver_source source = {0};
+ git_merge_file_result result = {0};
+ git_merge_driver *driver;
+ git_index_entry *merge_result;
+ git_odb *odb = NULL;
+ void *data;
+ int error = 0;
+
+ assert(resolved && diff_list && conflict);
+
+ *resolved = 0;
+
+ if (!merge_conflict_can_resolve_contents(conflict))
return 0;
- ancestor = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry) ?
+ source.repo = diff_list->repo;
+ source.file_opts = file_opts;
+ source.ancestor = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->ancestor_entry) ?
&conflict->ancestor_entry : NULL;
- ours = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) ?
+ source.ours = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) ?
&conflict->our_entry : NULL;
- theirs = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry) ?
+ source.theirs = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry) ?
&conflict->their_entry : NULL;
- if ((error = git_repository_odb(&odb, diff_list->repo)) < 0 ||
- (error = git_merge_file_from_index(&result, diff_list->repo, ancestor, ours, theirs, file_opts)) < 0 ||
- (!result.automergeable && !(file_opts->flags & GIT_MERGE_FILE_FAVOR__CONFLICTED)) ||
- (error = git_odb_write(&automerge_oid, odb, result.ptr, result.len, GIT_OBJ_BLOB)) < 0)
- goto done;
+ if (file_opts->favor != GIT_MERGE_FILE_FAVOR_NORMAL) {
+ /* if the user requested a particular type of resolution (via the
+ * favor flag) then let that override the gitattributes.
+ */
+ driver = &git_merge_driver__normal;
+ data = (void *)file_opts->favor;
+ } else {
+ /* find the merge driver for this file */
+ if ((error = git_merge_driver_for_source(&driver, &data, &source)) < 0)
+ goto done;
+ }
- if ((index_entry = git_pool_mallocz(&diff_list->pool, sizeof(git_index_entry))) == NULL)
- GITERR_CHECK_ALLOC(index_entry);
+ error = merge_conflict_invoke_driver(&merge_result,
+ driver, data, diff_list, &source);
- index_entry->path = git_pool_strdup(&diff_list->pool, result.path);
- GITERR_CHECK_ALLOC(index_entry->path);
+ if (error == GIT_PASSTHROUGH) {
+ data = NULL;
+ error = merge_conflict_invoke_driver(&merge_result,
+ &git_merge_driver__text, data, diff_list, &source);
+ }
+
+ if (error < 0) {
+ if (error == GIT_EMERGECONFLICT)
+ error = 0;
- index_entry->file_size = result.len;
- index_entry->mode = result.mode;
- git_oid_cpy(&index_entry->id, &automerge_oid);
+ goto done;
+ }
- git_vector_insert(&diff_list->staged, index_entry);
+ git_vector_insert(&diff_list->staged, merge_result);
git_vector_insert(&diff_list->resolved, (git_merge_diff *)conflict);
*resolved = 1;
@@ -911,7 +971,7 @@ static int merge_conflict_resolve(
if (!resolved && (error = merge_conflict_resolve_one_renamed(&resolved, diff_list, conflict)) < 0)
goto done;
- if (!resolved && (error = merge_conflict_resolve_automerge(&resolved, diff_list, conflict, file_opts)) < 0)
+ if (!resolved && (error = merge_conflict_resolve_contents(&resolved, diff_list, conflict, file_opts)) < 0)
goto done;
*out = resolved;
@@ -1819,28 +1879,6 @@ static git_iterator *iterator_given_or_empty(git_iterator **empty, git_iterator
return *empty;
}
-static int lookup_file_favor(
- git_merge_file_favor_t *file_favor,
- git_repository *repo,
- const char *path)
-{
- int error = 0;
- const char *value = NULL;
-
- if (path) {
- if ((error = git_attr_get(&value, repo, 0, path, "merge")) < 0)
- goto done;
-
- if (*file_favor == GIT_MERGE_FILE_FAVOR_NORMAL &&
- value && strcmp(value, "union") == 0) {
- *file_favor |= GIT_MERGE_FILE_FAVOR_UNION;
- }
- }
-
-done:
- return error;
-}
-
int git_merge__iterators(
git_index **out,
git_repository *repo,
@@ -1899,10 +1937,6 @@ int git_merge__iterators(
git_vector_foreach(&changes, i, conflict) {
int resolved = 0;
- /* Check for merge options in .gitattributes */
- if ((error = lookup_file_favor(&file_opts.favor, repo, conflict->our_entry.path) < 0))
- goto done;
-
if ((error = merge_conflict_resolve(
&resolved, diff_list, conflict, &file_opts)) < 0)
goto done;
diff --git a/src/merge.h b/src/merge.h
index bd839be49..10d77f3d8 100644
--- a/src/merge.h
+++ b/src/merge.h
@@ -12,8 +12,9 @@
#include "pool.h"
#include "iterator.h"
-#include "git2/merge.h"
#include "git2/types.h"
+#include "git2/merge.h"
+#include "git2/sys/merge.h"
#define GIT_MERGE_MSG_FILE "MERGE_MSG"
#define GIT_MERGE_MODE_FILE "MERGE_MODE"
@@ -22,6 +23,49 @@
#define GIT_MERGE_DEFAULT_RENAME_THRESHOLD 50
#define GIT_MERGE_DEFAULT_TARGET_LIMIT 1000
+
+/** Internal merge flags. */
+enum {
+ /** The merge is for a virtual base in a recursive merge. */
+ GIT_MERGE__VIRTUAL_BASE = (1 << 31),
+};
+
+enum {
+ /** Accept the conflict file, staging it as the merge result. */
+ GIT_MERGE_FILE_FAVOR__CONFLICTED = 4,
+};
+
+
+/* Merge drivers */
+
+struct git_merge_driver_source {
+ git_repository *repo;
+ const git_merge_file_options *file_opts;
+
+ const git_index_entry *ancestor;
+ const git_index_entry *ours;
+ const git_index_entry *theirs;
+};
+
+extern int git_merge_driver_for_path(
+ char **name_out,
+ git_merge_driver **driver_out,
+ git_repository *repo,
+ const char *path);
+
+/* Basic (normal) merge driver, takes favor type as the payload argument */
+extern git_merge_driver git_merge_driver__normal;
+
+/* Merge driver for text files, performs a standard three-way merge */
+extern git_merge_driver git_merge_driver__text;
+
+/* Merge driver for union-style merging */
+extern git_merge_driver git_merge_driver__union;
+
+/* Merge driver for unmergeable (binary) files: always produces conflicts */
+extern git_merge_driver git_merge_driver__binary;
+
+
/** Types of changes when files are merged from branch to branch. */
typedef enum {
/* No conflict - a change only occurs in one branch. */
@@ -70,7 +114,6 @@ typedef enum {
GIT_MERGE_DIFF_DF_CHILD = (1 << 11),
} git_merge_diff_type_t;
-
typedef struct {
git_repository *repo;
git_pool pool;
@@ -132,6 +175,12 @@ int git_merge_diff_list__find_renames(git_repository *repo, git_merge_diff_list
void git_merge_diff_list__free(git_merge_diff_list *diff_list);
+/* Merge driver configuration */
+int git_merge_driver_for_source(
+ git_merge_driver **driver_out,
+ void **data_out,
+ const git_merge_driver_source *src);
+
/* Merge metadata setup */
int git_merge__setup(
@@ -152,4 +201,50 @@ int git_merge__check_result(git_repository *repo, git_index *index_new);
int git_merge__append_conflicts_to_merge_msg(git_repository *repo, git_index *index);
+/* Merge files */
+
+GIT_INLINE(const char *) git_merge_file__best_path(
+ const char *ancestor,
+ const char *ours,
+ const char *theirs)
+{
+ if (!ancestor) {
+ if (ours && theirs && strcmp(ours, theirs) == 0)
+ return ours;
+
+ return NULL;
+ }
+
+ if (ours && strcmp(ancestor, ours) == 0)
+ return theirs;
+ else if(theirs && strcmp(ancestor, theirs) == 0)
+ return ours;
+
+ return NULL;
+}
+
+GIT_INLINE(uint32_t) git_merge_file__best_mode(
+ uint32_t ancestor, uint32_t ours, uint32_t theirs)
+{
+ /*
+ * If ancestor didn't exist and either ours or theirs is executable,
+ * assume executable. Otherwise, if any mode changed from the ancestor,
+ * use that one.
+ */
+ if (!ancestor) {
+ if (ours == GIT_FILEMODE_BLOB_EXECUTABLE ||
+ theirs == GIT_FILEMODE_BLOB_EXECUTABLE)
+ return GIT_FILEMODE_BLOB_EXECUTABLE;
+
+ return GIT_FILEMODE_BLOB;
+ } else if (ours && theirs) {
+ if (ancestor == ours)
+ return theirs;
+
+ return ours;
+ }
+
+ return 0;
+}
+
#endif
diff --git a/src/merge_driver.c b/src/merge_driver.c
new file mode 100644
index 000000000..5866e013e
--- /dev/null
+++ b/src/merge_driver.c
@@ -0,0 +1,403 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+
+#include "common.h"
+#include "vector.h"
+#include "global.h"
+#include "merge.h"
+#include "git2/merge.h"
+#include "git2/sys/merge.h"
+
+static const char *merge_driver_name__text = "text";
+static const char *merge_driver_name__union = "union";
+static const char *merge_driver_name__binary = "binary";
+
+struct merge_driver_registry {
+ git_vector drivers;
+};
+
+typedef struct {
+ git_merge_driver *driver;
+ int initialized;
+ char name[GIT_FLEX_ARRAY];
+} git_merge_driver_entry;
+
+static struct merge_driver_registry *merge_driver_registry = NULL;
+
+static int merge_driver_apply(
+ git_merge_driver *self,
+ void **payload,
+ const char **path_out,
+ uint32_t *mode_out,
+ git_buf *merged_out,
+ const git_merge_driver_source *src)
+{
+ git_merge_file_options file_opts = GIT_MERGE_FILE_OPTIONS_INIT;
+ git_merge_file_result result = {0};
+ int error;
+
+ GIT_UNUSED(self);
+
+ if (src->file_opts)
+ memcpy(&file_opts, src->file_opts, sizeof(git_merge_file_options));
+
+ file_opts.favor = (git_merge_file_favor_t) *payload;
+
+ if ((error = git_merge_file_from_index(&result, src->repo,
+ src->ancestor, src->ours, src->theirs, &file_opts)) < 0)
+ goto done;
+
+ if (!result.automergeable &&
+ !(file_opts.flags & GIT_MERGE_FILE_FAVOR__CONFLICTED)) {
+ error = GIT_EMERGECONFLICT;
+ goto done;
+ }
+
+ *path_out = git_merge_file__best_path(
+ src->ancestor ? src->ancestor->path : NULL,
+ src->ours ? src->ours->path : NULL,
+ src->theirs ? src->theirs->path : NULL);
+
+ *mode_out = git_merge_file__best_mode(
+ src->ancestor ? src->ancestor->mode : 0,
+ src->ours ? src->ours->mode : 0,
+ src->theirs ? src->theirs->mode : 0);
+
+ merged_out->ptr = (char *)result.ptr;
+ merged_out->size = result.len;
+ merged_out->asize = result.len;
+ result.ptr = NULL;
+
+done:
+ git_merge_file_result_free(&result);
+ return error;
+}
+
+static int merge_driver_text_check(
+ git_merge_driver *self,
+ void **payload,
+ const char *name,
+ const git_merge_driver_source *src)
+{
+ GIT_UNUSED(self);
+ GIT_UNUSED(name);
+ GIT_UNUSED(src);
+
+ *payload = (void *)GIT_MERGE_FILE_FAVOR_NORMAL;
+ return 0;
+}
+
+static int merge_driver_union_check(
+ git_merge_driver *self,
+ void **payload,
+ const char *name,
+ const git_merge_driver_source *src)
+{
+ GIT_UNUSED(self);
+ GIT_UNUSED(name);
+ GIT_UNUSED(src);
+
+ *payload = (void *)GIT_MERGE_FILE_FAVOR_UNION;
+ return 0;
+}
+
+static int merge_driver_binary_apply(
+ git_merge_driver *self,
+ void **payload,
+ const char **path_out,
+ uint32_t *mode_out,
+ git_buf *merged_out,
+ const git_merge_driver_source *src)
+{
+ GIT_UNUSED(self);
+ GIT_UNUSED(payload);
+ GIT_UNUSED(path_out);
+ GIT_UNUSED(mode_out);
+ GIT_UNUSED(merged_out);
+ GIT_UNUSED(src);
+
+ return GIT_EMERGECONFLICT;
+}
+
+static int merge_driver_entry_cmp(const void *a, const void *b)
+{
+ const git_merge_driver_entry *entry_a = a;
+ const git_merge_driver_entry *entry_b = b;
+
+ return strcmp(entry_a->name, entry_b->name);
+}
+
+static int merge_driver_entry_search(const void *a, const void *b)
+{
+ const char *name_a = a;
+ const git_merge_driver_entry *entry_b = b;
+
+ return strcmp(name_a, entry_b->name);
+}
+
+static void merge_driver_registry_shutdown(void)
+{
+ struct merge_driver_registry *reg;
+ git_merge_driver_entry *entry;
+ size_t i;
+
+ if ((reg = git__swap(merge_driver_registry, NULL)) == NULL)
+ return;
+
+ git_vector_foreach(&reg->drivers, i, entry) {
+ if (entry && entry->driver->shutdown)
+ entry->driver->shutdown(entry->driver);
+
+ git__free(entry);
+ }
+
+ git_vector_free(&reg->drivers);
+ git__free(reg);
+}
+
+git_merge_driver git_merge_driver__normal = {
+ GIT_MERGE_DRIVER_VERSION,
+ NULL,
+ NULL,
+ NULL,
+ merge_driver_apply
+};
+
+git_merge_driver git_merge_driver__text = {
+ GIT_MERGE_DRIVER_VERSION,
+ NULL,
+ NULL,
+ merge_driver_text_check,
+ merge_driver_apply
+};
+
+git_merge_driver git_merge_driver__union = {
+ GIT_MERGE_DRIVER_VERSION,
+ NULL,
+ NULL,
+ merge_driver_union_check,
+ merge_driver_apply
+};
+
+git_merge_driver git_merge_driver__binary = {
+ GIT_MERGE_DRIVER_VERSION,
+ NULL,
+ NULL,
+ NULL,
+ merge_driver_binary_apply
+};
+
+static int merge_driver_registry_initialize(void)
+{
+ struct merge_driver_registry *reg;
+ int error = 0;
+
+ if (merge_driver_registry)
+ return 0;
+
+ reg = git__calloc(1, sizeof(struct merge_driver_registry));
+ GITERR_CHECK_ALLOC(reg);
+
+ if ((error = git_vector_init(&reg->drivers, 3, merge_driver_entry_cmp)) < 0)
+ goto done;
+
+ reg = git__compare_and_swap(&merge_driver_registry, NULL, reg);
+
+ if (reg != NULL)
+ goto done;
+
+ git__on_shutdown(merge_driver_registry_shutdown);
+
+ if ((error = git_merge_driver_register(
+ merge_driver_name__text, &git_merge_driver__text)) < 0 ||
+ (error = git_merge_driver_register(
+ merge_driver_name__union, &git_merge_driver__union)) < 0 ||
+ (error = git_merge_driver_register(
+ merge_driver_name__binary, &git_merge_driver__binary)) < 0)
+ goto done;
+
+done:
+ if (error < 0)
+ merge_driver_registry_shutdown();
+
+ return error;
+}
+
+int git_merge_driver_register(const char *name, git_merge_driver *driver)
+{
+ git_merge_driver_entry *entry;
+
+ assert(name && driver);
+
+ if (merge_driver_registry_initialize() < 0)
+ return -1;
+
+ entry = git__calloc(1, sizeof(git_merge_driver_entry) + strlen(name) + 1);
+ GITERR_CHECK_ALLOC(entry);
+
+ strcpy(entry->name, name);
+ entry->driver = driver;
+
+ return git_vector_insert_sorted(
+ &merge_driver_registry->drivers, entry, NULL);
+}
+
+int git_merge_driver_unregister(const char *name)
+{
+ git_merge_driver_entry *entry;
+ size_t pos;
+ int error;
+
+ if ((error = git_vector_search2(&pos, &merge_driver_registry->drivers,
+ merge_driver_entry_search, name)) < 0)
+ return error;
+
+ entry = git_vector_get(&merge_driver_registry->drivers, pos);
+ git_vector_remove(&merge_driver_registry->drivers, pos);
+
+ if (entry->initialized && entry->driver->shutdown) {
+ entry->driver->shutdown(entry->driver);
+ entry->initialized = false;
+ }
+
+ git__free(entry);
+
+ return 0;
+}
+
+git_merge_driver *git_merge_driver_lookup(const char *name)
+{
+ git_merge_driver_entry *entry;
+ size_t pos;
+ int error;
+
+ /* If we've decided the merge driver to use internally - and not
+ * based on user configuration (in merge_driver_name_for_path)
+ * then we can use a hardcoded name instead of looking it up in
+ * the vector.
+ */
+ if (name == merge_driver_name__text)
+ return &git_merge_driver__text;
+ else if (name == merge_driver_name__binary)
+ return &git_merge_driver__binary;
+
+ if (merge_driver_registry_initialize() < 0)
+ return NULL;
+
+ error = git_vector_search2(&pos, &merge_driver_registry->drivers,
+ merge_driver_entry_search, name);
+
+ if (error == GIT_ENOTFOUND)
+ return NULL;
+
+ entry = git_vector_get(&merge_driver_registry->drivers, pos);
+
+ if (!entry->initialized) {
+ if (entry->driver->initialize &&
+ (error = entry->driver->initialize(entry->driver)) < 0)
+ return NULL;
+
+ entry->initialized = 1;
+ }
+
+ return entry->driver;
+}
+
+static git_merge_driver *merge_driver_lookup_with_default(const char *name)
+{
+ git_merge_driver *driver = git_merge_driver_lookup(name);
+
+ if (driver == NULL)
+ driver = git_merge_driver_lookup("*");
+
+ if (driver == NULL)
+ driver = &git_merge_driver__text;
+
+ return driver;
+}
+
+static int merge_driver_name_for_path(
+ const char **out,
+ git_repository *repo,
+ const char *path)
+{
+ const char *value;
+ int error;
+
+ *out = NULL;
+
+ if ((error = git_attr_get(&value, repo, 0, path, "merge")) < 0)
+ return error;
+
+ /* set: use the built-in 3-way merge driver ("text") */
+ if (GIT_ATTR_TRUE(value)) {
+ *out = merge_driver_name__text;
+ return 0;
+ }
+
+ /* unset: do not merge ("binary") */
+ if (GIT_ATTR_FALSE(value)) {
+ *out = merge_driver_name__binary;
+ return 0;
+ }
+
+ if (GIT_ATTR_UNSPECIFIED(value)) {
+ /* TODO */
+ /* if there's a merge.default configuration value, use it */
+ *out = merge_driver_name__text;
+ return 0;
+ }
+
+ *out = value;
+ return 0;
+}
+
+int git_merge_driver_for_source(
+ git_merge_driver **driver_out,
+ void **data_out,
+ const git_merge_driver_source *src)
+{
+ const char *path, *driver_name;
+ git_merge_driver *driver;
+ void *data = NULL;
+ int error = 0;
+
+ path = git_merge_file__best_path(
+ src->ancestor ? src->ancestor->path : NULL,
+ src->ours ? src->ours->path : NULL,
+ src->theirs ? src->theirs->path : NULL);
+
+ if ((error = merge_driver_name_for_path(&driver_name, src->repo, path)) < 0)
+ return error;
+
+ driver = merge_driver_lookup_with_default(driver_name);
+
+ if (driver->check)
+ error = driver->check(driver, &data, driver_name, src);
+
+ if (error == GIT_PASSTHROUGH)
+ driver = &git_merge_driver__text;
+ else if (error == GIT_EMERGECONFLICT)
+ driver = &git_merge_driver__binary;
+ else
+ goto done;
+
+ error = 0;
+ data = NULL;
+
+ if (driver->check)
+ error = driver->check(driver, &data, driver_name, src);
+
+ /* the text and binary drivers must succeed their check */
+ assert(error == 0);
+
+done:
+ *driver_out = driver;
+ *data_out = data;
+ return error;
+}
+
diff --git a/src/merge_file.c b/src/merge_file.c
index 6d4738065..731f4b724 100644
--- a/src/merge_file.c
+++ b/src/merge_file.c
@@ -11,6 +11,7 @@
#include "fileops.h"
#include "index.h"
#include "diff_xdiff.h"
+#include "merge.h"
#include "git2/repository.h"
#include "git2/object.h"
@@ -26,52 +27,6 @@
#define GIT_MERGE_FILE_SIDE_EXISTS(X) ((X)->mode != 0)
-GIT_INLINE(const char *) merge_file_best_path(
- const git_merge_file_input *ancestor,
- const git_merge_file_input *ours,
- const git_merge_file_input *theirs)
-{
- if (!ancestor) {
- if (ours && theirs && strcmp(ours->path, theirs->path) == 0)
- return ours->path;
-
- return NULL;
- }
-
- if (ours && strcmp(ancestor->path, ours->path) == 0)
- return theirs ? theirs->path : NULL;
- else if(theirs && strcmp(ancestor->path, theirs->path) == 0)
- return ours ? ours->path : NULL;
-
- return NULL;
-}
-
-GIT_INLINE(int) merge_file_best_mode(
- const git_merge_file_input *ancestor,
- const git_merge_file_input *ours,
- const git_merge_file_input *theirs)
-{
- /*
- * If ancestor didn't exist and either ours or theirs is executable,
- * assume executable. Otherwise, if any mode changed from the ancestor,
- * use that one.
- */
- if (!ancestor) {
- if ((ours && ours->mode == GIT_FILEMODE_BLOB_EXECUTABLE) ||
- (theirs && theirs->mode == GIT_FILEMODE_BLOB_EXECUTABLE))
- return GIT_FILEMODE_BLOB_EXECUTABLE;
-
- return GIT_FILEMODE_BLOB;
- } else if (ours && theirs) {
- if (ancestor->mode == ours->mode)
- return theirs->mode;
-
- return ours->mode;
- }
-
- return 0;
-}
-
int git_merge_file__input_from_index(
git_merge_file_input *input_out,
git_odb_object **odb_object_out,
@@ -177,8 +132,12 @@ static int merge_file__xdiff(
goto done;
}
- if ((path = merge_file_best_path(ancestor, ours, theirs)) != NULL &&
- (out->path = strdup(path)) == NULL) {
+ path = git_merge_file__best_path(
+ ancestor ? ancestor->path : NULL,
+ ours ? ours->path : NULL,
+ theirs ? theirs->path : NULL);
+
+ if (path != NULL && (out->path = git__strdup(path)) == NULL) {
error = -1;
goto done;
}
@@ -186,7 +145,10 @@ static int merge_file__xdiff(
out->automergeable = (xdl_result == 0);
out->ptr = (const char *)mmbuffer.ptr;
out->len = mmbuffer.size;
- out->mode = merge_file_best_mode(ancestor, ours, theirs);
+ out->mode = git_merge_file__best_mode(
+ ancestor ? ancestor->mode : 0,
+ ours ? ours->mode : 0,
+ theirs ? theirs->mode : 0);
done:
if (error < 0)
diff --git a/tests/merge/driver.c b/tests/merge/driver.c
new file mode 100644
index 000000000..34ed914dc
--- /dev/null
+++ b/tests/merge/driver.c
@@ -0,0 +1,208 @@
+#include "clar_libgit2.h"
+#include "git2/repository.h"
+#include "git2/merge.h"
+#include "buffer.h"
+#include "merge.h"
+
+#define TEST_REPO_PATH "merge-resolve"
+#define BRANCH_ID "7cb63eed597130ba4abb87b3e544b85021905520"
+
+static git_repository *repo;
+static git_index *repo_index;
+
+static void test_drivers_register(void);
+static void test_drivers_unregister(void);
+
+void test_merge_driver__initialize(void)
+{
+ git_config *cfg;
+
+ repo = cl_git_sandbox_init(TEST_REPO_PATH);
+ git_repository_index(&repo_index, repo);
+
+ /* Ensure that the user's merge.conflictstyle doesn't interfere */
+ cl_git_pass(git_repository_config(&cfg, repo));
+
+ cl_git_pass(git_config_set_string(cfg, "merge.conflictstyle", "merge"));
+ cl_git_pass(git_config_set_bool(cfg, "core.autocrlf", false));
+
+ test_drivers_register();
+
+ git_config_free(cfg);
+}
+
+void test_merge_driver__cleanup(void)
+{
+ test_drivers_unregister();
+
+ git_index_free(repo_index);
+ cl_git_sandbox_cleanup();
+}
+
+struct test_merge_driver {
+ git_merge_driver base;
+ int initialized;
+ int shutdown;
+};
+
+static int test_driver_init(git_merge_driver *s)
+{
+ struct test_merge_driver *self = (struct test_merge_driver *)s;
+ self->initialized = 1;
+ return 0;
+}
+
+static void test_driver_shutdown(git_merge_driver *s)
+{
+ struct test_merge_driver *self = (struct test_merge_driver *)s;
+ self->shutdown = 1;
+}
+
+static int test_driver_check(
+ git_merge_driver *s,
+ void **payload,
+ const char *name,
+ const git_merge_driver_source *src)
+{
+ GIT_UNUSED(s);
+ GIT_UNUSED(src);
+
+ *payload = git__strdup(name);
+ GITERR_CHECK_ALLOC(*payload);
+
+ return 0;
+}
+
+static int test_driver_apply(
+ git_merge_driver *s,
+ void **payload,
+ const char **path_out,
+ uint32_t *mode_out,
+ git_buf *merged_out,
+ const git_merge_driver_source *src)
+{
+ GIT_UNUSED(s);
+ GIT_UNUSED(src);
+
+ *path_out = "applied.txt";
+ *mode_out = GIT_FILEMODE_BLOB;
+
+ return git_buf_printf(merged_out, "This is the `%s` driver.\n",
+ (char *)*payload);
+}
+
+static void test_driver_cleanup(git_merge_driver *s, void *payload)
+{
+ GIT_UNUSED(s);
+
+ git__free(payload);
+}
+
+
+static struct test_merge_driver test_driver_custom = {
+ {
+ GIT_MERGE_DRIVER_VERSION,
+ test_driver_init,
+ test_driver_shutdown,
+ test_driver_check,
+ test_driver_apply,
+ test_driver_cleanup
+ },
+ 0,
+ 0,
+};
+
+static struct test_merge_driver test_driver_wildcard = {
+ {
+ GIT_MERGE_DRIVER_VERSION,
+ test_driver_init,
+ test_driver_shutdown,
+ test_driver_check,
+ test_driver_apply,
+ test_driver_cleanup
+ },
+ 0,
+ 0,
+};
+
+static void test_drivers_register(void)
+{
+ cl_git_pass(git_merge_driver_register("custom", &test_driver_custom.base));
+ cl_git_pass(git_merge_driver_register("*", &test_driver_wildcard.base));
+}
+
+static void test_drivers_unregister(void)
+{
+ cl_git_pass(git_merge_driver_unregister("custom"));
+ cl_git_pass(git_merge_driver_unregister("*"));
+}
+
+static void set_gitattributes_to(const char *driver)
+{
+ git_buf line = GIT_BUF_INIT;
+
+ cl_git_pass(git_buf_printf(&line, "automergeable.txt merge=%s\n", driver));
+ cl_git_mkfile(TEST_REPO_PATH "/.gitattributes", line.ptr);
+ git_buf_free(&line);
+}
+
+static void merge_branch(void)
+{
+ git_oid their_id;
+ git_annotated_commit *their_head;
+
+ cl_git_pass(git_oid_fromstr(&their_id, BRANCH_ID));
+ cl_git_pass(git_annotated_commit_lookup(&their_head, repo, &their_id));
+
+ cl_git_pass(git_merge(repo, (const git_annotated_commit **)&their_head,
+ 1, NULL, NULL));
+
+ git_annotated_commit_free(their_head);
+}
+
+void test_merge_driver__custom(void)
+{
+ const char *expected = "This is the `custom` driver.\n";
+ set_gitattributes_to("custom");
+ merge_branch();
+
+ cl_assert_equal_file(expected, strlen(expected),
+ TEST_REPO_PATH "/applied.txt");
+}
+
+void test_merge_driver__wildcard(void)
+{
+ const char *expected = "This is the `foobar` driver.\n";
+ set_gitattributes_to("foobar");
+ merge_branch();
+
+ cl_assert_equal_file(expected, strlen(expected),
+ TEST_REPO_PATH "/applied.txt");
+}
+
+void test_merge_driver__shutdown_is_called(void)
+{
+ test_driver_custom.initialized = 0;
+ test_driver_custom.shutdown = 0;
+ test_driver_wildcard.initialized = 0;
+ test_driver_wildcard.shutdown = 0;
+
+ /* run the merge with the custom driver */
+ set_gitattributes_to("custom");
+ merge_branch();
+
+ /* unregister the drivers, ensure their shutdown function is called */
+ test_drivers_unregister();
+
+ /* since the `custom` driver was used, it should have been initialized and
+ * shutdown, but the wildcard driver was not used at all and should not
+ * have been initialized or shutdown.
+ */
+ cl_assert(test_driver_custom.initialized);
+ cl_assert(test_driver_custom.shutdown);
+ cl_assert(!test_driver_wildcard.initialized);
+ cl_assert(!test_driver_wildcard.shutdown);
+
+ test_drivers_register();
+}
+