Commit e18df18b by Carlos Martín Nieto

Merge pull request #3564 from ethomson/merge_drivers

Custom merge drivers and proper gitattributes `merge` handling
parents e02acbb3 d953c450
......@@ -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
......
......@@ -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 */
......
/*
* 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
......@@ -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();
......
......@@ -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,
......@@ -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)
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 ((index_entry = git_pool_mallocz(&diff_list->pool, sizeof(git_index_entry))) == NULL)
GITERR_CHECK_ALLOC(index_entry);
if (driver == NULL)
fallback = true;
}
if (driver) {
error = merge_conflict_invoke_driver(&merge_result, name, driver,
diff_list, &source);
if (error == GIT_PASSTHROUGH)
fallback = true;
}
if (fallback) {
error = merge_conflict_invoke_driver(&merge_result, "text",
&git_merge_driver__text.base, diff_list, &source);
}
index_entry->path = git_pool_strdup(&diff_list->pool, result.path);
GITERR_CHECK_ALLOC(index_entry->path);
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;
......@@ -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);
......
......@@ -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
/*
* 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;
}
/*
* 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
......@@ -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)
......
#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();
}
......@@ -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;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment