summaryrefslogtreecommitdiff
path: root/src
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 /src
parente02acbb3c6076cc9acbcf8de7a258eee406b2921 (diff)
parentd953c4505e09756b4b4f72b431a51867281643ca (diff)
downloadlibgit2-e18df18bea14e17f648209b733359564a5836d8a.tar.gz
Merge pull request #3564 from ethomson/merge_drivers
Custom merge drivers and proper gitattributes `merge` handling
Diffstat (limited to 'src')
-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
6 files changed, 690 insertions, 114 deletions
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)