Commit 30a94ab75687ae52ad4f9081831110b10dbd82ca

Edward Thomson 2015-12-24T22:52:23

merge driver: allow custom default driver Allow merge users to configure a custom default merge driver via `git_merge_options`. Similarly, honor the `merge.default` configuration option.

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 21f972d..0e9ca15 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 560797a..c6f6cba 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/src/merge.c b/src/merge.c
index 0bc64ac..9a6442a 100644
--- a/src/merge.c
+++ b/src/merge.c
@@ -885,6 +885,7 @@ 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};
@@ -903,6 +904,7 @@ static int merge_conflict_resolve_contents(
 		return 0;
 
 	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;
@@ -955,6 +957,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;
@@ -962,16 +965,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_contents(&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;
@@ -1687,6 +1694,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);
@@ -1704,6 +1712,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);
 
@@ -1726,7 +1750,9 @@ static int merge_normalize_opts(
 		opts->metric->payload = (void *)GIT_HASHSIG_SMART_WHITESPACE;
 	}
 
-	return 0;
+done:
+	git_config_entry_free(entry);
+	return error;
 }
 
 
@@ -1938,7 +1964,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) {
@@ -1959,6 +1985,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);
diff --git a/src/merge.h b/src/merge.h
index 10d77f3..8eaa1ad 100644
--- a/src/merge.h
+++ b/src/merge.h
@@ -40,6 +40,7 @@ enum {
 
 struct git_merge_driver_source {
 	git_repository *repo;
+	const char *default_driver;
 	const git_merge_file_options *file_opts;
 
 	const git_index_entry *ancestor;
diff --git a/src/merge_driver.c b/src/merge_driver.c
index 5866e01..791afe0 100644
--- a/src/merge_driver.c
+++ b/src/merge_driver.c
@@ -307,23 +307,11 @@ git_merge_driver *git_merge_driver_lookup(const char *name)
     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 *path,
+	const char *default_driver)
 {
 	const char *value;
 	int error;
@@ -334,28 +322,37 @@ static int merge_driver_name_for_path(
 		return error;
 
 	/* set: use the built-in 3-way merge driver ("text") */
-	if (GIT_ATTR_TRUE(value)) {
+	if (GIT_ATTR_TRUE(value))
 		*out = merge_driver_name__text;
-		return 0;
-	}
 
 	/* unset: do not merge ("binary") */
-	if (GIT_ATTR_FALSE(value)) {
+	else 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 */
+	else if (GIT_ATTR_UNSPECIFIED(value) && default_driver)
+		*out = default_driver;
+
+	else if (GIT_ATTR_UNSPECIFIED(value))
 		*out = merge_driver_name__text;
-		return 0;
-	}
-	
-	*out = value;
+
+	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(
 	git_merge_driver **driver_out,
 	void **data_out,
@@ -371,20 +368,22 @@ int git_merge_driver_for_source(
 		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)
+	if ((error = merge_driver_name_for_path(
+			&driver_name, src->repo, path, src->default_driver)) < 0)
 		return error;
 
-	driver = merge_driver_lookup_with_default(driver_name);
+	driver = merge_driver_lookup_with_wildcard(driver_name);
 
-	if (driver->check)
+	if (driver && 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;
+		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;