From 6723899932eb5b6436e9bac65ffc9b6e644c7fee Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Tue, 16 May 2023 06:34:04 +0000 Subject: merge-ll: rename from ll-merge A long term (but rather minor) pet-peeve of mine was the name ll-merge.[ch]. I thought it made it harder to realize what stuff was related to merging when I was working on the merge machinery and trying to improve it. Further, back in d1cbe1e6d8a ("hash-ll.h: split out of hash.h to remove dependency on repository.h", 2023-04-22), we have split the portions of hash.h that do not depend upon repository.h into a "hash-ll.h" (due to the recommendation to use "ll" for "low-level" in its name[1], but which I used as a suffix precisely because of my distaste for "ll-merge"). When we discussed adding additional "*-ll.h" files, a request was made that we use "ll" consistently as either a prefix or a suffix. Since it is already in use as both a prefix and a suffix, the only way to do so is to rename some files. Besides my distaste for the ll-merge.[ch] name, let me also note that the files ll-fsmonitor.h, ll-hash.h, ll-merge.h, ll-object-store.h, ll-read-cache.h would have essentially nothing to do with each other and make no sense to group. But giving them the common "ll-" prefix would group them. Using "-ll" as a suffix thus seems just much more logical to me. Rename ll-merge.[ch] to merge-ll.[ch] to achieve this consistency, and to ensure we get a more logical grouping of files. [1] https://lore.kernel.org/git/kl6lsfcu1g8w.fsf@chooglen-macbookpro.roam.corp.google.com/ Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- merge-ll.c | 432 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 432 insertions(+) create mode 100644 merge-ll.c (limited to 'merge-ll.c') diff --git a/merge-ll.c b/merge-ll.c new file mode 100644 index 0000000000..740b8c6bfd --- /dev/null +++ b/merge-ll.c @@ -0,0 +1,432 @@ +/* + * Low level 3-way in-core file merge. + * + * Copyright (c) 2007 Junio C Hamano + */ + +#include "git-compat-util.h" +#include "config.h" +#include "convert.h" +#include "attr.h" +#include "xdiff-interface.h" +#include "run-command.h" +#include "merge-ll.h" +#include "quote.h" +#include "strbuf.h" +#include "wrapper.h" + +struct ll_merge_driver; + +typedef enum ll_merge_result (*ll_merge_fn)(const struct ll_merge_driver *, + mmbuffer_t *result, + const char *path, + mmfile_t *orig, const char *orig_name, + mmfile_t *src1, const char *name1, + mmfile_t *src2, const char *name2, + const struct ll_merge_options *opts, + int marker_size); + +struct ll_merge_driver { + const char *name; + const char *description; + ll_merge_fn fn; + const char *recursive; + struct ll_merge_driver *next; + char *cmdline; +}; + +static struct attr_check *merge_attributes; +static struct attr_check *load_merge_attributes(void) +{ + if (!merge_attributes) + merge_attributes = attr_check_initl("merge", "conflict-marker-size", NULL); + return merge_attributes; +} + +void reset_merge_attributes(void) +{ + attr_check_free(merge_attributes); + merge_attributes = NULL; +} + +/* + * Built-in low-levels + */ +static enum ll_merge_result ll_binary_merge(const struct ll_merge_driver *drv UNUSED, + mmbuffer_t *result, + const char *path UNUSED, + mmfile_t *orig, const char *orig_name UNUSED, + mmfile_t *src1, const char *name1 UNUSED, + mmfile_t *src2, const char *name2 UNUSED, + const struct ll_merge_options *opts, + int marker_size UNUSED) +{ + enum ll_merge_result ret; + mmfile_t *stolen; + assert(opts); + + /* + * The tentative merge result is the common ancestor for an + * internal merge. For the final merge, it is "ours" by + * default but -Xours/-Xtheirs can tweak the choice. + */ + if (opts->virtual_ancestor) { + stolen = orig; + ret = LL_MERGE_OK; + } else { + switch (opts->variant) { + default: + ret = LL_MERGE_BINARY_CONFLICT; + stolen = src1; + break; + case XDL_MERGE_FAVOR_OURS: + ret = LL_MERGE_OK; + stolen = src1; + break; + case XDL_MERGE_FAVOR_THEIRS: + ret = LL_MERGE_OK; + stolen = src2; + break; + } + } + + result->ptr = stolen->ptr; + result->size = stolen->size; + stolen->ptr = NULL; + + return ret; +} + +static enum ll_merge_result ll_xdl_merge(const struct ll_merge_driver *drv_unused, + mmbuffer_t *result, + const char *path, + mmfile_t *orig, const char *orig_name, + mmfile_t *src1, const char *name1, + mmfile_t *src2, const char *name2, + const struct ll_merge_options *opts, + int marker_size) +{ + enum ll_merge_result ret; + xmparam_t xmp; + int status; + assert(opts); + + if (orig->size > MAX_XDIFF_SIZE || + src1->size > MAX_XDIFF_SIZE || + src2->size > MAX_XDIFF_SIZE || + buffer_is_binary(orig->ptr, orig->size) || + buffer_is_binary(src1->ptr, src1->size) || + buffer_is_binary(src2->ptr, src2->size)) { + return ll_binary_merge(drv_unused, result, + path, + orig, orig_name, + src1, name1, + src2, name2, + opts, marker_size); + } + + memset(&xmp, 0, sizeof(xmp)); + xmp.level = XDL_MERGE_ZEALOUS; + xmp.favor = opts->variant; + xmp.xpp.flags = opts->xdl_opts; + if (git_xmerge_style >= 0) + xmp.style = git_xmerge_style; + if (marker_size > 0) + xmp.marker_size = marker_size; + xmp.ancestor = orig_name; + xmp.file1 = name1; + xmp.file2 = name2; + status = xdl_merge(orig, src1, src2, &xmp, result); + ret = (status > 0) ? LL_MERGE_CONFLICT : status; + return ret; +} + +static enum ll_merge_result ll_union_merge(const struct ll_merge_driver *drv_unused, + mmbuffer_t *result, + const char *path, + mmfile_t *orig, const char *orig_name, + mmfile_t *src1, const char *name1, + mmfile_t *src2, const char *name2, + const struct ll_merge_options *opts, + int marker_size) +{ + /* Use union favor */ + struct ll_merge_options o; + assert(opts); + o = *opts; + o.variant = XDL_MERGE_FAVOR_UNION; + return ll_xdl_merge(drv_unused, result, path, + orig, orig_name, src1, name1, src2, name2, + &o, marker_size); +} + +#define LL_BINARY_MERGE 0 +#define LL_TEXT_MERGE 1 +#define LL_UNION_MERGE 2 +static struct ll_merge_driver ll_merge_drv[] = { + { "binary", "built-in binary merge", ll_binary_merge }, + { "text", "built-in 3-way text merge", ll_xdl_merge }, + { "union", "built-in union merge", ll_union_merge }, +}; + +static void create_temp(mmfile_t *src, char *path, size_t len) +{ + int fd; + + xsnprintf(path, len, ".merge_file_XXXXXX"); + fd = xmkstemp(path); + if (write_in_full(fd, src->ptr, src->size) < 0) + die_errno("unable to write temp-file"); + close(fd); +} + +/* + * User defined low-level merge driver support. + */ +static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn, + mmbuffer_t *result, + const char *path, + mmfile_t *orig, const char *orig_name UNUSED, + mmfile_t *src1, const char *name1 UNUSED, + mmfile_t *src2, const char *name2 UNUSED, + const struct ll_merge_options *opts, + int marker_size) +{ + char temp[4][50]; + struct strbuf cmd = STRBUF_INIT; + struct strbuf_expand_dict_entry dict[6]; + struct strbuf path_sq = STRBUF_INIT; + struct child_process child = CHILD_PROCESS_INIT; + int status, fd, i; + struct stat st; + enum ll_merge_result ret; + assert(opts); + + sq_quote_buf(&path_sq, path); + dict[0].placeholder = "O"; dict[0].value = temp[0]; + dict[1].placeholder = "A"; dict[1].value = temp[1]; + dict[2].placeholder = "B"; dict[2].value = temp[2]; + dict[3].placeholder = "L"; dict[3].value = temp[3]; + dict[4].placeholder = "P"; dict[4].value = path_sq.buf; + dict[5].placeholder = NULL; dict[5].value = NULL; + + if (!fn->cmdline) + die("custom merge driver %s lacks command line.", fn->name); + + result->ptr = NULL; + result->size = 0; + create_temp(orig, temp[0], sizeof(temp[0])); + create_temp(src1, temp[1], sizeof(temp[1])); + create_temp(src2, temp[2], sizeof(temp[2])); + xsnprintf(temp[3], sizeof(temp[3]), "%d", marker_size); + + strbuf_expand(&cmd, fn->cmdline, strbuf_expand_dict_cb, &dict); + + child.use_shell = 1; + strvec_push(&child.args, cmd.buf); + status = run_command(&child); + fd = open(temp[1], O_RDONLY); + if (fd < 0) + goto bad; + if (fstat(fd, &st)) + goto close_bad; + result->size = st.st_size; + result->ptr = xmallocz(result->size); + if (read_in_full(fd, result->ptr, result->size) != result->size) { + FREE_AND_NULL(result->ptr); + result->size = 0; + } + close_bad: + close(fd); + bad: + for (i = 0; i < 3; i++) + unlink_or_warn(temp[i]); + strbuf_release(&cmd); + strbuf_release(&path_sq); + ret = (status > 0) ? LL_MERGE_CONFLICT : status; + return ret; +} + +/* + * merge.default and merge.driver configuration items + */ +static struct ll_merge_driver *ll_user_merge, **ll_user_merge_tail; +static const char *default_ll_merge; + +static int read_merge_config(const char *var, const char *value, + void *cb UNUSED) +{ + struct ll_merge_driver *fn; + const char *key, *name; + size_t namelen; + + if (!strcmp(var, "merge.default")) + return git_config_string(&default_ll_merge, var, value); + + /* + * We are not interested in anything but "merge..variable"; + * especially, we do not want to look at variables such as + * "merge.summary", "merge.tool", and "merge.verbosity". + */ + if (parse_config_key(var, "merge", &name, &namelen, &key) < 0 || !name) + return 0; + + /* + * Find existing one as we might be processing merge..var2 + * after seeing merge..var1. + */ + for (fn = ll_user_merge; fn; fn = fn->next) + if (!strncmp(fn->name, name, namelen) && !fn->name[namelen]) + break; + if (!fn) { + CALLOC_ARRAY(fn, 1); + fn->name = xmemdupz(name, namelen); + fn->fn = ll_ext_merge; + *ll_user_merge_tail = fn; + ll_user_merge_tail = &(fn->next); + } + + if (!strcmp("name", key)) + return git_config_string(&fn->description, var, value); + + if (!strcmp("driver", key)) { + if (!value) + return error("%s: lacks value", var); + /* + * merge..driver specifies the command line: + * + * command-line + * + * The command-line will be interpolated with the following + * tokens and is given to the shell: + * + * %O - temporary file name for the merge base. + * %A - temporary file name for our version. + * %B - temporary file name for the other branches' version. + * %L - conflict marker length + * %P - the original path (safely quoted for the shell) + * + * The external merge driver should write the results in the + * file named by %A, and signal that it has done with zero exit + * status. + */ + fn->cmdline = xstrdup(value); + return 0; + } + + if (!strcmp("recursive", key)) + return git_config_string(&fn->recursive, var, value); + + return 0; +} + +static void initialize_ll_merge(void) +{ + if (ll_user_merge_tail) + return; + ll_user_merge_tail = &ll_user_merge; + git_config(read_merge_config, NULL); +} + +static const struct ll_merge_driver *find_ll_merge_driver(const char *merge_attr) +{ + struct ll_merge_driver *fn; + const char *name; + int i; + + initialize_ll_merge(); + + if (ATTR_TRUE(merge_attr)) + return &ll_merge_drv[LL_TEXT_MERGE]; + else if (ATTR_FALSE(merge_attr)) + return &ll_merge_drv[LL_BINARY_MERGE]; + else if (ATTR_UNSET(merge_attr)) { + if (!default_ll_merge) + return &ll_merge_drv[LL_TEXT_MERGE]; + else + name = default_ll_merge; + } + else + name = merge_attr; + + for (fn = ll_user_merge; fn; fn = fn->next) + if (!strcmp(fn->name, name)) + return fn; + + for (i = 0; i < ARRAY_SIZE(ll_merge_drv); i++) + if (!strcmp(ll_merge_drv[i].name, name)) + return &ll_merge_drv[i]; + + /* default to the 3-way */ + return &ll_merge_drv[LL_TEXT_MERGE]; +} + +static void normalize_file(mmfile_t *mm, const char *path, struct index_state *istate) +{ + struct strbuf strbuf = STRBUF_INIT; + if (renormalize_buffer(istate, path, mm->ptr, mm->size, &strbuf)) { + free(mm->ptr); + mm->size = strbuf.len; + mm->ptr = strbuf_detach(&strbuf, NULL); + } +} + +enum ll_merge_result ll_merge(mmbuffer_t *result_buf, + const char *path, + mmfile_t *ancestor, const char *ancestor_label, + mmfile_t *ours, const char *our_label, + mmfile_t *theirs, const char *their_label, + struct index_state *istate, + const struct ll_merge_options *opts) +{ + struct attr_check *check = load_merge_attributes(); + static const struct ll_merge_options default_opts; + const char *ll_driver_name = NULL; + int marker_size = DEFAULT_CONFLICT_MARKER_SIZE; + const struct ll_merge_driver *driver; + + if (!opts) + opts = &default_opts; + + if (opts->renormalize) { + normalize_file(ancestor, path, istate); + normalize_file(ours, path, istate); + normalize_file(theirs, path, istate); + } + + git_check_attr(istate, path, check); + ll_driver_name = check->items[0].value; + if (check->items[1].value) { + marker_size = atoi(check->items[1].value); + if (marker_size <= 0) + marker_size = DEFAULT_CONFLICT_MARKER_SIZE; + } + driver = find_ll_merge_driver(ll_driver_name); + + if (opts->virtual_ancestor) { + if (driver->recursive) + driver = find_ll_merge_driver(driver->recursive); + } + if (opts->extra_marker_size) { + marker_size += opts->extra_marker_size; + } + return driver->fn(driver, result_buf, path, ancestor, ancestor_label, + ours, our_label, theirs, their_label, + opts, marker_size); +} + +int ll_merge_marker_size(struct index_state *istate, const char *path) +{ + static struct attr_check *check; + int marker_size = DEFAULT_CONFLICT_MARKER_SIZE; + + if (!check) + check = attr_check_initl("conflict-marker-size", NULL); + git_check_attr(istate, path, check); + if (check->items[0].value) { + marker_size = atoi(check->items[0].value); + if (marker_size <= 0) + marker_size = DEFAULT_CONFLICT_MARKER_SIZE; + } + return marker_size; +} -- cgit v1.2.3