mirror of
https://git.proxmox.com/git/libgit2
synced 2026-01-01 03:42:53 +00:00
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.
This commit is contained in:
parent
7a74590d8f
commit
3f04219fcd
230
include/git2/sys/merge.h
Normal file
230
include/git2/sys/merge.h
Normal file
@ -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
|
||||
206
src/merge.c
206
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(
|
||||
static bool merge_conflict_can_resolve_contents(
|
||||
const git_merge_diff *conflict)
|
||||
{
|
||||
if (!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->our_entry) ||
|
||||
!GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry))
|
||||
return false;
|
||||
|
||||
/* Reject D/F conflicts */
|
||||
if (conflict->type == GIT_MERGE_DIFF_DIRECTORY_FILE)
|
||||
return false;
|
||||
|
||||
/* Reject submodules. */
|
||||
if (S_ISGITLINK(conflict->ancestor_entry.mode) ||
|
||||
S_ISGITLINK(conflict->our_entry.mode) ||
|
||||
S_ISGITLINK(conflict->their_entry.mode))
|
||||
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 false;
|
||||
|
||||
/* Reject name conflicts */
|
||||
if (conflict->type == GIT_MERGE_DIFF_BOTH_RENAMED_2_TO_1 ||
|
||||
conflict->type == GIT_MERGE_DIFF_RENAMED_ADDED)
|
||||
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)
|
||||
{
|
||||
const git_index_entry *ancestor = NULL, *ours = NULL, *theirs = NULL;
|
||||
git_merge_driver_source source = {0};
|
||||
git_merge_file_result result = {0};
|
||||
git_index_entry *index_entry;
|
||||
git_merge_driver *driver;
|
||||
git_index_entry *merge_result;
|
||||
git_odb *odb = NULL;
|
||||
git_oid automerge_oid;
|
||||
void *data;
|
||||
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))
|
||||
if (!merge_conflict_can_resolve_contents(conflict))
|
||||
return 0;
|
||||
|
||||
/* Reject D/F conflicts */
|
||||
if (conflict->type == GIT_MERGE_DIFF_DIRECTORY_FILE)
|
||||
return 0;
|
||||
|
||||
/* Reject submodules. */
|
||||
if (S_ISGITLINK(conflict->ancestor_entry.mode) ||
|
||||
S_ISGITLINK(conflict->our_entry.mode) ||
|
||||
S_ISGITLINK(conflict->their_entry.mode))
|
||||
return 0;
|
||||
|
||||
/* 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;
|
||||
|
||||
/* Reject name conflicts */
|
||||
if (conflict->type == GIT_MERGE_DIFF_BOTH_RENAMED_2_TO_1 ||
|
||||
conflict->type == GIT_MERGE_DIFF_RENAMED_ADDED)
|
||||
return 0;
|
||||
|
||||
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 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);
|
||||
}
|
||||
|
||||
index_entry->file_size = result.len;
|
||||
index_entry->mode = result.mode;
|
||||
git_oid_cpy(&index_entry->id, &automerge_oid);
|
||||
if (error < 0) {
|
||||
if (error == GIT_EMERGECONFLICT)
|
||||
error = 0;
|
||||
|
||||
git_vector_insert(&diff_list->staged, index_entry);
|
||||
goto done;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
99
src/merge.h
99
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
|
||||
|
||||
403
src/merge_driver.c
Normal file
403
src/merge_driver.c
Normal file
@ -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(®->drivers, i, entry) {
|
||||
if (entry && entry->driver->shutdown)
|
||||
entry->driver->shutdown(entry->driver);
|
||||
|
||||
git__free(entry);
|
||||
}
|
||||
|
||||
git_vector_free(®->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(®->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;
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
208
tests/merge/driver.c
Normal file
208
tests/merge/driver.c
Normal file
@ -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();
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user