* [RFC PATCH v1 00/17] Rewrite the remaining merge strategies from shell to C
@ 2020-06-25 12:19 Alban Gruin
2020-06-25 12:19 ` [RFC PATCH v1 01/17] t6027: modernise tests Alban Gruin
` (17 more replies)
0 siblings, 18 replies; 221+ messages in thread
From: Alban Gruin @ 2020-06-25 12:19 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Alban Gruin
In an effort to reduce the number of shell scripts part of git, I
propose this patch converting the two remaining merge strategies,
resolve and octopus, from shell to C. This will enable slightly better
performance, better integration with git itself (no more forking to
perform these operations), better portability (Windows and shell scripts
don't mix well).
Three scripts are actually converted: first git-merge-one-file.sh, then
git-merge-resolve.sh, and finally git-merge-octopus.sh. Not only they
are converted, but they also are modified to operate without forking,
and then libified so they can be used by git without spawning another
process.
The first patch is not important to make the whole series work, but I
made this patch while working on this series.
Patches 2-5 rewrite, clean, and libify `git merge-one-file', used by the
resolve and octopus strategies.
Patch 6 libifies `git merge-index', so the rewritten `git
merge-one-file' can be called without forking.
Patch 7-8-9 rewrite, clean, and libify `git merge-resolve'.
Patch 10 moves a function, better_branch_name(), that will prove itself
useful in the C version of `git merge-octopus', but that is not part of
libgit.a.
Patches 11-12-13 rewrite, clean, and libify `git merge-octopus'.
Patches 14-15-16-17 teach `git merge' and the sequencer to call the
strategies without forking.
This series keeps the commands `git merge-one-file', `git merge-resolve'
and `git merge-octopus', so any script depending on them should keep
working without any changes.
This series is based on c9c318d6bf (The fourth batch, 2020-06-22). The
tip is tagged as "rewrite-and-cleanup-merge-strategies-v1" at
https://github.com/agrn/git.
Alban Gruin (17):
t6027: modernise tests
merge-one-file: rewrite in C
merge-one-file: remove calls to external processes
merge-one-file: use error() instead of fprintf(stderr, ...)
merge-one-file: libify merge_one_file()
merge-index: libify merge_one_path() and merge_all()
merge-resolve: rewrite in C
merge-resolve: remove calls to external processes
merge-resolve: libify merge_resolve()
merge-recursive: move better_branch_name() to merge.c
merge-octopus: rewrite in C
merge-octopus: remove calls to external processes
merge-octopus: libify merge_octopus()
merge: use the "resolve" strategy without forking
merge: use the "octopus" strategy without forking
sequencer: use the "resolve" strategy without forking
sequencer: use the "octopus" merge strategy without forking
Makefile | 7 +-
builtin.h | 3 +
builtin/merge-index.c | 77 +----
builtin/merge-octopus.c | 65 ++++
builtin/merge-one-file.c | 74 ++++
builtin/merge-recursive.c | 16 +-
builtin/merge-resolve.c | 69 ++++
builtin/merge.c | 9 +-
cache.h | 2 +-
git-merge-octopus.sh | 112 -------
git-merge-one-file.sh | 167 ---------
git-merge-resolve.sh | 54 ---
git.c | 3 +
merge-strategies.c | 577 ++++++++++++++++++++++++++++++++
merge-strategies.h | 44 +++
merge.c | 12 +
sequencer.c | 16 +-
t/t6027-merge-binary.sh | 27 +-
t/t6035-merge-dir-to-symlink.sh | 2 +-
19 files changed, 889 insertions(+), 447 deletions(-)
create mode 100644 builtin/merge-octopus.c
create mode 100644 builtin/merge-one-file.c
create mode 100644 builtin/merge-resolve.c
delete mode 100755 git-merge-octopus.sh
delete mode 100755 git-merge-one-file.sh
delete mode 100755 git-merge-resolve.sh
create mode 100644 merge-strategies.c
create mode 100644 merge-strategies.h
--
2.27.0.139.gc9c318d6bf
^ permalink raw reply [flat|nested] 221+ messages in thread
* [RFC PATCH v1 01/17] t6027: modernise tests
2020-06-25 12:19 [RFC PATCH v1 00/17] Rewrite the remaining merge strategies from shell to C Alban Gruin
@ 2020-06-25 12:19 ` Alban Gruin
2020-06-25 12:19 ` [RFC PATCH v1 02/17] merge-one-file: rewrite in C Alban Gruin
` (16 subsequent siblings)
17 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-06-25 12:19 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Alban Gruin
Some tests in t6027 uses a if/then/else to check if a command failed or
not, but we have the `test_must_fail' function to do it correctly for us
nowadays.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
t/t6027-merge-binary.sh | 27 ++++++---------------------
1 file changed, 6 insertions(+), 21 deletions(-)
diff --git a/t/t6027-merge-binary.sh b/t/t6027-merge-binary.sh
index 4e6c7cb77e..071d3f7343 100755
--- a/t/t6027-merge-binary.sh
+++ b/t/t6027-merge-binary.sh
@@ -5,7 +5,6 @@ test_description='ask merge-recursive to merge binary files'
. ./test-lib.sh
test_expect_success setup '
-
cat "$TEST_DIRECTORY"/test-binary-1.png >m &&
git add m &&
git ls-files -s | sed -e "s/ 0 / 1 /" >E1 &&
@@ -35,33 +34,19 @@ test_expect_success setup '
'
test_expect_success resolve '
-
rm -f a* m* &&
git reset --hard anchor &&
-
- if git merge -s resolve master
- then
- echo Oops, should not have succeeded
- false
- else
- git ls-files -s >current
- test_cmp expect current
- fi
+ test_must_fail git merge -s resolve master &&
+ git ls-files -s >current &&
+ test_cmp expect current
'
test_expect_success recursive '
-
rm -f a* m* &&
git reset --hard anchor &&
-
- if git merge -s recursive master
- then
- echo Oops, should not have succeeded
- false
- else
- git ls-files -s >current
- test_cmp expect current
- fi
+ test_must_fail git merge -s recursive master &&
+ git ls-files -s >current &&
+ test_cmp expect current
'
test_done
--
2.27.0.139.gc9c318d6bf
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [RFC PATCH v1 02/17] merge-one-file: rewrite in C
2020-06-25 12:19 [RFC PATCH v1 00/17] Rewrite the remaining merge strategies from shell to C Alban Gruin
2020-06-25 12:19 ` [RFC PATCH v1 01/17] t6027: modernise tests Alban Gruin
@ 2020-06-25 12:19 ` Alban Gruin
2020-06-25 14:55 ` Chris Torek
2020-06-25 15:16 ` Phillip Wood
2020-06-25 12:19 ` [RFC PATCH v1 03/17] merge-one-file: remove calls to external processes Alban Gruin
` (15 subsequent siblings)
17 siblings, 2 replies; 221+ messages in thread
From: Alban Gruin @ 2020-06-25 12:19 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Alban Gruin
This rewrites `git merge-one-file' from shell to C. This port is very
straightforward: it keeps using external processes to edit the index,
for instance. Errors are also displayed with fprintf() instead of
error(). Both of these will be addressed in the next few commits,
leading to its libification so its main function can be used from other
commands directly.
This also fixes a bug present in the original script: instead of
checking if a _regular_ file exists when a file exists in the branch to
merge, but not in our branch, the rewritten version checks if a file of
any kind (ie. a directory, ...) exists. This fixes the tests t6035.14,
where the branch to merge had a new file, `a/b', but our branch had a
directory there; it should have failed because a directory exists, but
it did not because there was no regular file called `a/b'. This test is
now marked as successful.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Makefile | 2 +-
builtin.h | 1 +
builtin/merge-one-file.c | 275 ++++++++++++++++++++++++++++++++
git-merge-one-file.sh | 167 -------------------
git.c | 1 +
t/t6035-merge-dir-to-symlink.sh | 2 +-
6 files changed, 279 insertions(+), 169 deletions(-)
create mode 100644 builtin/merge-one-file.c
delete mode 100755 git-merge-one-file.sh
diff --git a/Makefile b/Makefile
index 372139f1f2..19574f5133 100644
--- a/Makefile
+++ b/Makefile
@@ -596,7 +596,6 @@ SCRIPT_SH += git-bisect.sh
SCRIPT_SH += git-difftool--helper.sh
SCRIPT_SH += git-filter-branch.sh
SCRIPT_SH += git-merge-octopus.sh
-SCRIPT_SH += git-merge-one-file.sh
SCRIPT_SH += git-merge-resolve.sh
SCRIPT_SH += git-mergetool.sh
SCRIPT_SH += git-quiltimport.sh
@@ -1089,6 +1088,7 @@ BUILTIN_OBJS += builtin/mailsplit.o
BUILTIN_OBJS += builtin/merge-base.o
BUILTIN_OBJS += builtin/merge-file.o
BUILTIN_OBJS += builtin/merge-index.o
+BUILTIN_OBJS += builtin/merge-one-file.o
BUILTIN_OBJS += builtin/merge-ours.o
BUILTIN_OBJS += builtin/merge-recursive.o
BUILTIN_OBJS += builtin/merge-tree.o
diff --git a/builtin.h b/builtin.h
index a5ae15bfe5..9205d5ecdc 100644
--- a/builtin.h
+++ b/builtin.h
@@ -172,6 +172,7 @@ int cmd_merge_base(int argc, const char **argv, const char *prefix);
int cmd_merge_index(int argc, const char **argv, const char *prefix);
int cmd_merge_ours(int argc, const char **argv, const char *prefix);
int cmd_merge_file(int argc, const char **argv, const char *prefix);
+int cmd_merge_one_file(int argc, const char **argv, const char *prefix);
int cmd_merge_recursive(int argc, const char **argv, const char *prefix);
int cmd_merge_tree(int argc, const char **argv, const char *prefix);
int cmd_mktag(int argc, const char **argv, const char *prefix);
diff --git a/builtin/merge-one-file.c b/builtin/merge-one-file.c
new file mode 100644
index 0000000000..4992a6cd30
--- /dev/null
+++ b/builtin/merge-one-file.c
@@ -0,0 +1,275 @@
+/*
+ * Builtin "git merge-one-file"
+ *
+ * Copyright (c) 2020 Alban Gruin
+ *
+ * Based on git-merge-one-file.sh, written by Linus Torvalds.
+ *
+ * This is the git per-file merge script, called with
+ *
+ * $1 - original file SHA1 (or empty)
+ * $2 - file in branch1 SHA1 (or empty)
+ * $3 - file in branch2 SHA1 (or empty)
+ * $4 - pathname in repository
+ * $5 - original file mode (or empty)
+ * $6 - file in branch1 mode (or empty)
+ * $7 - file in branch2 mode (or empty)
+ *
+ * Handle some trivial cases.. The _really_ trivial cases have
+ * been handled already by git read-tree, but that one doesn't
+ * do any merges that might change the tree layout.
+ */
+
+#define USE_THE_INDEX_COMPATIBILITY_MACROS
+#include "cache.h"
+#include "builtin.h"
+#include "commit.h"
+#include "dir.h"
+#include "lockfile.h"
+#include "object-store.h"
+#include "run-command.h"
+#include "xdiff-interface.h"
+
+static int create_temp_file(const struct object_id *oid, struct strbuf *path)
+{
+ struct child_process cp = CHILD_PROCESS_INIT;
+ struct strbuf err = STRBUF_INIT;
+ int ret;
+
+ cp.git_cmd = 1;
+ argv_array_pushl(&cp.args, "unpack-file", oid_to_hex(oid), NULL);
+ ret = pipe_command(&cp, NULL, 0, path, 0, &err, 0);
+ if (!ret && path->len > 0)
+ strbuf_trim_trailing_newline(path);
+
+ fprintf(stderr, "%.*s", (int) err.len, err.buf);
+ strbuf_release(&err);
+
+ return ret;
+}
+
+static int add_to_index_cacheinfo(unsigned int mode,
+ const struct object_id *oid, const char *path)
+{
+ struct child_process cp = CHILD_PROCESS_INIT;
+
+ cp.git_cmd = 1;
+ argv_array_pushl(&cp.args, "update-index", "--add", "--cacheinfo", NULL);
+ argv_array_pushf(&cp.args, "%o,%s,%s", mode, oid_to_hex(oid), path);
+ return run_command(&cp);
+}
+
+static int remove_from_index(const char *path)
+{
+ struct child_process cp = CHILD_PROCESS_INIT;
+
+ cp.git_cmd = 1;
+ argv_array_pushl(&cp.args, "update-index", "--remove", "--", path, NULL);
+ return run_command(&cp);
+}
+
+static int checkout_from_index(const char *path)
+{
+ struct child_process cp = CHILD_PROCESS_INIT;
+
+ cp.git_cmd = 1;
+ argv_array_pushl(&cp.args, "checkout-index", "-u", "-f", "--", path, NULL);
+ return run_command(&cp);
+}
+
+static int merge_one_file_deleted(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
+{
+ if ((our_blob && orig_mode != our_mode) ||
+ (their_blob && orig_mode != their_mode)) {
+ fprintf(stderr, "ERROR: File %s deleted on one branch but had its\n", path);
+ fprintf(stderr, "ERROR: permissions changed on the other.\n");
+ return 1;
+ }
+
+ if (our_blob) {
+ printf("Removing %s\n", path);
+
+ if (file_exists(path))
+ remove_path(path);
+ }
+
+ return remove_from_index(path);
+}
+
+static int do_merge_one_file(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
+{
+ int ret, source, dest;
+ struct strbuf src1 = STRBUF_INIT, src2 = STRBUF_INIT, orig = STRBUF_INIT;
+ struct child_process cp_merge = CHILD_PROCESS_INIT,
+ cp_checkout = CHILD_PROCESS_INIT,
+ cp_update = CHILD_PROCESS_INIT;
+
+ if (our_mode == S_IFLNK || their_mode == S_IFLNK) {
+ fprintf(stderr, "ERROR: %s: Not merging symbolic link changes.\n", path);
+ return 1;
+ } else if (our_mode == S_IFGITLINK || their_mode == S_IFGITLINK) {
+ fprintf(stderr, "ERROR: %s: Not merging conflicting submodule changes.\n",
+ path);
+ return 1;
+ }
+
+ create_temp_file(our_blob, &src1);
+ create_temp_file(their_blob, &src2);
+
+ if (orig_blob) {
+ printf("Auto-merging %s\n", path);
+ create_temp_file(orig_blob, &orig);
+ } else {
+ printf("Added %s in both, but differently.\n", path);
+ create_temp_file(the_hash_algo->empty_blob, &orig);
+ }
+
+ cp_merge.git_cmd = 1;
+ argv_array_pushl(&cp_merge.args, "merge-file", src1.buf, orig.buf, src2.buf,
+ NULL);
+ ret = run_command(&cp_merge);
+
+ if (ret != 0)
+ ret = 1;
+
+ cp_checkout.git_cmd = 1;
+ argv_array_pushl(&cp_checkout.args, "checkout-index", "-f", "--stage=2",
+ "--", path, NULL);
+ if (run_command(&cp_checkout))
+ return 1;
+
+ source = open(src1.buf, O_RDONLY);
+ dest = open(path, O_WRONLY | O_TRUNC);
+
+ copy_fd(source, dest);
+
+ close(source);
+ close(dest);
+
+ unlink(orig.buf);
+ unlink(src1.buf);
+ unlink(src2.buf);
+
+ strbuf_release(&src1);
+ strbuf_release(&src2);
+ strbuf_release(&orig);
+
+ if (ret) {
+ fprintf(stderr, "ERROR: ");
+
+ if (!orig_blob) {
+ fprintf(stderr, "content conflict");
+ if (our_mode != their_mode)
+ fprintf(stderr, ", ");
+ }
+
+ if (our_mode != their_mode)
+ fprintf(stderr, "permissions conflict: %o->%o,%o",
+ orig_mode, our_mode, their_mode);
+
+ fprintf(stderr, " in %s\n", path);
+
+ return 1;
+ }
+
+ cp_update.git_cmd = 1;
+ argv_array_pushl(&cp_update.args, "update-index", "--", path, NULL);
+ return run_command(&cp_update);
+}
+
+static int merge_one_file(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
+{
+ if (orig_blob &&
+ ((our_blob && oideq(orig_blob, our_blob)) ||
+ (their_blob && oideq(orig_blob, their_blob))))
+ return merge_one_file_deleted(orig_blob, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+ else if (!orig_blob && our_blob && !their_blob) {
+ return add_to_index_cacheinfo(our_mode, our_blob, path);
+ } else if (!orig_blob && !our_blob && their_blob) {
+ printf("Adding %s\n", path);
+
+ if (file_exists(path)) {
+ fprintf(stderr, "ERROR: untracked %s is overwritten by the merge.\n", path);
+ return 1;
+ }
+
+ if (add_to_index_cacheinfo(their_mode, their_blob, path))
+ return 1;
+ return checkout_from_index(path);
+ } else if (!orig_blob && our_blob && their_blob &&
+ oideq(our_blob, their_blob)) {
+ if (our_mode != their_mode) {
+ fprintf(stderr, "ERROR: File %s added identically in both branches,", path);
+ fprintf(stderr, "ERROR: but permissions conflict %o->%o.\n",
+ our_mode, their_mode);
+ return 1;
+ }
+
+ printf("Adding %s\n", path);
+
+ if (add_to_index_cacheinfo(our_mode, our_blob, path))
+ return 1;
+ return checkout_from_index(path);
+ } else if (our_blob && their_blob)
+ return do_merge_one_file(orig_blob, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+ else {
+ char *orig_hex = "", *our_hex = "", *their_hex = "";
+
+ if (orig_blob)
+ orig_hex = oid_to_hex(orig_blob);
+ if (our_blob)
+ our_hex = oid_to_hex(our_blob);
+ if (their_blob)
+ their_hex = oid_to_hex(their_blob);
+
+ fprintf(stderr, "ERROR: %s: Not handling case %s -> %s -> %s\n",
+ path, orig_hex, our_hex, their_hex);
+ return 1;
+ }
+
+ return 0;
+}
+
+static const char builtin_merge_one_file_usage[] =
+ "git merge-one-file <orig blob> <our blob> <their blob> <path> "
+ "<orig mode> <our mode> <their mode>\n\n"
+ "Blob ids and modes should be empty for missing files.";
+
+int cmd_merge_one_file(int argc, const char **argv, const char *prefix)
+{
+ struct object_id orig_blob, our_blob, their_blob,
+ *p_orig_blob = NULL, *p_our_blob = NULL, *p_their_blob = NULL;
+ unsigned int orig_mode = 0, our_mode = 0, their_mode = 0;
+
+ if (argc != 8)
+ usage(builtin_merge_one_file_usage);
+
+ if (!get_oid(argv[1], &orig_blob)) {
+ p_orig_blob = &orig_blob;
+ orig_mode = strtol(argv[5], NULL, 8);
+ }
+
+ if (!get_oid(argv[2], &our_blob)) {
+ p_our_blob = &our_blob;
+ our_mode = strtol(argv[6], NULL, 8);
+ }
+
+ if (!get_oid(argv[3], &their_blob)) {
+ p_their_blob = &their_blob;
+ their_mode = strtol(argv[7], NULL, 8);
+ }
+
+ return merge_one_file(p_orig_blob, p_our_blob, p_their_blob, argv[4],
+ orig_mode, our_mode, their_mode);
+}
diff --git a/git-merge-one-file.sh b/git-merge-one-file.sh
deleted file mode 100755
index f6d9852d2f..0000000000
--- a/git-merge-one-file.sh
+++ /dev/null
@@ -1,167 +0,0 @@
-#!/bin/sh
-#
-# Copyright (c) Linus Torvalds, 2005
-#
-# This is the git per-file merge script, called with
-#
-# $1 - original file SHA1 (or empty)
-# $2 - file in branch1 SHA1 (or empty)
-# $3 - file in branch2 SHA1 (or empty)
-# $4 - pathname in repository
-# $5 - original file mode (or empty)
-# $6 - file in branch1 mode (or empty)
-# $7 - file in branch2 mode (or empty)
-#
-# Handle some trivial cases.. The _really_ trivial cases have
-# been handled already by git read-tree, but that one doesn't
-# do any merges that might change the tree layout.
-
-USAGE='<orig blob> <our blob> <their blob> <path>'
-USAGE="$USAGE <orig mode> <our mode> <their mode>"
-LONG_USAGE="usage: git merge-one-file $USAGE
-
-Blob ids and modes should be empty for missing files."
-
-SUBDIRECTORY_OK=Yes
-. git-sh-setup
-cd_to_toplevel
-require_work_tree
-
-if test $# != 7
-then
- echo "$LONG_USAGE"
- exit 1
-fi
-
-case "${1:-.}${2:-.}${3:-.}" in
-#
-# Deleted in both or deleted in one and unchanged in the other
-#
-"$1.." | "$1.$1" | "$1$1.")
- if { test -z "$6" && test "$5" != "$7"; } ||
- { test -z "$7" && test "$5" != "$6"; }
- then
- echo "ERROR: File $4 deleted on one branch but had its" >&2
- echo "ERROR: permissions changed on the other." >&2
- exit 1
- fi
-
- if test -n "$2"
- then
- echo "Removing $4"
- else
- # read-tree checked that index matches HEAD already,
- # so we know we do not have this path tracked.
- # there may be an unrelated working tree file here,
- # which we should just leave unmolested. Make sure
- # we do not have it in the index, though.
- exec git update-index --remove -- "$4"
- fi
- if test -f "$4"
- then
- rm -f -- "$4" &&
- rmdir -p "$(expr "z$4" : 'z\(.*\)/')" 2>/dev/null || :
- fi &&
- exec git update-index --remove -- "$4"
- ;;
-
-#
-# Added in one.
-#
-".$2.")
- # the other side did not add and we added so there is nothing
- # to be done, except making the path merged.
- exec git update-index --add --cacheinfo "$6" "$2" "$4"
- ;;
-"..$3")
- echo "Adding $4"
- if test -f "$4"
- then
- echo "ERROR: untracked $4 is overwritten by the merge." >&2
- exit 1
- fi
- git update-index --add --cacheinfo "$7" "$3" "$4" &&
- exec git checkout-index -u -f -- "$4"
- ;;
-
-#
-# Added in both, identically (check for same permissions).
-#
-".$3$2")
- if test "$6" != "$7"
- then
- echo "ERROR: File $4 added identically in both branches," >&2
- echo "ERROR: but permissions conflict $6->$7." >&2
- exit 1
- fi
- echo "Adding $4"
- git update-index --add --cacheinfo "$6" "$2" "$4" &&
- exec git checkout-index -u -f -- "$4"
- ;;
-
-#
-# Modified in both, but differently.
-#
-"$1$2$3" | ".$2$3")
-
- case ",$6,$7," in
- *,120000,*)
- echo "ERROR: $4: Not merging symbolic link changes." >&2
- exit 1
- ;;
- *,160000,*)
- echo "ERROR: $4: Not merging conflicting submodule changes." >&2
- exit 1
- ;;
- esac
-
- src1=$(git unpack-file $2)
- src2=$(git unpack-file $3)
- case "$1" in
- '')
- echo "Added $4 in both, but differently."
- orig=$(git unpack-file $(git hash-object /dev/null))
- ;;
- *)
- echo "Auto-merging $4"
- orig=$(git unpack-file $1)
- ;;
- esac
-
- git merge-file "$src1" "$orig" "$src2"
- ret=$?
- msg=
- if test $ret != 0 || test -z "$1"
- then
- msg='content conflict'
- ret=1
- fi
-
- # Create the working tree file, using "our tree" version from the
- # index, and then store the result of the merge.
- git checkout-index -f --stage=2 -- "$4" && cat "$src1" >"$4" || exit 1
- rm -f -- "$orig" "$src1" "$src2"
-
- if test "$6" != "$7"
- then
- if test -n "$msg"
- then
- msg="$msg, "
- fi
- msg="${msg}permissions conflict: $5->$6,$7"
- ret=1
- fi
-
- if test $ret != 0
- then
- echo "ERROR: $msg in $4" >&2
- exit 1
- fi
- exec git update-index -- "$4"
- ;;
-
-*)
- echo "ERROR: $4: Not handling case $1 -> $2 -> $3" >&2
- ;;
-esac
-exit 1
diff --git a/git.c b/git.c
index a2d337eed7..058d91a2a5 100644
--- a/git.c
+++ b/git.c
@@ -532,6 +532,7 @@ static struct cmd_struct commands[] = {
{ "merge-file", cmd_merge_file, RUN_SETUP_GENTLY },
{ "merge-index", cmd_merge_index, RUN_SETUP | NO_PARSEOPT },
{ "merge-ours", cmd_merge_ours, RUN_SETUP | NO_PARSEOPT },
+ { "merge-one-file", cmd_merge_one_file, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-ours", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-theirs", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
diff --git a/t/t6035-merge-dir-to-symlink.sh b/t/t6035-merge-dir-to-symlink.sh
index 2eddcc7664..5fb74e39a0 100755
--- a/t/t6035-merge-dir-to-symlink.sh
+++ b/t/t6035-merge-dir-to-symlink.sh
@@ -94,7 +94,7 @@ test_expect_success SYMLINKS 'a/b was resolved as symlink' '
test -h a/b
'
-test_expect_failure 'do not lose untracked in merge (resolve)' '
+test_expect_success 'do not lose untracked in merge (resolve)' '
git reset --hard &&
git checkout baseline^0 &&
>a/b/c/e &&
--
2.27.0.139.gc9c318d6bf
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [RFC PATCH v1 03/17] merge-one-file: remove calls to external processes
2020-06-25 12:19 [RFC PATCH v1 00/17] Rewrite the remaining merge strategies from shell to C Alban Gruin
2020-06-25 12:19 ` [RFC PATCH v1 01/17] t6027: modernise tests Alban Gruin
2020-06-25 12:19 ` [RFC PATCH v1 02/17] merge-one-file: rewrite in C Alban Gruin
@ 2020-06-25 12:19 ` Alban Gruin
2020-06-25 12:19 ` [RFC PATCH v1 04/17] merge-one-file: use error() instead of fprintf(stderr, ...) Alban Gruin
` (14 subsequent siblings)
17 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-06-25 12:19 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Alban Gruin
To save precious cycles by avoiding reading and flushing the index
repeatedly, or write temporary files when an operation can be performed
in-memory, this removes call to external processes:
- calls to `update-index --add --cacheinfo' are replaced by calls to
add_cache_entry();
- calls to `update-index --remove' are replaced by calls to
remove_file_from_cache();
- calls to `checkout-index -u -f' are replaced by calls to
checkout_entry();
- calls to `unpack-file' and `merge-files' are replaced by calls to
read_mmblob() and xdl_merge(), respectively, to merge files
in-memory;
- calls to `checkout-index -f --stage=2' are replaced by calls to
cache_file_exists();
- calls to `update-index' are replaced by calls to add_file_to_cache().
To enable these changes, the index is read and written back in
cmd_merge_one_file().
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge-one-file.c | 160 +++++++++++++++++++--------------------
1 file changed, 78 insertions(+), 82 deletions(-)
diff --git a/builtin/merge-one-file.c b/builtin/merge-one-file.c
index 4992a6cd30..d9ebd820cb 100644
--- a/builtin/merge-one-file.c
+++ b/builtin/merge-one-file.c
@@ -27,54 +27,48 @@
#include "dir.h"
#include "lockfile.h"
#include "object-store.h"
-#include "run-command.h"
#include "xdiff-interface.h"
-static int create_temp_file(const struct object_id *oid, struct strbuf *path)
-{
- struct child_process cp = CHILD_PROCESS_INIT;
- struct strbuf err = STRBUF_INIT;
- int ret;
-
- cp.git_cmd = 1;
- argv_array_pushl(&cp.args, "unpack-file", oid_to_hex(oid), NULL);
- ret = pipe_command(&cp, NULL, 0, path, 0, &err, 0);
- if (!ret && path->len > 0)
- strbuf_trim_trailing_newline(path);
-
- fprintf(stderr, "%.*s", (int) err.len, err.buf);
- strbuf_release(&err);
-
- return ret;
-}
-
static int add_to_index_cacheinfo(unsigned int mode,
const struct object_id *oid, const char *path)
{
- struct child_process cp = CHILD_PROCESS_INIT;
+ struct cache_entry *ce;
+ int len, option;
- cp.git_cmd = 1;
- argv_array_pushl(&cp.args, "update-index", "--add", "--cacheinfo", NULL);
- argv_array_pushf(&cp.args, "%o,%s,%s", mode, oid_to_hex(oid), path);
- return run_command(&cp);
-}
+ if (!verify_path(path, mode))
+ return error("Invalid path '%s'", path);
-static int remove_from_index(const char *path)
-{
- struct child_process cp = CHILD_PROCESS_INIT;
+ len = strlen(path);
+ ce = make_empty_cache_entry(&the_index, len);
- cp.git_cmd = 1;
- argv_array_pushl(&cp.args, "update-index", "--remove", "--", path, NULL);
- return run_command(&cp);
+ oidcpy(&ce->oid, oid);
+ memcpy(ce->name, path, len);
+ ce->ce_flags = create_ce_flags(0);
+ ce->ce_namelen = len;
+ ce->ce_mode = create_ce_mode(mode);
+ if (assume_unchanged)
+ ce->ce_flags |= CE_VALID;
+ option = ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE;
+ if (add_cache_entry(ce, option))
+ return error("%s: cannot add to the index", path);
+
+ return 0;
}
static int checkout_from_index(const char *path)
{
- struct child_process cp = CHILD_PROCESS_INIT;
+ struct checkout state;
+ struct cache_entry *ce;
- cp.git_cmd = 1;
- argv_array_pushl(&cp.args, "checkout-index", "-u", "-f", "--", path, NULL);
- return run_command(&cp);
+ state.istate = &the_index;
+ state.force = 1;
+ state.base_dir = "";
+ state.base_dir_len = 0;
+
+ ce = cache_file_exists(path, strlen(path), 0);
+ if (checkout_entry(ce, &state, NULL, NULL) < 0)
+ return error("%s: cannot checkout file", path);
+ return 0;
}
static int merge_one_file_deleted(const struct object_id *orig_blob,
@@ -96,7 +90,9 @@ static int merge_one_file_deleted(const struct object_id *orig_blob,
remove_path(path);
}
- return remove_from_index(path);
+ if (remove_file_from_cache(path))
+ return error("%s: cannot remove from the index", path);
+ return 0;
}
static int do_merge_one_file(const struct object_id *orig_blob,
@@ -104,61 +100,50 @@ static int do_merge_one_file(const struct object_id *orig_blob,
const struct object_id *their_blob, const char *path,
unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
{
- int ret, source, dest;
- struct strbuf src1 = STRBUF_INIT, src2 = STRBUF_INIT, orig = STRBUF_INIT;
- struct child_process cp_merge = CHILD_PROCESS_INIT,
- cp_checkout = CHILD_PROCESS_INIT,
- cp_update = CHILD_PROCESS_INIT;
+ int ret, i, dest;
+ mmbuffer_t result = {NULL, 0};
+ mmfile_t mmfs[3];
+ xmparam_t xmp = {{0}};
+ struct cache_entry *ce;
- if (our_mode == S_IFLNK || their_mode == S_IFLNK) {
- fprintf(stderr, "ERROR: %s: Not merging symbolic link changes.\n", path);
- return 1;
- } else if (our_mode == S_IFGITLINK || their_mode == S_IFGITLINK) {
- fprintf(stderr, "ERROR: %s: Not merging conflicting submodule changes.\n",
- path);
- return 1;
- }
+ if (our_mode == S_IFLNK || their_mode == S_IFLNK)
+ return error(_("%s: Not merging symbolic link changes."), path);
+ else if (our_mode == S_IFGITLINK || their_mode == S_IFGITLINK)
+ return error(_("%s: Not merging conflicting submodule changes."), path);
- create_temp_file(our_blob, &src1);
- create_temp_file(their_blob, &src2);
+ read_mmblob(mmfs + 0, our_blob);
+ read_mmblob(mmfs + 2, their_blob);
if (orig_blob) {
printf("Auto-merging %s\n", path);
- create_temp_file(orig_blob, &orig);
+ read_mmblob(mmfs + 1, orig_blob);
} else {
printf("Added %s in both, but differently.\n", path);
- create_temp_file(the_hash_algo->empty_blob, &orig);
+ read_mmblob(mmfs + 1, the_hash_algo->empty_blob);
}
- cp_merge.git_cmd = 1;
- argv_array_pushl(&cp_merge.args, "merge-file", src1.buf, orig.buf, src2.buf,
- NULL);
- ret = run_command(&cp_merge);
+ xmp.level = XDL_MERGE_ZEALOUS_ALNUM;
+ xmp.style = 0;
+ xmp.favor = 0;
- if (ret != 0)
+ ret = xdl_merge(mmfs + 1, mmfs + 0, mmfs + 2, &xmp, &result);
+
+ for (i = 0; i < 3; i++)
+ free(mmfs[i].ptr);
+
+ if (ret > 127)
ret = 1;
- cp_checkout.git_cmd = 1;
- argv_array_pushl(&cp_checkout.args, "checkout-index", "-f", "--stage=2",
- "--", path, NULL);
- if (run_command(&cp_checkout))
- return 1;
+ ce = cache_file_exists(path, strlen(path), 0);
+ if (!ce)
+ BUG("file is not present in the cache?");
- source = open(src1.buf, O_RDONLY);
- dest = open(path, O_WRONLY | O_TRUNC);
-
- copy_fd(source, dest);
-
- close(source);
+ unlink(path);
+ dest = open(path, O_WRONLY | O_CREAT, ce->ce_mode);
+ write_in_full(dest, result.ptr, result.size);
close(dest);
- unlink(orig.buf);
- unlink(src1.buf);
- unlink(src2.buf);
-
- strbuf_release(&src1);
- strbuf_release(&src2);
- strbuf_release(&orig);
+ free(result.ptr);
if (ret) {
fprintf(stderr, "ERROR: ");
@@ -178,9 +163,7 @@ static int do_merge_one_file(const struct object_id *orig_blob,
return 1;
}
- cp_update.git_cmd = 1;
- argv_array_pushl(&cp_update.args, "update-index", "--", path, NULL);
- return run_command(&cp_update);
+ return add_file_to_cache(path, 0);
}
static int merge_one_file(const struct object_id *orig_blob,
@@ -250,11 +233,17 @@ int cmd_merge_one_file(int argc, const char **argv, const char *prefix)
{
struct object_id orig_blob, our_blob, their_blob,
*p_orig_blob = NULL, *p_our_blob = NULL, *p_their_blob = NULL;
- unsigned int orig_mode = 0, our_mode = 0, their_mode = 0;
+ unsigned int orig_mode = 0, our_mode = 0, their_mode = 0, ret;
+ struct lock_file lock = LOCK_INIT;
if (argc != 8)
usage(builtin_merge_one_file_usage);
+ if (read_cache() < 0)
+ die("invalid index");
+
+ hold_locked_index(&lock, LOCK_DIE_ON_ERROR);
+
if (!get_oid(argv[1], &orig_blob)) {
p_orig_blob = &orig_blob;
orig_mode = strtol(argv[5], NULL, 8);
@@ -270,6 +259,13 @@ int cmd_merge_one_file(int argc, const char **argv, const char *prefix)
their_mode = strtol(argv[7], NULL, 8);
}
- return merge_one_file(p_orig_blob, p_our_blob, p_their_blob, argv[4],
- orig_mode, our_mode, their_mode);
+ ret = merge_one_file(p_orig_blob, p_our_blob, p_their_blob, argv[4],
+ orig_mode, our_mode, their_mode);
+
+ if (ret) {
+ rollback_lock_file(&lock);
+ return ret;
+ }
+
+ return write_locked_index(&the_index, &lock, COMMIT_LOCK);
}
--
2.27.0.139.gc9c318d6bf
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [RFC PATCH v1 04/17] merge-one-file: use error() instead of fprintf(stderr, ...)
2020-06-25 12:19 [RFC PATCH v1 00/17] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (2 preceding siblings ...)
2020-06-25 12:19 ` [RFC PATCH v1 03/17] merge-one-file: remove calls to external processes Alban Gruin
@ 2020-06-25 12:19 ` Alban Gruin
2020-06-25 12:19 ` [RFC PATCH v1 05/17] merge-one-file: libify merge_one_file() Alban Gruin
` (13 subsequent siblings)
17 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-06-25 12:19 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Alban Gruin
We have a handy helper function to display errors and return a value.
Use it instead of fprintf(stderr, ...).
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge-one-file.c | 43 +++++++++++++---------------------------
1 file changed, 14 insertions(+), 29 deletions(-)
diff --git a/builtin/merge-one-file.c b/builtin/merge-one-file.c
index d9ebd820cb..d612885723 100644
--- a/builtin/merge-one-file.c
+++ b/builtin/merge-one-file.c
@@ -77,11 +77,9 @@ static int merge_one_file_deleted(const struct object_id *orig_blob,
unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
{
if ((our_blob && orig_mode != our_mode) ||
- (their_blob && orig_mode != their_mode)) {
- fprintf(stderr, "ERROR: File %s deleted on one branch but had its\n", path);
- fprintf(stderr, "ERROR: permissions changed on the other.\n");
- return 1;
- }
+ (their_blob && orig_mode != their_mode))
+ return error(_("File %s deleted on one branch but had its "
+ "permissions changed on the other."), path);
if (our_blob) {
printf("Removing %s\n", path);
@@ -146,19 +144,11 @@ static int do_merge_one_file(const struct object_id *orig_blob,
free(result.ptr);
if (ret) {
- fprintf(stderr, "ERROR: ");
-
- if (!orig_blob) {
- fprintf(stderr, "content conflict");
- if (our_mode != their_mode)
- fprintf(stderr, ", ");
- }
-
+ if (!orig_blob)
+ error(_("content conflict in %s"), path);
if (our_mode != their_mode)
- fprintf(stderr, "permissions conflict: %o->%o,%o",
- orig_mode, our_mode, their_mode);
-
- fprintf(stderr, " in %s\n", path);
+ error(_("permission conflict: %o->%o,%o in %s"),
+ orig_mode, our_mode, their_mode, path);
return 1;
}
@@ -181,22 +171,18 @@ static int merge_one_file(const struct object_id *orig_blob,
} else if (!orig_blob && !our_blob && their_blob) {
printf("Adding %s\n", path);
- if (file_exists(path)) {
- fprintf(stderr, "ERROR: untracked %s is overwritten by the merge.\n", path);
- return 1;
- }
+ if (file_exists(path))
+ return error(_("untracked %s is overwritten by the merge."), path);
if (add_to_index_cacheinfo(their_mode, their_blob, path))
return 1;
return checkout_from_index(path);
} else if (!orig_blob && our_blob && their_blob &&
oideq(our_blob, their_blob)) {
- if (our_mode != their_mode) {
- fprintf(stderr, "ERROR: File %s added identically in both branches,", path);
- fprintf(stderr, "ERROR: but permissions conflict %o->%o.\n",
- our_mode, their_mode);
- return 1;
- }
+ if (our_mode != their_mode)
+ return error(_("File %s added identically in both branches, "
+ "but permissions conflict %o->%o."),
+ path, our_mode, their_mode);
printf("Adding %s\n", path);
@@ -216,9 +202,8 @@ static int merge_one_file(const struct object_id *orig_blob,
if (their_blob)
their_hex = oid_to_hex(their_blob);
- fprintf(stderr, "ERROR: %s: Not handling case %s -> %s -> %s\n",
+ return error(_("%s: Not handling case %s -> %s -> %s"),
path, orig_hex, our_hex, their_hex);
- return 1;
}
return 0;
--
2.27.0.139.gc9c318d6bf
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [RFC PATCH v1 05/17] merge-one-file: libify merge_one_file()
2020-06-25 12:19 [RFC PATCH v1 00/17] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (3 preceding siblings ...)
2020-06-25 12:19 ` [RFC PATCH v1 04/17] merge-one-file: use error() instead of fprintf(stderr, ...) Alban Gruin
@ 2020-06-25 12:19 ` Alban Gruin
2020-06-25 12:19 ` [RFC PATCH v1 06/17] merge-index: libify merge_one_path() and merge_all() Alban Gruin
` (12 subsequent siblings)
17 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-06-25 12:19 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Alban Gruin
This moves merge_one_file() (and its helper functions) to a new file,
merge-strategies.c. This will enable the resolve and octopus strategies
to directly call it instead of forking. It is also renamed
merge_strategies_one_file().
This is not a faithful copy-and-paste; in the builtin versions,
merge_one_file() operated on `the_repository' and `the_index', something
we cannot allow a function part of libgit.a to do. Hence, it now takes
a pointer to a repository as its first argument (and helper functions
takes a pointer to an `index_state').
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Notes:
This patch is best viewed with `--color-moved'.
Makefile | 1 +
builtin/merge-one-file.c | 190 +-------------------------------------
merge-strategies.c | 191 +++++++++++++++++++++++++++++++++++++++
merge-strategies.h | 13 +++
4 files changed, 209 insertions(+), 186 deletions(-)
create mode 100644 merge-strategies.c
create mode 100644 merge-strategies.h
diff --git a/Makefile b/Makefile
index 19574f5133..1ab4d160cb 100644
--- a/Makefile
+++ b/Makefile
@@ -911,6 +911,7 @@ LIB_OBJS += match-trees.o
LIB_OBJS += mem-pool.o
LIB_OBJS += merge-blobs.o
LIB_OBJS += merge-recursive.o
+LIB_OBJS += merge-strategies.o
LIB_OBJS += merge.o
LIB_OBJS += mergesort.o
LIB_OBJS += midx.o
diff --git a/builtin/merge-one-file.c b/builtin/merge-one-file.c
index d612885723..2f7a3e1db2 100644
--- a/builtin/merge-one-file.c
+++ b/builtin/merge-one-file.c
@@ -23,191 +23,8 @@
#define USE_THE_INDEX_COMPATIBILITY_MACROS
#include "cache.h"
#include "builtin.h"
-#include "commit.h"
-#include "dir.h"
#include "lockfile.h"
-#include "object-store.h"
-#include "xdiff-interface.h"
-
-static int add_to_index_cacheinfo(unsigned int mode,
- const struct object_id *oid, const char *path)
-{
- struct cache_entry *ce;
- int len, option;
-
- if (!verify_path(path, mode))
- return error("Invalid path '%s'", path);
-
- len = strlen(path);
- ce = make_empty_cache_entry(&the_index, len);
-
- oidcpy(&ce->oid, oid);
- memcpy(ce->name, path, len);
- ce->ce_flags = create_ce_flags(0);
- ce->ce_namelen = len;
- ce->ce_mode = create_ce_mode(mode);
- if (assume_unchanged)
- ce->ce_flags |= CE_VALID;
- option = ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE;
- if (add_cache_entry(ce, option))
- return error("%s: cannot add to the index", path);
-
- return 0;
-}
-
-static int checkout_from_index(const char *path)
-{
- struct checkout state;
- struct cache_entry *ce;
-
- state.istate = &the_index;
- state.force = 1;
- state.base_dir = "";
- state.base_dir_len = 0;
-
- ce = cache_file_exists(path, strlen(path), 0);
- if (checkout_entry(ce, &state, NULL, NULL) < 0)
- return error("%s: cannot checkout file", path);
- return 0;
-}
-
-static int merge_one_file_deleted(const struct object_id *orig_blob,
- const struct object_id *our_blob,
- const struct object_id *their_blob, const char *path,
- unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
-{
- if ((our_blob && orig_mode != our_mode) ||
- (their_blob && orig_mode != their_mode))
- return error(_("File %s deleted on one branch but had its "
- "permissions changed on the other."), path);
-
- if (our_blob) {
- printf("Removing %s\n", path);
-
- if (file_exists(path))
- remove_path(path);
- }
-
- if (remove_file_from_cache(path))
- return error("%s: cannot remove from the index", path);
- return 0;
-}
-
-static int do_merge_one_file(const struct object_id *orig_blob,
- const struct object_id *our_blob,
- const struct object_id *their_blob, const char *path,
- unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
-{
- int ret, i, dest;
- mmbuffer_t result = {NULL, 0};
- mmfile_t mmfs[3];
- xmparam_t xmp = {{0}};
- struct cache_entry *ce;
-
- if (our_mode == S_IFLNK || their_mode == S_IFLNK)
- return error(_("%s: Not merging symbolic link changes."), path);
- else if (our_mode == S_IFGITLINK || their_mode == S_IFGITLINK)
- return error(_("%s: Not merging conflicting submodule changes."), path);
-
- read_mmblob(mmfs + 0, our_blob);
- read_mmblob(mmfs + 2, their_blob);
-
- if (orig_blob) {
- printf("Auto-merging %s\n", path);
- read_mmblob(mmfs + 1, orig_blob);
- } else {
- printf("Added %s in both, but differently.\n", path);
- read_mmblob(mmfs + 1, the_hash_algo->empty_blob);
- }
-
- xmp.level = XDL_MERGE_ZEALOUS_ALNUM;
- xmp.style = 0;
- xmp.favor = 0;
-
- ret = xdl_merge(mmfs + 1, mmfs + 0, mmfs + 2, &xmp, &result);
-
- for (i = 0; i < 3; i++)
- free(mmfs[i].ptr);
-
- if (ret > 127)
- ret = 1;
-
- ce = cache_file_exists(path, strlen(path), 0);
- if (!ce)
- BUG("file is not present in the cache?");
-
- unlink(path);
- dest = open(path, O_WRONLY | O_CREAT, ce->ce_mode);
- write_in_full(dest, result.ptr, result.size);
- close(dest);
-
- free(result.ptr);
-
- if (ret) {
- if (!orig_blob)
- error(_("content conflict in %s"), path);
- if (our_mode != their_mode)
- error(_("permission conflict: %o->%o,%o in %s"),
- orig_mode, our_mode, their_mode, path);
-
- return 1;
- }
-
- return add_file_to_cache(path, 0);
-}
-
-static int merge_one_file(const struct object_id *orig_blob,
- const struct object_id *our_blob,
- const struct object_id *their_blob, const char *path,
- unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
-{
- if (orig_blob &&
- ((our_blob && oideq(orig_blob, our_blob)) ||
- (their_blob && oideq(orig_blob, their_blob))))
- return merge_one_file_deleted(orig_blob, our_blob, their_blob, path,
- orig_mode, our_mode, their_mode);
- else if (!orig_blob && our_blob && !their_blob) {
- return add_to_index_cacheinfo(our_mode, our_blob, path);
- } else if (!orig_blob && !our_blob && their_blob) {
- printf("Adding %s\n", path);
-
- if (file_exists(path))
- return error(_("untracked %s is overwritten by the merge."), path);
-
- if (add_to_index_cacheinfo(their_mode, their_blob, path))
- return 1;
- return checkout_from_index(path);
- } else if (!orig_blob && our_blob && their_blob &&
- oideq(our_blob, their_blob)) {
- if (our_mode != their_mode)
- return error(_("File %s added identically in both branches, "
- "but permissions conflict %o->%o."),
- path, our_mode, their_mode);
-
- printf("Adding %s\n", path);
-
- if (add_to_index_cacheinfo(our_mode, our_blob, path))
- return 1;
- return checkout_from_index(path);
- } else if (our_blob && their_blob)
- return do_merge_one_file(orig_blob, our_blob, their_blob, path,
- orig_mode, our_mode, their_mode);
- else {
- char *orig_hex = "", *our_hex = "", *their_hex = "";
-
- if (orig_blob)
- orig_hex = oid_to_hex(orig_blob);
- if (our_blob)
- our_hex = oid_to_hex(our_blob);
- if (their_blob)
- their_hex = oid_to_hex(their_blob);
-
- return error(_("%s: Not handling case %s -> %s -> %s"),
- path, orig_hex, our_hex, their_hex);
- }
-
- return 0;
-}
+#include "merge-strategies.h"
static const char builtin_merge_one_file_usage[] =
"git merge-one-file <orig blob> <our blob> <their blob> <path> "
@@ -244,8 +61,9 @@ int cmd_merge_one_file(int argc, const char **argv, const char *prefix)
their_mode = strtol(argv[7], NULL, 8);
}
- ret = merge_one_file(p_orig_blob, p_our_blob, p_their_blob, argv[4],
- orig_mode, our_mode, their_mode);
+ ret = merge_strategies_one_file(the_repository,
+ p_orig_blob, p_our_blob, p_their_blob, argv[4],
+ orig_mode, our_mode, their_mode);
if (ret) {
rollback_lock_file(&lock);
diff --git a/merge-strategies.c b/merge-strategies.c
new file mode 100644
index 0000000000..3a9fce9f22
--- /dev/null
+++ b/merge-strategies.c
@@ -0,0 +1,191 @@
+#include "cache.h"
+#include "dir.h"
+#include "merge-strategies.h"
+#include "xdiff-interface.h"
+
+static int add_to_index_cacheinfo(struct index_state *istate,
+ unsigned int mode,
+ const struct object_id *oid, const char *path)
+{
+ struct cache_entry *ce;
+ int len, option;
+
+ if (!verify_path(path, mode))
+ return error(_("Invalid path '%s'"), path);
+
+ len = strlen(path);
+ ce = make_empty_cache_entry(istate, len);
+
+ oidcpy(&ce->oid, oid);
+ memcpy(ce->name, path, len);
+ ce->ce_flags = create_ce_flags(0);
+ ce->ce_namelen = len;
+ ce->ce_mode = create_ce_mode(mode);
+ if (assume_unchanged)
+ ce->ce_flags |= CE_VALID;
+ option = ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE;
+ if (add_index_entry(istate, ce, option))
+ return error(_("%s: cannot add to the index"), path);
+
+ return 0;
+}
+
+static int checkout_from_index(struct index_state *istate, const char *path)
+{
+ struct checkout state = CHECKOUT_INIT;
+ struct cache_entry *ce;
+
+ state.istate = istate;
+ state.force = 1;
+ state.base_dir = "";
+ state.base_dir_len = 0;
+
+ ce = index_file_exists(istate, path, strlen(path), 0);
+ if (checkout_entry(ce, &state, NULL, NULL) < 0)
+ return error(_("%s: cannot checkout file"), path);
+ return 0;
+}
+
+static int merge_one_file_deleted(struct index_state *istate,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
+{
+ if ((our_blob && orig_mode != our_mode) ||
+ (their_blob && orig_mode != their_mode))
+ return error(_("File %s deleted on one branch but had its "
+ "permissions changed on the other."), path);
+
+ if (our_blob) {
+ printf("Removing %s\n", path);
+
+ if (file_exists(path))
+ remove_path(path);
+ }
+
+ if (remove_file_from_index(istate, path))
+ return error("%s: cannot remove from the index", path);
+ return 0;
+}
+
+static int do_merge_one_file(struct index_state *istate,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
+{
+ int ret, i, dest;
+ mmbuffer_t result = {NULL, 0};
+ mmfile_t mmfs[3];
+ xmparam_t xmp = {{0}};
+ struct cache_entry *ce;
+
+ if (our_mode == S_IFLNK || their_mode == S_IFLNK)
+ return error(_("%s: Not merging symbolic link changes."), path);
+ else if (our_mode == S_IFGITLINK || their_mode == S_IFGITLINK)
+ return error(_("%s: Not merging conflicting submodule changes."), path);
+
+ read_mmblob(mmfs + 0, our_blob);
+ read_mmblob(mmfs + 2, their_blob);
+
+ if (orig_blob) {
+ printf("Auto-merging %s\n", path);
+ read_mmblob(mmfs + 1, orig_blob);
+ } else {
+ printf("Added %s in both, but differently.\n", path);
+ read_mmblob(mmfs + 1, &null_oid);
+ }
+
+ xmp.level = XDL_MERGE_ZEALOUS_ALNUM;
+ xmp.style = 0;
+ xmp.favor = 0;
+
+ ret = xdl_merge(mmfs + 1, mmfs + 0, mmfs + 2, &xmp, &result);
+
+ for (i = 0; i < 3; i++)
+ free(mmfs[i].ptr);
+
+ if (ret > 127)
+ ret = 1;
+
+ ce = index_file_exists(istate, path, strlen(path), 0);
+ if (!ce)
+ BUG("file is not present in the cache?");
+
+ unlink(path);
+ dest = open(path, O_WRONLY | O_CREAT, ce->ce_mode);
+ write_in_full(dest, result.ptr, result.size);
+ close(dest);
+
+ free(result.ptr);
+
+ if (ret) {
+ if (!orig_blob)
+ error(_("content conflict in %s"), path);
+ if (our_mode != their_mode)
+ error(_("permission conflict: %o->%o,%o in %s"),
+ orig_mode, our_mode, their_mode, path);
+
+ return 1;
+ }
+
+ return add_file_to_index(istate, path, 0);
+}
+
+int merge_strategies_one_file(struct repository *r,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode,
+ unsigned int their_mode)
+{
+ if (orig_blob &&
+ ((our_blob && oideq(orig_blob, our_blob)) ||
+ (their_blob && oideq(orig_blob, their_blob))))
+ return merge_one_file_deleted(r->index,
+ orig_blob, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+ else if (!orig_blob && our_blob && !their_blob) {
+ return add_to_index_cacheinfo(r->index, our_mode, our_blob, path);
+ } else if (!orig_blob && !our_blob && their_blob) {
+ printf("Adding %s\n", path);
+
+ if (file_exists(path))
+ return error(_("untracked %s is overwritten by the merge."), path);
+
+ if (add_to_index_cacheinfo(r->index, their_mode, their_blob, path))
+ return 1;
+ return checkout_from_index(r->index, path);
+ } else if (!orig_blob && our_blob && their_blob &&
+ oideq(our_blob, their_blob)) {
+ if (our_mode != their_mode)
+ return error(_("File %s added identically in both branches, "
+ "but permissions conflict %o->%o."),
+ path, our_mode, their_mode);
+
+ printf("Adding %s\n", path);
+
+ if (add_to_index_cacheinfo(r->index, our_mode, our_blob, path))
+ return 1;
+ return checkout_from_index(r->index, path);
+ } else if (our_blob && their_blob)
+ return do_merge_one_file(r->index,
+ orig_blob, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+ else {
+ char *orig_hex = "", *our_hex = "", *their_hex = "";
+
+ if (orig_blob)
+ orig_hex = oid_to_hex(orig_blob);
+ if (our_blob)
+ our_hex = oid_to_hex(our_blob);
+ if (their_blob)
+ their_hex = oid_to_hex(their_blob);
+
+ return error(_("%s: Not handling case %s -> %s -> %s"),
+ path, orig_hex, our_hex, their_hex);
+ }
+
+ return 0;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
new file mode 100644
index 0000000000..b527d145c7
--- /dev/null
+++ b/merge-strategies.h
@@ -0,0 +1,13 @@
+#ifndef MERGE_STRATEGIES_H
+#define MERGE_STRATEGIES_H
+
+#include "object.h"
+
+int merge_strategies_one_file(struct repository *r,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode,
+ unsigned int their_mode);
+
+#endif /* MERGE_STRATEGIES_H */
--
2.27.0.139.gc9c318d6bf
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [RFC PATCH v1 06/17] merge-index: libify merge_one_path() and merge_all()
2020-06-25 12:19 [RFC PATCH v1 00/17] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (4 preceding siblings ...)
2020-06-25 12:19 ` [RFC PATCH v1 05/17] merge-one-file: libify merge_one_file() Alban Gruin
@ 2020-06-25 12:19 ` Alban Gruin
2020-06-26 10:13 ` Phillip Wood
2020-06-25 12:19 ` [RFC PATCH v1 07/17] merge-resolve: rewrite in C Alban Gruin
` (11 subsequent siblings)
17 siblings, 1 reply; 221+ messages in thread
From: Alban Gruin @ 2020-06-25 12:19 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Alban Gruin
The "resolve" and "octopus" merge strategies do not call directly `git
merge-one-file', they delegate the work to another git command, `git
merge-index', that will loop over files in the index and call the
specified command. Unfortunately, these functions are not part of
libgit.a, which means that once rewritten, the strategies would still
have to invoke `merge-one-file' by spawning a new process first.
To avoid this, this moves merge_one_path(), merge_all(), and their
helpers to merge-strategies.c. They also take a callback to dictate
what they should do for each file. For now, only one launching a new
process is defined to preserve the behaviour of the builtin version.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Notes:
This patch is best viewed with `--color-moved'.
builtin/merge-index.c | 77 +++------------------------------
merge-strategies.c | 99 +++++++++++++++++++++++++++++++++++++++++++
merge-strategies.h | 17 ++++++++
3 files changed, 123 insertions(+), 70 deletions(-)
diff --git a/builtin/merge-index.c b/builtin/merge-index.c
index 38ea6ad6ca..6cb666cc78 100644
--- a/builtin/merge-index.c
+++ b/builtin/merge-index.c
@@ -1,74 +1,11 @@
#define USE_THE_INDEX_COMPATIBILITY_MACROS
#include "builtin.h"
-#include "run-command.h"
-
-static const char *pgm;
-static int one_shot, quiet;
-static int err;
-
-static int merge_entry(int pos, const char *path)
-{
- int found;
- const char *arguments[] = { pgm, "", "", "", path, "", "", "", NULL };
- char hexbuf[4][GIT_MAX_HEXSZ + 1];
- char ownbuf[4][60];
-
- if (pos >= active_nr)
- die("git merge-index: %s not in the cache", path);
- found = 0;
- do {
- const struct cache_entry *ce = active_cache[pos];
- int stage = ce_stage(ce);
-
- if (strcmp(ce->name, path))
- break;
- found++;
- oid_to_hex_r(hexbuf[stage], &ce->oid);
- xsnprintf(ownbuf[stage], sizeof(ownbuf[stage]), "%o", ce->ce_mode);
- arguments[stage] = hexbuf[stage];
- arguments[stage + 4] = ownbuf[stage];
- } while (++pos < active_nr);
- if (!found)
- die("git merge-index: %s not in the cache", path);
-
- if (run_command_v_opt(arguments, 0)) {
- if (one_shot)
- err++;
- else {
- if (!quiet)
- die("merge program failed");
- exit(1);
- }
- }
- return found;
-}
-
-static void merge_one_path(const char *path)
-{
- int pos = cache_name_pos(path, strlen(path));
-
- /*
- * If it already exists in the cache as stage0, it's
- * already merged and there is nothing to do.
- */
- if (pos < 0)
- merge_entry(-pos-1, path);
-}
-
-static void merge_all(void)
-{
- int i;
- for (i = 0; i < active_nr; i++) {
- const struct cache_entry *ce = active_cache[i];
- if (!ce_stage(ce))
- continue;
- i += merge_entry(i, ce->name)-1;
- }
-}
+#include "merge-strategies.h"
int cmd_merge_index(int argc, const char **argv, const char *prefix)
{
- int i, force_file = 0;
+ int i, force_file = 0, err = 0, one_shot = 0, quiet = 0;
+ const char *pgm;
/* Without this we cannot rely on waitpid() to tell
* what happened to our children.
@@ -98,14 +35,14 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
continue;
}
if (!strcmp(arg, "-a")) {
- merge_all();
+ err |= merge_all(&the_index, one_shot, quiet,
+ merge_program_cb, (void *)pgm);
continue;
}
die("git merge-index: unknown option %s", arg);
}
- merge_one_path(arg);
+ err |= merge_one_path(&the_index, one_shot, quiet, arg,
+ merge_program_cb, (void *)pgm);
}
- if (err && !quiet)
- die("merge program failed");
return err;
}
diff --git a/merge-strategies.c b/merge-strategies.c
index 3a9fce9f22..f4c0b4acd6 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -1,6 +1,7 @@
#include "cache.h"
#include "dir.h"
#include "merge-strategies.h"
+#include "run-command.h"
#include "xdiff-interface.h"
static int add_to_index_cacheinfo(struct index_state *istate,
@@ -189,3 +190,101 @@ int merge_strategies_one_file(struct repository *r,
return 0;
}
+
+int merge_program_cb(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data)
+{
+ char ownbuf[3][60] = {{0}};
+ const char *arguments[] = { (char *)data, "", "", "", path,
+ ownbuf[0], ownbuf[1], ownbuf[2],
+ NULL };
+
+ if (orig_blob)
+ arguments[1] = oid_to_hex(orig_blob);
+ if (our_blob)
+ arguments[2] = oid_to_hex(our_blob);
+ if (their_blob)
+ arguments[3] = oid_to_hex(their_blob);
+
+ xsnprintf(ownbuf[0], sizeof(ownbuf[0]), "%o", orig_mode);
+ xsnprintf(ownbuf[1], sizeof(ownbuf[1]), "%o", our_mode);
+ xsnprintf(ownbuf[2], sizeof(ownbuf[2]), "%o", their_mode);
+
+ return run_command_v_opt(arguments, 0);
+}
+
+static int merge_entry(struct index_state *istate, int quiet, int pos,
+ const char *path, merge_cb cb, void *data)
+{
+ int found = 0;
+ const struct object_id *oids[3] = {NULL};
+ unsigned int modes[3] = {0};
+
+ do {
+ const struct cache_entry *ce = istate->cache[pos];
+ int stage = ce_stage(ce);
+
+ if (strcmp(ce->name, path))
+ break;
+ found++;
+ oids[stage - 1] = &ce->oid;
+ modes[stage - 1] = ce->ce_mode;
+ } while (++pos < istate->cache_nr);
+ if (!found)
+ return error(_("%s is not in the cache"), path);
+
+ if (cb(oids[0], oids[1], oids[2], path, modes[0], modes[1], modes[2], data)) {
+ if (!quiet)
+ error(_("Merge program failed"));
+ return -2;
+ }
+
+ return found;
+}
+
+int merge_one_path(struct index_state *istate, int oneshot, int quiet,
+ const char *path, merge_cb cb, void *data)
+{
+ int pos = index_name_pos(istate, path, strlen(path)), ret;
+
+ /*
+ * If it already exists in the cache as stage0, it's
+ * already merged and there is nothing to do.
+ */
+ if (pos < 0) {
+ ret = merge_entry(istate, quiet, -pos - 1, path, cb, data);
+ if (ret == -1)
+ return -1;
+ else if (ret == -2)
+ return 1;
+ }
+ return 0;
+}
+
+int merge_all(struct index_state *istate, int oneshot, int quiet,
+ merge_cb cb, void *data)
+{
+ int err = 0, i, ret;
+ for (i = 0; i < istate->cache_nr; i++) {
+ const struct cache_entry *ce = istate->cache[i];
+ if (!ce_stage(ce))
+ continue;
+
+ ret = merge_entry(istate, quiet, i, ce->name, cb, data);
+ if (ret > 0)
+ i += ret - 1;
+ else if (ret == -1)
+ return -1;
+ else if (ret == -2) {
+ if (oneshot)
+ err++;
+ else
+ return 1;
+ }
+ }
+
+ return err;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
index b527d145c7..cf78d7eaf4 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -10,4 +10,21 @@ int merge_strategies_one_file(struct repository *r,
unsigned int orig_mode, unsigned int our_mode,
unsigned int their_mode);
+typedef int (*merge_cb)(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
+int merge_program_cb(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
+int merge_one_path(struct index_state *istate, int oneshot, int quiet,
+ const char *path, merge_cb cb, void *data);
+int merge_all(struct index_state *istate, int oneshot, int quiet,
+ merge_cb cb, void *data);
+
#endif /* MERGE_STRATEGIES_H */
--
2.27.0.139.gc9c318d6bf
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [RFC PATCH v1 07/17] merge-resolve: rewrite in C
2020-06-25 12:19 [RFC PATCH v1 00/17] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (5 preceding siblings ...)
2020-06-25 12:19 ` [RFC PATCH v1 06/17] merge-index: libify merge_one_path() and merge_all() Alban Gruin
@ 2020-06-25 12:19 ` Alban Gruin
2020-06-25 12:19 ` [RFC PATCH v1 08/17] merge-resolve: remove calls to external processes Alban Gruin
` (10 subsequent siblings)
17 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-06-25 12:19 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Alban Gruin
This rewrites `git merge-resolve' from shell to C. As for `git
merge-one-file', this port keeps using external processes for operations
on the index, or to call `git merge-one-file'. This will be addressed
in the next two commits.
The parameters of merge_resolve() will be surprising at first glance:
why using a commit list for `bases' and `remote', where we could use an
oid array, and a pointer to an oid? Because, in a later commit,
try_merge_strategy() will be able to call merge_resolve() directly, and
it already uses a commit list for `bases' (`common') and
`remote' (`remoteheads'), and a string for `head_arg'. To reduce
frictions later, merge_resolve() takes the same types of parameters.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Makefile | 2 +-
builtin.h | 1 +
builtin/merge-resolve.c | 112 ++++++++++++++++++++++++++++++++++++++++
git-merge-resolve.sh | 54 -------------------
git.c | 1 +
5 files changed, 115 insertions(+), 55 deletions(-)
create mode 100644 builtin/merge-resolve.c
delete mode 100755 git-merge-resolve.sh
diff --git a/Makefile b/Makefile
index 1ab4d160cb..ccea651ac8 100644
--- a/Makefile
+++ b/Makefile
@@ -596,7 +596,6 @@ SCRIPT_SH += git-bisect.sh
SCRIPT_SH += git-difftool--helper.sh
SCRIPT_SH += git-filter-branch.sh
SCRIPT_SH += git-merge-octopus.sh
-SCRIPT_SH += git-merge-resolve.sh
SCRIPT_SH += git-mergetool.sh
SCRIPT_SH += git-quiltimport.sh
SCRIPT_SH += git-request-pull.sh
@@ -1092,6 +1091,7 @@ BUILTIN_OBJS += builtin/merge-index.o
BUILTIN_OBJS += builtin/merge-one-file.o
BUILTIN_OBJS += builtin/merge-ours.o
BUILTIN_OBJS += builtin/merge-recursive.o
+BUILTIN_OBJS += builtin/merge-resolve.o
BUILTIN_OBJS += builtin/merge-tree.o
BUILTIN_OBJS += builtin/merge.o
BUILTIN_OBJS += builtin/mktag.o
diff --git a/builtin.h b/builtin.h
index 9205d5ecdc..6ea207c9fd 100644
--- a/builtin.h
+++ b/builtin.h
@@ -174,6 +174,7 @@ int cmd_merge_ours(int argc, const char **argv, const char *prefix);
int cmd_merge_file(int argc, const char **argv, const char *prefix);
int cmd_merge_one_file(int argc, const char **argv, const char *prefix);
int cmd_merge_recursive(int argc, const char **argv, const char *prefix);
+int cmd_merge_resolve(int argc, const char **argv, const char *prefix);
int cmd_merge_tree(int argc, const char **argv, const char *prefix);
int cmd_mktag(int argc, const char **argv, const char *prefix);
int cmd_mktree(int argc, const char **argv, const char *prefix);
diff --git a/builtin/merge-resolve.c b/builtin/merge-resolve.c
new file mode 100644
index 0000000000..c66fef7b7f
--- /dev/null
+++ b/builtin/merge-resolve.c
@@ -0,0 +1,112 @@
+/*
+ * Builtin "git merge-resolve"
+ *
+ * Copyright (c) 2020 Alban Gruin
+ *
+ * Based on git-merge-resolve.sh, written by Linus Torvalds and Junio C
+ * Hamano.
+ *
+ * Resolve two trees, using enhanced multi-base read-tree.
+ */
+
+#include "cache.h"
+#include "builtin.h"
+#include "run-command.h"
+
+static int merge_resolve(struct commit_list *bases, const char *head_arg,
+ struct commit_list *remote)
+{
+ struct commit_list *j;
+ struct child_process cp_update = CHILD_PROCESS_INIT,
+ cp_read = CHILD_PROCESS_INIT,
+ cp_write = CHILD_PROCESS_INIT;
+
+ cp_update.git_cmd = 1;
+ argv_array_pushl(&cp_update.args, "update-index", "-q", "--refresh", NULL);
+ run_command(&cp_update);
+
+ cp_read.git_cmd = 1;
+ argv_array_pushl(&cp_read.args, "read-tree", "-u", "-m", "--aggressive", NULL);
+
+ for (j = bases; j && j->item; j = j->next)
+ argv_array_push(&cp_read.args, oid_to_hex(&j->item->object.oid));
+
+ if (head_arg)
+ argv_array_push(&cp_read.args, head_arg);
+ if (remote && remote->item)
+ argv_array_push(&cp_read.args, oid_to_hex(&remote->item->object.oid));
+
+ if (run_command(&cp_read))
+ return 2;
+
+ puts("Trying simple merge.");
+
+ cp_write.git_cmd = 1;
+ cp_write.no_stdout = 1;
+ cp_write.no_stderr = 1;
+ argv_array_push(&cp_write.args, "write-tree");
+ if (run_command(&cp_write)) {
+ struct child_process cp_merge = CHILD_PROCESS_INIT;
+
+ puts("Simple merge failed, trying Automatic merge.");
+
+ cp_merge.git_cmd = 1;
+ argv_array_pushl(&cp_merge.args, "merge-index", "-o",
+ "git-merge-one-file", "-a", NULL);
+ if (run_command(&cp_merge))
+ return 1;
+ }
+
+ return 0;
+}
+
+static const char builtin_merge_resolve_usage[] =
+ "git merge-resolve <bases>... -- <head> <remote>";
+
+int cmd_merge_resolve(int argc, const char **argv, const char *prefix)
+{
+ int i, is_baseless = 1, sep_seen = 0;
+ const char *head = NULL;
+ struct commit_list *bases = NULL, *remote = NULL;
+ struct commit_list **next_base = &bases;
+
+ if (argc < 5)
+ usage(builtin_merge_resolve_usage);
+
+ /* The first parameters up to -- are merge bases; the rest are
+ * heads. */
+ for (i = 1; i < argc; i++) {
+ if (strcmp(argv[i], "--") == 0)
+ sep_seen = 1;
+ else if (strcmp(argv[i], "-h") == 0)
+ usage(builtin_merge_resolve_usage);
+ else if (sep_seen && !head)
+ head = argv[i];
+ else if (remote) {
+ /* Give up if we are given two or more remotes.
+ * Not handling octopus. */
+ return 2;
+ } else {
+ struct object_id oid;
+
+ get_oid(argv[i], &oid);
+ is_baseless &= sep_seen;
+
+ if (!oideq(&oid, the_hash_algo->empty_tree)) {
+ struct commit *commit;
+ commit = lookup_commit_or_die(&oid, argv[i]);
+
+ if (sep_seen)
+ commit_list_append(commit, &remote);
+ else
+ next_base = commit_list_append(commit, next_base);
+ }
+ }
+ }
+
+ /* Give up if this is a baseless merge. */
+ if (is_baseless)
+ return 2;
+
+ return merge_resolve(bases, head, remote);
+}
diff --git a/git-merge-resolve.sh b/git-merge-resolve.sh
deleted file mode 100755
index 343fe7bccd..0000000000
--- a/git-merge-resolve.sh
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2005 Linus Torvalds
-# Copyright (c) 2005 Junio C Hamano
-#
-# Resolve two trees, using enhanced multi-base read-tree.
-
-# The first parameters up to -- are merge bases; the rest are heads.
-bases= head= remotes= sep_seen=
-for arg
-do
- case ",$sep_seen,$head,$arg," in
- *,--,)
- sep_seen=yes
- ;;
- ,yes,,*)
- head=$arg
- ;;
- ,yes,*)
- remotes="$remotes$arg "
- ;;
- *)
- bases="$bases$arg "
- ;;
- esac
-done
-
-# Give up if we are given two or more remotes -- not handling octopus.
-case "$remotes" in
-?*' '?*)
- exit 2 ;;
-esac
-
-# Give up if this is a baseless merge.
-if test '' = "$bases"
-then
- exit 2
-fi
-
-git update-index -q --refresh
-git read-tree -u -m --aggressive $bases $head $remotes || exit 2
-echo "Trying simple merge."
-if result_tree=$(git write-tree 2>/dev/null)
-then
- exit 0
-else
- echo "Simple merge failed, trying Automatic merge."
- if git merge-index -o git-merge-one-file -a
- then
- exit 0
- else
- exit 1
- fi
-fi
diff --git a/git.c b/git.c
index 058d91a2a5..2e92019493 100644
--- a/git.c
+++ b/git.c
@@ -536,6 +536,7 @@ static struct cmd_struct commands[] = {
{ "merge-recursive", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-ours", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-theirs", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
+ { "merge-resolve", cmd_merge_resolve, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-subtree", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-tree", cmd_merge_tree, RUN_SETUP | NO_PARSEOPT },
{ "mktag", cmd_mktag, RUN_SETUP | NO_PARSEOPT },
--
2.27.0.139.gc9c318d6bf
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [RFC PATCH v1 08/17] merge-resolve: remove calls to external processes
2020-06-25 12:19 [RFC PATCH v1 00/17] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (6 preceding siblings ...)
2020-06-25 12:19 ` [RFC PATCH v1 07/17] merge-resolve: rewrite in C Alban Gruin
@ 2020-06-25 12:19 ` Alban Gruin
2020-06-25 12:19 ` [RFC PATCH v1 09/17] merge-resolve: libify merge_resolve() Alban Gruin
` (9 subsequent siblings)
17 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-06-25 12:19 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Alban Gruin
This removes calls to external processes to avoid reading and writing
the index over and over again.
- The call to `update-index -q --refresh' is replaced by a call to
refresh_index().
- The call to `read-tree' is replaced by a call to unpack_trees() (and
all the setup needed).
- The call to `write-tree' is replaced by a call to
write_index_as_tree().
- The call to `merge-index', needed to invoke `git merge-one-file', is
replaced by a call to the new merge_all() function. A callback
function, merge_one_file_cb(), is added to allow it to call
merge_one_file() without forking.
Here too, the index is read in cmd_merge_resolve(), but merge_resolve()
takes care of writing it back to the disk.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge-resolve.c | 103 ++++++++++++++++++++++++++++------------
merge-strategies.c | 11 +++++
merge-strategies.h | 6 +++
3 files changed, 89 insertions(+), 31 deletions(-)
diff --git a/builtin/merge-resolve.c b/builtin/merge-resolve.c
index c66fef7b7f..2c364fcdb0 100644
--- a/builtin/merge-resolve.c
+++ b/builtin/merge-resolve.c
@@ -10,54 +10,91 @@
*/
#include "cache.h"
+#include "cache-tree.h"
#include "builtin.h"
-#include "run-command.h"
+#include "lockfile.h"
+#include "merge-strategies.h"
+#include "unpack-trees.h"
+
+static int add_tree(const struct object_id *oid, struct tree_desc *t)
+{
+ struct tree *tree;
+
+ tree = parse_tree_indirect(oid);
+ if (parse_tree(tree))
+ return -1;
+
+ init_tree_desc(t, tree->buffer, tree->size);
+ return 0;
+}
static int merge_resolve(struct commit_list *bases, const char *head_arg,
struct commit_list *remote)
{
+ int i = 0;
+ struct lock_file lock = LOCK_INIT;
+ struct tree_desc t[MAX_UNPACK_TREES];
+ struct unpack_trees_options opts;
+ struct object_id head, oid;
struct commit_list *j;
- struct child_process cp_update = CHILD_PROCESS_INIT,
- cp_read = CHILD_PROCESS_INIT,
- cp_write = CHILD_PROCESS_INIT;
-
- cp_update.git_cmd = 1;
- argv_array_pushl(&cp_update.args, "update-index", "-q", "--refresh", NULL);
- run_command(&cp_update);
-
- cp_read.git_cmd = 1;
- argv_array_pushl(&cp_read.args, "read-tree", "-u", "-m", "--aggressive", NULL);
-
- for (j = bases; j && j->item; j = j->next)
- argv_array_push(&cp_read.args, oid_to_hex(&j->item->object.oid));
if (head_arg)
- argv_array_push(&cp_read.args, head_arg);
- if (remote && remote->item)
- argv_array_push(&cp_read.args, oid_to_hex(&remote->item->object.oid));
+ get_oid(head_arg, &head);
- if (run_command(&cp_read))
- return 2;
+ repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
+ refresh_index(the_repository->index, 0, NULL, NULL, NULL);
+
+ memset(&opts, 0, sizeof(opts));
+ opts.head_idx = 1;
+ opts.src_index = the_repository->index;
+ opts.dst_index = the_repository->index;
+ opts.update = 1;
+ opts.merge = 1;
+ opts.aggressive = 1;
+
+ for (j = bases; j; j = j->next) {
+ if (add_tree(&j->item->object.oid, t + (i++)))
+ goto out;
+ }
+
+ if (head_arg && add_tree(&head, t + (i++)))
+ goto out;
+ if (remote && add_tree(&remote->item->object.oid, t + (i++)))
+ goto out;
+
+ if (i == 1)
+ opts.fn = oneway_merge;
+ else if (i == 2) {
+ opts.fn = twoway_merge;
+ opts.initial_checkout = is_index_unborn(the_repository->index);
+ } else if (i >= 3) {
+ opts.fn = threeway_merge;
+ opts.head_idx = i - 1;
+ }
+
+ if (unpack_trees(i, t, &opts))
+ goto out;
puts("Trying simple merge.");
+ write_locked_index(the_repository->index, &lock, COMMIT_LOCK);
- cp_write.git_cmd = 1;
- cp_write.no_stdout = 1;
- cp_write.no_stderr = 1;
- argv_array_push(&cp_write.args, "write-tree");
- if (run_command(&cp_write)) {
- struct child_process cp_merge = CHILD_PROCESS_INIT;
+ if (write_index_as_tree(&oid, the_repository->index,
+ the_repository->index_file, 0, NULL)) {
+ int ret;
- puts("Simple merge failed, trying Automatic merge.");
+ repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
+ ret = merge_all(the_repository->index, 0, 0,
+ merge_one_file_cb, the_repository);
- cp_merge.git_cmd = 1;
- argv_array_pushl(&cp_merge.args, "merge-index", "-o",
- "git-merge-one-file", "-a", NULL);
- if (run_command(&cp_merge))
- return 1;
+ write_locked_index(the_repository->index, &lock, COMMIT_LOCK);
+ return !!ret;
}
return 0;
+
+ out:
+ rollback_lock_file(&lock);
+ return 2;
}
static const char builtin_merge_resolve_usage[] =
@@ -73,6 +110,10 @@ int cmd_merge_resolve(int argc, const char **argv, const char *prefix)
if (argc < 5)
usage(builtin_merge_resolve_usage);
+ setup_work_tree();
+ if (repo_read_index(the_repository) < 0)
+ die("invalid index");
+
/* The first parameters up to -- are merge bases; the rest are
* heads. */
for (i = 1; i < argc; i++) {
diff --git a/merge-strategies.c b/merge-strategies.c
index f4c0b4acd6..39bfa1af7b 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -191,6 +191,17 @@ int merge_strategies_one_file(struct repository *r,
return 0;
}
+int merge_one_file_cb(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data)
+{
+ return merge_strategies_one_file((struct repository *)data,
+ orig_blob, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+}
+
int merge_program_cb(const struct object_id *orig_blob,
const struct object_id *our_blob,
const struct object_id *their_blob, const char *path,
diff --git a/merge-strategies.h b/merge-strategies.h
index cf78d7eaf4..40e175ca39 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -16,6 +16,12 @@ typedef int (*merge_cb)(const struct object_id *orig_blob,
unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
void *data);
+int merge_one_file_cb(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
int merge_program_cb(const struct object_id *orig_blob,
const struct object_id *our_blob,
const struct object_id *their_blob, const char *path,
--
2.27.0.139.gc9c318d6bf
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [RFC PATCH v1 09/17] merge-resolve: libify merge_resolve()
2020-06-25 12:19 [RFC PATCH v1 00/17] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (7 preceding siblings ...)
2020-06-25 12:19 ` [RFC PATCH v1 08/17] merge-resolve: remove calls to external processes Alban Gruin
@ 2020-06-25 12:19 ` Alban Gruin
2020-06-25 12:19 ` [RFC PATCH v1 10/17] merge-recursive: move better_branch_name() to merge.c Alban Gruin
` (8 subsequent siblings)
17 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-06-25 12:19 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Alban Gruin
This moves merge_resolve() (and its helper functions) to
merge-strategies.c. This will enable `git merge' and the sequencer to
directly call it instead of forking.
Here too, this is not a faithful copy-and-paste; the new
merge_resolve() (renamed merge_strategies_resolve()) takes a pointer to
the repository, instead of using `the_repository'.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Notes:
This patch is best viewed with `--color-moved'.
builtin/merge-resolve.c | 86 +----------------------------------------
merge-strategies.c | 85 ++++++++++++++++++++++++++++++++++++++++
merge-strategies.h | 5 +++
3 files changed, 91 insertions(+), 85 deletions(-)
diff --git a/builtin/merge-resolve.c b/builtin/merge-resolve.c
index 2c364fcdb0..59f734473b 100644
--- a/builtin/merge-resolve.c
+++ b/builtin/merge-resolve.c
@@ -10,92 +10,8 @@
*/
#include "cache.h"
-#include "cache-tree.h"
#include "builtin.h"
-#include "lockfile.h"
#include "merge-strategies.h"
-#include "unpack-trees.h"
-
-static int add_tree(const struct object_id *oid, struct tree_desc *t)
-{
- struct tree *tree;
-
- tree = parse_tree_indirect(oid);
- if (parse_tree(tree))
- return -1;
-
- init_tree_desc(t, tree->buffer, tree->size);
- return 0;
-}
-
-static int merge_resolve(struct commit_list *bases, const char *head_arg,
- struct commit_list *remote)
-{
- int i = 0;
- struct lock_file lock = LOCK_INIT;
- struct tree_desc t[MAX_UNPACK_TREES];
- struct unpack_trees_options opts;
- struct object_id head, oid;
- struct commit_list *j;
-
- if (head_arg)
- get_oid(head_arg, &head);
-
- repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
- refresh_index(the_repository->index, 0, NULL, NULL, NULL);
-
- memset(&opts, 0, sizeof(opts));
- opts.head_idx = 1;
- opts.src_index = the_repository->index;
- opts.dst_index = the_repository->index;
- opts.update = 1;
- opts.merge = 1;
- opts.aggressive = 1;
-
- for (j = bases; j; j = j->next) {
- if (add_tree(&j->item->object.oid, t + (i++)))
- goto out;
- }
-
- if (head_arg && add_tree(&head, t + (i++)))
- goto out;
- if (remote && add_tree(&remote->item->object.oid, t + (i++)))
- goto out;
-
- if (i == 1)
- opts.fn = oneway_merge;
- else if (i == 2) {
- opts.fn = twoway_merge;
- opts.initial_checkout = is_index_unborn(the_repository->index);
- } else if (i >= 3) {
- opts.fn = threeway_merge;
- opts.head_idx = i - 1;
- }
-
- if (unpack_trees(i, t, &opts))
- goto out;
-
- puts("Trying simple merge.");
- write_locked_index(the_repository->index, &lock, COMMIT_LOCK);
-
- if (write_index_as_tree(&oid, the_repository->index,
- the_repository->index_file, 0, NULL)) {
- int ret;
-
- repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
- ret = merge_all(the_repository->index, 0, 0,
- merge_one_file_cb, the_repository);
-
- write_locked_index(the_repository->index, &lock, COMMIT_LOCK);
- return !!ret;
- }
-
- return 0;
-
- out:
- rollback_lock_file(&lock);
- return 2;
-}
static const char builtin_merge_resolve_usage[] =
"git merge-resolve <bases>... -- <head> <remote>";
@@ -149,5 +65,5 @@ int cmd_merge_resolve(int argc, const char **argv, const char *prefix)
if (is_baseless)
return 2;
- return merge_resolve(bases, head, remote);
+ return merge_strategies_resolve(the_repository, bases, head, remote);
}
diff --git a/merge-strategies.c b/merge-strategies.c
index 39bfa1af7b..a12c575590 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -1,7 +1,10 @@
#include "cache.h"
+#include "cache-tree.h"
#include "dir.h"
+#include "lockfile.h"
#include "merge-strategies.h"
#include "run-command.h"
+#include "unpack-trees.h"
#include "xdiff-interface.h"
static int add_to_index_cacheinfo(struct index_state *istate,
@@ -299,3 +302,85 @@ int merge_all(struct index_state *istate, int oneshot, int quiet,
return err;
}
+
+static int add_tree(const struct object_id *oid, struct tree_desc *t)
+{
+ struct tree *tree;
+
+ tree = parse_tree_indirect(oid);
+ if (parse_tree(tree))
+ return -1;
+
+ init_tree_desc(t, tree->buffer, tree->size);
+ return 0;
+}
+
+int merge_strategies_resolve(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remote)
+{
+ int i = 0;
+ struct lock_file lock = LOCK_INIT;
+ struct tree_desc t[MAX_UNPACK_TREES];
+ struct unpack_trees_options opts;
+ struct object_id head, oid;
+ struct commit_list *j;
+
+ if (head_arg)
+ get_oid(head_arg, &head);
+
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+ refresh_index(r->index, 0, NULL, NULL, NULL);
+
+ memset(&opts, 0, sizeof(opts));
+ opts.head_idx = 1;
+ opts.src_index = r->index;
+ opts.dst_index = r->index;
+ opts.update = 1;
+ opts.merge = 1;
+ opts.aggressive = 1;
+
+ for (j = bases; j && j->item; j = j->next) {
+ if (add_tree(&j->item->object.oid, t + (i++)))
+ goto out;
+ }
+
+ if (head_arg && add_tree(&head, t + (i++)))
+ goto out;
+ if (remote && add_tree(&remote->item->object.oid, t + (i++)))
+ goto out;
+
+ if (i == 1)
+ opts.fn = oneway_merge;
+ else if (i == 2) {
+ opts.fn = twoway_merge;
+ opts.initial_checkout = is_index_unborn(r->index);
+ } else if (i >= 3) {
+ opts.fn = threeway_merge;
+ opts.head_idx = i - 1;
+ }
+
+ if (unpack_trees(i, t, &opts))
+ goto out;
+
+ puts("Trying simple merge.");
+ write_locked_index(r->index, &lock, COMMIT_LOCK);
+
+ if (write_index_as_tree(&oid, r->index, r->index_file,
+ WRITE_TREE_SILENT, NULL)) {
+ int ret;
+
+ puts("Simple merge failed, trying Automatic merge.");
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+ ret = merge_all(r->index, 0, 0, merge_one_file_cb, r);
+
+ write_locked_index(r->index, &lock, COMMIT_LOCK);
+ return !!ret;
+ }
+
+ return 0;
+
+ out:
+ rollback_lock_file(&lock);
+ return 2;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
index 40e175ca39..778f8ce9d6 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -1,6 +1,7 @@
#ifndef MERGE_STRATEGIES_H
#define MERGE_STRATEGIES_H
+#include "commit.h"
#include "object.h"
int merge_strategies_one_file(struct repository *r,
@@ -33,4 +34,8 @@ int merge_one_path(struct index_state *istate, int oneshot, int quiet,
int merge_all(struct index_state *istate, int oneshot, int quiet,
merge_cb cb, void *data);
+int merge_strategies_resolve(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remote);
+
#endif /* MERGE_STRATEGIES_H */
--
2.27.0.139.gc9c318d6bf
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [RFC PATCH v1 10/17] merge-recursive: move better_branch_name() to merge.c
2020-06-25 12:19 [RFC PATCH v1 00/17] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (8 preceding siblings ...)
2020-06-25 12:19 ` [RFC PATCH v1 09/17] merge-resolve: libify merge_resolve() Alban Gruin
@ 2020-06-25 12:19 ` Alban Gruin
2020-06-25 12:19 ` [RFC PATCH v1 11/17] merge-octopus: rewrite in C Alban Gruin
` (7 subsequent siblings)
17 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-06-25 12:19 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Alban Gruin
get_better_branch_name() will be used by rebase-octopus once it is
rewritten in C, so instead of duplicating it, this moves this function
preventively inside an appropriate file in libgit.a. This function is
also renamed to reflect its usage by merge strategies.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Notes:
This patch is best viewed with `--color-moved'.
builtin/merge-recursive.c | 16 ++--------------
cache.h | 2 +-
merge.c | 12 ++++++++++++
3 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/builtin/merge-recursive.c b/builtin/merge-recursive.c
index a4bfd8fc51..972243b5e9 100644
--- a/builtin/merge-recursive.c
+++ b/builtin/merge-recursive.c
@@ -8,18 +8,6 @@
static const char builtin_merge_recursive_usage[] =
"git %s <base>... -- <head> <remote> ...";
-static char *better_branch_name(const char *branch)
-{
- static char githead_env[8 + GIT_MAX_HEXSZ + 1];
- char *name;
-
- if (strlen(branch) != the_hash_algo->hexsz)
- return xstrdup(branch);
- xsnprintf(githead_env, sizeof(githead_env), "GITHEAD_%s", branch);
- name = getenv(githead_env);
- return xstrdup(name ? name : branch);
-}
-
int cmd_merge_recursive(int argc, const char **argv, const char *prefix)
{
const struct object_id *bases[21];
@@ -75,8 +63,8 @@ int cmd_merge_recursive(int argc, const char **argv, const char *prefix)
if (get_oid(o.branch2, &h2))
die(_("could not resolve ref '%s'"), o.branch2);
- o.branch1 = better1 = better_branch_name(o.branch1);
- o.branch2 = better2 = better_branch_name(o.branch2);
+ o.branch1 = better1 = merge_get_better_branch_name(o.branch1);
+ o.branch2 = better2 = merge_get_better_branch_name(o.branch2);
if (o.verbosity >= 3)
printf(_("Merging %s with %s\n"), o.branch1, o.branch2);
diff --git a/cache.h b/cache.h
index 0f0485ecfe..bbbd8e352d 100644
--- a/cache.h
+++ b/cache.h
@@ -1915,7 +1915,7 @@ int checkout_fast_forward(struct repository *r,
const struct object_id *from,
const struct object_id *to,
int overwrite_ignore);
-
+char *merge_get_better_branch_name(const char *branch);
int sane_execvp(const char *file, char *const argv[]);
diff --git a/merge.c b/merge.c
index aa36de2f64..5f3d05268f 100644
--- a/merge.c
+++ b/merge.c
@@ -108,3 +108,15 @@ int checkout_fast_forward(struct repository *r,
return error(_("unable to write new index file"));
return 0;
}
+
+char *merge_get_better_branch_name(const char *branch)
+{
+ static char githead_env[8 + GIT_MAX_HEXSZ + 1];
+ char *name;
+
+ if (strlen(branch) != the_hash_algo->hexsz)
+ return xstrdup(branch);
+ xsnprintf(githead_env, sizeof(githead_env), "GITHEAD_%s", branch);
+ name = getenv(githead_env);
+ return xstrdup(name ? name : branch);
+}
--
2.27.0.139.gc9c318d6bf
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [RFC PATCH v1 11/17] merge-octopus: rewrite in C
2020-06-25 12:19 [RFC PATCH v1 00/17] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (9 preceding siblings ...)
2020-06-25 12:19 ` [RFC PATCH v1 10/17] merge-recursive: move better_branch_name() to merge.c Alban Gruin
@ 2020-06-25 12:19 ` Alban Gruin
2020-06-25 12:19 ` [RFC PATCH v1 12/17] merge-octopus: remove calls to external processes Alban Gruin
` (6 subsequent siblings)
17 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-06-25 12:19 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Alban Gruin
This rewrites `git merge-octopus' from shell to C. As for the two last
conversions, this port keeps using external processes for operations on
the index, or to call `git merge-one-file'. This will be addressed in
the next two commits.
Here to, merge_octopus() takes two commit lists and a string to reduce
frictions when try_merge_strategies() will be modified to call it
directly.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Makefile | 2 +-
builtin.h | 1 +
builtin/merge-octopus.c | 241 ++++++++++++++++++++++++++++++++++++++++
git-merge-octopus.sh | 112 -------------------
git.c | 1 +
5 files changed, 244 insertions(+), 113 deletions(-)
create mode 100644 builtin/merge-octopus.c
delete mode 100755 git-merge-octopus.sh
diff --git a/Makefile b/Makefile
index ccea651ac8..8f45a3ec03 100644
--- a/Makefile
+++ b/Makefile
@@ -595,7 +595,6 @@ unexport CDPATH
SCRIPT_SH += git-bisect.sh
SCRIPT_SH += git-difftool--helper.sh
SCRIPT_SH += git-filter-branch.sh
-SCRIPT_SH += git-merge-octopus.sh
SCRIPT_SH += git-mergetool.sh
SCRIPT_SH += git-quiltimport.sh
SCRIPT_SH += git-request-pull.sh
@@ -1088,6 +1087,7 @@ BUILTIN_OBJS += builtin/mailsplit.o
BUILTIN_OBJS += builtin/merge-base.o
BUILTIN_OBJS += builtin/merge-file.o
BUILTIN_OBJS += builtin/merge-index.o
+BUILTIN_OBJS += builtin/merge-octopus.o
BUILTIN_OBJS += builtin/merge-one-file.o
BUILTIN_OBJS += builtin/merge-ours.o
BUILTIN_OBJS += builtin/merge-recursive.o
diff --git a/builtin.h b/builtin.h
index 6ea207c9fd..5a587ab70c 100644
--- a/builtin.h
+++ b/builtin.h
@@ -170,6 +170,7 @@ int cmd_mailsplit(int argc, const char **argv, const char *prefix);
int cmd_merge(int argc, const char **argv, const char *prefix);
int cmd_merge_base(int argc, const char **argv, const char *prefix);
int cmd_merge_index(int argc, const char **argv, const char *prefix);
+int cmd_merge_octopus(int argc, const char **argv, const char *prefix);
int cmd_merge_ours(int argc, const char **argv, const char *prefix);
int cmd_merge_file(int argc, const char **argv, const char *prefix);
int cmd_merge_one_file(int argc, const char **argv, const char *prefix);
diff --git a/builtin/merge-octopus.c b/builtin/merge-octopus.c
new file mode 100644
index 0000000000..6216beaa2b
--- /dev/null
+++ b/builtin/merge-octopus.c
@@ -0,0 +1,241 @@
+/*
+ * Builtin "git merge-octopus"
+ *
+ * Copyright (c) 2020 Alban Gruin
+ *
+ * Based on git-merge-octopus.sh, written by Junio C Hamano.
+ *
+ * Resolve two or more trees.
+ */
+
+#include "cache.h"
+#include "builtin.h"
+#include "commit-reach.h"
+#include "lockfile.h"
+#include "run-command.h"
+#include "unpack-trees.h"
+
+static int write_tree(struct tree **reference_tree)
+{
+ struct child_process cp = CHILD_PROCESS_INIT;
+ struct strbuf read_tree = STRBUF_INIT, err = STRBUF_INIT;
+ struct object_id oid;
+ int ret;
+
+ cp.git_cmd = 1;
+ argv_array_push(&cp.args, "write-tree");
+ ret = pipe_command(&cp, NULL, 0, &read_tree, 0, &err, 0);
+ if (err.len > 0)
+ fputs(err.buf, stderr);
+
+ strbuf_trim_trailing_newline(&read_tree);
+ get_oid(read_tree.buf, &oid);
+
+ *reference_tree = lookup_tree(the_repository, &oid);
+
+ strbuf_release(&read_tree);
+ strbuf_release(&err);
+ child_process_clear(&cp);
+
+ return ret;
+}
+
+static int merge_octopus(struct commit_list *bases, const char *head_arg,
+ struct commit_list *remotes)
+{
+ int non_ff_merge = 0, ret = 0, references = 1;
+ struct commit **reference_commit;
+ struct tree *reference_tree;
+ struct commit_list *j;
+ struct object_id head;
+
+ get_oid(head_arg, &head);
+ reference_commit = xcalloc(commit_list_count(remotes) + 1, sizeof(struct commit *));
+ reference_commit[0] = lookup_commit_reference(the_repository, &head);
+ reference_tree = get_commit_tree(reference_commit[0]);
+
+ for (j = remotes; j; j = j->next) {
+ struct commit *c = j->item;
+ struct object_id *oid = &c->object.oid;
+ struct commit_list *common, *k;
+ char *branch_name;
+ int can_ff = 1;
+
+ if (ret) {
+ puts(_("Automated merge did not work."));
+ puts(_("Should not be doing an octopus."));
+
+ ret = 2;
+ goto out;
+ }
+
+ branch_name = merge_get_better_branch_name(oid_to_hex(oid));
+ common = get_merge_bases_many(c, references, reference_commit);
+
+ if (!common)
+ die(_("Unable to find common commit with %s"), branch_name);
+
+ for (k = common; k && !oideq(&k->item->object.oid, oid); k = k->next);
+
+ if (k) {
+ printf(_("Already up to date with %s\n"), branch_name);
+ free(branch_name);
+ free_commit_list(common);
+ continue;
+ }
+
+ if (!non_ff_merge) {
+ int i;
+
+ for (i = 0, k = common; k && i < references && can_ff; k = k->next, i++) {
+ can_ff = oideq(&k->item->object.oid,
+ &reference_commit[i]->object.oid);
+ }
+ }
+
+ if (!non_ff_merge && can_ff) {
+ struct child_process cp = CHILD_PROCESS_INIT;
+
+ printf(_("Fast-forwarding to: %s\n"), branch_name);
+
+ cp.git_cmd = 1;
+ argv_array_pushl(&cp.args, "read-tree", "-u", "-m", NULL);
+ argv_array_push(&cp.args, oid_to_hex(&head));
+ argv_array_push(&cp.args, oid_to_hex(oid));
+
+ ret = run_command(&cp);
+ if (ret) {
+ free(branch_name);
+ free_commit_list(common);
+ goto out;
+ }
+
+ child_process_clear(&cp);
+ references = 0;
+ write_tree(&reference_tree);
+ } else {
+ struct commit_list *l;
+ struct tree *next = NULL;
+ struct child_process cp = CHILD_PROCESS_INIT;
+
+ non_ff_merge = 1;
+ printf(_("Trying simple merge with %s\n"), branch_name);
+
+ cp.git_cmd = 1;
+ argv_array_pushl(&cp.args, "read-tree", "-u", "-m", "--aggressive", NULL);
+
+ for (l = common; l; l = l->next)
+ argv_array_push(&cp.args, oid_to_hex(&l->item->object.oid));
+
+ argv_array_push(&cp.args, oid_to_hex(&reference_tree->object.oid));
+ argv_array_push(&cp.args, oid_to_hex(oid));
+
+ if (run_command(&cp)) {
+ ret = 2;
+
+ free(branch_name);
+ free_commit_list(common);
+
+ goto out;
+ }
+
+ child_process_clear(&cp);
+
+ if (write_tree(&next)) {
+ struct child_process cp = CHILD_PROCESS_INIT;
+ puts(_("Simple merge did not work, trying automatic merge."));
+
+ cp.git_cmd = 1;
+ argv_array_pushl(&cp.args, "merge-index", "-o",
+ "git-merge-one-file", "-a", NULL);
+ if (run_command(&cp))
+ ret = 1;
+
+ child_process_clear(&cp);
+ write_tree(&next);
+ }
+
+ reference_tree = next;
+ }
+
+ reference_commit[references++] = c;
+
+ free(branch_name);
+ free_commit_list(common);
+ }
+
+out:
+ free(reference_commit);
+ return ret;
+}
+
+static const char builtin_merge_octopus_usage[] =
+ "git merge-octopus [<bases>...] -- <head> <remote1> <remote2> [<remotes>...]";
+
+int cmd_merge_octopus(int argc, const char **argv, const char *prefix)
+{
+ int i, sep_seen = 0;
+ struct commit_list *bases = NULL, *remotes = NULL;
+ struct commit_list **next_base = &bases, **next_remote = &remotes;
+ const char *head_arg = NULL;
+ struct child_process cp = CHILD_PROCESS_INIT;
+ struct strbuf files = STRBUF_INIT;
+
+ if (argc < 5)
+ usage(builtin_merge_octopus_usage);
+
+ /* The first parameters up to -- are merge bases; the rest are
+ * heads. */
+ for (i = 1; i < argc; i++) {
+ if (strcmp(argv[i], "--") == 0)
+ sep_seen = 1;
+ else if (strcmp(argv[i], "-h") == 0)
+ usage(builtin_merge_octopus_usage);
+ else if (sep_seen && !head_arg)
+ head_arg = argv[i];
+ else {
+ struct object_id oid;
+
+ get_oid(argv[i], &oid);
+
+ if (!oideq(&oid, the_hash_algo->empty_tree)) {
+ struct commit *commit;
+ commit = lookup_commit_or_die(&oid, argv[i]);
+
+ if (sep_seen)
+ next_remote = commit_list_append(commit, next_remote);
+ else
+ next_base = commit_list_append(commit, next_base);
+ }
+ }
+ }
+
+ /* Reject if this is not an octopus -- resolve should be used
+ * instead. */
+ if (commit_list_count(remotes) < 2)
+ return 2;
+
+ cp.git_cmd = 1;
+ argv_array_pushl(&cp.args, "diff-index", "--cached",
+ "--name-only", "HEAD", "--", NULL);
+ pipe_command(&cp, NULL, 0, &files, 0, NULL, 0);
+ child_process_clear(&cp);
+
+ if (files.len > 0) {
+ struct strbuf **s, **b;
+
+ s = strbuf_split(&files, '\n');
+
+ fprintf(stderr, _("Error: Your local changes to the following "
+ "files would be overwritten by merge\n"));
+
+ for (b = s; *b; b++)
+ fprintf(stderr, " %.*s", (int)(*b)->len, (*b)->buf);
+
+ strbuf_list_free(s);
+ strbuf_release(&files);
+ return 2;
+ }
+
+ return merge_octopus(bases, head_arg, remotes);
+}
diff --git a/git-merge-octopus.sh b/git-merge-octopus.sh
deleted file mode 100755
index 7d19d37951..0000000000
--- a/git-merge-octopus.sh
+++ /dev/null
@@ -1,112 +0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2005 Junio C Hamano
-#
-# Resolve two or more trees.
-#
-
-. git-sh-setup
-
-LF='
-'
-
-# The first parameters up to -- are merge bases; the rest are heads.
-bases= head= remotes= sep_seen=
-for arg
-do
- case ",$sep_seen,$head,$arg," in
- *,--,)
- sep_seen=yes
- ;;
- ,yes,,*)
- head=$arg
- ;;
- ,yes,*)
- remotes="$remotes$arg "
- ;;
- *)
- bases="$bases$arg "
- ;;
- esac
-done
-
-# Reject if this is not an octopus -- resolve should be used instead.
-case "$remotes" in
-?*' '?*)
- ;;
-*)
- exit 2 ;;
-esac
-
-# MRC is the current "merge reference commit"
-# MRT is the current "merge result tree"
-
-if ! git diff-index --quiet --cached HEAD --
-then
- gettextln "Error: Your local changes to the following files would be overwritten by merge"
- git diff-index --cached --name-only HEAD -- | sed -e 's/^/ /'
- exit 2
-fi
-MRC=$(git rev-parse --verify -q $head)
-MRT=$(git write-tree)
-NON_FF_MERGE=0
-OCTOPUS_FAILURE=0
-for SHA1 in $remotes
-do
- case "$OCTOPUS_FAILURE" in
- 1)
- # We allow only last one to have a hand-resolvable
- # conflicts. Last round failed and we still had
- # a head to merge.
- gettextln "Automated merge did not work."
- gettextln "Should not be doing an octopus."
- exit 2
- esac
-
- eval pretty_name=\${GITHEAD_$SHA1:-$SHA1}
- if test "$SHA1" = "$pretty_name"
- then
- SHA1_UP="$(echo "$SHA1" | tr a-z A-Z)"
- eval pretty_name=\${GITHEAD_$SHA1_UP:-$pretty_name}
- fi
- common=$(git merge-base --all $SHA1 $MRC) ||
- die "$(eval_gettext "Unable to find common commit with \$pretty_name")"
-
- case "$LF$common$LF" in
- *"$LF$SHA1$LF"*)
- eval_gettextln "Already up to date with \$pretty_name"
- continue
- ;;
- esac
-
- if test "$common,$NON_FF_MERGE" = "$MRC,0"
- then
- # The first head being merged was a fast-forward.
- # Advance MRC to the head being merged, and use that
- # tree as the intermediate result of the merge.
- # We still need to count this as part of the parent set.
-
- eval_gettextln "Fast-forwarding to: \$pretty_name"
- git read-tree -u -m $head $SHA1 || exit
- MRC=$SHA1 MRT=$(git write-tree)
- continue
- fi
-
- NON_FF_MERGE=1
-
- eval_gettextln "Trying simple merge with \$pretty_name"
- git read-tree -u -m --aggressive $common $MRT $SHA1 || exit 2
- next=$(git write-tree 2>/dev/null)
- if test $? -ne 0
- then
- gettextln "Simple merge did not work, trying automatic merge."
- git merge-index -o git-merge-one-file -a ||
- OCTOPUS_FAILURE=1
- next=$(git write-tree 2>/dev/null)
- fi
-
- MRC="$MRC $SHA1"
- MRT=$next
-done
-
-exit "$OCTOPUS_FAILURE"
diff --git a/git.c b/git.c
index 2e92019493..28634cf61f 100644
--- a/git.c
+++ b/git.c
@@ -531,6 +531,7 @@ static struct cmd_struct commands[] = {
{ "merge-base", cmd_merge_base, RUN_SETUP },
{ "merge-file", cmd_merge_file, RUN_SETUP_GENTLY },
{ "merge-index", cmd_merge_index, RUN_SETUP | NO_PARSEOPT },
+ { "merge-octopus", cmd_merge_octopus, RUN_SETUP | NO_PARSEOPT },
{ "merge-ours", cmd_merge_ours, RUN_SETUP | NO_PARSEOPT },
{ "merge-one-file", cmd_merge_one_file, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
--
2.27.0.139.gc9c318d6bf
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [RFC PATCH v1 12/17] merge-octopus: remove calls to external processes
2020-06-25 12:19 [RFC PATCH v1 00/17] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (10 preceding siblings ...)
2020-06-25 12:19 ` [RFC PATCH v1 11/17] merge-octopus: rewrite in C Alban Gruin
@ 2020-06-25 12:19 ` Alban Gruin
2020-06-25 12:19 ` [RFC PATCH v1 13/17] merge-octopus: libify merge_octopus() Alban Gruin
` (5 subsequent siblings)
17 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-06-25 12:19 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Alban Gruin
This removes calls to external processes to avoid reading and writing
the index over and over again.
- Calls to `read-tree -u -m (--aggressive)?' are replaced by calls to
unpack_trees().
- The call to `write-tree' is replaced by a call to
write_index_as_tree().
- The call to `diff-index ...' is replaced by a call to
repo_index_has_changes(), and is moved from cmd_merge_octopus() to
merge_octopus().
- The call to `merge-index', needed to invoke `git merge-one-file', is
replaced by a call to merge_all().
The index is read in cmd_merge_octopus(), and is wrote back by
merge_octopus().
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge-octopus.c | 155 ++++++++++++++++++++++------------------
1 file changed, 86 insertions(+), 69 deletions(-)
diff --git a/builtin/merge-octopus.c b/builtin/merge-octopus.c
index 6216beaa2b..14310a4eb1 100644
--- a/builtin/merge-octopus.c
+++ b/builtin/merge-octopus.c
@@ -9,33 +9,70 @@
*/
#include "cache.h"
+#include "cache-tree.h"
#include "builtin.h"
#include "commit-reach.h"
#include "lockfile.h"
-#include "run-command.h"
+#include "merge-strategies.h"
#include "unpack-trees.h"
+static int fast_forward(const struct object_id *oids, int nr, int aggressive)
+{
+ int i;
+ struct tree_desc t[MAX_UNPACK_TREES];
+ struct unpack_trees_options opts;
+ struct lock_file lock = LOCK_INIT;
+
+ repo_read_index_preload(the_repository, NULL, 0);
+ if (refresh_index(the_repository->index, REFRESH_QUIET, NULL, NULL, NULL))
+ return -1;
+
+ repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
+
+ memset(&opts, 0, sizeof(opts));
+ opts.head_idx = 1;
+ opts.src_index = the_repository->index;
+ opts.dst_index = the_repository->index;
+ opts.merge = 1;
+ opts.update = 1;
+ opts.aggressive = aggressive;
+
+ for (i = 0; i < nr; i++) {
+ struct tree *tree;
+ tree = parse_tree_indirect(oids + i);
+ if (parse_tree(tree))
+ return -1;
+ init_tree_desc(t + i, tree->buffer, tree->size);
+ }
+
+ if (nr == 1)
+ opts.fn = oneway_merge;
+ else if (nr == 2) {
+ opts.fn = twoway_merge;
+ opts.initial_checkout = is_index_unborn(the_repository->index);
+ } else if (nr >= 3) {
+ opts.fn = threeway_merge;
+ opts.head_idx = nr - 1;
+ }
+
+ if (unpack_trees(nr, t, &opts))
+ return -1;
+
+ if (write_locked_index(the_repository->index, &lock, COMMIT_LOCK))
+ return error(_("unable to write new index file"));
+
+ return 0;
+}
+
static int write_tree(struct tree **reference_tree)
{
- struct child_process cp = CHILD_PROCESS_INIT;
- struct strbuf read_tree = STRBUF_INIT, err = STRBUF_INIT;
struct object_id oid;
int ret;
- cp.git_cmd = 1;
- argv_array_push(&cp.args, "write-tree");
- ret = pipe_command(&cp, NULL, 0, &read_tree, 0, &err, 0);
- if (err.len > 0)
- fputs(err.buf, stderr);
-
- strbuf_trim_trailing_newline(&read_tree);
- get_oid(read_tree.buf, &oid);
-
- *reference_tree = lookup_tree(the_repository, &oid);
-
- strbuf_release(&read_tree);
- strbuf_release(&err);
- child_process_clear(&cp);
+ ret = write_index_as_tree(&oid, the_repository->index,
+ the_repository->index_file, 0, NULL);
+ if (!ret)
+ *reference_tree = lookup_tree(the_repository, &oid);
return ret;
}
@@ -48,12 +85,23 @@ static int merge_octopus(struct commit_list *bases, const char *head_arg,
struct tree *reference_tree;
struct commit_list *j;
struct object_id head;
+ struct strbuf sb = STRBUF_INIT;
get_oid(head_arg, &head);
+
reference_commit = xcalloc(commit_list_count(remotes) + 1, sizeof(struct commit *));
reference_commit[0] = lookup_commit_reference(the_repository, &head);
reference_tree = get_commit_tree(reference_commit[0]);
+ if (repo_index_has_changes(the_repository, reference_tree, &sb)) {
+ error(_("Your local changes to the following files "
+ "would be overwritten by merge:\n %s"),
+ sb.buf);
+ strbuf_release(&sb);
+ ret = 2;
+ goto out;
+ }
+
for (j = remotes; j; j = j->next) {
struct commit *c = j->item;
struct object_id *oid = &c->object.oid;
@@ -94,43 +142,36 @@ static int merge_octopus(struct commit_list *bases, const char *head_arg,
}
if (!non_ff_merge && can_ff) {
- struct child_process cp = CHILD_PROCESS_INIT;
-
+ struct object_id oids[2];
printf(_("Fast-forwarding to: %s\n"), branch_name);
- cp.git_cmd = 1;
- argv_array_pushl(&cp.args, "read-tree", "-u", "-m", NULL);
- argv_array_push(&cp.args, oid_to_hex(&head));
- argv_array_push(&cp.args, oid_to_hex(oid));
+ oidcpy(oids, &head);
+ oidcpy(oids + 1, oid);
- ret = run_command(&cp);
+ ret = fast_forward(oids, 2, 0);
if (ret) {
free(branch_name);
free_commit_list(common);
goto out;
}
- child_process_clear(&cp);
references = 0;
write_tree(&reference_tree);
} else {
- struct commit_list *l;
+ int i = 0;
struct tree *next = NULL;
- struct child_process cp = CHILD_PROCESS_INIT;
+ struct object_id oids[MAX_UNPACK_TREES];
non_ff_merge = 1;
printf(_("Trying simple merge with %s\n"), branch_name);
- cp.git_cmd = 1;
- argv_array_pushl(&cp.args, "read-tree", "-u", "-m", "--aggressive", NULL);
+ for (k = common; k; k = k->next)
+ oidcpy(oids + (i++), &k->item->object.oid);
- for (l = common; l; l = l->next)
- argv_array_push(&cp.args, oid_to_hex(&l->item->object.oid));
+ oidcpy(oids + (i++), &reference_tree->object.oid);
+ oidcpy(oids + (i++), oid);
- argv_array_push(&cp.args, oid_to_hex(&reference_tree->object.oid));
- argv_array_push(&cp.args, oid_to_hex(oid));
-
- if (run_command(&cp)) {
+ if (fast_forward(oids, i, 1)) {
ret = 2;
free(branch_name);
@@ -139,19 +180,15 @@ static int merge_octopus(struct commit_list *bases, const char *head_arg,
goto out;
}
- child_process_clear(&cp);
-
if (write_tree(&next)) {
- struct child_process cp = CHILD_PROCESS_INIT;
+ struct lock_file lock = LOCK_INIT;
+
puts(_("Simple merge did not work, trying automatic merge."));
+ repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
+ ret = !!merge_all(the_repository->index, 0, 0,
+ merge_one_file_cb, the_repository);
+ write_locked_index(the_repository->index, &lock, COMMIT_LOCK);
- cp.git_cmd = 1;
- argv_array_pushl(&cp.args, "merge-index", "-o",
- "git-merge-one-file", "-a", NULL);
- if (run_command(&cp))
- ret = 1;
-
- child_process_clear(&cp);
write_tree(&next);
}
@@ -178,12 +215,14 @@ int cmd_merge_octopus(int argc, const char **argv, const char *prefix)
struct commit_list *bases = NULL, *remotes = NULL;
struct commit_list **next_base = &bases, **next_remote = &remotes;
const char *head_arg = NULL;
- struct child_process cp = CHILD_PROCESS_INIT;
- struct strbuf files = STRBUF_INIT;
if (argc < 5)
usage(builtin_merge_octopus_usage);
+ setup_work_tree();
+ if (repo_read_index(the_repository) < 0)
+ die("corrupted cache");
+
/* The first parameters up to -- are merge bases; the rest are
* heads. */
for (i = 1; i < argc; i++) {
@@ -215,27 +254,5 @@ int cmd_merge_octopus(int argc, const char **argv, const char *prefix)
if (commit_list_count(remotes) < 2)
return 2;
- cp.git_cmd = 1;
- argv_array_pushl(&cp.args, "diff-index", "--cached",
- "--name-only", "HEAD", "--", NULL);
- pipe_command(&cp, NULL, 0, &files, 0, NULL, 0);
- child_process_clear(&cp);
-
- if (files.len > 0) {
- struct strbuf **s, **b;
-
- s = strbuf_split(&files, '\n');
-
- fprintf(stderr, _("Error: Your local changes to the following "
- "files would be overwritten by merge\n"));
-
- for (b = s; *b; b++)
- fprintf(stderr, " %.*s", (int)(*b)->len, (*b)->buf);
-
- strbuf_list_free(s);
- strbuf_release(&files);
- return 2;
- }
-
return merge_octopus(bases, head_arg, remotes);
}
--
2.27.0.139.gc9c318d6bf
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [RFC PATCH v1 13/17] merge-octopus: libify merge_octopus()
2020-06-25 12:19 [RFC PATCH v1 00/17] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (11 preceding siblings ...)
2020-06-25 12:19 ` [RFC PATCH v1 12/17] merge-octopus: remove calls to external processes Alban Gruin
@ 2020-06-25 12:19 ` Alban Gruin
2020-06-25 12:19 ` [RFC PATCH v1 14/17] merge: use the "resolve" strategy without forking Alban Gruin
` (4 subsequent siblings)
17 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-06-25 12:19 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Alban Gruin
This moves merge_octopus() (and its helper functions) to
merge-strategies.c. This will enable `git merge' and the sequencer to
directly call it instead of forking.
Once again, this is not a faithful copy-and-paste; the new
merge_octopus() (renamed merge_strategies_octopus()) takes a pointer to
the repository, instead of using `the_repository'.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Notes:
This patch is best viewed with `--color-moved'.
builtin/merge-octopus.c | 197 +---------------------------------------
merge-strategies.c | 191 ++++++++++++++++++++++++++++++++++++++
merge-strategies.h | 3 +
3 files changed, 196 insertions(+), 195 deletions(-)
diff --git a/builtin/merge-octopus.c b/builtin/merge-octopus.c
index 14310a4eb1..37bbdf11cc 100644
--- a/builtin/merge-octopus.c
+++ b/builtin/merge-octopus.c
@@ -9,202 +9,9 @@
*/
#include "cache.h"
-#include "cache-tree.h"
#include "builtin.h"
-#include "commit-reach.h"
-#include "lockfile.h"
+#include "commit.h"
#include "merge-strategies.h"
-#include "unpack-trees.h"
-
-static int fast_forward(const struct object_id *oids, int nr, int aggressive)
-{
- int i;
- struct tree_desc t[MAX_UNPACK_TREES];
- struct unpack_trees_options opts;
- struct lock_file lock = LOCK_INIT;
-
- repo_read_index_preload(the_repository, NULL, 0);
- if (refresh_index(the_repository->index, REFRESH_QUIET, NULL, NULL, NULL))
- return -1;
-
- repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
-
- memset(&opts, 0, sizeof(opts));
- opts.head_idx = 1;
- opts.src_index = the_repository->index;
- opts.dst_index = the_repository->index;
- opts.merge = 1;
- opts.update = 1;
- opts.aggressive = aggressive;
-
- for (i = 0; i < nr; i++) {
- struct tree *tree;
- tree = parse_tree_indirect(oids + i);
- if (parse_tree(tree))
- return -1;
- init_tree_desc(t + i, tree->buffer, tree->size);
- }
-
- if (nr == 1)
- opts.fn = oneway_merge;
- else if (nr == 2) {
- opts.fn = twoway_merge;
- opts.initial_checkout = is_index_unborn(the_repository->index);
- } else if (nr >= 3) {
- opts.fn = threeway_merge;
- opts.head_idx = nr - 1;
- }
-
- if (unpack_trees(nr, t, &opts))
- return -1;
-
- if (write_locked_index(the_repository->index, &lock, COMMIT_LOCK))
- return error(_("unable to write new index file"));
-
- return 0;
-}
-
-static int write_tree(struct tree **reference_tree)
-{
- struct object_id oid;
- int ret;
-
- ret = write_index_as_tree(&oid, the_repository->index,
- the_repository->index_file, 0, NULL);
- if (!ret)
- *reference_tree = lookup_tree(the_repository, &oid);
-
- return ret;
-}
-
-static int merge_octopus(struct commit_list *bases, const char *head_arg,
- struct commit_list *remotes)
-{
- int non_ff_merge = 0, ret = 0, references = 1;
- struct commit **reference_commit;
- struct tree *reference_tree;
- struct commit_list *j;
- struct object_id head;
- struct strbuf sb = STRBUF_INIT;
-
- get_oid(head_arg, &head);
-
- reference_commit = xcalloc(commit_list_count(remotes) + 1, sizeof(struct commit *));
- reference_commit[0] = lookup_commit_reference(the_repository, &head);
- reference_tree = get_commit_tree(reference_commit[0]);
-
- if (repo_index_has_changes(the_repository, reference_tree, &sb)) {
- error(_("Your local changes to the following files "
- "would be overwritten by merge:\n %s"),
- sb.buf);
- strbuf_release(&sb);
- ret = 2;
- goto out;
- }
-
- for (j = remotes; j; j = j->next) {
- struct commit *c = j->item;
- struct object_id *oid = &c->object.oid;
- struct commit_list *common, *k;
- char *branch_name;
- int can_ff = 1;
-
- if (ret) {
- puts(_("Automated merge did not work."));
- puts(_("Should not be doing an octopus."));
-
- ret = 2;
- goto out;
- }
-
- branch_name = merge_get_better_branch_name(oid_to_hex(oid));
- common = get_merge_bases_many(c, references, reference_commit);
-
- if (!common)
- die(_("Unable to find common commit with %s"), branch_name);
-
- for (k = common; k && !oideq(&k->item->object.oid, oid); k = k->next);
-
- if (k) {
- printf(_("Already up to date with %s\n"), branch_name);
- free(branch_name);
- free_commit_list(common);
- continue;
- }
-
- if (!non_ff_merge) {
- int i;
-
- for (i = 0, k = common; k && i < references && can_ff; k = k->next, i++) {
- can_ff = oideq(&k->item->object.oid,
- &reference_commit[i]->object.oid);
- }
- }
-
- if (!non_ff_merge && can_ff) {
- struct object_id oids[2];
- printf(_("Fast-forwarding to: %s\n"), branch_name);
-
- oidcpy(oids, &head);
- oidcpy(oids + 1, oid);
-
- ret = fast_forward(oids, 2, 0);
- if (ret) {
- free(branch_name);
- free_commit_list(common);
- goto out;
- }
-
- references = 0;
- write_tree(&reference_tree);
- } else {
- int i = 0;
- struct tree *next = NULL;
- struct object_id oids[MAX_UNPACK_TREES];
-
- non_ff_merge = 1;
- printf(_("Trying simple merge with %s\n"), branch_name);
-
- for (k = common; k; k = k->next)
- oidcpy(oids + (i++), &k->item->object.oid);
-
- oidcpy(oids + (i++), &reference_tree->object.oid);
- oidcpy(oids + (i++), oid);
-
- if (fast_forward(oids, i, 1)) {
- ret = 2;
-
- free(branch_name);
- free_commit_list(common);
-
- goto out;
- }
-
- if (write_tree(&next)) {
- struct lock_file lock = LOCK_INIT;
-
- puts(_("Simple merge did not work, trying automatic merge."));
- repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
- ret = !!merge_all(the_repository->index, 0, 0,
- merge_one_file_cb, the_repository);
- write_locked_index(the_repository->index, &lock, COMMIT_LOCK);
-
- write_tree(&next);
- }
-
- reference_tree = next;
- }
-
- reference_commit[references++] = c;
-
- free(branch_name);
- free_commit_list(common);
- }
-
-out:
- free(reference_commit);
- return ret;
-}
static const char builtin_merge_octopus_usage[] =
"git merge-octopus [<bases>...] -- <head> <remote1> <remote2> [<remotes>...]";
@@ -254,5 +61,5 @@ int cmd_merge_octopus(int argc, const char **argv, const char *prefix)
if (commit_list_count(remotes) < 2)
return 2;
- return merge_octopus(bases, head_arg, remotes);
+ return merge_strategies_octopus(the_repository, bases, head_arg, remotes);
}
diff --git a/merge-strategies.c b/merge-strategies.c
index a12c575590..8395c4c787 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -1,5 +1,6 @@
#include "cache.h"
#include "cache-tree.h"
+#include "commit-reach.h"
#include "dir.h"
#include "lockfile.h"
#include "merge-strategies.h"
@@ -384,3 +385,193 @@ int merge_strategies_resolve(struct repository *r,
rollback_lock_file(&lock);
return 2;
}
+
+static int fast_forward(struct repository *r, const struct object_id *oids,
+ int nr, int aggressive)
+{
+ int i;
+ struct tree_desc t[MAX_UNPACK_TREES];
+ struct unpack_trees_options opts;
+ struct lock_file lock = LOCK_INIT;
+
+ repo_read_index_preload(r, NULL, 0);
+ if (refresh_index(r->index, REFRESH_QUIET, NULL, NULL, NULL))
+ return -1;
+
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+
+ memset(&opts, 0, sizeof(opts));
+ opts.head_idx = 1;
+ opts.src_index = r->index;
+ opts.dst_index = r->index;
+ opts.merge = 1;
+ opts.update = 1;
+ opts.aggressive = aggressive;
+
+ for (i = 0; i < nr; i++) {
+ struct tree *tree;
+ tree = parse_tree_indirect(oids + i);
+ if (parse_tree(tree))
+ return -1;
+ init_tree_desc(t + i, tree->buffer, tree->size);
+ }
+
+ if (nr == 1)
+ opts.fn = oneway_merge;
+ else if (nr == 2) {
+ opts.fn = twoway_merge;
+ opts.initial_checkout = is_index_unborn(r->index);
+ } else if (nr >= 3) {
+ opts.fn = threeway_merge;
+ opts.head_idx = nr - 1;
+ }
+
+ if (unpack_trees(nr, t, &opts))
+ return -1;
+
+ if (write_locked_index(r->index, &lock, COMMIT_LOCK))
+ return error(_("unable to write new index file"));
+
+ return 0;
+}
+
+static int write_tree(struct repository *r, struct tree **reference_tree)
+{
+ struct object_id oid;
+ int ret;
+
+ ret = write_index_as_tree(&oid, r->index, r->index_file, 0, NULL);
+ if (!ret)
+ *reference_tree = lookup_tree(r, &oid);
+
+ return ret;
+}
+
+int merge_strategies_octopus(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remotes)
+{
+ int non_ff_merge = 0, ret = 0, references = 1;
+ struct commit **reference_commit;
+ struct tree *reference_tree;
+ struct commit_list *j;
+ struct object_id head;
+ struct strbuf sb = STRBUF_INIT;
+
+ get_oid(head_arg, &head);
+
+ reference_commit = xcalloc(commit_list_count(remotes) + 1, sizeof(struct commit *));
+ reference_commit[0] = lookup_commit_reference(r, &head);
+ reference_tree = repo_get_commit_tree(r, reference_commit[0]);
+
+ if (repo_index_has_changes(r, reference_tree, &sb)) {
+ error(_("Your local changes to the following files "
+ "would be overwritten by merge:\n %s"),
+ sb.buf);
+ strbuf_release(&sb);
+ ret = 2;
+ goto out;
+ }
+
+ for (j = remotes; j && j->item; j = j->next) {
+ struct commit *c = j->item;
+ struct object_id *oid = &c->object.oid;
+ struct commit_list *common, *k;
+ char *branch_name;
+ int can_ff = 1;
+
+ if (ret) {
+ puts(_("Automated merge did not work."));
+ puts(_("Should not be doing an octopus."));
+
+ ret = 2;
+ goto out;
+ }
+
+ branch_name = merge_get_better_branch_name(oid_to_hex(oid));
+ common = get_merge_bases_many(c, references, reference_commit);
+
+ if (!common)
+ die(_("Unable to find common commit with %s"), branch_name);
+
+ for (k = common; k && !oideq(&k->item->object.oid, oid); k = k->next);
+
+ if (k) {
+ printf(_("Already up to date with %s\n"), branch_name);
+ free(branch_name);
+ free_commit_list(common);
+ continue;
+ }
+
+ if (!non_ff_merge) {
+ int i;
+
+ for (i = 0, k = common; k && i < references && can_ff; k = k->next, i++) {
+ can_ff = oideq(&k->item->object.oid,
+ &reference_commit[i]->object.oid);
+ }
+ }
+
+ if (!non_ff_merge && can_ff) {
+ struct object_id oids[2];
+ printf(_("Fast-forwarding to: %s\n"), branch_name);
+
+ oidcpy(oids, &head);
+ oidcpy(oids + 1, oid);
+
+ ret = fast_forward(r, oids, 2, 0);
+ if (ret) {
+ free(branch_name);
+ free_commit_list(common);
+ goto out;
+ }
+
+ references = 0;
+ write_tree(r, &reference_tree);
+ } else {
+ int i = 0;
+ struct tree *next = NULL;
+ struct object_id oids[MAX_UNPACK_TREES];
+
+ non_ff_merge = 1;
+ printf(_("Trying simple merge with %s\n"), branch_name);
+
+ for (k = common; k; k = k->next)
+ oidcpy(oids + (i++), &k->item->object.oid);
+
+ oidcpy(oids + (i++), &reference_tree->object.oid);
+ oidcpy(oids + (i++), oid);
+
+ if (fast_forward(r, oids, i, 1)) {
+ ret = 2;
+
+ free(branch_name);
+ free_commit_list(common);
+
+ goto out;
+ }
+
+ if (write_tree(r, &next)) {
+ struct lock_file lock = LOCK_INIT;
+
+ puts(_("Simple merge did not work, trying automatic merge."));
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+ ret = !!merge_all(r->index, 0, 0, merge_one_file_cb, r);
+ write_locked_index(r->index, &lock, COMMIT_LOCK);
+
+ write_tree(r, &next);
+ }
+
+ reference_tree = next;
+ }
+
+ reference_commit[references++] = c;
+
+ free(branch_name);
+ free_commit_list(common);
+ }
+
+out:
+ free(reference_commit);
+ return ret;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
index 778f8ce9d6..938411a04e 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -37,5 +37,8 @@ int merge_all(struct index_state *istate, int oneshot, int quiet,
int merge_strategies_resolve(struct repository *r,
struct commit_list *bases, const char *head_arg,
struct commit_list *remote);
+int merge_strategies_octopus(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remote);
#endif /* MERGE_STRATEGIES_H */
--
2.27.0.139.gc9c318d6bf
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [RFC PATCH v1 14/17] merge: use the "resolve" strategy without forking
2020-06-25 12:19 [RFC PATCH v1 00/17] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (12 preceding siblings ...)
2020-06-25 12:19 ` [RFC PATCH v1 13/17] merge-octopus: libify merge_octopus() Alban Gruin
@ 2020-06-25 12:19 ` Alban Gruin
2020-06-25 12:19 ` [RFC PATCH v1 15/17] merge: use the "octopus" " Alban Gruin
` (3 subsequent siblings)
17 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-06-25 12:19 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Alban Gruin
This teaches `git merge' to invoke the "resolve" strategy with a
function call instead of forking.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge.c | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/builtin/merge.c b/builtin/merge.c
index 7da707bf55..d50b4ad6ad 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -41,6 +41,7 @@
#include "commit-reach.h"
#include "wt-status.h"
#include "commit-graph.h"
+#include "merge-strategies.h"
#define DEFAULT_TWOHEAD (1<<0)
#define DEFAULT_OCTOPUS (1<<1)
@@ -744,7 +745,10 @@ static int try_merge_strategy(const char *strategy, struct commit_list *common,
COMMIT_LOCK | SKIP_IF_UNCHANGED))
die(_("unable to write %s"), get_index_file());
return clean ? 0 : 1;
- } else {
+ } else if (!strcmp(strategy, "resolve"))
+ return merge_strategies_resolve(the_repository, common,
+ head_arg, remoteheads);
+ else {
return try_merge_command(the_repository,
strategy, xopts_nr, xopts,
common, head_arg, remoteheads);
--
2.27.0.139.gc9c318d6bf
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [RFC PATCH v1 15/17] merge: use the "octopus" strategy without forking
2020-06-25 12:19 [RFC PATCH v1 00/17] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (13 preceding siblings ...)
2020-06-25 12:19 ` [RFC PATCH v1 14/17] merge: use the "resolve" strategy without forking Alban Gruin
@ 2020-06-25 12:19 ` Alban Gruin
2020-06-25 12:19 ` [RFC PATCH v1 16/17] sequencer: use the "resolve" " Alban Gruin
` (2 subsequent siblings)
17 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-06-25 12:19 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Alban Gruin
This teaches `git merge' to invoke the "octopus" strategy with a
function call instead of forking.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/builtin/merge.c b/builtin/merge.c
index d50b4ad6ad..53f64ddb87 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -748,6 +748,9 @@ static int try_merge_strategy(const char *strategy, struct commit_list *common,
} else if (!strcmp(strategy, "resolve"))
return merge_strategies_resolve(the_repository, common,
head_arg, remoteheads);
+ else if (!strcmp(strategy, "octopus"))
+ return merge_strategies_octopus(the_repository, common,
+ head_arg, remoteheads);
else {
return try_merge_command(the_repository,
strategy, xopts_nr, xopts,
--
2.27.0.139.gc9c318d6bf
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [RFC PATCH v1 16/17] sequencer: use the "resolve" strategy without forking
2020-06-25 12:19 [RFC PATCH v1 00/17] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (14 preceding siblings ...)
2020-06-25 12:19 ` [RFC PATCH v1 15/17] merge: use the "octopus" " Alban Gruin
@ 2020-06-25 12:19 ` Alban Gruin
2020-06-25 16:11 ` Phillip Wood
2020-06-25 12:19 ` [RFC PATCH v1 17/17] sequencer: use the "octopus" merge " Alban Gruin
2020-09-01 10:56 ` [PATCH v2 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
17 siblings, 1 reply; 221+ messages in thread
From: Alban Gruin @ 2020-06-25 12:19 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Alban Gruin
This teaches the sequencer to invoke the "resolve" strategy with a
function call instead of forking.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
sequencer.c | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/sequencer.c b/sequencer.c
index fd7701c88a..ea8dc58108 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -33,6 +33,7 @@
#include "commit-reach.h"
#include "rebase-interactive.h"
#include "reset.h"
+#include "merge-strategies.h"
#define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
@@ -1922,9 +1923,15 @@ static int do_pick_commit(struct repository *r,
commit_list_insert(base, &common);
commit_list_insert(next, &remotes);
- res |= try_merge_command(r, opts->strategy,
- opts->xopts_nr, (const char **)opts->xopts,
- common, oid_to_hex(&head), remotes);
+
+ if (!strcmp(opts->strategy, "resolve")) {
+ repo_read_index(r);
+ res |= merge_strategies_resolve(r, common, oid_to_hex(&head), remotes);
+ } else
+ res |= try_merge_command(r, opts->strategy,
+ opts->xopts_nr, (const char **)opts->xopts,
+ common, oid_to_hex(&head), remotes);
+
free_commit_list(common);
free_commit_list(remotes);
}
--
2.27.0.139.gc9c318d6bf
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [RFC PATCH v1 17/17] sequencer: use the "octopus" merge strategy without forking
2020-06-25 12:19 [RFC PATCH v1 00/17] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (15 preceding siblings ...)
2020-06-25 12:19 ` [RFC PATCH v1 16/17] sequencer: use the "resolve" " Alban Gruin
@ 2020-06-25 12:19 ` Alban Gruin
2020-09-01 10:56 ` [PATCH v2 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
17 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-06-25 12:19 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Alban Gruin
This teaches the sequencer to invoke the "octopus" strategy with a
function call instead of forking.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
sequencer.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/sequencer.c b/sequencer.c
index ea8dc58108..f9fa995b4b 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -1927,6 +1927,9 @@ static int do_pick_commit(struct repository *r,
if (!strcmp(opts->strategy, "resolve")) {
repo_read_index(r);
res |= merge_strategies_resolve(r, common, oid_to_hex(&head), remotes);
+ } else if (!strcmp(opts->strategy, "octopus")) {
+ repo_read_index(r);
+ res |= merge_strategies_octopus(r, common, oid_to_hex(&head), remotes);
} else
res |= try_merge_command(r, opts->strategy,
opts->xopts_nr, (const char **)opts->xopts,
--
2.27.0.139.gc9c318d6bf
^ permalink raw reply related [flat|nested] 221+ messages in thread
* Re: [RFC PATCH v1 02/17] merge-one-file: rewrite in C
2020-06-25 12:19 ` [RFC PATCH v1 02/17] merge-one-file: rewrite in C Alban Gruin
@ 2020-06-25 14:55 ` Chris Torek
2020-06-25 15:16 ` Phillip Wood
1 sibling, 0 replies; 221+ messages in thread
From: Chris Torek @ 2020-06-25 14:55 UTC (permalink / raw)
To: Alban Gruin; +Cc: git, Junio C Hamano
much snippage below, keeping just enough context to see which file,
function, etc:
On Thu, Jun 25, 2020 at 5:49 AM Alban Gruin <alban.gruin@gmail.com> wrote:
> diff --git a/builtin/merge-one-file.c b/builtin/merge-one-file.c
> new file mode 100644
> index 0000000000..4992a6cd30
> --- /dev/null
> +++ b/builtin/merge-one-file.c
> @@ -0,0 +1,275 @@
> +static int do_merge_one_file(const struct object_id *orig_blob,
> + const struct object_id *our_blob,
> + const struct object_id *their_blob, const char *path,
> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
> +{
> + int ret, source, dest;
> + source = open(src1.buf, O_RDONLY);
> + dest = open(path, O_WRONLY | O_TRUNC);
> +
> + copy_fd(source, dest);
> +
> + close(source);
> + close(dest);
> +
> + unlink(orig.buf);
> + unlink(src1.buf);
> + unlink(src2.buf);
Some of this goes away in subsequent patches, but most of these calls
should be checked for error returns, especially the two `open`s in case
someone has messed with permissions.
Chris
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [RFC PATCH v1 02/17] merge-one-file: rewrite in C
2020-06-25 12:19 ` [RFC PATCH v1 02/17] merge-one-file: rewrite in C Alban Gruin
2020-06-25 14:55 ` Chris Torek
@ 2020-06-25 15:16 ` Phillip Wood
2020-06-25 18:17 ` Phillip Wood
2020-07-12 11:22 ` Alban Gruin
1 sibling, 2 replies; 221+ messages in thread
From: Phillip Wood @ 2020-06-25 15:16 UTC (permalink / raw)
To: Alban Gruin, git; +Cc: Junio C Hamano
Hi Alban
I think this series is a great idea
On 25/06/2020 13:19, Alban Gruin wrote:
> This rewrites `git merge-one-file' from shell to C. This port is very
> straightforward: it keeps using external processes to edit the index,
> for instance. Errors are also displayed with fprintf() instead of
> error(). Both of these will be addressed in the next few commits,
> leading to its libification so its main function can be used from other
> commands directly.
>
> This also fixes a bug present in the original script: instead of
> checking if a _regular_ file exists when a file exists in the branch to
> merge, but not in our branch, the rewritten version checks if a file of
> any kind (ie. a directory, ...) exists. This fixes the tests t6035.14,
> where the branch to merge had a new file, `a/b', but our branch had a
> directory there; it should have failed because a directory exists, but
> it did not because there was no regular file called `a/b'. This test is
> now marked as successful.
>
> Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
> ---
> Makefile | 2 +-
> builtin.h | 1 +
> builtin/merge-one-file.c | 275 ++++++++++++++++++++++++++++++++
> git-merge-one-file.sh | 167 -------------------
> git.c | 1 +
> t/t6035-merge-dir-to-symlink.sh | 2 +-
> 6 files changed, 279 insertions(+), 169 deletions(-)
> create mode 100644 builtin/merge-one-file.c
> delete mode 100755 git-merge-one-file.sh
>
> diff --git a/Makefile b/Makefile
> index 372139f1f2..19574f5133 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -596,7 +596,6 @@ SCRIPT_SH += git-bisect.sh
> SCRIPT_SH += git-difftool--helper.sh
> SCRIPT_SH += git-filter-branch.sh
> SCRIPT_SH += git-merge-octopus.sh
> -SCRIPT_SH += git-merge-one-file.sh
> SCRIPT_SH += git-merge-resolve.sh
> SCRIPT_SH += git-mergetool.sh
> SCRIPT_SH += git-quiltimport.sh
> @@ -1089,6 +1088,7 @@ BUILTIN_OBJS += builtin/mailsplit.o
> BUILTIN_OBJS += builtin/merge-base.o
> BUILTIN_OBJS += builtin/merge-file.o
> BUILTIN_OBJS += builtin/merge-index.o
> +BUILTIN_OBJS += builtin/merge-one-file.o
> BUILTIN_OBJS += builtin/merge-ours.o
> BUILTIN_OBJS += builtin/merge-recursive.o
> BUILTIN_OBJS += builtin/merge-tree.o
> diff --git a/builtin.h b/builtin.h
> index a5ae15bfe5..9205d5ecdc 100644
> --- a/builtin.h
> +++ b/builtin.h
> @@ -172,6 +172,7 @@ int cmd_merge_base(int argc, const char **argv, const char *prefix);
> int cmd_merge_index(int argc, const char **argv, const char *prefix);
> int cmd_merge_ours(int argc, const char **argv, const char *prefix);
> int cmd_merge_file(int argc, const char **argv, const char *prefix);
> +int cmd_merge_one_file(int argc, const char **argv, const char *prefix);
> int cmd_merge_recursive(int argc, const char **argv, const char *prefix);
> int cmd_merge_tree(int argc, const char **argv, const char *prefix);
> int cmd_mktag(int argc, const char **argv, const char *prefix);
> diff --git a/builtin/merge-one-file.c b/builtin/merge-one-file.c
> new file mode 100644
> index 0000000000..4992a6cd30
> --- /dev/null
> +++ b/builtin/merge-one-file.c
> @@ -0,0 +1,275 @@
> +/*
> + * Builtin "git merge-one-file"
> + *
> + * Copyright (c) 2020 Alban Gruin
> + *
> + * Based on git-merge-one-file.sh, written by Linus Torvalds.
> + *
> + * This is the git per-file merge script, called with
> + *
> + * $1 - original file SHA1 (or empty)
> + * $2 - file in branch1 SHA1 (or empty)
> + * $3 - file in branch2 SHA1 (or empty)
> + * $4 - pathname in repository
> + * $5 - original file mode (or empty)
> + * $6 - file in branch1 mode (or empty)
> + * $7 - file in branch2 mode (or empty)
nit pick - these are now argv[1] etc rather than $1 etc
> + *
> + * Handle some trivial cases.. The _really_ trivial cases have
> + * been handled already by git read-tree, but that one doesn't
> + * do any merges that might change the tree layout.
> + */
> +
> +#define USE_THE_INDEX_COMPATIBILITY_MACROS
> +#include "cache.h"
> +#include "builtin.h"
> +#include "commit.h"
> +#include "dir.h"
> +#include "lockfile.h"
> +#include "object-store.h"
> +#include "run-command.h"
> +#include "xdiff-interface.h"
> +
> +static int create_temp_file(const struct object_id *oid, struct strbuf *path)
> +{
> + struct child_process cp = CHILD_PROCESS_INIT;
> + struct strbuf err = STRBUF_INIT;
> + int ret;
> +
> + cp.git_cmd = 1;
> + argv_array_pushl(&cp.args, "unpack-file", oid_to_hex(oid), NULL);
> + ret = pipe_command(&cp, NULL, 0, path, 0, &err, 0);
> + if (!ret && path->len > 0)
> + strbuf_trim_trailing_newline(path);
> +
> + fprintf(stderr, "%.*s", (int) err.len, err.buf);
> + strbuf_release(&err);
> +
> + return ret;
> +}
I know others will disagree but personally I'm not a huge fan of
rewriting shell functions in C that forks other builtins and then
converting the C to use the internal apis, it seems a much better to
just write the proper C version the first time. This is especially true
for simple function such as the ones in this file. That way the reviewer
gets a clear view of the final code from the patch, rather than having
to piece it together from a series of additions and deletions.
> +
> +static int add_to_index_cacheinfo(unsigned int mode,
> + const struct object_id *oid, const char *path)
> +{
> + struct child_process cp = CHILD_PROCESS_INIT;
> +
> + cp.git_cmd = 1;
> + argv_array_pushl(&cp.args, "update-index", "--add", "--cacheinfo", NULL);
> + argv_array_pushf(&cp.args, "%o,%s,%s", mode, oid_to_hex(oid), path);
> + return run_command(&cp);
> +}
> +
> +static int remove_from_index(const char *path)
> +{
> + struct child_process cp = CHILD_PROCESS_INIT;
> +
> + cp.git_cmd = 1;
> + argv_array_pushl(&cp.args, "update-index", "--remove", "--", path, NULL);
> + return run_command(&cp);
> +}
> +
> +static int checkout_from_index(const char *path)
> +{
> + struct child_process cp = CHILD_PROCESS_INIT;
> +
> + cp.git_cmd = 1;
> + argv_array_pushl(&cp.args, "checkout-index", "-u", "-f", "--", path, NULL);
> + return run_command(&cp);
> +}
> +
> +static int merge_one_file_deleted(const struct object_id *orig_blob,
> + const struct object_id *our_blob,
> + const struct object_id *their_blob, const char *path,
> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
> +{
> + if ((our_blob && orig_mode != our_mode) ||
> + (their_blob && orig_mode != their_mode)) {
> + fprintf(stderr, "ERROR: File %s deleted on one branch but had its\n", path);
> + fprintf(stderr, "ERROR: permissions changed on the other.\n");
> + return 1;
> + }
> +
> + if (our_blob) {
> + printf("Removing %s\n", path);
> +
> + if (file_exists(path))
> + remove_path(path);
> + }
> +
> + return remove_from_index(path);
> +}
> +
> +static int do_merge_one_file(const struct object_id *orig_blob,
> + const struct object_id *our_blob,
> + const struct object_id *their_blob, const char *path,
> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
> +{
> + int ret, source, dest;
> + struct strbuf src1 = STRBUF_INIT, src2 = STRBUF_INIT, orig = STRBUF_INIT;
> + struct child_process cp_merge = CHILD_PROCESS_INIT,
> + cp_checkout = CHILD_PROCESS_INIT,
> + cp_update = CHILD_PROCESS_INIT;
> +
> + if (our_mode == S_IFLNK || their_mode == S_IFLNK) {
> + fprintf(stderr, "ERROR: %s: Not merging symbolic link changes.\n", path);
> + return 1;
> + } else if (our_mode == S_IFGITLINK || their_mode == S_IFGITLINK) {
> + fprintf(stderr, "ERROR: %s: Not merging conflicting submodule changes.\n",
> + path);
> + return 1;
> + }
> +
> + create_temp_file(our_blob, &src1);
> + create_temp_file(their_blob, &src2);
> +
> + if (orig_blob) {
> + printf("Auto-merging %s\n", path);
> + create_temp_file(orig_blob, &orig);
> + } else {
> + printf("Added %s in both, but differently.\n", path);
> + create_temp_file(the_hash_algo->empty_blob, &orig);
> + }
> +
> + cp_merge.git_cmd = 1;
> + argv_array_pushl(&cp_merge.args, "merge-file", src1.buf, orig.buf, src2.buf,
> + NULL);
> + ret = run_command(&cp_merge);
> +
> + if (ret != 0)
> + ret = 1;
> +
> + cp_checkout.git_cmd = 1;
> + argv_array_pushl(&cp_checkout.args, "checkout-index", "-f", "--stage=2",
> + "--", path, NULL);
> + if (run_command(&cp_checkout))
> + return 1;
> +
> + source = open(src1.buf, O_RDONLY);
> + dest = open(path, O_WRONLY | O_TRUNC);
> +
> + copy_fd(source, dest);
> +
> + close(source);
> + close(dest);
> +
> + unlink(orig.buf);
> + unlink(src1.buf);
> + unlink(src2.buf);
> +
> + strbuf_release(&src1);
> + strbuf_release(&src2);
> + strbuf_release(&orig);
The whole business of creating temporary files and forking seems like a
lot of effort compared to calling ll_merge() which would also mean we
respect any merge attributes
> +
> + if (ret) {
> + fprintf(stderr, "ERROR: ");
> +
> + if (!orig_blob) {
I think the original does if (ret || !orig_blob) not &&
> + fprintf(stderr, "content conflict");
> + if (our_mode != their_mode)
> + fprintf(stderr, ", ");
sentence lego, in any case the message below should be printed
regardless of content conflicts. We should probably mark all these
messages for translation as well.
> + }
> +
> + if (our_mode != their_mode)
> + fprintf(stderr, "permissions conflict: %o->%o,%o",
> + orig_mode, our_mode, their_mode);
> +
> + fprintf(stderr, " in %s\n", path);
> +
> + return 1;
> + }
> +
> + cp_update.git_cmd = 1;
> + argv_array_pushl(&cp_update.args, "update-index", "--", path, NULL);
> + return run_command(&cp_update);
> +}
> +
> +static int merge_one_file(const struct object_id *orig_blob,
> + const struct object_id *our_blob,
> + const struct object_id *their_blob, const char *path,
> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
> +{
> + if (orig_blob &&
> + ((our_blob && oideq(orig_blob, our_blob)) ||
> + (their_blob && oideq(orig_blob, their_blob))))
> + return merge_one_file_deleted(orig_blob, our_blob, their_blob, path,
> + orig_mode, our_mode, their_mode);
It would be nice to preserve the comments from the script as I find they
help a lot in understanding which case each piece of code is handling.
The code above appears to be handling deletions but does not appear to
check that one side is actually missing. Shouldn't it be something like
if (orig_blob &&
((!their_blob && (our_blob && oideq(orig_blob, our_blob))) ||
(!our_blob && (their_blob && oideq(orig_blob, their_blob))))
Maybe this could do with a test case
> + else if (!orig_blob && our_blob && !their_blob) {
> + return add_to_index_cacheinfo(our_mode, our_blob, path);
> + } else if (!orig_blob && !our_blob && their_blob) {
> + printf("Adding %s\n", path);
> +
> + if (file_exists(path)) {
> + fprintf(stderr, "ERROR: untracked %s is overwritten by the merge.\n", path);
> + return 1;
> + }
> +
> + if (add_to_index_cacheinfo(their_mode, their_blob, path))
> + return 1;
> + return checkout_from_index(path);
> + } else if (!orig_blob && our_blob && their_blob &&
> + oideq(our_blob, their_blob)) {
> + if (our_mode != their_mode) {
> + fprintf(stderr, "ERROR: File %s added identically in both branches,", path);
> + fprintf(stderr, "ERROR: but permissions conflict %o->%o.\n",
> + our_mode, their_mode);
> + return 1;
> + }
> +
> + printf("Adding %s\n", path);
> +
> + if (add_to_index_cacheinfo(our_mode, our_blob, path))
> + return 1;
> + return checkout_from_index(path);
> + } else if (our_blob && their_blob)
> + return do_merge_one_file(orig_blob, our_blob, their_blob, path,
> + orig_mode, our_mode, their_mode);
> + else {
> + char *orig_hex = "", *our_hex = "", *their_hex = "";
> +
> + if (orig_blob)
> + orig_hex = oid_to_hex(orig_blob);
> + if (our_blob)
> + our_hex = oid_to_hex(our_blob);
> + if (their_blob)
> + their_hex = oid_to_hex(their_blob);
> +
> + fprintf(stderr, "ERROR: %s: Not handling case %s -> %s -> %s\n",
> + path, orig_hex, our_hex, their_hex);
> + return 1;
> + }
> +
> + return 0;
> +}
> +
> +static const char builtin_merge_one_file_usage[] =
> + "git merge-one-file <orig blob> <our blob> <their blob> <path> "
> + "<orig mode> <our mode> <their mode>\n\n"
> + "Blob ids and modes should be empty for missing files.";
> +
> +int cmd_merge_one_file(int argc, const char **argv, const char *prefix)
> +{
> + struct object_id orig_blob, our_blob, their_blob,
> + *p_orig_blob = NULL, *p_our_blob = NULL, *p_their_blob = NULL;
> + unsigned int orig_mode = 0, our_mode = 0, their_mode = 0;
> +
> + if (argc != 8)
> + usage(builtin_merge_one_file_usage);
> +
> + if (!get_oid(argv[1], &orig_blob)) {
> + p_orig_blob = &orig_blob;
> + orig_mode = strtol(argv[5], NULL, 8);
It would probably make sense to check that strtol() succeeds (and the
mode is sensible), and also that get_oid() fails because argv[1] is
empty, not because it is invalid.
Thanks for working on this
Best Wishes
Phillip
> + }
> +
> + if (!get_oid(argv[2], &our_blob)) {
> + p_our_blob = &our_blob;
> + our_mode = strtol(argv[6], NULL, 8);
> + }
> +
> + if (!get_oid(argv[3], &their_blob)) {
> + p_their_blob = &their_blob;
> + their_mode = strtol(argv[7], NULL, 8);
> + }
> +
> + return merge_one_file(p_orig_blob, p_our_blob, p_their_blob, argv[4],
> + orig_mode, our_mode, their_mode);
> +}
> diff --git a/git-merge-one-file.sh b/git-merge-one-file.sh
> deleted file mode 100755
> index f6d9852d2f..0000000000
> --- a/git-merge-one-file.sh
> +++ /dev/null
> @@ -1,167 +0,0 @@
> -#!/bin/sh
> -#
> -# Copyright (c) Linus Torvalds, 2005
> -#
> -# This is the git per-file merge script, called with
> -#
> -# $1 - original file SHA1 (or empty)
> -# $2 - file in branch1 SHA1 (or empty)
> -# $3 - file in branch2 SHA1 (or empty)
> -# $4 - pathname in repository
> -# $5 - original file mode (or empty)
> -# $6 - file in branch1 mode (or empty)
> -# $7 - file in branch2 mode (or empty)
> -#
> -# Handle some trivial cases.. The _really_ trivial cases have
> -# been handled already by git read-tree, but that one doesn't
> -# do any merges that might change the tree layout.
> -
> -USAGE='<orig blob> <our blob> <their blob> <path>'
> -USAGE="$USAGE <orig mode> <our mode> <their mode>"
> -LONG_USAGE="usage: git merge-one-file $USAGE
> -
> -Blob ids and modes should be empty for missing files."
> -
> -SUBDIRECTORY_OK=Yes
> -. git-sh-setup
> -cd_to_toplevel
> -require_work_tree
> -
> -if test $# != 7
> -then
> - echo "$LONG_USAGE"
> - exit 1
> -fi
> -
> -case "${1:-.}${2:-.}${3:-.}" in
> -#
> -# Deleted in both or deleted in one and unchanged in the other
> -#
> -"$1.." | "$1.$1" | "$1$1.")
> - if { test -z "$6" && test "$5" != "$7"; } ||
> - { test -z "$7" && test "$5" != "$6"; }
> - then
> - echo "ERROR: File $4 deleted on one branch but had its" >&2
> - echo "ERROR: permissions changed on the other." >&2
> - exit 1
> - fi
> -
> - if test -n "$2"
> - then
> - echo "Removing $4"
> - else
> - # read-tree checked that index matches HEAD already,
> - # so we know we do not have this path tracked.
> - # there may be an unrelated working tree file here,
> - # which we should just leave unmolested. Make sure
> - # we do not have it in the index, though.
> - exec git update-index --remove -- "$4"
> - fi
> - if test -f "$4"
> - then
> - rm -f -- "$4" &&
> - rmdir -p "$(expr "z$4" : 'z\(.*\)/')" 2>/dev/null || :
> - fi &&
> - exec git update-index --remove -- "$4"
> - ;;
> -
> -#
> -# Added in one.
> -#
> -".$2.")
> - # the other side did not add and we added so there is nothing
> - # to be done, except making the path merged.
> - exec git update-index --add --cacheinfo "$6" "$2" "$4"
> - ;;
> -"..$3")
> - echo "Adding $4"
> - if test -f "$4"
> - then
> - echo "ERROR: untracked $4 is overwritten by the merge." >&2
> - exit 1
> - fi
> - git update-index --add --cacheinfo "$7" "$3" "$4" &&
> - exec git checkout-index -u -f -- "$4"
> - ;;
> -
> -#
> -# Added in both, identically (check for same permissions).
> -#
> -".$3$2")
> - if test "$6" != "$7"
> - then
> - echo "ERROR: File $4 added identically in both branches," >&2
> - echo "ERROR: but permissions conflict $6->$7." >&2
> - exit 1
> - fi
> - echo "Adding $4"
> - git update-index --add --cacheinfo "$6" "$2" "$4" &&
> - exec git checkout-index -u -f -- "$4"
> - ;;
> -
> -#
> -# Modified in both, but differently.
> -#
> -"$1$2$3" | ".$2$3")
> -
> - case ",$6,$7," in
> - *,120000,*)
> - echo "ERROR: $4: Not merging symbolic link changes." >&2
> - exit 1
> - ;;
> - *,160000,*)
> - echo "ERROR: $4: Not merging conflicting submodule changes." >&2
> - exit 1
> - ;;
> - esac
> -
> - src1=$(git unpack-file $2)
> - src2=$(git unpack-file $3)
> - case "$1" in
> - '')
> - echo "Added $4 in both, but differently."
> - orig=$(git unpack-file $(git hash-object /dev/null))
> - ;;
> - *)
> - echo "Auto-merging $4"
> - orig=$(git unpack-file $1)
> - ;;
> - esac
> -
> - git merge-file "$src1" "$orig" "$src2"
> - ret=$?
> - msg=
> - if test $ret != 0 || test -z "$1"
> - then
> - msg='content conflict'
> - ret=1
> - fi
> -
> - # Create the working tree file, using "our tree" version from the
> - # index, and then store the result of the merge.
> - git checkout-index -f --stage=2 -- "$4" && cat "$src1" >"$4" || exit 1
> - rm -f -- "$orig" "$src1" "$src2"
> -
> - if test "$6" != "$7"
> - then
> - if test -n "$msg"
> - then
> - msg="$msg, "
> - fi
> - msg="${msg}permissions conflict: $5->$6,$7"
> - ret=1
> - fi
> -
> - if test $ret != 0
> - then
> - echo "ERROR: $msg in $4" >&2
> - exit 1
> - fi
> - exec git update-index -- "$4"
> - ;;
> -
> -*)
> - echo "ERROR: $4: Not handling case $1 -> $2 -> $3" >&2
> - ;;
> -esac
> -exit 1
> diff --git a/git.c b/git.c
> index a2d337eed7..058d91a2a5 100644
> --- a/git.c
> +++ b/git.c
> @@ -532,6 +532,7 @@ static struct cmd_struct commands[] = {
> { "merge-file", cmd_merge_file, RUN_SETUP_GENTLY },
> { "merge-index", cmd_merge_index, RUN_SETUP | NO_PARSEOPT },
> { "merge-ours", cmd_merge_ours, RUN_SETUP | NO_PARSEOPT },
> + { "merge-one-file", cmd_merge_one_file, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
> { "merge-recursive", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
> { "merge-recursive-ours", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
> { "merge-recursive-theirs", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
> diff --git a/t/t6035-merge-dir-to-symlink.sh b/t/t6035-merge-dir-to-symlink.sh
> index 2eddcc7664..5fb74e39a0 100755
> --- a/t/t6035-merge-dir-to-symlink.sh
> +++ b/t/t6035-merge-dir-to-symlink.sh
> @@ -94,7 +94,7 @@ test_expect_success SYMLINKS 'a/b was resolved as symlink' '
> test -h a/b
> '
>
> -test_expect_failure 'do not lose untracked in merge (resolve)' '
> +test_expect_success 'do not lose untracked in merge (resolve)' '
> git reset --hard &&
> git checkout baseline^0 &&
> >a/b/c/e &&
>
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [RFC PATCH v1 16/17] sequencer: use the "resolve" strategy without forking
2020-06-25 12:19 ` [RFC PATCH v1 16/17] sequencer: use the "resolve" " Alban Gruin
@ 2020-06-25 16:11 ` Phillip Wood
2020-07-12 11:27 ` Alban Gruin
0 siblings, 1 reply; 221+ messages in thread
From: Phillip Wood @ 2020-06-25 16:11 UTC (permalink / raw)
To: Alban Gruin, git; +Cc: Junio C Hamano
Hi Alban
On 25/06/2020 13:19, Alban Gruin wrote:
> This teaches the sequencer to invoke the "resolve" strategy with a
> function call instead of forking.
This is a good idea, however we should check the existing tests that use
this strategy to see if they are doing so to test the
try_merge_command() code path. I've got some patches in seen that use
'--strategy=resolve' to exercise the "non merge-recursive" code path, so
I'll update them to use a proper custom merge strategy.
Is it worth optimizing do_merge() to take advantage of resolve and
octopus being builtin as well?
Best Wishes
Phil
> Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
> ---
> sequencer.c | 13 ++++++++++---
> 1 file changed, 10 insertions(+), 3 deletions(-)
>
> diff --git a/sequencer.c b/sequencer.c
> index fd7701c88a..ea8dc58108 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -33,6 +33,7 @@
> #include "commit-reach.h"
> #include "rebase-interactive.h"
> #include "reset.h"
> +#include "merge-strategies.h"
>
> #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
>
> @@ -1922,9 +1923,15 @@ static int do_pick_commit(struct repository *r,
>
> commit_list_insert(base, &common);
> commit_list_insert(next, &remotes);
> - res |= try_merge_command(r, opts->strategy,
> - opts->xopts_nr, (const char **)opts->xopts,
> - common, oid_to_hex(&head), remotes);
> +
> + if (!strcmp(opts->strategy, "resolve")) {
> + repo_read_index(r);
> + res |= merge_strategies_resolve(r, common, oid_to_hex(&head), remotes);
> + } else
> + res |= try_merge_command(r, opts->strategy,
> + opts->xopts_nr, (const char **)opts->xopts,
> + common, oid_to_hex(&head), remotes);
> +
> free_commit_list(common);
> free_commit_list(remotes);
> }
>
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [RFC PATCH v1 02/17] merge-one-file: rewrite in C
2020-06-25 15:16 ` Phillip Wood
@ 2020-06-25 18:17 ` Phillip Wood
2020-06-26 14:33 ` Phillip Wood
2020-07-12 11:22 ` Alban Gruin
1 sibling, 1 reply; 221+ messages in thread
From: Phillip Wood @ 2020-06-25 18:17 UTC (permalink / raw)
To: Alban Gruin, git; +Cc: Junio C Hamano
On 25/06/2020 16:16, Phillip Wood wrote:
> Hi Alban
>
> I think this series is a great idea
>
> On 25/06/2020 13:19, Alban Gruin wrote:
>> This rewrites `git merge-one-file' from shell to C. This port is very
>> straightforward: it keeps using external processes to edit the index,
>> for instance. Errors are also displayed with fprintf() instead of
>> error(). Both of these will be addressed in the next few commits,
>> leading to its libification so its main function can be used from other
>> commands directly.
>>
>> This also fixes a bug present in the original script: instead of
>> checking if a _regular_ file exists when a file exists in the branch to
>> merge, but not in our branch, the rewritten version checks if a file of
>> any kind (ie. a directory, ...) exists. This fixes the tests t6035.14,
>> where the branch to merge had a new file, `a/b', but our branch had a
>> directory there; it should have failed because a directory exists, but
>> it did not because there was no regular file called `a/b'. This test is
>> now marked as successful.
>> [...]
>> +static int merge_one_file(const struct object_id *orig_blob,
>> + const struct object_id *our_blob,
>> + const struct object_id *their_blob, const char *path,
>> + unsigned int orig_mode, unsigned int our_mode, unsigned
>> int their_mode)
>> +{
>> + if (orig_blob &&
>> + ((our_blob && oideq(orig_blob, our_blob)) ||
>> + (their_blob && oideq(orig_blob, their_blob))))
>> + return merge_one_file_deleted(orig_blob, our_blob,
>> their_blob, path,
>> + orig_mode, our_mode, their_mode);
>
> It would be nice to preserve the comments from the script as I find they
> help a lot in understanding which case each piece of code is handling.
> The code above appears to be handling deletions but does not appear to
> check that one side is actually missing. Shouldn't it be something like
>
> if (orig_blob &&
> ((!their_blob && (our_blob && oideq(orig_blob, our_blob))) ||
> (!our_blob && (their_blob && oideq(orig_blob, their_blob))))
>
> Maybe this could do with a test case
The reason your version works is that if only one side has changed
read-tree will have done the merge itself so this only gets called if
one side has been deleted. However the original script printed an error
if someone accidentally called when the content had changed in only one
side and there were no mode changes. I think we want to keep that behavior.
In the future we could probably update this to also handle the cases
that read-tree normally takes care of rather than erroring out but I
don't think it is a high priority.
Best Wishes
Phillip
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [RFC PATCH v1 06/17] merge-index: libify merge_one_path() and merge_all()
2020-06-25 12:19 ` [RFC PATCH v1 06/17] merge-index: libify merge_one_path() and merge_all() Alban Gruin
@ 2020-06-26 10:13 ` Phillip Wood
2020-06-26 14:32 ` Phillip Wood
2020-07-12 11:36 ` Alban Gruin
0 siblings, 2 replies; 221+ messages in thread
From: Phillip Wood @ 2020-06-26 10:13 UTC (permalink / raw)
To: Alban Gruin, git; +Cc: Junio C Hamano
Hi Alban
On 25/06/2020 13:19, Alban Gruin wrote:
> The "resolve" and "octopus" merge strategies do not call directly `git
> merge-one-file', they delegate the work to another git command, `git
> merge-index', that will loop over files in the index and call the
> specified command. Unfortunately, these functions are not part of
> libgit.a, which means that once rewritten, the strategies would still
> have to invoke `merge-one-file' by spawning a new process first.
>
> To avoid this, this moves merge_one_path(), merge_all(), and their
> helpers to merge-strategies.c. They also take a callback to dictate
> what they should do for each file. For now, only one launching a new
> process is defined to preserve the behaviour of the builtin version.
>
> Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
> ---
>
> Notes:
> This patch is best viewed with `--color-moved'.
>
> builtin/merge-index.c | 77 +++------------------------------
> merge-strategies.c | 99 +++++++++++++++++++++++++++++++++++++++++++
> merge-strategies.h | 17 ++++++++
> 3 files changed, 123 insertions(+), 70 deletions(-)
>
> diff --git a/builtin/merge-index.c b/builtin/merge-index.c
> index 38ea6ad6ca..6cb666cc78 100644
> --- a/builtin/merge-index.c
> +++ b/builtin/merge-index.c
> @@ -1,74 +1,11 @@
> #define USE_THE_INDEX_COMPATIBILITY_MACROS
> #include "builtin.h"
> -#include "run-command.h"
> -
> -static const char *pgm;
> -static int one_shot, quiet;
> -static int err;
> -
> -static int merge_entry(int pos, const char *path)
> -{
> - int found;
> - const char *arguments[] = { pgm, "", "", "", path, "", "", "", NULL };
> - char hexbuf[4][GIT_MAX_HEXSZ + 1];
> - char ownbuf[4][60];
> -
> - if (pos >= active_nr)
> - die("git merge-index: %s not in the cache", path);
> - found = 0;
> - do {
> - const struct cache_entry *ce = active_cache[pos];
> - int stage = ce_stage(ce);
> -
> - if (strcmp(ce->name, path))
> - break;
> - found++;
> - oid_to_hex_r(hexbuf[stage], &ce->oid);
> - xsnprintf(ownbuf[stage], sizeof(ownbuf[stage]), "%o", ce->ce_mode);
> - arguments[stage] = hexbuf[stage];
> - arguments[stage + 4] = ownbuf[stage];
> - } while (++pos < active_nr);
> - if (!found)
> - die("git merge-index: %s not in the cache", path);
> -
> - if (run_command_v_opt(arguments, 0)) {
> - if (one_shot)
> - err++;
> - else {
> - if (!quiet)
> - die("merge program failed");
> - exit(1);
> - }
> - }
> - return found;
> -}
> -
> -static void merge_one_path(const char *path)
> -{
> - int pos = cache_name_pos(path, strlen(path));
> -
> - /*
> - * If it already exists in the cache as stage0, it's
> - * already merged and there is nothing to do.
> - */
> - if (pos < 0)
> - merge_entry(-pos-1, path);
> -}
> -
> -static void merge_all(void)
> -{
> - int i;
> - for (i = 0; i < active_nr; i++) {
> - const struct cache_entry *ce = active_cache[i];
> - if (!ce_stage(ce))
> - continue;
> - i += merge_entry(i, ce->name)-1;
> - }
> -}
> +#include "merge-strategies.h"
>
> int cmd_merge_index(int argc, const char **argv, const char *prefix)
> {
> - int i, force_file = 0;
> + int i, force_file = 0, err = 0, one_shot = 0, quiet = 0;
> + const char *pgm;
>
> /* Without this we cannot rely on waitpid() to tell
> * what happened to our children.
> @@ -98,14 +35,14 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
> continue;
> }
> if (!strcmp(arg, "-a")) {
> - merge_all();
> + err |= merge_all(&the_index, one_shot, quiet,
> + merge_program_cb, (void *)pgm);
> continue;
> }
> die("git merge-index: unknown option %s", arg);
> }
> - merge_one_path(arg);
> + err |= merge_one_path(&the_index, one_shot, quiet, arg,
> + merge_program_cb, (void *)pgm);
> }
> - if (err && !quiet)
> - die("merge program failed");
> return err;
> }
> diff --git a/merge-strategies.c b/merge-strategies.c
> index 3a9fce9f22..f4c0b4acd6 100644
> --- a/merge-strategies.c
> +++ b/merge-strategies.c
> @@ -1,6 +1,7 @@
> #include "cache.h"
> #include "dir.h"
> #include "merge-strategies.h"
> +#include "run-command.h"
> #include "xdiff-interface.h"
>
> static int add_to_index_cacheinfo(struct index_state *istate,
> @@ -189,3 +190,101 @@ int merge_strategies_one_file(struct repository *r,
>
> return 0;
> }
> +
> +int merge_program_cb(const struct object_id *orig_blob,
> + const struct object_id *our_blob,
> + const struct object_id *their_blob, const char *path,
> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
> + void *data)
Using void* is slightly unfortunate but it's needed later.
It would be nice to check if the program to run is git-merge-one-file
and call the appropriate function instead in that case so all users of
merge-index get the benefit of it being builtin. That probably wants to
be done in cmd_merge_index() rather than here though.
> +{
> + char ownbuf[3][60] = {{0}};
I know this is copied from above but it would be better to use
GIT_MAX_HEXSZ rather than 60
> + const char *arguments[] = { (char *)data, "", "", "", path,
> + ownbuf[0], ownbuf[1], ownbuf[2],
> + NULL };
> +
> + if (orig_blob)
> + arguments[1] = oid_to_hex(orig_blob);
> + if (our_blob)
> + arguments[2] = oid_to_hex(our_blob);
> + if (their_blob)
> + arguments[3] = oid_to_hex(their_blob);
> +
> + xsnprintf(ownbuf[0], sizeof(ownbuf[0]), "%o", orig_mode);
> + xsnprintf(ownbuf[1], sizeof(ownbuf[1]), "%o", our_mode);
> + xsnprintf(ownbuf[2], sizeof(ownbuf[2]), "%o", their_mode);
These are leaked. Also are you sure we want to fill out the mode if the
corresponding blob is missing - I guess it doesn't matter but it would
be good to check that - i think the original passed "". It also passed
"" rather than "0000..." for the blobs that were missing I think.
Best Wishes
Phillip
> +
> + return run_command_v_opt(arguments, 0);
> +}
> +
> +static int merge_entry(struct index_state *istate, int quiet, int pos,
> + const char *path, merge_cb cb, void *data)
> +{
> + int found = 0;
> + const struct object_id *oids[3] = {NULL};
> + unsigned int modes[3] = {0};
> +
> + do {
> + const struct cache_entry *ce = istate->cache[pos];
> + int stage = ce_stage(ce);
> +
> + if (strcmp(ce->name, path))
> + break;
> + found++;
> + oids[stage - 1] = &ce->oid;
> + modes[stage - 1] = ce->ce_mode;
> + } while (++pos < istate->cache_nr);
> + if (!found)
> + return error(_("%s is not in the cache"), path);
> +
> + if (cb(oids[0], oids[1], oids[2], path, modes[0], modes[1], modes[2], data)) {
> + if (!quiet)
> + error(_("Merge program failed"));
> + return -2;
> + }
> +
> + return found;
> +}
> +
> +int merge_one_path(struct index_state *istate, int oneshot, int quiet,
> + const char *path, merge_cb cb, void *data)
> +{
> + int pos = index_name_pos(istate, path, strlen(path)), ret;
> +
> + /*
> + * If it already exists in the cache as stage0, it's
> + * already merged and there is nothing to do.
> + */
> + if (pos < 0) {
> + ret = merge_entry(istate, quiet, -pos - 1, path, cb, data);
> + if (ret == -1)
> + return -1;
> + else if (ret == -2)
> + return 1;
> + }
> + return 0;
> +}
> +
> +int merge_all(struct index_state *istate, int oneshot, int quiet,
> + merge_cb cb, void *data)
> +{
> + int err = 0, i, ret;
> + for (i = 0; i < istate->cache_nr; i++) {
> + const struct cache_entry *ce = istate->cache[i];
> + if (!ce_stage(ce))
> + continue;
> +
> + ret = merge_entry(istate, quiet, i, ce->name, cb, data);
> + if (ret > 0)
> + i += ret - 1;
> + else if (ret == -1)
> + return -1;
> + else if (ret == -2) {
> + if (oneshot)
> + err++;
> + else
> + return 1;
> + }
> + }
> +
> + return err;
> +}
> diff --git a/merge-strategies.h b/merge-strategies.h
> index b527d145c7..cf78d7eaf4 100644
> --- a/merge-strategies.h
> +++ b/merge-strategies.h
> @@ -10,4 +10,21 @@ int merge_strategies_one_file(struct repository *r,
> unsigned int orig_mode, unsigned int our_mode,
> unsigned int their_mode);
>
> +typedef int (*merge_cb)(const struct object_id *orig_blob,
> + const struct object_id *our_blob,
> + const struct object_id *their_blob, const char *path,
> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
> + void *data);
> +
> +int merge_program_cb(const struct object_id *orig_blob,
> + const struct object_id *our_blob,
> + const struct object_id *their_blob, const char *path,
> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
> + void *data);
> +
> +int merge_one_path(struct index_state *istate, int oneshot, int quiet,
> + const char *path, merge_cb cb, void *data);
> +int merge_all(struct index_state *istate, int oneshot, int quiet,
> + merge_cb cb, void *data);
> +
> #endif /* MERGE_STRATEGIES_H */
>
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [RFC PATCH v1 06/17] merge-index: libify merge_one_path() and merge_all()
2020-06-26 10:13 ` Phillip Wood
@ 2020-06-26 14:32 ` Phillip Wood
2020-07-12 11:36 ` Alban Gruin
1 sibling, 0 replies; 221+ messages in thread
From: Phillip Wood @ 2020-06-26 14:32 UTC (permalink / raw)
To: Alban Gruin, git; +Cc: Junio C Hamano
Hi Alban
On 26/06/2020 11:13, Phillip Wood wrote:
> Hi Alban
>
> On 25/06/2020 13:19, Alban Gruin wrote:
>> The "resolve" and "octopus" merge strategies do not call directly `git
>> merge-one-file', they delegate the work to another git command, `git
>> merge-index', that will loop over files in the index and call the
>> specified command. Unfortunately, these functions are not part of
>> libgit.a, which means that once rewritten, the strategies would still
>> have to invoke `merge-one-file' by spawning a new process first.
>>
>> To avoid this, this moves merge_one_path(), merge_all(), and their
>> helpers to merge-strategies.c. They also take a callback to dictate
>> what they should do for each file. For now, only one launching a new
>> process is defined to preserve the behaviour of the builtin version.
>>
>> Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
>> ---
>>
>> Notes:
>> This patch is best viewed with `--color-moved'.
>>
>> builtin/merge-index.c | 77 +++------------------------------
>> merge-strategies.c | 99 +++++++++++++++++++++++++++++++++++++++++++
>> merge-strategies.h | 17 ++++++++
>> 3 files changed, 123 insertions(+), 70 deletions(-)
>>
>> diff --git a/builtin/merge-index.c b/builtin/merge-index.c
>> index 38ea6ad6ca..6cb666cc78 100644
>> --- a/builtin/merge-index.c
>> +++ b/builtin/merge-index.c
>> @@ -1,74 +1,11 @@
>> #define USE_THE_INDEX_COMPATIBILITY_MACROS
>> #include "builtin.h"
>> -#include "run-command.h"
>> -
>> -static const char *pgm;
>> -static int one_shot, quiet;
>> -static int err;
>> -
>> -static int merge_entry(int pos, const char *path)
>> -{
>> - int found;
>> - const char *arguments[] = { pgm, "", "", "", path, "", "", "", NULL };
>> - char hexbuf[4][GIT_MAX_HEXSZ + 1];
>> - char ownbuf[4][60];
>> -
>> - if (pos >= active_nr)
>> - die("git merge-index: %s not in the cache", path);
>> - found = 0;
>> - do {
>> - const struct cache_entry *ce = active_cache[pos];
>> - int stage = ce_stage(ce);
>> -
>> - if (strcmp(ce->name, path))
>> - break;
>> - found++;
>> - oid_to_hex_r(hexbuf[stage], &ce->oid);
>> - xsnprintf(ownbuf[stage], sizeof(ownbuf[stage]), "%o", ce->ce_mode);
>> - arguments[stage] = hexbuf[stage];
>> - arguments[stage + 4] = ownbuf[stage];
>> - } while (++pos < active_nr);
>> - if (!found)
>> - die("git merge-index: %s not in the cache", path);
>> -
>> - if (run_command_v_opt(arguments, 0)) {
>> - if (one_shot)
>> - err++;
>> - else {
>> - if (!quiet)
>> - die("merge program failed");
>> - exit(1);
>> - }
>> - }
>> - return found;
>> -}
>> -
>> -static void merge_one_path(const char *path)
>> -{
>> - int pos = cache_name_pos(path, strlen(path));
>> -
>> - /*
>> - * If it already exists in the cache as stage0, it's
>> - * already merged and there is nothing to do.
>> - */
>> - if (pos < 0)
>> - merge_entry(-pos-1, path);
>> -}
>> -
>> -static void merge_all(void)
>> -{
>> - int i;
>> - for (i = 0; i < active_nr; i++) {
>> - const struct cache_entry *ce = active_cache[i];
>> - if (!ce_stage(ce))
>> - continue;
>> - i += merge_entry(i, ce->name)-1;
>> - }
>> -}
>> +#include "merge-strategies.h"
>>
>> int cmd_merge_index(int argc, const char **argv, const char *prefix)
>> {
>> - int i, force_file = 0;
>> + int i, force_file = 0, err = 0, one_shot = 0, quiet = 0;
>> + const char *pgm;
>>
>> /* Without this we cannot rely on waitpid() to tell
>> * what happened to our children.
>> @@ -98,14 +35,14 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
>> continue;
>> }
>> if (!strcmp(arg, "-a")) {
>> - merge_all();
>> + err |= merge_all(&the_index, one_shot, quiet,
>> + merge_program_cb, (void *)pgm);
>> continue;
>> }
>> die("git merge-index: unknown option %s", arg);
>> }
>> - merge_one_path(arg);
>> + err |= merge_one_path(&the_index, one_shot, quiet, arg,
>> + merge_program_cb, (void *)pgm);
>> }
>> - if (err && !quiet)
>> - die("merge program failed");
>> return err;
>> }
>> diff --git a/merge-strategies.c b/merge-strategies.c
>> index 3a9fce9f22..f4c0b4acd6 100644
>> --- a/merge-strategies.c
>> +++ b/merge-strategies.c
>> @@ -1,6 +1,7 @@
>> #include "cache.h"
>> #include "dir.h"
>> #include "merge-strategies.h"
>> +#include "run-command.h"
>> #include "xdiff-interface.h"
>>
>> static int add_to_index_cacheinfo(struct index_state *istate,
>> @@ -189,3 +190,101 @@ int merge_strategies_one_file(struct repository *r,
>>
>> return 0;
>> }
>> +
>> +int merge_program_cb(const struct object_id *orig_blob,
>> + const struct object_id *our_blob,
>> + const struct object_id *their_blob, const char *path,
>> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
>> + void *data)
>
> Using void* is slightly unfortunate but it's needed later.
>
> It would be nice to check if the program to run is git-merge-one-file
> and call the appropriate function instead in that case so all users of
> merge-index get the benefit of it being builtin. That probably wants to
> be done in cmd_merge_index() rather than here though.
>
>> +{
>> + char ownbuf[3][60] = {{0}};
>
> I know this is copied from above but it would be better to use
> GIT_MAX_HEXSZ rather than 60
>
>> + const char *arguments[] = { (char *)data, "", "", "", path,
>> + ownbuf[0], ownbuf[1], ownbuf[2],
>> + NULL };
>> +
>> + if (orig_blob)
>> + arguments[1] = oid_to_hex(orig_blob);
>> + if (our_blob)
>> + arguments[2] = oid_to_hex(our_blob);
>> + if (their_blob)
>> + arguments[3] = oid_to_hex(their_blob);
>> +
>> + xsnprintf(ownbuf[0], sizeof(ownbuf[0]), "%o", orig_mode);
>> + xsnprintf(ownbuf[1], sizeof(ownbuf[1]), "%o", our_mode);
>> + xsnprintf(ownbuf[2], sizeof(ownbuf[2]), "%o", their_mode);
Sorry ignore all the comments below, they are nonsense
Best Wishes
Phillip
> These are leaked. Also are you sure we want to fill out the mode if the
> corresponding blob is missing - I guess it doesn't matter but it would
> be good to check that - i think the original passed "". It also passed
> "" rather than "0000..." for the blobs that were missing I think.
>
> Best Wishes
>
> Phillip
>
>> +
>> + return run_command_v_opt(arguments, 0);
>> +}
>> +
>> +static int merge_entry(struct index_state *istate, int quiet, int pos,
>> + const char *path, merge_cb cb, void *data)
>> +{
>> + int found = 0;
>> + const struct object_id *oids[3] = {NULL};
>> + unsigned int modes[3] = {0};
>> +
>> + do {
>> + const struct cache_entry *ce = istate->cache[pos];
>> + int stage = ce_stage(ce);
>> +
>> + if (strcmp(ce->name, path))
>> + break;
>> + found++;
>> + oids[stage - 1] = &ce->oid;
>> + modes[stage - 1] = ce->ce_mode;
>> + } while (++pos < istate->cache_nr);
>> + if (!found)
>> + return error(_("%s is not in the cache"), path);
>> +
>> + if (cb(oids[0], oids[1], oids[2], path, modes[0], modes[1], modes[2], data)) {
>> + if (!quiet)
>> + error(_("Merge program failed"));
>> + return -2;
>> + }
>> +
>> + return found;
>> +}
>> +
>> +int merge_one_path(struct index_state *istate, int oneshot, int quiet,
>> + const char *path, merge_cb cb, void *data)
>> +{
>> + int pos = index_name_pos(istate, path, strlen(path)), ret;
>> +
>> + /*
>> + * If it already exists in the cache as stage0, it's
>> + * already merged and there is nothing to do.
>> + */
>> + if (pos < 0) {
>> + ret = merge_entry(istate, quiet, -pos - 1, path, cb, data);
>> + if (ret == -1)
>> + return -1;
>> + else if (ret == -2)
>> + return 1;
>> + }
>> + return 0;
>> +}
>> +
>> +int merge_all(struct index_state *istate, int oneshot, int quiet,
>> + merge_cb cb, void *data)
>> +{
>> + int err = 0, i, ret;
>> + for (i = 0; i < istate->cache_nr; i++) {
>> + const struct cache_entry *ce = istate->cache[i];
>> + if (!ce_stage(ce))
>> + continue;
>> +
>> + ret = merge_entry(istate, quiet, i, ce->name, cb, data);
>> + if (ret > 0)
>> + i += ret - 1;
>> + else if (ret == -1)
>> + return -1;
>> + else if (ret == -2) {
>> + if (oneshot)
>> + err++;
>> + else
>> + return 1;
>> + }
>> + }
>> +
>> + return err;
>> +}
>> diff --git a/merge-strategies.h b/merge-strategies.h
>> index b527d145c7..cf78d7eaf4 100644
>> --- a/merge-strategies.h
>> +++ b/merge-strategies.h
>> @@ -10,4 +10,21 @@ int merge_strategies_one_file(struct repository *r,
>> unsigned int orig_mode, unsigned int our_mode,
>> unsigned int their_mode);
>>
>> +typedef int (*merge_cb)(const struct object_id *orig_blob,
>> + const struct object_id *our_blob,
>> + const struct object_id *their_blob, const char *path,
>> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
>> + void *data);
>> +
>> +int merge_program_cb(const struct object_id *orig_blob,
>> + const struct object_id *our_blob,
>> + const struct object_id *their_blob, const char *path,
>> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
>> + void *data);
>> +
>> +int merge_one_path(struct index_state *istate, int oneshot, int quiet,
>> + const char *path, merge_cb cb, void *data);
>> +int merge_all(struct index_state *istate, int oneshot, int quiet,
>> + merge_cb cb, void *data);
>> +
>> #endif /* MERGE_STRATEGIES_H */
>>
>
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [RFC PATCH v1 02/17] merge-one-file: rewrite in C
2020-06-25 18:17 ` Phillip Wood
@ 2020-06-26 14:33 ` Phillip Wood
0 siblings, 0 replies; 221+ messages in thread
From: Phillip Wood @ 2020-06-26 14:33 UTC (permalink / raw)
To: Alban Gruin, git; +Cc: Junio C Hamano
On 25/06/2020 19:17, Phillip Wood wrote:
> On 25/06/2020 16:16, Phillip Wood wrote:
>> Hi Alban
>>
>> I think this series is a great idea
>>
>> On 25/06/2020 13:19, Alban Gruin wrote:
>>> This rewrites `git merge-one-file' from shell to C. This port is very
>>> straightforward: it keeps using external processes to edit the index,
>>> for instance. Errors are also displayed with fprintf() instead of
>>> error(). Both of these will be addressed in the next few commits,
>>> leading to its libification so its main function can be used from other
>>> commands directly.
>>>
>>> This also fixes a bug present in the original script: instead of
>>> checking if a _regular_ file exists when a file exists in the branch to
>>> merge, but not in our branch, the rewritten version checks if a file of
>>> any kind (ie. a directory, ...) exists. This fixes the tests t6035.14,
>>> where the branch to merge had a new file, `a/b', but our branch had a
>>> directory there; it should have failed because a directory exists, but
>>> it did not because there was no regular file called `a/b'. This test is
>>> now marked as successful.
>>> [...]
>>> +static int merge_one_file(const struct object_id *orig_blob,
>>> + const struct object_id *our_blob,
>>> + const struct object_id *their_blob, const char *path,
>>> + unsigned int orig_mode, unsigned int our_mode, unsigned
>>> int their_mode)
>>> +{
>>> + if (orig_blob &&
>>> + ((our_blob && oideq(orig_blob, our_blob)) ||
>>> + (their_blob && oideq(orig_blob, their_blob))))
>>> + return merge_one_file_deleted(orig_blob, our_blob,
>>> their_blob, path,
>>> + orig_mode, our_mode, their_mode);
>>
>> It would be nice to preserve the comments from the script as I find they
>> help a lot in understanding which case each piece of code is handling.
>> The code above appears to be handling deletions but does not appear to
>> check that one side is actually missing. Shouldn't it be something like
>>
>> if (orig_blob &&
>> ((!their_blob && (our_blob && oideq(orig_blob, our_blob))) ||
>> (!our_blob && (their_blob && oideq(orig_blob, their_blob))))
>>
>> Maybe this could do with a test case
>
> The reason your version works is that if only one side has changed
> read-tree will have done the merge itself so this only gets called if
> one side has been deleted. However the original script printed an error
> if someone accidentally called when the content had changed in only one
> side and there were no mode changes. I think we want to keep that behavior.
Actually I think the original probably handles this case by calling 'git
merge-file'
Best Wishes
Phillip
> In the future we could probably update this to also handle the cases
> that read-tree normally takes care of rather than erroring out but I
> don't think it is a high priority.
>
> Best Wishes
>
> Phillip
>
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [RFC PATCH v1 02/17] merge-one-file: rewrite in C
2020-06-25 15:16 ` Phillip Wood
2020-06-25 18:17 ` Phillip Wood
@ 2020-07-12 11:22 ` Alban Gruin
1 sibling, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-07-12 11:22 UTC (permalink / raw)
To: phillip.wood, git; +Cc: Junio C Hamano
[-- Attachment #1: Type: text/plain, Size: 6408 bytes --]
Hi Phillip,
Phillip Wood (phillip.wood123@gmail.com) a écrit :
> Hi Alban
>
> I think this series is a great idea
>
> On 25/06/2020 13:19, Alban Gruin wrote:
> -%<-
> > diff --git a/builtin/merge-one-file.c b/builtin/merge-one-file.c
> > new file mode 100644
> > index 0000000000..4992a6cd30
> > --- /dev/null
> > +++ b/builtin/merge-one-file.c
> > @@ -0,0 +1,275 @@
> > +/*
> > + * Builtin "git merge-one-file"
> > + *
> > + * Copyright (c) 2020 Alban Gruin
> > + *
> > + * Based on git-merge-one-file.sh, written by Linus Torvalds.
> > + *
> > + * This is the git per-file merge script, called with
> > + *
> > + * $1 - original file SHA1 (or empty)
> > + * $2 - file in branch1 SHA1 (or empty)
> > + * $3 - file in branch2 SHA1 (or empty)
> > + * $4 - pathname in repository
> > + * $5 - original file mode (or empty)
> > + * $6 - file in branch1 mode (or empty)
> > + * $7 - file in branch2 mode (or empty)
>
> nit pick - these are now argv[1] etc rather than $1 etc
>
I'll change that, and replace "script" by "utility".
> > + *
> > + * Handle some trivial cases.. The _really_ trivial cases have
> > + * been handled already by git read-tree, but that one doesn't
> > + * do any merges that might change the tree layout.
> > + */
> > +
> > +#define USE_THE_INDEX_COMPATIBILITY_MACROS
> > +#include "cache.h"
> > +#include "builtin.h"
> > +#include "commit.h"
> > +#include "dir.h"
> > +#include "lockfile.h"
> > +#include "object-store.h"
> > +#include "run-command.h"
> > +#include "xdiff-interface.h"
> > +
> > +static int create_temp_file(const struct object_id *oid, struct strbuf
> > *path)
> > +{
> > + struct child_process cp = CHILD_PROCESS_INIT;
> > + struct strbuf err = STRBUF_INIT;
> > + int ret;
> > +
> > + cp.git_cmd = 1;
> > + argv_array_pushl(&cp.args, "unpack-file", oid_to_hex(oid), NULL);
> > + ret = pipe_command(&cp, NULL, 0, path, 0, &err, 0);
> > + if (!ret && path->len > 0)
> > + strbuf_trim_trailing_newline(path);
> > +
> > + fprintf(stderr, "%.*s", (int) err.len, err.buf);
> > + strbuf_release(&err);
> > +
> > + return ret;
> > +}
>
> I know others will disagree but personally I'm not a huge fan of rewriting
> shell functions in C that forks other builtins and then converting the C to
> use the internal apis, it seems a much better to just write the proper C
> version the first time. This is especially true for simple function such as
> the ones in this file. That way the reviewer gets a clear view of the final
> code from the patch, rather than having to piece it together from a series of
> additions and deletions.
>
I understand -- I'll squash the "rewrite" and "use internal APIs" patches
together as a last step for the v2, so I'd be able to get them back with
all the changes made in the v2 if needed.
> -%<-
> > +static int do_merge_one_file(const struct object_id *orig_blob,
> > + const struct object_id *our_blob,
> > + const struct object_id *their_blob, const char
> > *path,
> > + unsigned int orig_mode, unsigned int our_mode,
> > unsigned int their_mode)
> > +{
> > + int ret, source, dest;
> > + struct strbuf src1 = STRBUF_INIT, src2 = STRBUF_INIT, orig =
> > STRBUF_INIT;
> > + struct child_process cp_merge = CHILD_PROCESS_INIT,
> > + cp_checkout = CHILD_PROCESS_INIT,
> > + cp_update = CHILD_PROCESS_INIT;
> > +
> > + if (our_mode == S_IFLNK || their_mode == S_IFLNK) {
> > + fprintf(stderr, "ERROR: %s: Not merging symbolic link
> > changes.\n", path);
> > + return 1;
> > + } else if (our_mode == S_IFGITLINK || their_mode == S_IFGITLINK) {
> > + fprintf(stderr, "ERROR: %s: Not merging conflicting submodule
> > changes.\n",
> > + path);
> > + return 1;
> > + }
> > +
> > + create_temp_file(our_blob, &src1);
> > + create_temp_file(their_blob, &src2);
> > +
> > + if (orig_blob) {
> > + printf("Auto-merging %s\n", path);
> > + create_temp_file(orig_blob, &orig);
> > + } else {
> > + printf("Added %s in both, but differently.\n", path);
> > + create_temp_file(the_hash_algo->empty_blob, &orig);
> > + }
> > +
> > + cp_merge.git_cmd = 1;
> > + argv_array_pushl(&cp_merge.args, "merge-file", src1.buf, orig.buf,
> > src2.buf,
> > + NULL);
> > + ret = run_command(&cp_merge);
> > +
> > + if (ret != 0)
> > + ret = 1;
> > +
> > + cp_checkout.git_cmd = 1;
> > + argv_array_pushl(&cp_checkout.args, "checkout-index", "-f",
> > "--stage=2",
> > + "--", path, NULL);
> > + if (run_command(&cp_checkout))
> > + return 1;
> > +
> > + source = open(src1.buf, O_RDONLY);
> > + dest = open(path, O_WRONLY | O_TRUNC);
> > +
> > + copy_fd(source, dest);
> > +
> > + close(source);
> > + close(dest);
> > +
> > + unlink(orig.buf);
> > + unlink(src1.buf);
> > + unlink(src2.buf);
> > +
> > + strbuf_release(&src1);
> > + strbuf_release(&src2);
> > + strbuf_release(&orig);
>
> The whole business of creating temporary files and forking seems like a lot of
> effort compared to calling ll_merge() which would also mean we respect any
> merge attributes
>
> > +
> > + if (ret) {
> > + fprintf(stderr, "ERROR: ");
> > +
> > + if (!orig_blob) {
>
> I think the original does if (ret || !orig_blob) not &&
Good catch.
> > + fprintf(stderr, "content conflict");
> > + if (our_mode != their_mode)
> > + fprintf(stderr, ", ");
>
> sentence lego, in any case the message below should be printed regardless of
> content conflicts. We should probably mark all these messages for translation
> as well.
>
Yeah, I think I will replace them with two calls to `error()'.
> -%<-
> > +int cmd_merge_one_file(int argc, const char **argv, const char *prefix)
> > +{
> > + struct object_id orig_blob, our_blob, their_blob,
> > + *p_orig_blob = NULL, *p_our_blob = NULL, *p_their_blob = NULL;
> > + unsigned int orig_mode = 0, our_mode = 0, their_mode = 0;
> > +
> > + if (argc != 8)
> > + usage(builtin_merge_one_file_usage);
> > +
> > + if (!get_oid(argv[1], &orig_blob)) {
> > + p_orig_blob = &orig_blob;
> > + orig_mode = strtol(argv[5], NULL, 8);
>
> It would probably make sense to check that strtol() succeeds (and the mode is
> sensible), and also that get_oid() fails because argv[1] is empty, not because
> it is invalid.
>
Checking that `orig_mode' and friends are lower than 0800, and that
`*argv[1]' is not equal to '\0' should be enough, right?
> Thanks for working on this
As always, thank you for your reviews.
> Best Wishes
>
> Phillip
>
>
Cheers,
Alban
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [RFC PATCH v1 16/17] sequencer: use the "resolve" strategy without forking
2020-06-25 16:11 ` Phillip Wood
@ 2020-07-12 11:27 ` Alban Gruin
0 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-07-12 11:27 UTC (permalink / raw)
To: phillip.wood, git; +Cc: Junio C Hamano
[-- Attachment #1: Type: text/plain, Size: 932 bytes --]
Hi Phillip,
Phillip Wood (phillip.wood123@gmail.com) a écrit :
> Hi Alban
>
> On 25/06/2020 13:19, Alban Gruin wrote:
> > This teaches the sequencer to invoke the "resolve" strategy with a
> > function call instead of forking.
>
> This is a good idea, however we should check the existing tests that use this
> strategy to see if they are doing so to test the try_merge_command() code
> path. I've got some patches in seen that use '--strategy=resolve' to exercise
> the "non merge-recursive" code path, so I'll update them to use a proper
> custom merge strategy.
>
> Is it worth optimizing do_merge() to take advantage of resolve and octopus
> being builtin as well?
>
Hmm, I see that do_merge() doesn't call directly the strategies, and
delegates this work to git-merge. If calling the new APIs does not imply
to copy/paste too much code from merge.c, then my answer is yes.
> Best Wishes
>
> Phil
>
Cheers,
Alban
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [RFC PATCH v1 06/17] merge-index: libify merge_one_path() and merge_all()
2020-06-26 10:13 ` Phillip Wood
2020-06-26 14:32 ` Phillip Wood
@ 2020-07-12 11:36 ` Alban Gruin
2020-07-12 18:02 ` Phillip Wood
1 sibling, 1 reply; 221+ messages in thread
From: Alban Gruin @ 2020-07-12 11:36 UTC (permalink / raw)
To: Phillip Wood, git; +Cc: Junio C Hamano
Hi Phillip,
Phillip Wood (phillip.wood123@gmail.com) a écrit :
> Hi Alban
>
> On 25/06/2020 13:19, Alban Gruin wrote:
> -%<-
> > diff --git a/merge-strategies.c b/merge-strategies.c
> > index 3a9fce9f22..f4c0b4acd6 100644
> > --- a/merge-strategies.c
> > +++ b/merge-strategies.c
> > @@ -1,6 +1,7 @@
> > #include "cache.h"
> > #include "dir.h"
> > #include "merge-strategies.h"
> > +#include "run-command.h"
> > #include "xdiff-interface.h"
> >
> > static int add_to_index_cacheinfo(struct index_state *istate,
> > @@ -189,3 +190,101 @@ int merge_strategies_one_file(struct repository *r,
> >
> > return 0;
> > }
> > +
> > +int merge_program_cb(const struct object_id *orig_blob,
> > + const struct object_id *our_blob,
> > + const struct object_id *their_blob, const char *path,
> > + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
> > + void *data)
>
> Using void* is slightly unfortunate but it's needed later.
>
> It would be nice to check if the program to run is git-merge-one-file
> and call the appropriate function instead in that case so all users of
> merge-index get the benefit of it being builtin. That probably wants to
> be done in cmd_merge_index() rather than here though.
>
Dunno, I am not completely comfortable with changing a parameter that
specifically describe a program, to a parameter that may be a program,
except in one case where `merge-index' should lock the index, setup the
worktree, and call a function instead.
Well, I say that, but implementing that behaviour is not that hard:
-- snip --
diff --git a/builtin/merge-index.c b/builtin/merge-index.c
index 6cb666cc78..19fff9a113 100644
--- a/builtin/merge-index.c
+++ b/builtin/merge-index.c
@@ -1,11 +1,15 @@
#define USE_THE_INDEX_COMPATIBILITY_MACROS
#include "builtin.h"
+#include "lockfile.h"
#include "merge-strategies.h"
int cmd_merge_index(int argc, const char **argv, const char *prefix)
{
int i, force_file = 0, err = 0, one_shot = 0, quiet = 0;
const char *pgm;
+ void *data;
+ merge_cb merge_action;
+ struct lock_file lock = LOCK_INIT;
/* Without this we cannot rely on waitpid() to tell
* what happened to our children.
@@ -26,7 +30,19 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
quiet = 1;
i++;
}
+
pgm = argv[i++];
+ if (!strcmp(pgm, "git-merge-one-file")) {
+ merge_action = merge_one_file_cb;
+ data = (void *)the_repository;
+
+ setup_work_tree();
+ hold_locked_index(&lock, LOCK_DIE_ON_ERROR);
+ } else {
+ merge_action = merge_program_cb;
+ data = (void *)pgm;
+ }
+
for (; i < argc; i++) {
const char *arg = argv[i];
if (!force_file && *arg == '-') {
@@ -36,13 +52,22 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
}
if (!strcmp(arg, "-a")) {
err |= merge_all(&the_index, one_shot, quiet,
- merge_program_cb, (void *)pgm);
+ merge_action, data);
continue;
}
die("git merge-index: unknown option %s", arg);
}
err |= merge_one_path(&the_index, one_shot, quiet, arg,
- merge_program_cb, (void *)pgm);
+ merge_action, data);
+ }
+
+ if (merge_action == merge_one_file_cb) {
+ if (err) {
+ rollback_lock_file(&lock);
+ return err;
+ }
+
+ return write_locked_index(&the_index, &lock, COMMIT_LOCK);
}
return err;
}
-- snap --
> > +{
> > + char ownbuf[3][60] = {{0}};
>
> I know this is copied from above but it would be better to use
> GIT_MAX_HEXSZ rather than 60
>
Cheers,
Alban
^ permalink raw reply related [flat|nested] 221+ messages in thread
* Re: [RFC PATCH v1 06/17] merge-index: libify merge_one_path() and merge_all()
2020-07-12 11:36 ` Alban Gruin
@ 2020-07-12 18:02 ` Phillip Wood
2020-07-12 20:10 ` Alban Gruin
0 siblings, 1 reply; 221+ messages in thread
From: Phillip Wood @ 2020-07-12 18:02 UTC (permalink / raw)
To: Alban Gruin, git; +Cc: Junio C Hamano
Hi Alban
On 12/07/2020 12:36, Alban Gruin wrote:
> Hi Phillip,
>
> Phillip Wood (phillip.wood123@gmail.com) a écrit :
>
>> Hi Alban
>>
>> On 25/06/2020 13:19, Alban Gruin wrote:
>> -%<-
>>> diff --git a/merge-strategies.c b/merge-strategies.c
>>> index 3a9fce9f22..f4c0b4acd6 100644
>>> --- a/merge-strategies.c
>>> +++ b/merge-strategies.c
>>> @@ -1,6 +1,7 @@
>>> #include "cache.h"
>>> #include "dir.h"
>>> #include "merge-strategies.h"
>>> +#include "run-command.h"
>>> #include "xdiff-interface.h"
>>>
>>> static int add_to_index_cacheinfo(struct index_state *istate,
>>> @@ -189,3 +190,101 @@ int merge_strategies_one_file(struct repository *r,
>>>
>>> return 0;
>>> }
>>> +
>>> +int merge_program_cb(const struct object_id *orig_blob,
>>> + const struct object_id *our_blob,
>>> + const struct object_id *their_blob, const char *path,
>>> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
>>> + void *data)
>>
>> Using void* is slightly unfortunate but it's needed later.
>>
>> It would be nice to check if the program to run is git-merge-one-file
>> and call the appropriate function instead in that case so all users of
>> merge-index get the benefit of it being builtin. That probably wants to
>> be done in cmd_merge_index() rather than here though.
>>
>
> Dunno, I am not completely comfortable with changing a parameter that
> specifically describe a program, to a parameter that may be a program,
> except in one case where `merge-index' should lock the index, setup the
> worktree, and call a function instead.
There is some previous discussion about this at
https://lore.kernel.org/git/xmqqblv5kr9u.fsf@gitster-ct.c.googlers.com/
I'll try and have a proper look at your comments towards the end of the
week (or maybe the week after the way things are at the moment...)
Best Wishes
Phillip
> Well, I say that, but implementing that behaviour is not that hard:
>
> -- snip --
> diff --git a/builtin/merge-index.c b/builtin/merge-index.c
> index 6cb666cc78..19fff9a113 100644
> --- a/builtin/merge-index.c
> +++ b/builtin/merge-index.c
> @@ -1,11 +1,15 @@
> #define USE_THE_INDEX_COMPATIBILITY_MACROS
> #include "builtin.h"
> +#include "lockfile.h"
> #include "merge-strategies.h"
>
> int cmd_merge_index(int argc, const char **argv, const char *prefix)
> {
> int i, force_file = 0, err = 0, one_shot = 0, quiet = 0;
> const char *pgm;
> + void *data;
> + merge_cb merge_action;
> + struct lock_file lock = LOCK_INIT;
>
> /* Without this we cannot rely on waitpid() to tell
> * what happened to our children.
> @@ -26,7 +30,19 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
> quiet = 1;
> i++;
> }
> +
> pgm = argv[i++];
> + if (!strcmp(pgm, "git-merge-one-file")) {
> + merge_action = merge_one_file_cb;
> + data = (void *)the_repository;
> +
> + setup_work_tree();
> + hold_locked_index(&lock, LOCK_DIE_ON_ERROR);
> + } else {
> + merge_action = merge_program_cb;
> + data = (void *)pgm;
> + }
> +
> for (; i < argc; i++) {
> const char *arg = argv[i];
> if (!force_file && *arg == '-') {
> @@ -36,13 +52,22 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
> }
> if (!strcmp(arg, "-a")) {
> err |= merge_all(&the_index, one_shot, quiet,
> - merge_program_cb, (void *)pgm);
> + merge_action, data);
> continue;
> }
> die("git merge-index: unknown option %s", arg);
> }
> err |= merge_one_path(&the_index, one_shot, quiet, arg,
> - merge_program_cb, (void *)pgm);
> + merge_action, data);
> + }
> +
> + if (merge_action == merge_one_file_cb) {
> + if (err) {
> + rollback_lock_file(&lock);
> + return err;
> + }
> +
> + return write_locked_index(&the_index, &lock, COMMIT_LOCK);
> }
> return err;
> }
> -- snap --
>
>>> +{
>>> + char ownbuf[3][60] = {{0}};
>>
>> I know this is copied from above but it would be better to use
>> GIT_MAX_HEXSZ rather than 60
>>
>
> Cheers,
> Alban
>
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [RFC PATCH v1 06/17] merge-index: libify merge_one_path() and merge_all()
2020-07-12 18:02 ` Phillip Wood
@ 2020-07-12 20:10 ` Alban Gruin
0 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-07-12 20:10 UTC (permalink / raw)
To: Phillip Wood, git; +Cc: Junio C Hamano
Hi Phillip,
Phillip Wood (phillip.wood123@gmail.com) a écrit :
> Hi Alban
>
> On 12/07/2020 12:36, Alban Gruin wrote:
> > Hi Phillip,
> >
> > Phillip Wood (phillip.wood123@gmail.com) a écrit :
> >
> >> Hi Alban
> >>
> >> On 25/06/2020 13:19, Alban Gruin wrote:
> >> -%<-
> >>> diff --git a/merge-strategies.c b/merge-strategies.c
> >>> index 3a9fce9f22..f4c0b4acd6 100644
> >>> --- a/merge-strategies.c
> >>> +++ b/merge-strategies.c
> >>> @@ -1,6 +1,7 @@
> >>> #include "cache.h"
> >>> #include "dir.h"
> >>> #include "merge-strategies.h"
> >>> +#include "run-command.h"
> >>> #include "xdiff-interface.h"
> >>>
> >>> static int add_to_index_cacheinfo(struct index_state *istate,
> >>> @@ -189,3 +190,101 @@ int merge_strategies_one_file(struct repository *r,
> >>>
> >>> return 0;
> >>> }
> >>> +
> >>> +int merge_program_cb(const struct object_id *orig_blob,
> >>> + const struct object_id *our_blob,
> >>> + const struct object_id *their_blob, const char *path,
> >>> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
> >>> + void *data)
> >>
> >> Using void* is slightly unfortunate but it's needed later.
> >>
> >> It would be nice to check if the program to run is git-merge-one-file
> >> and call the appropriate function instead in that case so all users of
> >> merge-index get the benefit of it being builtin. That probably wants to
> >> be done in cmd_merge_index() rather than here though.
> >>
> >
> > Dunno, I am not completely comfortable with changing a parameter that
> > specifically describe a program, to a parameter that may be a program,
> > except in one case where `merge-index' should lock the index, setup the
> > worktree, and call a function instead.
>
> There is some previous discussion about this at
> https://lore.kernel.org/git/xmqqblv5kr9u.fsf@gitster-ct.c.googlers.com/
>
Thanks. If no-one seems really against doing that, I'll include the patch
below in the v2, with an additional note in the man page.
> I'll try and have a proper look at your comments towards the end of the
> week (or maybe the week after the way things are at the moment...)
>
> Best Wishes
>
> Phillip
>
Cheers,
Alban
^ permalink raw reply [flat|nested] 221+ messages in thread
* [PATCH v2 00/11] Rewrite the remaining merge strategies from shell to C
2020-06-25 12:19 [RFC PATCH v1 00/17] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (16 preceding siblings ...)
2020-06-25 12:19 ` [RFC PATCH v1 17/17] sequencer: use the "octopus" merge " Alban Gruin
@ 2020-09-01 10:56 ` Alban Gruin
2020-09-01 10:56 ` [PATCH v2 01/11] t6027: modernise tests Alban Gruin
` (11 more replies)
17 siblings, 12 replies; 221+ messages in thread
From: Alban Gruin @ 2020-09-01 10:56 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
In a effort to reduce the number of shell scripts in git's codebase, I
propose this patch series converting the two remaining merge strategies,
resolve and octopus, from shell to C. This will enable slightly better
performance, better integration with git itself (no more forking to
perform these operations), better portability (Windows and shell scripts
don't mix well).
Three scripts are actually converted: first git-merge-one-file.sh, then
git-merge-resolve.sh, and finally git-merge-octopus.sh. Not only they
are converted, but they also are modified to operate without forking,
and then libified so they can be used by git without spawning another
process.
The first patch is not important to make the whole series work, but I
made this patch while working on it.
This series keeps the commands `git merge-one-file', `git
merge-resolve', and `git merge-octopus', so any script depending on them
should keep working without any changes.
This series is based on d9cd433147 (po: add missing letter for French
message, 2020-08-27). The tip is tagged as
"rewrite-merge-strategies-v2" at https://github.com/agrn/git.
Changes since v1:
- Merged commits rewriting and libifying scripts.
- Introduce checks in merge-one-file to check that file modes are
correct.
- Use ll_merge() instead of xdl_merge().
- merge-index does no longer fork to call git-merge-one-file.
- Remove usage of the_index in merge-one-file.c.
- Mark more strings for translation.
- Carry more comments from the original scripts.
- Use GIT_MAX_HEXSZ instead of hardcoding 60.
Alban Gruin (11):
t6027: modernise tests
merge-one-file: rewrite in C
merge-index: libify merge_one_path() and merge_all()
merge-index: don't fork if the requested program is
`git-merge-one-file'
merge-resolve: rewrite in C
merge-recursive: move better_branch_name() to merge.c
merge-octopus: rewrite in C
merge: use the "resolve" strategy without forking
merge: use the "octopus" strategy without forking
sequencer: use the "resolve" strategy without forking
sequencer: use the "octopus" merge strategy without forking
Makefile | 7 +-
builtin.h | 3 +
builtin/merge-index.c | 102 ++----
builtin/merge-octopus.c | 65 ++++
builtin/merge-one-file.c | 85 +++++
builtin/merge-recursive.c | 16 +-
builtin/merge-resolve.c | 69 ++++
builtin/merge.c | 9 +-
cache.h | 2 +-
git-merge-octopus.sh | 112 ------
git-merge-one-file.sh | 167 ---------
git-merge-resolve.sh | 54 ---
git.c | 3 +
merge-strategies.c | 594 ++++++++++++++++++++++++++++++++
merge-strategies.h | 44 +++
merge.c | 12 +
sequencer.c | 16 +-
t/t6407-merge-binary.sh | 27 +-
t/t6415-merge-dir-to-symlink.sh | 2 +-
19 files changed, 942 insertions(+), 447 deletions(-)
create mode 100644 builtin/merge-octopus.c
create mode 100644 builtin/merge-one-file.c
create mode 100644 builtin/merge-resolve.c
delete mode 100755 git-merge-octopus.sh
delete mode 100755 git-merge-one-file.sh
delete mode 100755 git-merge-resolve.sh
create mode 100644 merge-strategies.c
create mode 100644 merge-strategies.h
Range-diff against v1:
1: 50e15b5243 ! 1: 28c8fd11b6 t6027: modernise tests
@@ Commit message
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
- ## t/t6027-merge-binary.sh ##
-@@ t/t6027-merge-binary.sh: test_description='ask merge-recursive to merge binary files'
+ ## t/t6407-merge-binary.sh ##
+@@ t/t6407-merge-binary.sh: test_description='ask merge-recursive to merge binary files'
. ./test-lib.sh
test_expect_success setup '
@@ t/t6027-merge-binary.sh: test_description='ask merge-recursive to merge binary f
cat "$TEST_DIRECTORY"/test-binary-1.png >m &&
git add m &&
git ls-files -s | sed -e "s/ 0 / 1 /" >E1 &&
-@@ t/t6027-merge-binary.sh: test_expect_success setup '
+@@ t/t6407-merge-binary.sh: test_expect_success setup '
'
test_expect_success resolve '
2: 08a337738e < -: ---------- merge-one-file: rewrite in C
3: 5da78d5de1 < -: ---------- merge-one-file: remove calls to external processes
4: 11c0da9e13 < -: ---------- merge-one-file: use error() instead of fprintf(stderr, ...)
5: df28965c8e < -: ---------- merge-one-file: libify merge_one_file()
-: ---------- > 2: f5ab0fdf0a merge-one-file: rewrite in C
6: 84f2f2946a ! 3: 7f3ce7da17 merge-index: libify merge_one_path() and merge_all()
@@ builtin/merge-index.c: int cmd_merge_index(int argc, const char **argv, const ch
## merge-strategies.c ##
@@
- #include "cache.h"
#include "dir.h"
+ #include "ll-merge.h"
#include "merge-strategies.h"
+#include "run-command.h"
#include "xdiff-interface.h"
@@ merge-strategies.c: int merge_strategies_one_file(struct repository *r,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data)
+{
-+ char ownbuf[3][60] = {{0}};
++ char ownbuf[3][GIT_MAX_HEXSZ] = {{0}};
+ const char *arguments[] = { (char *)data, "", "", "", path,
+ ownbuf[0], ownbuf[1], ownbuf[2],
+ NULL };
7: 1f864a4840 < -: ---------- merge-resolve: rewrite in C
8: 3517990e6a < -: ---------- merge-resolve: remove calls to external processes
9: 9831fe1729 < -: ---------- merge-resolve: libify merge_resolve()
-: ---------- > 4: 07e6a6aaef merge-index: don't fork if the requested program is `git-merge-one-file'
-: ---------- > 5: 117d4fc840 merge-resolve: rewrite in C
10: 99d42e8ea1 = 6: 4fc955962b merge-recursive: move better_branch_name() to merge.c
11: 3182673ea7 < -: ---------- merge-octopus: rewrite in C
12: 8f4cfcefb7 < -: ---------- merge-octopus: remove calls to external processes
13: d4dba22988 < -: ---------- merge-octopus: libify merge_octopus()
-: ---------- > 7: e7b9e15b34 merge-octopus: rewrite in C
14: bbe50cd770 = 8: cd0662201d merge: use the "resolve" strategy without forking
15: b7aff6fb3a = 9: 0525ff0183 merge: use the "octopus" strategy without forking
16: c1cdcce3a9 = 10: 6fbf599ba4 sequencer: use the "resolve" strategy without forking
17: e68765cdc7 = 11: 2c2dc3cc62 sequencer: use the "octopus" merge strategy without forking
--
2.28.0.370.g2c2dc3cc62
^ permalink raw reply [flat|nested] 221+ messages in thread
* [PATCH v2 01/11] t6027: modernise tests
2020-09-01 10:56 ` [PATCH v2 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
@ 2020-09-01 10:56 ` Alban Gruin
2020-09-01 10:56 ` [PATCH v2 02/11] merge-one-file: rewrite in C Alban Gruin
` (10 subsequent siblings)
11 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-09-01 10:56 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
Some tests in t6027 uses a if/then/else to check if a command failed or
not, but we have the `test_must_fail' function to do it correctly for us
nowadays.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
t/t6407-merge-binary.sh | 27 ++++++---------------------
1 file changed, 6 insertions(+), 21 deletions(-)
diff --git a/t/t6407-merge-binary.sh b/t/t6407-merge-binary.sh
index 4e6c7cb77e..071d3f7343 100755
--- a/t/t6407-merge-binary.sh
+++ b/t/t6407-merge-binary.sh
@@ -5,7 +5,6 @@ test_description='ask merge-recursive to merge binary files'
. ./test-lib.sh
test_expect_success setup '
-
cat "$TEST_DIRECTORY"/test-binary-1.png >m &&
git add m &&
git ls-files -s | sed -e "s/ 0 / 1 /" >E1 &&
@@ -35,33 +34,19 @@ test_expect_success setup '
'
test_expect_success resolve '
-
rm -f a* m* &&
git reset --hard anchor &&
-
- if git merge -s resolve master
- then
- echo Oops, should not have succeeded
- false
- else
- git ls-files -s >current
- test_cmp expect current
- fi
+ test_must_fail git merge -s resolve master &&
+ git ls-files -s >current &&
+ test_cmp expect current
'
test_expect_success recursive '
-
rm -f a* m* &&
git reset --hard anchor &&
-
- if git merge -s recursive master
- then
- echo Oops, should not have succeeded
- false
- else
- git ls-files -s >current
- test_cmp expect current
- fi
+ test_must_fail git merge -s recursive master &&
+ git ls-files -s >current &&
+ test_cmp expect current
'
test_done
--
2.28.0.370.g2c2dc3cc62
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v2 02/11] merge-one-file: rewrite in C
2020-09-01 10:56 ` [PATCH v2 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
2020-09-01 10:56 ` [PATCH v2 01/11] t6027: modernise tests Alban Gruin
@ 2020-09-01 10:56 ` Alban Gruin
2020-09-01 21:06 ` Junio C Hamano
2020-09-01 10:56 ` [PATCH v2 03/11] merge-index: libify merge_one_path() and merge_all() Alban Gruin
` (9 subsequent siblings)
11 siblings, 1 reply; 221+ messages in thread
From: Alban Gruin @ 2020-09-01 10:56 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
This rewrites `git merge-one-file' from shell to C. This port is not
completely straightforward: to save precious cycles by avoiding reading
and flushing the index repeatedly, write temporary files when an
operation can be performed in-memory, or allow other function to use the
rewrite without forking nor worrying about the index, the calls to
external processes are replaced by calls to functions in libgit.a:
- calls to `update-index --add --cacheinfo' are replaced by calls to
add_cache_entry();
- calls to `update-index --remove' are replaced by calls to
remove_file_from_cache();
- calls to `checkout-index -u -f' are replaced by calls to
checkout_entry();
- calls to `unpack-file' and `merge-files' are replaced by calls to
read_mmblob() and ll_merge(), respectively, to merge files
in-memory;
- calls to `checkout-index -f --stage=2' are replaced by calls to
cache_file_exists();
- calls to `update-index' are replaced by calls to add_file_to_cache().
The bulk of the rewrite is done in a new file in libgit.a,
merge-strategies.c. This will enable the resolve and octopus strategies
to directly call it instead of forking.
This also fixes a bug present in the original script: instead of
checking if a _regular_ file exists when a file exists in the branch to
merge, but not in our branch, the rewritten version checks if a file of
any kind (ie. a directory, ...) exists. This fixes the tests t6035.14,
where the branch to merge had a new file, `a/b', but our branch had a
directory there; it should have failed because a directory exists, but
it did not because there was no regular file called `a/b'. This test is
now marked as successful.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Makefile | 3 +-
builtin.h | 1 +
builtin/merge-one-file.c | 85 ++++++++++++++
git-merge-one-file.sh | 167 ---------------------------
git.c | 1 +
merge-strategies.c | 199 ++++++++++++++++++++++++++++++++
merge-strategies.h | 13 +++
t/t6415-merge-dir-to-symlink.sh | 2 +-
8 files changed, 302 insertions(+), 169 deletions(-)
create mode 100644 builtin/merge-one-file.c
delete mode 100755 git-merge-one-file.sh
create mode 100644 merge-strategies.c
create mode 100644 merge-strategies.h
diff --git a/Makefile b/Makefile
index 65f8cfb236..8849d54063 100644
--- a/Makefile
+++ b/Makefile
@@ -596,7 +596,6 @@ SCRIPT_SH += git-bisect.sh
SCRIPT_SH += git-difftool--helper.sh
SCRIPT_SH += git-filter-branch.sh
SCRIPT_SH += git-merge-octopus.sh
-SCRIPT_SH += git-merge-one-file.sh
SCRIPT_SH += git-merge-resolve.sh
SCRIPT_SH += git-mergetool.sh
SCRIPT_SH += git-quiltimport.sh
@@ -911,6 +910,7 @@ LIB_OBJS += match-trees.o
LIB_OBJS += mem-pool.o
LIB_OBJS += merge-blobs.o
LIB_OBJS += merge-recursive.o
+LIB_OBJS += merge-strategies.o
LIB_OBJS += merge.o
LIB_OBJS += mergesort.o
LIB_OBJS += midx.o
@@ -1089,6 +1089,7 @@ BUILTIN_OBJS += builtin/mailsplit.o
BUILTIN_OBJS += builtin/merge-base.o
BUILTIN_OBJS += builtin/merge-file.o
BUILTIN_OBJS += builtin/merge-index.o
+BUILTIN_OBJS += builtin/merge-one-file.o
BUILTIN_OBJS += builtin/merge-ours.o
BUILTIN_OBJS += builtin/merge-recursive.o
BUILTIN_OBJS += builtin/merge-tree.o
diff --git a/builtin.h b/builtin.h
index a5ae15bfe5..9205d5ecdc 100644
--- a/builtin.h
+++ b/builtin.h
@@ -172,6 +172,7 @@ int cmd_merge_base(int argc, const char **argv, const char *prefix);
int cmd_merge_index(int argc, const char **argv, const char *prefix);
int cmd_merge_ours(int argc, const char **argv, const char *prefix);
int cmd_merge_file(int argc, const char **argv, const char *prefix);
+int cmd_merge_one_file(int argc, const char **argv, const char *prefix);
int cmd_merge_recursive(int argc, const char **argv, const char *prefix);
int cmd_merge_tree(int argc, const char **argv, const char *prefix);
int cmd_mktag(int argc, const char **argv, const char *prefix);
diff --git a/builtin/merge-one-file.c b/builtin/merge-one-file.c
new file mode 100644
index 0000000000..306a86c2f0
--- /dev/null
+++ b/builtin/merge-one-file.c
@@ -0,0 +1,85 @@
+/*
+ * Builtin "git merge-one-file"
+ *
+ * Copyright (c) 2020 Alban Gruin
+ *
+ * Based on git-merge-one-file.sh, written by Linus Torvalds.
+ *
+ * This is the git per-file merge utility, called with
+ *
+ * argv[1] - original file SHA1 (or empty)
+ * argv[2] - file in branch1 SHA1 (or empty)
+ * argv[3] - file in branch2 SHA1 (or empty)
+ * argv[4] - pathname in repository
+ * argv[5] - original file mode (or empty)
+ * argv[6] - file in branch1 mode (or empty)
+ * argv[7] - file in branch2 mode (or empty)
+ *
+ * Handle some trivial cases. The _really_ trivial cases have been
+ * handled already by git read-tree, but that one doesn't do any merges
+ * that might change the tree layout.
+ */
+
+#include "cache.h"
+#include "builtin.h"
+#include "lockfile.h"
+#include "merge-strategies.h"
+
+static const char builtin_merge_one_file_usage[] =
+ "git merge-one-file <orig blob> <our blob> <their blob> <path> "
+ "<orig mode> <our mode> <their mode>\n\n"
+ "Blob ids and modes should be empty for missing files.";
+
+int cmd_merge_one_file(int argc, const char **argv, const char *prefix)
+{
+ struct object_id orig_blob, our_blob, their_blob,
+ *p_orig_blob = NULL, *p_our_blob = NULL, *p_their_blob = NULL;
+ unsigned int orig_mode = 0, our_mode = 0, their_mode = 0, ret = 0;
+ struct lock_file lock = LOCK_INIT;
+
+ if (argc != 8)
+ usage(builtin_merge_one_file_usage);
+
+ if (repo_read_index(the_repository) < 0)
+ die("invalid index");
+
+ repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
+
+ if (!get_oid(argv[1], &orig_blob)) {
+ p_orig_blob = &orig_blob;
+ orig_mode = strtol(argv[5], NULL, 8);
+
+ if (!(S_ISREG(orig_mode) || S_ISDIR(orig_mode) || S_ISLNK(orig_mode)))
+ ret |= error(_("invalid 'orig' mode: %o"), orig_mode);
+ }
+
+ if (!get_oid(argv[2], &our_blob)) {
+ p_our_blob = &our_blob;
+ our_mode = strtol(argv[6], NULL, 8);
+
+ if (!(S_ISREG(our_mode) || S_ISDIR(our_mode) || S_ISLNK(our_mode)))
+ ret |= error(_("invalid 'our' mode: %o"), our_mode);
+ }
+
+ if (!get_oid(argv[3], &their_blob)) {
+ p_their_blob = &their_blob;
+ their_mode = strtol(argv[7], NULL, 8);
+
+ if (!(S_ISREG(their_mode) || S_ISDIR(their_mode) || S_ISLNK(their_mode)))
+ ret = error(_("invalid 'their' mode: %o"), their_mode);
+ }
+
+ if (ret)
+ return ret;
+
+ ret = merge_strategies_one_file(the_repository,
+ p_orig_blob, p_our_blob, p_their_blob, argv[4],
+ orig_mode, our_mode, their_mode);
+
+ if (ret) {
+ rollback_lock_file(&lock);
+ return ret;
+ }
+
+ return write_locked_index(the_repository->index, &lock, COMMIT_LOCK);
+}
diff --git a/git-merge-one-file.sh b/git-merge-one-file.sh
deleted file mode 100755
index f6d9852d2f..0000000000
--- a/git-merge-one-file.sh
+++ /dev/null
@@ -1,167 +0,0 @@
-#!/bin/sh
-#
-# Copyright (c) Linus Torvalds, 2005
-#
-# This is the git per-file merge script, called with
-#
-# $1 - original file SHA1 (or empty)
-# $2 - file in branch1 SHA1 (or empty)
-# $3 - file in branch2 SHA1 (or empty)
-# $4 - pathname in repository
-# $5 - original file mode (or empty)
-# $6 - file in branch1 mode (or empty)
-# $7 - file in branch2 mode (or empty)
-#
-# Handle some trivial cases.. The _really_ trivial cases have
-# been handled already by git read-tree, but that one doesn't
-# do any merges that might change the tree layout.
-
-USAGE='<orig blob> <our blob> <their blob> <path>'
-USAGE="$USAGE <orig mode> <our mode> <their mode>"
-LONG_USAGE="usage: git merge-one-file $USAGE
-
-Blob ids and modes should be empty for missing files."
-
-SUBDIRECTORY_OK=Yes
-. git-sh-setup
-cd_to_toplevel
-require_work_tree
-
-if test $# != 7
-then
- echo "$LONG_USAGE"
- exit 1
-fi
-
-case "${1:-.}${2:-.}${3:-.}" in
-#
-# Deleted in both or deleted in one and unchanged in the other
-#
-"$1.." | "$1.$1" | "$1$1.")
- if { test -z "$6" && test "$5" != "$7"; } ||
- { test -z "$7" && test "$5" != "$6"; }
- then
- echo "ERROR: File $4 deleted on one branch but had its" >&2
- echo "ERROR: permissions changed on the other." >&2
- exit 1
- fi
-
- if test -n "$2"
- then
- echo "Removing $4"
- else
- # read-tree checked that index matches HEAD already,
- # so we know we do not have this path tracked.
- # there may be an unrelated working tree file here,
- # which we should just leave unmolested. Make sure
- # we do not have it in the index, though.
- exec git update-index --remove -- "$4"
- fi
- if test -f "$4"
- then
- rm -f -- "$4" &&
- rmdir -p "$(expr "z$4" : 'z\(.*\)/')" 2>/dev/null || :
- fi &&
- exec git update-index --remove -- "$4"
- ;;
-
-#
-# Added in one.
-#
-".$2.")
- # the other side did not add and we added so there is nothing
- # to be done, except making the path merged.
- exec git update-index --add --cacheinfo "$6" "$2" "$4"
- ;;
-"..$3")
- echo "Adding $4"
- if test -f "$4"
- then
- echo "ERROR: untracked $4 is overwritten by the merge." >&2
- exit 1
- fi
- git update-index --add --cacheinfo "$7" "$3" "$4" &&
- exec git checkout-index -u -f -- "$4"
- ;;
-
-#
-# Added in both, identically (check for same permissions).
-#
-".$3$2")
- if test "$6" != "$7"
- then
- echo "ERROR: File $4 added identically in both branches," >&2
- echo "ERROR: but permissions conflict $6->$7." >&2
- exit 1
- fi
- echo "Adding $4"
- git update-index --add --cacheinfo "$6" "$2" "$4" &&
- exec git checkout-index -u -f -- "$4"
- ;;
-
-#
-# Modified in both, but differently.
-#
-"$1$2$3" | ".$2$3")
-
- case ",$6,$7," in
- *,120000,*)
- echo "ERROR: $4: Not merging symbolic link changes." >&2
- exit 1
- ;;
- *,160000,*)
- echo "ERROR: $4: Not merging conflicting submodule changes." >&2
- exit 1
- ;;
- esac
-
- src1=$(git unpack-file $2)
- src2=$(git unpack-file $3)
- case "$1" in
- '')
- echo "Added $4 in both, but differently."
- orig=$(git unpack-file $(git hash-object /dev/null))
- ;;
- *)
- echo "Auto-merging $4"
- orig=$(git unpack-file $1)
- ;;
- esac
-
- git merge-file "$src1" "$orig" "$src2"
- ret=$?
- msg=
- if test $ret != 0 || test -z "$1"
- then
- msg='content conflict'
- ret=1
- fi
-
- # Create the working tree file, using "our tree" version from the
- # index, and then store the result of the merge.
- git checkout-index -f --stage=2 -- "$4" && cat "$src1" >"$4" || exit 1
- rm -f -- "$orig" "$src1" "$src2"
-
- if test "$6" != "$7"
- then
- if test -n "$msg"
- then
- msg="$msg, "
- fi
- msg="${msg}permissions conflict: $5->$6,$7"
- ret=1
- fi
-
- if test $ret != 0
- then
- echo "ERROR: $msg in $4" >&2
- exit 1
- fi
- exec git update-index -- "$4"
- ;;
-
-*)
- echo "ERROR: $4: Not handling case $1 -> $2 -> $3" >&2
- ;;
-esac
-exit 1
diff --git a/git.c b/git.c
index 8bd1d7551d..c97fea36c1 100644
--- a/git.c
+++ b/git.c
@@ -534,6 +534,7 @@ static struct cmd_struct commands[] = {
{ "merge-file", cmd_merge_file, RUN_SETUP_GENTLY },
{ "merge-index", cmd_merge_index, RUN_SETUP | NO_PARSEOPT },
{ "merge-ours", cmd_merge_ours, RUN_SETUP | NO_PARSEOPT },
+ { "merge-one-file", cmd_merge_one_file, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-ours", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-theirs", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
diff --git a/merge-strategies.c b/merge-strategies.c
new file mode 100644
index 0000000000..f2af4a894d
--- /dev/null
+++ b/merge-strategies.c
@@ -0,0 +1,199 @@
+#include "cache.h"
+#include "dir.h"
+#include "ll-merge.h"
+#include "merge-strategies.h"
+#include "xdiff-interface.h"
+
+static int add_to_index_cacheinfo(struct index_state *istate,
+ unsigned int mode,
+ const struct object_id *oid, const char *path)
+{
+ struct cache_entry *ce;
+ int len, option;
+
+ if (!verify_path(path, mode))
+ return error(_("Invalid path '%s'"), path);
+
+ len = strlen(path);
+ ce = make_empty_cache_entry(istate, len);
+
+ oidcpy(&ce->oid, oid);
+ memcpy(ce->name, path, len);
+ ce->ce_flags = create_ce_flags(0);
+ ce->ce_namelen = len;
+ ce->ce_mode = create_ce_mode(mode);
+ if (assume_unchanged)
+ ce->ce_flags |= CE_VALID;
+ option = ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE;
+ if (add_index_entry(istate, ce, option))
+ return error(_("%s: cannot add to the index"), path);
+
+ return 0;
+}
+
+static int checkout_from_index(struct index_state *istate, const char *path)
+{
+ struct checkout state = CHECKOUT_INIT;
+ struct cache_entry *ce;
+
+ state.istate = istate;
+ state.force = 1;
+ state.base_dir = "";
+ state.base_dir_len = 0;
+
+ ce = index_file_exists(istate, path, strlen(path), 0);
+ if (checkout_entry(ce, &state, NULL, NULL) < 0)
+ return error(_("%s: cannot checkout file"), path);
+ return 0;
+}
+
+static int merge_one_file_deleted(struct index_state *istate,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
+{
+ if ((our_blob && orig_mode != our_mode) ||
+ (their_blob && orig_mode != their_mode))
+ return error(_("File %s deleted on one branch but had its "
+ "permissions changed on the other."), path);
+
+ if (our_blob) {
+ printf(_("Removing %s\n"), path);
+
+ if (file_exists(path))
+ remove_path(path);
+ }
+
+ if (remove_file_from_index(istate, path))
+ return error("%s: cannot remove from the index", path);
+ return 0;
+}
+
+static int do_merge_one_file(struct index_state *istate,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
+{
+ int ret, i, dest;
+ mmbuffer_t result = {NULL, 0};
+ mmfile_t mmfs[3];
+ struct ll_merge_options merge_opts = {0};
+ struct cache_entry *ce;
+
+ if (our_mode == S_IFLNK || their_mode == S_IFLNK)
+ return error(_("%s: Not merging symbolic link changes."), path);
+ else if (our_mode == S_IFGITLINK || their_mode == S_IFGITLINK)
+ return error(_("%s: Not merging conflicting submodule changes."), path);
+
+ read_mmblob(mmfs + 1, our_blob);
+ read_mmblob(mmfs + 2, their_blob);
+
+ if (orig_blob) {
+ printf(_("Auto-merging %s\n"), path);
+ read_mmblob(mmfs + 0, orig_blob);
+ } else {
+ printf(_("Added %s in both, but differently.\n"), path);
+ read_mmblob(mmfs + 0, &null_oid);
+ }
+
+ merge_opts.xdl_opts = XDL_MERGE_ZEALOUS_ALNUM;
+ ret = ll_merge(&result, path,
+ mmfs + 0, "orig",
+ mmfs + 1, "our",
+ mmfs + 2, "their",
+ istate, &merge_opts);
+
+ for (i = 0; i < 3; i++)
+ free(mmfs[i].ptr);
+
+ if (ret > 127 || !orig_blob)
+ ret = error(_("content conflict in %s"), path);
+
+ /* Create the working tree file, using "our tree" version from
+ the index, and then store the result of the merge. */
+ ce = index_file_exists(istate, path, strlen(path), 0);
+ if (!ce)
+ BUG("file is not present in the cache?");
+
+ unlink(path);
+ dest = open(path, O_WRONLY | O_CREAT, ce->ce_mode);
+ write_in_full(dest, result.ptr, result.size);
+ close(dest);
+
+ free(result.ptr);
+
+ if (ret && our_mode != their_mode)
+ return error(_("permission conflict: %o->%o,%o in %s"),
+ orig_mode, our_mode, their_mode, path);
+ if (ret)
+ return 1;
+
+ return add_file_to_index(istate, path, 0);
+}
+
+int merge_strategies_one_file(struct repository *r,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode,
+ unsigned int their_mode)
+{
+ if (orig_blob &&
+ ((!their_blob && our_blob && oideq(orig_blob, our_blob)) ||
+ (!our_blob && their_blob && oideq(orig_blob, their_blob))))
+ /* Deleted in both or deleted in one and unchanged in
+ the other */
+ return merge_one_file_deleted(r->index,
+ orig_blob, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+ else if (!orig_blob && our_blob && !their_blob) {
+ /* Added in one. The other side did not add and we
+ added so there is nothing to be done, except making
+ the path merged. */
+ return add_to_index_cacheinfo(r->index, our_mode, our_blob, path);
+ } else if (!orig_blob && !our_blob && their_blob) {
+ printf(_("Adding %s\n"), path);
+
+ if (file_exists(path))
+ return error(_("untracked %s is overwritten by the merge."), path);
+
+ if (add_to_index_cacheinfo(r->index, their_mode, their_blob, path))
+ return 1;
+ return checkout_from_index(r->index, path);
+ } else if (!orig_blob && our_blob && their_blob &&
+ oideq(our_blob, their_blob)) {
+ /* Added in both, identically (check for same
+ permissions). */
+ if (our_mode != their_mode)
+ return error(_("File %s added identically in both branches, "
+ "but permissions conflict %o->%o."),
+ path, our_mode, their_mode);
+
+ printf(_("Adding %s\n"), path);
+
+ if (add_to_index_cacheinfo(r->index, our_mode, our_blob, path))
+ return 1;
+ return checkout_from_index(r->index, path);
+ } else if (our_blob && their_blob)
+ /* Modified in both, but differently. */
+ return do_merge_one_file(r->index,
+ orig_blob, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+ else {
+ char *orig_hex = "", *our_hex = "", *their_hex = "";
+
+ if (orig_blob)
+ orig_hex = oid_to_hex(orig_blob);
+ if (our_blob)
+ our_hex = oid_to_hex(our_blob);
+ if (their_blob)
+ their_hex = oid_to_hex(their_blob);
+
+ return error(_("%s: Not handling case %s -> %s -> %s"),
+ path, orig_hex, our_hex, their_hex);
+ }
+
+ return 0;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
new file mode 100644
index 0000000000..b527d145c7
--- /dev/null
+++ b/merge-strategies.h
@@ -0,0 +1,13 @@
+#ifndef MERGE_STRATEGIES_H
+#define MERGE_STRATEGIES_H
+
+#include "object.h"
+
+int merge_strategies_one_file(struct repository *r,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode,
+ unsigned int their_mode);
+
+#endif /* MERGE_STRATEGIES_H */
diff --git a/t/t6415-merge-dir-to-symlink.sh b/t/t6415-merge-dir-to-symlink.sh
index 2eddcc7664..5fb74e39a0 100755
--- a/t/t6415-merge-dir-to-symlink.sh
+++ b/t/t6415-merge-dir-to-symlink.sh
@@ -94,7 +94,7 @@ test_expect_success SYMLINKS 'a/b was resolved as symlink' '
test -h a/b
'
-test_expect_failure 'do not lose untracked in merge (resolve)' '
+test_expect_success 'do not lose untracked in merge (resolve)' '
git reset --hard &&
git checkout baseline^0 &&
>a/b/c/e &&
--
2.28.0.370.g2c2dc3cc62
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v2 03/11] merge-index: libify merge_one_path() and merge_all()
2020-09-01 10:56 ` [PATCH v2 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
2020-09-01 10:56 ` [PATCH v2 01/11] t6027: modernise tests Alban Gruin
2020-09-01 10:56 ` [PATCH v2 02/11] merge-one-file: rewrite in C Alban Gruin
@ 2020-09-01 10:56 ` Alban Gruin
2020-09-01 21:11 ` Junio C Hamano
2020-09-01 10:56 ` [PATCH v2 04/11] merge-index: don't fork if the requested program is `git-merge-one-file' Alban Gruin
` (8 subsequent siblings)
11 siblings, 1 reply; 221+ messages in thread
From: Alban Gruin @ 2020-09-01 10:56 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
The "resolve" and "octopus" merge strategies do not call directly `git
merge-one-file', they delegate the work to another git command, `git
merge-index', that will loop over files in the index and call the
specified command. Unfortunately, these functions are not part of
libgit.a, which means that once rewritten, the strategies would still
have to invoke `merge-one-file' by spawning a new process first.
To avoid this, this moves merge_one_path(), merge_all(), and their
helpers to merge-strategies.c. They also take a callback to dictate
what they should do for each file. For now, only one launching a new
process is defined to preserve the behaviour of the builtin version.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge-index.c | 77 +++------------------------------
merge-strategies.c | 99 +++++++++++++++++++++++++++++++++++++++++++
merge-strategies.h | 17 ++++++++
3 files changed, 123 insertions(+), 70 deletions(-)
diff --git a/builtin/merge-index.c b/builtin/merge-index.c
index 38ea6ad6ca..6cb666cc78 100644
--- a/builtin/merge-index.c
+++ b/builtin/merge-index.c
@@ -1,74 +1,11 @@
#define USE_THE_INDEX_COMPATIBILITY_MACROS
#include "builtin.h"
-#include "run-command.h"
-
-static const char *pgm;
-static int one_shot, quiet;
-static int err;
-
-static int merge_entry(int pos, const char *path)
-{
- int found;
- const char *arguments[] = { pgm, "", "", "", path, "", "", "", NULL };
- char hexbuf[4][GIT_MAX_HEXSZ + 1];
- char ownbuf[4][60];
-
- if (pos >= active_nr)
- die("git merge-index: %s not in the cache", path);
- found = 0;
- do {
- const struct cache_entry *ce = active_cache[pos];
- int stage = ce_stage(ce);
-
- if (strcmp(ce->name, path))
- break;
- found++;
- oid_to_hex_r(hexbuf[stage], &ce->oid);
- xsnprintf(ownbuf[stage], sizeof(ownbuf[stage]), "%o", ce->ce_mode);
- arguments[stage] = hexbuf[stage];
- arguments[stage + 4] = ownbuf[stage];
- } while (++pos < active_nr);
- if (!found)
- die("git merge-index: %s not in the cache", path);
-
- if (run_command_v_opt(arguments, 0)) {
- if (one_shot)
- err++;
- else {
- if (!quiet)
- die("merge program failed");
- exit(1);
- }
- }
- return found;
-}
-
-static void merge_one_path(const char *path)
-{
- int pos = cache_name_pos(path, strlen(path));
-
- /*
- * If it already exists in the cache as stage0, it's
- * already merged and there is nothing to do.
- */
- if (pos < 0)
- merge_entry(-pos-1, path);
-}
-
-static void merge_all(void)
-{
- int i;
- for (i = 0; i < active_nr; i++) {
- const struct cache_entry *ce = active_cache[i];
- if (!ce_stage(ce))
- continue;
- i += merge_entry(i, ce->name)-1;
- }
-}
+#include "merge-strategies.h"
int cmd_merge_index(int argc, const char **argv, const char *prefix)
{
- int i, force_file = 0;
+ int i, force_file = 0, err = 0, one_shot = 0, quiet = 0;
+ const char *pgm;
/* Without this we cannot rely on waitpid() to tell
* what happened to our children.
@@ -98,14 +35,14 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
continue;
}
if (!strcmp(arg, "-a")) {
- merge_all();
+ err |= merge_all(&the_index, one_shot, quiet,
+ merge_program_cb, (void *)pgm);
continue;
}
die("git merge-index: unknown option %s", arg);
}
- merge_one_path(arg);
+ err |= merge_one_path(&the_index, one_shot, quiet, arg,
+ merge_program_cb, (void *)pgm);
}
- if (err && !quiet)
- die("merge program failed");
return err;
}
diff --git a/merge-strategies.c b/merge-strategies.c
index f2af4a894d..ffd6cf77d6 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -2,6 +2,7 @@
#include "dir.h"
#include "ll-merge.h"
#include "merge-strategies.h"
+#include "run-command.h"
#include "xdiff-interface.h"
static int add_to_index_cacheinfo(struct index_state *istate,
@@ -197,3 +198,101 @@ int merge_strategies_one_file(struct repository *r,
return 0;
}
+
+int merge_program_cb(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data)
+{
+ char ownbuf[3][GIT_MAX_HEXSZ] = {{0}};
+ const char *arguments[] = { (char *)data, "", "", "", path,
+ ownbuf[0], ownbuf[1], ownbuf[2],
+ NULL };
+
+ if (orig_blob)
+ arguments[1] = oid_to_hex(orig_blob);
+ if (our_blob)
+ arguments[2] = oid_to_hex(our_blob);
+ if (their_blob)
+ arguments[3] = oid_to_hex(their_blob);
+
+ xsnprintf(ownbuf[0], sizeof(ownbuf[0]), "%o", orig_mode);
+ xsnprintf(ownbuf[1], sizeof(ownbuf[1]), "%o", our_mode);
+ xsnprintf(ownbuf[2], sizeof(ownbuf[2]), "%o", their_mode);
+
+ return run_command_v_opt(arguments, 0);
+}
+
+static int merge_entry(struct index_state *istate, int quiet, int pos,
+ const char *path, merge_cb cb, void *data)
+{
+ int found = 0;
+ const struct object_id *oids[3] = {NULL};
+ unsigned int modes[3] = {0};
+
+ do {
+ const struct cache_entry *ce = istate->cache[pos];
+ int stage = ce_stage(ce);
+
+ if (strcmp(ce->name, path))
+ break;
+ found++;
+ oids[stage - 1] = &ce->oid;
+ modes[stage - 1] = ce->ce_mode;
+ } while (++pos < istate->cache_nr);
+ if (!found)
+ return error(_("%s is not in the cache"), path);
+
+ if (cb(oids[0], oids[1], oids[2], path, modes[0], modes[1], modes[2], data)) {
+ if (!quiet)
+ error(_("Merge program failed"));
+ return -2;
+ }
+
+ return found;
+}
+
+int merge_one_path(struct index_state *istate, int oneshot, int quiet,
+ const char *path, merge_cb cb, void *data)
+{
+ int pos = index_name_pos(istate, path, strlen(path)), ret;
+
+ /*
+ * If it already exists in the cache as stage0, it's
+ * already merged and there is nothing to do.
+ */
+ if (pos < 0) {
+ ret = merge_entry(istate, quiet, -pos - 1, path, cb, data);
+ if (ret == -1)
+ return -1;
+ else if (ret == -2)
+ return 1;
+ }
+ return 0;
+}
+
+int merge_all(struct index_state *istate, int oneshot, int quiet,
+ merge_cb cb, void *data)
+{
+ int err = 0, i, ret;
+ for (i = 0; i < istate->cache_nr; i++) {
+ const struct cache_entry *ce = istate->cache[i];
+ if (!ce_stage(ce))
+ continue;
+
+ ret = merge_entry(istate, quiet, i, ce->name, cb, data);
+ if (ret > 0)
+ i += ret - 1;
+ else if (ret == -1)
+ return -1;
+ else if (ret == -2) {
+ if (oneshot)
+ err++;
+ else
+ return 1;
+ }
+ }
+
+ return err;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
index b527d145c7..cf78d7eaf4 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -10,4 +10,21 @@ int merge_strategies_one_file(struct repository *r,
unsigned int orig_mode, unsigned int our_mode,
unsigned int their_mode);
+typedef int (*merge_cb)(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
+int merge_program_cb(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
+int merge_one_path(struct index_state *istate, int oneshot, int quiet,
+ const char *path, merge_cb cb, void *data);
+int merge_all(struct index_state *istate, int oneshot, int quiet,
+ merge_cb cb, void *data);
+
#endif /* MERGE_STRATEGIES_H */
--
2.28.0.370.g2c2dc3cc62
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v2 04/11] merge-index: don't fork if the requested program is `git-merge-one-file'
2020-09-01 10:56 ` [PATCH v2 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (2 preceding siblings ...)
2020-09-01 10:56 ` [PATCH v2 03/11] merge-index: libify merge_one_path() and merge_all() Alban Gruin
@ 2020-09-01 10:56 ` Alban Gruin
2020-09-01 10:56 ` [PATCH v2 05/11] merge-resolve: rewrite in C Alban Gruin
` (7 subsequent siblings)
11 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-09-01 10:56 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
Since `git-merge-one-file' has been rewritten and libified, this teaches
`merge-index' to call merge_strategies_one_file() without forking using
a new callback, merge_one_file_cb().
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge-index.c | 29 +++++++++++++++++++++++++++--
merge-strategies.c | 11 +++++++++++
merge-strategies.h | 6 ++++++
3 files changed, 44 insertions(+), 2 deletions(-)
diff --git a/builtin/merge-index.c b/builtin/merge-index.c
index 6cb666cc78..19fff9a113 100644
--- a/builtin/merge-index.c
+++ b/builtin/merge-index.c
@@ -1,11 +1,15 @@
#define USE_THE_INDEX_COMPATIBILITY_MACROS
#include "builtin.h"
+#include "lockfile.h"
#include "merge-strategies.h"
int cmd_merge_index(int argc, const char **argv, const char *prefix)
{
int i, force_file = 0, err = 0, one_shot = 0, quiet = 0;
const char *pgm;
+ void *data;
+ merge_cb merge_action;
+ struct lock_file lock = LOCK_INIT;
/* Without this we cannot rely on waitpid() to tell
* what happened to our children.
@@ -26,7 +30,19 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
quiet = 1;
i++;
}
+
pgm = argv[i++];
+ if (!strcmp(pgm, "git-merge-one-file")) {
+ merge_action = merge_one_file_cb;
+ data = (void *)the_repository;
+
+ setup_work_tree();
+ hold_locked_index(&lock, LOCK_DIE_ON_ERROR);
+ } else {
+ merge_action = merge_program_cb;
+ data = (void *)pgm;
+ }
+
for (; i < argc; i++) {
const char *arg = argv[i];
if (!force_file && *arg == '-') {
@@ -36,13 +52,22 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
}
if (!strcmp(arg, "-a")) {
err |= merge_all(&the_index, one_shot, quiet,
- merge_program_cb, (void *)pgm);
+ merge_action, data);
continue;
}
die("git merge-index: unknown option %s", arg);
}
err |= merge_one_path(&the_index, one_shot, quiet, arg,
- merge_program_cb, (void *)pgm);
+ merge_action, data);
+ }
+
+ if (merge_action == merge_one_file_cb) {
+ if (err) {
+ rollback_lock_file(&lock);
+ return err;
+ }
+
+ return write_locked_index(&the_index, &lock, COMMIT_LOCK);
}
return err;
}
diff --git a/merge-strategies.c b/merge-strategies.c
index ffd6cf77d6..00738863e4 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -199,6 +199,17 @@ int merge_strategies_one_file(struct repository *r,
return 0;
}
+int merge_one_file_cb(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data)
+{
+ return merge_strategies_one_file((struct repository *)data,
+ orig_blob, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+}
+
int merge_program_cb(const struct object_id *orig_blob,
const struct object_id *our_blob,
const struct object_id *their_blob, const char *path,
diff --git a/merge-strategies.h b/merge-strategies.h
index cf78d7eaf4..40e175ca39 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -16,6 +16,12 @@ typedef int (*merge_cb)(const struct object_id *orig_blob,
unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
void *data);
+int merge_one_file_cb(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
int merge_program_cb(const struct object_id *orig_blob,
const struct object_id *our_blob,
const struct object_id *their_blob, const char *path,
--
2.28.0.370.g2c2dc3cc62
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v2 05/11] merge-resolve: rewrite in C
2020-09-01 10:56 ` [PATCH v2 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (3 preceding siblings ...)
2020-09-01 10:56 ` [PATCH v2 04/11] merge-index: don't fork if the requested program is `git-merge-one-file' Alban Gruin
@ 2020-09-01 10:56 ` Alban Gruin
2020-09-01 10:57 ` [PATCH v2 06/11] merge-recursive: move better_branch_name() to merge.c Alban Gruin
` (6 subsequent siblings)
11 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-09-01 10:56 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
This rewrites `git merge-resolve' from shell to C. As for `git
merge-one-file', this port is not completely straightforward and removes
calls to external processes to avoid reading and writing the index over
and over again.
- The call to `update-index -q --refresh' is replaced by a call to
refresh_index().
- The call to `read-tree' is replaced by a call to unpack_trees() (and
all the setup needed).
- The call to `write-tree' is replaced by a call to
write_index_as_tree().
- The call to `merge-index', needed to invoke `git merge-one-file', is
replaced by a call to the new merge_all() function. A callback
function, merge_one_file_cb(), is added to allow it to call
merge_one_file() without forking.
Here too, the index is read in cmd_merge_resolve(), but
merge_strategies_resolve() takes care of writing it back to the disk.
The parameters of merge_strategies_resolve() will be surprising at first
glance: why using a commit list for `bases' and `remote', where we could
use an oid array, and a pointer to an oid? Because, in a later commit,
try_merge_strategy() will be able to call merge_strategies_resolve()
directly, and it already uses a commit list for `bases' (`common') and
`remote' (`remoteheads'), and a string for `head_arg'. To reduce
frictions later, merge_strategies_resolve() takes the same types of
parameters.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Makefile | 2 +-
builtin.h | 1 +
builtin/merge-resolve.c | 69 +++++++++++++++++++++++++++++++++
git-merge-resolve.sh | 54 --------------------------
git.c | 1 +
merge-strategies.c | 85 +++++++++++++++++++++++++++++++++++++++++
merge-strategies.h | 5 +++
7 files changed, 162 insertions(+), 55 deletions(-)
create mode 100644 builtin/merge-resolve.c
delete mode 100755 git-merge-resolve.sh
diff --git a/Makefile b/Makefile
index 8849d54063..929c3dc3eb 100644
--- a/Makefile
+++ b/Makefile
@@ -596,7 +596,6 @@ SCRIPT_SH += git-bisect.sh
SCRIPT_SH += git-difftool--helper.sh
SCRIPT_SH += git-filter-branch.sh
SCRIPT_SH += git-merge-octopus.sh
-SCRIPT_SH += git-merge-resolve.sh
SCRIPT_SH += git-mergetool.sh
SCRIPT_SH += git-quiltimport.sh
SCRIPT_SH += git-request-pull.sh
@@ -1092,6 +1091,7 @@ BUILTIN_OBJS += builtin/merge-index.o
BUILTIN_OBJS += builtin/merge-one-file.o
BUILTIN_OBJS += builtin/merge-ours.o
BUILTIN_OBJS += builtin/merge-recursive.o
+BUILTIN_OBJS += builtin/merge-resolve.o
BUILTIN_OBJS += builtin/merge-tree.o
BUILTIN_OBJS += builtin/merge.o
BUILTIN_OBJS += builtin/mktag.o
diff --git a/builtin.h b/builtin.h
index 9205d5ecdc..6ea207c9fd 100644
--- a/builtin.h
+++ b/builtin.h
@@ -174,6 +174,7 @@ int cmd_merge_ours(int argc, const char **argv, const char *prefix);
int cmd_merge_file(int argc, const char **argv, const char *prefix);
int cmd_merge_one_file(int argc, const char **argv, const char *prefix);
int cmd_merge_recursive(int argc, const char **argv, const char *prefix);
+int cmd_merge_resolve(int argc, const char **argv, const char *prefix);
int cmd_merge_tree(int argc, const char **argv, const char *prefix);
int cmd_mktag(int argc, const char **argv, const char *prefix);
int cmd_mktree(int argc, const char **argv, const char *prefix);
diff --git a/builtin/merge-resolve.c b/builtin/merge-resolve.c
new file mode 100644
index 0000000000..59f734473b
--- /dev/null
+++ b/builtin/merge-resolve.c
@@ -0,0 +1,69 @@
+/*
+ * Builtin "git merge-resolve"
+ *
+ * Copyright (c) 2020 Alban Gruin
+ *
+ * Based on git-merge-resolve.sh, written by Linus Torvalds and Junio C
+ * Hamano.
+ *
+ * Resolve two trees, using enhanced multi-base read-tree.
+ */
+
+#include "cache.h"
+#include "builtin.h"
+#include "merge-strategies.h"
+
+static const char builtin_merge_resolve_usage[] =
+ "git merge-resolve <bases>... -- <head> <remote>";
+
+int cmd_merge_resolve(int argc, const char **argv, const char *prefix)
+{
+ int i, is_baseless = 1, sep_seen = 0;
+ const char *head = NULL;
+ struct commit_list *bases = NULL, *remote = NULL;
+ struct commit_list **next_base = &bases;
+
+ if (argc < 5)
+ usage(builtin_merge_resolve_usage);
+
+ setup_work_tree();
+ if (repo_read_index(the_repository) < 0)
+ die("invalid index");
+
+ /* The first parameters up to -- are merge bases; the rest are
+ * heads. */
+ for (i = 1; i < argc; i++) {
+ if (strcmp(argv[i], "--") == 0)
+ sep_seen = 1;
+ else if (strcmp(argv[i], "-h") == 0)
+ usage(builtin_merge_resolve_usage);
+ else if (sep_seen && !head)
+ head = argv[i];
+ else if (remote) {
+ /* Give up if we are given two or more remotes.
+ * Not handling octopus. */
+ return 2;
+ } else {
+ struct object_id oid;
+
+ get_oid(argv[i], &oid);
+ is_baseless &= sep_seen;
+
+ if (!oideq(&oid, the_hash_algo->empty_tree)) {
+ struct commit *commit;
+ commit = lookup_commit_or_die(&oid, argv[i]);
+
+ if (sep_seen)
+ commit_list_append(commit, &remote);
+ else
+ next_base = commit_list_append(commit, next_base);
+ }
+ }
+ }
+
+ /* Give up if this is a baseless merge. */
+ if (is_baseless)
+ return 2;
+
+ return merge_strategies_resolve(the_repository, bases, head, remote);
+}
diff --git a/git-merge-resolve.sh b/git-merge-resolve.sh
deleted file mode 100755
index 343fe7bccd..0000000000
--- a/git-merge-resolve.sh
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2005 Linus Torvalds
-# Copyright (c) 2005 Junio C Hamano
-#
-# Resolve two trees, using enhanced multi-base read-tree.
-
-# The first parameters up to -- are merge bases; the rest are heads.
-bases= head= remotes= sep_seen=
-for arg
-do
- case ",$sep_seen,$head,$arg," in
- *,--,)
- sep_seen=yes
- ;;
- ,yes,,*)
- head=$arg
- ;;
- ,yes,*)
- remotes="$remotes$arg "
- ;;
- *)
- bases="$bases$arg "
- ;;
- esac
-done
-
-# Give up if we are given two or more remotes -- not handling octopus.
-case "$remotes" in
-?*' '?*)
- exit 2 ;;
-esac
-
-# Give up if this is a baseless merge.
-if test '' = "$bases"
-then
- exit 2
-fi
-
-git update-index -q --refresh
-git read-tree -u -m --aggressive $bases $head $remotes || exit 2
-echo "Trying simple merge."
-if result_tree=$(git write-tree 2>/dev/null)
-then
- exit 0
-else
- echo "Simple merge failed, trying Automatic merge."
- if git merge-index -o git-merge-one-file -a
- then
- exit 0
- else
- exit 1
- fi
-fi
diff --git a/git.c b/git.c
index c97fea36c1..794ca6e9f0 100644
--- a/git.c
+++ b/git.c
@@ -538,6 +538,7 @@ static struct cmd_struct commands[] = {
{ "merge-recursive", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-ours", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-theirs", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
+ { "merge-resolve", cmd_merge_resolve, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-subtree", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-tree", cmd_merge_tree, RUN_SETUP | NO_PARSEOPT },
{ "mktag", cmd_mktag, RUN_SETUP | NO_PARSEOPT },
diff --git a/merge-strategies.c b/merge-strategies.c
index 00738863e4..6b905dfc38 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -1,8 +1,11 @@
#include "cache.h"
+#include "cache-tree.h"
#include "dir.h"
#include "ll-merge.h"
+#include "lockfile.h"
#include "merge-strategies.h"
#include "run-command.h"
+#include "unpack-trees.h"
#include "xdiff-interface.h"
static int add_to_index_cacheinfo(struct index_state *istate,
@@ -307,3 +310,85 @@ int merge_all(struct index_state *istate, int oneshot, int quiet,
return err;
}
+
+static int add_tree(const struct object_id *oid, struct tree_desc *t)
+{
+ struct tree *tree;
+
+ tree = parse_tree_indirect(oid);
+ if (parse_tree(tree))
+ return -1;
+
+ init_tree_desc(t, tree->buffer, tree->size);
+ return 0;
+}
+
+int merge_strategies_resolve(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remote)
+{
+ int i = 0;
+ struct lock_file lock = LOCK_INIT;
+ struct tree_desc t[MAX_UNPACK_TREES];
+ struct unpack_trees_options opts;
+ struct object_id head, oid;
+ struct commit_list *j;
+
+ if (head_arg)
+ get_oid(head_arg, &head);
+
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+ refresh_index(r->index, 0, NULL, NULL, NULL);
+
+ memset(&opts, 0, sizeof(opts));
+ opts.head_idx = 1;
+ opts.src_index = r->index;
+ opts.dst_index = r->index;
+ opts.update = 1;
+ opts.merge = 1;
+ opts.aggressive = 1;
+
+ for (j = bases; j && j->item; j = j->next) {
+ if (add_tree(&j->item->object.oid, t + (i++)))
+ goto out;
+ }
+
+ if (head_arg && add_tree(&head, t + (i++)))
+ goto out;
+ if (remote && add_tree(&remote->item->object.oid, t + (i++)))
+ goto out;
+
+ if (i == 1)
+ opts.fn = oneway_merge;
+ else if (i == 2) {
+ opts.fn = twoway_merge;
+ opts.initial_checkout = is_index_unborn(r->index);
+ } else if (i >= 3) {
+ opts.fn = threeway_merge;
+ opts.head_idx = i - 1;
+ }
+
+ if (unpack_trees(i, t, &opts))
+ goto out;
+
+ puts(_("Trying simple merge."));
+ write_locked_index(r->index, &lock, COMMIT_LOCK);
+
+ if (write_index_as_tree(&oid, r->index, r->index_file,
+ WRITE_TREE_SILENT, NULL)) {
+ int ret;
+
+ puts(_("Simple merge failed, trying Automatic merge."));
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+ ret = merge_all(r->index, 0, 0, merge_one_file_cb, r);
+
+ write_locked_index(r->index, &lock, COMMIT_LOCK);
+ return !!ret;
+ }
+
+ return 0;
+
+ out:
+ rollback_lock_file(&lock);
+ return 2;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
index 40e175ca39..778f8ce9d6 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -1,6 +1,7 @@
#ifndef MERGE_STRATEGIES_H
#define MERGE_STRATEGIES_H
+#include "commit.h"
#include "object.h"
int merge_strategies_one_file(struct repository *r,
@@ -33,4 +34,8 @@ int merge_one_path(struct index_state *istate, int oneshot, int quiet,
int merge_all(struct index_state *istate, int oneshot, int quiet,
merge_cb cb, void *data);
+int merge_strategies_resolve(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remote);
+
#endif /* MERGE_STRATEGIES_H */
--
2.28.0.370.g2c2dc3cc62
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v2 06/11] merge-recursive: move better_branch_name() to merge.c
2020-09-01 10:56 ` [PATCH v2 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (4 preceding siblings ...)
2020-09-01 10:56 ` [PATCH v2 05/11] merge-resolve: rewrite in C Alban Gruin
@ 2020-09-01 10:57 ` Alban Gruin
2020-09-01 10:57 ` [PATCH v2 07/11] merge-octopus: rewrite in C Alban Gruin
` (5 subsequent siblings)
11 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-09-01 10:57 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
get_better_branch_name() will be used by rebase-octopus once it is
rewritten in C, so instead of duplicating it, this moves this function
preventively inside an appropriate file in libgit.a. This function is
also renamed to reflect its usage by merge strategies.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge-recursive.c | 16 ++--------------
cache.h | 2 +-
merge.c | 12 ++++++++++++
3 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/builtin/merge-recursive.c b/builtin/merge-recursive.c
index a4bfd8fc51..972243b5e9 100644
--- a/builtin/merge-recursive.c
+++ b/builtin/merge-recursive.c
@@ -8,18 +8,6 @@
static const char builtin_merge_recursive_usage[] =
"git %s <base>... -- <head> <remote> ...";
-static char *better_branch_name(const char *branch)
-{
- static char githead_env[8 + GIT_MAX_HEXSZ + 1];
- char *name;
-
- if (strlen(branch) != the_hash_algo->hexsz)
- return xstrdup(branch);
- xsnprintf(githead_env, sizeof(githead_env), "GITHEAD_%s", branch);
- name = getenv(githead_env);
- return xstrdup(name ? name : branch);
-}
-
int cmd_merge_recursive(int argc, const char **argv, const char *prefix)
{
const struct object_id *bases[21];
@@ -75,8 +63,8 @@ int cmd_merge_recursive(int argc, const char **argv, const char *prefix)
if (get_oid(o.branch2, &h2))
die(_("could not resolve ref '%s'"), o.branch2);
- o.branch1 = better1 = better_branch_name(o.branch1);
- o.branch2 = better2 = better_branch_name(o.branch2);
+ o.branch1 = better1 = merge_get_better_branch_name(o.branch1);
+ o.branch2 = better2 = merge_get_better_branch_name(o.branch2);
if (o.verbosity >= 3)
printf(_("Merging %s with %s\n"), o.branch1, o.branch2);
diff --git a/cache.h b/cache.h
index 4cad61ffa4..a926b0bc87 100644
--- a/cache.h
+++ b/cache.h
@@ -1917,7 +1917,7 @@ int checkout_fast_forward(struct repository *r,
const struct object_id *from,
const struct object_id *to,
int overwrite_ignore);
-
+char *merge_get_better_branch_name(const char *branch);
int sane_execvp(const char *file, char *const argv[]);
diff --git a/merge.c b/merge.c
index 5fb88af102..801d673c5f 100644
--- a/merge.c
+++ b/merge.c
@@ -109,3 +109,15 @@ int checkout_fast_forward(struct repository *r,
return error(_("unable to write new index file"));
return 0;
}
+
+char *merge_get_better_branch_name(const char *branch)
+{
+ static char githead_env[8 + GIT_MAX_HEXSZ + 1];
+ char *name;
+
+ if (strlen(branch) != the_hash_algo->hexsz)
+ return xstrdup(branch);
+ xsnprintf(githead_env, sizeof(githead_env), "GITHEAD_%s", branch);
+ name = getenv(githead_env);
+ return xstrdup(name ? name : branch);
+}
--
2.28.0.370.g2c2dc3cc62
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v2 07/11] merge-octopus: rewrite in C
2020-09-01 10:56 ` [PATCH v2 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (5 preceding siblings ...)
2020-09-01 10:57 ` [PATCH v2 06/11] merge-recursive: move better_branch_name() to merge.c Alban Gruin
@ 2020-09-01 10:57 ` Alban Gruin
2020-09-01 10:57 ` [PATCH v2 08/11] merge: use the "resolve" strategy without forking Alban Gruin
` (4 subsequent siblings)
11 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-09-01 10:57 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
This rewrites `git merge-octopus' from shell to C. As for the two last
conversions, this port removes calls to external processes to avoid
reading and writing the index over and over again.
- Calls to `read-tree -u -m (--aggressive)?' are replaced by calls to
unpack_trees().
- The call to `write-tree' is replaced by a call to
write_index_as_tree().
- The call to `diff-index ...' is replaced by a call to
repo_index_has_changes(), and is moved from cmd_merge_octopus() to
merge_octopus().
- The call to `merge-index', needed to invoke `git merge-one-file', is
replaced by a call to merge_all().
The index is read in cmd_merge_octopus(), and is wrote back by
merge_strategies_octopus().
Here to, merge_strategies_octopus() takes two commit lists and a string
to reduce frictions when try_merge_strategies() will be modified to call
it directly.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Makefile | 2 +-
builtin.h | 1 +
builtin/merge-octopus.c | 65 +++++++++++++
git-merge-octopus.sh | 112 ----------------------
git.c | 1 +
merge-strategies.c | 200 ++++++++++++++++++++++++++++++++++++++++
merge-strategies.h | 3 +
7 files changed, 271 insertions(+), 113 deletions(-)
create mode 100644 builtin/merge-octopus.c
delete mode 100755 git-merge-octopus.sh
diff --git a/Makefile b/Makefile
index 929c3dc3eb..2fb26d9692 100644
--- a/Makefile
+++ b/Makefile
@@ -595,7 +595,6 @@ unexport CDPATH
SCRIPT_SH += git-bisect.sh
SCRIPT_SH += git-difftool--helper.sh
SCRIPT_SH += git-filter-branch.sh
-SCRIPT_SH += git-merge-octopus.sh
SCRIPT_SH += git-mergetool.sh
SCRIPT_SH += git-quiltimport.sh
SCRIPT_SH += git-request-pull.sh
@@ -1088,6 +1087,7 @@ BUILTIN_OBJS += builtin/mailsplit.o
BUILTIN_OBJS += builtin/merge-base.o
BUILTIN_OBJS += builtin/merge-file.o
BUILTIN_OBJS += builtin/merge-index.o
+BUILTIN_OBJS += builtin/merge-octopus.o
BUILTIN_OBJS += builtin/merge-one-file.o
BUILTIN_OBJS += builtin/merge-ours.o
BUILTIN_OBJS += builtin/merge-recursive.o
diff --git a/builtin.h b/builtin.h
index 6ea207c9fd..5a587ab70c 100644
--- a/builtin.h
+++ b/builtin.h
@@ -170,6 +170,7 @@ int cmd_mailsplit(int argc, const char **argv, const char *prefix);
int cmd_merge(int argc, const char **argv, const char *prefix);
int cmd_merge_base(int argc, const char **argv, const char *prefix);
int cmd_merge_index(int argc, const char **argv, const char *prefix);
+int cmd_merge_octopus(int argc, const char **argv, const char *prefix);
int cmd_merge_ours(int argc, const char **argv, const char *prefix);
int cmd_merge_file(int argc, const char **argv, const char *prefix);
int cmd_merge_one_file(int argc, const char **argv, const char *prefix);
diff --git a/builtin/merge-octopus.c b/builtin/merge-octopus.c
new file mode 100644
index 0000000000..37bbdf11cc
--- /dev/null
+++ b/builtin/merge-octopus.c
@@ -0,0 +1,65 @@
+/*
+ * Builtin "git merge-octopus"
+ *
+ * Copyright (c) 2020 Alban Gruin
+ *
+ * Based on git-merge-octopus.sh, written by Junio C Hamano.
+ *
+ * Resolve two or more trees.
+ */
+
+#include "cache.h"
+#include "builtin.h"
+#include "commit.h"
+#include "merge-strategies.h"
+
+static const char builtin_merge_octopus_usage[] =
+ "git merge-octopus [<bases>...] -- <head> <remote1> <remote2> [<remotes>...]";
+
+int cmd_merge_octopus(int argc, const char **argv, const char *prefix)
+{
+ int i, sep_seen = 0;
+ struct commit_list *bases = NULL, *remotes = NULL;
+ struct commit_list **next_base = &bases, **next_remote = &remotes;
+ const char *head_arg = NULL;
+
+ if (argc < 5)
+ usage(builtin_merge_octopus_usage);
+
+ setup_work_tree();
+ if (repo_read_index(the_repository) < 0)
+ die("corrupted cache");
+
+ /* The first parameters up to -- are merge bases; the rest are
+ * heads. */
+ for (i = 1; i < argc; i++) {
+ if (strcmp(argv[i], "--") == 0)
+ sep_seen = 1;
+ else if (strcmp(argv[i], "-h") == 0)
+ usage(builtin_merge_octopus_usage);
+ else if (sep_seen && !head_arg)
+ head_arg = argv[i];
+ else {
+ struct object_id oid;
+
+ get_oid(argv[i], &oid);
+
+ if (!oideq(&oid, the_hash_algo->empty_tree)) {
+ struct commit *commit;
+ commit = lookup_commit_or_die(&oid, argv[i]);
+
+ if (sep_seen)
+ next_remote = commit_list_append(commit, next_remote);
+ else
+ next_base = commit_list_append(commit, next_base);
+ }
+ }
+ }
+
+ /* Reject if this is not an octopus -- resolve should be used
+ * instead. */
+ if (commit_list_count(remotes) < 2)
+ return 2;
+
+ return merge_strategies_octopus(the_repository, bases, head_arg, remotes);
+}
diff --git a/git-merge-octopus.sh b/git-merge-octopus.sh
deleted file mode 100755
index 7d19d37951..0000000000
--- a/git-merge-octopus.sh
+++ /dev/null
@@ -1,112 +0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2005 Junio C Hamano
-#
-# Resolve two or more trees.
-#
-
-. git-sh-setup
-
-LF='
-'
-
-# The first parameters up to -- are merge bases; the rest are heads.
-bases= head= remotes= sep_seen=
-for arg
-do
- case ",$sep_seen,$head,$arg," in
- *,--,)
- sep_seen=yes
- ;;
- ,yes,,*)
- head=$arg
- ;;
- ,yes,*)
- remotes="$remotes$arg "
- ;;
- *)
- bases="$bases$arg "
- ;;
- esac
-done
-
-# Reject if this is not an octopus -- resolve should be used instead.
-case "$remotes" in
-?*' '?*)
- ;;
-*)
- exit 2 ;;
-esac
-
-# MRC is the current "merge reference commit"
-# MRT is the current "merge result tree"
-
-if ! git diff-index --quiet --cached HEAD --
-then
- gettextln "Error: Your local changes to the following files would be overwritten by merge"
- git diff-index --cached --name-only HEAD -- | sed -e 's/^/ /'
- exit 2
-fi
-MRC=$(git rev-parse --verify -q $head)
-MRT=$(git write-tree)
-NON_FF_MERGE=0
-OCTOPUS_FAILURE=0
-for SHA1 in $remotes
-do
- case "$OCTOPUS_FAILURE" in
- 1)
- # We allow only last one to have a hand-resolvable
- # conflicts. Last round failed and we still had
- # a head to merge.
- gettextln "Automated merge did not work."
- gettextln "Should not be doing an octopus."
- exit 2
- esac
-
- eval pretty_name=\${GITHEAD_$SHA1:-$SHA1}
- if test "$SHA1" = "$pretty_name"
- then
- SHA1_UP="$(echo "$SHA1" | tr a-z A-Z)"
- eval pretty_name=\${GITHEAD_$SHA1_UP:-$pretty_name}
- fi
- common=$(git merge-base --all $SHA1 $MRC) ||
- die "$(eval_gettext "Unable to find common commit with \$pretty_name")"
-
- case "$LF$common$LF" in
- *"$LF$SHA1$LF"*)
- eval_gettextln "Already up to date with \$pretty_name"
- continue
- ;;
- esac
-
- if test "$common,$NON_FF_MERGE" = "$MRC,0"
- then
- # The first head being merged was a fast-forward.
- # Advance MRC to the head being merged, and use that
- # tree as the intermediate result of the merge.
- # We still need to count this as part of the parent set.
-
- eval_gettextln "Fast-forwarding to: \$pretty_name"
- git read-tree -u -m $head $SHA1 || exit
- MRC=$SHA1 MRT=$(git write-tree)
- continue
- fi
-
- NON_FF_MERGE=1
-
- eval_gettextln "Trying simple merge with \$pretty_name"
- git read-tree -u -m --aggressive $common $MRT $SHA1 || exit 2
- next=$(git write-tree 2>/dev/null)
- if test $? -ne 0
- then
- gettextln "Simple merge did not work, trying automatic merge."
- git merge-index -o git-merge-one-file -a ||
- OCTOPUS_FAILURE=1
- next=$(git write-tree 2>/dev/null)
- fi
-
- MRC="$MRC $SHA1"
- MRT=$next
-done
-
-exit "$OCTOPUS_FAILURE"
diff --git a/git.c b/git.c
index 794ca6e9f0..df0bebdafc 100644
--- a/git.c
+++ b/git.c
@@ -533,6 +533,7 @@ static struct cmd_struct commands[] = {
{ "merge-base", cmd_merge_base, RUN_SETUP },
{ "merge-file", cmd_merge_file, RUN_SETUP_GENTLY },
{ "merge-index", cmd_merge_index, RUN_SETUP | NO_PARSEOPT },
+ { "merge-octopus", cmd_merge_octopus, RUN_SETUP | NO_PARSEOPT },
{ "merge-ours", cmd_merge_ours, RUN_SETUP | NO_PARSEOPT },
{ "merge-one-file", cmd_merge_one_file, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
diff --git a/merge-strategies.c b/merge-strategies.c
index 6b905dfc38..dee86389e3 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -1,5 +1,6 @@
#include "cache.h"
#include "cache-tree.h"
+#include "commit-reach.h"
#include "dir.h"
#include "ll-merge.h"
#include "lockfile.h"
@@ -392,3 +393,202 @@ int merge_strategies_resolve(struct repository *r,
rollback_lock_file(&lock);
return 2;
}
+
+static int fast_forward(struct repository *r, const struct object_id *oids,
+ int nr, int aggressive)
+{
+ int i;
+ struct tree_desc t[MAX_UNPACK_TREES];
+ struct unpack_trees_options opts;
+ struct lock_file lock = LOCK_INIT;
+
+ repo_read_index_preload(r, NULL, 0);
+ if (refresh_index(r->index, REFRESH_QUIET, NULL, NULL, NULL))
+ return -1;
+
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+
+ memset(&opts, 0, sizeof(opts));
+ opts.head_idx = 1;
+ opts.src_index = r->index;
+ opts.dst_index = r->index;
+ opts.merge = 1;
+ opts.update = 1;
+ opts.aggressive = aggressive;
+
+ for (i = 0; i < nr; i++) {
+ struct tree *tree;
+ tree = parse_tree_indirect(oids + i);
+ if (parse_tree(tree))
+ return -1;
+ init_tree_desc(t + i, tree->buffer, tree->size);
+ }
+
+ if (nr == 1)
+ opts.fn = oneway_merge;
+ else if (nr == 2) {
+ opts.fn = twoway_merge;
+ opts.initial_checkout = is_index_unborn(r->index);
+ } else if (nr >= 3) {
+ opts.fn = threeway_merge;
+ opts.head_idx = nr - 1;
+ }
+
+ if (unpack_trees(nr, t, &opts))
+ return -1;
+
+ if (write_locked_index(r->index, &lock, COMMIT_LOCK))
+ return error(_("unable to write new index file"));
+
+ return 0;
+}
+
+static int write_tree(struct repository *r, struct tree **reference_tree)
+{
+ struct object_id oid;
+ int ret;
+
+ ret = write_index_as_tree(&oid, r->index, r->index_file, 0, NULL);
+ if (!ret)
+ *reference_tree = lookup_tree(r, &oid);
+
+ return ret;
+}
+
+int merge_strategies_octopus(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remotes)
+{
+ int non_ff_merge = 0, ret = 0, references = 1;
+ struct commit **reference_commit;
+ struct tree *reference_tree;
+ struct commit_list *j;
+ struct object_id head;
+ struct strbuf sb = STRBUF_INIT;
+
+ get_oid(head_arg, &head);
+
+ reference_commit = xcalloc(commit_list_count(remotes) + 1, sizeof(struct commit *));
+ reference_commit[0] = lookup_commit_reference(r, &head);
+ reference_tree = repo_get_commit_tree(r, reference_commit[0]);
+
+ if (repo_index_has_changes(r, reference_tree, &sb)) {
+ error(_("Your local changes to the following files "
+ "would be overwritten by merge:\n %s"),
+ sb.buf);
+ strbuf_release(&sb);
+ ret = 2;
+ goto out;
+ }
+
+ for (j = remotes; j && j->item; j = j->next) {
+ struct commit *c = j->item;
+ struct object_id *oid = &c->object.oid;
+ struct commit_list *common, *k;
+ char *branch_name;
+ int can_ff = 1;
+
+ if (ret) {
+ /* We allow only last one to have a
+ hand-resolvable conflicts. Last round failed
+ and we still had a head to merge. */
+ puts(_("Automated merge did not work."));
+ puts(_("Should not be doing an octopus."));
+
+ ret = 2;
+ goto out;
+ }
+
+ branch_name = merge_get_better_branch_name(oid_to_hex(oid));
+ common = get_merge_bases_many(c, references, reference_commit);
+
+ if (!common)
+ die(_("Unable to find common commit with %s"), branch_name);
+
+ for (k = common; k && !oideq(&k->item->object.oid, oid); k = k->next);
+
+ if (k) {
+ printf(_("Already up to date with %s\n"), branch_name);
+ free(branch_name);
+ free_commit_list(common);
+ continue;
+ }
+
+ if (!non_ff_merge) {
+ int i;
+
+ for (i = 0, k = common; k && i < references && can_ff; k = k->next, i++) {
+ can_ff = oideq(&k->item->object.oid,
+ &reference_commit[i]->object.oid);
+ }
+ }
+
+ if (!non_ff_merge && can_ff) {
+ /* The first head being merged was a
+ fast-forward. Advance the reference commit
+ to the head being merged, and use that tree
+ as the intermediate result of the merge. We
+ still need to count this as part of the
+ parent set. */
+ struct object_id oids[2];
+ printf(_("Fast-forwarding to: %s\n"), branch_name);
+
+ oidcpy(oids, &head);
+ oidcpy(oids + 1, oid);
+
+ ret = fast_forward(r, oids, 2, 0);
+ if (ret) {
+ free(branch_name);
+ free_commit_list(common);
+ goto out;
+ }
+
+ references = 0;
+ write_tree(r, &reference_tree);
+ } else {
+ int i = 0;
+ struct tree *next = NULL;
+ struct object_id oids[MAX_UNPACK_TREES];
+
+ non_ff_merge = 1;
+ printf(_("Trying simple merge with %s\n"), branch_name);
+
+ for (k = common; k; k = k->next)
+ oidcpy(oids + (i++), &k->item->object.oid);
+
+ oidcpy(oids + (i++), &reference_tree->object.oid);
+ oidcpy(oids + (i++), oid);
+
+ if (fast_forward(r, oids, i, 1)) {
+ ret = 2;
+
+ free(branch_name);
+ free_commit_list(common);
+
+ goto out;
+ }
+
+ if (write_tree(r, &next)) {
+ struct lock_file lock = LOCK_INIT;
+
+ puts(_("Simple merge did not work, trying automatic merge."));
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+ ret = !!merge_all(r->index, 0, 0, merge_one_file_cb, r);
+ write_locked_index(r->index, &lock, COMMIT_LOCK);
+
+ write_tree(r, &next);
+ }
+
+ reference_tree = next;
+ }
+
+ reference_commit[references++] = c;
+
+ free(branch_name);
+ free_commit_list(common);
+ }
+
+out:
+ free(reference_commit);
+ return ret;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
index 778f8ce9d6..938411a04e 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -37,5 +37,8 @@ int merge_all(struct index_state *istate, int oneshot, int quiet,
int merge_strategies_resolve(struct repository *r,
struct commit_list *bases, const char *head_arg,
struct commit_list *remote);
+int merge_strategies_octopus(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remote);
#endif /* MERGE_STRATEGIES_H */
--
2.28.0.370.g2c2dc3cc62
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v2 08/11] merge: use the "resolve" strategy without forking
2020-09-01 10:56 ` [PATCH v2 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (6 preceding siblings ...)
2020-09-01 10:57 ` [PATCH v2 07/11] merge-octopus: rewrite in C Alban Gruin
@ 2020-09-01 10:57 ` Alban Gruin
2020-09-01 10:57 ` [PATCH v2 09/11] merge: use the "octopus" " Alban Gruin
` (3 subsequent siblings)
11 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-09-01 10:57 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
This teaches `git merge' to invoke the "resolve" strategy with a
function call instead of forking.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge.c | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/builtin/merge.c b/builtin/merge.c
index 74829a838e..541d9bed02 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -41,6 +41,7 @@
#include "commit-reach.h"
#include "wt-status.h"
#include "commit-graph.h"
+#include "merge-strategies.h"
#define DEFAULT_TWOHEAD (1<<0)
#define DEFAULT_OCTOPUS (1<<1)
@@ -740,7 +741,10 @@ static int try_merge_strategy(const char *strategy, struct commit_list *common,
COMMIT_LOCK | SKIP_IF_UNCHANGED))
die(_("unable to write %s"), get_index_file());
return clean ? 0 : 1;
- } else {
+ } else if (!strcmp(strategy, "resolve"))
+ return merge_strategies_resolve(the_repository, common,
+ head_arg, remoteheads);
+ else {
return try_merge_command(the_repository,
strategy, xopts_nr, xopts,
common, head_arg, remoteheads);
--
2.28.0.370.g2c2dc3cc62
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v2 09/11] merge: use the "octopus" strategy without forking
2020-09-01 10:56 ` [PATCH v2 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (7 preceding siblings ...)
2020-09-01 10:57 ` [PATCH v2 08/11] merge: use the "resolve" strategy without forking Alban Gruin
@ 2020-09-01 10:57 ` Alban Gruin
2020-09-01 10:57 ` [PATCH v2 10/11] sequencer: use the "resolve" " Alban Gruin
` (2 subsequent siblings)
11 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-09-01 10:57 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
This teaches `git merge' to invoke the "octopus" strategy with a
function call instead of forking.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/builtin/merge.c b/builtin/merge.c
index 541d9bed02..90e092ad02 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -744,6 +744,9 @@ static int try_merge_strategy(const char *strategy, struct commit_list *common,
} else if (!strcmp(strategy, "resolve"))
return merge_strategies_resolve(the_repository, common,
head_arg, remoteheads);
+ else if (!strcmp(strategy, "octopus"))
+ return merge_strategies_octopus(the_repository, common,
+ head_arg, remoteheads);
else {
return try_merge_command(the_repository,
strategy, xopts_nr, xopts,
--
2.28.0.370.g2c2dc3cc62
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v2 10/11] sequencer: use the "resolve" strategy without forking
2020-09-01 10:56 ` [PATCH v2 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (8 preceding siblings ...)
2020-09-01 10:57 ` [PATCH v2 09/11] merge: use the "octopus" " Alban Gruin
@ 2020-09-01 10:57 ` Alban Gruin
2020-09-01 10:57 ` [PATCH v2 11/11] sequencer: use the "octopus" merge " Alban Gruin
2020-10-05 12:26 ` [PATCH v3 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
11 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-09-01 10:57 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
This teaches the sequencer to invoke the "resolve" strategy with a
function call instead of forking.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
sequencer.c | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/sequencer.c b/sequencer.c
index 2425896911..c4c7b28d24 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -33,6 +33,7 @@
#include "commit-reach.h"
#include "rebase-interactive.h"
#include "reset.h"
+#include "merge-strategies.h"
#define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
@@ -1922,9 +1923,15 @@ static int do_pick_commit(struct repository *r,
commit_list_insert(base, &common);
commit_list_insert(next, &remotes);
- res |= try_merge_command(r, opts->strategy,
- opts->xopts_nr, (const char **)opts->xopts,
- common, oid_to_hex(&head), remotes);
+
+ if (!strcmp(opts->strategy, "resolve")) {
+ repo_read_index(r);
+ res |= merge_strategies_resolve(r, common, oid_to_hex(&head), remotes);
+ } else
+ res |= try_merge_command(r, opts->strategy,
+ opts->xopts_nr, (const char **)opts->xopts,
+ common, oid_to_hex(&head), remotes);
+
free_commit_list(common);
free_commit_list(remotes);
}
--
2.28.0.370.g2c2dc3cc62
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v2 11/11] sequencer: use the "octopus" merge strategy without forking
2020-09-01 10:56 ` [PATCH v2 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (9 preceding siblings ...)
2020-09-01 10:57 ` [PATCH v2 10/11] sequencer: use the "resolve" " Alban Gruin
@ 2020-09-01 10:57 ` Alban Gruin
2020-10-05 12:26 ` [PATCH v3 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
11 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-09-01 10:57 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
This teaches the sequencer to invoke the "octopus" strategy with a
function call instead of forking.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
sequencer.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/sequencer.c b/sequencer.c
index c4c7b28d24..34853b8970 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -1927,6 +1927,9 @@ static int do_pick_commit(struct repository *r,
if (!strcmp(opts->strategy, "resolve")) {
repo_read_index(r);
res |= merge_strategies_resolve(r, common, oid_to_hex(&head), remotes);
+ } else if (!strcmp(opts->strategy, "octopus")) {
+ repo_read_index(r);
+ res |= merge_strategies_octopus(r, common, oid_to_hex(&head), remotes);
} else
res |= try_merge_command(r, opts->strategy,
opts->xopts_nr, (const char **)opts->xopts,
--
2.28.0.370.g2c2dc3cc62
^ permalink raw reply related [flat|nested] 221+ messages in thread
* Re: [PATCH v2 02/11] merge-one-file: rewrite in C
2020-09-01 10:56 ` [PATCH v2 02/11] merge-one-file: rewrite in C Alban Gruin
@ 2020-09-01 21:06 ` Junio C Hamano
2020-09-02 14:50 ` Alban Gruin
0 siblings, 1 reply; 221+ messages in thread
From: Junio C Hamano @ 2020-09-01 21:06 UTC (permalink / raw)
To: Alban Gruin; +Cc: git, phillip.wood
Alban Gruin <alban.gruin@gmail.com> writes:
> diff --git a/builtin/merge-one-file.c b/builtin/merge-one-file.c
> new file mode 100644
> index 0000000000..306a86c2f0
> --- /dev/null
> +++ b/builtin/merge-one-file.c
> @@ -0,0 +1,85 @@
> +/*
> + * Builtin "git merge-one-file"
> + *
> + * Copyright (c) 2020 Alban Gruin
> + *
> + * Based on git-merge-one-file.sh, written by Linus Torvalds.
> + *
> + * This is the git per-file merge utility, called with
> + *
> + * argv[1] - original file SHA1 (or empty)
> + * argv[2] - file in branch1 SHA1 (or empty)
> + * argv[3] - file in branch2 SHA1 (or empty)
> + * argv[4] - pathname in repository
> + * argv[5] - original file mode (or empty)
> + * argv[6] - file in branch1 mode (or empty)
> + * argv[7] - file in branch2 mode (or empty)
> + *
> + * Handle some trivial cases. The _really_ trivial cases have been
> + * handled already by git read-tree, but that one doesn't do any merges
> + * that might change the tree layout.
> + */
> +
> +#include "cache.h"
> +#include "builtin.h"
> +#include "lockfile.h"
> +#include "merge-strategies.h"
> +
> +static const char builtin_merge_one_file_usage[] =
> + "git merge-one-file <orig blob> <our blob> <their blob> <path> "
> + "<orig mode> <our mode> <their mode>\n\n"
> + "Blob ids and modes should be empty for missing files.";
> +
> +int cmd_merge_one_file(int argc, const char **argv, const char *prefix)
> +{
> + struct object_id orig_blob, our_blob, their_blob,
> + *p_orig_blob = NULL, *p_our_blob = NULL, *p_their_blob = NULL;
> + unsigned int orig_mode = 0, our_mode = 0, their_mode = 0, ret = 0;
> + struct lock_file lock = LOCK_INIT;
> +
> + if (argc != 8)
> + usage(builtin_merge_one_file_usage);
> +
> + if (repo_read_index(the_repository) < 0)
> + die("invalid index");
> +
> + repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
I do understand why we would want merge_strategies_one_file() helper
introduced by this step so that the helper can work in an arbitrary
repository (hence taking a pointer to repository structure as one of
its parameters).
But the "merge-one-file" command will always work in the_repository.
I do not see a point in using helpers that can work in an arbitrary
repository, like repo_read_index() or repo_hold_locked_index(), in
the above. I only see downsides --- it is longer to read, makes
readers wonder if there is something tricky involving another
repository going on, etc.
> + if (!get_oid(argv[1], &orig_blob)) {
> + p_orig_blob = &orig_blob;
> + orig_mode = strtol(argv[5], NULL, 8);
Write a wrapper around strtol(...,...,8) to reduce repetition, and
make sure you do not pass NULL as the second parameter to strtol()
to always check you parsed the string to the end.
> + ret = merge_strategies_one_file(the_repository,
> + p_orig_blob, p_our_blob, p_their_blob, argv[4],
> + orig_mode, our_mode, their_mode);
Here, as I said above, it is perfectly fine to pass
the_repository().
> + if (ret) {
> + rollback_lock_file(&lock);
> + return ret;
> + }
> +
> + return write_locked_index(the_repository->index, &lock, COMMIT_LOCK);
Likewise, I do not see much point in saying the_repository->index; the_index
is a perfectly fine short-hand.
> diff --git a/merge-strategies.c b/merge-strategies.c
> new file mode 100644
> index 0000000000..f2af4a894d
> --- /dev/null
> +++ b/merge-strategies.c
> @@ -0,0 +1,199 @@
> +#include "cache.h"
> +#include "dir.h"
> +#include "ll-merge.h"
> +#include "merge-strategies.h"
> +#include "xdiff-interface.h"
> +
> +static int add_to_index_cacheinfo(struct index_state *istate,
> + unsigned int mode,
> + const struct object_id *oid, const char *path)
> +{
> + struct cache_entry *ce;
> + int len, option;
> +
> + if (!verify_path(path, mode))
> + return error(_("Invalid path '%s'"), path);
> +
> + len = strlen(path);
> + ce = make_empty_cache_entry(istate, len);
> +
> + oidcpy(&ce->oid, oid);
> + memcpy(ce->name, path, len);
> + ce->ce_flags = create_ce_flags(0);
> + ce->ce_namelen = len;
> + ce->ce_mode = create_ce_mode(mode);
> + if (assume_unchanged)
> + ce->ce_flags |= CE_VALID;
> + option = ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE;
> + if (add_index_entry(istate, ce, option))
> + return error(_("%s: cannot add to the index"), path);
> +
> + return 0;
> +}
> +
> +static int checkout_from_index(struct index_state *istate, const char *path)
> +{
> + struct checkout state = CHECKOUT_INIT;
> + struct cache_entry *ce;
> +
> + state.istate = istate;
> + state.force = 1;
> + state.base_dir = "";
> + state.base_dir_len = 0;
> +
> + ce = index_file_exists(istate, path, strlen(path), 0);
> + if (checkout_entry(ce, &state, NULL, NULL) < 0)
> + return error(_("%s: cannot checkout file"), path);
> + return 0;
> +}
> +
> +static int merge_one_file_deleted(struct index_state *istate,
> + const struct object_id *orig_blob,
> + const struct object_id *our_blob,
> + const struct object_id *their_blob, const char *path,
> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
> +{
> + if ((our_blob && orig_mode != our_mode) ||
> + (their_blob && orig_mode != their_mode))
> + return error(_("File %s deleted on one branch but had its "
> + "permissions changed on the other."), path);
> +
> + if (our_blob) {
> + printf(_("Removing %s\n"), path);
> +
> + if (file_exists(path))
> + remove_path(path);
> + }
> +
> + if (remove_file_from_index(istate, path))
> + return error("%s: cannot remove from the index", path);
> + return 0;
> +}
These functions we see above all are now easy to write these days,
thanks to the previous work that built many helpers to perform ommon
operations (e.g. remove_path()). Reusing them is very good.
> +static int do_merge_one_file(struct index_state *istate,
> + const struct object_id *orig_blob,
> + const struct object_id *our_blob,
> + const struct object_id *their_blob, const char *path,
> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
> +{
> + int ret, i, dest;
> + mmbuffer_t result = {NULL, 0};
> + mmfile_t mmfs[3];
> + struct ll_merge_options merge_opts = {0};
> + struct cache_entry *ce;
> +
> + if (our_mode == S_IFLNK || their_mode == S_IFLNK)
> + return error(_("%s: Not merging symbolic link changes."), path);
> + else if (our_mode == S_IFGITLINK || their_mode == S_IFGITLINK)
> + return error(_("%s: Not merging conflicting submodule changes."), path);
> +
> + read_mmblob(mmfs + 1, our_blob);
> + read_mmblob(mmfs + 2, their_blob);
> +
> + if (orig_blob) {
> + printf(_("Auto-merging %s\n"), path);
> + read_mmblob(mmfs + 0, orig_blob);
> + } else {
> + printf(_("Added %s in both, but differently.\n"), path);
> + read_mmblob(mmfs + 0, &null_oid);
> + }
> +
> + merge_opts.xdl_opts = XDL_MERGE_ZEALOUS_ALNUM;
> + ret = ll_merge(&result, path,
> + mmfs + 0, "orig",
> + mmfs + 1, "our",
> + mmfs + 2, "their",
> + istate, &merge_opts);
> +
> + for (i = 0; i < 3; i++)
> + free(mmfs[i].ptr);
> +
> + if (ret > 127 || !orig_blob)
> + ret = error(_("content conflict in %s"), path);
The original only checked if ret is zero or non-zero; here we
require ret to be large. Intended?
ll_merge() that called ll_xdl_merge() (i.e. the most common case)
would return the value returned from xdl_merge(), which can be -1
when we got an error before calling xdl_do_merge(). xdl_do_merge()
in turn can return -1. The most common case returns the value
returned from xdl_cleanup_merge(), which is 0 for clean merge, and
any positive integer (not clipped to 127 or 128) for conflicted one.
> + /* Create the working tree file, using "our tree" version from
> + the index, and then store the result of the merge. */
Style. (cf. Documentation/CodingGuidelines).
> + ce = index_file_exists(istate, path, strlen(path), 0);
> + if (!ce)
> + BUG("file is not present in the cache?");
> +
> + unlink(path);
> + dest = open(path, O_WRONLY | O_CREAT, ce->ce_mode);
> + write_in_full(dest, result.ptr, result.size);
If open() fails, we write to a bogus file descriptor here.
> + close(dest);
> +
> + free(result.ptr);
> +
> + if (ret && our_mode != their_mode)
> + return error(_("permission conflict: %o->%o,%o in %s"),
> + orig_mode, our_mode, their_mode, path);
> + if (ret)
> + return 1;
What is the error returning convention around here? Our usual
convention is that 0 signals a success, and negative reports an
error. Returning the value returned from add_file_to_index() below,
and error() above, are consistent with the convention, but this one
returns 1 that is not. When deviating from convention, it needs to
be documented for the callers in a comment before the function
definition.
> +
> + return add_file_to_index(istate, path, 0);
> +}
> +int merge_strategies_one_file(struct repository *r,
> + const struct object_id *orig_blob,
> + const struct object_id *our_blob,
> + const struct object_id *their_blob, const char *path,
> + unsigned int orig_mode, unsigned int our_mode,
> + unsigned int their_mode)
> +{
> + if (orig_blob &&
> + ((!their_blob && our_blob && oideq(orig_blob, our_blob)) ||
> + (!our_blob && their_blob && oideq(orig_blob, their_blob))))
> + /* Deleted in both or deleted in one and unchanged in
> + the other */
> + return merge_one_file_deleted(r->index,
> + orig_blob, our_blob, their_blob, path,
> + orig_mode, our_mode, their_mode);
> + else if (!orig_blob && our_blob && !their_blob) {
> + /* Added in one. The other side did not add and we
> + added so there is nothing to be done, except making
> + the path merged. */
> + return add_to_index_cacheinfo(r->index, our_mode, our_blob, path);
> + } else if (!orig_blob && !our_blob && their_blob) {
> + printf(_("Adding %s\n"), path);
> +
> + if (file_exists(path))
> + return error(_("untracked %s is overwritten by the merge."), path);
> +
> + if (add_to_index_cacheinfo(r->index, their_mode, their_blob, path))
> + return 1;
> + return checkout_from_index(r->index, path);
> + } else if (!orig_blob && our_blob && their_blob &&
> + oideq(our_blob, their_blob)) {
> + /* Added in both, identically (check for same
> + permissions). */
> + if (our_mode != their_mode)
> + return error(_("File %s added identically in both branches, "
> + "but permissions conflict %o->%o."),
> + path, our_mode, their_mode);
> +
> + printf(_("Adding %s\n"), path);
> +
> + if (add_to_index_cacheinfo(r->index, our_mode, our_blob, path))
> + return 1;
> + return checkout_from_index(r->index, path);
> + } else if (our_blob && their_blob)
> + /* Modified in both, but differently. */
> + return do_merge_one_file(r->index,
> + orig_blob, our_blob, their_blob, path,
> + orig_mode, our_mode, their_mode);
> + else {
> + char *orig_hex = "", *our_hex = "", *their_hex = "";
> +
> + if (orig_blob)
> + orig_hex = oid_to_hex(orig_blob);
> + if (our_blob)
> + our_hex = oid_to_hex(our_blob);
> + if (their_blob)
> + their_hex = oid_to_hex(their_blob);
Prepare three char [] buffers and use oid_to_hex_r() instead,
instead of relying that we'd have sufficient number of entries in
the rotating buffer.
> + return error(_("%s: Not handling case %s -> %s -> %s"),
> + path, orig_hex, our_hex, their_hex);
> + }
> +
> + return 0;
> +}
> diff --git a/merge-strategies.h b/merge-strategies.h
> new file mode 100644
> index 0000000000..b527d145c7
> --- /dev/null
> +++ b/merge-strategies.h
> @@ -0,0 +1,13 @@
> +#ifndef MERGE_STRATEGIES_H
> +#define MERGE_STRATEGIES_H
> +
> +#include "object.h"
> +
> +int merge_strategies_one_file(struct repository *r,
> + const struct object_id *orig_blob,
> + const struct object_id *our_blob,
> + const struct object_id *their_blob, const char *path,
> + unsigned int orig_mode, unsigned int our_mode,
> + unsigned int their_mode);
> +
> +#endif /* MERGE_STRATEGIES_H */
> diff --git a/t/t6415-merge-dir-to-symlink.sh b/t/t6415-merge-dir-to-symlink.sh
> index 2eddcc7664..5fb74e39a0 100755
> --- a/t/t6415-merge-dir-to-symlink.sh
> +++ b/t/t6415-merge-dir-to-symlink.sh
> @@ -94,7 +94,7 @@ test_expect_success SYMLINKS 'a/b was resolved as symlink' '
> test -h a/b
> '
>
> -test_expect_failure 'do not lose untracked in merge (resolve)' '
> +test_expect_success 'do not lose untracked in merge (resolve)' '
> git reset --hard &&
> git checkout baseline^0 &&
> >a/b/c/e &&
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [PATCH v2 03/11] merge-index: libify merge_one_path() and merge_all()
2020-09-01 10:56 ` [PATCH v2 03/11] merge-index: libify merge_one_path() and merge_all() Alban Gruin
@ 2020-09-01 21:11 ` Junio C Hamano
2020-09-02 15:37 ` Alban Gruin
0 siblings, 1 reply; 221+ messages in thread
From: Junio C Hamano @ 2020-09-01 21:11 UTC (permalink / raw)
To: Alban Gruin; +Cc: git, phillip.wood
Alban Gruin <alban.gruin@gmail.com> writes:
> The "resolve" and "octopus" merge strategies do not call directly `git
> merge-one-file', they delegate the work to another git command, `git
> merge-index', that will loop over files in the index and call the
> specified command. Unfortunately, these functions are not part of
> libgit.a, which means that once rewritten, the strategies would still
> have to invoke `merge-one-file' by spawning a new process first.
>
> To avoid this, this moves merge_one_path(), merge_all(), and their
> helpers to merge-strategies.c. They also take a callback to dictate
> what they should do for each file. For now, only one launching a new
> process is defined to preserve the behaviour of the builtin version.
... of the "builtin" version? I thought this series is introducing
a new builtin version? Puzzled...
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [PATCH v2 02/11] merge-one-file: rewrite in C
2020-09-01 21:06 ` Junio C Hamano
@ 2020-09-02 14:50 ` Alban Gruin
0 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-09-02 14:50 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git, phillip.wood
Hi Junio,
Le 01/09/2020 à 23:06, Junio C Hamano a écrit :
> Alban Gruin <alban.gruin@gmail.com> writes:
>
>> diff --git a/builtin/merge-one-file.c b/builtin/merge-one-file.c
>> new file mode 100644
>> index 0000000000..306a86c2f0
>> --- /dev/null
>> +++ b/builtin/merge-one-file.c
>> @@ -0,0 +1,85 @@
>> +/*
>> + * Builtin "git merge-one-file"
>> + *
>> + * Copyright (c) 2020 Alban Gruin
>> + *
>> + * Based on git-merge-one-file.sh, written by Linus Torvalds.
>> + *
>> + * This is the git per-file merge utility, called with
>> + *
>> + * argv[1] - original file SHA1 (or empty)
>> + * argv[2] - file in branch1 SHA1 (or empty)
>> + * argv[3] - file in branch2 SHA1 (or empty)
>> + * argv[4] - pathname in repository
>> + * argv[5] - original file mode (or empty)
>> + * argv[6] - file in branch1 mode (or empty)
>> + * argv[7] - file in branch2 mode (or empty)
>> + *
>> + * Handle some trivial cases. The _really_ trivial cases have been
>> + * handled already by git read-tree, but that one doesn't do any merges
>> + * that might change the tree layout.
>> + */
>> +
>> +#include "cache.h"
>> +#include "builtin.h"
>> +#include "lockfile.h"
>> +#include "merge-strategies.h"
>> +
>> +static const char builtin_merge_one_file_usage[] =
>> + "git merge-one-file <orig blob> <our blob> <their blob> <path> "
>> + "<orig mode> <our mode> <their mode>\n\n"
>> + "Blob ids and modes should be empty for missing files.";
>> +
>> +int cmd_merge_one_file(int argc, const char **argv, const char *prefix)
>> +{
>> + struct object_id orig_blob, our_blob, their_blob,
>> + *p_orig_blob = NULL, *p_our_blob = NULL, *p_their_blob = NULL;
>> + unsigned int orig_mode = 0, our_mode = 0, their_mode = 0, ret = 0;
>> + struct lock_file lock = LOCK_INIT;
>> +
>> + if (argc != 8)
>> + usage(builtin_merge_one_file_usage);
>> +
>> + if (repo_read_index(the_repository) < 0)
>> + die("invalid index");
>> +
>> + repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
>
> I do understand why we would want merge_strategies_one_file() helper
> introduced by this step so that the helper can work in an arbitrary
> repository (hence taking a pointer to repository structure as one of
> its parameters).
>
> But the "merge-one-file" command will always work in the_repository.
> I do not see a point in using helpers that can work in an arbitrary
> repository, like repo_read_index() or repo_hold_locked_index(), in
> the above. I only see downsides --- it is longer to read, makes
> readers wonder if there is something tricky involving another
> repository going on, etc.
>
I was under the impression that using the_index is just deprecated, and
that we ought to avoid using it, even in builtins.
Will update that.
>> + if (!get_oid(argv[1], &orig_blob)) {
>> + p_orig_blob = &orig_blob;
>> + orig_mode = strtol(argv[5], NULL, 8);
>
> Write a wrapper around strtol(...,...,8) to reduce repetition, and
> make sure you do not pass NULL as the second parameter to strtol()
> to always check you parsed the string to the end.
>
>> + ret = merge_strategies_one_file(the_repository,
>> + p_orig_blob, p_our_blob, p_their_blob, argv[4],
>> + orig_mode, our_mode, their_mode);
>
> Here, as I said above, it is perfectly fine to pass
> the_repository().
>
>> + if (ret) {
>> + rollback_lock_file(&lock);
>> + return ret;
>> + }
>> +
>> + return write_locked_index(the_repository->index, &lock, COMMIT_LOCK);
>
> Likewise, I do not see much point in saying the_repository->index; the_index
> is a perfectly fine short-hand.
>
> -%<-
>> +static int do_merge_one_file(struct index_state *istate,
>> + const struct object_id *orig_blob,
>> + const struct object_id *our_blob,
>> + const struct object_id *their_blob, const char *path,
>> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
>> +{
>> + int ret, i, dest;
>> + mmbuffer_t result = {NULL, 0};
>> + mmfile_t mmfs[3];
>> + struct ll_merge_options merge_opts = {0};
>> + struct cache_entry *ce;
>> +
>> + if (our_mode == S_IFLNK || their_mode == S_IFLNK)
>> + return error(_("%s: Not merging symbolic link changes."), path);
>> + else if (our_mode == S_IFGITLINK || their_mode == S_IFGITLINK)
>> + return error(_("%s: Not merging conflicting submodule changes."), path);
>> +
>> + read_mmblob(mmfs + 1, our_blob);
>> + read_mmblob(mmfs + 2, their_blob);
>> +
>> + if (orig_blob) {
>> + printf(_("Auto-merging %s\n"), path);
>> + read_mmblob(mmfs + 0, orig_blob);
>> + } else {
>> + printf(_("Added %s in both, but differently.\n"), path);
>> + read_mmblob(mmfs + 0, &null_oid);
>> + }
>> +
>> + merge_opts.xdl_opts = XDL_MERGE_ZEALOUS_ALNUM;
>> + ret = ll_merge(&result, path,
>> + mmfs + 0, "orig",
>> + mmfs + 1, "our",
>> + mmfs + 2, "their",
>> + istate, &merge_opts);
>> +
>> + for (i = 0; i < 3; i++)
>> + free(mmfs[i].ptr);
>> +
>> + if (ret > 127 || !orig_blob)
>> + ret = error(_("content conflict in %s"), path);
>
> The original only checked if ret is zero or non-zero; here we
> require ret to be large. Intended?
>
> ll_merge() that called ll_xdl_merge() (i.e. the most common case)
> would return the value returned from xdl_merge(), which can be -1
> when we got an error before calling xdl_do_merge(). xdl_do_merge()
> in turn can return -1. The most common case returns the value
> returned from xdl_cleanup_merge(), which is 0 for clean merge, and
> any positive integer (not clipped to 127 or 128) for conflicted one.
>
Huh, not sure why I did this, and I'm puzzled that it did not broke
anything.
>> + /* Create the working tree file, using "our tree" version from
>> + the index, and then store the result of the merge. */
>
> Style. (cf. Documentation/CodingGuidelines).
>
>> + ce = index_file_exists(istate, path, strlen(path), 0);
>> + if (!ce)
>> + BUG("file is not present in the cache?");
>> +
>> + unlink(path);
>> + dest = open(path, O_WRONLY | O_CREAT, ce->ce_mode);
>> + write_in_full(dest, result.ptr, result.size);
>
> If open() fails, we write to a bogus file descriptor here.
>
>> + close(dest);
>> +
>> + free(result.ptr);
>> +
>> + if (ret && our_mode != their_mode)
>> + return error(_("permission conflict: %o->%o,%o in %s"),
>> + orig_mode, our_mode, their_mode, path);
>> + if (ret)
>> + return 1;
>
> What is the error returning convention around here? Our usual
> convention is that 0 signals a success, and negative reports an
> error. Returning the value returned from add_file_to_index() below,
> and error() above, are consistent with the convention, but this one
> returns 1 that is not. When deviating from convention, it needs to
> be documented for the callers in a comment before the function
> definition.
>
I stayed to close to the shell script on this one…
Note that this is not the case for "resolve" and "octopus", they use the
convention for merge backends, documented in builtin/merge.c:
> /*
> * The backend exits with 1 when conflicts are
> * left to be resolved, with 2 when it does not
> * handle the given merge at all.
> */
(In practice, it looks like any non-zero value lower than 2 indicates a
merge conflict, any value greater or equal to 2 is a general failure.)
>> +
>> + return add_file_to_index(istate, path, 0);
>> +}
>
>
>
>> +int merge_strategies_one_file(struct repository *r,
>> + const struct object_id *orig_blob,
>> + const struct object_id *our_blob,
>> + const struct object_id *their_blob, const char *path,
>> + unsigned int orig_mode, unsigned int our_mode,
>> + unsigned int their_mode)
>> +{
>> + if (orig_blob &&
>> + ((!their_blob && our_blob && oideq(orig_blob, our_blob)) ||
>> + (!our_blob && their_blob && oideq(orig_blob, their_blob))))
>> + /* Deleted in both or deleted in one and unchanged in
>> + the other */
>> + return merge_one_file_deleted(r->index,
>> + orig_blob, our_blob, their_blob, path,
>> + orig_mode, our_mode, their_mode);
>> + else if (!orig_blob && our_blob && !their_blob) {
>> + /* Added in one. The other side did not add and we
>> + added so there is nothing to be done, except making
>> + the path merged. */
>> + return add_to_index_cacheinfo(r->index, our_mode, our_blob, path);
>> + } else if (!orig_blob && !our_blob && their_blob) {
>> + printf(_("Adding %s\n"), path);
>> +
>> + if (file_exists(path))
>> + return error(_("untracked %s is overwritten by the merge."), path);
>> +
>> + if (add_to_index_cacheinfo(r->index, their_mode, their_blob, path))
>> + return 1;
>> + return checkout_from_index(r->index, path);
>> + } else if (!orig_blob && our_blob && their_blob &&
>> + oideq(our_blob, their_blob)) {
>> + /* Added in both, identically (check for same
>> + permissions). */
>> + if (our_mode != their_mode)
>> + return error(_("File %s added identically in both branches, "
>> + "but permissions conflict %o->%o."),
>> + path, our_mode, their_mode);
>> +
>> + printf(_("Adding %s\n"), path);
>> +
>> + if (add_to_index_cacheinfo(r->index, our_mode, our_blob, path))
>> + return 1;
>> + return checkout_from_index(r->index, path);
>> + } else if (our_blob && their_blob)
>> + /* Modified in both, but differently. */
>> + return do_merge_one_file(r->index,
>> + orig_blob, our_blob, their_blob, path,
>> + orig_mode, our_mode, their_mode);
>> + else {
>> + char *orig_hex = "", *our_hex = "", *their_hex = "";
>> +
>> + if (orig_blob)
>> + orig_hex = oid_to_hex(orig_blob);
>> + if (our_blob)
>> + our_hex = oid_to_hex(our_blob);
>> + if (their_blob)
>> + their_hex = oid_to_hex(their_blob);
>
> Prepare three char [] buffers and use oid_to_hex_r() instead,
> instead of relying that we'd have sufficient number of entries in
> the rotating buffer.
>
>> + return error(_("%s: Not handling case %s -> %s -> %s"),
>> + path, orig_hex, our_hex, their_hex);
>> + }
>> +
>> + return 0;
>> +}
>> diff --git a/merge-strategies.h b/merge-strategies.h
>> new file mode 100644
>> index 0000000000..b527d145c7
>> --- /dev/null
>> +++ b/merge-strategies.h
>> @@ -0,0 +1,13 @@
>> +#ifndef MERGE_STRATEGIES_H
>> +#define MERGE_STRATEGIES_H
>> +
>> +#include "object.h"
>> +
>> +int merge_strategies_one_file(struct repository *r,
>> + const struct object_id *orig_blob,
>> + const struct object_id *our_blob,
>> + const struct object_id *their_blob, const char *path,
>> + unsigned int orig_mode, unsigned int our_mode,
>> + unsigned int their_mode);
>> +
>> +#endif /* MERGE_STRATEGIES_H */
>> diff --git a/t/t6415-merge-dir-to-symlink.sh b/t/t6415-merge-dir-to-symlink.sh
>> index 2eddcc7664..5fb74e39a0 100755
>> --- a/t/t6415-merge-dir-to-symlink.sh
>> +++ b/t/t6415-merge-dir-to-symlink.sh
>> @@ -94,7 +94,7 @@ test_expect_success SYMLINKS 'a/b was resolved as symlink' '
>> test -h a/b
>> '
>>
>> -test_expect_failure 'do not lose untracked in merge (resolve)' '
>> +test_expect_success 'do not lose untracked in merge (resolve)' '
>> git reset --hard &&
>> git checkout baseline^0 &&
>> >a/b/c/e &&
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [PATCH v2 03/11] merge-index: libify merge_one_path() and merge_all()
2020-09-01 21:11 ` Junio C Hamano
@ 2020-09-02 15:37 ` Alban Gruin
0 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-09-02 15:37 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git, phillip.wood
Le 01/09/2020 à 23:11, Junio C Hamano a écrit :
> Alban Gruin <alban.gruin@gmail.com> writes:
>
>> The "resolve" and "octopus" merge strategies do not call directly `git
>> merge-one-file', they delegate the work to another git command, `git
>> merge-index', that will loop over files in the index and call the
>> specified command. Unfortunately, these functions are not part of
>> libgit.a, which means that once rewritten, the strategies would still
>> have to invoke `merge-one-file' by spawning a new process first.
>>
>> To avoid this, this moves merge_one_path(), merge_all(), and their
>> helpers to merge-strategies.c. They also take a callback to dictate
>> what they should do for each file. For now, only one launching a new
>> process is defined to preserve the behaviour of the builtin version.
>
> ... of the "builtin" version? I thought this series is introducing
> a new builtin version? Puzzled...
>
`merge-index' is already a builtin, this step libifies it. Its core
feature is to call repeatedly a command (usually it's
`git-merge-one-file'), but the new version will call a callback instead,
so its behaviour is not hardcoded. This patch only provides a callback
starting a new command to preserve its behaviour.
Perhaps rewording the last sentence like this would be better?
For now, only one launching a new process is defined, to preserve the
behaviour of `merge-index'.
^ permalink raw reply [flat|nested] 221+ messages in thread
* [PATCH v3 00/11] Rewrite the remaining merge strategies from shell to C
2020-09-01 10:56 ` [PATCH v2 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (10 preceding siblings ...)
2020-09-01 10:57 ` [PATCH v2 11/11] sequencer: use the "octopus" merge " Alban Gruin
@ 2020-10-05 12:26 ` Alban Gruin
2020-10-05 12:26 ` [PATCH v3 01/11] t6027: modernise tests Alban Gruin
` (12 more replies)
11 siblings, 13 replies; 221+ messages in thread
From: Alban Gruin @ 2020-10-05 12:26 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
In a effort to reduce the number of shell scripts in git's codebase, I
propose this patch series converting the two remaining merge strategies,
resolve and octopus, from shell to C. This will enable slightly better
performance, better integration with git itself (no more forking to
perform these operations), better portability (Windows and shell scripts
don't mix well).
Three scripts are actually converted: first git-merge-one-file.sh, then
git-merge-resolve.sh, and finally git-merge-octopus.sh. Not only they
are converted, but they also are modified to operate without forking,
and then libified so they can be used by git without spawning another
process.
The first patch is not important to make the whole series work, but I
made this patch while working on it.
This series keeps the commands `git merge-one-file', `git
merge-resolve', and `git merge-octopus', so any script depending on them
should keep working without any changes.
This series is based on 306ee63a70 (Eighteenth batch, 2020-09-29). The
tip is tagged as "rewrite-merge-strategies-v3" at
https://github.com/agrn/git.
Changes since v2:
- Enable `USE_THE_INDEX_COMPATIBILITY_MACROS' in merge-one-file.c and
use read_cache() and hold_locked_index() instead of repo_read_index()
and repo_hold_locked_index() to improve readability.
- Move file mode parsing to its own function in merge-one-file.c.
- Improve IO errors handling in do_merge_one_file().
- Return -1 instead of 1 when erroring out in do_merge_one_file() and
merge_strategies_one_file().
- Use oid_to_hex_r() instead of oid_to_hex() in do_merge_one_file().
- Reformat multilines comments.
- Reworded a sentence in commit 3/11.
Alban Gruin (11):
t6027: modernise tests
merge-one-file: rewrite in C
merge-index: libify merge_one_path() and merge_all()
merge-index: don't fork if the requested program is
`git-merge-one-file'
merge-resolve: rewrite in C
merge-recursive: move better_branch_name() to merge.c
merge-octopus: rewrite in C
merge: use the "resolve" strategy without forking
merge: use the "octopus" strategy without forking
sequencer: use the "resolve" strategy without forking
sequencer: use the "octopus" merge strategy without forking
Makefile | 7 +-
builtin.h | 3 +
builtin/merge-index.c | 102 ++----
builtin/merge-octopus.c | 69 ++++
builtin/merge-one-file.c | 92 +++++
builtin/merge-recursive.c | 16 +-
builtin/merge-resolve.c | 69 ++++
builtin/merge.c | 9 +-
cache.h | 2 +-
git-merge-octopus.sh | 112 ------
git-merge-one-file.sh | 167 ---------
git-merge-resolve.sh | 54 ---
git.c | 3 +
merge-strategies.c | 613 ++++++++++++++++++++++++++++++++
merge-strategies.h | 44 +++
merge.c | 12 +
sequencer.c | 16 +-
t/t6407-merge-binary.sh | 27 +-
t/t6415-merge-dir-to-symlink.sh | 2 +-
19 files changed, 972 insertions(+), 447 deletions(-)
create mode 100644 builtin/merge-octopus.c
create mode 100644 builtin/merge-one-file.c
create mode 100644 builtin/merge-resolve.c
delete mode 100755 git-merge-octopus.sh
delete mode 100755 git-merge-one-file.sh
delete mode 100755 git-merge-resolve.sh
create mode 100644 merge-strategies.c
create mode 100644 merge-strategies.h
Range-diff against v2:
1: 28c8fd11b6 = 1: 08c7df596a t6027: modernise tests
2: f5ab0fdf0a ! 2: ce911c99c0 merge-one-file: rewrite in C
@@ builtin/merge-one-file.c (new)
+ * that might change the tree layout.
+ */
+
++#define USE_THE_INDEX_COMPATIBILITY_MACROS
+#include "cache.h"
+#include "builtin.h"
+#include "lockfile.h"
@@ builtin/merge-one-file.c (new)
+ "<orig mode> <our mode> <their mode>\n\n"
+ "Blob ids and modes should be empty for missing files.";
+
++static int read_mode(const char *name, const char *arg, unsigned int *mode)
++{
++ char *last;
++ int ret = 0;
++
++ *mode = strtol(arg, &last, 8);
++
++ if (*last)
++ ret = error(_("invalid '%s' mode: expected nothing, got '%c'"), name, *last);
++ else if (!(S_ISREG(*mode) || S_ISDIR(*mode) || S_ISLNK(*mode)))
++ ret = error(_("invalid '%s' mode: %o"), name, *mode);
++
++ return ret;
++}
++
+int cmd_merge_one_file(int argc, const char **argv, const char *prefix)
+{
+ struct object_id orig_blob, our_blob, their_blob,
@@ builtin/merge-one-file.c (new)
+ if (argc != 8)
+ usage(builtin_merge_one_file_usage);
+
-+ if (repo_read_index(the_repository) < 0)
++ if (read_cache() < 0)
+ die("invalid index");
+
-+ repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
++ hold_locked_index(&lock, LOCK_DIE_ON_ERROR);
+
+ if (!get_oid(argv[1], &orig_blob)) {
+ p_orig_blob = &orig_blob;
-+ orig_mode = strtol(argv[5], NULL, 8);
-+
-+ if (!(S_ISREG(orig_mode) || S_ISDIR(orig_mode) || S_ISLNK(orig_mode)))
-+ ret |= error(_("invalid 'orig' mode: %o"), orig_mode);
++ ret = read_mode("orig", argv[5], &orig_mode);
+ }
+
+ if (!get_oid(argv[2], &our_blob)) {
+ p_our_blob = &our_blob;
-+ our_mode = strtol(argv[6], NULL, 8);
-+
-+ if (!(S_ISREG(our_mode) || S_ISDIR(our_mode) || S_ISLNK(our_mode)))
-+ ret |= error(_("invalid 'our' mode: %o"), our_mode);
++ ret = read_mode("our", argv[6], &our_mode);
+ }
+
+ if (!get_oid(argv[3], &their_blob)) {
+ p_their_blob = &their_blob;
-+ their_mode = strtol(argv[7], NULL, 8);
-+
-+ if (!(S_ISREG(their_mode) || S_ISDIR(their_mode) || S_ISLNK(their_mode)))
-+ ret = error(_("invalid 'their' mode: %o"), their_mode);
++ ret = read_mode("their", argv[7], &their_mode);
+ }
+
+ if (ret)
@@ builtin/merge-one-file.c (new)
+
+ if (ret) {
+ rollback_lock_file(&lock);
-+ return ret;
++ return !!ret;
+ }
+
-+ return write_locked_index(the_repository->index, &lock, COMMIT_LOCK);
++ return write_locked_index(&the_index, &lock, COMMIT_LOCK);
+}
## git-merge-one-file.sh (deleted) ##
@@ merge-strategies.c (new)
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
+{
+ int ret, i, dest;
++ ssize_t written;
+ mmbuffer_t result = {NULL, 0};
+ mmfile_t mmfs[3];
+ struct ll_merge_options merge_opts = {0};
@@ merge-strategies.c (new)
+ for (i = 0; i < 3; i++)
+ free(mmfs[i].ptr);
+
-+ if (ret > 127 || !orig_blob)
-+ ret = error(_("content conflict in %s"), path);
++ if (ret < 0) {
++ free(result.ptr);
++ return error(_("Failed to execute internal merge"));
++ }
+
-+ /* Create the working tree file, using "our tree" version from
-+ the index, and then store the result of the merge. */
++ /*
++ * Create the working tree file, using "our tree" version from
++ * the index, and then store the result of the merge.
++ */
+ ce = index_file_exists(istate, path, strlen(path), 0);
+ if (!ce)
+ BUG("file is not present in the cache?");
+
+ unlink(path);
-+ dest = open(path, O_WRONLY | O_CREAT, ce->ce_mode);
-+ write_in_full(dest, result.ptr, result.size);
++ if ((dest = open(path, O_WRONLY | O_CREAT, ce->ce_mode)) < 0) {
++ free(result.ptr);
++ return error_errno(_("failed to open file '%s'"), path);
++ }
++
++ written = write_in_full(dest, result.ptr, result.size);
+ close(dest);
+
+ free(result.ptr);
+
-+ if (ret && our_mode != their_mode)
++ if (written < 0)
++ return error_errno(_("failed to write to '%s'"), path);
++
++ if (ret != 0 || !orig_blob)
++ ret = error(_("content conflict in %s"), path);
++ if (our_mode != their_mode)
+ return error(_("permission conflict: %o->%o,%o in %s"),
+ orig_mode, our_mode, their_mode, path);
+ if (ret)
-+ return 1;
++ return -1;
+
+ return add_file_to_index(istate, path, 0);
+}
@@ merge-strategies.c (new)
+ if (orig_blob &&
+ ((!their_blob && our_blob && oideq(orig_blob, our_blob)) ||
+ (!our_blob && their_blob && oideq(orig_blob, their_blob))))
-+ /* Deleted in both or deleted in one and unchanged in
-+ the other */
++ /* Deleted in both or deleted in one and unchanged in the other. */
+ return merge_one_file_deleted(r->index,
+ orig_blob, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+ else if (!orig_blob && our_blob && !their_blob) {
-+ /* Added in one. The other side did not add and we
-+ added so there is nothing to be done, except making
-+ the path merged. */
++ /*
++ * Added in one. The other side did not add and we
++ * added so there is nothing to be done, except making
++ * the path merged.
++ */
+ return add_to_index_cacheinfo(r->index, our_mode, our_blob, path);
+ } else if (!orig_blob && !our_blob && their_blob) {
+ printf(_("Adding %s\n"), path);
@@ merge-strategies.c (new)
+ return error(_("untracked %s is overwritten by the merge."), path);
+
+ if (add_to_index_cacheinfo(r->index, their_mode, their_blob, path))
-+ return 1;
++ return -1;
+ return checkout_from_index(r->index, path);
+ } else if (!orig_blob && our_blob && their_blob &&
+ oideq(our_blob, their_blob)) {
-+ /* Added in both, identically (check for same
-+ permissions). */
++ /* Added in both, identically (check for same permissions). */
+ if (our_mode != their_mode)
+ return error(_("File %s added identically in both branches, "
+ "but permissions conflict %o->%o."),
@@ merge-strategies.c (new)
+ printf(_("Adding %s\n"), path);
+
+ if (add_to_index_cacheinfo(r->index, our_mode, our_blob, path))
-+ return 1;
++ return -1;
+ return checkout_from_index(r->index, path);
+ } else if (our_blob && their_blob)
+ /* Modified in both, but differently. */
@@ merge-strategies.c (new)
+ orig_blob, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+ else {
-+ char *orig_hex = "", *our_hex = "", *their_hex = "";
++ char orig_hex[GIT_MAX_HEXSZ] = {0}, our_hex[GIT_MAX_HEXSZ] = {0},
++ their_hex[GIT_MAX_HEXSZ] = {0};
+
+ if (orig_blob)
-+ orig_hex = oid_to_hex(orig_blob);
++ oid_to_hex_r(orig_hex, orig_blob);
+ if (our_blob)
-+ our_hex = oid_to_hex(our_blob);
++ oid_to_hex_r(our_hex, our_blob);
+ if (their_blob)
-+ their_hex = oid_to_hex(their_blob);
++ oid_to_hex_r(their_hex, their_blob);
+
+ return error(_("%s: Not handling case %s -> %s -> %s"),
+ path, orig_hex, our_hex, their_hex);
3: 7f3ce7da17 ! 3: 7f0999f5a3 merge-index: libify merge_one_path() and merge_all()
@@ Commit message
To avoid this, this moves merge_one_path(), merge_all(), and their
helpers to merge-strategies.c. They also take a callback to dictate
- what they should do for each file. For now, only one launching a new
- process is defined to preserve the behaviour of the builtin version.
+ what they should do for each file. For now, to preserve the behaviour
+ of `merge-index', only one callback, launching a new process, is
+ defined.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
4: 07e6a6aaef = 4: c0bc05406d merge-index: don't fork if the requested program is `git-merge-one-file'
5: 117d4fc840 = 5: cbfe192982 merge-resolve: rewrite in C
6: 4fc955962b = 6: 35e386f626 merge-recursive: move better_branch_name() to merge.c
7: e7b9e15b34 ! 7: 41eb0f7199 merge-octopus: rewrite in C
@@ Makefile: BUILTIN_OBJS += builtin/mailsplit.o
BUILTIN_OBJS += builtin/merge-recursive.o
## builtin.h ##
-@@ builtin.h: int cmd_mailsplit(int argc, const char **argv, const char *prefix);
+@@ builtin.h: int cmd_maintenance(int argc, const char **argv, const char *prefix);
int cmd_merge(int argc, const char **argv, const char *prefix);
int cmd_merge_base(int argc, const char **argv, const char *prefix);
int cmd_merge_index(int argc, const char **argv, const char *prefix);
@@ builtin/merge-octopus.c (new)
+ if (repo_read_index(the_repository) < 0)
+ die("corrupted cache");
+
-+ /* The first parameters up to -- are merge bases; the rest are
-+ * heads. */
++ /*
++ * The first parameters up to -- are merge bases; the rest are
++ * heads.
++ */
+ for (i = 1; i < argc; i++) {
+ if (strcmp(argv[i], "--") == 0)
+ sep_seen = 1;
@@ builtin/merge-octopus.c (new)
+ }
+ }
+
-+ /* Reject if this is not an octopus -- resolve should be used
-+ * instead. */
++ /*
++ * Reject if this is not an octopus -- resolve should be used
++ * instead.
++ */
+ if (commit_list_count(remotes) < 2)
+ return 2;
+
@@ merge-strategies.c: int merge_strategies_resolve(struct repository *r,
+ int can_ff = 1;
+
+ if (ret) {
-+ /* We allow only last one to have a
-+ hand-resolvable conflicts. Last round failed
-+ and we still had a head to merge. */
++ /*
++ * We allow only last one to have a
++ * hand-resolvable conflicts. Last round failed
++ * and we still had a head to merge.
++ */
+ puts(_("Automated merge did not work."));
+ puts(_("Should not be doing an octopus."));
+
@@ merge-strategies.c: int merge_strategies_resolve(struct repository *r,
+ }
+
+ if (!non_ff_merge && can_ff) {
-+ /* The first head being merged was a
-+ fast-forward. Advance the reference commit
-+ to the head being merged, and use that tree
-+ as the intermediate result of the merge. We
-+ still need to count this as part of the
-+ parent set. */
++ /*
++ * The first head being merged was a
++ * fast-forward. Advance the reference commit
++ * to the head being merged, and use that tree
++ * as the intermediate result of the merge. We
++ * still need to count this as part of the
++ * parent set.
++ */
+ struct object_id oids[2];
+ printf(_("Fast-forwarding to: %s\n"), branch_name);
+
8: cd0662201d = 8: 8f6c1ac057 merge: use the "resolve" strategy without forking
9: 0525ff0183 = 9: b1125261d1 merge: use the "octopus" strategy without forking
10: 6fbf599ba4 = 10: 8d0932fd02 sequencer: use the "resolve" strategy without forking
11: 2c2dc3cc62 = 11: e304723957 sequencer: use the "octopus" merge strategy without forking
--
2.28.0.662.ge304723957
^ permalink raw reply [flat|nested] 221+ messages in thread
* [PATCH v3 01/11] t6027: modernise tests
2020-10-05 12:26 ` [PATCH v3 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
@ 2020-10-05 12:26 ` Alban Gruin
2020-10-06 20:50 ` Junio C Hamano
2020-10-05 12:26 ` [PATCH v3 02/11] merge-one-file: rewrite in C Alban Gruin
` (11 subsequent siblings)
12 siblings, 1 reply; 221+ messages in thread
From: Alban Gruin @ 2020-10-05 12:26 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
Some tests in t6027 uses a if/then/else to check if a command failed or
not, but we have the `test_must_fail' function to do it correctly for us
nowadays.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
t/t6407-merge-binary.sh | 27 ++++++---------------------
1 file changed, 6 insertions(+), 21 deletions(-)
diff --git a/t/t6407-merge-binary.sh b/t/t6407-merge-binary.sh
index 4e6c7cb77e..071d3f7343 100755
--- a/t/t6407-merge-binary.sh
+++ b/t/t6407-merge-binary.sh
@@ -5,7 +5,6 @@ test_description='ask merge-recursive to merge binary files'
. ./test-lib.sh
test_expect_success setup '
-
cat "$TEST_DIRECTORY"/test-binary-1.png >m &&
git add m &&
git ls-files -s | sed -e "s/ 0 / 1 /" >E1 &&
@@ -35,33 +34,19 @@ test_expect_success setup '
'
test_expect_success resolve '
-
rm -f a* m* &&
git reset --hard anchor &&
-
- if git merge -s resolve master
- then
- echo Oops, should not have succeeded
- false
- else
- git ls-files -s >current
- test_cmp expect current
- fi
+ test_must_fail git merge -s resolve master &&
+ git ls-files -s >current &&
+ test_cmp expect current
'
test_expect_success recursive '
-
rm -f a* m* &&
git reset --hard anchor &&
-
- if git merge -s recursive master
- then
- echo Oops, should not have succeeded
- false
- else
- git ls-files -s >current
- test_cmp expect current
- fi
+ test_must_fail git merge -s recursive master &&
+ git ls-files -s >current &&
+ test_cmp expect current
'
test_done
--
2.28.0.662.ge304723957
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v3 02/11] merge-one-file: rewrite in C
2020-10-05 12:26 ` [PATCH v3 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
2020-10-05 12:26 ` [PATCH v3 01/11] t6027: modernise tests Alban Gruin
@ 2020-10-05 12:26 ` Alban Gruin
2020-10-06 22:01 ` Junio C Hamano
2020-10-05 12:26 ` [PATCH v3 03/11] merge-index: libify merge_one_path() and merge_all() Alban Gruin
` (10 subsequent siblings)
12 siblings, 1 reply; 221+ messages in thread
From: Alban Gruin @ 2020-10-05 12:26 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
This rewrites `git merge-one-file' from shell to C. This port is not
completely straightforward: to save precious cycles by avoiding reading
and flushing the index repeatedly, write temporary files when an
operation can be performed in-memory, or allow other function to use the
rewrite without forking nor worrying about the index, the calls to
external processes are replaced by calls to functions in libgit.a:
- calls to `update-index --add --cacheinfo' are replaced by calls to
add_cache_entry();
- calls to `update-index --remove' are replaced by calls to
remove_file_from_cache();
- calls to `checkout-index -u -f' are replaced by calls to
checkout_entry();
- calls to `unpack-file' and `merge-files' are replaced by calls to
read_mmblob() and ll_merge(), respectively, to merge files
in-memory;
- calls to `checkout-index -f --stage=2' are replaced by calls to
cache_file_exists();
- calls to `update-index' are replaced by calls to add_file_to_cache().
The bulk of the rewrite is done in a new file in libgit.a,
merge-strategies.c. This will enable the resolve and octopus strategies
to directly call it instead of forking.
This also fixes a bug present in the original script: instead of
checking if a _regular_ file exists when a file exists in the branch to
merge, but not in our branch, the rewritten version checks if a file of
any kind (ie. a directory, ...) exists. This fixes the tests t6035.14,
where the branch to merge had a new file, `a/b', but our branch had a
directory there; it should have failed because a directory exists, but
it did not because there was no regular file called `a/b'. This test is
now marked as successful.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Makefile | 3 +-
builtin.h | 1 +
builtin/merge-one-file.c | 92 ++++++++++++++
git-merge-one-file.sh | 167 -------------------------
git.c | 1 +
merge-strategies.c | 214 ++++++++++++++++++++++++++++++++
merge-strategies.h | 13 ++
t/t6415-merge-dir-to-symlink.sh | 2 +-
8 files changed, 324 insertions(+), 169 deletions(-)
create mode 100644 builtin/merge-one-file.c
delete mode 100755 git-merge-one-file.sh
create mode 100644 merge-strategies.c
create mode 100644 merge-strategies.h
diff --git a/Makefile b/Makefile
index de53954590..6dfdb33cb2 100644
--- a/Makefile
+++ b/Makefile
@@ -601,7 +601,6 @@ SCRIPT_SH += git-bisect.sh
SCRIPT_SH += git-difftool--helper.sh
SCRIPT_SH += git-filter-branch.sh
SCRIPT_SH += git-merge-octopus.sh
-SCRIPT_SH += git-merge-one-file.sh
SCRIPT_SH += git-merge-resolve.sh
SCRIPT_SH += git-mergetool.sh
SCRIPT_SH += git-quiltimport.sh
@@ -909,6 +908,7 @@ LIB_OBJS += match-trees.o
LIB_OBJS += mem-pool.o
LIB_OBJS += merge-blobs.o
LIB_OBJS += merge-recursive.o
+LIB_OBJS += merge-strategies.o
LIB_OBJS += merge.o
LIB_OBJS += mergesort.o
LIB_OBJS += midx.o
@@ -1094,6 +1094,7 @@ BUILTIN_OBJS += builtin/mailsplit.o
BUILTIN_OBJS += builtin/merge-base.o
BUILTIN_OBJS += builtin/merge-file.o
BUILTIN_OBJS += builtin/merge-index.o
+BUILTIN_OBJS += builtin/merge-one-file.o
BUILTIN_OBJS += builtin/merge-ours.o
BUILTIN_OBJS += builtin/merge-recursive.o
BUILTIN_OBJS += builtin/merge-tree.o
diff --git a/builtin.h b/builtin.h
index 53fb290963..4d2cd78856 100644
--- a/builtin.h
+++ b/builtin.h
@@ -178,6 +178,7 @@ int cmd_merge_base(int argc, const char **argv, const char *prefix);
int cmd_merge_index(int argc, const char **argv, const char *prefix);
int cmd_merge_ours(int argc, const char **argv, const char *prefix);
int cmd_merge_file(int argc, const char **argv, const char *prefix);
+int cmd_merge_one_file(int argc, const char **argv, const char *prefix);
int cmd_merge_recursive(int argc, const char **argv, const char *prefix);
int cmd_merge_tree(int argc, const char **argv, const char *prefix);
int cmd_mktag(int argc, const char **argv, const char *prefix);
diff --git a/builtin/merge-one-file.c b/builtin/merge-one-file.c
new file mode 100644
index 0000000000..598338ba16
--- /dev/null
+++ b/builtin/merge-one-file.c
@@ -0,0 +1,92 @@
+/*
+ * Builtin "git merge-one-file"
+ *
+ * Copyright (c) 2020 Alban Gruin
+ *
+ * Based on git-merge-one-file.sh, written by Linus Torvalds.
+ *
+ * This is the git per-file merge utility, called with
+ *
+ * argv[1] - original file SHA1 (or empty)
+ * argv[2] - file in branch1 SHA1 (or empty)
+ * argv[3] - file in branch2 SHA1 (or empty)
+ * argv[4] - pathname in repository
+ * argv[5] - original file mode (or empty)
+ * argv[6] - file in branch1 mode (or empty)
+ * argv[7] - file in branch2 mode (or empty)
+ *
+ * Handle some trivial cases. The _really_ trivial cases have been
+ * handled already by git read-tree, but that one doesn't do any merges
+ * that might change the tree layout.
+ */
+
+#define USE_THE_INDEX_COMPATIBILITY_MACROS
+#include "cache.h"
+#include "builtin.h"
+#include "lockfile.h"
+#include "merge-strategies.h"
+
+static const char builtin_merge_one_file_usage[] =
+ "git merge-one-file <orig blob> <our blob> <their blob> <path> "
+ "<orig mode> <our mode> <their mode>\n\n"
+ "Blob ids and modes should be empty for missing files.";
+
+static int read_mode(const char *name, const char *arg, unsigned int *mode)
+{
+ char *last;
+ int ret = 0;
+
+ *mode = strtol(arg, &last, 8);
+
+ if (*last)
+ ret = error(_("invalid '%s' mode: expected nothing, got '%c'"), name, *last);
+ else if (!(S_ISREG(*mode) || S_ISDIR(*mode) || S_ISLNK(*mode)))
+ ret = error(_("invalid '%s' mode: %o"), name, *mode);
+
+ return ret;
+}
+
+int cmd_merge_one_file(int argc, const char **argv, const char *prefix)
+{
+ struct object_id orig_blob, our_blob, their_blob,
+ *p_orig_blob = NULL, *p_our_blob = NULL, *p_their_blob = NULL;
+ unsigned int orig_mode = 0, our_mode = 0, their_mode = 0, ret = 0;
+ struct lock_file lock = LOCK_INIT;
+
+ if (argc != 8)
+ usage(builtin_merge_one_file_usage);
+
+ if (read_cache() < 0)
+ die("invalid index");
+
+ hold_locked_index(&lock, LOCK_DIE_ON_ERROR);
+
+ if (!get_oid(argv[1], &orig_blob)) {
+ p_orig_blob = &orig_blob;
+ ret = read_mode("orig", argv[5], &orig_mode);
+ }
+
+ if (!get_oid(argv[2], &our_blob)) {
+ p_our_blob = &our_blob;
+ ret = read_mode("our", argv[6], &our_mode);
+ }
+
+ if (!get_oid(argv[3], &their_blob)) {
+ p_their_blob = &their_blob;
+ ret = read_mode("their", argv[7], &their_mode);
+ }
+
+ if (ret)
+ return ret;
+
+ ret = merge_strategies_one_file(the_repository,
+ p_orig_blob, p_our_blob, p_their_blob, argv[4],
+ orig_mode, our_mode, their_mode);
+
+ if (ret) {
+ rollback_lock_file(&lock);
+ return !!ret;
+ }
+
+ return write_locked_index(&the_index, &lock, COMMIT_LOCK);
+}
diff --git a/git-merge-one-file.sh b/git-merge-one-file.sh
deleted file mode 100755
index f6d9852d2f..0000000000
--- a/git-merge-one-file.sh
+++ /dev/null
@@ -1,167 +0,0 @@
-#!/bin/sh
-#
-# Copyright (c) Linus Torvalds, 2005
-#
-# This is the git per-file merge script, called with
-#
-# $1 - original file SHA1 (or empty)
-# $2 - file in branch1 SHA1 (or empty)
-# $3 - file in branch2 SHA1 (or empty)
-# $4 - pathname in repository
-# $5 - original file mode (or empty)
-# $6 - file in branch1 mode (or empty)
-# $7 - file in branch2 mode (or empty)
-#
-# Handle some trivial cases.. The _really_ trivial cases have
-# been handled already by git read-tree, but that one doesn't
-# do any merges that might change the tree layout.
-
-USAGE='<orig blob> <our blob> <their blob> <path>'
-USAGE="$USAGE <orig mode> <our mode> <their mode>"
-LONG_USAGE="usage: git merge-one-file $USAGE
-
-Blob ids and modes should be empty for missing files."
-
-SUBDIRECTORY_OK=Yes
-. git-sh-setup
-cd_to_toplevel
-require_work_tree
-
-if test $# != 7
-then
- echo "$LONG_USAGE"
- exit 1
-fi
-
-case "${1:-.}${2:-.}${3:-.}" in
-#
-# Deleted in both or deleted in one and unchanged in the other
-#
-"$1.." | "$1.$1" | "$1$1.")
- if { test -z "$6" && test "$5" != "$7"; } ||
- { test -z "$7" && test "$5" != "$6"; }
- then
- echo "ERROR: File $4 deleted on one branch but had its" >&2
- echo "ERROR: permissions changed on the other." >&2
- exit 1
- fi
-
- if test -n "$2"
- then
- echo "Removing $4"
- else
- # read-tree checked that index matches HEAD already,
- # so we know we do not have this path tracked.
- # there may be an unrelated working tree file here,
- # which we should just leave unmolested. Make sure
- # we do not have it in the index, though.
- exec git update-index --remove -- "$4"
- fi
- if test -f "$4"
- then
- rm -f -- "$4" &&
- rmdir -p "$(expr "z$4" : 'z\(.*\)/')" 2>/dev/null || :
- fi &&
- exec git update-index --remove -- "$4"
- ;;
-
-#
-# Added in one.
-#
-".$2.")
- # the other side did not add and we added so there is nothing
- # to be done, except making the path merged.
- exec git update-index --add --cacheinfo "$6" "$2" "$4"
- ;;
-"..$3")
- echo "Adding $4"
- if test -f "$4"
- then
- echo "ERROR: untracked $4 is overwritten by the merge." >&2
- exit 1
- fi
- git update-index --add --cacheinfo "$7" "$3" "$4" &&
- exec git checkout-index -u -f -- "$4"
- ;;
-
-#
-# Added in both, identically (check for same permissions).
-#
-".$3$2")
- if test "$6" != "$7"
- then
- echo "ERROR: File $4 added identically in both branches," >&2
- echo "ERROR: but permissions conflict $6->$7." >&2
- exit 1
- fi
- echo "Adding $4"
- git update-index --add --cacheinfo "$6" "$2" "$4" &&
- exec git checkout-index -u -f -- "$4"
- ;;
-
-#
-# Modified in both, but differently.
-#
-"$1$2$3" | ".$2$3")
-
- case ",$6,$7," in
- *,120000,*)
- echo "ERROR: $4: Not merging symbolic link changes." >&2
- exit 1
- ;;
- *,160000,*)
- echo "ERROR: $4: Not merging conflicting submodule changes." >&2
- exit 1
- ;;
- esac
-
- src1=$(git unpack-file $2)
- src2=$(git unpack-file $3)
- case "$1" in
- '')
- echo "Added $4 in both, but differently."
- orig=$(git unpack-file $(git hash-object /dev/null))
- ;;
- *)
- echo "Auto-merging $4"
- orig=$(git unpack-file $1)
- ;;
- esac
-
- git merge-file "$src1" "$orig" "$src2"
- ret=$?
- msg=
- if test $ret != 0 || test -z "$1"
- then
- msg='content conflict'
- ret=1
- fi
-
- # Create the working tree file, using "our tree" version from the
- # index, and then store the result of the merge.
- git checkout-index -f --stage=2 -- "$4" && cat "$src1" >"$4" || exit 1
- rm -f -- "$orig" "$src1" "$src2"
-
- if test "$6" != "$7"
- then
- if test -n "$msg"
- then
- msg="$msg, "
- fi
- msg="${msg}permissions conflict: $5->$6,$7"
- ret=1
- fi
-
- if test $ret != 0
- then
- echo "ERROR: $msg in $4" >&2
- exit 1
- fi
- exec git update-index -- "$4"
- ;;
-
-*)
- echo "ERROR: $4: Not handling case $1 -> $2 -> $3" >&2
- ;;
-esac
-exit 1
diff --git a/git.c b/git.c
index f1e8b56d99..a4d3f98094 100644
--- a/git.c
+++ b/git.c
@@ -540,6 +540,7 @@ static struct cmd_struct commands[] = {
{ "merge-file", cmd_merge_file, RUN_SETUP_GENTLY },
{ "merge-index", cmd_merge_index, RUN_SETUP | NO_PARSEOPT },
{ "merge-ours", cmd_merge_ours, RUN_SETUP | NO_PARSEOPT },
+ { "merge-one-file", cmd_merge_one_file, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-ours", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-theirs", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
diff --git a/merge-strategies.c b/merge-strategies.c
new file mode 100644
index 0000000000..bbe6f48698
--- /dev/null
+++ b/merge-strategies.c
@@ -0,0 +1,214 @@
+#include "cache.h"
+#include "dir.h"
+#include "ll-merge.h"
+#include "merge-strategies.h"
+#include "xdiff-interface.h"
+
+static int add_to_index_cacheinfo(struct index_state *istate,
+ unsigned int mode,
+ const struct object_id *oid, const char *path)
+{
+ struct cache_entry *ce;
+ int len, option;
+
+ if (!verify_path(path, mode))
+ return error(_("Invalid path '%s'"), path);
+
+ len = strlen(path);
+ ce = make_empty_cache_entry(istate, len);
+
+ oidcpy(&ce->oid, oid);
+ memcpy(ce->name, path, len);
+ ce->ce_flags = create_ce_flags(0);
+ ce->ce_namelen = len;
+ ce->ce_mode = create_ce_mode(mode);
+ if (assume_unchanged)
+ ce->ce_flags |= CE_VALID;
+ option = ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE;
+ if (add_index_entry(istate, ce, option))
+ return error(_("%s: cannot add to the index"), path);
+
+ return 0;
+}
+
+static int checkout_from_index(struct index_state *istate, const char *path)
+{
+ struct checkout state = CHECKOUT_INIT;
+ struct cache_entry *ce;
+
+ state.istate = istate;
+ state.force = 1;
+ state.base_dir = "";
+ state.base_dir_len = 0;
+
+ ce = index_file_exists(istate, path, strlen(path), 0);
+ if (checkout_entry(ce, &state, NULL, NULL) < 0)
+ return error(_("%s: cannot checkout file"), path);
+ return 0;
+}
+
+static int merge_one_file_deleted(struct index_state *istate,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
+{
+ if ((our_blob && orig_mode != our_mode) ||
+ (their_blob && orig_mode != their_mode))
+ return error(_("File %s deleted on one branch but had its "
+ "permissions changed on the other."), path);
+
+ if (our_blob) {
+ printf(_("Removing %s\n"), path);
+
+ if (file_exists(path))
+ remove_path(path);
+ }
+
+ if (remove_file_from_index(istate, path))
+ return error("%s: cannot remove from the index", path);
+ return 0;
+}
+
+static int do_merge_one_file(struct index_state *istate,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
+{
+ int ret, i, dest;
+ ssize_t written;
+ mmbuffer_t result = {NULL, 0};
+ mmfile_t mmfs[3];
+ struct ll_merge_options merge_opts = {0};
+ struct cache_entry *ce;
+
+ if (our_mode == S_IFLNK || their_mode == S_IFLNK)
+ return error(_("%s: Not merging symbolic link changes."), path);
+ else if (our_mode == S_IFGITLINK || their_mode == S_IFGITLINK)
+ return error(_("%s: Not merging conflicting submodule changes."), path);
+
+ read_mmblob(mmfs + 1, our_blob);
+ read_mmblob(mmfs + 2, their_blob);
+
+ if (orig_blob) {
+ printf(_("Auto-merging %s\n"), path);
+ read_mmblob(mmfs + 0, orig_blob);
+ } else {
+ printf(_("Added %s in both, but differently.\n"), path);
+ read_mmblob(mmfs + 0, &null_oid);
+ }
+
+ merge_opts.xdl_opts = XDL_MERGE_ZEALOUS_ALNUM;
+ ret = ll_merge(&result, path,
+ mmfs + 0, "orig",
+ mmfs + 1, "our",
+ mmfs + 2, "their",
+ istate, &merge_opts);
+
+ for (i = 0; i < 3; i++)
+ free(mmfs[i].ptr);
+
+ if (ret < 0) {
+ free(result.ptr);
+ return error(_("Failed to execute internal merge"));
+ }
+
+ /*
+ * Create the working tree file, using "our tree" version from
+ * the index, and then store the result of the merge.
+ */
+ ce = index_file_exists(istate, path, strlen(path), 0);
+ if (!ce)
+ BUG("file is not present in the cache?");
+
+ unlink(path);
+ if ((dest = open(path, O_WRONLY | O_CREAT, ce->ce_mode)) < 0) {
+ free(result.ptr);
+ return error_errno(_("failed to open file '%s'"), path);
+ }
+
+ written = write_in_full(dest, result.ptr, result.size);
+ close(dest);
+
+ free(result.ptr);
+
+ if (written < 0)
+ return error_errno(_("failed to write to '%s'"), path);
+
+ if (ret != 0 || !orig_blob)
+ ret = error(_("content conflict in %s"), path);
+ if (our_mode != their_mode)
+ return error(_("permission conflict: %o->%o,%o in %s"),
+ orig_mode, our_mode, their_mode, path);
+ if (ret)
+ return -1;
+
+ return add_file_to_index(istate, path, 0);
+}
+
+int merge_strategies_one_file(struct repository *r,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode,
+ unsigned int their_mode)
+{
+ if (orig_blob &&
+ ((!their_blob && our_blob && oideq(orig_blob, our_blob)) ||
+ (!our_blob && their_blob && oideq(orig_blob, their_blob))))
+ /* Deleted in both or deleted in one and unchanged in the other. */
+ return merge_one_file_deleted(r->index,
+ orig_blob, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+ else if (!orig_blob && our_blob && !their_blob) {
+ /*
+ * Added in one. The other side did not add and we
+ * added so there is nothing to be done, except making
+ * the path merged.
+ */
+ return add_to_index_cacheinfo(r->index, our_mode, our_blob, path);
+ } else if (!orig_blob && !our_blob && their_blob) {
+ printf(_("Adding %s\n"), path);
+
+ if (file_exists(path))
+ return error(_("untracked %s is overwritten by the merge."), path);
+
+ if (add_to_index_cacheinfo(r->index, their_mode, their_blob, path))
+ return -1;
+ return checkout_from_index(r->index, path);
+ } else if (!orig_blob && our_blob && their_blob &&
+ oideq(our_blob, their_blob)) {
+ /* Added in both, identically (check for same permissions). */
+ if (our_mode != their_mode)
+ return error(_("File %s added identically in both branches, "
+ "but permissions conflict %o->%o."),
+ path, our_mode, their_mode);
+
+ printf(_("Adding %s\n"), path);
+
+ if (add_to_index_cacheinfo(r->index, our_mode, our_blob, path))
+ return -1;
+ return checkout_from_index(r->index, path);
+ } else if (our_blob && their_blob)
+ /* Modified in both, but differently. */
+ return do_merge_one_file(r->index,
+ orig_blob, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+ else {
+ char orig_hex[GIT_MAX_HEXSZ] = {0}, our_hex[GIT_MAX_HEXSZ] = {0},
+ their_hex[GIT_MAX_HEXSZ] = {0};
+
+ if (orig_blob)
+ oid_to_hex_r(orig_hex, orig_blob);
+ if (our_blob)
+ oid_to_hex_r(our_hex, our_blob);
+ if (their_blob)
+ oid_to_hex_r(their_hex, their_blob);
+
+ return error(_("%s: Not handling case %s -> %s -> %s"),
+ path, orig_hex, our_hex, their_hex);
+ }
+
+ return 0;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
new file mode 100644
index 0000000000..b527d145c7
--- /dev/null
+++ b/merge-strategies.h
@@ -0,0 +1,13 @@
+#ifndef MERGE_STRATEGIES_H
+#define MERGE_STRATEGIES_H
+
+#include "object.h"
+
+int merge_strategies_one_file(struct repository *r,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode,
+ unsigned int their_mode);
+
+#endif /* MERGE_STRATEGIES_H */
diff --git a/t/t6415-merge-dir-to-symlink.sh b/t/t6415-merge-dir-to-symlink.sh
index 2eddcc7664..5fb74e39a0 100755
--- a/t/t6415-merge-dir-to-symlink.sh
+++ b/t/t6415-merge-dir-to-symlink.sh
@@ -94,7 +94,7 @@ test_expect_success SYMLINKS 'a/b was resolved as symlink' '
test -h a/b
'
-test_expect_failure 'do not lose untracked in merge (resolve)' '
+test_expect_success 'do not lose untracked in merge (resolve)' '
git reset --hard &&
git checkout baseline^0 &&
>a/b/c/e &&
--
2.28.0.662.ge304723957
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v3 03/11] merge-index: libify merge_one_path() and merge_all()
2020-10-05 12:26 ` [PATCH v3 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
2020-10-05 12:26 ` [PATCH v3 01/11] t6027: modernise tests Alban Gruin
2020-10-05 12:26 ` [PATCH v3 02/11] merge-one-file: rewrite in C Alban Gruin
@ 2020-10-05 12:26 ` Alban Gruin
2020-10-09 4:48 ` Junio C Hamano
2020-10-05 12:26 ` [PATCH v3 04/11] merge-index: don't fork if the requested program is `git-merge-one-file' Alban Gruin
` (9 subsequent siblings)
12 siblings, 1 reply; 221+ messages in thread
From: Alban Gruin @ 2020-10-05 12:26 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
The "resolve" and "octopus" merge strategies do not call directly `git
merge-one-file', they delegate the work to another git command, `git
merge-index', that will loop over files in the index and call the
specified command. Unfortunately, these functions are not part of
libgit.a, which means that once rewritten, the strategies would still
have to invoke `merge-one-file' by spawning a new process first.
To avoid this, this moves merge_one_path(), merge_all(), and their
helpers to merge-strategies.c. They also take a callback to dictate
what they should do for each file. For now, to preserve the behaviour
of `merge-index', only one callback, launching a new process, is
defined.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge-index.c | 77 +++------------------------------
merge-strategies.c | 99 +++++++++++++++++++++++++++++++++++++++++++
merge-strategies.h | 17 ++++++++
3 files changed, 123 insertions(+), 70 deletions(-)
diff --git a/builtin/merge-index.c b/builtin/merge-index.c
index 38ea6ad6ca..6cb666cc78 100644
--- a/builtin/merge-index.c
+++ b/builtin/merge-index.c
@@ -1,74 +1,11 @@
#define USE_THE_INDEX_COMPATIBILITY_MACROS
#include "builtin.h"
-#include "run-command.h"
-
-static const char *pgm;
-static int one_shot, quiet;
-static int err;
-
-static int merge_entry(int pos, const char *path)
-{
- int found;
- const char *arguments[] = { pgm, "", "", "", path, "", "", "", NULL };
- char hexbuf[4][GIT_MAX_HEXSZ + 1];
- char ownbuf[4][60];
-
- if (pos >= active_nr)
- die("git merge-index: %s not in the cache", path);
- found = 0;
- do {
- const struct cache_entry *ce = active_cache[pos];
- int stage = ce_stage(ce);
-
- if (strcmp(ce->name, path))
- break;
- found++;
- oid_to_hex_r(hexbuf[stage], &ce->oid);
- xsnprintf(ownbuf[stage], sizeof(ownbuf[stage]), "%o", ce->ce_mode);
- arguments[stage] = hexbuf[stage];
- arguments[stage + 4] = ownbuf[stage];
- } while (++pos < active_nr);
- if (!found)
- die("git merge-index: %s not in the cache", path);
-
- if (run_command_v_opt(arguments, 0)) {
- if (one_shot)
- err++;
- else {
- if (!quiet)
- die("merge program failed");
- exit(1);
- }
- }
- return found;
-}
-
-static void merge_one_path(const char *path)
-{
- int pos = cache_name_pos(path, strlen(path));
-
- /*
- * If it already exists in the cache as stage0, it's
- * already merged and there is nothing to do.
- */
- if (pos < 0)
- merge_entry(-pos-1, path);
-}
-
-static void merge_all(void)
-{
- int i;
- for (i = 0; i < active_nr; i++) {
- const struct cache_entry *ce = active_cache[i];
- if (!ce_stage(ce))
- continue;
- i += merge_entry(i, ce->name)-1;
- }
-}
+#include "merge-strategies.h"
int cmd_merge_index(int argc, const char **argv, const char *prefix)
{
- int i, force_file = 0;
+ int i, force_file = 0, err = 0, one_shot = 0, quiet = 0;
+ const char *pgm;
/* Without this we cannot rely on waitpid() to tell
* what happened to our children.
@@ -98,14 +35,14 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
continue;
}
if (!strcmp(arg, "-a")) {
- merge_all();
+ err |= merge_all(&the_index, one_shot, quiet,
+ merge_program_cb, (void *)pgm);
continue;
}
die("git merge-index: unknown option %s", arg);
}
- merge_one_path(arg);
+ err |= merge_one_path(&the_index, one_shot, quiet, arg,
+ merge_program_cb, (void *)pgm);
}
- if (err && !quiet)
- die("merge program failed");
return err;
}
diff --git a/merge-strategies.c b/merge-strategies.c
index bbe6f48698..f0e30f5624 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -2,6 +2,7 @@
#include "dir.h"
#include "ll-merge.h"
#include "merge-strategies.h"
+#include "run-command.h"
#include "xdiff-interface.h"
static int add_to_index_cacheinfo(struct index_state *istate,
@@ -212,3 +213,101 @@ int merge_strategies_one_file(struct repository *r,
return 0;
}
+
+int merge_program_cb(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data)
+{
+ char ownbuf[3][GIT_MAX_HEXSZ] = {{0}};
+ const char *arguments[] = { (char *)data, "", "", "", path,
+ ownbuf[0], ownbuf[1], ownbuf[2],
+ NULL };
+
+ if (orig_blob)
+ arguments[1] = oid_to_hex(orig_blob);
+ if (our_blob)
+ arguments[2] = oid_to_hex(our_blob);
+ if (their_blob)
+ arguments[3] = oid_to_hex(their_blob);
+
+ xsnprintf(ownbuf[0], sizeof(ownbuf[0]), "%o", orig_mode);
+ xsnprintf(ownbuf[1], sizeof(ownbuf[1]), "%o", our_mode);
+ xsnprintf(ownbuf[2], sizeof(ownbuf[2]), "%o", their_mode);
+
+ return run_command_v_opt(arguments, 0);
+}
+
+static int merge_entry(struct index_state *istate, int quiet, int pos,
+ const char *path, merge_cb cb, void *data)
+{
+ int found = 0;
+ const struct object_id *oids[3] = {NULL};
+ unsigned int modes[3] = {0};
+
+ do {
+ const struct cache_entry *ce = istate->cache[pos];
+ int stage = ce_stage(ce);
+
+ if (strcmp(ce->name, path))
+ break;
+ found++;
+ oids[stage - 1] = &ce->oid;
+ modes[stage - 1] = ce->ce_mode;
+ } while (++pos < istate->cache_nr);
+ if (!found)
+ return error(_("%s is not in the cache"), path);
+
+ if (cb(oids[0], oids[1], oids[2], path, modes[0], modes[1], modes[2], data)) {
+ if (!quiet)
+ error(_("Merge program failed"));
+ return -2;
+ }
+
+ return found;
+}
+
+int merge_one_path(struct index_state *istate, int oneshot, int quiet,
+ const char *path, merge_cb cb, void *data)
+{
+ int pos = index_name_pos(istate, path, strlen(path)), ret;
+
+ /*
+ * If it already exists in the cache as stage0, it's
+ * already merged and there is nothing to do.
+ */
+ if (pos < 0) {
+ ret = merge_entry(istate, quiet, -pos - 1, path, cb, data);
+ if (ret == -1)
+ return -1;
+ else if (ret == -2)
+ return 1;
+ }
+ return 0;
+}
+
+int merge_all(struct index_state *istate, int oneshot, int quiet,
+ merge_cb cb, void *data)
+{
+ int err = 0, i, ret;
+ for (i = 0; i < istate->cache_nr; i++) {
+ const struct cache_entry *ce = istate->cache[i];
+ if (!ce_stage(ce))
+ continue;
+
+ ret = merge_entry(istate, quiet, i, ce->name, cb, data);
+ if (ret > 0)
+ i += ret - 1;
+ else if (ret == -1)
+ return -1;
+ else if (ret == -2) {
+ if (oneshot)
+ err++;
+ else
+ return 1;
+ }
+ }
+
+ return err;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
index b527d145c7..cf78d7eaf4 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -10,4 +10,21 @@ int merge_strategies_one_file(struct repository *r,
unsigned int orig_mode, unsigned int our_mode,
unsigned int their_mode);
+typedef int (*merge_cb)(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
+int merge_program_cb(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
+int merge_one_path(struct index_state *istate, int oneshot, int quiet,
+ const char *path, merge_cb cb, void *data);
+int merge_all(struct index_state *istate, int oneshot, int quiet,
+ merge_cb cb, void *data);
+
#endif /* MERGE_STRATEGIES_H */
--
2.28.0.662.ge304723957
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v3 04/11] merge-index: don't fork if the requested program is `git-merge-one-file'
2020-10-05 12:26 ` [PATCH v3 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (2 preceding siblings ...)
2020-10-05 12:26 ` [PATCH v3 03/11] merge-index: libify merge_one_path() and merge_all() Alban Gruin
@ 2020-10-05 12:26 ` Alban Gruin
2020-10-16 19:07 ` Junio C Hamano
2020-10-05 12:26 ` [PATCH v3 05/11] merge-resolve: rewrite in C Alban Gruin
` (8 subsequent siblings)
12 siblings, 1 reply; 221+ messages in thread
From: Alban Gruin @ 2020-10-05 12:26 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
Since `git-merge-one-file' has been rewritten and libified, this teaches
`merge-index' to call merge_strategies_one_file() without forking using
a new callback, merge_one_file_cb().
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge-index.c | 29 +++++++++++++++++++++++++++--
merge-strategies.c | 11 +++++++++++
merge-strategies.h | 6 ++++++
3 files changed, 44 insertions(+), 2 deletions(-)
diff --git a/builtin/merge-index.c b/builtin/merge-index.c
index 6cb666cc78..19fff9a113 100644
--- a/builtin/merge-index.c
+++ b/builtin/merge-index.c
@@ -1,11 +1,15 @@
#define USE_THE_INDEX_COMPATIBILITY_MACROS
#include "builtin.h"
+#include "lockfile.h"
#include "merge-strategies.h"
int cmd_merge_index(int argc, const char **argv, const char *prefix)
{
int i, force_file = 0, err = 0, one_shot = 0, quiet = 0;
const char *pgm;
+ void *data;
+ merge_cb merge_action;
+ struct lock_file lock = LOCK_INIT;
/* Without this we cannot rely on waitpid() to tell
* what happened to our children.
@@ -26,7 +30,19 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
quiet = 1;
i++;
}
+
pgm = argv[i++];
+ if (!strcmp(pgm, "git-merge-one-file")) {
+ merge_action = merge_one_file_cb;
+ data = (void *)the_repository;
+
+ setup_work_tree();
+ hold_locked_index(&lock, LOCK_DIE_ON_ERROR);
+ } else {
+ merge_action = merge_program_cb;
+ data = (void *)pgm;
+ }
+
for (; i < argc; i++) {
const char *arg = argv[i];
if (!force_file && *arg == '-') {
@@ -36,13 +52,22 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
}
if (!strcmp(arg, "-a")) {
err |= merge_all(&the_index, one_shot, quiet,
- merge_program_cb, (void *)pgm);
+ merge_action, data);
continue;
}
die("git merge-index: unknown option %s", arg);
}
err |= merge_one_path(&the_index, one_shot, quiet, arg,
- merge_program_cb, (void *)pgm);
+ merge_action, data);
+ }
+
+ if (merge_action == merge_one_file_cb) {
+ if (err) {
+ rollback_lock_file(&lock);
+ return err;
+ }
+
+ return write_locked_index(&the_index, &lock, COMMIT_LOCK);
}
return err;
}
diff --git a/merge-strategies.c b/merge-strategies.c
index f0e30f5624..c022ba9748 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -214,6 +214,17 @@ int merge_strategies_one_file(struct repository *r,
return 0;
}
+int merge_one_file_cb(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data)
+{
+ return merge_strategies_one_file((struct repository *)data,
+ orig_blob, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+}
+
int merge_program_cb(const struct object_id *orig_blob,
const struct object_id *our_blob,
const struct object_id *their_blob, const char *path,
diff --git a/merge-strategies.h b/merge-strategies.h
index cf78d7eaf4..40e175ca39 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -16,6 +16,12 @@ typedef int (*merge_cb)(const struct object_id *orig_blob,
unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
void *data);
+int merge_one_file_cb(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
int merge_program_cb(const struct object_id *orig_blob,
const struct object_id *our_blob,
const struct object_id *their_blob, const char *path,
--
2.28.0.662.ge304723957
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v3 05/11] merge-resolve: rewrite in C
2020-10-05 12:26 ` [PATCH v3 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (3 preceding siblings ...)
2020-10-05 12:26 ` [PATCH v3 04/11] merge-index: don't fork if the requested program is `git-merge-one-file' Alban Gruin
@ 2020-10-05 12:26 ` Alban Gruin
2020-10-16 19:19 ` Junio C Hamano
2020-10-05 12:26 ` [PATCH v3 06/11] merge-recursive: move better_branch_name() to merge.c Alban Gruin
` (7 subsequent siblings)
12 siblings, 1 reply; 221+ messages in thread
From: Alban Gruin @ 2020-10-05 12:26 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
This rewrites `git merge-resolve' from shell to C. As for `git
merge-one-file', this port is not completely straightforward and removes
calls to external processes to avoid reading and writing the index over
and over again.
- The call to `update-index -q --refresh' is replaced by a call to
refresh_index().
- The call to `read-tree' is replaced by a call to unpack_trees() (and
all the setup needed).
- The call to `write-tree' is replaced by a call to
write_index_as_tree().
- The call to `merge-index', needed to invoke `git merge-one-file', is
replaced by a call to the new merge_all() function. A callback
function, merge_one_file_cb(), is added to allow it to call
merge_one_file() without forking.
Here too, the index is read in cmd_merge_resolve(), but
merge_strategies_resolve() takes care of writing it back to the disk.
The parameters of merge_strategies_resolve() will be surprising at first
glance: why using a commit list for `bases' and `remote', where we could
use an oid array, and a pointer to an oid? Because, in a later commit,
try_merge_strategy() will be able to call merge_strategies_resolve()
directly, and it already uses a commit list for `bases' (`common') and
`remote' (`remoteheads'), and a string for `head_arg'. To reduce
frictions later, merge_strategies_resolve() takes the same types of
parameters.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Makefile | 2 +-
builtin.h | 1 +
builtin/merge-resolve.c | 69 +++++++++++++++++++++++++++++++++
git-merge-resolve.sh | 54 --------------------------
git.c | 1 +
merge-strategies.c | 85 +++++++++++++++++++++++++++++++++++++++++
merge-strategies.h | 5 +++
7 files changed, 162 insertions(+), 55 deletions(-)
create mode 100644 builtin/merge-resolve.c
delete mode 100755 git-merge-resolve.sh
diff --git a/Makefile b/Makefile
index 6dfdb33cb2..3cc6b192f1 100644
--- a/Makefile
+++ b/Makefile
@@ -601,7 +601,6 @@ SCRIPT_SH += git-bisect.sh
SCRIPT_SH += git-difftool--helper.sh
SCRIPT_SH += git-filter-branch.sh
SCRIPT_SH += git-merge-octopus.sh
-SCRIPT_SH += git-merge-resolve.sh
SCRIPT_SH += git-mergetool.sh
SCRIPT_SH += git-quiltimport.sh
SCRIPT_SH += git-request-pull.sh
@@ -1097,6 +1096,7 @@ BUILTIN_OBJS += builtin/merge-index.o
BUILTIN_OBJS += builtin/merge-one-file.o
BUILTIN_OBJS += builtin/merge-ours.o
BUILTIN_OBJS += builtin/merge-recursive.o
+BUILTIN_OBJS += builtin/merge-resolve.o
BUILTIN_OBJS += builtin/merge-tree.o
BUILTIN_OBJS += builtin/merge.o
BUILTIN_OBJS += builtin/mktag.o
diff --git a/builtin.h b/builtin.h
index 4d2cd78856..35e91c16d0 100644
--- a/builtin.h
+++ b/builtin.h
@@ -180,6 +180,7 @@ int cmd_merge_ours(int argc, const char **argv, const char *prefix);
int cmd_merge_file(int argc, const char **argv, const char *prefix);
int cmd_merge_one_file(int argc, const char **argv, const char *prefix);
int cmd_merge_recursive(int argc, const char **argv, const char *prefix);
+int cmd_merge_resolve(int argc, const char **argv, const char *prefix);
int cmd_merge_tree(int argc, const char **argv, const char *prefix);
int cmd_mktag(int argc, const char **argv, const char *prefix);
int cmd_mktree(int argc, const char **argv, const char *prefix);
diff --git a/builtin/merge-resolve.c b/builtin/merge-resolve.c
new file mode 100644
index 0000000000..59f734473b
--- /dev/null
+++ b/builtin/merge-resolve.c
@@ -0,0 +1,69 @@
+/*
+ * Builtin "git merge-resolve"
+ *
+ * Copyright (c) 2020 Alban Gruin
+ *
+ * Based on git-merge-resolve.sh, written by Linus Torvalds and Junio C
+ * Hamano.
+ *
+ * Resolve two trees, using enhanced multi-base read-tree.
+ */
+
+#include "cache.h"
+#include "builtin.h"
+#include "merge-strategies.h"
+
+static const char builtin_merge_resolve_usage[] =
+ "git merge-resolve <bases>... -- <head> <remote>";
+
+int cmd_merge_resolve(int argc, const char **argv, const char *prefix)
+{
+ int i, is_baseless = 1, sep_seen = 0;
+ const char *head = NULL;
+ struct commit_list *bases = NULL, *remote = NULL;
+ struct commit_list **next_base = &bases;
+
+ if (argc < 5)
+ usage(builtin_merge_resolve_usage);
+
+ setup_work_tree();
+ if (repo_read_index(the_repository) < 0)
+ die("invalid index");
+
+ /* The first parameters up to -- are merge bases; the rest are
+ * heads. */
+ for (i = 1; i < argc; i++) {
+ if (strcmp(argv[i], "--") == 0)
+ sep_seen = 1;
+ else if (strcmp(argv[i], "-h") == 0)
+ usage(builtin_merge_resolve_usage);
+ else if (sep_seen && !head)
+ head = argv[i];
+ else if (remote) {
+ /* Give up if we are given two or more remotes.
+ * Not handling octopus. */
+ return 2;
+ } else {
+ struct object_id oid;
+
+ get_oid(argv[i], &oid);
+ is_baseless &= sep_seen;
+
+ if (!oideq(&oid, the_hash_algo->empty_tree)) {
+ struct commit *commit;
+ commit = lookup_commit_or_die(&oid, argv[i]);
+
+ if (sep_seen)
+ commit_list_append(commit, &remote);
+ else
+ next_base = commit_list_append(commit, next_base);
+ }
+ }
+ }
+
+ /* Give up if this is a baseless merge. */
+ if (is_baseless)
+ return 2;
+
+ return merge_strategies_resolve(the_repository, bases, head, remote);
+}
diff --git a/git-merge-resolve.sh b/git-merge-resolve.sh
deleted file mode 100755
index 343fe7bccd..0000000000
--- a/git-merge-resolve.sh
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2005 Linus Torvalds
-# Copyright (c) 2005 Junio C Hamano
-#
-# Resolve two trees, using enhanced multi-base read-tree.
-
-# The first parameters up to -- are merge bases; the rest are heads.
-bases= head= remotes= sep_seen=
-for arg
-do
- case ",$sep_seen,$head,$arg," in
- *,--,)
- sep_seen=yes
- ;;
- ,yes,,*)
- head=$arg
- ;;
- ,yes,*)
- remotes="$remotes$arg "
- ;;
- *)
- bases="$bases$arg "
- ;;
- esac
-done
-
-# Give up if we are given two or more remotes -- not handling octopus.
-case "$remotes" in
-?*' '?*)
- exit 2 ;;
-esac
-
-# Give up if this is a baseless merge.
-if test '' = "$bases"
-then
- exit 2
-fi
-
-git update-index -q --refresh
-git read-tree -u -m --aggressive $bases $head $remotes || exit 2
-echo "Trying simple merge."
-if result_tree=$(git write-tree 2>/dev/null)
-then
- exit 0
-else
- echo "Simple merge failed, trying Automatic merge."
- if git merge-index -o git-merge-one-file -a
- then
- exit 0
- else
- exit 1
- fi
-fi
diff --git a/git.c b/git.c
index a4d3f98094..64a1a1de41 100644
--- a/git.c
+++ b/git.c
@@ -544,6 +544,7 @@ static struct cmd_struct commands[] = {
{ "merge-recursive", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-ours", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-theirs", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
+ { "merge-resolve", cmd_merge_resolve, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-subtree", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-tree", cmd_merge_tree, RUN_SETUP | NO_PARSEOPT },
{ "mktag", cmd_mktag, RUN_SETUP | NO_PARSEOPT },
diff --git a/merge-strategies.c b/merge-strategies.c
index c022ba9748..6b4b3d03a6 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -1,8 +1,11 @@
#include "cache.h"
+#include "cache-tree.h"
#include "dir.h"
#include "ll-merge.h"
+#include "lockfile.h"
#include "merge-strategies.h"
#include "run-command.h"
+#include "unpack-trees.h"
#include "xdiff-interface.h"
static int add_to_index_cacheinfo(struct index_state *istate,
@@ -322,3 +325,85 @@ int merge_all(struct index_state *istate, int oneshot, int quiet,
return err;
}
+
+static int add_tree(const struct object_id *oid, struct tree_desc *t)
+{
+ struct tree *tree;
+
+ tree = parse_tree_indirect(oid);
+ if (parse_tree(tree))
+ return -1;
+
+ init_tree_desc(t, tree->buffer, tree->size);
+ return 0;
+}
+
+int merge_strategies_resolve(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remote)
+{
+ int i = 0;
+ struct lock_file lock = LOCK_INIT;
+ struct tree_desc t[MAX_UNPACK_TREES];
+ struct unpack_trees_options opts;
+ struct object_id head, oid;
+ struct commit_list *j;
+
+ if (head_arg)
+ get_oid(head_arg, &head);
+
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+ refresh_index(r->index, 0, NULL, NULL, NULL);
+
+ memset(&opts, 0, sizeof(opts));
+ opts.head_idx = 1;
+ opts.src_index = r->index;
+ opts.dst_index = r->index;
+ opts.update = 1;
+ opts.merge = 1;
+ opts.aggressive = 1;
+
+ for (j = bases; j && j->item; j = j->next) {
+ if (add_tree(&j->item->object.oid, t + (i++)))
+ goto out;
+ }
+
+ if (head_arg && add_tree(&head, t + (i++)))
+ goto out;
+ if (remote && add_tree(&remote->item->object.oid, t + (i++)))
+ goto out;
+
+ if (i == 1)
+ opts.fn = oneway_merge;
+ else if (i == 2) {
+ opts.fn = twoway_merge;
+ opts.initial_checkout = is_index_unborn(r->index);
+ } else if (i >= 3) {
+ opts.fn = threeway_merge;
+ opts.head_idx = i - 1;
+ }
+
+ if (unpack_trees(i, t, &opts))
+ goto out;
+
+ puts(_("Trying simple merge."));
+ write_locked_index(r->index, &lock, COMMIT_LOCK);
+
+ if (write_index_as_tree(&oid, r->index, r->index_file,
+ WRITE_TREE_SILENT, NULL)) {
+ int ret;
+
+ puts(_("Simple merge failed, trying Automatic merge."));
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+ ret = merge_all(r->index, 0, 0, merge_one_file_cb, r);
+
+ write_locked_index(r->index, &lock, COMMIT_LOCK);
+ return !!ret;
+ }
+
+ return 0;
+
+ out:
+ rollback_lock_file(&lock);
+ return 2;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
index 40e175ca39..778f8ce9d6 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -1,6 +1,7 @@
#ifndef MERGE_STRATEGIES_H
#define MERGE_STRATEGIES_H
+#include "commit.h"
#include "object.h"
int merge_strategies_one_file(struct repository *r,
@@ -33,4 +34,8 @@ int merge_one_path(struct index_state *istate, int oneshot, int quiet,
int merge_all(struct index_state *istate, int oneshot, int quiet,
merge_cb cb, void *data);
+int merge_strategies_resolve(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remote);
+
#endif /* MERGE_STRATEGIES_H */
--
2.28.0.662.ge304723957
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v3 06/11] merge-recursive: move better_branch_name() to merge.c
2020-10-05 12:26 ` [PATCH v3 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (4 preceding siblings ...)
2020-10-05 12:26 ` [PATCH v3 05/11] merge-resolve: rewrite in C Alban Gruin
@ 2020-10-05 12:26 ` Alban Gruin
2020-10-05 12:26 ` [PATCH v3 07/11] merge-octopus: rewrite in C Alban Gruin
` (6 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-10-05 12:26 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
get_better_branch_name() will be used by rebase-octopus once it is
rewritten in C, so instead of duplicating it, this moves this function
preventively inside an appropriate file in libgit.a. This function is
also renamed to reflect its usage by merge strategies.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge-recursive.c | 16 ++--------------
cache.h | 2 +-
merge.c | 12 ++++++++++++
3 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/builtin/merge-recursive.c b/builtin/merge-recursive.c
index a4bfd8fc51..972243b5e9 100644
--- a/builtin/merge-recursive.c
+++ b/builtin/merge-recursive.c
@@ -8,18 +8,6 @@
static const char builtin_merge_recursive_usage[] =
"git %s <base>... -- <head> <remote> ...";
-static char *better_branch_name(const char *branch)
-{
- static char githead_env[8 + GIT_MAX_HEXSZ + 1];
- char *name;
-
- if (strlen(branch) != the_hash_algo->hexsz)
- return xstrdup(branch);
- xsnprintf(githead_env, sizeof(githead_env), "GITHEAD_%s", branch);
- name = getenv(githead_env);
- return xstrdup(name ? name : branch);
-}
-
int cmd_merge_recursive(int argc, const char **argv, const char *prefix)
{
const struct object_id *bases[21];
@@ -75,8 +63,8 @@ int cmd_merge_recursive(int argc, const char **argv, const char *prefix)
if (get_oid(o.branch2, &h2))
die(_("could not resolve ref '%s'"), o.branch2);
- o.branch1 = better1 = better_branch_name(o.branch1);
- o.branch2 = better2 = better_branch_name(o.branch2);
+ o.branch1 = better1 = merge_get_better_branch_name(o.branch1);
+ o.branch2 = better2 = merge_get_better_branch_name(o.branch2);
if (o.verbosity >= 3)
printf(_("Merging %s with %s\n"), o.branch1, o.branch2);
diff --git a/cache.h b/cache.h
index c0072d43b1..5fa0ed8d1a 100644
--- a/cache.h
+++ b/cache.h
@@ -1928,7 +1928,7 @@ int checkout_fast_forward(struct repository *r,
const struct object_id *from,
const struct object_id *to,
int overwrite_ignore);
-
+char *merge_get_better_branch_name(const char *branch);
int sane_execvp(const char *file, char *const argv[]);
diff --git a/merge.c b/merge.c
index 5fb88af102..801d673c5f 100644
--- a/merge.c
+++ b/merge.c
@@ -109,3 +109,15 @@ int checkout_fast_forward(struct repository *r,
return error(_("unable to write new index file"));
return 0;
}
+
+char *merge_get_better_branch_name(const char *branch)
+{
+ static char githead_env[8 + GIT_MAX_HEXSZ + 1];
+ char *name;
+
+ if (strlen(branch) != the_hash_algo->hexsz)
+ return xstrdup(branch);
+ xsnprintf(githead_env, sizeof(githead_env), "GITHEAD_%s", branch);
+ name = getenv(githead_env);
+ return xstrdup(name ? name : branch);
+}
--
2.28.0.662.ge304723957
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v3 07/11] merge-octopus: rewrite in C
2020-10-05 12:26 ` [PATCH v3 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (5 preceding siblings ...)
2020-10-05 12:26 ` [PATCH v3 06/11] merge-recursive: move better_branch_name() to merge.c Alban Gruin
@ 2020-10-05 12:26 ` Alban Gruin
2020-10-05 12:26 ` [PATCH v3 08/11] merge: use the "resolve" strategy without forking Alban Gruin
` (5 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-10-05 12:26 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
This rewrites `git merge-octopus' from shell to C. As for the two last
conversions, this port removes calls to external processes to avoid
reading and writing the index over and over again.
- Calls to `read-tree -u -m (--aggressive)?' are replaced by calls to
unpack_trees().
- The call to `write-tree' is replaced by a call to
write_index_as_tree().
- The call to `diff-index ...' is replaced by a call to
repo_index_has_changes(), and is moved from cmd_merge_octopus() to
merge_octopus().
- The call to `merge-index', needed to invoke `git merge-one-file', is
replaced by a call to merge_all().
The index is read in cmd_merge_octopus(), and is wrote back by
merge_strategies_octopus().
Here to, merge_strategies_octopus() takes two commit lists and a string
to reduce frictions when try_merge_strategies() will be modified to call
it directly.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Makefile | 2 +-
builtin.h | 1 +
builtin/merge-octopus.c | 69 ++++++++++++++
git-merge-octopus.sh | 112 ----------------------
git.c | 1 +
merge-strategies.c | 204 ++++++++++++++++++++++++++++++++++++++++
merge-strategies.h | 3 +
7 files changed, 279 insertions(+), 113 deletions(-)
create mode 100644 builtin/merge-octopus.c
delete mode 100755 git-merge-octopus.sh
diff --git a/Makefile b/Makefile
index 3cc6b192f1..2b2bdffafe 100644
--- a/Makefile
+++ b/Makefile
@@ -600,7 +600,6 @@ unexport CDPATH
SCRIPT_SH += git-bisect.sh
SCRIPT_SH += git-difftool--helper.sh
SCRIPT_SH += git-filter-branch.sh
-SCRIPT_SH += git-merge-octopus.sh
SCRIPT_SH += git-mergetool.sh
SCRIPT_SH += git-quiltimport.sh
SCRIPT_SH += git-request-pull.sh
@@ -1093,6 +1092,7 @@ BUILTIN_OBJS += builtin/mailsplit.o
BUILTIN_OBJS += builtin/merge-base.o
BUILTIN_OBJS += builtin/merge-file.o
BUILTIN_OBJS += builtin/merge-index.o
+BUILTIN_OBJS += builtin/merge-octopus.o
BUILTIN_OBJS += builtin/merge-one-file.o
BUILTIN_OBJS += builtin/merge-ours.o
BUILTIN_OBJS += builtin/merge-recursive.o
diff --git a/builtin.h b/builtin.h
index 35e91c16d0..50225404a0 100644
--- a/builtin.h
+++ b/builtin.h
@@ -176,6 +176,7 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix);
int cmd_merge(int argc, const char **argv, const char *prefix);
int cmd_merge_base(int argc, const char **argv, const char *prefix);
int cmd_merge_index(int argc, const char **argv, const char *prefix);
+int cmd_merge_octopus(int argc, const char **argv, const char *prefix);
int cmd_merge_ours(int argc, const char **argv, const char *prefix);
int cmd_merge_file(int argc, const char **argv, const char *prefix);
int cmd_merge_one_file(int argc, const char **argv, const char *prefix);
diff --git a/builtin/merge-octopus.c b/builtin/merge-octopus.c
new file mode 100644
index 0000000000..abf0981fe8
--- /dev/null
+++ b/builtin/merge-octopus.c
@@ -0,0 +1,69 @@
+/*
+ * Builtin "git merge-octopus"
+ *
+ * Copyright (c) 2020 Alban Gruin
+ *
+ * Based on git-merge-octopus.sh, written by Junio C Hamano.
+ *
+ * Resolve two or more trees.
+ */
+
+#include "cache.h"
+#include "builtin.h"
+#include "commit.h"
+#include "merge-strategies.h"
+
+static const char builtin_merge_octopus_usage[] =
+ "git merge-octopus [<bases>...] -- <head> <remote1> <remote2> [<remotes>...]";
+
+int cmd_merge_octopus(int argc, const char **argv, const char *prefix)
+{
+ int i, sep_seen = 0;
+ struct commit_list *bases = NULL, *remotes = NULL;
+ struct commit_list **next_base = &bases, **next_remote = &remotes;
+ const char *head_arg = NULL;
+
+ if (argc < 5)
+ usage(builtin_merge_octopus_usage);
+
+ setup_work_tree();
+ if (repo_read_index(the_repository) < 0)
+ die("corrupted cache");
+
+ /*
+ * The first parameters up to -- are merge bases; the rest are
+ * heads.
+ */
+ for (i = 1; i < argc; i++) {
+ if (strcmp(argv[i], "--") == 0)
+ sep_seen = 1;
+ else if (strcmp(argv[i], "-h") == 0)
+ usage(builtin_merge_octopus_usage);
+ else if (sep_seen && !head_arg)
+ head_arg = argv[i];
+ else {
+ struct object_id oid;
+
+ get_oid(argv[i], &oid);
+
+ if (!oideq(&oid, the_hash_algo->empty_tree)) {
+ struct commit *commit;
+ commit = lookup_commit_or_die(&oid, argv[i]);
+
+ if (sep_seen)
+ next_remote = commit_list_append(commit, next_remote);
+ else
+ next_base = commit_list_append(commit, next_base);
+ }
+ }
+ }
+
+ /*
+ * Reject if this is not an octopus -- resolve should be used
+ * instead.
+ */
+ if (commit_list_count(remotes) < 2)
+ return 2;
+
+ return merge_strategies_octopus(the_repository, bases, head_arg, remotes);
+}
diff --git a/git-merge-octopus.sh b/git-merge-octopus.sh
deleted file mode 100755
index 7d19d37951..0000000000
--- a/git-merge-octopus.sh
+++ /dev/null
@@ -1,112 +0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2005 Junio C Hamano
-#
-# Resolve two or more trees.
-#
-
-. git-sh-setup
-
-LF='
-'
-
-# The first parameters up to -- are merge bases; the rest are heads.
-bases= head= remotes= sep_seen=
-for arg
-do
- case ",$sep_seen,$head,$arg," in
- *,--,)
- sep_seen=yes
- ;;
- ,yes,,*)
- head=$arg
- ;;
- ,yes,*)
- remotes="$remotes$arg "
- ;;
- *)
- bases="$bases$arg "
- ;;
- esac
-done
-
-# Reject if this is not an octopus -- resolve should be used instead.
-case "$remotes" in
-?*' '?*)
- ;;
-*)
- exit 2 ;;
-esac
-
-# MRC is the current "merge reference commit"
-# MRT is the current "merge result tree"
-
-if ! git diff-index --quiet --cached HEAD --
-then
- gettextln "Error: Your local changes to the following files would be overwritten by merge"
- git diff-index --cached --name-only HEAD -- | sed -e 's/^/ /'
- exit 2
-fi
-MRC=$(git rev-parse --verify -q $head)
-MRT=$(git write-tree)
-NON_FF_MERGE=0
-OCTOPUS_FAILURE=0
-for SHA1 in $remotes
-do
- case "$OCTOPUS_FAILURE" in
- 1)
- # We allow only last one to have a hand-resolvable
- # conflicts. Last round failed and we still had
- # a head to merge.
- gettextln "Automated merge did not work."
- gettextln "Should not be doing an octopus."
- exit 2
- esac
-
- eval pretty_name=\${GITHEAD_$SHA1:-$SHA1}
- if test "$SHA1" = "$pretty_name"
- then
- SHA1_UP="$(echo "$SHA1" | tr a-z A-Z)"
- eval pretty_name=\${GITHEAD_$SHA1_UP:-$pretty_name}
- fi
- common=$(git merge-base --all $SHA1 $MRC) ||
- die "$(eval_gettext "Unable to find common commit with \$pretty_name")"
-
- case "$LF$common$LF" in
- *"$LF$SHA1$LF"*)
- eval_gettextln "Already up to date with \$pretty_name"
- continue
- ;;
- esac
-
- if test "$common,$NON_FF_MERGE" = "$MRC,0"
- then
- # The first head being merged was a fast-forward.
- # Advance MRC to the head being merged, and use that
- # tree as the intermediate result of the merge.
- # We still need to count this as part of the parent set.
-
- eval_gettextln "Fast-forwarding to: \$pretty_name"
- git read-tree -u -m $head $SHA1 || exit
- MRC=$SHA1 MRT=$(git write-tree)
- continue
- fi
-
- NON_FF_MERGE=1
-
- eval_gettextln "Trying simple merge with \$pretty_name"
- git read-tree -u -m --aggressive $common $MRT $SHA1 || exit 2
- next=$(git write-tree 2>/dev/null)
- if test $? -ne 0
- then
- gettextln "Simple merge did not work, trying automatic merge."
- git merge-index -o git-merge-one-file -a ||
- OCTOPUS_FAILURE=1
- next=$(git write-tree 2>/dev/null)
- fi
-
- MRC="$MRC $SHA1"
- MRT=$next
-done
-
-exit "$OCTOPUS_FAILURE"
diff --git a/git.c b/git.c
index 64a1a1de41..d51fb5d2bf 100644
--- a/git.c
+++ b/git.c
@@ -539,6 +539,7 @@ static struct cmd_struct commands[] = {
{ "merge-base", cmd_merge_base, RUN_SETUP },
{ "merge-file", cmd_merge_file, RUN_SETUP_GENTLY },
{ "merge-index", cmd_merge_index, RUN_SETUP | NO_PARSEOPT },
+ { "merge-octopus", cmd_merge_octopus, RUN_SETUP | NO_PARSEOPT },
{ "merge-ours", cmd_merge_ours, RUN_SETUP | NO_PARSEOPT },
{ "merge-one-file", cmd_merge_one_file, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
diff --git a/merge-strategies.c b/merge-strategies.c
index 6b4b3d03a6..37c662094e 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -1,5 +1,6 @@
#include "cache.h"
#include "cache-tree.h"
+#include "commit-reach.h"
#include "dir.h"
#include "ll-merge.h"
#include "lockfile.h"
@@ -407,3 +408,206 @@ int merge_strategies_resolve(struct repository *r,
rollback_lock_file(&lock);
return 2;
}
+
+static int fast_forward(struct repository *r, const struct object_id *oids,
+ int nr, int aggressive)
+{
+ int i;
+ struct tree_desc t[MAX_UNPACK_TREES];
+ struct unpack_trees_options opts;
+ struct lock_file lock = LOCK_INIT;
+
+ repo_read_index_preload(r, NULL, 0);
+ if (refresh_index(r->index, REFRESH_QUIET, NULL, NULL, NULL))
+ return -1;
+
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+
+ memset(&opts, 0, sizeof(opts));
+ opts.head_idx = 1;
+ opts.src_index = r->index;
+ opts.dst_index = r->index;
+ opts.merge = 1;
+ opts.update = 1;
+ opts.aggressive = aggressive;
+
+ for (i = 0; i < nr; i++) {
+ struct tree *tree;
+ tree = parse_tree_indirect(oids + i);
+ if (parse_tree(tree))
+ return -1;
+ init_tree_desc(t + i, tree->buffer, tree->size);
+ }
+
+ if (nr == 1)
+ opts.fn = oneway_merge;
+ else if (nr == 2) {
+ opts.fn = twoway_merge;
+ opts.initial_checkout = is_index_unborn(r->index);
+ } else if (nr >= 3) {
+ opts.fn = threeway_merge;
+ opts.head_idx = nr - 1;
+ }
+
+ if (unpack_trees(nr, t, &opts))
+ return -1;
+
+ if (write_locked_index(r->index, &lock, COMMIT_LOCK))
+ return error(_("unable to write new index file"));
+
+ return 0;
+}
+
+static int write_tree(struct repository *r, struct tree **reference_tree)
+{
+ struct object_id oid;
+ int ret;
+
+ ret = write_index_as_tree(&oid, r->index, r->index_file, 0, NULL);
+ if (!ret)
+ *reference_tree = lookup_tree(r, &oid);
+
+ return ret;
+}
+
+int merge_strategies_octopus(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remotes)
+{
+ int non_ff_merge = 0, ret = 0, references = 1;
+ struct commit **reference_commit;
+ struct tree *reference_tree;
+ struct commit_list *j;
+ struct object_id head;
+ struct strbuf sb = STRBUF_INIT;
+
+ get_oid(head_arg, &head);
+
+ reference_commit = xcalloc(commit_list_count(remotes) + 1, sizeof(struct commit *));
+ reference_commit[0] = lookup_commit_reference(r, &head);
+ reference_tree = repo_get_commit_tree(r, reference_commit[0]);
+
+ if (repo_index_has_changes(r, reference_tree, &sb)) {
+ error(_("Your local changes to the following files "
+ "would be overwritten by merge:\n %s"),
+ sb.buf);
+ strbuf_release(&sb);
+ ret = 2;
+ goto out;
+ }
+
+ for (j = remotes; j && j->item; j = j->next) {
+ struct commit *c = j->item;
+ struct object_id *oid = &c->object.oid;
+ struct commit_list *common, *k;
+ char *branch_name;
+ int can_ff = 1;
+
+ if (ret) {
+ /*
+ * We allow only last one to have a
+ * hand-resolvable conflicts. Last round failed
+ * and we still had a head to merge.
+ */
+ puts(_("Automated merge did not work."));
+ puts(_("Should not be doing an octopus."));
+
+ ret = 2;
+ goto out;
+ }
+
+ branch_name = merge_get_better_branch_name(oid_to_hex(oid));
+ common = get_merge_bases_many(c, references, reference_commit);
+
+ if (!common)
+ die(_("Unable to find common commit with %s"), branch_name);
+
+ for (k = common; k && !oideq(&k->item->object.oid, oid); k = k->next);
+
+ if (k) {
+ printf(_("Already up to date with %s\n"), branch_name);
+ free(branch_name);
+ free_commit_list(common);
+ continue;
+ }
+
+ if (!non_ff_merge) {
+ int i;
+
+ for (i = 0, k = common; k && i < references && can_ff; k = k->next, i++) {
+ can_ff = oideq(&k->item->object.oid,
+ &reference_commit[i]->object.oid);
+ }
+ }
+
+ if (!non_ff_merge && can_ff) {
+ /*
+ * The first head being merged was a
+ * fast-forward. Advance the reference commit
+ * to the head being merged, and use that tree
+ * as the intermediate result of the merge. We
+ * still need to count this as part of the
+ * parent set.
+ */
+ struct object_id oids[2];
+ printf(_("Fast-forwarding to: %s\n"), branch_name);
+
+ oidcpy(oids, &head);
+ oidcpy(oids + 1, oid);
+
+ ret = fast_forward(r, oids, 2, 0);
+ if (ret) {
+ free(branch_name);
+ free_commit_list(common);
+ goto out;
+ }
+
+ references = 0;
+ write_tree(r, &reference_tree);
+ } else {
+ int i = 0;
+ struct tree *next = NULL;
+ struct object_id oids[MAX_UNPACK_TREES];
+
+ non_ff_merge = 1;
+ printf(_("Trying simple merge with %s\n"), branch_name);
+
+ for (k = common; k; k = k->next)
+ oidcpy(oids + (i++), &k->item->object.oid);
+
+ oidcpy(oids + (i++), &reference_tree->object.oid);
+ oidcpy(oids + (i++), oid);
+
+ if (fast_forward(r, oids, i, 1)) {
+ ret = 2;
+
+ free(branch_name);
+ free_commit_list(common);
+
+ goto out;
+ }
+
+ if (write_tree(r, &next)) {
+ struct lock_file lock = LOCK_INIT;
+
+ puts(_("Simple merge did not work, trying automatic merge."));
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+ ret = !!merge_all(r->index, 0, 0, merge_one_file_cb, r);
+ write_locked_index(r->index, &lock, COMMIT_LOCK);
+
+ write_tree(r, &next);
+ }
+
+ reference_tree = next;
+ }
+
+ reference_commit[references++] = c;
+
+ free(branch_name);
+ free_commit_list(common);
+ }
+
+out:
+ free(reference_commit);
+ return ret;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
index 778f8ce9d6..938411a04e 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -37,5 +37,8 @@ int merge_all(struct index_state *istate, int oneshot, int quiet,
int merge_strategies_resolve(struct repository *r,
struct commit_list *bases, const char *head_arg,
struct commit_list *remote);
+int merge_strategies_octopus(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remote);
#endif /* MERGE_STRATEGIES_H */
--
2.28.0.662.ge304723957
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v3 08/11] merge: use the "resolve" strategy without forking
2020-10-05 12:26 ` [PATCH v3 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (6 preceding siblings ...)
2020-10-05 12:26 ` [PATCH v3 07/11] merge-octopus: rewrite in C Alban Gruin
@ 2020-10-05 12:26 ` Alban Gruin
2020-10-05 12:26 ` [PATCH v3 09/11] merge: use the "octopus" " Alban Gruin
` (4 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-10-05 12:26 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
This teaches `git merge' to invoke the "resolve" strategy with a
function call instead of forking.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge.c | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/builtin/merge.c b/builtin/merge.c
index 9d5359edc2..ddfefd8ce3 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -41,6 +41,7 @@
#include "commit-reach.h"
#include "wt-status.h"
#include "commit-graph.h"
+#include "merge-strategies.h"
#define DEFAULT_TWOHEAD (1<<0)
#define DEFAULT_OCTOPUS (1<<1)
@@ -740,7 +741,10 @@ static int try_merge_strategy(const char *strategy, struct commit_list *common,
COMMIT_LOCK | SKIP_IF_UNCHANGED))
die(_("unable to write %s"), get_index_file());
return clean ? 0 : 1;
- } else {
+ } else if (!strcmp(strategy, "resolve"))
+ return merge_strategies_resolve(the_repository, common,
+ head_arg, remoteheads);
+ else {
return try_merge_command(the_repository,
strategy, xopts_nr, xopts,
common, head_arg, remoteheads);
--
2.28.0.662.ge304723957
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v3 09/11] merge: use the "octopus" strategy without forking
2020-10-05 12:26 ` [PATCH v3 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (7 preceding siblings ...)
2020-10-05 12:26 ` [PATCH v3 08/11] merge: use the "resolve" strategy without forking Alban Gruin
@ 2020-10-05 12:26 ` Alban Gruin
2020-10-05 12:26 ` [PATCH v3 10/11] sequencer: use the "resolve" " Alban Gruin
` (3 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-10-05 12:26 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
This teaches `git merge' to invoke the "octopus" strategy with a
function call instead of forking.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/builtin/merge.c b/builtin/merge.c
index ddfefd8ce3..02a2367647 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -744,6 +744,9 @@ static int try_merge_strategy(const char *strategy, struct commit_list *common,
} else if (!strcmp(strategy, "resolve"))
return merge_strategies_resolve(the_repository, common,
head_arg, remoteheads);
+ else if (!strcmp(strategy, "octopus"))
+ return merge_strategies_octopus(the_repository, common,
+ head_arg, remoteheads);
else {
return try_merge_command(the_repository,
strategy, xopts_nr, xopts,
--
2.28.0.662.ge304723957
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v3 10/11] sequencer: use the "resolve" strategy without forking
2020-10-05 12:26 ` [PATCH v3 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (8 preceding siblings ...)
2020-10-05 12:26 ` [PATCH v3 09/11] merge: use the "octopus" " Alban Gruin
@ 2020-10-05 12:26 ` Alban Gruin
2020-10-05 12:26 ` [PATCH v3 11/11] sequencer: use the "octopus" merge " Alban Gruin
` (2 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-10-05 12:26 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
This teaches the sequencer to invoke the "resolve" strategy with a
function call instead of forking.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
sequencer.c | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/sequencer.c b/sequencer.c
index e8676e965f..ff411d54af 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -33,6 +33,7 @@
#include "commit-reach.h"
#include "rebase-interactive.h"
#include "reset.h"
+#include "merge-strategies.h"
#define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
@@ -2000,9 +2001,15 @@ static int do_pick_commit(struct repository *r,
commit_list_insert(base, &common);
commit_list_insert(next, &remotes);
- res |= try_merge_command(r, opts->strategy,
- opts->xopts_nr, (const char **)opts->xopts,
- common, oid_to_hex(&head), remotes);
+
+ if (!strcmp(opts->strategy, "resolve")) {
+ repo_read_index(r);
+ res |= merge_strategies_resolve(r, common, oid_to_hex(&head), remotes);
+ } else
+ res |= try_merge_command(r, opts->strategy,
+ opts->xopts_nr, (const char **)opts->xopts,
+ common, oid_to_hex(&head), remotes);
+
free_commit_list(common);
free_commit_list(remotes);
}
--
2.28.0.662.ge304723957
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v3 11/11] sequencer: use the "octopus" merge strategy without forking
2020-10-05 12:26 ` [PATCH v3 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (9 preceding siblings ...)
2020-10-05 12:26 ` [PATCH v3 10/11] sequencer: use the "resolve" " Alban Gruin
@ 2020-10-05 12:26 ` Alban Gruin
2020-10-07 6:57 ` [PATCH v3 00/11] Rewrite the remaining merge strategies from shell to C Johannes Schindelin
2020-11-13 11:04 ` [PATCH v4 00/12] " Alban Gruin
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-10-05 12:26 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, phillip.wood, Alban Gruin
This teaches the sequencer to invoke the "octopus" strategy with a
function call instead of forking.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
sequencer.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/sequencer.c b/sequencer.c
index ff411d54af..746afad930 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2005,6 +2005,9 @@ static int do_pick_commit(struct repository *r,
if (!strcmp(opts->strategy, "resolve")) {
repo_read_index(r);
res |= merge_strategies_resolve(r, common, oid_to_hex(&head), remotes);
+ } else if (!strcmp(opts->strategy, "octopus")) {
+ repo_read_index(r);
+ res |= merge_strategies_octopus(r, common, oid_to_hex(&head), remotes);
} else
res |= try_merge_command(r, opts->strategy,
opts->xopts_nr, (const char **)opts->xopts,
--
2.28.0.662.ge304723957
^ permalink raw reply related [flat|nested] 221+ messages in thread
* Re: [PATCH v3 01/11] t6027: modernise tests
2020-10-05 12:26 ` [PATCH v3 01/11] t6027: modernise tests Alban Gruin
@ 2020-10-06 20:50 ` Junio C Hamano
0 siblings, 0 replies; 221+ messages in thread
From: Junio C Hamano @ 2020-10-06 20:50 UTC (permalink / raw)
To: Alban Gruin; +Cc: git, phillip.wood
Alban Gruin <alban.gruin@gmail.com> writes:
> Some tests in t6027 uses a if/then/else to check if a command failed or
s/uses/use/;
> not, but we have the `test_must_fail' function to do it correctly for us
> nowadays.
Makes sense. The patch text reads good, too.
> Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
> ---
> t/t6407-merge-binary.sh | 27 ++++++---------------------
> 1 file changed, 6 insertions(+), 21 deletions(-)
>
> diff --git a/t/t6407-merge-binary.sh b/t/t6407-merge-binary.sh
> index 4e6c7cb77e..071d3f7343 100755
> --- a/t/t6407-merge-binary.sh
> +++ b/t/t6407-merge-binary.sh
> @@ -5,7 +5,6 @@ test_description='ask merge-recursive to merge binary files'
> . ./test-lib.sh
>
> test_expect_success setup '
> -
> cat "$TEST_DIRECTORY"/test-binary-1.png >m &&
> git add m &&
> git ls-files -s | sed -e "s/ 0 / 1 /" >E1 &&
> @@ -35,33 +34,19 @@ test_expect_success setup '
> '
>
> test_expect_success resolve '
> -
> rm -f a* m* &&
> git reset --hard anchor &&
> -
> - if git merge -s resolve master
> - then
> - echo Oops, should not have succeeded
> - false
> - else
> - git ls-files -s >current
> - test_cmp expect current
> - fi
> + test_must_fail git merge -s resolve master &&
> + git ls-files -s >current &&
> + test_cmp expect current
> '
>
> test_expect_success recursive '
> -
> rm -f a* m* &&
> git reset --hard anchor &&
> -
> - if git merge -s recursive master
> - then
> - echo Oops, should not have succeeded
> - false
> - else
> - git ls-files -s >current
> - test_cmp expect current
> - fi
> + test_must_fail git merge -s recursive master &&
> + git ls-files -s >current &&
> + test_cmp expect current
> '
>
> test_done
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [PATCH v3 02/11] merge-one-file: rewrite in C
2020-10-05 12:26 ` [PATCH v3 02/11] merge-one-file: rewrite in C Alban Gruin
@ 2020-10-06 22:01 ` Junio C Hamano
2020-10-21 19:47 ` Alban Gruin
0 siblings, 1 reply; 221+ messages in thread
From: Junio C Hamano @ 2020-10-06 22:01 UTC (permalink / raw)
To: Alban Gruin; +Cc: git, phillip.wood
Alban Gruin <alban.gruin@gmail.com> writes:
> This rewrites `git merge-one-file' from shell to C. This port is not
> completely straightforward: to save precious cycles by avoiding reading
> and flushing the index repeatedly, write temporary files when an
> operation can be performed in-memory, or allow other function to use the
> rewrite without forking nor worrying about the index,...
So, the in-core index is still used, but when the contents of the in-core
index does not have to be written out disk, we just don't? Makes sense.
> diff --git a/builtin/merge-one-file.c b/builtin/merge-one-file.c
> new file mode 100644
> index 0000000000..598338ba16
> --- /dev/null
> +++ b/builtin/merge-one-file.c
> @@ -0,0 +1,92 @@
> +/*
> + * Builtin "git merge-one-file"
> + *
> + * Copyright (c) 2020 Alban Gruin
> + *
> + * Based on git-merge-one-file.sh, written by Linus Torvalds.
> + *
> + * This is the git per-file merge utility, called with
> + *
> + * argv[1] - original file SHA1 (or empty)
> + * argv[2] - file in branch1 SHA1 (or empty)
> + * argv[3] - file in branch2 SHA1 (or empty)
Let's modernize this comment while we are at it.
SHA1 -> "object name" (or "blob object name")
> + * argv[4] - pathname in repository
> + * argv[5] - original file mode (or empty)
> + * argv[6] - file in branch1 mode (or empty)
> + * argv[7] - file in branch2 mode (or empty)
> + *
> + * Handle some trivial cases. The _really_ trivial cases have been
> + * handled already by git read-tree, but that one doesn't do any merges
> + * that might change the tree layout.
> + */
> +
> +#define USE_THE_INDEX_COMPATIBILITY_MACROS
> +#include "cache.h"
> +#include "builtin.h"
> +#include "lockfile.h"
> +#include "merge-strategies.h"
> +
> +static const char builtin_merge_one_file_usage[] =
> + "git merge-one-file <orig blob> <our blob> <their blob> <path> "
> + "<orig mode> <our mode> <their mode>\n\n"
> + "Blob ids and modes should be empty for missing files.";
> +
> +static int read_mode(const char *name, const char *arg, unsigned int *mode)
> +{
> + char *last;
> + int ret = 0;
> +
> + *mode = strtol(arg, &last, 8);
> +
> + if (*last)
> + ret = error(_("invalid '%s' mode: expected nothing, got '%c'"), name, *last);
> + else if (!(S_ISREG(*mode) || S_ISDIR(*mode) || S_ISLNK(*mode)))
> + ret = error(_("invalid '%s' mode: %o"), name, *mode);
> +
> + return ret;
> +}
> +
> +int cmd_merge_one_file(int argc, const char **argv, const char *prefix)
> +{
> + struct object_id orig_blob, our_blob, their_blob,
> + *p_orig_blob = NULL, *p_our_blob = NULL, *p_their_blob = NULL;
> + unsigned int orig_mode = 0, our_mode = 0, their_mode = 0, ret = 0;
> + struct lock_file lock = LOCK_INIT;
> +
> + if (argc != 8)
> + usage(builtin_merge_one_file_usage);
> +
> + if (read_cache() < 0)
> + die("invalid index");
> +
> + hold_locked_index(&lock, LOCK_DIE_ON_ERROR);
> +
> + if (!get_oid(argv[1], &orig_blob)) {
> + p_orig_blob = &orig_blob;
> + ret = read_mode("orig", argv[5], &orig_mode);
> + }
argv[1] is defined as "either the object name of the blob in the
common ancestor, or an empty string". So you need to distinguish
three cases here, but you are only catching two.
- argv[1] is an empty string; p_orig_blob can legitimately be left
NULL.
- argv[1] is a valid blob object name. orig_blob should be
populated and p_orig_blob should point at it.
- argv[1] is garbage, names a non-blob object, or there is no such
object with that name. Don't we want to catch it as a mistake?
Also, when argv[1] is an empty string, argv[5] must also be an empty
string, or we got a wrong input---don't we want to catch it as a
mistake?
The third case needs a bit of thought. For example, if $1 and $2
are the same and points at a non-existent object, we know we won't
care because we only care about $3. In a lazily-cloned repository,
that may matter---we would not want to fail even if we not have blob
$1 and $2, as long as they are reasonably spelled a full hexadecimal
object name. But we would want to fail if blob object named by $3
is missing.
One way to achieve semantics closer to the above than the posted
patch may be to tighten the parsing. Instead of using "anything
goes" get_oid(), use get_oid_hex(), perhaps.
> + if (!get_oid(argv[2], &our_blob)) {
> + p_our_blob = &our_blob;
> + ret = read_mode("our", argv[6], &our_mode);
> + }
> +
> + if (!get_oid(argv[3], &their_blob)) {
> + p_their_blob = &their_blob;
> + ret = read_mode("their", argv[7], &their_mode);
> + }
> +
> + if (ret)
> + return ret;
> +
> + ret = merge_strategies_one_file(the_repository,
> + p_orig_blob, p_our_blob, p_their_blob, argv[4],
> + orig_mode, our_mode, their_mode);
That's a funny function name. It's not like the function will be
taught different strategy to handle the three-way merge, no? It
probably makes sense to name it after what it does, which is "three
way merge".
> + if (ret) {
> + rollback_lock_file(&lock);
> + return !!ret;
> + }
> +
> + return write_locked_index(&the_index, &lock, COMMIT_LOCK);
> +}
> diff --git a/merge-strategies.c b/merge-strategies.c
> new file mode 100644
> index 0000000000..bbe6f48698
> --- /dev/null
> +++ b/merge-strategies.c
> @@ -0,0 +1,214 @@
> +#include "cache.h"
> +#include "dir.h"
> +#include "ll-merge.h"
> +#include "merge-strategies.h"
> +#include "xdiff-interface.h"
> +
> +static int add_to_index_cacheinfo(struct index_state *istate,
> + unsigned int mode,
> + const struct object_id *oid, const char *path)
> +{
> + struct cache_entry *ce;
> + int len, option;
> +
> + if (!verify_path(path, mode))
> + return error(_("Invalid path '%s'"), path);
> +
> + len = strlen(path);
> + ce = make_empty_cache_entry(istate, len);
> +
> + oidcpy(&ce->oid, oid);
> + memcpy(ce->name, path, len);
> + ce->ce_flags = create_ce_flags(0);
> + ce->ce_namelen = len;
> + ce->ce_mode = create_ce_mode(mode);
> + if (assume_unchanged)
> + ce->ce_flags |= CE_VALID;
> + option = ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE;
> + if (add_index_entry(istate, ce, option))
> + return error(_("%s: cannot add to the index"), path);
> +
> + return 0;
> +}
The above correctly does 'git update-index --add --cacheinfo "$6"
"$2" "$4"' but don't copy-and-paste existing code to do so. Add one
preliminary patch before everything else in the series to massage
and extract add_cacheinfo() function out of builtin/update-index.c,
move it to somewhere common like read-cache.c and so that we can
call it from here.
> +static int checkout_from_index(struct index_state *istate, const char *path)
> +{
> + struct checkout state = CHECKOUT_INIT;
> + struct cache_entry *ce;
> +
> + state.istate = istate;
> + state.force = 1;
> + state.base_dir = "";
> + state.base_dir_len = 0;
> +
> + ce = index_file_exists(istate, path, strlen(path), 0);
This call is unfortunate for the reasons I mention later.
But if you must have this call, then you need to sanity check what
you get from index_file_exists(). ce must be a merged cache entry,
so
if (!ce || ce_stage(ce))
BUG(...);
> + if (checkout_entry(ce, &state, NULL, NULL) < 0)
> + return error(_("%s: cannot checkout file"), path);
> + return 0;
> +}
> +
> +static int merge_one_file_deleted(struct index_state *istate,
> + const struct object_id *orig_blob,
> + const struct object_id *our_blob,
> + const struct object_id *their_blob, const char *path,
> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
> +{
> + if ((our_blob && orig_mode != our_mode) ||
> + (their_blob && orig_mode != their_mode))
> + return error(_("File %s deleted on one branch but had its "
> + "permissions changed on the other."), path);
> +
> + if (our_blob) {
> + printf(_("Removing %s\n"), path);
> +
> + if (file_exists(path))
> + remove_path(path);
> + }
> +
> + if (remove_file_from_index(istate, path))
> + return error("%s: cannot remove from the index", path);
> + return 0;
If the side that did not remove changed the mode, we don't silently
remove but fail and give a chance to inspect the situation to the
end user. If we had the blob and it is removed by them, we give a
message and only in that case we remove the file from the working
tree, together with any leading directory that has become empty.
And after that we make sure that the path is no longer in the
index. The function removes entries for the path at all the stages,
which is exactly what we want.
OK.
> +}
> +
> +static int do_merge_one_file(struct index_state *istate,
> + const struct object_id *orig_blob,
> + const struct object_id *our_blob,
> + const struct object_id *their_blob, const char *path,
> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
> +{
> + int ret, i, dest;
> + ssize_t written;
> + mmbuffer_t result = {NULL, 0};
> + mmfile_t mmfs[3];
> + struct ll_merge_options merge_opts = {0};
> + struct cache_entry *ce;
> +
> + if (our_mode == S_IFLNK || their_mode == S_IFLNK)
> + return error(_("%s: Not merging symbolic link changes."), path);
> + else if (our_mode == S_IFGITLINK || their_mode == S_IFGITLINK)
> + return error(_("%s: Not merging conflicting submodule changes."), path);
> +
> + read_mmblob(mmfs + 1, our_blob);
> + read_mmblob(mmfs + 2, their_blob);
> +
> + if (orig_blob) {
> + printf(_("Auto-merging %s\n"), path);
> + read_mmblob(mmfs + 0, orig_blob);
> + } else {
> + printf(_("Added %s in both, but differently.\n"), path);
> + read_mmblob(mmfs + 0, &null_oid);
> + }
> +
> + merge_opts.xdl_opts = XDL_MERGE_ZEALOUS_ALNUM;
> + ret = ll_merge(&result, path,
> + mmfs + 0, "orig",
> + mmfs + 1, "our",
> + mmfs + 2, "their",
> + istate, &merge_opts);
Is it correct to call into ll_merge() here? The original used to
call "git merge-file" which called into xdl_merge(). Calling into
ll_merge() means the path is used to look up the attributes and use
the custom merge driver, which I am not offhand sure is what we want
to see at this low level (and if it turns out to be a good idea, we
definitely should explain the change of semantics in the proposed
log message for this commit).
> + for (i = 0; i < 3; i++)
> + free(mmfs[i].ptr);
> +
> + if (ret < 0) {
> + free(result.ptr);
> + return error(_("Failed to execute internal merge"));
> + }
> +
> + /*
> + * Create the working tree file, using "our tree" version from
> + * the index, and then store the result of the merge.
> + */
The above is copied from the original, to explain what it did after
the comment, but it does not seem to match what the new code does.
> + ce = index_file_exists(istate, path, strlen(path), 0);
> + if (!ce)
> + BUG("file is not present in the cache?");
> +
> + unlink(path);
> + if ((dest = open(path, O_WRONLY | O_CREAT, ce->ce_mode)) < 0) {
> + free(result.ptr);
> + return error_errno(_("failed to open file '%s'"), path);
> + }
> +
> + written = write_in_full(dest, result.ptr, result.size);
> + close(dest);
> +
> + free(result.ptr);
> +
> + if (written < 0)
> + return error_errno(_("failed to write to '%s'"), path);
> +
This open(..., ce->ce_mode) call is way insufficient.
The comment we have above this part of the code talks about the
difficulty of doing this correctly in scripted version. Creating a
file by 'git checkout-index -f --stage=2 -- "$4"' and reusing it to
store the merged contents was the cleanest and easiest way without
having direct access to adjust_shared_perm() to create a working
tree file with the correct permission bits.
We are writing in C, so we should be able to do much better than the
scripted version, as we can later call adjust_shared_perm().
> + if (ret != 0 || !orig_blob)
> + ret = error(_("content conflict in %s"), path);
> + if (our_mode != their_mode)
> + return error(_("permission conflict: %o->%o,%o in %s"),
> + orig_mode, our_mode, their_mode, path);
> + if (ret)
> + return -1;
> +
> + return add_file_to_index(istate, path, 0);
> +}
> +
> +int merge_strategies_one_file(struct repository *r,
> + const struct object_id *orig_blob,
> + const struct object_id *our_blob,
> + const struct object_id *their_blob, const char *path,
> + unsigned int orig_mode, unsigned int our_mode,
> + unsigned int their_mode)
> +{
In a long if/else if/else if/.../else cascade, enclose all bodies in
braces, if any one of them has a multi-statement body, to avoid
being distracting.
> + if (orig_blob &&
> + ((!their_blob && our_blob && oideq(orig_blob, our_blob)) ||
> + (!our_blob && their_blob && oideq(orig_blob, their_blob))))
> + /* Deleted in both or deleted in one and unchanged in the other. */
> + return merge_one_file_deleted(r->index,
> + orig_blob, our_blob, their_blob, path,
> + orig_mode, our_mode, their_mode);
OK, we've already reviewed that function.
> + else if (!orig_blob && our_blob && !their_blob) {
> + /*
> + * Added in one. The other side did not add and we
> + * added so there is nothing to be done, except making
> + * the path merged.
> + */
> + return add_to_index_cacheinfo(r->index, our_mode, our_blob, path);
OK, we've already reviewed that function.
> + } else if (!orig_blob && !our_blob && their_blob) {
> + printf(_("Adding %s\n"), path);
> +
> + if (file_exists(path))
> + return error(_("untracked %s is overwritten by the merge."), path);
> +
> + if (add_to_index_cacheinfo(r->index, their_mode, their_blob, path))
> + return -1;
> + return checkout_from_index(r->index, path);
You did "add_to_index_cacheinfo()", so you MUST know which ce is to
be checked out.
Consider if it is worth to teach add_to_index_cacheinfo() to give
you ce back and pass it to checkout_from_index(); that way, you do
not have to call index_file_exists() based on path in the function.
> + } else if (!orig_blob && our_blob && their_blob &&
> + oideq(our_blob, their_blob)) {
> + /* Added in both, identically (check for same permissions). */
> + if (our_mode != their_mode)
> + return error(_("File %s added identically in both branches, "
> + "but permissions conflict %o->%o."),
> + path, our_mode, their_mode);
> +
> + printf(_("Adding %s\n"), path);
> +
> + if (add_to_index_cacheinfo(r->index, our_mode, our_blob, path))
> + return -1;
> + return checkout_from_index(r->index, path);
Ditto.
> + } else if (our_blob && their_blob)
> + /* Modified in both, but differently. */
> + return do_merge_one_file(r->index,
> + orig_blob, our_blob, their_blob, path,
> + orig_mode, our_mode, their_mode);
> + else {
> + char orig_hex[GIT_MAX_HEXSZ] = {0}, our_hex[GIT_MAX_HEXSZ] = {0},
> + their_hex[GIT_MAX_HEXSZ] = {0};
> +
> + if (orig_blob)
> + oid_to_hex_r(orig_hex, orig_blob);
> + if (our_blob)
> + oid_to_hex_r(our_hex, our_blob);
> + if (their_blob)
> + oid_to_hex_r(their_hex, their_blob);
> +
> + return error(_("%s: Not handling case %s -> %s -> %s"),
> + path, orig_hex, our_hex, their_hex);
> + }
> +
> + return 0;
> +}
I can see that this does go in the right direction. With a bit more
attention to details it would soon be production-ready quality.
Thanks.
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [PATCH v3 00/11] Rewrite the remaining merge strategies from shell to C
2020-10-05 12:26 ` [PATCH v3 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (10 preceding siblings ...)
2020-10-05 12:26 ` [PATCH v3 11/11] sequencer: use the "octopus" merge " Alban Gruin
@ 2020-10-07 6:57 ` Johannes Schindelin
2020-11-13 11:04 ` [PATCH v4 00/12] " Alban Gruin
12 siblings, 0 replies; 221+ messages in thread
From: Johannes Schindelin @ 2020-10-07 6:57 UTC (permalink / raw)
To: Alban Gruin; +Cc: git, Junio C Hamano, phillip.wood
Hi Alban,
On Mon, 5 Oct 2020, Alban Gruin wrote:
> In a effort to reduce the number of shell scripts in git's codebase, I
> propose this patch series converting the two remaining merge strategies,
> resolve and octopus, from shell to C. This will enable slightly better
> performance, better integration with git itself (no more forking to
> perform these operations), better portability (Windows and shell scripts
> don't mix well).
>
> Three scripts are actually converted: first git-merge-one-file.sh, then
> git-merge-resolve.sh, and finally git-merge-octopus.sh. Not only they
> are converted, but they also are modified to operate without forking,
> and then libified so they can be used by git without spawning another
> process.
>
> The first patch is not important to make the whole series work, but I
> made this patch while working on it.
>
> This series keeps the commands `git merge-one-file', `git
> merge-resolve', and `git merge-octopus', so any script depending on them
> should keep working without any changes.
While that may be true, with SKIP_DASHED_BUILT_INS=YesPlease, it is no
longer true. And that is a good thing!
However, it also broke the CI build (`seen` sees a breakage in t9902.199).
I will send out a fix shortly (see
https://github.com/gitgitgadget/git/pull/745 for details).
Ciao,
Dscho
>
> This series is based on 306ee63a70 (Eighteenth batch, 2020-09-29). The
> tip is tagged as "rewrite-merge-strategies-v3" at
> https://github.com/agrn/git.
>
> Changes since v2:
>
> - Enable `USE_THE_INDEX_COMPATIBILITY_MACROS' in merge-one-file.c and
> use read_cache() and hold_locked_index() instead of repo_read_index()
> and repo_hold_locked_index() to improve readability.
>
> - Move file mode parsing to its own function in merge-one-file.c.
>
> - Improve IO errors handling in do_merge_one_file().
>
> - Return -1 instead of 1 when erroring out in do_merge_one_file() and
> merge_strategies_one_file().
>
> - Use oid_to_hex_r() instead of oid_to_hex() in do_merge_one_file().
>
> - Reformat multilines comments.
>
> - Reworded a sentence in commit 3/11.
>
> Alban Gruin (11):
> t6027: modernise tests
> merge-one-file: rewrite in C
> merge-index: libify merge_one_path() and merge_all()
> merge-index: don't fork if the requested program is
> `git-merge-one-file'
> merge-resolve: rewrite in C
> merge-recursive: move better_branch_name() to merge.c
> merge-octopus: rewrite in C
> merge: use the "resolve" strategy without forking
> merge: use the "octopus" strategy without forking
> sequencer: use the "resolve" strategy without forking
> sequencer: use the "octopus" merge strategy without forking
>
> Makefile | 7 +-
> builtin.h | 3 +
> builtin/merge-index.c | 102 ++----
> builtin/merge-octopus.c | 69 ++++
> builtin/merge-one-file.c | 92 +++++
> builtin/merge-recursive.c | 16 +-
> builtin/merge-resolve.c | 69 ++++
> builtin/merge.c | 9 +-
> cache.h | 2 +-
> git-merge-octopus.sh | 112 ------
> git-merge-one-file.sh | 167 ---------
> git-merge-resolve.sh | 54 ---
> git.c | 3 +
> merge-strategies.c | 613 ++++++++++++++++++++++++++++++++
> merge-strategies.h | 44 +++
> merge.c | 12 +
> sequencer.c | 16 +-
> t/t6407-merge-binary.sh | 27 +-
> t/t6415-merge-dir-to-symlink.sh | 2 +-
> 19 files changed, 972 insertions(+), 447 deletions(-)
> create mode 100644 builtin/merge-octopus.c
> create mode 100644 builtin/merge-one-file.c
> create mode 100644 builtin/merge-resolve.c
> delete mode 100755 git-merge-octopus.sh
> delete mode 100755 git-merge-one-file.sh
> delete mode 100755 git-merge-resolve.sh
> create mode 100644 merge-strategies.c
> create mode 100644 merge-strategies.h
>
> Range-diff against v2:
> 1: 28c8fd11b6 = 1: 08c7df596a t6027: modernise tests
> 2: f5ab0fdf0a ! 2: ce911c99c0 merge-one-file: rewrite in C
> @@ builtin/merge-one-file.c (new)
> + * that might change the tree layout.
> + */
> +
> ++#define USE_THE_INDEX_COMPATIBILITY_MACROS
> +#include "cache.h"
> +#include "builtin.h"
> +#include "lockfile.h"
> @@ builtin/merge-one-file.c (new)
> + "<orig mode> <our mode> <their mode>\n\n"
> + "Blob ids and modes should be empty for missing files.";
> +
> ++static int read_mode(const char *name, const char *arg, unsigned int *mode)
> ++{
> ++ char *last;
> ++ int ret = 0;
> ++
> ++ *mode = strtol(arg, &last, 8);
> ++
> ++ if (*last)
> ++ ret = error(_("invalid '%s' mode: expected nothing, got '%c'"), name, *last);
> ++ else if (!(S_ISREG(*mode) || S_ISDIR(*mode) || S_ISLNK(*mode)))
> ++ ret = error(_("invalid '%s' mode: %o"), name, *mode);
> ++
> ++ return ret;
> ++}
> ++
> +int cmd_merge_one_file(int argc, const char **argv, const char *prefix)
> +{
> + struct object_id orig_blob, our_blob, their_blob,
> @@ builtin/merge-one-file.c (new)
> + if (argc != 8)
> + usage(builtin_merge_one_file_usage);
> +
> -+ if (repo_read_index(the_repository) < 0)
> ++ if (read_cache() < 0)
> + die("invalid index");
> +
> -+ repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
> ++ hold_locked_index(&lock, LOCK_DIE_ON_ERROR);
> +
> + if (!get_oid(argv[1], &orig_blob)) {
> + p_orig_blob = &orig_blob;
> -+ orig_mode = strtol(argv[5], NULL, 8);
> -+
> -+ if (!(S_ISREG(orig_mode) || S_ISDIR(orig_mode) || S_ISLNK(orig_mode)))
> -+ ret |= error(_("invalid 'orig' mode: %o"), orig_mode);
> ++ ret = read_mode("orig", argv[5], &orig_mode);
> + }
> +
> + if (!get_oid(argv[2], &our_blob)) {
> + p_our_blob = &our_blob;
> -+ our_mode = strtol(argv[6], NULL, 8);
> -+
> -+ if (!(S_ISREG(our_mode) || S_ISDIR(our_mode) || S_ISLNK(our_mode)))
> -+ ret |= error(_("invalid 'our' mode: %o"), our_mode);
> ++ ret = read_mode("our", argv[6], &our_mode);
> + }
> +
> + if (!get_oid(argv[3], &their_blob)) {
> + p_their_blob = &their_blob;
> -+ their_mode = strtol(argv[7], NULL, 8);
> -+
> -+ if (!(S_ISREG(their_mode) || S_ISDIR(their_mode) || S_ISLNK(their_mode)))
> -+ ret = error(_("invalid 'their' mode: %o"), their_mode);
> ++ ret = read_mode("their", argv[7], &their_mode);
> + }
> +
> + if (ret)
> @@ builtin/merge-one-file.c (new)
> +
> + if (ret) {
> + rollback_lock_file(&lock);
> -+ return ret;
> ++ return !!ret;
> + }
> +
> -+ return write_locked_index(the_repository->index, &lock, COMMIT_LOCK);
> ++ return write_locked_index(&the_index, &lock, COMMIT_LOCK);
> +}
>
> ## git-merge-one-file.sh (deleted) ##
> @@ merge-strategies.c (new)
> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
> +{
> + int ret, i, dest;
> ++ ssize_t written;
> + mmbuffer_t result = {NULL, 0};
> + mmfile_t mmfs[3];
> + struct ll_merge_options merge_opts = {0};
> @@ merge-strategies.c (new)
> + for (i = 0; i < 3; i++)
> + free(mmfs[i].ptr);
> +
> -+ if (ret > 127 || !orig_blob)
> -+ ret = error(_("content conflict in %s"), path);
> ++ if (ret < 0) {
> ++ free(result.ptr);
> ++ return error(_("Failed to execute internal merge"));
> ++ }
> +
> -+ /* Create the working tree file, using "our tree" version from
> -+ the index, and then store the result of the merge. */
> ++ /*
> ++ * Create the working tree file, using "our tree" version from
> ++ * the index, and then store the result of the merge.
> ++ */
> + ce = index_file_exists(istate, path, strlen(path), 0);
> + if (!ce)
> + BUG("file is not present in the cache?");
> +
> + unlink(path);
> -+ dest = open(path, O_WRONLY | O_CREAT, ce->ce_mode);
> -+ write_in_full(dest, result.ptr, result.size);
> ++ if ((dest = open(path, O_WRONLY | O_CREAT, ce->ce_mode)) < 0) {
> ++ free(result.ptr);
> ++ return error_errno(_("failed to open file '%s'"), path);
> ++ }
> ++
> ++ written = write_in_full(dest, result.ptr, result.size);
> + close(dest);
> +
> + free(result.ptr);
> +
> -+ if (ret && our_mode != their_mode)
> ++ if (written < 0)
> ++ return error_errno(_("failed to write to '%s'"), path);
> ++
> ++ if (ret != 0 || !orig_blob)
> ++ ret = error(_("content conflict in %s"), path);
> ++ if (our_mode != their_mode)
> + return error(_("permission conflict: %o->%o,%o in %s"),
> + orig_mode, our_mode, their_mode, path);
> + if (ret)
> -+ return 1;
> ++ return -1;
> +
> + return add_file_to_index(istate, path, 0);
> +}
> @@ merge-strategies.c (new)
> + if (orig_blob &&
> + ((!their_blob && our_blob && oideq(orig_blob, our_blob)) ||
> + (!our_blob && their_blob && oideq(orig_blob, their_blob))))
> -+ /* Deleted in both or deleted in one and unchanged in
> -+ the other */
> ++ /* Deleted in both or deleted in one and unchanged in the other. */
> + return merge_one_file_deleted(r->index,
> + orig_blob, our_blob, their_blob, path,
> + orig_mode, our_mode, their_mode);
> + else if (!orig_blob && our_blob && !their_blob) {
> -+ /* Added in one. The other side did not add and we
> -+ added so there is nothing to be done, except making
> -+ the path merged. */
> ++ /*
> ++ * Added in one. The other side did not add and we
> ++ * added so there is nothing to be done, except making
> ++ * the path merged.
> ++ */
> + return add_to_index_cacheinfo(r->index, our_mode, our_blob, path);
> + } else if (!orig_blob && !our_blob && their_blob) {
> + printf(_("Adding %s\n"), path);
> @@ merge-strategies.c (new)
> + return error(_("untracked %s is overwritten by the merge."), path);
> +
> + if (add_to_index_cacheinfo(r->index, their_mode, their_blob, path))
> -+ return 1;
> ++ return -1;
> + return checkout_from_index(r->index, path);
> + } else if (!orig_blob && our_blob && their_blob &&
> + oideq(our_blob, their_blob)) {
> -+ /* Added in both, identically (check for same
> -+ permissions). */
> ++ /* Added in both, identically (check for same permissions). */
> + if (our_mode != their_mode)
> + return error(_("File %s added identically in both branches, "
> + "but permissions conflict %o->%o."),
> @@ merge-strategies.c (new)
> + printf(_("Adding %s\n"), path);
> +
> + if (add_to_index_cacheinfo(r->index, our_mode, our_blob, path))
> -+ return 1;
> ++ return -1;
> + return checkout_from_index(r->index, path);
> + } else if (our_blob && their_blob)
> + /* Modified in both, but differently. */
> @@ merge-strategies.c (new)
> + orig_blob, our_blob, their_blob, path,
> + orig_mode, our_mode, their_mode);
> + else {
> -+ char *orig_hex = "", *our_hex = "", *their_hex = "";
> ++ char orig_hex[GIT_MAX_HEXSZ] = {0}, our_hex[GIT_MAX_HEXSZ] = {0},
> ++ their_hex[GIT_MAX_HEXSZ] = {0};
> +
> + if (orig_blob)
> -+ orig_hex = oid_to_hex(orig_blob);
> ++ oid_to_hex_r(orig_hex, orig_blob);
> + if (our_blob)
> -+ our_hex = oid_to_hex(our_blob);
> ++ oid_to_hex_r(our_hex, our_blob);
> + if (their_blob)
> -+ their_hex = oid_to_hex(their_blob);
> ++ oid_to_hex_r(their_hex, their_blob);
> +
> + return error(_("%s: Not handling case %s -> %s -> %s"),
> + path, orig_hex, our_hex, their_hex);
> 3: 7f3ce7da17 ! 3: 7f0999f5a3 merge-index: libify merge_one_path() and merge_all()
> @@ Commit message
>
> To avoid this, this moves merge_one_path(), merge_all(), and their
> helpers to merge-strategies.c. They also take a callback to dictate
> - what they should do for each file. For now, only one launching a new
> - process is defined to preserve the behaviour of the builtin version.
> + what they should do for each file. For now, to preserve the behaviour
> + of `merge-index', only one callback, launching a new process, is
> + defined.
>
> Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
>
> 4: 07e6a6aaef = 4: c0bc05406d merge-index: don't fork if the requested program is `git-merge-one-file'
> 5: 117d4fc840 = 5: cbfe192982 merge-resolve: rewrite in C
> 6: 4fc955962b = 6: 35e386f626 merge-recursive: move better_branch_name() to merge.c
> 7: e7b9e15b34 ! 7: 41eb0f7199 merge-octopus: rewrite in C
> @@ Makefile: BUILTIN_OBJS += builtin/mailsplit.o
> BUILTIN_OBJS += builtin/merge-recursive.o
>
> ## builtin.h ##
> -@@ builtin.h: int cmd_mailsplit(int argc, const char **argv, const char *prefix);
> +@@ builtin.h: int cmd_maintenance(int argc, const char **argv, const char *prefix);
> int cmd_merge(int argc, const char **argv, const char *prefix);
> int cmd_merge_base(int argc, const char **argv, const char *prefix);
> int cmd_merge_index(int argc, const char **argv, const char *prefix);
> @@ builtin/merge-octopus.c (new)
> + if (repo_read_index(the_repository) < 0)
> + die("corrupted cache");
> +
> -+ /* The first parameters up to -- are merge bases; the rest are
> -+ * heads. */
> ++ /*
> ++ * The first parameters up to -- are merge bases; the rest are
> ++ * heads.
> ++ */
> + for (i = 1; i < argc; i++) {
> + if (strcmp(argv[i], "--") == 0)
> + sep_seen = 1;
> @@ builtin/merge-octopus.c (new)
> + }
> + }
> +
> -+ /* Reject if this is not an octopus -- resolve should be used
> -+ * instead. */
> ++ /*
> ++ * Reject if this is not an octopus -- resolve should be used
> ++ * instead.
> ++ */
> + if (commit_list_count(remotes) < 2)
> + return 2;
> +
> @@ merge-strategies.c: int merge_strategies_resolve(struct repository *r,
> + int can_ff = 1;
> +
> + if (ret) {
> -+ /* We allow only last one to have a
> -+ hand-resolvable conflicts. Last round failed
> -+ and we still had a head to merge. */
> ++ /*
> ++ * We allow only last one to have a
> ++ * hand-resolvable conflicts. Last round failed
> ++ * and we still had a head to merge.
> ++ */
> + puts(_("Automated merge did not work."));
> + puts(_("Should not be doing an octopus."));
> +
> @@ merge-strategies.c: int merge_strategies_resolve(struct repository *r,
> + }
> +
> + if (!non_ff_merge && can_ff) {
> -+ /* The first head being merged was a
> -+ fast-forward. Advance the reference commit
> -+ to the head being merged, and use that tree
> -+ as the intermediate result of the merge. We
> -+ still need to count this as part of the
> -+ parent set. */
> ++ /*
> ++ * The first head being merged was a
> ++ * fast-forward. Advance the reference commit
> ++ * to the head being merged, and use that tree
> ++ * as the intermediate result of the merge. We
> ++ * still need to count this as part of the
> ++ * parent set.
> ++ */
> + struct object_id oids[2];
> + printf(_("Fast-forwarding to: %s\n"), branch_name);
> +
> 8: cd0662201d = 8: 8f6c1ac057 merge: use the "resolve" strategy without forking
> 9: 0525ff0183 = 9: b1125261d1 merge: use the "octopus" strategy without forking
> 10: 6fbf599ba4 = 10: 8d0932fd02 sequencer: use the "resolve" strategy without forking
> 11: 2c2dc3cc62 = 11: e304723957 sequencer: use the "octopus" merge strategy without forking
> --
> 2.28.0.662.ge304723957
>
>
>
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [PATCH v3 03/11] merge-index: libify merge_one_path() and merge_all()
2020-10-05 12:26 ` [PATCH v3 03/11] merge-index: libify merge_one_path() and merge_all() Alban Gruin
@ 2020-10-09 4:48 ` Junio C Hamano
2020-11-06 19:53 ` Alban Gruin
0 siblings, 1 reply; 221+ messages in thread
From: Junio C Hamano @ 2020-10-09 4:48 UTC (permalink / raw)
To: Alban Gruin; +Cc: git, phillip.wood
Alban Gruin <alban.gruin@gmail.com> writes:
> diff --git a/merge-strategies.c b/merge-strategies.c
> index bbe6f48698..f0e30f5624 100644
> --- a/merge-strategies.c
> +++ b/merge-strategies.c
> @@ -2,6 +2,7 @@
> #include "dir.h"
> #include "ll-merge.h"
> #include "merge-strategies.h"
> +#include "run-command.h"
> #include "xdiff-interface.h"
>
> static int add_to_index_cacheinfo(struct index_state *istate,
> @@ -212,3 +213,101 @@ int merge_strategies_one_file(struct repository *r,
>
> return 0;
> }
> +
> +int merge_program_cb(const struct object_id *orig_blob,
> + const struct object_id *our_blob,
> + const struct object_id *their_blob, const char *path,
> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
> + void *data)
> +{
> + char ownbuf[3][GIT_MAX_HEXSZ] = {{0}};
> + const char *arguments[] = { (char *)data, "", "", "", path,
> + ownbuf[0], ownbuf[1], ownbuf[2],
> + NULL };
> +
> + if (orig_blob)
> + arguments[1] = oid_to_hex(orig_blob);
> + if (our_blob)
> + arguments[2] = oid_to_hex(our_blob);
> + if (their_blob)
> + arguments[3] = oid_to_hex(their_blob);
oid_to_hex() uses 4-slot rotating buffer, no? Relying on the fact
that three would be available here without getting reused (or,
rather, our caller didn't make its own calls and/or does not mind
us invalidating all but one slot for them) feels a bit iffy.
Extending ownbuf[] to 6 elements and using oid_to_hex_r() would be a
trivial way to clarify the code.
> + xsnprintf(ownbuf[0], sizeof(ownbuf[0]), "%o", orig_mode);
> + xsnprintf(ownbuf[1], sizeof(ownbuf[1]), "%o", our_mode);
> + xsnprintf(ownbuf[2], sizeof(ownbuf[2]), "%o", their_mode);
And these mode bits would not need GIT_MAX_HEXSZ to begin with.
This smells like a WIP that hasn't been carefullly proofread.
char oidbuf[3][GIT_MAX_HEXSZ] = { 0 };
char modebuf[3][8] = { 0 };
char *args[] = {
data, oidbuf[0], oidbuf[1], oidbuf[2], path,
modebuf[0], modebuf[1], modebuf[2], NULL,
};
if (orig_blob)
oid_to_hex_r(oidbuf[0], orig_blob);
...
xsnprintf(modebuf[0], ...);
Eh, wait. Is this meant to be able to drive "git-merge-one-file",
i.e. a missing common/ours/theirs is indicated by an empty string
in both oiod and mode? If so, an unconditional xsnprintf() would
either give garbage or "0" at best, neither of which is an empty
string. So the body would be more like
if (orig_blob) {
oid_to_hex_r(oidbuf[0], orig_blob);
xsnprintf(modebuf[0], "%o", orig_mode);
}
if (our_blob) {
oid_to_hex_r(oidbuf[1], our_blob);
xsnprintf(modebuf[1], "%o", our_mode);
}
...
wouldn't it?
> + return run_command_v_opt(arguments, 0);
> +}
> +
> +static int merge_entry(struct index_state *istate, int quiet, int pos,
> + const char *path, merge_cb cb, void *data)
When we use an identifier "cb", it typically means callback data,
not a callback function which is often called "fn". So, name the
type "merge_fn" (or "merge_func"), and call the parameter "fn".
> +{
> + int found = 0;
> + const struct object_id *oids[3] = {NULL};
> + unsigned int modes[3] = {0};
> +
> + do {
> + const struct cache_entry *ce = istate->cache[pos];
> + int stage = ce_stage(ce);
> +
> + if (strcmp(ce->name, path))
> + break;
> + found++;
> + oids[stage - 1] = &ce->oid;
> + modes[stage - 1] = ce->ce_mode;
> + } while (++pos < istate->cache_nr);
> + if (!found)
> + return error(_("%s is not in the cache"), path);
> +
> + if (cb(oids[0], oids[1], oids[2], path, modes[0], modes[1], modes[2], data)) {
> + if (!quiet)
> + error(_("Merge program failed"));
> + return -2;
> + }
> +
> + return found;
> +}
This copies from builtin/merge-index.c::merge_entry().
> +int merge_one_path(struct index_state *istate, int oneshot, int quiet,
> + const char *path, merge_cb cb, void *data)
> +{
> + int pos = index_name_pos(istate, path, strlen(path)), ret;
> +
> + /*
> + * If it already exists in the cache as stage0, it's
> + * already merged and there is nothing to do.
> + */
> + if (pos < 0) {
> + ret = merge_entry(istate, quiet, -pos - 1, path, cb, data);
> + if (ret == -1)
> + return -1;
> + else if (ret == -2)
> + return 1;
> + }
> + return 0;
> +}
Likewise from the same function in that file.
Are we removing the "git merge-index" program? Reusing the same
identifier for these copied-and-pasted pairs of functions bothers
me for two reasons.
- An indentifier that was clear and unique enough in the original
context as a file-scope static may not be a good name as a global
identifier.
- Having two similar-looking functions with the same name makes
reading and learning the codebase starting at "git grep" hits
more difficult than necessary.
> +int merge_all(struct index_state *istate, int oneshot, int quiet,
> + merge_cb cb, void *data)
> +{
> + int err = 0, i, ret;
> + for (i = 0; i < istate->cache_nr; i++) {
> + const struct cache_entry *ce = istate->cache[i];
> + if (!ce_stage(ce))
> + continue;
> +
> + ret = merge_entry(istate, quiet, i, ce->name, cb, data);
> + if (ret > 0)
> + i += ret - 1;
> + else if (ret == -1)
> + return -1;
> + else if (ret == -2) {
> + if (oneshot)
> + err++;
> + else
> + return 1;
> + }
> + }
> +
> + return err;
> +}
Likewise.
> diff --git a/merge-strategies.h b/merge-strategies.h
> index b527d145c7..cf78d7eaf4 100644
> --- a/merge-strategies.h
> +++ b/merge-strategies.h
> @@ -10,4 +10,21 @@ int merge_strategies_one_file(struct repository *r,
> unsigned int orig_mode, unsigned int our_mode,
> unsigned int their_mode);
>
> +typedef int (*merge_cb)(const struct object_id *orig_blob,
> + const struct object_id *our_blob,
> + const struct object_id *their_blob, const char *path,
> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
> + void *data);
Call it "merge_one_file_func", probably.
> +int merge_program_cb(const struct object_id *orig_blob,
Call it spawn_merge_one_file() perhaps?
> + const struct object_id *our_blob,
> + const struct object_id *their_blob, const char *path,
> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
> + void *data);
> +
> +int merge_one_path(struct index_state *istate, int oneshot, int quiet,
> + const char *path, merge_cb cb, void *data);
> +int merge_all(struct index_state *istate, int oneshot, int quiet,
> + merge_cb cb, void *data);
> #endif /* MERGE_STRATEGIES_H */
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [PATCH v3 04/11] merge-index: don't fork if the requested program is `git-merge-one-file'
2020-10-05 12:26 ` [PATCH v3 04/11] merge-index: don't fork if the requested program is `git-merge-one-file' Alban Gruin
@ 2020-10-16 19:07 ` Junio C Hamano
0 siblings, 0 replies; 221+ messages in thread
From: Junio C Hamano @ 2020-10-16 19:07 UTC (permalink / raw)
To: Alban Gruin; +Cc: git, phillip.wood
Alban Gruin <alban.gruin@gmail.com> writes:
> Since `git-merge-one-file' has been rewritten and libified, this teaches
> `merge-index' to call merge_strategies_one_file() without forking using
> a new callback, merge_one_file_cb().
>
> Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
> ---
I do not know how much of the change in this patch survives when the
previous step gets adjusted, so I'll skip this step for now.
Thanks.
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [PATCH v3 05/11] merge-resolve: rewrite in C
2020-10-05 12:26 ` [PATCH v3 05/11] merge-resolve: rewrite in C Alban Gruin
@ 2020-10-16 19:19 ` Junio C Hamano
2020-11-06 19:53 ` Alban Gruin
0 siblings, 1 reply; 221+ messages in thread
From: Junio C Hamano @ 2020-10-16 19:19 UTC (permalink / raw)
To: Alban Gruin; +Cc: git, phillip.wood
Alban Gruin <alban.gruin@gmail.com> writes:
> +#include "cache.h"
> +#include "builtin.h"
> +#include "merge-strategies.h"
> +
> +static const char builtin_merge_resolve_usage[] =
> + "git merge-resolve <bases>... -- <head> <remote>";
> +
> +int cmd_merge_resolve(int argc, const char **argv, const char *prefix)
> +{
> + int i, is_baseless = 1, sep_seen = 0;
> + const char *head = NULL;
> + struct commit_list *bases = NULL, *remote = NULL;
> + struct commit_list **next_base = &bases;
> +
> + if (argc < 5)
> + usage(builtin_merge_resolve_usage);
> +
> + setup_work_tree();
> + if (repo_read_index(the_repository) < 0)
> + die("invalid index");
> +
> + /* The first parameters up to -- are merge bases; the rest are
> + * heads. */
Style (I won't repeat).
> + for (i = 1; i < argc; i++) {
> + if (strcmp(argv[i], "--") == 0)
if (!strcmp(...))
is more typical than comparing with "== 0".
> + sep_seen = 1;
> + else if (strcmp(argv[i], "-h") == 0)
> + usage(builtin_merge_resolve_usage);
> + else if (sep_seen && !head)
> + head = argv[i];
> + else if (remote) {
> + /* Give up if we are given two or more remotes.
> + * Not handling octopus. */
> + return 2;
> + } else {
> + struct object_id oid;
> +
> + get_oid(argv[i], &oid);
> + is_baseless &= sep_seen;
> +
> + if (!oideq(&oid, the_hash_algo->empty_tree)) {
What is this business about an empty tree about?
> + struct commit *commit;
> + commit = lookup_commit_or_die(&oid, argv[i]);
> +
> + if (sep_seen)
> + commit_list_append(commit, &remote);
> + else
> + next_base = commit_list_append(commit, next_base);
> + }
> + }
> + }
> +
> + /* Give up if this is a baseless merge. */
> + if (is_baseless)
> + return 2;
This is quite convoluted.
The original is much more straight-forward. We just said "grab
everything before we see '--' and call them bases; immediately after
'--' is HEAD and everything else is remote. Now do we have any
base? Otherwise we cannot handle it".
I cannot see an equivalence to it in the rewritten result, with the
bit operation with is_baseless and sep_seen. Wouldn't it be the
matter of checking if next_base is NULL, or is there something more
subtle that deserves in-code comment going on?
Thanks.
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [PATCH v3 02/11] merge-one-file: rewrite in C
2020-10-06 22:01 ` Junio C Hamano
@ 2020-10-21 19:47 ` Alban Gruin
2020-10-21 20:28 ` Junio C Hamano
2020-10-21 20:30 ` Junio C Hamano
0 siblings, 2 replies; 221+ messages in thread
From: Alban Gruin @ 2020-10-21 19:47 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git, phillip.wood
Hi Junio,
On 07/10/2020 00:01, Junio C Hamano wrote:
> Alban Gruin <alban.gruin@gmail.com> writes:
>
>> This rewrites `git merge-one-file' from shell to C. This port is not
>> completely straightforward: to save precious cycles by avoiding reading
>> and flushing the index repeatedly, write temporary files when an
>> operation can be performed in-memory, or allow other function to use the
>> rewrite without forking nor worrying about the index,...
>
> So, the in-core index is still used, but when the contents of the in-core
> index does not have to be written out disk, we just don't? Makes sense.
>
>> diff --git a/builtin/merge-one-file.c b/builtin/merge-one-file.c
>> new file mode 100644
>> index 0000000000..598338ba16
>> --- /dev/null
>> +++ b/builtin/merge-one-file.c
>> @@ -0,0 +1,92 @@
>> +/*
>> + * Builtin "git merge-one-file"
>> + *
>> + * Copyright (c) 2020 Alban Gruin
>> + *
>> + * Based on git-merge-one-file.sh, written by Linus Torvalds.
>> + *
>> + * This is the git per-file merge utility, called with
>> + *
>> + * argv[1] - original file SHA1 (or empty)
>> + * argv[2] - file in branch1 SHA1 (or empty)
>> + * argv[3] - file in branch2 SHA1 (or empty)
>
> Let's modernize this comment while we are at it.
>
> SHA1 -> "object name" (or "blob object name")
>
>> + * argv[4] - pathname in repository
>> + * argv[5] - original file mode (or empty)
>> + * argv[6] - file in branch1 mode (or empty)
>> + * argv[7] - file in branch2 mode (or empty)
>> + *
>> + * Handle some trivial cases. The _really_ trivial cases have been
>> + * handled already by git read-tree, but that one doesn't do any merges
>> + * that might change the tree layout.
>> + */
>> +
>> +#define USE_THE_INDEX_COMPATIBILITY_MACROS
>> +#include "cache.h"
>> +#include "builtin.h"
>> +#include "lockfile.h"
>> +#include "merge-strategies.h"
>> +
>> +static const char builtin_merge_one_file_usage[] =
>> + "git merge-one-file <orig blob> <our blob> <their blob> <path> "
>> + "<orig mode> <our mode> <their mode>\n\n"
>> + "Blob ids and modes should be empty for missing files.";
>> +
>> +static int read_mode(const char *name, const char *arg, unsigned int *mode)
>> +{
>> + char *last;
>> + int ret = 0;
>> +
>> + *mode = strtol(arg, &last, 8);
>> +
>> + if (*last)
>> + ret = error(_("invalid '%s' mode: expected nothing, got '%c'"), name, *last);
>> + else if (!(S_ISREG(*mode) || S_ISDIR(*mode) || S_ISLNK(*mode)))
>> + ret = error(_("invalid '%s' mode: %o"), name, *mode);
>> +
>> + return ret;
>> +}
>> +
>> +int cmd_merge_one_file(int argc, const char **argv, const char *prefix)
>> +{
>> + struct object_id orig_blob, our_blob, their_blob,
>> + *p_orig_blob = NULL, *p_our_blob = NULL, *p_their_blob = NULL;
>> + unsigned int orig_mode = 0, our_mode = 0, their_mode = 0, ret = 0;
>> + struct lock_file lock = LOCK_INIT;
>> +
>> + if (argc != 8)
>> + usage(builtin_merge_one_file_usage);
>> +
>> + if (read_cache() < 0)
>> + die("invalid index");
>> +
>> + hold_locked_index(&lock, LOCK_DIE_ON_ERROR);
>> +
>> + if (!get_oid(argv[1], &orig_blob)) {
>> + p_orig_blob = &orig_blob;
>> + ret = read_mode("orig", argv[5], &orig_mode);
>> + }
>
> argv[1] is defined as "either the object name of the blob in the
> common ancestor, or an empty string". So you need to distinguish
> three cases here, but you are only catching two.
>
> - argv[1] is an empty string; p_orig_blob can legitimately be left
> NULL.
>
> - argv[1] is a valid blob object name. orig_blob should be
> populated and p_orig_blob should point at it.
>
> - argv[1] is garbage, names a non-blob object, or there is no such
> object with that name. Don't we want to catch it as a mistake?
>
> Also, when argv[1] is an empty string, argv[5] must also be an empty
> string, or we got a wrong input---don't we want to catch it as a
> mistake?
>
> The third case needs a bit of thought. For example, if $1 and $2
> are the same and points at a non-existent object, we know we won't
> care because we only care about $3. In a lazily-cloned repository,
> that may matter---we would not want to fail even if we not have blob
> $1 and $2, as long as they are reasonably spelled a full hexadecimal
> object name. But we would want to fail if blob object named by $3
> is missing.
>
> One way to achieve semantics closer to the above than the posted
> patch may be to tighten the parsing. Instead of using "anything
> goes" get_oid(), use get_oid_hex(), perhaps.
>
>> + if (!get_oid(argv[2], &our_blob)) {
>> + p_our_blob = &our_blob;
>> + ret = read_mode("our", argv[6], &our_mode);
>> + }
>> +
>> + if (!get_oid(argv[3], &their_blob)) {
>> + p_their_blob = &their_blob;
>> + ret = read_mode("their", argv[7], &their_mode);
>> + }
>> +
>> + if (ret)
>> + return ret;
>> +
>> + ret = merge_strategies_one_file(the_repository,
>> + p_orig_blob, p_our_blob, p_their_blob, argv[4],
>> + orig_mode, our_mode, their_mode);
>
> That's a funny function name. It's not like the function will be
> taught different strategy to handle the three-way merge, no? It
> probably makes sense to name it after what it does, which is "three
> way merge".
>
Okay. There's already a function called threeway_merge() in
unpack_trees() that does something different.
merge_strategies_threeway() should be good?
>> + if (ret) {
>> + rollback_lock_file(&lock);
>> + return !!ret;
>> + }
>> +
>> + return write_locked_index(&the_index, &lock, COMMIT_LOCK);
>> +}
>
>> diff --git a/merge-strategies.c b/merge-strategies.c
>> new file mode 100644
>> index 0000000000..bbe6f48698
>> --- /dev/null
>> +++ b/merge-strategies.c
>> @@ -0,0 +1,214 @@
>> +#include "cache.h"
>> +#include "dir.h"
>> +#include "ll-merge.h"
>> +#include "merge-strategies.h"
>> +#include "xdiff-interface.h"
>> +
>
>> +static int add_to_index_cacheinfo(struct index_state *istate,
>> + unsigned int mode,
>> + const struct object_id *oid, const char *path)
>> +{
>> + struct cache_entry *ce;
>> + int len, option;
>> +
>> + if (!verify_path(path, mode))
>> + return error(_("Invalid path '%s'"), path);
>> +
>> + len = strlen(path);
>> + ce = make_empty_cache_entry(istate, len);
>> +
>> + oidcpy(&ce->oid, oid);
>> + memcpy(ce->name, path, len);
>> + ce->ce_flags = create_ce_flags(0);
>> + ce->ce_namelen = len;
>> + ce->ce_mode = create_ce_mode(mode);
>> + if (assume_unchanged)
>> + ce->ce_flags |= CE_VALID;
>> + option = ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE;
>> + if (add_index_entry(istate, ce, option))
>> + return error(_("%s: cannot add to the index"), path);
>> +
>> + return 0;
>> +}
>
> The above correctly does 'git update-index --add --cacheinfo "$6"
> "$2" "$4"' but don't copy-and-paste existing code to do so. Add one
> preliminary patch before everything else in the series to massage
> and extract add_cacheinfo() function out of builtin/update-index.c,
> move it to somewhere common like read-cache.c and so that we can
> call it from here.
>
Hmm, I’d really like to do this, but I have one remark/question about
it. In builtin/update-index.c, when add_cache_entry() fails, this
message is printed:
cannot add to the index - missing --add option?
Obviously, this is not what we want to show in git-merge when
add_index_entry() fails. But then, verify_path() can also fail, and
will show a sensible message for any situation:
Invalid path '%s'
Should I return error when verify_path() fails, but eg. -2 in the case
of add_index_entry(), and if this new add_cacheinfo() returns -2 but not
-1, print the correct message? Or let the caller verify the path so it
cannot fail because of this?
>> +static int checkout_from_index(struct index_state *istate, const char *path)
>> +{
>> + struct checkout state = CHECKOUT_INIT;
>> + struct cache_entry *ce;
>> +
>> + state.istate = istate;
>> + state.force = 1;
>> + state.base_dir = "";
>> + state.base_dir_len = 0;
>> +
>> + ce = index_file_exists(istate, path, strlen(path), 0);
>
> This call is unfortunate for the reasons I mention later.
>
> But if you must have this call, then you need to sanity check what
> you get from index_file_exists(). ce must be a merged cache entry,
> so
>
> if (!ce || ce_stage(ce))
> BUG(...);
>
That’s ok, I managed to remove it following your advice.
>> + if (checkout_entry(ce, &state, NULL, NULL) < 0)
>> + return error(_("%s: cannot checkout file"), path);
>> + return 0;
>> +}
>> +
>> +static int merge_one_file_deleted(struct index_state *istate,
>> + const struct object_id *orig_blob,
>> + const struct object_id *our_blob,
>> + const struct object_id *their_blob, const char *path,
>> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
>> +{
>> + if ((our_blob && orig_mode != our_mode) ||
>> + (their_blob && orig_mode != their_mode))
>> + return error(_("File %s deleted on one branch but had its "
>> + "permissions changed on the other."), path);
>> +
>> + if (our_blob) {
>> + printf(_("Removing %s\n"), path);
>> +
>> + if (file_exists(path))
>> + remove_path(path);
>> + }
>> +
>> + if (remove_file_from_index(istate, path))
>> + return error("%s: cannot remove from the index", path);
>> + return 0;
>
> If the side that did not remove changed the mode, we don't silently
> remove but fail and give a chance to inspect the situation to the
> end user. If we had the blob and it is removed by them, we give a
> message and only in that case we remove the file from the working
> tree, together with any leading directory that has become empty.
>
> And after that we make sure that the path is no longer in the
> index. The function removes entries for the path at all the stages,
> which is exactly what we want.
>
> OK.
>
>> +}
>> +
>> +static int do_merge_one_file(struct index_state *istate,
>> + const struct object_id *orig_blob,
>> + const struct object_id *our_blob,
>> + const struct object_id *their_blob, const char *path,
>> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
>> +{
>> + int ret, i, dest;
>> + ssize_t written;
>> + mmbuffer_t result = {NULL, 0};
>> + mmfile_t mmfs[3];
>> + struct ll_merge_options merge_opts = {0};
>> + struct cache_entry *ce;
>> +
>> + if (our_mode == S_IFLNK || their_mode == S_IFLNK)
>> + return error(_("%s: Not merging symbolic link changes."), path);
>> + else if (our_mode == S_IFGITLINK || their_mode == S_IFGITLINK)
>> + return error(_("%s: Not merging conflicting submodule changes."), path);
>> +
>> + read_mmblob(mmfs + 1, our_blob);
>> + read_mmblob(mmfs + 2, their_blob);
>> +
>> + if (orig_blob) {
>> + printf(_("Auto-merging %s\n"), path);
>> + read_mmblob(mmfs + 0, orig_blob);
>> + } else {
>> + printf(_("Added %s in both, but differently.\n"), path);
>> + read_mmblob(mmfs + 0, &null_oid);
>> + }
>> +
>> + merge_opts.xdl_opts = XDL_MERGE_ZEALOUS_ALNUM;
>> + ret = ll_merge(&result, path,
>> + mmfs + 0, "orig",
>> + mmfs + 1, "our",
>> + mmfs + 2, "their",
>> + istate, &merge_opts);
>
> Is it correct to call into ll_merge() here? The original used to
> call "git merge-file" which called into xdl_merge(). Calling into
> ll_merge() means the path is used to look up the attributes and use
> the custom merge driver, which I am not offhand sure is what we want
> to see at this low level (and if it turns out to be a good idea, we
> definitely should explain the change of semantics in the proposed
> log message for this commit).
>
>> + for (i = 0; i < 3; i++)
>> + free(mmfs[i].ptr);
>> +
>> + if (ret < 0) {
>> + free(result.ptr);
>> + return error(_("Failed to execute internal merge"));
>> + }
>> +
>> + /*
>> + * Create the working tree file, using "our tree" version from
>> + * the index, and then store the result of the merge.
>> + */
>
> The above is copied from the original, to explain what it did after
> the comment, but it does not seem to match what the new code does.
>
>> + ce = index_file_exists(istate, path, strlen(path), 0);
>> + if (!ce)
>> + BUG("file is not present in the cache?");
>> +
>> + unlink(path);
>> + if ((dest = open(path, O_WRONLY | O_CREAT, ce->ce_mode)) < 0) {
>> + free(result.ptr);
>> + return error_errno(_("failed to open file '%s'"), path);
>> + }
>> +
>> + written = write_in_full(dest, result.ptr, result.size);
>> + close(dest);
>> +
>> + free(result.ptr);
>> +
>> + if (written < 0)
>> + return error_errno(_("failed to write to '%s'"), path);
>> +
>
> This open(..., ce->ce_mode) call is way insufficient.
>
> The comment we have above this part of the code talks about the
> difficulty of doing this correctly in scripted version. Creating a
> file by 'git checkout-index -f --stage=2 -- "$4"' and reusing it to
> store the merged contents was the cleanest and easiest way without
> having direct access to adjust_shared_perm() to create a working
> tree file with the correct permission bits.
>
> We are writing in C, so we should be able to do much better than the
> scripted version, as we can later call adjust_shared_perm().
>
I'm not sure I understand the issue correctly.
Is this because I fetch an entry from the index to have the mode of the
file, instead of using `our_mode'? So I should move the error handling
of ll_merge()/xdl_merge() and the detection of the permission conflict
before writing in the file, and call open(…, our_mode)?
I'm also not sure why we need adjust_shared_perm() here.
>> + if (ret != 0 || !orig_blob)
>> + ret = error(_("content conflict in %s"), path);
>> + if (our_mode != their_mode)
>> + return error(_("permission conflict: %o->%o,%o in %s"),
>> + orig_mode, our_mode, their_mode, path);
>> + if (ret)
>> + return -1;
>> +
>> + return add_file_to_index(istate, path, 0);
>> +}
>> +
>> +int merge_strategies_one_file(struct repository *r,
>> + const struct object_id *orig_blob,
>> + const struct object_id *our_blob,
>> + const struct object_id *their_blob, const char *path,
>> + unsigned int orig_mode, unsigned int our_mode,
>> + unsigned int their_mode)
>> +{
>
> In a long if/else if/else if/.../else cascade, enclose all bodies in
> braces, if any one of them has a multi-statement body, to avoid
> being distracting.
>
>> + if (orig_blob &&
>> + ((!their_blob && our_blob && oideq(orig_blob, our_blob)) ||
>> + (!our_blob && their_blob && oideq(orig_blob, their_blob))))
>> + /* Deleted in both or deleted in one and unchanged in the other. */
>> + return merge_one_file_deleted(r->index,
>> + orig_blob, our_blob, their_blob, path,
>> + orig_mode, our_mode, their_mode);
>
> OK, we've already reviewed that function.
>
>> + else if (!orig_blob && our_blob && !their_blob) {
>> + /*
>> + * Added in one. The other side did not add and we
>> + * added so there is nothing to be done, except making
>> + * the path merged.
>> + */
>> + return add_to_index_cacheinfo(r->index, our_mode, our_blob, path);
>
> OK, we've already reviewed that function.
>
>> + } else if (!orig_blob && !our_blob && their_blob) {
>> + printf(_("Adding %s\n"), path);
>> +
>> + if (file_exists(path))
>> + return error(_("untracked %s is overwritten by the merge."), path);
>> +
>> + if (add_to_index_cacheinfo(r->index, their_mode, their_blob, path))
>> + return -1;
>> + return checkout_from_index(r->index, path);
>
> You did "add_to_index_cacheinfo()", so you MUST know which ce is to
> be checked out.
>
> Consider if it is worth to teach add_to_index_cacheinfo() to give
> you ce back and pass it to checkout_from_index(); that way, you do
> not have to call index_file_exists() based on path in the function.
>
OK, this is doable.
>> + } else if (!orig_blob && our_blob && their_blob &&
>> + oideq(our_blob, their_blob)) {
>> + /* Added in both, identically (check for same permissions). */
>> + if (our_mode != their_mode)
>> + return error(_("File %s added identically in both branches, "
>> + "but permissions conflict %o->%o."),
>> + path, our_mode, their_mode);
>> +
>> + printf(_("Adding %s\n"), path);
>> +
>> + if (add_to_index_cacheinfo(r->index, our_mode, our_blob, path))
>> + return -1;
>> + return checkout_from_index(r->index, path);
>
> Ditto.
>
>> + } else if (our_blob && their_blob)
>> + /* Modified in both, but differently. */
>> + return do_merge_one_file(r->index,
>> + orig_blob, our_blob, their_blob, path,
>> + orig_mode, our_mode, their_mode);
>> + else {
>> + char orig_hex[GIT_MAX_HEXSZ] = {0}, our_hex[GIT_MAX_HEXSZ] = {0},
>> + their_hex[GIT_MAX_HEXSZ] = {0};
>> +
>> + if (orig_blob)
>> + oid_to_hex_r(orig_hex, orig_blob);
>> + if (our_blob)
>> + oid_to_hex_r(our_hex, our_blob);
>> + if (their_blob)
>> + oid_to_hex_r(their_hex, their_blob);
>> +
>> + return error(_("%s: Not handling case %s -> %s -> %s"),
>> + path, orig_hex, our_hex, their_hex);
>> + }
>> +
>> + return 0;
>> +}
>
> I can see that this does go in the right direction. With a bit more
> attention to details it would soon be production-ready quality.
>
> Thanks.
>
Thank you,
Alban
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [PATCH v3 02/11] merge-one-file: rewrite in C
2020-10-21 19:47 ` Alban Gruin
@ 2020-10-21 20:28 ` Junio C Hamano
2020-10-21 21:20 ` Junio C Hamano
2020-10-21 20:30 ` Junio C Hamano
1 sibling, 1 reply; 221+ messages in thread
From: Junio C Hamano @ 2020-10-21 20:28 UTC (permalink / raw)
To: Alban Gruin; +Cc: git, phillip.wood
Alban Gruin <alban.gruin@gmail.com> writes:
>>> + /*
>>> + * Create the working tree file, using "our tree" version from
>>> + * the index, and then store the result of the merge.
>>> + */
>>
>> The above is copied from the original, to explain what it did after
>> the comment, but it does not seem to match what the new code does.
>>
>>> + ce = index_file_exists(istate, path, strlen(path), 0);
>>> + if (!ce)
>>> + BUG("file is not present in the cache?");
>>> +
>>> + unlink(path);
>>> + if ((dest = open(path, O_WRONLY | O_CREAT, ce->ce_mode)) < 0) {
>>> + free(result.ptr);
>>> + return error_errno(_("failed to open file '%s'"), path);
>>> + }
>>> +
>>> + written = write_in_full(dest, result.ptr, result.size);
>>> + close(dest);
>>> +
>>> + free(result.ptr);
>>> +
>>> + if (written < 0)
>>> + return error_errno(_("failed to write to '%s'"), path);
>>> +
>>
>> This open(..., ce->ce_mode) call is way insufficient.
>>
>> The comment we have above this part of the code talks about the
>> difficulty of doing this correctly in scripted version. Creating a
>> file by 'git checkout-index -f --stage=2 -- "$4"' and reusing it to
>> store the merged contents was the cleanest and easiest way without
>> having direct access to adjust_shared_perm() to create a working
>> tree file with the correct permission bits.
The original that the comment applies to does this
git checkout-index -f --stage=2 -- "$4" && cat "$src1" >"$4" || exit 1
to create path "$4" with the correct mode bits, instead of a naïve
mv "$src1" "$4"
because the filemode 'git checkout-index -f --stage=2 -- "$4"' gives
to file "$4" is by definition the most correct one for the path.
The command knows how user's umask and type bits in the index should
interact and produce the final mode bits, but "$src1" was created
without any regard to the mode bits---the 'git merge-file' command
only cares about the contents and not filemode. We can even lose
the executable bit that way. And preparing "$4" and then catting
the computed contents into it was a roundabout way (it wastes the
entire writing-out of the contents from the index), and that was
what the comment was about.
But all that is unnecessary once you port this to C. So the comment
does not apply to the code you wrote, I think, and should just be
dropped.
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [PATCH v3 02/11] merge-one-file: rewrite in C
2020-10-21 19:47 ` Alban Gruin
2020-10-21 20:28 ` Junio C Hamano
@ 2020-10-21 20:30 ` Junio C Hamano
1 sibling, 0 replies; 221+ messages in thread
From: Junio C Hamano @ 2020-10-21 20:30 UTC (permalink / raw)
To: Alban Gruin; +Cc: git, phillip.wood
Alban Gruin <alban.gruin@gmail.com> writes:
>>> + int ret, i, dest;
>>> + ssize_t written;
>>> + mmbuffer_t result = {NULL, 0};
>>> + mmfile_t mmfs[3];
>>> + struct ll_merge_options merge_opts = {0};
>>> + struct cache_entry *ce;
>>> +
>>> + if (our_mode == S_IFLNK || their_mode == S_IFLNK)
>>> + return error(_("%s: Not merging symbolic link changes."), path);
>>> + else if (our_mode == S_IFGITLINK || their_mode == S_IFGITLINK)
>>> + return error(_("%s: Not merging conflicting submodule changes."), path);
>>> +
>>> + read_mmblob(mmfs + 1, our_blob);
>>> + read_mmblob(mmfs + 2, their_blob);
>>> +
>>> + if (orig_blob) {
>>> + printf(_("Auto-merging %s\n"), path);
>>> + read_mmblob(mmfs + 0, orig_blob);
>>> + } else {
>>> + printf(_("Added %s in both, but differently.\n"), path);
>>> + read_mmblob(mmfs + 0, &null_oid);
>>> + }
>>> +
>>> + merge_opts.xdl_opts = XDL_MERGE_ZEALOUS_ALNUM;
>>> + ret = ll_merge(&result, path,
>>> + mmfs + 0, "orig",
>>> + mmfs + 1, "our",
>>> + mmfs + 2, "their",
>>> + istate, &merge_opts);
>>
>> Is it correct to call into ll_merge() here? The original used to
>> call "git merge-file" which called into xdl_merge(). Calling into
>> ll_merge() means the path is used to look up the attributes and use
>> the custom merge driver, which I am not offhand sure is what we want
>> to see at this low level (and if it turns out to be a good idea, we
>> definitely should explain the change of semantics in the proposed
>> log message for this commit).
I am still not sure if it is correct to call ll_merge() and not the
xdl_merge() from here. We need to highlight this change in the log
message, if we were still going to do this.
Thanks.
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [PATCH v3 02/11] merge-one-file: rewrite in C
2020-10-21 20:28 ` Junio C Hamano
@ 2020-10-21 21:20 ` Junio C Hamano
0 siblings, 0 replies; 221+ messages in thread
From: Junio C Hamano @ 2020-10-21 21:20 UTC (permalink / raw)
To: Alban Gruin; +Cc: git, phillip.wood
Junio C Hamano <gitster@pobox.com> writes:
>>> This open(..., ce->ce_mode) call is way insufficient.
>>> ...
> But all that is unnecessary once you port this to C. So the comment
> does not apply to the code you wrote, I think, and should just be
> dropped.
Sorry, forgot to mention one thing. Using ce->ce_mode to create the
output file is the way how helpers in entry.c check out paths from
the index to the working tree, so the code is OK. It's just the
copied comment was about the issue that your code did not even have
to worry about.
Thanks.
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [PATCH v3 03/11] merge-index: libify merge_one_path() and merge_all()
2020-10-09 4:48 ` Junio C Hamano
@ 2020-11-06 19:53 ` Alban Gruin
0 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-06 19:53 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git, phillip.wood
Hi Junio,
Le 09/10/2020 à 06:48, Junio C Hamano a écrit :
> Alban Gruin <alban.gruin@gmail.com> writes:
>
>> diff --git a/merge-strategies.c b/merge-strategies.c
>> index bbe6f48698..f0e30f5624 100644
>> --- a/merge-strategies.c
>> +++ b/merge-strategies.c
>> @@ -2,6 +2,7 @@
>> #include "dir.h"
>> #include "ll-merge.h"
>> #include "merge-strategies.h"
>> +#include "run-command.h"
>> #include "xdiff-interface.h"
>>
>> static int add_to_index_cacheinfo(struct index_state *istate,
>> @@ -212,3 +213,101 @@ int merge_strategies_one_file(struct repository *r,
>>
>> return 0;
>> }
>> +
>> +int merge_program_cb(const struct object_id *orig_blob,
>> + const struct object_id *our_blob,
>> + const struct object_id *their_blob, const char *path,
>> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
>> + void *data)
>> +{
>> + char ownbuf[3][GIT_MAX_HEXSZ] = {{0}};
>> + const char *arguments[] = { (char *)data, "", "", "", path,
>> + ownbuf[0], ownbuf[1], ownbuf[2],
>> + NULL };
>> +
>> + if (orig_blob)
>> + arguments[1] = oid_to_hex(orig_blob);
>> + if (our_blob)
>> + arguments[2] = oid_to_hex(our_blob);
>> + if (their_blob)
>> + arguments[3] = oid_to_hex(their_blob);
>
> oid_to_hex() uses 4-slot rotating buffer, no? Relying on the fact
> that three would be available here without getting reused (or,
> rather, our caller didn't make its own calls and/or does not mind
> us invalidating all but one slot for them) feels a bit iffy.
>
> Extending ownbuf[] to 6 elements and using oid_to_hex_r() would be a
> trivial way to clarify the code.
>
>> + xsnprintf(ownbuf[0], sizeof(ownbuf[0]), "%o", orig_mode);
>> + xsnprintf(ownbuf[1], sizeof(ownbuf[1]), "%o", our_mode);
>> + xsnprintf(ownbuf[2], sizeof(ownbuf[2]), "%o", their_mode);
>
> And these mode bits would not need GIT_MAX_HEXSZ to begin with.
> This smells like a WIP that hasn't been carefullly proofread.
>
> char oidbuf[3][GIT_MAX_HEXSZ] = { 0 };
> char modebuf[3][8] = { 0 };
So here I picked GIT_MAX_HEXSZ + 1 and 10 for those buffers, they are
already used by builtin/diff.c.
> char *args[] = {
> data, oidbuf[0], oidbuf[1], oidbuf[2], path,
> modebuf[0], modebuf[1], modebuf[2], NULL,
> };
>
> if (orig_blob)
> oid_to_hex_r(oidbuf[0], orig_blob);
> ...
> xsnprintf(modebuf[0], ...);
>
>
> Eh, wait. Is this meant to be able to drive "git-merge-one-file",
> i.e. a missing common/ours/theirs is indicated by an empty string
> in both oiod and mode? If so, an unconditional xsnprintf() would
> either give garbage or "0" at best, neither of which is an empty
> string. So the body would be more like
>
> if (orig_blob) {
> oid_to_hex_r(oidbuf[0], orig_blob);
> xsnprintf(modebuf[0], "%o", orig_mode);
> }
> if (our_blob) {
> oid_to_hex_r(oidbuf[1], our_blob);
> xsnprintf(modebuf[1], "%o", our_mode);
> }
> ...
>
> wouldn't it?
>
Yes, especially since you suggested to error out if an empty oid has a
non-empty mode in the second patch.
>> + return run_command_v_opt(arguments, 0);
>> +}
>> +
>> +static int merge_entry(struct index_state *istate, int quiet, int pos,
>> + const char *path, merge_cb cb, void *data)
>
> When we use an identifier "cb", it typically means callback data,
> not a callback function which is often called "fn". So, name the
> type "merge_fn" (or "merge_func"), and call the parameter "fn".
>
>> +{
>> + int found = 0;
>> + const struct object_id *oids[3] = {NULL};
>> + unsigned int modes[3] = {0};
>> +
>> + do {
>> + const struct cache_entry *ce = istate->cache[pos];
>> + int stage = ce_stage(ce);
>> +
>> + if (strcmp(ce->name, path))
>> + break;
>> + found++;
>> + oids[stage - 1] = &ce->oid;
>> + modes[stage - 1] = ce->ce_mode;
>> + } while (++pos < istate->cache_nr);
>> + if (!found)
>> + return error(_("%s is not in the cache"), path);
>> +
>> + if (cb(oids[0], oids[1], oids[2], path, modes[0], modes[1], modes[2], data)) {
>> + if (!quiet)
>> + error(_("Merge program failed"));
>> + return -2;
>> + }
>> +
>> + return found;
>> +}
>
> This copies from builtin/merge-index.c::merge_entry().
>
>> +int merge_one_path(struct index_state *istate, int oneshot, int quiet,
>> + const char *path, merge_cb cb, void *data)
>> +{
>> + int pos = index_name_pos(istate, path, strlen(path)), ret;
>> +
>> + /*
>> + * If it already exists in the cache as stage0, it's
>> + * already merged and there is nothing to do.
>> + */
>> + if (pos < 0) {
>> + ret = merge_entry(istate, quiet, -pos - 1, path, cb, data);
>> + if (ret == -1)
>> + return -1;
>> + else if (ret == -2)
>> + return 1;
>> + }
>> + return 0;
>> +}
>
> Likewise from the same function in that file.
>
> Are we removing the "git merge-index" program? Reusing the same
> identifier for these copied-and-pasted pairs of functions bothers
> me for two reasons.
>
> - An indentifier that was clear and unique enough in the original
> context as a file-scope static may not be a good name as a global
> identifier.
>
> - Having two similar-looking functions with the same name makes
> reading and learning the codebase starting at "git grep" hits
> more difficult than necessary.
>
I don't plan to remove `git merge-index' -- nor any other program, for
that matter. Why not renaming merge_one_path() and merge_all(),
merge_index_path() and merge_all_index()?
>> +int merge_all(struct index_state *istate, int oneshot, int quiet,
>> + merge_cb cb, void *data)
>> +{
>> + int err = 0, i, ret;
>> + for (i = 0; i < istate->cache_nr; i++) {
>> + const struct cache_entry *ce = istate->cache[i];
>> + if (!ce_stage(ce))
>> + continue;
>> +
>> + ret = merge_entry(istate, quiet, i, ce->name, cb, data);
>> + if (ret > 0)
>> + i += ret - 1;
>> + else if (ret == -1)
>> + return -1;
>> + else if (ret == -2) {
>> + if (oneshot)
>> + err++;
>> + else
>> + return 1;
>> + }
>> + }
>> +
>> + return err;
>> +}
>
> Likewise.
>
>> diff --git a/merge-strategies.h b/merge-strategies.h
>> index b527d145c7..cf78d7eaf4 100644
>> --- a/merge-strategies.h
>> +++ b/merge-strategies.h
>> @@ -10,4 +10,21 @@ int merge_strategies_one_file(struct repository *r,
>> unsigned int orig_mode, unsigned int our_mode,
>> unsigned int their_mode);
>>
>> +typedef int (*merge_cb)(const struct object_id *orig_blob,
>> + const struct object_id *our_blob,
>> + const struct object_id *their_blob, const char *path,
>> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
>> + void *data);
>
> Call it "merge_one_file_func", probably.
>
>> +int merge_program_cb(const struct object_id *orig_blob,
>
> Call it spawn_merge_one_file() perhaps?
>
>> + const struct object_id *our_blob,
>> + const struct object_id *their_blob, const char *path,
>> + unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
>> + void *data);
>> +
>> +int merge_one_path(struct index_state *istate, int oneshot, int quiet,
>> + const char *path, merge_cb cb, void *data);
>> +int merge_all(struct index_state *istate, int oneshot, int quiet,
>> + merge_cb cb, void *data);
>> #endif /* MERGE_STRATEGIES_H */
Ack for the rest, the two function names are the only thing I am still
missing on this patch right now.
Alban
^ permalink raw reply [flat|nested] 221+ messages in thread
* Re: [PATCH v3 05/11] merge-resolve: rewrite in C
2020-10-16 19:19 ` Junio C Hamano
@ 2020-11-06 19:53 ` Alban Gruin
0 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-06 19:53 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git, phillip.wood
Le 16/10/2020 à 21:19, Junio C Hamano a écrit :
> Alban Gruin <alban.gruin@gmail.com> writes:
>
>> +#include "cache.h"
>> +#include "builtin.h"
>> +#include "merge-strategies.h"
>> +
>> +static const char builtin_merge_resolve_usage[] =
>> + "git merge-resolve <bases>... -- <head> <remote>";
>> +
>> +int cmd_merge_resolve(int argc, const char **argv, const char *prefix)
>> +{
>> + int i, is_baseless = 1, sep_seen = 0;
>> + const char *head = NULL;
>> + struct commit_list *bases = NULL, *remote = NULL;
>> + struct commit_list **next_base = &bases;
>> +
>> + if (argc < 5)
>> + usage(builtin_merge_resolve_usage);
>> +
>> + setup_work_tree();
>> + if (repo_read_index(the_repository) < 0)
>> + die("invalid index");
>> +
>> + /* The first parameters up to -- are merge bases; the rest are
>> + * heads. */
>
> Style (I won't repeat).
>
>> + for (i = 1; i < argc; i++) {
>> + if (strcmp(argv[i], "--") == 0)
>
> if (!strcmp(...))
>
> is more typical than comparing with "== 0".
>
>> + sep_seen = 1;
>> + else if (strcmp(argv[i], "-h") == 0)
>> + usage(builtin_merge_resolve_usage);
>> + else if (sep_seen && !head)
>> + head = argv[i];
>> + else if (remote) {
>> + /* Give up if we are given two or more remotes.
>> + * Not handling octopus. */
>> + return 2;
>> + } else {
>> + struct object_id oid;
>> +
>> + get_oid(argv[i], &oid);
>> + is_baseless &= sep_seen;
>> +
>> + if (!oideq(&oid, the_hash_algo->empty_tree)) {
>
> What is this business about an empty tree about?
>
I don’t remember my intent here -- perhaps I wanted to avoid merges on
empty trees… I’ll remove that from here and merge-octopus.c.
>> + struct commit *commit;
>> + commit = lookup_commit_or_die(&oid, argv[i]);
>> +
>> + if (sep_seen)
>> + commit_list_append(commit, &remote);
>> + else
>> + next_base = commit_list_append(commit, next_base);
>> + }
>> + }
>> + }
>> +
>> + /* Give up if this is a baseless merge. */
>> + if (is_baseless)
>> + return 2;
>
> This is quite convoluted.
>
> The original is much more straight-forward. We just said "grab
> everything before we see '--' and call them bases; immediately after
> '--' is HEAD and everything else is remote. Now do we have any
> base? Otherwise we cannot handle it".
>
> I cannot see an equivalence to it in the rewritten result, with the
> bit operation with is_baseless and sep_seen. Wouldn't it be the
> matter of checking if next_base is NULL, or is there something more
> subtle that deserves in-code comment going on?
>
After re-reading this many, many weeks later, I can confirm that this is
convoluted, and that there is a much better way to perform some checks…
for instance, checking if `bases' is NULL instead of having
`is_baseless', or checking after the loop if `remotes->next' is not NULL
to verify if there is multiple remotes.
> Thanks.
>
Alban
^ permalink raw reply [flat|nested] 221+ messages in thread
* [PATCH v4 00/12] Rewrite the remaining merge strategies from shell to C
2020-10-05 12:26 ` [PATCH v3 00/11] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (11 preceding siblings ...)
2020-10-07 6:57 ` [PATCH v3 00/11] Rewrite the remaining merge strategies from shell to C Johannes Schindelin
@ 2020-11-13 11:04 ` Alban Gruin
2020-11-13 11:04 ` [PATCH v4 01/12] t6027: modernise tests Alban Gruin
` (12 more replies)
12 siblings, 13 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-13 11:04 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
In a effort to reduce the number of shell scripts in git's codebase, I
propose this patch series converting the two remaining merge strategies,
resolve and octopus, from shell to C. This will enable slightly better
performance, better integration with git itself (no more forking to
perform these operations), better portability (Windows and shell scripts
don't mix well).
Three scripts are actually converted: first git-merge-one-file.sh, then
git-merge-resolve.sh, and finally git-merge-octopus.sh. Not only they
are converted, but they also are modified to operate without forking,
and then libified so they can be used by git without spawning another
process.
The first patch is not important to make the whole series work, but I
made this patch while working on it.
This series keeps the commands `git merge-one-file', `git
merge-resolve', and `git merge-octopus', so any script depending on them
should keep working without any changes.
This series is based on 306ee63a70 (Eighteenth batch, 2020-09-29). The
tip is tagged as "rewrite-merge-strategies-v4" at
https://github.com/agrn/git.
Changes since v3:
- [2/12] Move add_cacheinfo() to read-cache.c and rename it
add_to_index_cacheinfo(). That way, there is no need to copy it to
merge-strategies.c. It also returns the new cache entry.
- [3/12] Changed SHA1 to "object name" in the comments
- [3/12] Error out if an object was not specified but a corresponding
mode was.
- [3/12] Add a cache entry parameter to checkout_from_index() to avoid
calling index_file_exists(), as all of its callers now have the new
cache entry thanks to add_to_index_cacheinfo().
- [3/12] Replace ll_merge() with xdl_merge() in do_merge_one_file().
- [3/12] Fail earlier in the case of a permission conflict in
do_merge_one_file().
- [3/12] Use `our_mode' instead of fetching a cache entry to define the
mode of a merged file in do_merge_one_file().
- [3/12] Rename merge_strategies_one_file() to merge_three_way().
- [3/12] Reformatted a long chain of if/else if/else blocks.
- [4/12] Rename merge_all() to merge_all_index(), merge_one_path() by
merge_index_path(), merge_program_cb() to merge_one_file_spawn(),
`merge_cb' to `merge_fn', and the parameters `cb' to `fn'.
- [4/12] Use oid_to_hex_r() instead of oid_to_hex() in
merge_one_file_spawn().
- [5/12] Rename merge_one_file_cb() to merge_one_file_func().
- [6/12, 8/12] Enable `USE_THE_INDEX_COMPATIBILITY_MACROS' and use
read_cache() instead of repo_read_index().
- [6/12] The parameter parsing has been rewritten to look less
convoluted.
- [6/12] Reformatted multi-line comments.
- [7/12] Fixed multiple mistakes in the commit message.
- [8/12] The parameters parsing has been rewritten to look more like
builtin/merge-resolve.c.
- [3/12, 6/12, 8/12] Removed obsolete informations from commit
messages.
Alban Gruin (12):
t6027: modernise tests
update-index: move add_cacheinfo() to read-cache.c
merge-one-file: rewrite in C
merge-index: libify merge_one_path() and merge_all()
merge-index: don't fork if the requested program is
`git-merge-one-file'
merge-resolve: rewrite in C
merge-recursive: move better_branch_name() to merge.c
merge-octopus: rewrite in C
merge: use the "resolve" strategy without forking
merge: use the "octopus" strategy without forking
sequencer: use the "resolve" strategy without forking
sequencer: use the "octopus" merge strategy without forking
Makefile | 7 +-
builtin.h | 3 +
builtin/merge-index.c | 102 ++----
builtin/merge-octopus.c | 69 ++++
builtin/merge-one-file.c | 94 ++++++
builtin/merge-recursive.c | 16 +-
builtin/merge-resolve.c | 73 ++++
builtin/merge.c | 9 +-
builtin/update-index.c | 25 +-
cache.h | 7 +-
git-merge-octopus.sh | 112 -------
git-merge-one-file.sh | 167 ---------
git-merge-resolve.sh | 54 ---
git.c | 3 +
merge-strategies.c | 576 ++++++++++++++++++++++++++++++++
merge-strategies.h | 43 +++
merge.c | 12 +
read-cache.c | 35 ++
sequencer.c | 16 +-
t/t6407-merge-binary.sh | 27 +-
t/t6415-merge-dir-to-symlink.sh | 2 +-
21 files changed, 987 insertions(+), 465 deletions(-)
create mode 100644 builtin/merge-octopus.c
create mode 100644 builtin/merge-one-file.c
create mode 100644 builtin/merge-resolve.c
delete mode 100755 git-merge-octopus.sh
delete mode 100755 git-merge-one-file.sh
delete mode 100755 git-merge-resolve.sh
create mode 100644 merge-strategies.c
create mode 100644 merge-strategies.h
Range-diff against v3:
1: 08c7df596a = 1: 08c7df596a t6027: modernise tests
-: ---------- > 2: df237da758 update-index: move add_cacheinfo() to read-cache.c
2: ce911c99c0 ! 3: b64bad0d23 merge-one-file: rewrite in C
@@ -10,22 +10,23 @@
external processes are replaced by calls to functions in libgit.a:
- calls to `update-index --add --cacheinfo' are replaced by calls to
- add_cache_entry();
+ add_to_index_cacheinfo();
- calls to `update-index --remove' are replaced by calls to
- remove_file_from_cache();
+ remove_file_from_index();
- calls to `checkout-index -u -f' are replaced by calls to
checkout_entry();
- calls to `unpack-file' and `merge-files' are replaced by calls to
- read_mmblob() and ll_merge(), respectively, to merge files
+ read_mmblob() and xdl_merge(), respectively, to merge files
in-memory;
- - calls to `checkout-index -f --stage=2' are replaced by calls to
- cache_file_exists();
+ - calls to `checkout-index -f --stage=2' are removed, as this is needed
+ to have the correct permission bits on the merged file from the
+ script, but not in the C version;
- - calls to `update-index' are replaced by calls to add_file_to_cache().
+ - calls to `update-index' are replaced by calls to add_file_to_index().
The bulk of the rewrite is done in a new file in libgit.a,
merge-strategies.c. This will enable the resolve and octopus strategies
@@ -96,9 +97,9 @@
+ *
+ * This is the git per-file merge utility, called with
+ *
-+ * argv[1] - original file SHA1 (or empty)
-+ * argv[2] - file in branch1 SHA1 (or empty)
-+ * argv[3] - file in branch2 SHA1 (or empty)
++ * argv[1] - original file object name (or empty)
++ * argv[2] - file in branch1 object name (or empty)
++ * argv[3] - file in branch2 object name (or empty)
+ * argv[4] - pathname in repository
+ * argv[5] - original file mode (or empty)
+ * argv[6] - file in branch1 mode (or empty)
@@ -150,27 +151,29 @@
+
+ hold_locked_index(&lock, LOCK_DIE_ON_ERROR);
+
-+ if (!get_oid(argv[1], &orig_blob)) {
++ if (!get_oid_hex(argv[1], &orig_blob)) {
+ p_orig_blob = &orig_blob;
+ ret = read_mode("orig", argv[5], &orig_mode);
-+ }
++ } else if (!*argv[1] && *argv[5])
++ ret = error(_("no 'orig' object id given, but a mode was still given."));
+
-+ if (!get_oid(argv[2], &our_blob)) {
++ if (!get_oid_hex(argv[2], &our_blob)) {
+ p_our_blob = &our_blob;
+ ret = read_mode("our", argv[6], &our_mode);
-+ }
++ } else if (!*argv[2] && *argv[6])
++ ret = error(_("no 'our' object id given, but a mode was still given."));
+
-+ if (!get_oid(argv[3], &their_blob)) {
++ if (!get_oid_hex(argv[3], &their_blob)) {
+ p_their_blob = &their_blob;
+ ret = read_mode("their", argv[7], &their_mode);
-+ }
++ } else if (!*argv[3] && *argv[7])
++ ret = error(_("no 'their' object id given, but a mode was still given."));
+
+ if (ret)
+ return ret;
+
-+ ret = merge_strategies_one_file(the_repository,
-+ p_orig_blob, p_our_blob, p_their_blob, argv[4],
-+ orig_mode, our_mode, their_mode);
++ ret = merge_three_way(the_repository, p_orig_blob, p_our_blob, p_their_blob,
++ argv[4], orig_mode, our_mode, their_mode);
+
+ if (ret) {
+ rollback_lock_file(&lock);
@@ -372,55 +375,25 @@
@@
+#include "cache.h"
+#include "dir.h"
-+#include "ll-merge.h"
+#include "merge-strategies.h"
+#include "xdiff-interface.h"
+
-+static int add_to_index_cacheinfo(struct index_state *istate,
-+ unsigned int mode,
-+ const struct object_id *oid, const char *path)
-+{
-+ struct cache_entry *ce;
-+ int len, option;
-+
-+ if (!verify_path(path, mode))
-+ return error(_("Invalid path '%s'"), path);
-+
-+ len = strlen(path);
-+ ce = make_empty_cache_entry(istate, len);
-+
-+ oidcpy(&ce->oid, oid);
-+ memcpy(ce->name, path, len);
-+ ce->ce_flags = create_ce_flags(0);
-+ ce->ce_namelen = len;
-+ ce->ce_mode = create_ce_mode(mode);
-+ if (assume_unchanged)
-+ ce->ce_flags |= CE_VALID;
-+ option = ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE;
-+ if (add_index_entry(istate, ce, option))
-+ return error(_("%s: cannot add to the index"), path);
-+
-+ return 0;
-+}
-+
-+static int checkout_from_index(struct index_state *istate, const char *path)
++static int checkout_from_index(struct index_state *istate, const char *path,
++ struct cache_entry *ce)
+{
+ struct checkout state = CHECKOUT_INIT;
-+ struct cache_entry *ce;
+
+ state.istate = istate;
+ state.force = 1;
+ state.base_dir = "";
+ state.base_dir_len = 0;
+
-+ ce = index_file_exists(istate, path, strlen(path), 0);
+ if (checkout_entry(ce, &state, NULL, NULL) < 0)
+ return error(_("%s: cannot checkout file"), path);
+ return 0;
+}
+
+static int merge_one_file_deleted(struct index_state *istate,
-+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
@@ -452,16 +425,15 @@
+ ssize_t written;
+ mmbuffer_t result = {NULL, 0};
+ mmfile_t mmfs[3];
-+ struct ll_merge_options merge_opts = {0};
-+ struct cache_entry *ce;
++ xmparam_t xmp = {{0}};
+
+ if (our_mode == S_IFLNK || their_mode == S_IFLNK)
+ return error(_("%s: Not merging symbolic link changes."), path);
+ else if (our_mode == S_IFGITLINK || their_mode == S_IFGITLINK)
+ return error(_("%s: Not merging conflicting submodule changes."), path);
-+
-+ read_mmblob(mmfs + 1, our_blob);
-+ read_mmblob(mmfs + 2, their_blob);
++ else if (our_mode != their_mode)
++ return error(_("permission conflict: %o->%o,%o in %s"),
++ orig_mode, our_mode, their_mode, path);
+
+ if (orig_blob) {
+ printf(_("Auto-merging %s\n"), path);
@@ -471,12 +443,14 @@
+ read_mmblob(mmfs + 0, &null_oid);
+ }
+
-+ merge_opts.xdl_opts = XDL_MERGE_ZEALOUS_ALNUM;
-+ ret = ll_merge(&result, path,
-+ mmfs + 0, "orig",
-+ mmfs + 1, "our",
-+ mmfs + 2, "their",
-+ istate, &merge_opts);
++ read_mmblob(mmfs + 1, our_blob);
++ read_mmblob(mmfs + 2, their_blob);
++
++ xmp.level = XDL_MERGE_ZEALOUS_ALNUM;
++ xmp.style = 0;
++ xmp.favor = 0;
++
++ ret = xdl_merge(mmfs + 0, mmfs + 1, mmfs + 2, &xmp, &result);
+
+ for (i = 0; i < 3; i++)
+ free(mmfs[i].ptr);
@@ -484,18 +458,13 @@
+ if (ret < 0) {
+ free(result.ptr);
+ return error(_("Failed to execute internal merge"));
++ } else if (ret > 0 || !orig_blob) {
++ free(result.ptr);
++ return error(_("content conflict in %s"), path);
+ }
+
-+ /*
-+ * Create the working tree file, using "our tree" version from
-+ * the index, and then store the result of the merge.
-+ */
-+ ce = index_file_exists(istate, path, strlen(path), 0);
-+ if (!ce)
-+ BUG("file is not present in the cache?");
-+
+ unlink(path);
-+ if ((dest = open(path, O_WRONLY | O_CREAT, ce->ce_mode)) < 0) {
++ if ((dest = open(path, O_WRONLY | O_CREAT, our_mode)) < 0) {
+ free(result.ptr);
+ return error_errno(_("failed to open file '%s'"), path);
+ }
@@ -508,49 +477,42 @@
+ if (written < 0)
+ return error_errno(_("failed to write to '%s'"), path);
+
-+ if (ret != 0 || !orig_blob)
-+ ret = error(_("content conflict in %s"), path);
-+ if (our_mode != their_mode)
-+ return error(_("permission conflict: %o->%o,%o in %s"),
-+ orig_mode, our_mode, their_mode, path);
-+ if (ret)
-+ return -1;
-+
+ return add_file_to_index(istate, path, 0);
+}
+
-+int merge_strategies_one_file(struct repository *r,
-+ const struct object_id *orig_blob,
-+ const struct object_id *our_blob,
-+ const struct object_id *their_blob, const char *path,
-+ unsigned int orig_mode, unsigned int our_mode,
-+ unsigned int their_mode)
++int merge_three_way(struct repository *r,
++ const struct object_id *orig_blob,
++ const struct object_id *our_blob,
++ const struct object_id *their_blob, const char *path,
++ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
+{
+ if (orig_blob &&
+ ((!their_blob && our_blob && oideq(orig_blob, our_blob)) ||
-+ (!our_blob && their_blob && oideq(orig_blob, their_blob))))
++ (!our_blob && their_blob && oideq(orig_blob, their_blob)))) {
+ /* Deleted in both or deleted in one and unchanged in the other. */
-+ return merge_one_file_deleted(r->index,
-+ orig_blob, our_blob, their_blob, path,
++ return merge_one_file_deleted(r->index, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
-+ else if (!orig_blob && our_blob && !their_blob) {
++ } else if (!orig_blob && our_blob && !their_blob) {
+ /*
+ * Added in one. The other side did not add and we
+ * added so there is nothing to be done, except making
+ * the path merged.
+ */
-+ return add_to_index_cacheinfo(r->index, our_mode, our_blob, path);
++ return add_to_index_cacheinfo(r->index, our_mode, our_blob, path, 0, 1, 1, NULL);
+ } else if (!orig_blob && !our_blob && their_blob) {
++ struct cache_entry *ce;
+ printf(_("Adding %s\n"), path);
+
+ if (file_exists(path))
+ return error(_("untracked %s is overwritten by the merge."), path);
+
-+ if (add_to_index_cacheinfo(r->index, their_mode, their_blob, path))
++ if (add_to_index_cacheinfo(r->index, their_mode, their_blob, path, 0, 1, 1, &ce))
+ return -1;
-+ return checkout_from_index(r->index, path);
++ return checkout_from_index(r->index, path, ce);
+ } else if (!orig_blob && our_blob && their_blob &&
+ oideq(our_blob, their_blob)) {
++ struct cache_entry *ce;
++
+ /* Added in both, identically (check for same permissions). */
+ if (our_mode != their_mode)
+ return error(_("File %s added identically in both branches, "
@@ -559,15 +521,15 @@
+
+ printf(_("Adding %s\n"), path);
+
-+ if (add_to_index_cacheinfo(r->index, our_mode, our_blob, path))
++ if (add_to_index_cacheinfo(r->index, our_mode, our_blob, path, 0, 1, 1, &ce))
+ return -1;
-+ return checkout_from_index(r->index, path);
-+ } else if (our_blob && their_blob)
++ return checkout_from_index(r->index, path, ce);
++ } else if (our_blob && their_blob) {
+ /* Modified in both, but differently. */
+ return do_merge_one_file(r->index,
+ orig_blob, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
-+ else {
++ } else {
+ char orig_hex[GIT_MAX_HEXSZ] = {0}, our_hex[GIT_MAX_HEXSZ] = {0},
+ their_hex[GIT_MAX_HEXSZ] = {0};
+
@@ -595,12 +557,11 @@
+
+#include "object.h"
+
-+int merge_strategies_one_file(struct repository *r,
-+ const struct object_id *orig_blob,
-+ const struct object_id *our_blob,
-+ const struct object_id *their_blob, const char *path,
-+ unsigned int orig_mode, unsigned int our_mode,
-+ unsigned int their_mode);
++int merge_three_way(struct repository *r,
++ const struct object_id *orig_blob,
++ const struct object_id *our_blob,
++ const struct object_id *their_blob, const char *path,
++ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode);
+
+#endif /* MERGE_STRATEGIES_H */
3: 7f0999f5a3 ! 4: c5577dc691 merge-index: libify merge_one_path() and merge_all()
@@ -9,11 +9,11 @@
libgit.a, which means that once rewritten, the strategies would still
have to invoke `merge-one-file' by spawning a new process first.
- To avoid this, this moves merge_one_path(), merge_all(), and their
- helpers to merge-strategies.c. They also take a callback to dictate
- what they should do for each file. For now, to preserve the behaviour
- of `merge-index', only one callback, launching a new process, is
- defined.
+ To avoid this, this moves and renames merge_one_path(), merge_all(), and
+ their helpers to merge-strategies.c. They also take a callback to
+ dictate what they should do for each file. For now, to preserve the
+ behaviour of `merge-index', only one callback, launching a new process,
+ is defined.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
@@ -103,15 +103,15 @@
}
if (!strcmp(arg, "-a")) {
- merge_all();
-+ err |= merge_all(&the_index, one_shot, quiet,
-+ merge_program_cb, (void *)pgm);
++ err |= merge_all_index(&the_index, one_shot, quiet,
++ merge_one_file_spawn, (void *)pgm);
continue;
}
die("git merge-index: unknown option %s", arg);
}
- merge_one_path(arg);
-+ err |= merge_one_path(&the_index, one_shot, quiet, arg,
-+ merge_program_cb, (void *)pgm);
++ err |= merge_index_path(&the_index, one_shot, quiet, arg,
++ merge_one_file_spawn, (void *)pgm);
}
- if (err && !quiet)
- die("merge program failed");
@@ -122,45 +122,49 @@
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@
+ #include "cache.h"
#include "dir.h"
- #include "ll-merge.h"
#include "merge-strategies.h"
+#include "run-command.h"
#include "xdiff-interface.h"
- static int add_to_index_cacheinfo(struct index_state *istate,
+ static int checkout_from_index(struct index_state *istate, const char *path,
@@
return 0;
}
+
-+int merge_program_cb(const struct object_id *orig_blob,
-+ const struct object_id *our_blob,
-+ const struct object_id *their_blob, const char *path,
-+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
-+ void *data)
++int merge_one_file_spawn(const struct object_id *orig_blob,
++ const struct object_id *our_blob,
++ const struct object_id *their_blob, const char *path,
++ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
++ void *data)
+{
-+ char ownbuf[3][GIT_MAX_HEXSZ] = {{0}};
-+ const char *arguments[] = { (char *)data, "", "", "", path,
-+ ownbuf[0], ownbuf[1], ownbuf[2],
-+ NULL };
++ char oids[3][GIT_MAX_HEXSZ + 1] = {{0}};
++ char modes[3][10] = {{0}};
++ const char *arguments[] = { (char *)data, oids[0], oids[1], oids[2],
++ path, modes[0], modes[1], modes[2], NULL };
+
-+ if (orig_blob)
-+ arguments[1] = oid_to_hex(orig_blob);
-+ if (our_blob)
-+ arguments[2] = oid_to_hex(our_blob);
-+ if (their_blob)
-+ arguments[3] = oid_to_hex(their_blob);
++ if (orig_blob) {
++ oid_to_hex_r(oids[0], orig_blob);
++ xsnprintf(modes[0], sizeof(modes[0]), "%06o", orig_mode);
++ }
+
-+ xsnprintf(ownbuf[0], sizeof(ownbuf[0]), "%o", orig_mode);
-+ xsnprintf(ownbuf[1], sizeof(ownbuf[1]), "%o", our_mode);
-+ xsnprintf(ownbuf[2], sizeof(ownbuf[2]), "%o", their_mode);
++ if (our_blob) {
++ oid_to_hex_r(oids[1], our_blob);
++ xsnprintf(modes[1], sizeof(modes[1]), "%06o", our_mode);
++ }
++
++ if (their_blob) {
++ oid_to_hex_r(oids[2], their_blob);
++ xsnprintf(modes[2], sizeof(modes[2]), "%06o", their_mode);
++ }
+
+ return run_command_v_opt(arguments, 0);
+}
+
+static int merge_entry(struct index_state *istate, int quiet, int pos,
-+ const char *path, merge_cb cb, void *data)
++ const char *path, merge_fn fn, void *data)
+{
+ int found = 0;
+ const struct object_id *oids[3] = {NULL};
@@ -179,7 +183,7 @@
+ if (!found)
+ return error(_("%s is not in the cache"), path);
+
-+ if (cb(oids[0], oids[1], oids[2], path, modes[0], modes[1], modes[2], data)) {
++ if (fn(oids[0], oids[1], oids[2], path, modes[0], modes[1], modes[2], data)) {
+ if (!quiet)
+ error(_("Merge program failed"));
+ return -2;
@@ -188,8 +192,8 @@
+ return found;
+}
+
-+int merge_one_path(struct index_state *istate, int oneshot, int quiet,
-+ const char *path, merge_cb cb, void *data)
++int merge_index_path(struct index_state *istate, int oneshot, int quiet,
++ const char *path, merge_fn fn, void *data)
+{
+ int pos = index_name_pos(istate, path, strlen(path)), ret;
+
@@ -198,7 +202,7 @@
+ * already merged and there is nothing to do.
+ */
+ if (pos < 0) {
-+ ret = merge_entry(istate, quiet, -pos - 1, path, cb, data);
++ ret = merge_entry(istate, quiet, -pos - 1, path, fn, data);
+ if (ret == -1)
+ return -1;
+ else if (ret == -2)
@@ -207,8 +211,8 @@
+ return 0;
+}
+
-+int merge_all(struct index_state *istate, int oneshot, int quiet,
-+ merge_cb cb, void *data)
++int merge_all_index(struct index_state *istate, int oneshot, int quiet,
++ merge_fn fn, void *data)
+{
+ int err = 0, i, ret;
+ for (i = 0; i < istate->cache_nr; i++) {
@@ -216,7 +220,7 @@
+ if (!ce_stage(ce))
+ continue;
+
-+ ret = merge_entry(istate, quiet, i, ce->name, cb, data);
++ ret = merge_entry(istate, quiet, i, ce->name, fn, data);
+ if (ret > 0)
+ i += ret - 1;
+ else if (ret == -1)
@@ -236,24 +240,24 @@
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@
- unsigned int orig_mode, unsigned int our_mode,
- unsigned int their_mode);
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode);
-+typedef int (*merge_cb)(const struct object_id *orig_blob,
++typedef int (*merge_fn)(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
-+int merge_program_cb(const struct object_id *orig_blob,
-+ const struct object_id *our_blob,
-+ const struct object_id *their_blob, const char *path,
-+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
-+ void *data);
++int merge_one_file_spawn(const struct object_id *orig_blob,
++ const struct object_id *our_blob,
++ const struct object_id *their_blob, const char *path,
++ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
++ void *data);
+
-+int merge_one_path(struct index_state *istate, int oneshot, int quiet,
-+ const char *path, merge_cb cb, void *data);
-+int merge_all(struct index_state *istate, int oneshot, int quiet,
-+ merge_cb cb, void *data);
++int merge_index_path(struct index_state *istate, int oneshot, int quiet,
++ const char *path, merge_fn fn, void *data);
++int merge_all_index(struct index_state *istate, int oneshot, int quiet,
++ merge_fn fn, void *data);
+
#endif /* MERGE_STRATEGIES_H */
4: c0bc05406d ! 5: a0e6cebe89 merge-index: don't fork if the requested program is `git-merge-one-file'
@@ -3,8 +3,8 @@
merge-index: don't fork if the requested program is `git-merge-one-file'
Since `git-merge-one-file' has been rewritten and libified, this teaches
- `merge-index' to call merge_strategies_one_file() without forking using
- a new callback, merge_one_file_cb().
+ `merge-index' to call merge_three_way() without forking using a new
+ callback, merge_one_file_func().
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
@@ -22,7 +22,7 @@
int i, force_file = 0, err = 0, one_shot = 0, quiet = 0;
const char *pgm;
+ void *data;
-+ merge_cb merge_action;
++ merge_fn merge_action;
+ struct lock_file lock = LOCK_INIT;
/* Without this we cannot rely on waitpid() to tell
@@ -34,13 +34,13 @@
+
pgm = argv[i++];
+ if (!strcmp(pgm, "git-merge-one-file")) {
-+ merge_action = merge_one_file_cb;
++ merge_action = merge_one_file_func;
+ data = (void *)the_repository;
+
+ setup_work_tree();
+ hold_locked_index(&lock, LOCK_DIE_ON_ERROR);
+ } else {
-+ merge_action = merge_program_cb;
++ merge_action = merge_one_file_spawn;
+ data = (void *)pgm;
+ }
+
@@ -50,19 +50,19 @@
@@
}
if (!strcmp(arg, "-a")) {
- err |= merge_all(&the_index, one_shot, quiet,
-- merge_program_cb, (void *)pgm);
-+ merge_action, data);
+ err |= merge_all_index(&the_index, one_shot, quiet,
+- merge_one_file_spawn, (void *)pgm);
++ merge_action, data);
continue;
}
die("git merge-index: unknown option %s", arg);
}
- err |= merge_one_path(&the_index, one_shot, quiet, arg,
-- merge_program_cb, (void *)pgm);
-+ merge_action, data);
+ err |= merge_index_path(&the_index, one_shot, quiet, arg,
+- merge_one_file_spawn, (void *)pgm);
++ merge_action, data);
+ }
+
-+ if (merge_action == merge_one_file_cb) {
++ if (merge_action == merge_one_file_func) {
+ if (err) {
+ rollback_lock_file(&lock);
+ return err;
@@ -80,20 +80,20 @@
return 0;
}
-+int merge_one_file_cb(const struct object_id *orig_blob,
-+ const struct object_id *our_blob,
-+ const struct object_id *their_blob, const char *path,
-+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
-+ void *data)
++int merge_one_file_func(const struct object_id *orig_blob,
++ const struct object_id *our_blob,
++ const struct object_id *their_blob, const char *path,
++ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
++ void *data)
+{
-+ return merge_strategies_one_file((struct repository *)data,
-+ orig_blob, our_blob, their_blob, path,
-+ orig_mode, our_mode, their_mode);
++ return merge_three_way((struct repository *)data,
++ orig_blob, our_blob, their_blob, path,
++ orig_mode, our_mode, their_mode);
+}
+
- int merge_program_cb(const struct object_id *orig_blob,
- const struct object_id *our_blob,
- const struct object_id *their_blob, const char *path,
+ int merge_one_file_spawn(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
diff --git a/merge-strategies.h b/merge-strategies.h
--- a/merge-strategies.h
@@ -102,12 +102,12 @@
unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
void *data);
-+int merge_one_file_cb(const struct object_id *orig_blob,
-+ const struct object_id *our_blob,
-+ const struct object_id *their_blob, const char *path,
-+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
-+ void *data);
++int merge_one_file_func(const struct object_id *orig_blob,
++ const struct object_id *our_blob,
++ const struct object_id *their_blob, const char *path,
++ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
++ void *data);
+
- int merge_program_cb(const struct object_id *orig_blob,
- const struct object_id *our_blob,
- const struct object_id *their_blob, const char *path,
+ int merge_one_file_spawn(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
5: cbfe192982 ! 6: 94fbc7e286 merge-resolve: rewrite in C
@@ -17,12 +17,10 @@
write_index_as_tree().
- The call to `merge-index', needed to invoke `git merge-one-file', is
- replaced by a call to the new merge_all() function. A callback
- function, merge_one_file_cb(), is added to allow it to call
- merge_one_file() without forking.
+ replaced by a call to the new merge_all_index() function.
- Here too, the index is read in cmd_merge_resolve(), but
- merge_strategies_resolve() takes care of writing it back to the disk.
+ The index is read in cmd_merge_resolve(), and is wrote back by
+ merge_strategies_resolve().
The parameters of merge_strategies_resolve() will be surprising at first
glance: why using a commit list for `bases' and `remote', where we could
@@ -83,6 +81,7 @@
+ * Resolve two trees, using enhanced multi-base read-tree.
+ */
+
++#define USE_THE_INDEX_COMPATIBILITY_MACROS
+#include "cache.h"
+#include "builtin.h"
+#include "merge-strategies.h"
@@ -92,7 +91,7 @@
+
+int cmd_merge_resolve(int argc, const char **argv, const char *prefix)
+{
-+ int i, is_baseless = 1, sep_seen = 0;
++ int i, sep_seen = 0;
+ const char *head = NULL;
+ struct commit_list *bases = NULL, *remote = NULL;
+ struct commit_list **next_base = &bases;
@@ -101,42 +100,45 @@
+ usage(builtin_merge_resolve_usage);
+
+ setup_work_tree();
-+ if (repo_read_index(the_repository) < 0)
++ if (read_cache() < 0)
+ die("invalid index");
+
-+ /* The first parameters up to -- are merge bases; the rest are
-+ * heads. */
++ /*
++ * The first parameters up to -- are merge bases; the rest are
++ * heads.
++ */
+ for (i = 1; i < argc; i++) {
-+ if (strcmp(argv[i], "--") == 0)
++ if (!strcmp(argv[i], "--"))
+ sep_seen = 1;
-+ else if (strcmp(argv[i], "-h") == 0)
++ else if (!strcmp(argv[i], "-h"))
+ usage(builtin_merge_resolve_usage);
+ else if (sep_seen && !head)
+ head = argv[i];
-+ else if (remote) {
-+ /* Give up if we are given two or more remotes.
-+ * Not handling octopus. */
-+ return 2;
-+ } else {
++ else {
+ struct object_id oid;
++ struct commit *commit;
+
-+ get_oid(argv[i], &oid);
-+ is_baseless &= sep_seen;
++ if (get_oid(argv[i], &oid))
++ die("object %s not found.", argv[i]);
+
-+ if (!oideq(&oid, the_hash_algo->empty_tree)) {
-+ struct commit *commit;
-+ commit = lookup_commit_or_die(&oid, argv[i]);
++ commit = lookup_commit_or_die(&oid, argv[i]);
+
-+ if (sep_seen)
-+ commit_list_append(commit, &remote);
-+ else
-+ next_base = commit_list_append(commit, next_base);
-+ }
++ if (sep_seen)
++ commit_list_insert(commit, &remote);
++ else
++ next_base = commit_list_append(commit, next_base);
+ }
+ }
+
++ /*
++ * Give up if we are given two or more remotes. Not handling
++ * octopus.
++ */
++ if (remote && remote->next)
++ return 2;
++
+ /* Give up if this is a baseless merge. */
-+ if (is_baseless)
++ if (!bases)
+ return 2;
+
+ return merge_strategies_resolve(the_repository, bases, head, remote);
@@ -221,14 +223,13 @@
#include "cache.h"
+#include "cache-tree.h"
#include "dir.h"
- #include "ll-merge.h"
+#include "lockfile.h"
#include "merge-strategies.h"
#include "run-command.h"
+#include "unpack-trees.h"
#include "xdiff-interface.h"
- static int add_to_index_cacheinfo(struct index_state *istate,
+ static int checkout_from_index(struct index_state *istate, const char *path,
@@
return err;
@@ -303,7 +304,7 @@
+
+ puts(_("Simple merge failed, trying Automatic merge."));
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
-+ ret = merge_all(r->index, 0, 0, merge_one_file_cb, r);
++ ret = merge_all_index(r->index, 0, 0, merge_one_file_func, r);
+
+ write_locked_index(r->index, &lock, COMMIT_LOCK);
+ return !!ret;
@@ -326,10 +327,10 @@
+#include "commit.h"
#include "object.h"
- int merge_strategies_one_file(struct repository *r,
+ int merge_three_way(struct repository *r,
@@
- int merge_all(struct index_state *istate, int oneshot, int quiet,
- merge_cb cb, void *data);
+ int merge_all_index(struct index_state *istate, int oneshot, int quiet,
+ merge_fn fn, void *data);
+int merge_strategies_resolve(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
6: 35e386f626 ! 7: b582b7e5d1 merge-recursive: move better_branch_name() to merge.c
@@ -2,8 +2,8 @@
merge-recursive: move better_branch_name() to merge.c
- get_better_branch_name() will be used by rebase-octopus once it is
- rewritten in C, so instead of duplicating it, this moves this function
+ better_branch_name() will be used by merge-octopus once it is rewritten
+ in C, so instead of duplicating it, this moves this function
preventively inside an appropriate file in libgit.a. This function is
also renamed to reflect its usage by merge strategies.
7: 41eb0f7199 ! 8: d1936645d5 merge-octopus: rewrite in C
@@ -13,11 +13,10 @@
write_index_as_tree().
- The call to `diff-index ...' is replaced by a call to
- repo_index_has_changes(), and is moved from cmd_merge_octopus() to
- merge_octopus().
+ repo_index_has_changes().
- The call to `merge-index', needed to invoke `git merge-one-file', is
- replaced by a call to merge_all().
+ replaced by a call to merge_all_index().
The index is read in cmd_merge_octopus(), and is wrote back by
merge_strategies_octopus().
@@ -75,6 +74,7 @@
+ * Resolve two or more trees.
+ */
+
++#define USE_THE_INDEX_COMPATIBILITY_MACROS
+#include "cache.h"
+#include "builtin.h"
+#include "commit.h"
@@ -94,8 +94,8 @@
+ usage(builtin_merge_octopus_usage);
+
+ setup_work_tree();
-+ if (repo_read_index(the_repository) < 0)
-+ die("corrupted cache");
++ if (read_cache() < 0)
++ die("invalid index");
+
+ /*
+ * The first parameters up to -- are merge bases; the rest are
@@ -110,18 +110,17 @@
+ head_arg = argv[i];
+ else {
+ struct object_id oid;
++ struct commit *commit;
+
-+ get_oid(argv[i], &oid);
++ if (get_oid(argv[i], &oid))
++ die("object %s not found.", argv[i]);
+
-+ if (!oideq(&oid, the_hash_algo->empty_tree)) {
-+ struct commit *commit;
-+ commit = lookup_commit_or_die(&oid, argv[i]);
++ commit = lookup_commit_or_die(&oid, argv[i]);
+
-+ if (sep_seen)
-+ next_remote = commit_list_append(commit, next_remote);
-+ else
-+ next_base = commit_list_append(commit, next_base);
-+ }
++ if (sep_seen)
++ next_remote = commit_list_append(commit, next_remote);
++ else
++ next_base = commit_list_append(commit, next_base);
+ }
+ }
+
@@ -273,8 +272,8 @@
#include "cache-tree.h"
+#include "commit-reach.h"
#include "dir.h"
- #include "ll-merge.h"
#include "lockfile.h"
+ #include "merge-strategies.h"
@@
rollback_lock_file(&lock);
return 2;
@@ -463,7 +462,7 @@
+
+ puts(_("Simple merge did not work, trying automatic merge."));
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
-+ ret = !!merge_all(r->index, 0, 0, merge_one_file_cb, r);
++ ret = !!merge_all_index(r->index, 0, 0, merge_one_file_func, r);
+ write_locked_index(r->index, &lock, COMMIT_LOCK);
+
+ write_tree(r, &next);
8: 8f6c1ac057 = 9: 26b1a3979c merge: use the "resolve" strategy without forking
9: b1125261d1 = 10: 23bc9824df merge: use the "octopus" strategy without forking
10: 8d0932fd02 = 11: 3a340f5984 sequencer: use the "resolve" strategy without forking
11: e304723957 = 12: ce3723cf34 sequencer: use the "octopus" merge strategy without forking
--
2.20.1
^ permalink raw reply [flat|nested] 221+ messages in thread
* [PATCH v4 01/12] t6027: modernise tests
2020-11-13 11:04 ` [PATCH v4 00/12] " Alban Gruin
@ 2020-11-13 11:04 ` Alban Gruin
2020-11-13 11:04 ` [PATCH v4 02/12] update-index: move add_cacheinfo() to read-cache.c Alban Gruin
` (11 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-13 11:04 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
Some tests in t6027 uses a if/then/else to check if a command failed or
not, but we have the `test_must_fail' function to do it correctly for us
nowadays.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
t/t6407-merge-binary.sh | 27 ++++++---------------------
1 file changed, 6 insertions(+), 21 deletions(-)
diff --git a/t/t6407-merge-binary.sh b/t/t6407-merge-binary.sh
index 4e6c7cb77e..071d3f7343 100755
--- a/t/t6407-merge-binary.sh
+++ b/t/t6407-merge-binary.sh
@@ -5,7 +5,6 @@ test_description='ask merge-recursive to merge binary files'
. ./test-lib.sh
test_expect_success setup '
-
cat "$TEST_DIRECTORY"/test-binary-1.png >m &&
git add m &&
git ls-files -s | sed -e "s/ 0 / 1 /" >E1 &&
@@ -35,33 +34,19 @@ test_expect_success setup '
'
test_expect_success resolve '
-
rm -f a* m* &&
git reset --hard anchor &&
-
- if git merge -s resolve master
- then
- echo Oops, should not have succeeded
- false
- else
- git ls-files -s >current
- test_cmp expect current
- fi
+ test_must_fail git merge -s resolve master &&
+ git ls-files -s >current &&
+ test_cmp expect current
'
test_expect_success recursive '
-
rm -f a* m* &&
git reset --hard anchor &&
-
- if git merge -s recursive master
- then
- echo Oops, should not have succeeded
- false
- else
- git ls-files -s >current
- test_cmp expect current
- fi
+ test_must_fail git merge -s recursive master &&
+ git ls-files -s >current &&
+ test_cmp expect current
'
test_done
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v4 02/12] update-index: move add_cacheinfo() to read-cache.c
2020-11-13 11:04 ` [PATCH v4 00/12] " Alban Gruin
2020-11-13 11:04 ` [PATCH v4 01/12] t6027: modernise tests Alban Gruin
@ 2020-11-13 11:04 ` Alban Gruin
2020-11-13 11:04 ` [PATCH v4 03/12] merge-one-file: rewrite in C Alban Gruin
` (10 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-13 11:04 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
This moves the function add_cacheinfo() that already exists in
update-index.c to update-index.c, renames it add_to_index_cacheinfo(),
and adds an `istate' parameter. The new cache entry is returned through
a pointer passed in the parameters. The return value is either 0
(success), -1 (invalid path), or -2 (failed to add the file in the
index).
This will become useful in the next commit, when the three-way merge
will need to call this function.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/update-index.c | 25 +++++++------------------
cache.h | 5 +++++
read-cache.c | 35 +++++++++++++++++++++++++++++++++++
3 files changed, 47 insertions(+), 18 deletions(-)
diff --git a/builtin/update-index.c b/builtin/update-index.c
index 79087bccea..44862f5e1d 100644
--- a/builtin/update-index.c
+++ b/builtin/update-index.c
@@ -404,27 +404,16 @@ static int process_path(const char *path, struct stat *st, int stat_errno)
static int add_cacheinfo(unsigned int mode, const struct object_id *oid,
const char *path, int stage)
{
- int len, option;
- struct cache_entry *ce;
+ int res;
- if (!verify_path(path, mode))
- return error("Invalid path '%s'", path);
-
- len = strlen(path);
- ce = make_empty_cache_entry(&the_index, len);
-
- oidcpy(&ce->oid, oid);
- memcpy(ce->name, path, len);
- ce->ce_flags = create_ce_flags(stage);
- ce->ce_namelen = len;
- ce->ce_mode = create_ce_mode(mode);
- if (assume_unchanged)
- ce->ce_flags |= CE_VALID;
- option = allow_add ? ADD_CACHE_OK_TO_ADD : 0;
- option |= allow_replace ? ADD_CACHE_OK_TO_REPLACE : 0;
- if (add_cache_entry(ce, option))
+ res = add_to_index_cacheinfo(&the_index, mode, oid, path, stage,
+ allow_add, allow_replace, NULL);
+ if (res == -1)
+ return res;
+ if (res == -2)
return error("%s: cannot add to the index - missing --add option?",
path);
+
report("add '%s'", path);
return 0;
}
diff --git a/cache.h b/cache.h
index c0072d43b1..be16ab3215 100644
--- a/cache.h
+++ b/cache.h
@@ -830,6 +830,11 @@ int remove_file_from_index(struct index_state *, const char *path);
int add_to_index(struct index_state *, const char *path, struct stat *, int flags);
int add_file_to_index(struct index_state *, const char *path, int flags);
+int add_to_index_cacheinfo(struct index_state *, unsigned int mode,
+ const struct object_id *oid, const char *path,
+ int stage, int allow_add, int allow_replace,
+ struct cache_entry **pce);
+
int chmod_index_entry(struct index_state *, struct cache_entry *ce, char flip);
int ce_same_name(const struct cache_entry *a, const struct cache_entry *b);
void set_object_name_for_intent_to_add_entry(struct cache_entry *ce);
diff --git a/read-cache.c b/read-cache.c
index ecf6f68994..c25f951db4 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -1350,6 +1350,41 @@ int add_index_entry(struct index_state *istate, struct cache_entry *ce, int opti
return 0;
}
+int add_to_index_cacheinfo(struct index_state *istate, unsigned int mode,
+ const struct object_id *oid, const char *path,
+ int stage, int allow_add, int allow_replace,
+ struct cache_entry **pce)
+{
+ int len, option;
+ struct cache_entry *ce = NULL;
+
+ if (!verify_path(path, mode))
+ return error(_("Invalid path '%s'"), path);
+
+ len = strlen(path);
+ ce = make_empty_cache_entry(istate, len);
+
+ oidcpy(&ce->oid, oid);
+ memcpy(ce->name, path, len);
+ ce->ce_flags = create_ce_flags(stage);
+ ce->ce_namelen = len;
+ ce->ce_mode = create_ce_mode(mode);
+ if (assume_unchanged)
+ ce->ce_flags |= CE_VALID;
+ option = allow_add ? ADD_CACHE_OK_TO_ADD : 0;
+ option |= allow_replace ? ADD_CACHE_OK_TO_REPLACE : 0;
+
+ if (add_index_entry(istate, ce, option)) {
+ discard_cache_entry(ce);
+ return -2;
+ }
+
+ if (pce)
+ *pce = ce;
+
+ return 0;
+}
+
/*
* "refresh" does not calculate a new sha1 file or bring the
* cache up-to-date for mode/content changes. But what it
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v4 03/12] merge-one-file: rewrite in C
2020-11-13 11:04 ` [PATCH v4 00/12] " Alban Gruin
2020-11-13 11:04 ` [PATCH v4 01/12] t6027: modernise tests Alban Gruin
2020-11-13 11:04 ` [PATCH v4 02/12] update-index: move add_cacheinfo() to read-cache.c Alban Gruin
@ 2020-11-13 11:04 ` Alban Gruin
2020-11-13 11:04 ` [PATCH v4 04/12] merge-index: libify merge_one_path() and merge_all() Alban Gruin
` (9 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-13 11:04 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
This rewrites `git merge-one-file' from shell to C. This port is not
completely straightforward: to save precious cycles by avoiding reading
and flushing the index repeatedly, write temporary files when an
operation can be performed in-memory, or allow other function to use the
rewrite without forking nor worrying about the index, the calls to
external processes are replaced by calls to functions in libgit.a:
- calls to `update-index --add --cacheinfo' are replaced by calls to
add_to_index_cacheinfo();
- calls to `update-index --remove' are replaced by calls to
remove_file_from_index();
- calls to `checkout-index -u -f' are replaced by calls to
checkout_entry();
- calls to `unpack-file' and `merge-files' are replaced by calls to
read_mmblob() and xdl_merge(), respectively, to merge files
in-memory;
- calls to `checkout-index -f --stage=2' are removed, as this is needed
to have the correct permission bits on the merged file from the
script, but not in the C version;
- calls to `update-index' are replaced by calls to add_file_to_index().
The bulk of the rewrite is done in a new file in libgit.a,
merge-strategies.c. This will enable the resolve and octopus strategies
to directly call it instead of forking.
This also fixes a bug present in the original script: instead of
checking if a _regular_ file exists when a file exists in the branch to
merge, but not in our branch, the rewritten version checks if a file of
any kind (ie. a directory, ...) exists. This fixes the tests t6035.14,
where the branch to merge had a new file, `a/b', but our branch had a
directory there; it should have failed because a directory exists, but
it did not because there was no regular file called `a/b'. This test is
now marked as successful.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Makefile | 3 +-
builtin.h | 1 +
builtin/merge-one-file.c | 94 +++++++++++++++++
git-merge-one-file.sh | 167 ------------------------------
git.c | 1 +
merge-strategies.c | 173 ++++++++++++++++++++++++++++++++
merge-strategies.h | 12 +++
t/t6415-merge-dir-to-symlink.sh | 2 +-
8 files changed, 284 insertions(+), 169 deletions(-)
create mode 100644 builtin/merge-one-file.c
delete mode 100755 git-merge-one-file.sh
create mode 100644 merge-strategies.c
create mode 100644 merge-strategies.h
diff --git a/Makefile b/Makefile
index de53954590..6dfdb33cb2 100644
--- a/Makefile
+++ b/Makefile
@@ -601,7 +601,6 @@ SCRIPT_SH += git-bisect.sh
SCRIPT_SH += git-difftool--helper.sh
SCRIPT_SH += git-filter-branch.sh
SCRIPT_SH += git-merge-octopus.sh
-SCRIPT_SH += git-merge-one-file.sh
SCRIPT_SH += git-merge-resolve.sh
SCRIPT_SH += git-mergetool.sh
SCRIPT_SH += git-quiltimport.sh
@@ -909,6 +908,7 @@ LIB_OBJS += match-trees.o
LIB_OBJS += mem-pool.o
LIB_OBJS += merge-blobs.o
LIB_OBJS += merge-recursive.o
+LIB_OBJS += merge-strategies.o
LIB_OBJS += merge.o
LIB_OBJS += mergesort.o
LIB_OBJS += midx.o
@@ -1094,6 +1094,7 @@ BUILTIN_OBJS += builtin/mailsplit.o
BUILTIN_OBJS += builtin/merge-base.o
BUILTIN_OBJS += builtin/merge-file.o
BUILTIN_OBJS += builtin/merge-index.o
+BUILTIN_OBJS += builtin/merge-one-file.o
BUILTIN_OBJS += builtin/merge-ours.o
BUILTIN_OBJS += builtin/merge-recursive.o
BUILTIN_OBJS += builtin/merge-tree.o
diff --git a/builtin.h b/builtin.h
index 53fb290963..4d2cd78856 100644
--- a/builtin.h
+++ b/builtin.h
@@ -178,6 +178,7 @@ int cmd_merge_base(int argc, const char **argv, const char *prefix);
int cmd_merge_index(int argc, const char **argv, const char *prefix);
int cmd_merge_ours(int argc, const char **argv, const char *prefix);
int cmd_merge_file(int argc, const char **argv, const char *prefix);
+int cmd_merge_one_file(int argc, const char **argv, const char *prefix);
int cmd_merge_recursive(int argc, const char **argv, const char *prefix);
int cmd_merge_tree(int argc, const char **argv, const char *prefix);
int cmd_mktag(int argc, const char **argv, const char *prefix);
diff --git a/builtin/merge-one-file.c b/builtin/merge-one-file.c
new file mode 100644
index 0000000000..9c21778e1d
--- /dev/null
+++ b/builtin/merge-one-file.c
@@ -0,0 +1,94 @@
+/*
+ * Builtin "git merge-one-file"
+ *
+ * Copyright (c) 2020 Alban Gruin
+ *
+ * Based on git-merge-one-file.sh, written by Linus Torvalds.
+ *
+ * This is the git per-file merge utility, called with
+ *
+ * argv[1] - original file object name (or empty)
+ * argv[2] - file in branch1 object name (or empty)
+ * argv[3] - file in branch2 object name (or empty)
+ * argv[4] - pathname in repository
+ * argv[5] - original file mode (or empty)
+ * argv[6] - file in branch1 mode (or empty)
+ * argv[7] - file in branch2 mode (or empty)
+ *
+ * Handle some trivial cases. The _really_ trivial cases have been
+ * handled already by git read-tree, but that one doesn't do any merges
+ * that might change the tree layout.
+ */
+
+#define USE_THE_INDEX_COMPATIBILITY_MACROS
+#include "cache.h"
+#include "builtin.h"
+#include "lockfile.h"
+#include "merge-strategies.h"
+
+static const char builtin_merge_one_file_usage[] =
+ "git merge-one-file <orig blob> <our blob> <their blob> <path> "
+ "<orig mode> <our mode> <their mode>\n\n"
+ "Blob ids and modes should be empty for missing files.";
+
+static int read_mode(const char *name, const char *arg, unsigned int *mode)
+{
+ char *last;
+ int ret = 0;
+
+ *mode = strtol(arg, &last, 8);
+
+ if (*last)
+ ret = error(_("invalid '%s' mode: expected nothing, got '%c'"), name, *last);
+ else if (!(S_ISREG(*mode) || S_ISDIR(*mode) || S_ISLNK(*mode)))
+ ret = error(_("invalid '%s' mode: %o"), name, *mode);
+
+ return ret;
+}
+
+int cmd_merge_one_file(int argc, const char **argv, const char *prefix)
+{
+ struct object_id orig_blob, our_blob, their_blob,
+ *p_orig_blob = NULL, *p_our_blob = NULL, *p_their_blob = NULL;
+ unsigned int orig_mode = 0, our_mode = 0, their_mode = 0, ret = 0;
+ struct lock_file lock = LOCK_INIT;
+
+ if (argc != 8)
+ usage(builtin_merge_one_file_usage);
+
+ if (read_cache() < 0)
+ die("invalid index");
+
+ hold_locked_index(&lock, LOCK_DIE_ON_ERROR);
+
+ if (!get_oid_hex(argv[1], &orig_blob)) {
+ p_orig_blob = &orig_blob;
+ ret = read_mode("orig", argv[5], &orig_mode);
+ } else if (!*argv[1] && *argv[5])
+ ret = error(_("no 'orig' object id given, but a mode was still given."));
+
+ if (!get_oid_hex(argv[2], &our_blob)) {
+ p_our_blob = &our_blob;
+ ret = read_mode("our", argv[6], &our_mode);
+ } else if (!*argv[2] && *argv[6])
+ ret = error(_("no 'our' object id given, but a mode was still given."));
+
+ if (!get_oid_hex(argv[3], &their_blob)) {
+ p_their_blob = &their_blob;
+ ret = read_mode("their", argv[7], &their_mode);
+ } else if (!*argv[3] && *argv[7])
+ ret = error(_("no 'their' object id given, but a mode was still given."));
+
+ if (ret)
+ return ret;
+
+ ret = merge_three_way(the_repository, p_orig_blob, p_our_blob, p_their_blob,
+ argv[4], orig_mode, our_mode, their_mode);
+
+ if (ret) {
+ rollback_lock_file(&lock);
+ return !!ret;
+ }
+
+ return write_locked_index(&the_index, &lock, COMMIT_LOCK);
+}
diff --git a/git-merge-one-file.sh b/git-merge-one-file.sh
deleted file mode 100755
index f6d9852d2f..0000000000
--- a/git-merge-one-file.sh
+++ /dev/null
@@ -1,167 +0,0 @@
-#!/bin/sh
-#
-# Copyright (c) Linus Torvalds, 2005
-#
-# This is the git per-file merge script, called with
-#
-# $1 - original file SHA1 (or empty)
-# $2 - file in branch1 SHA1 (or empty)
-# $3 - file in branch2 SHA1 (or empty)
-# $4 - pathname in repository
-# $5 - original file mode (or empty)
-# $6 - file in branch1 mode (or empty)
-# $7 - file in branch2 mode (or empty)
-#
-# Handle some trivial cases.. The _really_ trivial cases have
-# been handled already by git read-tree, but that one doesn't
-# do any merges that might change the tree layout.
-
-USAGE='<orig blob> <our blob> <their blob> <path>'
-USAGE="$USAGE <orig mode> <our mode> <their mode>"
-LONG_USAGE="usage: git merge-one-file $USAGE
-
-Blob ids and modes should be empty for missing files."
-
-SUBDIRECTORY_OK=Yes
-. git-sh-setup
-cd_to_toplevel
-require_work_tree
-
-if test $# != 7
-then
- echo "$LONG_USAGE"
- exit 1
-fi
-
-case "${1:-.}${2:-.}${3:-.}" in
-#
-# Deleted in both or deleted in one and unchanged in the other
-#
-"$1.." | "$1.$1" | "$1$1.")
- if { test -z "$6" && test "$5" != "$7"; } ||
- { test -z "$7" && test "$5" != "$6"; }
- then
- echo "ERROR: File $4 deleted on one branch but had its" >&2
- echo "ERROR: permissions changed on the other." >&2
- exit 1
- fi
-
- if test -n "$2"
- then
- echo "Removing $4"
- else
- # read-tree checked that index matches HEAD already,
- # so we know we do not have this path tracked.
- # there may be an unrelated working tree file here,
- # which we should just leave unmolested. Make sure
- # we do not have it in the index, though.
- exec git update-index --remove -- "$4"
- fi
- if test -f "$4"
- then
- rm -f -- "$4" &&
- rmdir -p "$(expr "z$4" : 'z\(.*\)/')" 2>/dev/null || :
- fi &&
- exec git update-index --remove -- "$4"
- ;;
-
-#
-# Added in one.
-#
-".$2.")
- # the other side did not add and we added so there is nothing
- # to be done, except making the path merged.
- exec git update-index --add --cacheinfo "$6" "$2" "$4"
- ;;
-"..$3")
- echo "Adding $4"
- if test -f "$4"
- then
- echo "ERROR: untracked $4 is overwritten by the merge." >&2
- exit 1
- fi
- git update-index --add --cacheinfo "$7" "$3" "$4" &&
- exec git checkout-index -u -f -- "$4"
- ;;
-
-#
-# Added in both, identically (check for same permissions).
-#
-".$3$2")
- if test "$6" != "$7"
- then
- echo "ERROR: File $4 added identically in both branches," >&2
- echo "ERROR: but permissions conflict $6->$7." >&2
- exit 1
- fi
- echo "Adding $4"
- git update-index --add --cacheinfo "$6" "$2" "$4" &&
- exec git checkout-index -u -f -- "$4"
- ;;
-
-#
-# Modified in both, but differently.
-#
-"$1$2$3" | ".$2$3")
-
- case ",$6,$7," in
- *,120000,*)
- echo "ERROR: $4: Not merging symbolic link changes." >&2
- exit 1
- ;;
- *,160000,*)
- echo "ERROR: $4: Not merging conflicting submodule changes." >&2
- exit 1
- ;;
- esac
-
- src1=$(git unpack-file $2)
- src2=$(git unpack-file $3)
- case "$1" in
- '')
- echo "Added $4 in both, but differently."
- orig=$(git unpack-file $(git hash-object /dev/null))
- ;;
- *)
- echo "Auto-merging $4"
- orig=$(git unpack-file $1)
- ;;
- esac
-
- git merge-file "$src1" "$orig" "$src2"
- ret=$?
- msg=
- if test $ret != 0 || test -z "$1"
- then
- msg='content conflict'
- ret=1
- fi
-
- # Create the working tree file, using "our tree" version from the
- # index, and then store the result of the merge.
- git checkout-index -f --stage=2 -- "$4" && cat "$src1" >"$4" || exit 1
- rm -f -- "$orig" "$src1" "$src2"
-
- if test "$6" != "$7"
- then
- if test -n "$msg"
- then
- msg="$msg, "
- fi
- msg="${msg}permissions conflict: $5->$6,$7"
- ret=1
- fi
-
- if test $ret != 0
- then
- echo "ERROR: $msg in $4" >&2
- exit 1
- fi
- exec git update-index -- "$4"
- ;;
-
-*)
- echo "ERROR: $4: Not handling case $1 -> $2 -> $3" >&2
- ;;
-esac
-exit 1
diff --git a/git.c b/git.c
index f1e8b56d99..a4d3f98094 100644
--- a/git.c
+++ b/git.c
@@ -540,6 +540,7 @@ static struct cmd_struct commands[] = {
{ "merge-file", cmd_merge_file, RUN_SETUP_GENTLY },
{ "merge-index", cmd_merge_index, RUN_SETUP | NO_PARSEOPT },
{ "merge-ours", cmd_merge_ours, RUN_SETUP | NO_PARSEOPT },
+ { "merge-one-file", cmd_merge_one_file, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-ours", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-theirs", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
diff --git a/merge-strategies.c b/merge-strategies.c
new file mode 100644
index 0000000000..f5fdb15bbf
--- /dev/null
+++ b/merge-strategies.c
@@ -0,0 +1,173 @@
+#include "cache.h"
+#include "dir.h"
+#include "merge-strategies.h"
+#include "xdiff-interface.h"
+
+static int checkout_from_index(struct index_state *istate, const char *path,
+ struct cache_entry *ce)
+{
+ struct checkout state = CHECKOUT_INIT;
+
+ state.istate = istate;
+ state.force = 1;
+ state.base_dir = "";
+ state.base_dir_len = 0;
+
+ if (checkout_entry(ce, &state, NULL, NULL) < 0)
+ return error(_("%s: cannot checkout file"), path);
+ return 0;
+}
+
+static int merge_one_file_deleted(struct index_state *istate,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
+{
+ if ((our_blob && orig_mode != our_mode) ||
+ (their_blob && orig_mode != their_mode))
+ return error(_("File %s deleted on one branch but had its "
+ "permissions changed on the other."), path);
+
+ if (our_blob) {
+ printf(_("Removing %s\n"), path);
+
+ if (file_exists(path))
+ remove_path(path);
+ }
+
+ if (remove_file_from_index(istate, path))
+ return error("%s: cannot remove from the index", path);
+ return 0;
+}
+
+static int do_merge_one_file(struct index_state *istate,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
+{
+ int ret, i, dest;
+ ssize_t written;
+ mmbuffer_t result = {NULL, 0};
+ mmfile_t mmfs[3];
+ xmparam_t xmp = {{0}};
+
+ if (our_mode == S_IFLNK || their_mode == S_IFLNK)
+ return error(_("%s: Not merging symbolic link changes."), path);
+ else if (our_mode == S_IFGITLINK || their_mode == S_IFGITLINK)
+ return error(_("%s: Not merging conflicting submodule changes."), path);
+ else if (our_mode != their_mode)
+ return error(_("permission conflict: %o->%o,%o in %s"),
+ orig_mode, our_mode, their_mode, path);
+
+ if (orig_blob) {
+ printf(_("Auto-merging %s\n"), path);
+ read_mmblob(mmfs + 0, orig_blob);
+ } else {
+ printf(_("Added %s in both, but differently.\n"), path);
+ read_mmblob(mmfs + 0, &null_oid);
+ }
+
+ read_mmblob(mmfs + 1, our_blob);
+ read_mmblob(mmfs + 2, their_blob);
+
+ xmp.level = XDL_MERGE_ZEALOUS_ALNUM;
+ xmp.style = 0;
+ xmp.favor = 0;
+
+ ret = xdl_merge(mmfs + 0, mmfs + 1, mmfs + 2, &xmp, &result);
+
+ for (i = 0; i < 3; i++)
+ free(mmfs[i].ptr);
+
+ if (ret < 0) {
+ free(result.ptr);
+ return error(_("Failed to execute internal merge"));
+ } else if (ret > 0 || !orig_blob) {
+ free(result.ptr);
+ return error(_("content conflict in %s"), path);
+ }
+
+ unlink(path);
+ if ((dest = open(path, O_WRONLY | O_CREAT, our_mode)) < 0) {
+ free(result.ptr);
+ return error_errno(_("failed to open file '%s'"), path);
+ }
+
+ written = write_in_full(dest, result.ptr, result.size);
+ close(dest);
+
+ free(result.ptr);
+
+ if (written < 0)
+ return error_errno(_("failed to write to '%s'"), path);
+
+ return add_file_to_index(istate, path, 0);
+}
+
+int merge_three_way(struct repository *r,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
+{
+ if (orig_blob &&
+ ((!their_blob && our_blob && oideq(orig_blob, our_blob)) ||
+ (!our_blob && their_blob && oideq(orig_blob, their_blob)))) {
+ /* Deleted in both or deleted in one and unchanged in the other. */
+ return merge_one_file_deleted(r->index, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+ } else if (!orig_blob && our_blob && !their_blob) {
+ /*
+ * Added in one. The other side did not add and we
+ * added so there is nothing to be done, except making
+ * the path merged.
+ */
+ return add_to_index_cacheinfo(r->index, our_mode, our_blob, path, 0, 1, 1, NULL);
+ } else if (!orig_blob && !our_blob && their_blob) {
+ struct cache_entry *ce;
+ printf(_("Adding %s\n"), path);
+
+ if (file_exists(path))
+ return error(_("untracked %s is overwritten by the merge."), path);
+
+ if (add_to_index_cacheinfo(r->index, their_mode, their_blob, path, 0, 1, 1, &ce))
+ return -1;
+ return checkout_from_index(r->index, path, ce);
+ } else if (!orig_blob && our_blob && their_blob &&
+ oideq(our_blob, their_blob)) {
+ struct cache_entry *ce;
+
+ /* Added in both, identically (check for same permissions). */
+ if (our_mode != their_mode)
+ return error(_("File %s added identically in both branches, "
+ "but permissions conflict %o->%o."),
+ path, our_mode, their_mode);
+
+ printf(_("Adding %s\n"), path);
+
+ if (add_to_index_cacheinfo(r->index, our_mode, our_blob, path, 0, 1, 1, &ce))
+ return -1;
+ return checkout_from_index(r->index, path, ce);
+ } else if (our_blob && their_blob) {
+ /* Modified in both, but differently. */
+ return do_merge_one_file(r->index,
+ orig_blob, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+ } else {
+ char orig_hex[GIT_MAX_HEXSZ] = {0}, our_hex[GIT_MAX_HEXSZ] = {0},
+ their_hex[GIT_MAX_HEXSZ] = {0};
+
+ if (orig_blob)
+ oid_to_hex_r(orig_hex, orig_blob);
+ if (our_blob)
+ oid_to_hex_r(our_hex, our_blob);
+ if (their_blob)
+ oid_to_hex_r(their_hex, their_blob);
+
+ return error(_("%s: Not handling case %s -> %s -> %s"),
+ path, orig_hex, our_hex, their_hex);
+ }
+
+ return 0;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
new file mode 100644
index 0000000000..e624c4f27c
--- /dev/null
+++ b/merge-strategies.h
@@ -0,0 +1,12 @@
+#ifndef MERGE_STRATEGIES_H
+#define MERGE_STRATEGIES_H
+
+#include "object.h"
+
+int merge_three_way(struct repository *r,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode);
+
+#endif /* MERGE_STRATEGIES_H */
diff --git a/t/t6415-merge-dir-to-symlink.sh b/t/t6415-merge-dir-to-symlink.sh
index 2eddcc7664..5fb74e39a0 100755
--- a/t/t6415-merge-dir-to-symlink.sh
+++ b/t/t6415-merge-dir-to-symlink.sh
@@ -94,7 +94,7 @@ test_expect_success SYMLINKS 'a/b was resolved as symlink' '
test -h a/b
'
-test_expect_failure 'do not lose untracked in merge (resolve)' '
+test_expect_success 'do not lose untracked in merge (resolve)' '
git reset --hard &&
git checkout baseline^0 &&
>a/b/c/e &&
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v4 04/12] merge-index: libify merge_one_path() and merge_all()
2020-11-13 11:04 ` [PATCH v4 00/12] " Alban Gruin
` (2 preceding siblings ...)
2020-11-13 11:04 ` [PATCH v4 03/12] merge-one-file: rewrite in C Alban Gruin
@ 2020-11-13 11:04 ` Alban Gruin
2020-11-13 11:04 ` [PATCH v4 05/12] merge-index: don't fork if the requested program is `git-merge-one-file' Alban Gruin
` (8 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-13 11:04 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
The "resolve" and "octopus" merge strategies do not call directly `git
merge-one-file', they delegate the work to another git command, `git
merge-index', that will loop over files in the index and call the
specified command. Unfortunately, these functions are not part of
libgit.a, which means that once rewritten, the strategies would still
have to invoke `merge-one-file' by spawning a new process first.
To avoid this, this moves and renames merge_one_path(), merge_all(), and
their helpers to merge-strategies.c. They also take a callback to
dictate what they should do for each file. For now, to preserve the
behaviour of `merge-index', only one callback, launching a new process,
is defined.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge-index.c | 77 +++----------------------------
merge-strategies.c | 103 ++++++++++++++++++++++++++++++++++++++++++
merge-strategies.h | 17 +++++++
3 files changed, 127 insertions(+), 70 deletions(-)
diff --git a/builtin/merge-index.c b/builtin/merge-index.c
index 38ea6ad6ca..49e3382fb9 100644
--- a/builtin/merge-index.c
+++ b/builtin/merge-index.c
@@ -1,74 +1,11 @@
#define USE_THE_INDEX_COMPATIBILITY_MACROS
#include "builtin.h"
-#include "run-command.h"
-
-static const char *pgm;
-static int one_shot, quiet;
-static int err;
-
-static int merge_entry(int pos, const char *path)
-{
- int found;
- const char *arguments[] = { pgm, "", "", "", path, "", "", "", NULL };
- char hexbuf[4][GIT_MAX_HEXSZ + 1];
- char ownbuf[4][60];
-
- if (pos >= active_nr)
- die("git merge-index: %s not in the cache", path);
- found = 0;
- do {
- const struct cache_entry *ce = active_cache[pos];
- int stage = ce_stage(ce);
-
- if (strcmp(ce->name, path))
- break;
- found++;
- oid_to_hex_r(hexbuf[stage], &ce->oid);
- xsnprintf(ownbuf[stage], sizeof(ownbuf[stage]), "%o", ce->ce_mode);
- arguments[stage] = hexbuf[stage];
- arguments[stage + 4] = ownbuf[stage];
- } while (++pos < active_nr);
- if (!found)
- die("git merge-index: %s not in the cache", path);
-
- if (run_command_v_opt(arguments, 0)) {
- if (one_shot)
- err++;
- else {
- if (!quiet)
- die("merge program failed");
- exit(1);
- }
- }
- return found;
-}
-
-static void merge_one_path(const char *path)
-{
- int pos = cache_name_pos(path, strlen(path));
-
- /*
- * If it already exists in the cache as stage0, it's
- * already merged and there is nothing to do.
- */
- if (pos < 0)
- merge_entry(-pos-1, path);
-}
-
-static void merge_all(void)
-{
- int i;
- for (i = 0; i < active_nr; i++) {
- const struct cache_entry *ce = active_cache[i];
- if (!ce_stage(ce))
- continue;
- i += merge_entry(i, ce->name)-1;
- }
-}
+#include "merge-strategies.h"
int cmd_merge_index(int argc, const char **argv, const char *prefix)
{
- int i, force_file = 0;
+ int i, force_file = 0, err = 0, one_shot = 0, quiet = 0;
+ const char *pgm;
/* Without this we cannot rely on waitpid() to tell
* what happened to our children.
@@ -98,14 +35,14 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
continue;
}
if (!strcmp(arg, "-a")) {
- merge_all();
+ err |= merge_all_index(&the_index, one_shot, quiet,
+ merge_one_file_spawn, (void *)pgm);
continue;
}
die("git merge-index: unknown option %s", arg);
}
- merge_one_path(arg);
+ err |= merge_index_path(&the_index, one_shot, quiet, arg,
+ merge_one_file_spawn, (void *)pgm);
}
- if (err && !quiet)
- die("merge program failed");
return err;
}
diff --git a/merge-strategies.c b/merge-strategies.c
index f5fdb15bbf..e1d121c993 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -1,6 +1,7 @@
#include "cache.h"
#include "dir.h"
#include "merge-strategies.h"
+#include "run-command.h"
#include "xdiff-interface.h"
static int checkout_from_index(struct index_state *istate, const char *path,
@@ -171,3 +172,105 @@ int merge_three_way(struct repository *r,
return 0;
}
+
+int merge_one_file_spawn(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data)
+{
+ char oids[3][GIT_MAX_HEXSZ + 1] = {{0}};
+ char modes[3][10] = {{0}};
+ const char *arguments[] = { (char *)data, oids[0], oids[1], oids[2],
+ path, modes[0], modes[1], modes[2], NULL };
+
+ if (orig_blob) {
+ oid_to_hex_r(oids[0], orig_blob);
+ xsnprintf(modes[0], sizeof(modes[0]), "%06o", orig_mode);
+ }
+
+ if (our_blob) {
+ oid_to_hex_r(oids[1], our_blob);
+ xsnprintf(modes[1], sizeof(modes[1]), "%06o", our_mode);
+ }
+
+ if (their_blob) {
+ oid_to_hex_r(oids[2], their_blob);
+ xsnprintf(modes[2], sizeof(modes[2]), "%06o", their_mode);
+ }
+
+ return run_command_v_opt(arguments, 0);
+}
+
+static int merge_entry(struct index_state *istate, int quiet, int pos,
+ const char *path, merge_fn fn, void *data)
+{
+ int found = 0;
+ const struct object_id *oids[3] = {NULL};
+ unsigned int modes[3] = {0};
+
+ do {
+ const struct cache_entry *ce = istate->cache[pos];
+ int stage = ce_stage(ce);
+
+ if (strcmp(ce->name, path))
+ break;
+ found++;
+ oids[stage - 1] = &ce->oid;
+ modes[stage - 1] = ce->ce_mode;
+ } while (++pos < istate->cache_nr);
+ if (!found)
+ return error(_("%s is not in the cache"), path);
+
+ if (fn(oids[0], oids[1], oids[2], path, modes[0], modes[1], modes[2], data)) {
+ if (!quiet)
+ error(_("Merge program failed"));
+ return -2;
+ }
+
+ return found;
+}
+
+int merge_index_path(struct index_state *istate, int oneshot, int quiet,
+ const char *path, merge_fn fn, void *data)
+{
+ int pos = index_name_pos(istate, path, strlen(path)), ret;
+
+ /*
+ * If it already exists in the cache as stage0, it's
+ * already merged and there is nothing to do.
+ */
+ if (pos < 0) {
+ ret = merge_entry(istate, quiet, -pos - 1, path, fn, data);
+ if (ret == -1)
+ return -1;
+ else if (ret == -2)
+ return 1;
+ }
+ return 0;
+}
+
+int merge_all_index(struct index_state *istate, int oneshot, int quiet,
+ merge_fn fn, void *data)
+{
+ int err = 0, i, ret;
+ for (i = 0; i < istate->cache_nr; i++) {
+ const struct cache_entry *ce = istate->cache[i];
+ if (!ce_stage(ce))
+ continue;
+
+ ret = merge_entry(istate, quiet, i, ce->name, fn, data);
+ if (ret > 0)
+ i += ret - 1;
+ else if (ret == -1)
+ return -1;
+ else if (ret == -2) {
+ if (oneshot)
+ err++;
+ else
+ return 1;
+ }
+ }
+
+ return err;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
index e624c4f27c..d2f52d6792 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -9,4 +9,21 @@ int merge_three_way(struct repository *r,
const struct object_id *their_blob, const char *path,
unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode);
+typedef int (*merge_fn)(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
+int merge_one_file_spawn(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
+int merge_index_path(struct index_state *istate, int oneshot, int quiet,
+ const char *path, merge_fn fn, void *data);
+int merge_all_index(struct index_state *istate, int oneshot, int quiet,
+ merge_fn fn, void *data);
+
#endif /* MERGE_STRATEGIES_H */
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v4 05/12] merge-index: don't fork if the requested program is `git-merge-one-file'
2020-11-13 11:04 ` [PATCH v4 00/12] " Alban Gruin
` (3 preceding siblings ...)
2020-11-13 11:04 ` [PATCH v4 04/12] merge-index: libify merge_one_path() and merge_all() Alban Gruin
@ 2020-11-13 11:04 ` Alban Gruin
2020-11-13 11:04 ` [PATCH v4 06/12] merge-resolve: rewrite in C Alban Gruin
` (7 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-13 11:04 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
Since `git-merge-one-file' has been rewritten and libified, this teaches
`merge-index' to call merge_three_way() without forking using a new
callback, merge_one_file_func().
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge-index.c | 29 +++++++++++++++++++++++++++--
merge-strategies.c | 11 +++++++++++
merge-strategies.h | 6 ++++++
3 files changed, 44 insertions(+), 2 deletions(-)
diff --git a/builtin/merge-index.c b/builtin/merge-index.c
index 49e3382fb9..e684811d35 100644
--- a/builtin/merge-index.c
+++ b/builtin/merge-index.c
@@ -1,11 +1,15 @@
#define USE_THE_INDEX_COMPATIBILITY_MACROS
#include "builtin.h"
+#include "lockfile.h"
#include "merge-strategies.h"
int cmd_merge_index(int argc, const char **argv, const char *prefix)
{
int i, force_file = 0, err = 0, one_shot = 0, quiet = 0;
const char *pgm;
+ void *data;
+ merge_fn merge_action;
+ struct lock_file lock = LOCK_INIT;
/* Without this we cannot rely on waitpid() to tell
* what happened to our children.
@@ -26,7 +30,19 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
quiet = 1;
i++;
}
+
pgm = argv[i++];
+ if (!strcmp(pgm, "git-merge-one-file")) {
+ merge_action = merge_one_file_func;
+ data = (void *)the_repository;
+
+ setup_work_tree();
+ hold_locked_index(&lock, LOCK_DIE_ON_ERROR);
+ } else {
+ merge_action = merge_one_file_spawn;
+ data = (void *)pgm;
+ }
+
for (; i < argc; i++) {
const char *arg = argv[i];
if (!force_file && *arg == '-') {
@@ -36,13 +52,22 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
}
if (!strcmp(arg, "-a")) {
err |= merge_all_index(&the_index, one_shot, quiet,
- merge_one_file_spawn, (void *)pgm);
+ merge_action, data);
continue;
}
die("git merge-index: unknown option %s", arg);
}
err |= merge_index_path(&the_index, one_shot, quiet, arg,
- merge_one_file_spawn, (void *)pgm);
+ merge_action, data);
+ }
+
+ if (merge_action == merge_one_file_func) {
+ if (err) {
+ rollback_lock_file(&lock);
+ return err;
+ }
+
+ return write_locked_index(&the_index, &lock, COMMIT_LOCK);
}
return err;
}
diff --git a/merge-strategies.c b/merge-strategies.c
index e1d121c993..aa31b7045c 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -173,6 +173,17 @@ int merge_three_way(struct repository *r,
return 0;
}
+int merge_one_file_func(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data)
+{
+ return merge_three_way((struct repository *)data,
+ orig_blob, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+}
+
int merge_one_file_spawn(const struct object_id *orig_blob,
const struct object_id *our_blob,
const struct object_id *their_blob, const char *path,
diff --git a/merge-strategies.h b/merge-strategies.h
index d2f52d6792..b69a12b390 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -15,6 +15,12 @@ typedef int (*merge_fn)(const struct object_id *orig_blob,
unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
void *data);
+int merge_one_file_func(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
int merge_one_file_spawn(const struct object_id *orig_blob,
const struct object_id *our_blob,
const struct object_id *their_blob, const char *path,
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v4 06/12] merge-resolve: rewrite in C
2020-11-13 11:04 ` [PATCH v4 00/12] " Alban Gruin
` (4 preceding siblings ...)
2020-11-13 11:04 ` [PATCH v4 05/12] merge-index: don't fork if the requested program is `git-merge-one-file' Alban Gruin
@ 2020-11-13 11:04 ` Alban Gruin
2020-11-13 11:04 ` [PATCH v4 07/12] merge-recursive: move better_branch_name() to merge.c Alban Gruin
` (6 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-13 11:04 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
This rewrites `git merge-resolve' from shell to C. As for `git
merge-one-file', this port is not completely straightforward and removes
calls to external processes to avoid reading and writing the index over
and over again.
- The call to `update-index -q --refresh' is replaced by a call to
refresh_index().
- The call to `read-tree' is replaced by a call to unpack_trees() (and
all the setup needed).
- The call to `write-tree' is replaced by a call to
write_index_as_tree().
- The call to `merge-index', needed to invoke `git merge-one-file', is
replaced by a call to the new merge_all_index() function.
The index is read in cmd_merge_resolve(), and is wrote back by
merge_strategies_resolve().
The parameters of merge_strategies_resolve() will be surprising at first
glance: why using a commit list for `bases' and `remote', where we could
use an oid array, and a pointer to an oid? Because, in a later commit,
try_merge_strategy() will be able to call merge_strategies_resolve()
directly, and it already uses a commit list for `bases' (`common') and
`remote' (`remoteheads'), and a string for `head_arg'. To reduce
frictions later, merge_strategies_resolve() takes the same types of
parameters.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Makefile | 2 +-
builtin.h | 1 +
builtin/merge-resolve.c | 73 +++++++++++++++++++++++++++++++++++
git-merge-resolve.sh | 54 --------------------------
git.c | 1 +
merge-strategies.c | 85 +++++++++++++++++++++++++++++++++++++++++
merge-strategies.h | 5 +++
7 files changed, 166 insertions(+), 55 deletions(-)
create mode 100644 builtin/merge-resolve.c
delete mode 100755 git-merge-resolve.sh
diff --git a/Makefile b/Makefile
index 6dfdb33cb2..3cc6b192f1 100644
--- a/Makefile
+++ b/Makefile
@@ -601,7 +601,6 @@ SCRIPT_SH += git-bisect.sh
SCRIPT_SH += git-difftool--helper.sh
SCRIPT_SH += git-filter-branch.sh
SCRIPT_SH += git-merge-octopus.sh
-SCRIPT_SH += git-merge-resolve.sh
SCRIPT_SH += git-mergetool.sh
SCRIPT_SH += git-quiltimport.sh
SCRIPT_SH += git-request-pull.sh
@@ -1097,6 +1096,7 @@ BUILTIN_OBJS += builtin/merge-index.o
BUILTIN_OBJS += builtin/merge-one-file.o
BUILTIN_OBJS += builtin/merge-ours.o
BUILTIN_OBJS += builtin/merge-recursive.o
+BUILTIN_OBJS += builtin/merge-resolve.o
BUILTIN_OBJS += builtin/merge-tree.o
BUILTIN_OBJS += builtin/merge.o
BUILTIN_OBJS += builtin/mktag.o
diff --git a/builtin.h b/builtin.h
index 4d2cd78856..35e91c16d0 100644
--- a/builtin.h
+++ b/builtin.h
@@ -180,6 +180,7 @@ int cmd_merge_ours(int argc, const char **argv, const char *prefix);
int cmd_merge_file(int argc, const char **argv, const char *prefix);
int cmd_merge_one_file(int argc, const char **argv, const char *prefix);
int cmd_merge_recursive(int argc, const char **argv, const char *prefix);
+int cmd_merge_resolve(int argc, const char **argv, const char *prefix);
int cmd_merge_tree(int argc, const char **argv, const char *prefix);
int cmd_mktag(int argc, const char **argv, const char *prefix);
int cmd_mktree(int argc, const char **argv, const char *prefix);
diff --git a/builtin/merge-resolve.c b/builtin/merge-resolve.c
new file mode 100644
index 0000000000..dca31676b8
--- /dev/null
+++ b/builtin/merge-resolve.c
@@ -0,0 +1,73 @@
+/*
+ * Builtin "git merge-resolve"
+ *
+ * Copyright (c) 2020 Alban Gruin
+ *
+ * Based on git-merge-resolve.sh, written by Linus Torvalds and Junio C
+ * Hamano.
+ *
+ * Resolve two trees, using enhanced multi-base read-tree.
+ */
+
+#define USE_THE_INDEX_COMPATIBILITY_MACROS
+#include "cache.h"
+#include "builtin.h"
+#include "merge-strategies.h"
+
+static const char builtin_merge_resolve_usage[] =
+ "git merge-resolve <bases>... -- <head> <remote>";
+
+int cmd_merge_resolve(int argc, const char **argv, const char *prefix)
+{
+ int i, sep_seen = 0;
+ const char *head = NULL;
+ struct commit_list *bases = NULL, *remote = NULL;
+ struct commit_list **next_base = &bases;
+
+ if (argc < 5)
+ usage(builtin_merge_resolve_usage);
+
+ setup_work_tree();
+ if (read_cache() < 0)
+ die("invalid index");
+
+ /*
+ * The first parameters up to -- are merge bases; the rest are
+ * heads.
+ */
+ for (i = 1; i < argc; i++) {
+ if (!strcmp(argv[i], "--"))
+ sep_seen = 1;
+ else if (!strcmp(argv[i], "-h"))
+ usage(builtin_merge_resolve_usage);
+ else if (sep_seen && !head)
+ head = argv[i];
+ else {
+ struct object_id oid;
+ struct commit *commit;
+
+ if (get_oid(argv[i], &oid))
+ die("object %s not found.", argv[i]);
+
+ commit = lookup_commit_or_die(&oid, argv[i]);
+
+ if (sep_seen)
+ commit_list_insert(commit, &remote);
+ else
+ next_base = commit_list_append(commit, next_base);
+ }
+ }
+
+ /*
+ * Give up if we are given two or more remotes. Not handling
+ * octopus.
+ */
+ if (remote && remote->next)
+ return 2;
+
+ /* Give up if this is a baseless merge. */
+ if (!bases)
+ return 2;
+
+ return merge_strategies_resolve(the_repository, bases, head, remote);
+}
diff --git a/git-merge-resolve.sh b/git-merge-resolve.sh
deleted file mode 100755
index 343fe7bccd..0000000000
--- a/git-merge-resolve.sh
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2005 Linus Torvalds
-# Copyright (c) 2005 Junio C Hamano
-#
-# Resolve two trees, using enhanced multi-base read-tree.
-
-# The first parameters up to -- are merge bases; the rest are heads.
-bases= head= remotes= sep_seen=
-for arg
-do
- case ",$sep_seen,$head,$arg," in
- *,--,)
- sep_seen=yes
- ;;
- ,yes,,*)
- head=$arg
- ;;
- ,yes,*)
- remotes="$remotes$arg "
- ;;
- *)
- bases="$bases$arg "
- ;;
- esac
-done
-
-# Give up if we are given two or more remotes -- not handling octopus.
-case "$remotes" in
-?*' '?*)
- exit 2 ;;
-esac
-
-# Give up if this is a baseless merge.
-if test '' = "$bases"
-then
- exit 2
-fi
-
-git update-index -q --refresh
-git read-tree -u -m --aggressive $bases $head $remotes || exit 2
-echo "Trying simple merge."
-if result_tree=$(git write-tree 2>/dev/null)
-then
- exit 0
-else
- echo "Simple merge failed, trying Automatic merge."
- if git merge-index -o git-merge-one-file -a
- then
- exit 0
- else
- exit 1
- fi
-fi
diff --git a/git.c b/git.c
index a4d3f98094..64a1a1de41 100644
--- a/git.c
+++ b/git.c
@@ -544,6 +544,7 @@ static struct cmd_struct commands[] = {
{ "merge-recursive", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-ours", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-theirs", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
+ { "merge-resolve", cmd_merge_resolve, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-subtree", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-tree", cmd_merge_tree, RUN_SETUP | NO_PARSEOPT },
{ "mktag", cmd_mktag, RUN_SETUP | NO_PARSEOPT },
diff --git a/merge-strategies.c b/merge-strategies.c
index aa31b7045c..2b34ea0b76 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -1,7 +1,10 @@
#include "cache.h"
+#include "cache-tree.h"
#include "dir.h"
+#include "lockfile.h"
#include "merge-strategies.h"
#include "run-command.h"
+#include "unpack-trees.h"
#include "xdiff-interface.h"
static int checkout_from_index(struct index_state *istate, const char *path,
@@ -285,3 +288,85 @@ int merge_all_index(struct index_state *istate, int oneshot, int quiet,
return err;
}
+
+static int add_tree(const struct object_id *oid, struct tree_desc *t)
+{
+ struct tree *tree;
+
+ tree = parse_tree_indirect(oid);
+ if (parse_tree(tree))
+ return -1;
+
+ init_tree_desc(t, tree->buffer, tree->size);
+ return 0;
+}
+
+int merge_strategies_resolve(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remote)
+{
+ int i = 0;
+ struct lock_file lock = LOCK_INIT;
+ struct tree_desc t[MAX_UNPACK_TREES];
+ struct unpack_trees_options opts;
+ struct object_id head, oid;
+ struct commit_list *j;
+
+ if (head_arg)
+ get_oid(head_arg, &head);
+
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+ refresh_index(r->index, 0, NULL, NULL, NULL);
+
+ memset(&opts, 0, sizeof(opts));
+ opts.head_idx = 1;
+ opts.src_index = r->index;
+ opts.dst_index = r->index;
+ opts.update = 1;
+ opts.merge = 1;
+ opts.aggressive = 1;
+
+ for (j = bases; j && j->item; j = j->next) {
+ if (add_tree(&j->item->object.oid, t + (i++)))
+ goto out;
+ }
+
+ if (head_arg && add_tree(&head, t + (i++)))
+ goto out;
+ if (remote && add_tree(&remote->item->object.oid, t + (i++)))
+ goto out;
+
+ if (i == 1)
+ opts.fn = oneway_merge;
+ else if (i == 2) {
+ opts.fn = twoway_merge;
+ opts.initial_checkout = is_index_unborn(r->index);
+ } else if (i >= 3) {
+ opts.fn = threeway_merge;
+ opts.head_idx = i - 1;
+ }
+
+ if (unpack_trees(i, t, &opts))
+ goto out;
+
+ puts(_("Trying simple merge."));
+ write_locked_index(r->index, &lock, COMMIT_LOCK);
+
+ if (write_index_as_tree(&oid, r->index, r->index_file,
+ WRITE_TREE_SILENT, NULL)) {
+ int ret;
+
+ puts(_("Simple merge failed, trying Automatic merge."));
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+ ret = merge_all_index(r->index, 0, 0, merge_one_file_func, r);
+
+ write_locked_index(r->index, &lock, COMMIT_LOCK);
+ return !!ret;
+ }
+
+ return 0;
+
+ out:
+ rollback_lock_file(&lock);
+ return 2;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
index b69a12b390..4f996261b4 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -1,6 +1,7 @@
#ifndef MERGE_STRATEGIES_H
#define MERGE_STRATEGIES_H
+#include "commit.h"
#include "object.h"
int merge_three_way(struct repository *r,
@@ -32,4 +33,8 @@ int merge_index_path(struct index_state *istate, int oneshot, int quiet,
int merge_all_index(struct index_state *istate, int oneshot, int quiet,
merge_fn fn, void *data);
+int merge_strategies_resolve(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remote);
+
#endif /* MERGE_STRATEGIES_H */
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v4 07/12] merge-recursive: move better_branch_name() to merge.c
2020-11-13 11:04 ` [PATCH v4 00/12] " Alban Gruin
` (5 preceding siblings ...)
2020-11-13 11:04 ` [PATCH v4 06/12] merge-resolve: rewrite in C Alban Gruin
@ 2020-11-13 11:04 ` Alban Gruin
2020-11-13 11:04 ` [PATCH v4 08/12] merge-octopus: rewrite in C Alban Gruin
` (5 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-13 11:04 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
better_branch_name() will be used by merge-octopus once it is rewritten
in C, so instead of duplicating it, this moves this function
preventively inside an appropriate file in libgit.a. This function is
also renamed to reflect its usage by merge strategies.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge-recursive.c | 16 ++--------------
cache.h | 2 +-
merge.c | 12 ++++++++++++
3 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/builtin/merge-recursive.c b/builtin/merge-recursive.c
index a4bfd8fc51..972243b5e9 100644
--- a/builtin/merge-recursive.c
+++ b/builtin/merge-recursive.c
@@ -8,18 +8,6 @@
static const char builtin_merge_recursive_usage[] =
"git %s <base>... -- <head> <remote> ...";
-static char *better_branch_name(const char *branch)
-{
- static char githead_env[8 + GIT_MAX_HEXSZ + 1];
- char *name;
-
- if (strlen(branch) != the_hash_algo->hexsz)
- return xstrdup(branch);
- xsnprintf(githead_env, sizeof(githead_env), "GITHEAD_%s", branch);
- name = getenv(githead_env);
- return xstrdup(name ? name : branch);
-}
-
int cmd_merge_recursive(int argc, const char **argv, const char *prefix)
{
const struct object_id *bases[21];
@@ -75,8 +63,8 @@ int cmd_merge_recursive(int argc, const char **argv, const char *prefix)
if (get_oid(o.branch2, &h2))
die(_("could not resolve ref '%s'"), o.branch2);
- o.branch1 = better1 = better_branch_name(o.branch1);
- o.branch2 = better2 = better_branch_name(o.branch2);
+ o.branch1 = better1 = merge_get_better_branch_name(o.branch1);
+ o.branch2 = better2 = merge_get_better_branch_name(o.branch2);
if (o.verbosity >= 3)
printf(_("Merging %s with %s\n"), o.branch1, o.branch2);
diff --git a/cache.h b/cache.h
index be16ab3215..2d844576ea 100644
--- a/cache.h
+++ b/cache.h
@@ -1933,7 +1933,7 @@ int checkout_fast_forward(struct repository *r,
const struct object_id *from,
const struct object_id *to,
int overwrite_ignore);
-
+char *merge_get_better_branch_name(const char *branch);
int sane_execvp(const char *file, char *const argv[]);
diff --git a/merge.c b/merge.c
index 5fb88af102..801d673c5f 100644
--- a/merge.c
+++ b/merge.c
@@ -109,3 +109,15 @@ int checkout_fast_forward(struct repository *r,
return error(_("unable to write new index file"));
return 0;
}
+
+char *merge_get_better_branch_name(const char *branch)
+{
+ static char githead_env[8 + GIT_MAX_HEXSZ + 1];
+ char *name;
+
+ if (strlen(branch) != the_hash_algo->hexsz)
+ return xstrdup(branch);
+ xsnprintf(githead_env, sizeof(githead_env), "GITHEAD_%s", branch);
+ name = getenv(githead_env);
+ return xstrdup(name ? name : branch);
+}
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v4 08/12] merge-octopus: rewrite in C
2020-11-13 11:04 ` [PATCH v4 00/12] " Alban Gruin
` (6 preceding siblings ...)
2020-11-13 11:04 ` [PATCH v4 07/12] merge-recursive: move better_branch_name() to merge.c Alban Gruin
@ 2020-11-13 11:04 ` Alban Gruin
2020-11-13 11:04 ` [PATCH v4 09/12] merge: use the "resolve" strategy without forking Alban Gruin
` (4 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-13 11:04 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
This rewrites `git merge-octopus' from shell to C. As for the two last
conversions, this port removes calls to external processes to avoid
reading and writing the index over and over again.
- Calls to `read-tree -u -m (--aggressive)?' are replaced by calls to
unpack_trees().
- The call to `write-tree' is replaced by a call to
write_index_as_tree().
- The call to `diff-index ...' is replaced by a call to
repo_index_has_changes().
- The call to `merge-index', needed to invoke `git merge-one-file', is
replaced by a call to merge_all_index().
The index is read in cmd_merge_octopus(), and is wrote back by
merge_strategies_octopus().
Here to, merge_strategies_octopus() takes two commit lists and a string
to reduce frictions when try_merge_strategies() will be modified to call
it directly.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Makefile | 2 +-
builtin.h | 1 +
builtin/merge-octopus.c | 69 ++++++++++++++
git-merge-octopus.sh | 112 ----------------------
git.c | 1 +
merge-strategies.c | 204 ++++++++++++++++++++++++++++++++++++++++
merge-strategies.h | 3 +
7 files changed, 279 insertions(+), 113 deletions(-)
create mode 100644 builtin/merge-octopus.c
delete mode 100755 git-merge-octopus.sh
diff --git a/Makefile b/Makefile
index 3cc6b192f1..2b2bdffafe 100644
--- a/Makefile
+++ b/Makefile
@@ -600,7 +600,6 @@ unexport CDPATH
SCRIPT_SH += git-bisect.sh
SCRIPT_SH += git-difftool--helper.sh
SCRIPT_SH += git-filter-branch.sh
-SCRIPT_SH += git-merge-octopus.sh
SCRIPT_SH += git-mergetool.sh
SCRIPT_SH += git-quiltimport.sh
SCRIPT_SH += git-request-pull.sh
@@ -1093,6 +1092,7 @@ BUILTIN_OBJS += builtin/mailsplit.o
BUILTIN_OBJS += builtin/merge-base.o
BUILTIN_OBJS += builtin/merge-file.o
BUILTIN_OBJS += builtin/merge-index.o
+BUILTIN_OBJS += builtin/merge-octopus.o
BUILTIN_OBJS += builtin/merge-one-file.o
BUILTIN_OBJS += builtin/merge-ours.o
BUILTIN_OBJS += builtin/merge-recursive.o
diff --git a/builtin.h b/builtin.h
index 35e91c16d0..50225404a0 100644
--- a/builtin.h
+++ b/builtin.h
@@ -176,6 +176,7 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix);
int cmd_merge(int argc, const char **argv, const char *prefix);
int cmd_merge_base(int argc, const char **argv, const char *prefix);
int cmd_merge_index(int argc, const char **argv, const char *prefix);
+int cmd_merge_octopus(int argc, const char **argv, const char *prefix);
int cmd_merge_ours(int argc, const char **argv, const char *prefix);
int cmd_merge_file(int argc, const char **argv, const char *prefix);
int cmd_merge_one_file(int argc, const char **argv, const char *prefix);
diff --git a/builtin/merge-octopus.c b/builtin/merge-octopus.c
new file mode 100644
index 0000000000..ca8f9f345d
--- /dev/null
+++ b/builtin/merge-octopus.c
@@ -0,0 +1,69 @@
+/*
+ * Builtin "git merge-octopus"
+ *
+ * Copyright (c) 2020 Alban Gruin
+ *
+ * Based on git-merge-octopus.sh, written by Junio C Hamano.
+ *
+ * Resolve two or more trees.
+ */
+
+#define USE_THE_INDEX_COMPATIBILITY_MACROS
+#include "cache.h"
+#include "builtin.h"
+#include "commit.h"
+#include "merge-strategies.h"
+
+static const char builtin_merge_octopus_usage[] =
+ "git merge-octopus [<bases>...] -- <head> <remote1> <remote2> [<remotes>...]";
+
+int cmd_merge_octopus(int argc, const char **argv, const char *prefix)
+{
+ int i, sep_seen = 0;
+ struct commit_list *bases = NULL, *remotes = NULL;
+ struct commit_list **next_base = &bases, **next_remote = &remotes;
+ const char *head_arg = NULL;
+
+ if (argc < 5)
+ usage(builtin_merge_octopus_usage);
+
+ setup_work_tree();
+ if (read_cache() < 0)
+ die("invalid index");
+
+ /*
+ * The first parameters up to -- are merge bases; the rest are
+ * heads.
+ */
+ for (i = 1; i < argc; i++) {
+ if (strcmp(argv[i], "--") == 0)
+ sep_seen = 1;
+ else if (strcmp(argv[i], "-h") == 0)
+ usage(builtin_merge_octopus_usage);
+ else if (sep_seen && !head_arg)
+ head_arg = argv[i];
+ else {
+ struct object_id oid;
+ struct commit *commit;
+
+ if (get_oid(argv[i], &oid))
+ die("object %s not found.", argv[i]);
+
+ commit = lookup_commit_or_die(&oid, argv[i]);
+
+ if (sep_seen)
+ next_remote = commit_list_append(commit, next_remote);
+ else
+ next_base = commit_list_append(commit, next_base);
+ }
+ }
+
+ /*
+ * Reject if this is not an octopus -- resolve should be used
+ * instead.
+ */
+ if (commit_list_count(remotes) < 2)
+ return 2;
+
+ return merge_strategies_octopus(the_repository, bases, head_arg, remotes);
+}
diff --git a/git-merge-octopus.sh b/git-merge-octopus.sh
deleted file mode 100755
index 7d19d37951..0000000000
--- a/git-merge-octopus.sh
+++ /dev/null
@@ -1,112 +0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2005 Junio C Hamano
-#
-# Resolve two or more trees.
-#
-
-. git-sh-setup
-
-LF='
-'
-
-# The first parameters up to -- are merge bases; the rest are heads.
-bases= head= remotes= sep_seen=
-for arg
-do
- case ",$sep_seen,$head,$arg," in
- *,--,)
- sep_seen=yes
- ;;
- ,yes,,*)
- head=$arg
- ;;
- ,yes,*)
- remotes="$remotes$arg "
- ;;
- *)
- bases="$bases$arg "
- ;;
- esac
-done
-
-# Reject if this is not an octopus -- resolve should be used instead.
-case "$remotes" in
-?*' '?*)
- ;;
-*)
- exit 2 ;;
-esac
-
-# MRC is the current "merge reference commit"
-# MRT is the current "merge result tree"
-
-if ! git diff-index --quiet --cached HEAD --
-then
- gettextln "Error: Your local changes to the following files would be overwritten by merge"
- git diff-index --cached --name-only HEAD -- | sed -e 's/^/ /'
- exit 2
-fi
-MRC=$(git rev-parse --verify -q $head)
-MRT=$(git write-tree)
-NON_FF_MERGE=0
-OCTOPUS_FAILURE=0
-for SHA1 in $remotes
-do
- case "$OCTOPUS_FAILURE" in
- 1)
- # We allow only last one to have a hand-resolvable
- # conflicts. Last round failed and we still had
- # a head to merge.
- gettextln "Automated merge did not work."
- gettextln "Should not be doing an octopus."
- exit 2
- esac
-
- eval pretty_name=\${GITHEAD_$SHA1:-$SHA1}
- if test "$SHA1" = "$pretty_name"
- then
- SHA1_UP="$(echo "$SHA1" | tr a-z A-Z)"
- eval pretty_name=\${GITHEAD_$SHA1_UP:-$pretty_name}
- fi
- common=$(git merge-base --all $SHA1 $MRC) ||
- die "$(eval_gettext "Unable to find common commit with \$pretty_name")"
-
- case "$LF$common$LF" in
- *"$LF$SHA1$LF"*)
- eval_gettextln "Already up to date with \$pretty_name"
- continue
- ;;
- esac
-
- if test "$common,$NON_FF_MERGE" = "$MRC,0"
- then
- # The first head being merged was a fast-forward.
- # Advance MRC to the head being merged, and use that
- # tree as the intermediate result of the merge.
- # We still need to count this as part of the parent set.
-
- eval_gettextln "Fast-forwarding to: \$pretty_name"
- git read-tree -u -m $head $SHA1 || exit
- MRC=$SHA1 MRT=$(git write-tree)
- continue
- fi
-
- NON_FF_MERGE=1
-
- eval_gettextln "Trying simple merge with \$pretty_name"
- git read-tree -u -m --aggressive $common $MRT $SHA1 || exit 2
- next=$(git write-tree 2>/dev/null)
- if test $? -ne 0
- then
- gettextln "Simple merge did not work, trying automatic merge."
- git merge-index -o git-merge-one-file -a ||
- OCTOPUS_FAILURE=1
- next=$(git write-tree 2>/dev/null)
- fi
-
- MRC="$MRC $SHA1"
- MRT=$next
-done
-
-exit "$OCTOPUS_FAILURE"
diff --git a/git.c b/git.c
index 64a1a1de41..d51fb5d2bf 100644
--- a/git.c
+++ b/git.c
@@ -539,6 +539,7 @@ static struct cmd_struct commands[] = {
{ "merge-base", cmd_merge_base, RUN_SETUP },
{ "merge-file", cmd_merge_file, RUN_SETUP_GENTLY },
{ "merge-index", cmd_merge_index, RUN_SETUP | NO_PARSEOPT },
+ { "merge-octopus", cmd_merge_octopus, RUN_SETUP | NO_PARSEOPT },
{ "merge-ours", cmd_merge_ours, RUN_SETUP | NO_PARSEOPT },
{ "merge-one-file", cmd_merge_one_file, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
diff --git a/merge-strategies.c b/merge-strategies.c
index 2b34ea0b76..2ae27f4a80 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -1,5 +1,6 @@
#include "cache.h"
#include "cache-tree.h"
+#include "commit-reach.h"
#include "dir.h"
#include "lockfile.h"
#include "merge-strategies.h"
@@ -370,3 +371,206 @@ int merge_strategies_resolve(struct repository *r,
rollback_lock_file(&lock);
return 2;
}
+
+static int fast_forward(struct repository *r, const struct object_id *oids,
+ int nr, int aggressive)
+{
+ int i;
+ struct tree_desc t[MAX_UNPACK_TREES];
+ struct unpack_trees_options opts;
+ struct lock_file lock = LOCK_INIT;
+
+ repo_read_index_preload(r, NULL, 0);
+ if (refresh_index(r->index, REFRESH_QUIET, NULL, NULL, NULL))
+ return -1;
+
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+
+ memset(&opts, 0, sizeof(opts));
+ opts.head_idx = 1;
+ opts.src_index = r->index;
+ opts.dst_index = r->index;
+ opts.merge = 1;
+ opts.update = 1;
+ opts.aggressive = aggressive;
+
+ for (i = 0; i < nr; i++) {
+ struct tree *tree;
+ tree = parse_tree_indirect(oids + i);
+ if (parse_tree(tree))
+ return -1;
+ init_tree_desc(t + i, tree->buffer, tree->size);
+ }
+
+ if (nr == 1)
+ opts.fn = oneway_merge;
+ else if (nr == 2) {
+ opts.fn = twoway_merge;
+ opts.initial_checkout = is_index_unborn(r->index);
+ } else if (nr >= 3) {
+ opts.fn = threeway_merge;
+ opts.head_idx = nr - 1;
+ }
+
+ if (unpack_trees(nr, t, &opts))
+ return -1;
+
+ if (write_locked_index(r->index, &lock, COMMIT_LOCK))
+ return error(_("unable to write new index file"));
+
+ return 0;
+}
+
+static int write_tree(struct repository *r, struct tree **reference_tree)
+{
+ struct object_id oid;
+ int ret;
+
+ ret = write_index_as_tree(&oid, r->index, r->index_file, 0, NULL);
+ if (!ret)
+ *reference_tree = lookup_tree(r, &oid);
+
+ return ret;
+}
+
+int merge_strategies_octopus(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remotes)
+{
+ int non_ff_merge = 0, ret = 0, references = 1;
+ struct commit **reference_commit;
+ struct tree *reference_tree;
+ struct commit_list *j;
+ struct object_id head;
+ struct strbuf sb = STRBUF_INIT;
+
+ get_oid(head_arg, &head);
+
+ reference_commit = xcalloc(commit_list_count(remotes) + 1, sizeof(struct commit *));
+ reference_commit[0] = lookup_commit_reference(r, &head);
+ reference_tree = repo_get_commit_tree(r, reference_commit[0]);
+
+ if (repo_index_has_changes(r, reference_tree, &sb)) {
+ error(_("Your local changes to the following files "
+ "would be overwritten by merge:\n %s"),
+ sb.buf);
+ strbuf_release(&sb);
+ ret = 2;
+ goto out;
+ }
+
+ for (j = remotes; j && j->item; j = j->next) {
+ struct commit *c = j->item;
+ struct object_id *oid = &c->object.oid;
+ struct commit_list *common, *k;
+ char *branch_name;
+ int can_ff = 1;
+
+ if (ret) {
+ /*
+ * We allow only last one to have a
+ * hand-resolvable conflicts. Last round failed
+ * and we still had a head to merge.
+ */
+ puts(_("Automated merge did not work."));
+ puts(_("Should not be doing an octopus."));
+
+ ret = 2;
+ goto out;
+ }
+
+ branch_name = merge_get_better_branch_name(oid_to_hex(oid));
+ common = get_merge_bases_many(c, references, reference_commit);
+
+ if (!common)
+ die(_("Unable to find common commit with %s"), branch_name);
+
+ for (k = common; k && !oideq(&k->item->object.oid, oid); k = k->next);
+
+ if (k) {
+ printf(_("Already up to date with %s\n"), branch_name);
+ free(branch_name);
+ free_commit_list(common);
+ continue;
+ }
+
+ if (!non_ff_merge) {
+ int i;
+
+ for (i = 0, k = common; k && i < references && can_ff; k = k->next, i++) {
+ can_ff = oideq(&k->item->object.oid,
+ &reference_commit[i]->object.oid);
+ }
+ }
+
+ if (!non_ff_merge && can_ff) {
+ /*
+ * The first head being merged was a
+ * fast-forward. Advance the reference commit
+ * to the head being merged, and use that tree
+ * as the intermediate result of the merge. We
+ * still need to count this as part of the
+ * parent set.
+ */
+ struct object_id oids[2];
+ printf(_("Fast-forwarding to: %s\n"), branch_name);
+
+ oidcpy(oids, &head);
+ oidcpy(oids + 1, oid);
+
+ ret = fast_forward(r, oids, 2, 0);
+ if (ret) {
+ free(branch_name);
+ free_commit_list(common);
+ goto out;
+ }
+
+ references = 0;
+ write_tree(r, &reference_tree);
+ } else {
+ int i = 0;
+ struct tree *next = NULL;
+ struct object_id oids[MAX_UNPACK_TREES];
+
+ non_ff_merge = 1;
+ printf(_("Trying simple merge with %s\n"), branch_name);
+
+ for (k = common; k; k = k->next)
+ oidcpy(oids + (i++), &k->item->object.oid);
+
+ oidcpy(oids + (i++), &reference_tree->object.oid);
+ oidcpy(oids + (i++), oid);
+
+ if (fast_forward(r, oids, i, 1)) {
+ ret = 2;
+
+ free(branch_name);
+ free_commit_list(common);
+
+ goto out;
+ }
+
+ if (write_tree(r, &next)) {
+ struct lock_file lock = LOCK_INIT;
+
+ puts(_("Simple merge did not work, trying automatic merge."));
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+ ret = !!merge_all_index(r->index, 0, 0, merge_one_file_func, r);
+ write_locked_index(r->index, &lock, COMMIT_LOCK);
+
+ write_tree(r, &next);
+ }
+
+ reference_tree = next;
+ }
+
+ reference_commit[references++] = c;
+
+ free(branch_name);
+ free_commit_list(common);
+ }
+
+out:
+ free(reference_commit);
+ return ret;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
index 4f996261b4..05232a5a89 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -36,5 +36,8 @@ int merge_all_index(struct index_state *istate, int oneshot, int quiet,
int merge_strategies_resolve(struct repository *r,
struct commit_list *bases, const char *head_arg,
struct commit_list *remote);
+int merge_strategies_octopus(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remote);
#endif /* MERGE_STRATEGIES_H */
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v4 09/12] merge: use the "resolve" strategy without forking
2020-11-13 11:04 ` [PATCH v4 00/12] " Alban Gruin
` (7 preceding siblings ...)
2020-11-13 11:04 ` [PATCH v4 08/12] merge-octopus: rewrite in C Alban Gruin
@ 2020-11-13 11:04 ` Alban Gruin
2020-11-13 11:04 ` [PATCH v4 10/12] merge: use the "octopus" " Alban Gruin
` (3 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-13 11:04 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
This teaches `git merge' to invoke the "resolve" strategy with a
function call instead of forking.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge.c | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/builtin/merge.c b/builtin/merge.c
index 9d5359edc2..ddfefd8ce3 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -41,6 +41,7 @@
#include "commit-reach.h"
#include "wt-status.h"
#include "commit-graph.h"
+#include "merge-strategies.h"
#define DEFAULT_TWOHEAD (1<<0)
#define DEFAULT_OCTOPUS (1<<1)
@@ -740,7 +741,10 @@ static int try_merge_strategy(const char *strategy, struct commit_list *common,
COMMIT_LOCK | SKIP_IF_UNCHANGED))
die(_("unable to write %s"), get_index_file());
return clean ? 0 : 1;
- } else {
+ } else if (!strcmp(strategy, "resolve"))
+ return merge_strategies_resolve(the_repository, common,
+ head_arg, remoteheads);
+ else {
return try_merge_command(the_repository,
strategy, xopts_nr, xopts,
common, head_arg, remoteheads);
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v4 10/12] merge: use the "octopus" strategy without forking
2020-11-13 11:04 ` [PATCH v4 00/12] " Alban Gruin
` (8 preceding siblings ...)
2020-11-13 11:04 ` [PATCH v4 09/12] merge: use the "resolve" strategy without forking Alban Gruin
@ 2020-11-13 11:04 ` Alban Gruin
2020-11-13 11:04 ` [PATCH v4 11/12] sequencer: use the "resolve" " Alban Gruin
` (2 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-13 11:04 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
This teaches `git merge' to invoke the "octopus" strategy with a
function call instead of forking.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/builtin/merge.c b/builtin/merge.c
index ddfefd8ce3..02a2367647 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -744,6 +744,9 @@ static int try_merge_strategy(const char *strategy, struct commit_list *common,
} else if (!strcmp(strategy, "resolve"))
return merge_strategies_resolve(the_repository, common,
head_arg, remoteheads);
+ else if (!strcmp(strategy, "octopus"))
+ return merge_strategies_octopus(the_repository, common,
+ head_arg, remoteheads);
else {
return try_merge_command(the_repository,
strategy, xopts_nr, xopts,
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v4 11/12] sequencer: use the "resolve" strategy without forking
2020-11-13 11:04 ` [PATCH v4 00/12] " Alban Gruin
` (9 preceding siblings ...)
2020-11-13 11:04 ` [PATCH v4 10/12] merge: use the "octopus" " Alban Gruin
@ 2020-11-13 11:04 ` Alban Gruin
2020-11-13 11:04 ` [PATCH v4 12/12] sequencer: use the "octopus" merge " Alban Gruin
2020-11-16 10:21 ` [PATCH v5 00/12] Rewrite the remaining merge strategies from shell to C Alban Gruin
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-13 11:04 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
This teaches the sequencer to invoke the "resolve" strategy with a
function call instead of forking.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
sequencer.c | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/sequencer.c b/sequencer.c
index e8676e965f..ff411d54af 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -33,6 +33,7 @@
#include "commit-reach.h"
#include "rebase-interactive.h"
#include "reset.h"
+#include "merge-strategies.h"
#define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
@@ -2000,9 +2001,15 @@ static int do_pick_commit(struct repository *r,
commit_list_insert(base, &common);
commit_list_insert(next, &remotes);
- res |= try_merge_command(r, opts->strategy,
- opts->xopts_nr, (const char **)opts->xopts,
- common, oid_to_hex(&head), remotes);
+
+ if (!strcmp(opts->strategy, "resolve")) {
+ repo_read_index(r);
+ res |= merge_strategies_resolve(r, common, oid_to_hex(&head), remotes);
+ } else
+ res |= try_merge_command(r, opts->strategy,
+ opts->xopts_nr, (const char **)opts->xopts,
+ common, oid_to_hex(&head), remotes);
+
free_commit_list(common);
free_commit_list(remotes);
}
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v4 12/12] sequencer: use the "octopus" merge strategy without forking
2020-11-13 11:04 ` [PATCH v4 00/12] " Alban Gruin
` (10 preceding siblings ...)
2020-11-13 11:04 ` [PATCH v4 11/12] sequencer: use the "resolve" " Alban Gruin
@ 2020-11-13 11:04 ` Alban Gruin
2020-11-16 10:21 ` [PATCH v5 00/12] Rewrite the remaining merge strategies from shell to C Alban Gruin
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-13 11:04 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
This teaches the sequencer to invoke the "octopus" strategy with a
function call instead of forking.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
sequencer.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/sequencer.c b/sequencer.c
index ff411d54af..746afad930 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2005,6 +2005,9 @@ static int do_pick_commit(struct repository *r,
if (!strcmp(opts->strategy, "resolve")) {
repo_read_index(r);
res |= merge_strategies_resolve(r, common, oid_to_hex(&head), remotes);
+ } else if (!strcmp(opts->strategy, "octopus")) {
+ repo_read_index(r);
+ res |= merge_strategies_octopus(r, common, oid_to_hex(&head), remotes);
} else
res |= try_merge_command(r, opts->strategy,
opts->xopts_nr, (const char **)opts->xopts,
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v5 00/12] Rewrite the remaining merge strategies from shell to C
2020-11-13 11:04 ` [PATCH v4 00/12] " Alban Gruin
` (11 preceding siblings ...)
2020-11-13 11:04 ` [PATCH v4 12/12] sequencer: use the "octopus" merge " Alban Gruin
@ 2020-11-16 10:21 ` Alban Gruin
2020-11-16 10:21 ` [PATCH v5 01/12] t6027: modernise tests Alban Gruin
` (12 more replies)
12 siblings, 13 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-16 10:21 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
In a effort to reduce the number of shell scripts in git's codebase, I
propose this patch series converting the two remaining merge strategies,
resolve and octopus, from shell to C. This will enable slightly better
performance, better integration with git itself (no more forking to
perform these operations), better portability (Windows and shell scripts
don't mix well).
Three scripts are actually converted: first git-merge-one-file.sh, then
git-merge-resolve.sh, and finally git-merge-octopus.sh. Not only they
are converted, but they also are modified to operate without forking,
and then libified so they can be used by git without spawning another
process.
The first patch is not important to make the whole series work, but I
made this patch while working on it.
This series keeps the commands `git merge-one-file', `git
merge-resolve', and `git merge-octopus', so any script depending on them
should keep working without any changes.
This series is based on 306ee63a70 (Eighteenth batch, 2020-09-29). The
tip is tagged as "rewrite-merge-strategies-v5" at
https://github.com/agrn/git.
Changes since v4:
- [3/12] Split long lines to 80 characters max.
- [6/12, 8/12] Define fast_forward() when rewriting `merge-resolve'
instead of `merge-octopus' and use it in merge_strategies_resolve()
to reduce code duplication. This version takes a list `tree_desc'
instead of a list of oids.
- [6/12, 8/12] Rename some variables (eg. i -> nr, j -> i, k -> j).
- [8/12] Rewrote the two loops detecting if the merge was a
fast-forward, or if a step was already up to date, to make only one
less convoluted loop.
- [8/12] Moved the blocks doing a fast-forward and a non-fast-forward
merge to their own functions to make the code simpler. That way,
there is no need to free `branch_name' and `common' each time an
error is handled.
- [8/12] A call to die has been replaced by an error()/return.
- [9/12, 10/12] Reformatted a chain of if/else if/else blocks.
Alban Gruin (12):
t6027: modernise tests
update-index: move add_cacheinfo() to read-cache.c
merge-one-file: rewrite in C
merge-index: libify merge_one_path() and merge_all()
merge-index: don't fork if the requested program is
`git-merge-one-file'
merge-resolve: rewrite in C
merge-recursive: move better_branch_name() to merge.c
merge-octopus: rewrite in C
merge: use the "resolve" strategy without forking
merge: use the "octopus" strategy without forking
sequencer: use the "resolve" strategy without forking
sequencer: use the "octopus" merge strategy without forking
Makefile | 7 +-
builtin.h | 3 +
builtin/merge-index.c | 102 ++----
builtin/merge-octopus.c | 69 ++++
builtin/merge-one-file.c | 94 ++++++
builtin/merge-recursive.c | 16 +-
builtin/merge-resolve.c | 73 +++++
builtin/merge.c | 7 +
builtin/update-index.c | 25 +-
cache.h | 7 +-
git-merge-octopus.sh | 112 -------
git-merge-one-file.sh | 167 ----------
git-merge-resolve.sh | 54 ---
git.c | 3 +
merge-strategies.c | 564 ++++++++++++++++++++++++++++++++
merge-strategies.h | 43 +++
merge.c | 12 +
read-cache.c | 35 ++
sequencer.c | 16 +-
t/t6407-merge-binary.sh | 27 +-
t/t6415-merge-dir-to-symlink.sh | 2 +-
21 files changed, 974 insertions(+), 464 deletions(-)
create mode 100644 builtin/merge-octopus.c
create mode 100644 builtin/merge-one-file.c
create mode 100644 builtin/merge-resolve.c
delete mode 100755 git-merge-octopus.sh
delete mode 100755 git-merge-one-file.sh
delete mode 100755 git-merge-resolve.sh
create mode 100644 merge-strategies.c
create mode 100644 merge-strategies.h
Range-diff against v4:
1: 08c7df596a = 1: 08c7df596a t6027: modernise tests
2: df237da758 = 2: df237da758 update-index: move add_cacheinfo() to read-cache.c
3: b64bad0d23 ! 3: eedddde8ea merge-one-file: rewrite in C
@@ -498,7 +498,8 @@
+ * added so there is nothing to be done, except making
+ * the path merged.
+ */
-+ return add_to_index_cacheinfo(r->index, our_mode, our_blob, path, 0, 1, 1, NULL);
++ return add_to_index_cacheinfo(r->index, our_mode, our_blob,
++ path, 0, 1, 1, NULL);
+ } else if (!orig_blob && !our_blob && their_blob) {
+ struct cache_entry *ce;
+ printf(_("Adding %s\n"), path);
@@ -506,7 +507,8 @@
+ if (file_exists(path))
+ return error(_("untracked %s is overwritten by the merge."), path);
+
-+ if (add_to_index_cacheinfo(r->index, their_mode, their_blob, path, 0, 1, 1, &ce))
++ if (add_to_index_cacheinfo(r->index, their_mode, their_blob,
++ path, 0, 1, 1, &ce))
+ return -1;
+ return checkout_from_index(r->index, path, ce);
+ } else if (!orig_blob && our_blob && their_blob &&
@@ -521,7 +523,8 @@
+
+ printf(_("Adding %s\n"), path);
+
-+ if (add_to_index_cacheinfo(r->index, our_mode, our_blob, path, 0, 1, 1, &ce))
++ if (add_to_index_cacheinfo(r->index, our_mode, our_blob,
++ path, 0, 1, 1, &ce))
+ return -1;
+ return checkout_from_index(r->index, path, ce);
+ } else if (our_blob && their_blob) {
4: c5577dc691 = 4: a9b9942243 merge-index: libify merge_one_path() and merge_all()
5: a0e6cebe89 = 5: 12775907c5 merge-index: don't fork if the requested program is `git-merge-one-file'
6: 94fbc7e286 ! 6: 54a4a12504 merge-resolve: rewrite in C
@@ -235,72 +235,86 @@
return err;
}
+
-+static int add_tree(const struct object_id *oid, struct tree_desc *t)
++static int fast_forward(struct repository *r, struct tree_desc *t,
++ int nr, int aggressive)
+{
-+ struct tree *tree;
-+
-+ tree = parse_tree_indirect(oid);
-+ if (parse_tree(tree))
-+ return -1;
-+
-+ init_tree_desc(t, tree->buffer, tree->size);
-+ return 0;
-+}
-+
-+int merge_strategies_resolve(struct repository *r,
-+ struct commit_list *bases, const char *head_arg,
-+ struct commit_list *remote)
-+{
-+ int i = 0;
-+ struct lock_file lock = LOCK_INIT;
-+ struct tree_desc t[MAX_UNPACK_TREES];
+ struct unpack_trees_options opts;
-+ struct object_id head, oid;
-+ struct commit_list *j;
-+
-+ if (head_arg)
-+ get_oid(head_arg, &head);
++ struct lock_file lock = LOCK_INIT;
+
++ refresh_index(r->index, REFRESH_QUIET, NULL, NULL, NULL);
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
-+ refresh_index(r->index, 0, NULL, NULL, NULL);
+
+ memset(&opts, 0, sizeof(opts));
+ opts.head_idx = 1;
+ opts.src_index = r->index;
+ opts.dst_index = r->index;
-+ opts.update = 1;
+ opts.merge = 1;
-+ opts.aggressive = 1;
++ opts.update = 1;
++ opts.aggressive = aggressive;
+
-+ for (j = bases; j && j->item; j = j->next) {
-+ if (add_tree(&j->item->object.oid, t + (i++)))
-+ goto out;
-+ }
-+
-+ if (head_arg && add_tree(&head, t + (i++)))
-+ goto out;
-+ if (remote && add_tree(&remote->item->object.oid, t + (i++)))
-+ goto out;
-+
-+ if (i == 1)
++ if (nr == 1)
+ opts.fn = oneway_merge;
-+ else if (i == 2) {
++ else if (nr == 2) {
+ opts.fn = twoway_merge;
+ opts.initial_checkout = is_index_unborn(r->index);
-+ } else if (i >= 3) {
++ } else if (nr >= 3) {
+ opts.fn = threeway_merge;
-+ opts.head_idx = i - 1;
++ opts.head_idx = nr - 1;
+ }
+
-+ if (unpack_trees(i, t, &opts))
-+ goto out;
++ if (unpack_trees(nr, t, &opts))
++ return -1;
++
++ if (write_locked_index(r->index, &lock, COMMIT_LOCK))
++ return error(_("unable to write new index file"));
++
++ return 0;
++}
++
++static int add_tree(struct tree *tree, struct tree_desc *t)
++{
++ if (parse_tree(tree))
++ return -1;
++
++ init_tree_desc(t, tree->buffer, tree->size);
++ return 0;
++}
++
++int merge_strategies_resolve(struct repository *r,
++ struct commit_list *bases, const char *head_arg,
++ struct commit_list *remote)
++{
++ struct tree_desc t[MAX_UNPACK_TREES];
++ struct object_id head, oid;
++ struct commit_list *i;
++ int nr = 0;
++
++ if (head_arg)
++ get_oid(head_arg, &head);
+
+ puts(_("Trying simple merge."));
-+ write_locked_index(r->index, &lock, COMMIT_LOCK);
++
++ for (i = bases; i && i->item; i = i->next) {
++ if (add_tree(repo_get_commit_tree(r, i->item), t + (nr++)))
++ return 2;
++ }
++
++ if (head_arg) {
++ struct tree *tree = parse_tree_indirect(&head);
++ if (add_tree(tree, t + (nr++)))
++ return 2;
++ }
++
++ if (remote && add_tree(repo_get_commit_tree(r, remote->item), t + (nr++)))
++ return 2;
++
++ if (fast_forward(r, t, nr, 1))
++ return 2;
+
+ if (write_index_as_tree(&oid, r->index, r->index_file,
+ WRITE_TREE_SILENT, NULL)) {
+ int ret;
++ struct lock_file lock = LOCK_INIT;
+
+ puts(_("Simple merge failed, trying Automatic merge."));
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
@@ -311,10 +325,6 @@
+ }
+
+ return 0;
-+
-+ out:
-+ rollback_lock_file(&lock);
-+ return 2;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
7: b582b7e5d1 = 7: 7c4ad06b95 merge-recursive: move better_branch_name() to merge.c
8: d1936645d5 ! 8: edbe08d41b merge-octopus: rewrite in C
@@ -275,88 +275,107 @@
#include "lockfile.h"
#include "merge-strategies.h"
@@
- rollback_lock_file(&lock);
- return 2;
+
+ return 0;
}
+
-+static int fast_forward(struct repository *r, const struct object_id *oids,
-+ int nr, int aggressive)
-+{
-+ int i;
-+ struct tree_desc t[MAX_UNPACK_TREES];
-+ struct unpack_trees_options opts;
-+ struct lock_file lock = LOCK_INIT;
-+
-+ repo_read_index_preload(r, NULL, 0);
-+ if (refresh_index(r->index, REFRESH_QUIET, NULL, NULL, NULL))
-+ return -1;
-+
-+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
-+
-+ memset(&opts, 0, sizeof(opts));
-+ opts.head_idx = 1;
-+ opts.src_index = r->index;
-+ opts.dst_index = r->index;
-+ opts.merge = 1;
-+ opts.update = 1;
-+ opts.aggressive = aggressive;
-+
-+ for (i = 0; i < nr; i++) {
-+ struct tree *tree;
-+ tree = parse_tree_indirect(oids + i);
-+ if (parse_tree(tree))
-+ return -1;
-+ init_tree_desc(t + i, tree->buffer, tree->size);
-+ }
-+
-+ if (nr == 1)
-+ opts.fn = oneway_merge;
-+ else if (nr == 2) {
-+ opts.fn = twoway_merge;
-+ opts.initial_checkout = is_index_unborn(r->index);
-+ } else if (nr >= 3) {
-+ opts.fn = threeway_merge;
-+ opts.head_idx = nr - 1;
-+ }
-+
-+ if (unpack_trees(nr, t, &opts))
-+ return -1;
-+
-+ if (write_locked_index(r->index, &lock, COMMIT_LOCK))
-+ return error(_("unable to write new index file"));
-+
-+ return 0;
-+}
-+
+static int write_tree(struct repository *r, struct tree **reference_tree)
+{
+ struct object_id oid;
+ int ret;
+
-+ ret = write_index_as_tree(&oid, r->index, r->index_file, 0, NULL);
-+ if (!ret)
++ if (!(ret = write_index_as_tree(&oid, r->index, r->index_file, 0, NULL)))
+ *reference_tree = lookup_tree(r, &oid);
+
+ return ret;
+}
+
++static int octopus_fast_forward(struct repository *r, const char *branch_name,
++ struct tree *tree_head, struct tree *current_tree,
++ struct tree **reference_tree)
++{
++ /*
++ * The first head being merged was a fast-forward. Advance the
++ * reference commit to the head being merged, and use that tree
++ * as the intermediate result of the merge. We still need to
++ * count this as part of the parent set.
++ */
++ struct tree_desc t[2];
++
++ printf(_("Fast-forwarding to: %s\n"), branch_name);
++
++ init_tree_desc(t, tree_head->buffer, tree_head->size);
++ if (add_tree(current_tree, t + 1))
++ return -1;
++ if (fast_forward(r, t, 2, 0))
++ return -1;
++ if (write_tree(r, reference_tree))
++ return -1;
++
++ return 0;
++}
++
++static int octopus_do_merge(struct repository *r, const char *branch_name,
++ struct commit_list *common, struct tree *current_tree,
++ struct tree **reference_tree)
++{
++ struct tree_desc t[MAX_UNPACK_TREES];
++ struct commit_list *j;
++ int nr = 0, ret = 0;
++
++ printf(_("Trying simple merge with %s\n"), branch_name);
++
++ for (j = common; j; j = j->next) {
++ struct tree *tree = repo_get_commit_tree(r, j->item);
++ if (add_tree(tree, t + (nr++)))
++ return -1;
++ }
++
++ if (add_tree(*reference_tree, t + (nr++)))
++ return -1;
++ if (add_tree(current_tree, t + (nr++)))
++ return -1;
++ if (fast_forward(r, t, nr, 1))
++ return -1;
++
++ if (write_tree(r, reference_tree)) {
++ struct lock_file lock = LOCK_INIT;
++
++ puts(_("Simple merge did not work, trying automatic merge."));
++ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
++ ret = merge_all_index(r->index, 0, 0, merge_one_file_func, r);
++ write_locked_index(r->index, &lock, COMMIT_LOCK);
++
++ write_tree(r, reference_tree);
++ }
++
++ return ret ? -2 : 0;
++}
++
+int merge_strategies_octopus(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remotes)
+{
-+ int non_ff_merge = 0, ret = 0, references = 1;
++ int ff_merge = 1, ret = 0, references = 1;
+ struct commit **reference_commit;
-+ struct tree *reference_tree;
-+ struct commit_list *j;
++ struct tree *reference_tree, *tree_head;
++ struct commit_list *i;
+ struct object_id head;
+ struct strbuf sb = STRBUF_INIT;
+
+ get_oid(head_arg, &head);
+
-+ reference_commit = xcalloc(commit_list_count(remotes) + 1, sizeof(struct commit *));
++ reference_commit = xcalloc(commit_list_count(remotes) + 1,
++ sizeof(struct commit *));
+ reference_commit[0] = lookup_commit_reference(r, &head);
+ reference_tree = repo_get_commit_tree(r, reference_commit[0]);
+
++ tree_head = repo_get_commit_tree(r, reference_commit[0]);
++ if (parse_tree(tree_head)) {
++ ret = 2;
++ goto out;
++ }
++
+ if (repo_index_has_changes(r, reference_tree, &sb)) {
+ error(_("Your local changes to the following files "
+ "would be overwritten by merge:\n %s"),
@@ -366,12 +385,13 @@
+ goto out;
+ }
+
-+ for (j = remotes; j && j->item; j = j->next) {
-+ struct commit *c = j->item;
++ for (i = remotes; i && i->item; i = i->next) {
++ struct commit *c = i->item;
+ struct object_id *oid = &c->object.oid;
-+ struct commit_list *common, *k;
++ struct tree *current_tree = repo_get_commit_tree(r, c);
++ struct commit_list *common, *j;
+ char *branch_name;
-+ int can_ff = 1;
++ int k = 0, up_to_date = 0;
+
+ if (ret) {
+ /*
@@ -389,92 +409,47 @@
+ branch_name = merge_get_better_branch_name(oid_to_hex(oid));
+ common = get_merge_bases_many(c, references, reference_commit);
+
-+ if (!common)
-+ die(_("Unable to find common commit with %s"), branch_name);
++ if (!common) {
++ error(_("Unable to find common commit with %s"), branch_name);
+
-+ for (k = common; k && !oideq(&k->item->object.oid, oid); k = k->next);
++ free(branch_name);
++ free_commit_list(common);
+
-+ if (k) {
++ ret = 2;
++ goto out;
++ }
++
++ for (j = common; j && !(up_to_date || !ff_merge); j = j->next) {
++ up_to_date |= oideq(&j->item->object.oid, oid);
++
++ if (k < references)
++ ff_merge &= oideq(&j->item->object.oid, &reference_commit[k++]->object.oid);
++ }
++
++ if (up_to_date) {
+ printf(_("Already up to date with %s\n"), branch_name);
++
+ free(branch_name);
+ free_commit_list(common);
+ continue;
+ }
+
-+ if (!non_ff_merge) {
-+ int i;
-+
-+ for (i = 0, k = common; k && i < references && can_ff; k = k->next, i++) {
-+ can_ff = oideq(&k->item->object.oid,
-+ &reference_commit[i]->object.oid);
-+ }
-+ }
-+
-+ if (!non_ff_merge && can_ff) {
-+ /*
-+ * The first head being merged was a
-+ * fast-forward. Advance the reference commit
-+ * to the head being merged, and use that tree
-+ * as the intermediate result of the merge. We
-+ * still need to count this as part of the
-+ * parent set.
-+ */
-+ struct object_id oids[2];
-+ printf(_("Fast-forwarding to: %s\n"), branch_name);
-+
-+ oidcpy(oids, &head);
-+ oidcpy(oids + 1, oid);
-+
-+ ret = fast_forward(r, oids, 2, 0);
-+ if (ret) {
-+ free(branch_name);
-+ free_commit_list(common);
-+ goto out;
-+ }
-+
++ if (ff_merge) {
++ ret = octopus_fast_forward(r, branch_name, tree_head,
++ current_tree, &reference_tree);
+ references = 0;
-+ write_tree(r, &reference_tree);
+ } else {
-+ int i = 0;
-+ struct tree *next = NULL;
-+ struct object_id oids[MAX_UNPACK_TREES];
-+
-+ non_ff_merge = 1;
-+ printf(_("Trying simple merge with %s\n"), branch_name);
-+
-+ for (k = common; k; k = k->next)
-+ oidcpy(oids + (i++), &k->item->object.oid);
-+
-+ oidcpy(oids + (i++), &reference_tree->object.oid);
-+ oidcpy(oids + (i++), oid);
-+
-+ if (fast_forward(r, oids, i, 1)) {
-+ ret = 2;
-+
-+ free(branch_name);
-+ free_commit_list(common);
-+
-+ goto out;
-+ }
-+
-+ if (write_tree(r, &next)) {
-+ struct lock_file lock = LOCK_INIT;
-+
-+ puts(_("Simple merge did not work, trying automatic merge."));
-+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
-+ ret = !!merge_all_index(r->index, 0, 0, merge_one_file_func, r);
-+ write_locked_index(r->index, &lock, COMMIT_LOCK);
-+
-+ write_tree(r, &next);
-+ }
-+
-+ reference_tree = next;
++ ret = octopus_do_merge(r, branch_name, common,
++ current_tree, &reference_tree);
+ }
+
-+ reference_commit[references++] = c;
-+
+ free(branch_name);
+ free_commit_list(common);
++
++ if (ret == -1)
++ goto out;
++
++ reference_commit[references++] = c;
+ }
+
+out:
9: 26b1a3979c ! 9: e677b27c06 merge: use the "resolve" strategy without forking
@@ -22,11 +22,9 @@
COMMIT_LOCK | SKIP_IF_UNCHANGED))
die(_("unable to write %s"), get_index_file());
return clean ? 0 : 1;
-- } else {
-+ } else if (!strcmp(strategy, "resolve"))
++ } else if (!strcmp(strategy, "resolve")) {
+ return merge_strategies_resolve(the_repository, common,
+ head_arg, remoteheads);
-+ else {
+ } else {
return try_merge_command(the_repository,
strategy, xopts_nr, xopts,
- common, head_arg, remoteheads);
10: 23bc9824df ! 10: 963f316fd6 merge: use the "octopus" strategy without forking
@@ -11,12 +11,12 @@
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@
- } else if (!strcmp(strategy, "resolve"))
+ } else if (!strcmp(strategy, "resolve")) {
return merge_strategies_resolve(the_repository, common,
head_arg, remoteheads);
-+ else if (!strcmp(strategy, "octopus"))
++ } else if (!strcmp(strategy, "octopus")) {
+ return merge_strategies_octopus(the_repository, common,
+ head_arg, remoteheads);
- else {
+ } else {
return try_merge_command(the_repository,
strategy, xopts_nr, xopts,
11: 3a340f5984 = 11: 0ad967a7e5 sequencer: use the "resolve" strategy without forking
12: ce3723cf34 = 12: 3814f61717 sequencer: use the "octopus" merge strategy without forking
--
2.20.1
^ permalink raw reply [flat|nested] 221+ messages in thread
* [PATCH v5 01/12] t6027: modernise tests
2020-11-16 10:21 ` [PATCH v5 00/12] Rewrite the remaining merge strategies from shell to C Alban Gruin
@ 2020-11-16 10:21 ` Alban Gruin
2020-11-16 10:21 ` [PATCH v5 02/12] update-index: move add_cacheinfo() to read-cache.c Alban Gruin
` (11 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-16 10:21 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
Some tests in t6027 uses a if/then/else to check if a command failed or
not, but we have the `test_must_fail' function to do it correctly for us
nowadays.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
t/t6407-merge-binary.sh | 27 ++++++---------------------
1 file changed, 6 insertions(+), 21 deletions(-)
diff --git a/t/t6407-merge-binary.sh b/t/t6407-merge-binary.sh
index 4e6c7cb77e..071d3f7343 100755
--- a/t/t6407-merge-binary.sh
+++ b/t/t6407-merge-binary.sh
@@ -5,7 +5,6 @@ test_description='ask merge-recursive to merge binary files'
. ./test-lib.sh
test_expect_success setup '
-
cat "$TEST_DIRECTORY"/test-binary-1.png >m &&
git add m &&
git ls-files -s | sed -e "s/ 0 / 1 /" >E1 &&
@@ -35,33 +34,19 @@ test_expect_success setup '
'
test_expect_success resolve '
-
rm -f a* m* &&
git reset --hard anchor &&
-
- if git merge -s resolve master
- then
- echo Oops, should not have succeeded
- false
- else
- git ls-files -s >current
- test_cmp expect current
- fi
+ test_must_fail git merge -s resolve master &&
+ git ls-files -s >current &&
+ test_cmp expect current
'
test_expect_success recursive '
-
rm -f a* m* &&
git reset --hard anchor &&
-
- if git merge -s recursive master
- then
- echo Oops, should not have succeeded
- false
- else
- git ls-files -s >current
- test_cmp expect current
- fi
+ test_must_fail git merge -s recursive master &&
+ git ls-files -s >current &&
+ test_cmp expect current
'
test_done
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v5 02/12] update-index: move add_cacheinfo() to read-cache.c
2020-11-16 10:21 ` [PATCH v5 00/12] Rewrite the remaining merge strategies from shell to C Alban Gruin
2020-11-16 10:21 ` [PATCH v5 01/12] t6027: modernise tests Alban Gruin
@ 2020-11-16 10:21 ` Alban Gruin
2020-11-16 10:21 ` [PATCH v5 03/12] merge-one-file: rewrite in C Alban Gruin
` (10 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-16 10:21 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
This moves the function add_cacheinfo() that already exists in
update-index.c to update-index.c, renames it add_to_index_cacheinfo(),
and adds an `istate' parameter. The new cache entry is returned through
a pointer passed in the parameters. The return value is either 0
(success), -1 (invalid path), or -2 (failed to add the file in the
index).
This will become useful in the next commit, when the three-way merge
will need to call this function.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/update-index.c | 25 +++++++------------------
cache.h | 5 +++++
read-cache.c | 35 +++++++++++++++++++++++++++++++++++
3 files changed, 47 insertions(+), 18 deletions(-)
diff --git a/builtin/update-index.c b/builtin/update-index.c
index 79087bccea..44862f5e1d 100644
--- a/builtin/update-index.c
+++ b/builtin/update-index.c
@@ -404,27 +404,16 @@ static int process_path(const char *path, struct stat *st, int stat_errno)
static int add_cacheinfo(unsigned int mode, const struct object_id *oid,
const char *path, int stage)
{
- int len, option;
- struct cache_entry *ce;
+ int res;
- if (!verify_path(path, mode))
- return error("Invalid path '%s'", path);
-
- len = strlen(path);
- ce = make_empty_cache_entry(&the_index, len);
-
- oidcpy(&ce->oid, oid);
- memcpy(ce->name, path, len);
- ce->ce_flags = create_ce_flags(stage);
- ce->ce_namelen = len;
- ce->ce_mode = create_ce_mode(mode);
- if (assume_unchanged)
- ce->ce_flags |= CE_VALID;
- option = allow_add ? ADD_CACHE_OK_TO_ADD : 0;
- option |= allow_replace ? ADD_CACHE_OK_TO_REPLACE : 0;
- if (add_cache_entry(ce, option))
+ res = add_to_index_cacheinfo(&the_index, mode, oid, path, stage,
+ allow_add, allow_replace, NULL);
+ if (res == -1)
+ return res;
+ if (res == -2)
return error("%s: cannot add to the index - missing --add option?",
path);
+
report("add '%s'", path);
return 0;
}
diff --git a/cache.h b/cache.h
index c0072d43b1..be16ab3215 100644
--- a/cache.h
+++ b/cache.h
@@ -830,6 +830,11 @@ int remove_file_from_index(struct index_state *, const char *path);
int add_to_index(struct index_state *, const char *path, struct stat *, int flags);
int add_file_to_index(struct index_state *, const char *path, int flags);
+int add_to_index_cacheinfo(struct index_state *, unsigned int mode,
+ const struct object_id *oid, const char *path,
+ int stage, int allow_add, int allow_replace,
+ struct cache_entry **pce);
+
int chmod_index_entry(struct index_state *, struct cache_entry *ce, char flip);
int ce_same_name(const struct cache_entry *a, const struct cache_entry *b);
void set_object_name_for_intent_to_add_entry(struct cache_entry *ce);
diff --git a/read-cache.c b/read-cache.c
index ecf6f68994..c25f951db4 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -1350,6 +1350,41 @@ int add_index_entry(struct index_state *istate, struct cache_entry *ce, int opti
return 0;
}
+int add_to_index_cacheinfo(struct index_state *istate, unsigned int mode,
+ const struct object_id *oid, const char *path,
+ int stage, int allow_add, int allow_replace,
+ struct cache_entry **pce)
+{
+ int len, option;
+ struct cache_entry *ce = NULL;
+
+ if (!verify_path(path, mode))
+ return error(_("Invalid path '%s'"), path);
+
+ len = strlen(path);
+ ce = make_empty_cache_entry(istate, len);
+
+ oidcpy(&ce->oid, oid);
+ memcpy(ce->name, path, len);
+ ce->ce_flags = create_ce_flags(stage);
+ ce->ce_namelen = len;
+ ce->ce_mode = create_ce_mode(mode);
+ if (assume_unchanged)
+ ce->ce_flags |= CE_VALID;
+ option = allow_add ? ADD_CACHE_OK_TO_ADD : 0;
+ option |= allow_replace ? ADD_CACHE_OK_TO_REPLACE : 0;
+
+ if (add_index_entry(istate, ce, option)) {
+ discard_cache_entry(ce);
+ return -2;
+ }
+
+ if (pce)
+ *pce = ce;
+
+ return 0;
+}
+
/*
* "refresh" does not calculate a new sha1 file or bring the
* cache up-to-date for mode/content changes. But what it
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v5 03/12] merge-one-file: rewrite in C
2020-11-16 10:21 ` [PATCH v5 00/12] Rewrite the remaining merge strategies from shell to C Alban Gruin
2020-11-16 10:21 ` [PATCH v5 01/12] t6027: modernise tests Alban Gruin
2020-11-16 10:21 ` [PATCH v5 02/12] update-index: move add_cacheinfo() to read-cache.c Alban Gruin
@ 2020-11-16 10:21 ` Alban Gruin
2020-11-16 10:21 ` [PATCH v5 04/12] merge-index: libify merge_one_path() and merge_all() Alban Gruin
` (9 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-16 10:21 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
This rewrites `git merge-one-file' from shell to C. This port is not
completely straightforward: to save precious cycles by avoiding reading
and flushing the index repeatedly, write temporary files when an
operation can be performed in-memory, or allow other function to use the
rewrite without forking nor worrying about the index, the calls to
external processes are replaced by calls to functions in libgit.a:
- calls to `update-index --add --cacheinfo' are replaced by calls to
add_to_index_cacheinfo();
- calls to `update-index --remove' are replaced by calls to
remove_file_from_index();
- calls to `checkout-index -u -f' are replaced by calls to
checkout_entry();
- calls to `unpack-file' and `merge-files' are replaced by calls to
read_mmblob() and xdl_merge(), respectively, to merge files
in-memory;
- calls to `checkout-index -f --stage=2' are removed, as this is needed
to have the correct permission bits on the merged file from the
script, but not in the C version;
- calls to `update-index' are replaced by calls to add_file_to_index().
The bulk of the rewrite is done in a new file in libgit.a,
merge-strategies.c. This will enable the resolve and octopus strategies
to directly call it instead of forking.
This also fixes a bug present in the original script: instead of
checking if a _regular_ file exists when a file exists in the branch to
merge, but not in our branch, the rewritten version checks if a file of
any kind (ie. a directory, ...) exists. This fixes the tests t6035.14,
where the branch to merge had a new file, `a/b', but our branch had a
directory there; it should have failed because a directory exists, but
it did not because there was no regular file called `a/b'. This test is
now marked as successful.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Makefile | 3 +-
builtin.h | 1 +
builtin/merge-one-file.c | 94 +++++++++++++++++
git-merge-one-file.sh | 167 ------------------------------
git.c | 1 +
merge-strategies.c | 176 ++++++++++++++++++++++++++++++++
merge-strategies.h | 12 +++
t/t6415-merge-dir-to-symlink.sh | 2 +-
8 files changed, 287 insertions(+), 169 deletions(-)
create mode 100644 builtin/merge-one-file.c
delete mode 100755 git-merge-one-file.sh
create mode 100644 merge-strategies.c
create mode 100644 merge-strategies.h
diff --git a/Makefile b/Makefile
index de53954590..6dfdb33cb2 100644
--- a/Makefile
+++ b/Makefile
@@ -601,7 +601,6 @@ SCRIPT_SH += git-bisect.sh
SCRIPT_SH += git-difftool--helper.sh
SCRIPT_SH += git-filter-branch.sh
SCRIPT_SH += git-merge-octopus.sh
-SCRIPT_SH += git-merge-one-file.sh
SCRIPT_SH += git-merge-resolve.sh
SCRIPT_SH += git-mergetool.sh
SCRIPT_SH += git-quiltimport.sh
@@ -909,6 +908,7 @@ LIB_OBJS += match-trees.o
LIB_OBJS += mem-pool.o
LIB_OBJS += merge-blobs.o
LIB_OBJS += merge-recursive.o
+LIB_OBJS += merge-strategies.o
LIB_OBJS += merge.o
LIB_OBJS += mergesort.o
LIB_OBJS += midx.o
@@ -1094,6 +1094,7 @@ BUILTIN_OBJS += builtin/mailsplit.o
BUILTIN_OBJS += builtin/merge-base.o
BUILTIN_OBJS += builtin/merge-file.o
BUILTIN_OBJS += builtin/merge-index.o
+BUILTIN_OBJS += builtin/merge-one-file.o
BUILTIN_OBJS += builtin/merge-ours.o
BUILTIN_OBJS += builtin/merge-recursive.o
BUILTIN_OBJS += builtin/merge-tree.o
diff --git a/builtin.h b/builtin.h
index 53fb290963..4d2cd78856 100644
--- a/builtin.h
+++ b/builtin.h
@@ -178,6 +178,7 @@ int cmd_merge_base(int argc, const char **argv, const char *prefix);
int cmd_merge_index(int argc, const char **argv, const char *prefix);
int cmd_merge_ours(int argc, const char **argv, const char *prefix);
int cmd_merge_file(int argc, const char **argv, const char *prefix);
+int cmd_merge_one_file(int argc, const char **argv, const char *prefix);
int cmd_merge_recursive(int argc, const char **argv, const char *prefix);
int cmd_merge_tree(int argc, const char **argv, const char *prefix);
int cmd_mktag(int argc, const char **argv, const char *prefix);
diff --git a/builtin/merge-one-file.c b/builtin/merge-one-file.c
new file mode 100644
index 0000000000..9c21778e1d
--- /dev/null
+++ b/builtin/merge-one-file.c
@@ -0,0 +1,94 @@
+/*
+ * Builtin "git merge-one-file"
+ *
+ * Copyright (c) 2020 Alban Gruin
+ *
+ * Based on git-merge-one-file.sh, written by Linus Torvalds.
+ *
+ * This is the git per-file merge utility, called with
+ *
+ * argv[1] - original file object name (or empty)
+ * argv[2] - file in branch1 object name (or empty)
+ * argv[3] - file in branch2 object name (or empty)
+ * argv[4] - pathname in repository
+ * argv[5] - original file mode (or empty)
+ * argv[6] - file in branch1 mode (or empty)
+ * argv[7] - file in branch2 mode (or empty)
+ *
+ * Handle some trivial cases. The _really_ trivial cases have been
+ * handled already by git read-tree, but that one doesn't do any merges
+ * that might change the tree layout.
+ */
+
+#define USE_THE_INDEX_COMPATIBILITY_MACROS
+#include "cache.h"
+#include "builtin.h"
+#include "lockfile.h"
+#include "merge-strategies.h"
+
+static const char builtin_merge_one_file_usage[] =
+ "git merge-one-file <orig blob> <our blob> <their blob> <path> "
+ "<orig mode> <our mode> <their mode>\n\n"
+ "Blob ids and modes should be empty for missing files.";
+
+static int read_mode(const char *name, const char *arg, unsigned int *mode)
+{
+ char *last;
+ int ret = 0;
+
+ *mode = strtol(arg, &last, 8);
+
+ if (*last)
+ ret = error(_("invalid '%s' mode: expected nothing, got '%c'"), name, *last);
+ else if (!(S_ISREG(*mode) || S_ISDIR(*mode) || S_ISLNK(*mode)))
+ ret = error(_("invalid '%s' mode: %o"), name, *mode);
+
+ return ret;
+}
+
+int cmd_merge_one_file(int argc, const char **argv, const char *prefix)
+{
+ struct object_id orig_blob, our_blob, their_blob,
+ *p_orig_blob = NULL, *p_our_blob = NULL, *p_their_blob = NULL;
+ unsigned int orig_mode = 0, our_mode = 0, their_mode = 0, ret = 0;
+ struct lock_file lock = LOCK_INIT;
+
+ if (argc != 8)
+ usage(builtin_merge_one_file_usage);
+
+ if (read_cache() < 0)
+ die("invalid index");
+
+ hold_locked_index(&lock, LOCK_DIE_ON_ERROR);
+
+ if (!get_oid_hex(argv[1], &orig_blob)) {
+ p_orig_blob = &orig_blob;
+ ret = read_mode("orig", argv[5], &orig_mode);
+ } else if (!*argv[1] && *argv[5])
+ ret = error(_("no 'orig' object id given, but a mode was still given."));
+
+ if (!get_oid_hex(argv[2], &our_blob)) {
+ p_our_blob = &our_blob;
+ ret = read_mode("our", argv[6], &our_mode);
+ } else if (!*argv[2] && *argv[6])
+ ret = error(_("no 'our' object id given, but a mode was still given."));
+
+ if (!get_oid_hex(argv[3], &their_blob)) {
+ p_their_blob = &their_blob;
+ ret = read_mode("their", argv[7], &their_mode);
+ } else if (!*argv[3] && *argv[7])
+ ret = error(_("no 'their' object id given, but a mode was still given."));
+
+ if (ret)
+ return ret;
+
+ ret = merge_three_way(the_repository, p_orig_blob, p_our_blob, p_their_blob,
+ argv[4], orig_mode, our_mode, their_mode);
+
+ if (ret) {
+ rollback_lock_file(&lock);
+ return !!ret;
+ }
+
+ return write_locked_index(&the_index, &lock, COMMIT_LOCK);
+}
diff --git a/git-merge-one-file.sh b/git-merge-one-file.sh
deleted file mode 100755
index f6d9852d2f..0000000000
--- a/git-merge-one-file.sh
+++ /dev/null
@@ -1,167 +0,0 @@
-#!/bin/sh
-#
-# Copyright (c) Linus Torvalds, 2005
-#
-# This is the git per-file merge script, called with
-#
-# $1 - original file SHA1 (or empty)
-# $2 - file in branch1 SHA1 (or empty)
-# $3 - file in branch2 SHA1 (or empty)
-# $4 - pathname in repository
-# $5 - original file mode (or empty)
-# $6 - file in branch1 mode (or empty)
-# $7 - file in branch2 mode (or empty)
-#
-# Handle some trivial cases.. The _really_ trivial cases have
-# been handled already by git read-tree, but that one doesn't
-# do any merges that might change the tree layout.
-
-USAGE='<orig blob> <our blob> <their blob> <path>'
-USAGE="$USAGE <orig mode> <our mode> <their mode>"
-LONG_USAGE="usage: git merge-one-file $USAGE
-
-Blob ids and modes should be empty for missing files."
-
-SUBDIRECTORY_OK=Yes
-. git-sh-setup
-cd_to_toplevel
-require_work_tree
-
-if test $# != 7
-then
- echo "$LONG_USAGE"
- exit 1
-fi
-
-case "${1:-.}${2:-.}${3:-.}" in
-#
-# Deleted in both or deleted in one and unchanged in the other
-#
-"$1.." | "$1.$1" | "$1$1.")
- if { test -z "$6" && test "$5" != "$7"; } ||
- { test -z "$7" && test "$5" != "$6"; }
- then
- echo "ERROR: File $4 deleted on one branch but had its" >&2
- echo "ERROR: permissions changed on the other." >&2
- exit 1
- fi
-
- if test -n "$2"
- then
- echo "Removing $4"
- else
- # read-tree checked that index matches HEAD already,
- # so we know we do not have this path tracked.
- # there may be an unrelated working tree file here,
- # which we should just leave unmolested. Make sure
- # we do not have it in the index, though.
- exec git update-index --remove -- "$4"
- fi
- if test -f "$4"
- then
- rm -f -- "$4" &&
- rmdir -p "$(expr "z$4" : 'z\(.*\)/')" 2>/dev/null || :
- fi &&
- exec git update-index --remove -- "$4"
- ;;
-
-#
-# Added in one.
-#
-".$2.")
- # the other side did not add and we added so there is nothing
- # to be done, except making the path merged.
- exec git update-index --add --cacheinfo "$6" "$2" "$4"
- ;;
-"..$3")
- echo "Adding $4"
- if test -f "$4"
- then
- echo "ERROR: untracked $4 is overwritten by the merge." >&2
- exit 1
- fi
- git update-index --add --cacheinfo "$7" "$3" "$4" &&
- exec git checkout-index -u -f -- "$4"
- ;;
-
-#
-# Added in both, identically (check for same permissions).
-#
-".$3$2")
- if test "$6" != "$7"
- then
- echo "ERROR: File $4 added identically in both branches," >&2
- echo "ERROR: but permissions conflict $6->$7." >&2
- exit 1
- fi
- echo "Adding $4"
- git update-index --add --cacheinfo "$6" "$2" "$4" &&
- exec git checkout-index -u -f -- "$4"
- ;;
-
-#
-# Modified in both, but differently.
-#
-"$1$2$3" | ".$2$3")
-
- case ",$6,$7," in
- *,120000,*)
- echo "ERROR: $4: Not merging symbolic link changes." >&2
- exit 1
- ;;
- *,160000,*)
- echo "ERROR: $4: Not merging conflicting submodule changes." >&2
- exit 1
- ;;
- esac
-
- src1=$(git unpack-file $2)
- src2=$(git unpack-file $3)
- case "$1" in
- '')
- echo "Added $4 in both, but differently."
- orig=$(git unpack-file $(git hash-object /dev/null))
- ;;
- *)
- echo "Auto-merging $4"
- orig=$(git unpack-file $1)
- ;;
- esac
-
- git merge-file "$src1" "$orig" "$src2"
- ret=$?
- msg=
- if test $ret != 0 || test -z "$1"
- then
- msg='content conflict'
- ret=1
- fi
-
- # Create the working tree file, using "our tree" version from the
- # index, and then store the result of the merge.
- git checkout-index -f --stage=2 -- "$4" && cat "$src1" >"$4" || exit 1
- rm -f -- "$orig" "$src1" "$src2"
-
- if test "$6" != "$7"
- then
- if test -n "$msg"
- then
- msg="$msg, "
- fi
- msg="${msg}permissions conflict: $5->$6,$7"
- ret=1
- fi
-
- if test $ret != 0
- then
- echo "ERROR: $msg in $4" >&2
- exit 1
- fi
- exec git update-index -- "$4"
- ;;
-
-*)
- echo "ERROR: $4: Not handling case $1 -> $2 -> $3" >&2
- ;;
-esac
-exit 1
diff --git a/git.c b/git.c
index f1e8b56d99..a4d3f98094 100644
--- a/git.c
+++ b/git.c
@@ -540,6 +540,7 @@ static struct cmd_struct commands[] = {
{ "merge-file", cmd_merge_file, RUN_SETUP_GENTLY },
{ "merge-index", cmd_merge_index, RUN_SETUP | NO_PARSEOPT },
{ "merge-ours", cmd_merge_ours, RUN_SETUP | NO_PARSEOPT },
+ { "merge-one-file", cmd_merge_one_file, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-ours", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-theirs", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
diff --git a/merge-strategies.c b/merge-strategies.c
new file mode 100644
index 0000000000..c5576dc891
--- /dev/null
+++ b/merge-strategies.c
@@ -0,0 +1,176 @@
+#include "cache.h"
+#include "dir.h"
+#include "merge-strategies.h"
+#include "xdiff-interface.h"
+
+static int checkout_from_index(struct index_state *istate, const char *path,
+ struct cache_entry *ce)
+{
+ struct checkout state = CHECKOUT_INIT;
+
+ state.istate = istate;
+ state.force = 1;
+ state.base_dir = "";
+ state.base_dir_len = 0;
+
+ if (checkout_entry(ce, &state, NULL, NULL) < 0)
+ return error(_("%s: cannot checkout file"), path);
+ return 0;
+}
+
+static int merge_one_file_deleted(struct index_state *istate,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
+{
+ if ((our_blob && orig_mode != our_mode) ||
+ (their_blob && orig_mode != their_mode))
+ return error(_("File %s deleted on one branch but had its "
+ "permissions changed on the other."), path);
+
+ if (our_blob) {
+ printf(_("Removing %s\n"), path);
+
+ if (file_exists(path))
+ remove_path(path);
+ }
+
+ if (remove_file_from_index(istate, path))
+ return error("%s: cannot remove from the index", path);
+ return 0;
+}
+
+static int do_merge_one_file(struct index_state *istate,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
+{
+ int ret, i, dest;
+ ssize_t written;
+ mmbuffer_t result = {NULL, 0};
+ mmfile_t mmfs[3];
+ xmparam_t xmp = {{0}};
+
+ if (our_mode == S_IFLNK || their_mode == S_IFLNK)
+ return error(_("%s: Not merging symbolic link changes."), path);
+ else if (our_mode == S_IFGITLINK || their_mode == S_IFGITLINK)
+ return error(_("%s: Not merging conflicting submodule changes."), path);
+ else if (our_mode != their_mode)
+ return error(_("permission conflict: %o->%o,%o in %s"),
+ orig_mode, our_mode, their_mode, path);
+
+ if (orig_blob) {
+ printf(_("Auto-merging %s\n"), path);
+ read_mmblob(mmfs + 0, orig_blob);
+ } else {
+ printf(_("Added %s in both, but differently.\n"), path);
+ read_mmblob(mmfs + 0, &null_oid);
+ }
+
+ read_mmblob(mmfs + 1, our_blob);
+ read_mmblob(mmfs + 2, their_blob);
+
+ xmp.level = XDL_MERGE_ZEALOUS_ALNUM;
+ xmp.style = 0;
+ xmp.favor = 0;
+
+ ret = xdl_merge(mmfs + 0, mmfs + 1, mmfs + 2, &xmp, &result);
+
+ for (i = 0; i < 3; i++)
+ free(mmfs[i].ptr);
+
+ if (ret < 0) {
+ free(result.ptr);
+ return error(_("Failed to execute internal merge"));
+ } else if (ret > 0 || !orig_blob) {
+ free(result.ptr);
+ return error(_("content conflict in %s"), path);
+ }
+
+ unlink(path);
+ if ((dest = open(path, O_WRONLY | O_CREAT, our_mode)) < 0) {
+ free(result.ptr);
+ return error_errno(_("failed to open file '%s'"), path);
+ }
+
+ written = write_in_full(dest, result.ptr, result.size);
+ close(dest);
+
+ free(result.ptr);
+
+ if (written < 0)
+ return error_errno(_("failed to write to '%s'"), path);
+
+ return add_file_to_index(istate, path, 0);
+}
+
+int merge_three_way(struct repository *r,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
+{
+ if (orig_blob &&
+ ((!their_blob && our_blob && oideq(orig_blob, our_blob)) ||
+ (!our_blob && their_blob && oideq(orig_blob, their_blob)))) {
+ /* Deleted in both or deleted in one and unchanged in the other. */
+ return merge_one_file_deleted(r->index, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+ } else if (!orig_blob && our_blob && !their_blob) {
+ /*
+ * Added in one. The other side did not add and we
+ * added so there is nothing to be done, except making
+ * the path merged.
+ */
+ return add_to_index_cacheinfo(r->index, our_mode, our_blob,
+ path, 0, 1, 1, NULL);
+ } else if (!orig_blob && !our_blob && their_blob) {
+ struct cache_entry *ce;
+ printf(_("Adding %s\n"), path);
+
+ if (file_exists(path))
+ return error(_("untracked %s is overwritten by the merge."), path);
+
+ if (add_to_index_cacheinfo(r->index, their_mode, their_blob,
+ path, 0, 1, 1, &ce))
+ return -1;
+ return checkout_from_index(r->index, path, ce);
+ } else if (!orig_blob && our_blob && their_blob &&
+ oideq(our_blob, their_blob)) {
+ struct cache_entry *ce;
+
+ /* Added in both, identically (check for same permissions). */
+ if (our_mode != their_mode)
+ return error(_("File %s added identically in both branches, "
+ "but permissions conflict %o->%o."),
+ path, our_mode, their_mode);
+
+ printf(_("Adding %s\n"), path);
+
+ if (add_to_index_cacheinfo(r->index, our_mode, our_blob,
+ path, 0, 1, 1, &ce))
+ return -1;
+ return checkout_from_index(r->index, path, ce);
+ } else if (our_blob && their_blob) {
+ /* Modified in both, but differently. */
+ return do_merge_one_file(r->index,
+ orig_blob, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+ } else {
+ char orig_hex[GIT_MAX_HEXSZ] = {0}, our_hex[GIT_MAX_HEXSZ] = {0},
+ their_hex[GIT_MAX_HEXSZ] = {0};
+
+ if (orig_blob)
+ oid_to_hex_r(orig_hex, orig_blob);
+ if (our_blob)
+ oid_to_hex_r(our_hex, our_blob);
+ if (their_blob)
+ oid_to_hex_r(their_hex, their_blob);
+
+ return error(_("%s: Not handling case %s -> %s -> %s"),
+ path, orig_hex, our_hex, their_hex);
+ }
+
+ return 0;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
new file mode 100644
index 0000000000..e624c4f27c
--- /dev/null
+++ b/merge-strategies.h
@@ -0,0 +1,12 @@
+#ifndef MERGE_STRATEGIES_H
+#define MERGE_STRATEGIES_H
+
+#include "object.h"
+
+int merge_three_way(struct repository *r,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode);
+
+#endif /* MERGE_STRATEGIES_H */
diff --git a/t/t6415-merge-dir-to-symlink.sh b/t/t6415-merge-dir-to-symlink.sh
index 2eddcc7664..5fb74e39a0 100755
--- a/t/t6415-merge-dir-to-symlink.sh
+++ b/t/t6415-merge-dir-to-symlink.sh
@@ -94,7 +94,7 @@ test_expect_success SYMLINKS 'a/b was resolved as symlink' '
test -h a/b
'
-test_expect_failure 'do not lose untracked in merge (resolve)' '
+test_expect_success 'do not lose untracked in merge (resolve)' '
git reset --hard &&
git checkout baseline^0 &&
>a/b/c/e &&
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v5 04/12] merge-index: libify merge_one_path() and merge_all()
2020-11-16 10:21 ` [PATCH v5 00/12] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (2 preceding siblings ...)
2020-11-16 10:21 ` [PATCH v5 03/12] merge-one-file: rewrite in C Alban Gruin
@ 2020-11-16 10:21 ` Alban Gruin
2020-11-16 10:21 ` [PATCH v5 05/12] merge-index: don't fork if the requested program is `git-merge-one-file' Alban Gruin
` (8 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-16 10:21 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
The "resolve" and "octopus" merge strategies do not call directly `git
merge-one-file', they delegate the work to another git command, `git
merge-index', that will loop over files in the index and call the
specified command. Unfortunately, these functions are not part of
libgit.a, which means that once rewritten, the strategies would still
have to invoke `merge-one-file' by spawning a new process first.
To avoid this, this moves and renames merge_one_path(), merge_all(), and
their helpers to merge-strategies.c. They also take a callback to
dictate what they should do for each file. For now, to preserve the
behaviour of `merge-index', only one callback, launching a new process,
is defined.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge-index.c | 77 +++----------------------------
merge-strategies.c | 103 ++++++++++++++++++++++++++++++++++++++++++
merge-strategies.h | 17 +++++++
3 files changed, 127 insertions(+), 70 deletions(-)
diff --git a/builtin/merge-index.c b/builtin/merge-index.c
index 38ea6ad6ca..49e3382fb9 100644
--- a/builtin/merge-index.c
+++ b/builtin/merge-index.c
@@ -1,74 +1,11 @@
#define USE_THE_INDEX_COMPATIBILITY_MACROS
#include "builtin.h"
-#include "run-command.h"
-
-static const char *pgm;
-static int one_shot, quiet;
-static int err;
-
-static int merge_entry(int pos, const char *path)
-{
- int found;
- const char *arguments[] = { pgm, "", "", "", path, "", "", "", NULL };
- char hexbuf[4][GIT_MAX_HEXSZ + 1];
- char ownbuf[4][60];
-
- if (pos >= active_nr)
- die("git merge-index: %s not in the cache", path);
- found = 0;
- do {
- const struct cache_entry *ce = active_cache[pos];
- int stage = ce_stage(ce);
-
- if (strcmp(ce->name, path))
- break;
- found++;
- oid_to_hex_r(hexbuf[stage], &ce->oid);
- xsnprintf(ownbuf[stage], sizeof(ownbuf[stage]), "%o", ce->ce_mode);
- arguments[stage] = hexbuf[stage];
- arguments[stage + 4] = ownbuf[stage];
- } while (++pos < active_nr);
- if (!found)
- die("git merge-index: %s not in the cache", path);
-
- if (run_command_v_opt(arguments, 0)) {
- if (one_shot)
- err++;
- else {
- if (!quiet)
- die("merge program failed");
- exit(1);
- }
- }
- return found;
-}
-
-static void merge_one_path(const char *path)
-{
- int pos = cache_name_pos(path, strlen(path));
-
- /*
- * If it already exists in the cache as stage0, it's
- * already merged and there is nothing to do.
- */
- if (pos < 0)
- merge_entry(-pos-1, path);
-}
-
-static void merge_all(void)
-{
- int i;
- for (i = 0; i < active_nr; i++) {
- const struct cache_entry *ce = active_cache[i];
- if (!ce_stage(ce))
- continue;
- i += merge_entry(i, ce->name)-1;
- }
-}
+#include "merge-strategies.h"
int cmd_merge_index(int argc, const char **argv, const char *prefix)
{
- int i, force_file = 0;
+ int i, force_file = 0, err = 0, one_shot = 0, quiet = 0;
+ const char *pgm;
/* Without this we cannot rely on waitpid() to tell
* what happened to our children.
@@ -98,14 +35,14 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
continue;
}
if (!strcmp(arg, "-a")) {
- merge_all();
+ err |= merge_all_index(&the_index, one_shot, quiet,
+ merge_one_file_spawn, (void *)pgm);
continue;
}
die("git merge-index: unknown option %s", arg);
}
- merge_one_path(arg);
+ err |= merge_index_path(&the_index, one_shot, quiet, arg,
+ merge_one_file_spawn, (void *)pgm);
}
- if (err && !quiet)
- die("merge program failed");
return err;
}
diff --git a/merge-strategies.c b/merge-strategies.c
index c5576dc891..4eb96129f1 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -1,6 +1,7 @@
#include "cache.h"
#include "dir.h"
#include "merge-strategies.h"
+#include "run-command.h"
#include "xdiff-interface.h"
static int checkout_from_index(struct index_state *istate, const char *path,
@@ -174,3 +175,105 @@ int merge_three_way(struct repository *r,
return 0;
}
+
+int merge_one_file_spawn(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data)
+{
+ char oids[3][GIT_MAX_HEXSZ + 1] = {{0}};
+ char modes[3][10] = {{0}};
+ const char *arguments[] = { (char *)data, oids[0], oids[1], oids[2],
+ path, modes[0], modes[1], modes[2], NULL };
+
+ if (orig_blob) {
+ oid_to_hex_r(oids[0], orig_blob);
+ xsnprintf(modes[0], sizeof(modes[0]), "%06o", orig_mode);
+ }
+
+ if (our_blob) {
+ oid_to_hex_r(oids[1], our_blob);
+ xsnprintf(modes[1], sizeof(modes[1]), "%06o", our_mode);
+ }
+
+ if (their_blob) {
+ oid_to_hex_r(oids[2], their_blob);
+ xsnprintf(modes[2], sizeof(modes[2]), "%06o", their_mode);
+ }
+
+ return run_command_v_opt(arguments, 0);
+}
+
+static int merge_entry(struct index_state *istate, int quiet, int pos,
+ const char *path, merge_fn fn, void *data)
+{
+ int found = 0;
+ const struct object_id *oids[3] = {NULL};
+ unsigned int modes[3] = {0};
+
+ do {
+ const struct cache_entry *ce = istate->cache[pos];
+ int stage = ce_stage(ce);
+
+ if (strcmp(ce->name, path))
+ break;
+ found++;
+ oids[stage - 1] = &ce->oid;
+ modes[stage - 1] = ce->ce_mode;
+ } while (++pos < istate->cache_nr);
+ if (!found)
+ return error(_("%s is not in the cache"), path);
+
+ if (fn(oids[0], oids[1], oids[2], path, modes[0], modes[1], modes[2], data)) {
+ if (!quiet)
+ error(_("Merge program failed"));
+ return -2;
+ }
+
+ return found;
+}
+
+int merge_index_path(struct index_state *istate, int oneshot, int quiet,
+ const char *path, merge_fn fn, void *data)
+{
+ int pos = index_name_pos(istate, path, strlen(path)), ret;
+
+ /*
+ * If it already exists in the cache as stage0, it's
+ * already merged and there is nothing to do.
+ */
+ if (pos < 0) {
+ ret = merge_entry(istate, quiet, -pos - 1, path, fn, data);
+ if (ret == -1)
+ return -1;
+ else if (ret == -2)
+ return 1;
+ }
+ return 0;
+}
+
+int merge_all_index(struct index_state *istate, int oneshot, int quiet,
+ merge_fn fn, void *data)
+{
+ int err = 0, i, ret;
+ for (i = 0; i < istate->cache_nr; i++) {
+ const struct cache_entry *ce = istate->cache[i];
+ if (!ce_stage(ce))
+ continue;
+
+ ret = merge_entry(istate, quiet, i, ce->name, fn, data);
+ if (ret > 0)
+ i += ret - 1;
+ else if (ret == -1)
+ return -1;
+ else if (ret == -2) {
+ if (oneshot)
+ err++;
+ else
+ return 1;
+ }
+ }
+
+ return err;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
index e624c4f27c..d2f52d6792 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -9,4 +9,21 @@ int merge_three_way(struct repository *r,
const struct object_id *their_blob, const char *path,
unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode);
+typedef int (*merge_fn)(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
+int merge_one_file_spawn(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
+int merge_index_path(struct index_state *istate, int oneshot, int quiet,
+ const char *path, merge_fn fn, void *data);
+int merge_all_index(struct index_state *istate, int oneshot, int quiet,
+ merge_fn fn, void *data);
+
#endif /* MERGE_STRATEGIES_H */
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v5 05/12] merge-index: don't fork if the requested program is `git-merge-one-file'
2020-11-16 10:21 ` [PATCH v5 00/12] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (3 preceding siblings ...)
2020-11-16 10:21 ` [PATCH v5 04/12] merge-index: libify merge_one_path() and merge_all() Alban Gruin
@ 2020-11-16 10:21 ` Alban Gruin
2020-11-16 10:21 ` [PATCH v5 06/12] merge-resolve: rewrite in C Alban Gruin
` (7 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-16 10:21 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
Since `git-merge-one-file' has been rewritten and libified, this teaches
`merge-index' to call merge_three_way() without forking using a new
callback, merge_one_file_func().
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge-index.c | 29 +++++++++++++++++++++++++++--
merge-strategies.c | 11 +++++++++++
merge-strategies.h | 6 ++++++
3 files changed, 44 insertions(+), 2 deletions(-)
diff --git a/builtin/merge-index.c b/builtin/merge-index.c
index 49e3382fb9..e684811d35 100644
--- a/builtin/merge-index.c
+++ b/builtin/merge-index.c
@@ -1,11 +1,15 @@
#define USE_THE_INDEX_COMPATIBILITY_MACROS
#include "builtin.h"
+#include "lockfile.h"
#include "merge-strategies.h"
int cmd_merge_index(int argc, const char **argv, const char *prefix)
{
int i, force_file = 0, err = 0, one_shot = 0, quiet = 0;
const char *pgm;
+ void *data;
+ merge_fn merge_action;
+ struct lock_file lock = LOCK_INIT;
/* Without this we cannot rely on waitpid() to tell
* what happened to our children.
@@ -26,7 +30,19 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
quiet = 1;
i++;
}
+
pgm = argv[i++];
+ if (!strcmp(pgm, "git-merge-one-file")) {
+ merge_action = merge_one_file_func;
+ data = (void *)the_repository;
+
+ setup_work_tree();
+ hold_locked_index(&lock, LOCK_DIE_ON_ERROR);
+ } else {
+ merge_action = merge_one_file_spawn;
+ data = (void *)pgm;
+ }
+
for (; i < argc; i++) {
const char *arg = argv[i];
if (!force_file && *arg == '-') {
@@ -36,13 +52,22 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
}
if (!strcmp(arg, "-a")) {
err |= merge_all_index(&the_index, one_shot, quiet,
- merge_one_file_spawn, (void *)pgm);
+ merge_action, data);
continue;
}
die("git merge-index: unknown option %s", arg);
}
err |= merge_index_path(&the_index, one_shot, quiet, arg,
- merge_one_file_spawn, (void *)pgm);
+ merge_action, data);
+ }
+
+ if (merge_action == merge_one_file_func) {
+ if (err) {
+ rollback_lock_file(&lock);
+ return err;
+ }
+
+ return write_locked_index(&the_index, &lock, COMMIT_LOCK);
}
return err;
}
diff --git a/merge-strategies.c b/merge-strategies.c
index 4eb96129f1..2ed3a8dd68 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -176,6 +176,17 @@ int merge_three_way(struct repository *r,
return 0;
}
+int merge_one_file_func(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data)
+{
+ return merge_three_way((struct repository *)data,
+ orig_blob, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+}
+
int merge_one_file_spawn(const struct object_id *orig_blob,
const struct object_id *our_blob,
const struct object_id *their_blob, const char *path,
diff --git a/merge-strategies.h b/merge-strategies.h
index d2f52d6792..b69a12b390 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -15,6 +15,12 @@ typedef int (*merge_fn)(const struct object_id *orig_blob,
unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
void *data);
+int merge_one_file_func(const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
int merge_one_file_spawn(const struct object_id *orig_blob,
const struct object_id *our_blob,
const struct object_id *their_blob, const char *path,
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v5 06/12] merge-resolve: rewrite in C
2020-11-16 10:21 ` [PATCH v5 00/12] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (4 preceding siblings ...)
2020-11-16 10:21 ` [PATCH v5 05/12] merge-index: don't fork if the requested program is `git-merge-one-file' Alban Gruin
@ 2020-11-16 10:21 ` Alban Gruin
2020-11-16 10:21 ` [PATCH v5 07/12] merge-recursive: move better_branch_name() to merge.c Alban Gruin
` (6 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-16 10:21 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
This rewrites `git merge-resolve' from shell to C. As for `git
merge-one-file', this port is not completely straightforward and removes
calls to external processes to avoid reading and writing the index over
and over again.
- The call to `update-index -q --refresh' is replaced by a call to
refresh_index().
- The call to `read-tree' is replaced by a call to unpack_trees() (and
all the setup needed).
- The call to `write-tree' is replaced by a call to
write_index_as_tree().
- The call to `merge-index', needed to invoke `git merge-one-file', is
replaced by a call to the new merge_all_index() function.
The index is read in cmd_merge_resolve(), and is wrote back by
merge_strategies_resolve().
The parameters of merge_strategies_resolve() will be surprising at first
glance: why using a commit list for `bases' and `remote', where we could
use an oid array, and a pointer to an oid? Because, in a later commit,
try_merge_strategy() will be able to call merge_strategies_resolve()
directly, and it already uses a commit list for `bases' (`common') and
`remote' (`remoteheads'), and a string for `head_arg'. To reduce
frictions later, merge_strategies_resolve() takes the same types of
parameters.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Makefile | 2 +-
builtin.h | 1 +
builtin/merge-resolve.c | 73 +++++++++++++++++++++++++++++++
git-merge-resolve.sh | 54 -----------------------
git.c | 1 +
merge-strategies.c | 95 +++++++++++++++++++++++++++++++++++++++++
merge-strategies.h | 5 +++
7 files changed, 176 insertions(+), 55 deletions(-)
create mode 100644 builtin/merge-resolve.c
delete mode 100755 git-merge-resolve.sh
diff --git a/Makefile b/Makefile
index 6dfdb33cb2..3cc6b192f1 100644
--- a/Makefile
+++ b/Makefile
@@ -601,7 +601,6 @@ SCRIPT_SH += git-bisect.sh
SCRIPT_SH += git-difftool--helper.sh
SCRIPT_SH += git-filter-branch.sh
SCRIPT_SH += git-merge-octopus.sh
-SCRIPT_SH += git-merge-resolve.sh
SCRIPT_SH += git-mergetool.sh
SCRIPT_SH += git-quiltimport.sh
SCRIPT_SH += git-request-pull.sh
@@ -1097,6 +1096,7 @@ BUILTIN_OBJS += builtin/merge-index.o
BUILTIN_OBJS += builtin/merge-one-file.o
BUILTIN_OBJS += builtin/merge-ours.o
BUILTIN_OBJS += builtin/merge-recursive.o
+BUILTIN_OBJS += builtin/merge-resolve.o
BUILTIN_OBJS += builtin/merge-tree.o
BUILTIN_OBJS += builtin/merge.o
BUILTIN_OBJS += builtin/mktag.o
diff --git a/builtin.h b/builtin.h
index 4d2cd78856..35e91c16d0 100644
--- a/builtin.h
+++ b/builtin.h
@@ -180,6 +180,7 @@ int cmd_merge_ours(int argc, const char **argv, const char *prefix);
int cmd_merge_file(int argc, const char **argv, const char *prefix);
int cmd_merge_one_file(int argc, const char **argv, const char *prefix);
int cmd_merge_recursive(int argc, const char **argv, const char *prefix);
+int cmd_merge_resolve(int argc, const char **argv, const char *prefix);
int cmd_merge_tree(int argc, const char **argv, const char *prefix);
int cmd_mktag(int argc, const char **argv, const char *prefix);
int cmd_mktree(int argc, const char **argv, const char *prefix);
diff --git a/builtin/merge-resolve.c b/builtin/merge-resolve.c
new file mode 100644
index 0000000000..dca31676b8
--- /dev/null
+++ b/builtin/merge-resolve.c
@@ -0,0 +1,73 @@
+/*
+ * Builtin "git merge-resolve"
+ *
+ * Copyright (c) 2020 Alban Gruin
+ *
+ * Based on git-merge-resolve.sh, written by Linus Torvalds and Junio C
+ * Hamano.
+ *
+ * Resolve two trees, using enhanced multi-base read-tree.
+ */
+
+#define USE_THE_INDEX_COMPATIBILITY_MACROS
+#include "cache.h"
+#include "builtin.h"
+#include "merge-strategies.h"
+
+static const char builtin_merge_resolve_usage[] =
+ "git merge-resolve <bases>... -- <head> <remote>";
+
+int cmd_merge_resolve(int argc, const char **argv, const char *prefix)
+{
+ int i, sep_seen = 0;
+ const char *head = NULL;
+ struct commit_list *bases = NULL, *remote = NULL;
+ struct commit_list **next_base = &bases;
+
+ if (argc < 5)
+ usage(builtin_merge_resolve_usage);
+
+ setup_work_tree();
+ if (read_cache() < 0)
+ die("invalid index");
+
+ /*
+ * The first parameters up to -- are merge bases; the rest are
+ * heads.
+ */
+ for (i = 1; i < argc; i++) {
+ if (!strcmp(argv[i], "--"))
+ sep_seen = 1;
+ else if (!strcmp(argv[i], "-h"))
+ usage(builtin_merge_resolve_usage);
+ else if (sep_seen && !head)
+ head = argv[i];
+ else {
+ struct object_id oid;
+ struct commit *commit;
+
+ if (get_oid(argv[i], &oid))
+ die("object %s not found.", argv[i]);
+
+ commit = lookup_commit_or_die(&oid, argv[i]);
+
+ if (sep_seen)
+ commit_list_insert(commit, &remote);
+ else
+ next_base = commit_list_append(commit, next_base);
+ }
+ }
+
+ /*
+ * Give up if we are given two or more remotes. Not handling
+ * octopus.
+ */
+ if (remote && remote->next)
+ return 2;
+
+ /* Give up if this is a baseless merge. */
+ if (!bases)
+ return 2;
+
+ return merge_strategies_resolve(the_repository, bases, head, remote);
+}
diff --git a/git-merge-resolve.sh b/git-merge-resolve.sh
deleted file mode 100755
index 343fe7bccd..0000000000
--- a/git-merge-resolve.sh
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2005 Linus Torvalds
-# Copyright (c) 2005 Junio C Hamano
-#
-# Resolve two trees, using enhanced multi-base read-tree.
-
-# The first parameters up to -- are merge bases; the rest are heads.
-bases= head= remotes= sep_seen=
-for arg
-do
- case ",$sep_seen,$head,$arg," in
- *,--,)
- sep_seen=yes
- ;;
- ,yes,,*)
- head=$arg
- ;;
- ,yes,*)
- remotes="$remotes$arg "
- ;;
- *)
- bases="$bases$arg "
- ;;
- esac
-done
-
-# Give up if we are given two or more remotes -- not handling octopus.
-case "$remotes" in
-?*' '?*)
- exit 2 ;;
-esac
-
-# Give up if this is a baseless merge.
-if test '' = "$bases"
-then
- exit 2
-fi
-
-git update-index -q --refresh
-git read-tree -u -m --aggressive $bases $head $remotes || exit 2
-echo "Trying simple merge."
-if result_tree=$(git write-tree 2>/dev/null)
-then
- exit 0
-else
- echo "Simple merge failed, trying Automatic merge."
- if git merge-index -o git-merge-one-file -a
- then
- exit 0
- else
- exit 1
- fi
-fi
diff --git a/git.c b/git.c
index a4d3f98094..64a1a1de41 100644
--- a/git.c
+++ b/git.c
@@ -544,6 +544,7 @@ static struct cmd_struct commands[] = {
{ "merge-recursive", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-ours", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-theirs", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
+ { "merge-resolve", cmd_merge_resolve, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-subtree", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-tree", cmd_merge_tree, RUN_SETUP | NO_PARSEOPT },
{ "mktag", cmd_mktag, RUN_SETUP | NO_PARSEOPT },
diff --git a/merge-strategies.c b/merge-strategies.c
index 2ed3a8dd68..9fafee5954 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -1,7 +1,10 @@
#include "cache.h"
+#include "cache-tree.h"
#include "dir.h"
+#include "lockfile.h"
#include "merge-strategies.h"
#include "run-command.h"
+#include "unpack-trees.h"
#include "xdiff-interface.h"
static int checkout_from_index(struct index_state *istate, const char *path,
@@ -288,3 +291,95 @@ int merge_all_index(struct index_state *istate, int oneshot, int quiet,
return err;
}
+
+static int fast_forward(struct repository *r, struct tree_desc *t,
+ int nr, int aggressive)
+{
+ struct unpack_trees_options opts;
+ struct lock_file lock = LOCK_INIT;
+
+ refresh_index(r->index, REFRESH_QUIET, NULL, NULL, NULL);
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+
+ memset(&opts, 0, sizeof(opts));
+ opts.head_idx = 1;
+ opts.src_index = r->index;
+ opts.dst_index = r->index;
+ opts.merge = 1;
+ opts.update = 1;
+ opts.aggressive = aggressive;
+
+ if (nr == 1)
+ opts.fn = oneway_merge;
+ else if (nr == 2) {
+ opts.fn = twoway_merge;
+ opts.initial_checkout = is_index_unborn(r->index);
+ } else if (nr >= 3) {
+ opts.fn = threeway_merge;
+ opts.head_idx = nr - 1;
+ }
+
+ if (unpack_trees(nr, t, &opts))
+ return -1;
+
+ if (write_locked_index(r->index, &lock, COMMIT_LOCK))
+ return error(_("unable to write new index file"));
+
+ return 0;
+}
+
+static int add_tree(struct tree *tree, struct tree_desc *t)
+{
+ if (parse_tree(tree))
+ return -1;
+
+ init_tree_desc(t, tree->buffer, tree->size);
+ return 0;
+}
+
+int merge_strategies_resolve(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remote)
+{
+ struct tree_desc t[MAX_UNPACK_TREES];
+ struct object_id head, oid;
+ struct commit_list *i;
+ int nr = 0;
+
+ if (head_arg)
+ get_oid(head_arg, &head);
+
+ puts(_("Trying simple merge."));
+
+ for (i = bases; i && i->item; i = i->next) {
+ if (add_tree(repo_get_commit_tree(r, i->item), t + (nr++)))
+ return 2;
+ }
+
+ if (head_arg) {
+ struct tree *tree = parse_tree_indirect(&head);
+ if (add_tree(tree, t + (nr++)))
+ return 2;
+ }
+
+ if (remote && add_tree(repo_get_commit_tree(r, remote->item), t + (nr++)))
+ return 2;
+
+ if (fast_forward(r, t, nr, 1))
+ return 2;
+
+ if (write_index_as_tree(&oid, r->index, r->index_file,
+ WRITE_TREE_SILENT, NULL)) {
+ int ret;
+ struct lock_file lock = LOCK_INIT;
+
+ puts(_("Simple merge failed, trying Automatic merge."));
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+ ret = merge_all_index(r->index, 0, 0, merge_one_file_func, r);
+
+ write_locked_index(r->index, &lock, COMMIT_LOCK);
+ return !!ret;
+ }
+
+ return 0;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
index b69a12b390..4f996261b4 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -1,6 +1,7 @@
#ifndef MERGE_STRATEGIES_H
#define MERGE_STRATEGIES_H
+#include "commit.h"
#include "object.h"
int merge_three_way(struct repository *r,
@@ -32,4 +33,8 @@ int merge_index_path(struct index_state *istate, int oneshot, int quiet,
int merge_all_index(struct index_state *istate, int oneshot, int quiet,
merge_fn fn, void *data);
+int merge_strategies_resolve(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remote);
+
#endif /* MERGE_STRATEGIES_H */
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v5 07/12] merge-recursive: move better_branch_name() to merge.c
2020-11-16 10:21 ` [PATCH v5 00/12] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (5 preceding siblings ...)
2020-11-16 10:21 ` [PATCH v5 06/12] merge-resolve: rewrite in C Alban Gruin
@ 2020-11-16 10:21 ` Alban Gruin
2020-11-16 10:21 ` [PATCH v5 08/12] merge-octopus: rewrite in C Alban Gruin
` (5 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-16 10:21 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
better_branch_name() will be used by merge-octopus once it is rewritten
in C, so instead of duplicating it, this moves this function
preventively inside an appropriate file in libgit.a. This function is
also renamed to reflect its usage by merge strategies.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge-recursive.c | 16 ++--------------
cache.h | 2 +-
merge.c | 12 ++++++++++++
3 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/builtin/merge-recursive.c b/builtin/merge-recursive.c
index a4bfd8fc51..972243b5e9 100644
--- a/builtin/merge-recursive.c
+++ b/builtin/merge-recursive.c
@@ -8,18 +8,6 @@
static const char builtin_merge_recursive_usage[] =
"git %s <base>... -- <head> <remote> ...";
-static char *better_branch_name(const char *branch)
-{
- static char githead_env[8 + GIT_MAX_HEXSZ + 1];
- char *name;
-
- if (strlen(branch) != the_hash_algo->hexsz)
- return xstrdup(branch);
- xsnprintf(githead_env, sizeof(githead_env), "GITHEAD_%s", branch);
- name = getenv(githead_env);
- return xstrdup(name ? name : branch);
-}
-
int cmd_merge_recursive(int argc, const char **argv, const char *prefix)
{
const struct object_id *bases[21];
@@ -75,8 +63,8 @@ int cmd_merge_recursive(int argc, const char **argv, const char *prefix)
if (get_oid(o.branch2, &h2))
die(_("could not resolve ref '%s'"), o.branch2);
- o.branch1 = better1 = better_branch_name(o.branch1);
- o.branch2 = better2 = better_branch_name(o.branch2);
+ o.branch1 = better1 = merge_get_better_branch_name(o.branch1);
+ o.branch2 = better2 = merge_get_better_branch_name(o.branch2);
if (o.verbosity >= 3)
printf(_("Merging %s with %s\n"), o.branch1, o.branch2);
diff --git a/cache.h b/cache.h
index be16ab3215..2d844576ea 100644
--- a/cache.h
+++ b/cache.h
@@ -1933,7 +1933,7 @@ int checkout_fast_forward(struct repository *r,
const struct object_id *from,
const struct object_id *to,
int overwrite_ignore);
-
+char *merge_get_better_branch_name(const char *branch);
int sane_execvp(const char *file, char *const argv[]);
diff --git a/merge.c b/merge.c
index 5fb88af102..801d673c5f 100644
--- a/merge.c
+++ b/merge.c
@@ -109,3 +109,15 @@ int checkout_fast_forward(struct repository *r,
return error(_("unable to write new index file"));
return 0;
}
+
+char *merge_get_better_branch_name(const char *branch)
+{
+ static char githead_env[8 + GIT_MAX_HEXSZ + 1];
+ char *name;
+
+ if (strlen(branch) != the_hash_algo->hexsz)
+ return xstrdup(branch);
+ xsnprintf(githead_env, sizeof(githead_env), "GITHEAD_%s", branch);
+ name = getenv(githead_env);
+ return xstrdup(name ? name : branch);
+}
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v5 08/12] merge-octopus: rewrite in C
2020-11-16 10:21 ` [PATCH v5 00/12] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (6 preceding siblings ...)
2020-11-16 10:21 ` [PATCH v5 07/12] merge-recursive: move better_branch_name() to merge.c Alban Gruin
@ 2020-11-16 10:21 ` Alban Gruin
2020-11-16 10:21 ` [PATCH v5 09/12] merge: use the "resolve" strategy without forking Alban Gruin
` (4 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-16 10:21 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
This rewrites `git merge-octopus' from shell to C. As for the two last
conversions, this port removes calls to external processes to avoid
reading and writing the index over and over again.
- Calls to `read-tree -u -m (--aggressive)?' are replaced by calls to
unpack_trees().
- The call to `write-tree' is replaced by a call to
write_index_as_tree().
- The call to `diff-index ...' is replaced by a call to
repo_index_has_changes().
- The call to `merge-index', needed to invoke `git merge-one-file', is
replaced by a call to merge_all_index().
The index is read in cmd_merge_octopus(), and is wrote back by
merge_strategies_octopus().
Here to, merge_strategies_octopus() takes two commit lists and a string
to reduce frictions when try_merge_strategies() will be modified to call
it directly.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Makefile | 2 +-
builtin.h | 1 +
builtin/merge-octopus.c | 69 ++++++++++++++++
git-merge-octopus.sh | 112 -------------------------
git.c | 1 +
merge-strategies.c | 179 ++++++++++++++++++++++++++++++++++++++++
merge-strategies.h | 3 +
7 files changed, 254 insertions(+), 113 deletions(-)
create mode 100644 builtin/merge-octopus.c
delete mode 100755 git-merge-octopus.sh
diff --git a/Makefile b/Makefile
index 3cc6b192f1..2b2bdffafe 100644
--- a/Makefile
+++ b/Makefile
@@ -600,7 +600,6 @@ unexport CDPATH
SCRIPT_SH += git-bisect.sh
SCRIPT_SH += git-difftool--helper.sh
SCRIPT_SH += git-filter-branch.sh
-SCRIPT_SH += git-merge-octopus.sh
SCRIPT_SH += git-mergetool.sh
SCRIPT_SH += git-quiltimport.sh
SCRIPT_SH += git-request-pull.sh
@@ -1093,6 +1092,7 @@ BUILTIN_OBJS += builtin/mailsplit.o
BUILTIN_OBJS += builtin/merge-base.o
BUILTIN_OBJS += builtin/merge-file.o
BUILTIN_OBJS += builtin/merge-index.o
+BUILTIN_OBJS += builtin/merge-octopus.o
BUILTIN_OBJS += builtin/merge-one-file.o
BUILTIN_OBJS += builtin/merge-ours.o
BUILTIN_OBJS += builtin/merge-recursive.o
diff --git a/builtin.h b/builtin.h
index 35e91c16d0..50225404a0 100644
--- a/builtin.h
+++ b/builtin.h
@@ -176,6 +176,7 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix);
int cmd_merge(int argc, const char **argv, const char *prefix);
int cmd_merge_base(int argc, const char **argv, const char *prefix);
int cmd_merge_index(int argc, const char **argv, const char *prefix);
+int cmd_merge_octopus(int argc, const char **argv, const char *prefix);
int cmd_merge_ours(int argc, const char **argv, const char *prefix);
int cmd_merge_file(int argc, const char **argv, const char *prefix);
int cmd_merge_one_file(int argc, const char **argv, const char *prefix);
diff --git a/builtin/merge-octopus.c b/builtin/merge-octopus.c
new file mode 100644
index 0000000000..ca8f9f345d
--- /dev/null
+++ b/builtin/merge-octopus.c
@@ -0,0 +1,69 @@
+/*
+ * Builtin "git merge-octopus"
+ *
+ * Copyright (c) 2020 Alban Gruin
+ *
+ * Based on git-merge-octopus.sh, written by Junio C Hamano.
+ *
+ * Resolve two or more trees.
+ */
+
+#define USE_THE_INDEX_COMPATIBILITY_MACROS
+#include "cache.h"
+#include "builtin.h"
+#include "commit.h"
+#include "merge-strategies.h"
+
+static const char builtin_merge_octopus_usage[] =
+ "git merge-octopus [<bases>...] -- <head> <remote1> <remote2> [<remotes>...]";
+
+int cmd_merge_octopus(int argc, const char **argv, const char *prefix)
+{
+ int i, sep_seen = 0;
+ struct commit_list *bases = NULL, *remotes = NULL;
+ struct commit_list **next_base = &bases, **next_remote = &remotes;
+ const char *head_arg = NULL;
+
+ if (argc < 5)
+ usage(builtin_merge_octopus_usage);
+
+ setup_work_tree();
+ if (read_cache() < 0)
+ die("invalid index");
+
+ /*
+ * The first parameters up to -- are merge bases; the rest are
+ * heads.
+ */
+ for (i = 1; i < argc; i++) {
+ if (strcmp(argv[i], "--") == 0)
+ sep_seen = 1;
+ else if (strcmp(argv[i], "-h") == 0)
+ usage(builtin_merge_octopus_usage);
+ else if (sep_seen && !head_arg)
+ head_arg = argv[i];
+ else {
+ struct object_id oid;
+ struct commit *commit;
+
+ if (get_oid(argv[i], &oid))
+ die("object %s not found.", argv[i]);
+
+ commit = lookup_commit_or_die(&oid, argv[i]);
+
+ if (sep_seen)
+ next_remote = commit_list_append(commit, next_remote);
+ else
+ next_base = commit_list_append(commit, next_base);
+ }
+ }
+
+ /*
+ * Reject if this is not an octopus -- resolve should be used
+ * instead.
+ */
+ if (commit_list_count(remotes) < 2)
+ return 2;
+
+ return merge_strategies_octopus(the_repository, bases, head_arg, remotes);
+}
diff --git a/git-merge-octopus.sh b/git-merge-octopus.sh
deleted file mode 100755
index 7d19d37951..0000000000
--- a/git-merge-octopus.sh
+++ /dev/null
@@ -1,112 +0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2005 Junio C Hamano
-#
-# Resolve two or more trees.
-#
-
-. git-sh-setup
-
-LF='
-'
-
-# The first parameters up to -- are merge bases; the rest are heads.
-bases= head= remotes= sep_seen=
-for arg
-do
- case ",$sep_seen,$head,$arg," in
- *,--,)
- sep_seen=yes
- ;;
- ,yes,,*)
- head=$arg
- ;;
- ,yes,*)
- remotes="$remotes$arg "
- ;;
- *)
- bases="$bases$arg "
- ;;
- esac
-done
-
-# Reject if this is not an octopus -- resolve should be used instead.
-case "$remotes" in
-?*' '?*)
- ;;
-*)
- exit 2 ;;
-esac
-
-# MRC is the current "merge reference commit"
-# MRT is the current "merge result tree"
-
-if ! git diff-index --quiet --cached HEAD --
-then
- gettextln "Error: Your local changes to the following files would be overwritten by merge"
- git diff-index --cached --name-only HEAD -- | sed -e 's/^/ /'
- exit 2
-fi
-MRC=$(git rev-parse --verify -q $head)
-MRT=$(git write-tree)
-NON_FF_MERGE=0
-OCTOPUS_FAILURE=0
-for SHA1 in $remotes
-do
- case "$OCTOPUS_FAILURE" in
- 1)
- # We allow only last one to have a hand-resolvable
- # conflicts. Last round failed and we still had
- # a head to merge.
- gettextln "Automated merge did not work."
- gettextln "Should not be doing an octopus."
- exit 2
- esac
-
- eval pretty_name=\${GITHEAD_$SHA1:-$SHA1}
- if test "$SHA1" = "$pretty_name"
- then
- SHA1_UP="$(echo "$SHA1" | tr a-z A-Z)"
- eval pretty_name=\${GITHEAD_$SHA1_UP:-$pretty_name}
- fi
- common=$(git merge-base --all $SHA1 $MRC) ||
- die "$(eval_gettext "Unable to find common commit with \$pretty_name")"
-
- case "$LF$common$LF" in
- *"$LF$SHA1$LF"*)
- eval_gettextln "Already up to date with \$pretty_name"
- continue
- ;;
- esac
-
- if test "$common,$NON_FF_MERGE" = "$MRC,0"
- then
- # The first head being merged was a fast-forward.
- # Advance MRC to the head being merged, and use that
- # tree as the intermediate result of the merge.
- # We still need to count this as part of the parent set.
-
- eval_gettextln "Fast-forwarding to: \$pretty_name"
- git read-tree -u -m $head $SHA1 || exit
- MRC=$SHA1 MRT=$(git write-tree)
- continue
- fi
-
- NON_FF_MERGE=1
-
- eval_gettextln "Trying simple merge with \$pretty_name"
- git read-tree -u -m --aggressive $common $MRT $SHA1 || exit 2
- next=$(git write-tree 2>/dev/null)
- if test $? -ne 0
- then
- gettextln "Simple merge did not work, trying automatic merge."
- git merge-index -o git-merge-one-file -a ||
- OCTOPUS_FAILURE=1
- next=$(git write-tree 2>/dev/null)
- fi
-
- MRC="$MRC $SHA1"
- MRT=$next
-done
-
-exit "$OCTOPUS_FAILURE"
diff --git a/git.c b/git.c
index 64a1a1de41..d51fb5d2bf 100644
--- a/git.c
+++ b/git.c
@@ -539,6 +539,7 @@ static struct cmd_struct commands[] = {
{ "merge-base", cmd_merge_base, RUN_SETUP },
{ "merge-file", cmd_merge_file, RUN_SETUP_GENTLY },
{ "merge-index", cmd_merge_index, RUN_SETUP | NO_PARSEOPT },
+ { "merge-octopus", cmd_merge_octopus, RUN_SETUP | NO_PARSEOPT },
{ "merge-ours", cmd_merge_ours, RUN_SETUP | NO_PARSEOPT },
{ "merge-one-file", cmd_merge_one_file, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
diff --git a/merge-strategies.c b/merge-strategies.c
index 9fafee5954..970ff4793d 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -1,5 +1,6 @@
#include "cache.h"
#include "cache-tree.h"
+#include "commit-reach.h"
#include "dir.h"
#include "lockfile.h"
#include "merge-strategies.h"
@@ -383,3 +384,181 @@ int merge_strategies_resolve(struct repository *r,
return 0;
}
+
+static int write_tree(struct repository *r, struct tree **reference_tree)
+{
+ struct object_id oid;
+ int ret;
+
+ if (!(ret = write_index_as_tree(&oid, r->index, r->index_file, 0, NULL)))
+ *reference_tree = lookup_tree(r, &oid);
+
+ return ret;
+}
+
+static int octopus_fast_forward(struct repository *r, const char *branch_name,
+ struct tree *tree_head, struct tree *current_tree,
+ struct tree **reference_tree)
+{
+ /*
+ * The first head being merged was a fast-forward. Advance the
+ * reference commit to the head being merged, and use that tree
+ * as the intermediate result of the merge. We still need to
+ * count this as part of the parent set.
+ */
+ struct tree_desc t[2];
+
+ printf(_("Fast-forwarding to: %s\n"), branch_name);
+
+ init_tree_desc(t, tree_head->buffer, tree_head->size);
+ if (add_tree(current_tree, t + 1))
+ return -1;
+ if (fast_forward(r, t, 2, 0))
+ return -1;
+ if (write_tree(r, reference_tree))
+ return -1;
+
+ return 0;
+}
+
+static int octopus_do_merge(struct repository *r, const char *branch_name,
+ struct commit_list *common, struct tree *current_tree,
+ struct tree **reference_tree)
+{
+ struct tree_desc t[MAX_UNPACK_TREES];
+ struct commit_list *j;
+ int nr = 0, ret = 0;
+
+ printf(_("Trying simple merge with %s\n"), branch_name);
+
+ for (j = common; j; j = j->next) {
+ struct tree *tree = repo_get_commit_tree(r, j->item);
+ if (add_tree(tree, t + (nr++)))
+ return -1;
+ }
+
+ if (add_tree(*reference_tree, t + (nr++)))
+ return -1;
+ if (add_tree(current_tree, t + (nr++)))
+ return -1;
+ if (fast_forward(r, t, nr, 1))
+ return -1;
+
+ if (write_tree(r, reference_tree)) {
+ struct lock_file lock = LOCK_INIT;
+
+ puts(_("Simple merge did not work, trying automatic merge."));
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+ ret = merge_all_index(r->index, 0, 0, merge_one_file_func, r);
+ write_locked_index(r->index, &lock, COMMIT_LOCK);
+
+ write_tree(r, reference_tree);
+ }
+
+ return ret ? -2 : 0;
+}
+
+int merge_strategies_octopus(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remotes)
+{
+ int ff_merge = 1, ret = 0, references = 1;
+ struct commit **reference_commit;
+ struct tree *reference_tree, *tree_head;
+ struct commit_list *i;
+ struct object_id head;
+ struct strbuf sb = STRBUF_INIT;
+
+ get_oid(head_arg, &head);
+
+ reference_commit = xcalloc(commit_list_count(remotes) + 1,
+ sizeof(struct commit *));
+ reference_commit[0] = lookup_commit_reference(r, &head);
+ reference_tree = repo_get_commit_tree(r, reference_commit[0]);
+
+ tree_head = repo_get_commit_tree(r, reference_commit[0]);
+ if (parse_tree(tree_head)) {
+ ret = 2;
+ goto out;
+ }
+
+ if (repo_index_has_changes(r, reference_tree, &sb)) {
+ error(_("Your local changes to the following files "
+ "would be overwritten by merge:\n %s"),
+ sb.buf);
+ strbuf_release(&sb);
+ ret = 2;
+ goto out;
+ }
+
+ for (i = remotes; i && i->item; i = i->next) {
+ struct commit *c = i->item;
+ struct object_id *oid = &c->object.oid;
+ struct tree *current_tree = repo_get_commit_tree(r, c);
+ struct commit_list *common, *j;
+ char *branch_name;
+ int k = 0, up_to_date = 0;
+
+ if (ret) {
+ /*
+ * We allow only last one to have a
+ * hand-resolvable conflicts. Last round failed
+ * and we still had a head to merge.
+ */
+ puts(_("Automated merge did not work."));
+ puts(_("Should not be doing an octopus."));
+
+ ret = 2;
+ goto out;
+ }
+
+ branch_name = merge_get_better_branch_name(oid_to_hex(oid));
+ common = get_merge_bases_many(c, references, reference_commit);
+
+ if (!common) {
+ error(_("Unable to find common commit with %s"), branch_name);
+
+ free(branch_name);
+ free_commit_list(common);
+
+ ret = 2;
+ goto out;
+ }
+
+ for (j = common; j && !(up_to_date || !ff_merge); j = j->next) {
+ up_to_date |= oideq(&j->item->object.oid, oid);
+
+ if (k < references)
+ ff_merge &= oideq(&j->item->object.oid, &reference_commit[k++]->object.oid);
+ }
+
+ if (up_to_date) {
+ printf(_("Already up to date with %s\n"), branch_name);
+
+ free(branch_name);
+ free_commit_list(common);
+ continue;
+ }
+
+ if (ff_merge) {
+ ret = octopus_fast_forward(r, branch_name, tree_head,
+ current_tree, &reference_tree);
+ references = 0;
+ } else {
+ ret = octopus_do_merge(r, branch_name, common,
+ current_tree, &reference_tree);
+ }
+
+ free(branch_name);
+ free_commit_list(common);
+
+ if (ret == -1)
+ goto out;
+
+ reference_commit[references++] = c;
+ }
+
+out:
+ free(reference_commit);
+ return ret;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
index 4f996261b4..05232a5a89 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -36,5 +36,8 @@ int merge_all_index(struct index_state *istate, int oneshot, int quiet,
int merge_strategies_resolve(struct repository *r,
struct commit_list *bases, const char *head_arg,
struct commit_list *remote);
+int merge_strategies_octopus(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remote);
#endif /* MERGE_STRATEGIES_H */
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v5 09/12] merge: use the "resolve" strategy without forking
2020-11-16 10:21 ` [PATCH v5 00/12] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (7 preceding siblings ...)
2020-11-16 10:21 ` [PATCH v5 08/12] merge-octopus: rewrite in C Alban Gruin
@ 2020-11-16 10:21 ` Alban Gruin
2020-11-16 10:21 ` [PATCH v5 10/12] merge: use the "octopus" " Alban Gruin
` (3 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-16 10:21 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
This teaches `git merge' to invoke the "resolve" strategy with a
function call instead of forking.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/builtin/merge.c b/builtin/merge.c
index 9d5359edc2..3b35aa320c 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -41,6 +41,7 @@
#include "commit-reach.h"
#include "wt-status.h"
#include "commit-graph.h"
+#include "merge-strategies.h"
#define DEFAULT_TWOHEAD (1<<0)
#define DEFAULT_OCTOPUS (1<<1)
@@ -740,6 +741,9 @@ static int try_merge_strategy(const char *strategy, struct commit_list *common,
COMMIT_LOCK | SKIP_IF_UNCHANGED))
die(_("unable to write %s"), get_index_file());
return clean ? 0 : 1;
+ } else if (!strcmp(strategy, "resolve")) {
+ return merge_strategies_resolve(the_repository, common,
+ head_arg, remoteheads);
} else {
return try_merge_command(the_repository,
strategy, xopts_nr, xopts,
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v5 10/12] merge: use the "octopus" strategy without forking
2020-11-16 10:21 ` [PATCH v5 00/12] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (8 preceding siblings ...)
2020-11-16 10:21 ` [PATCH v5 09/12] merge: use the "resolve" strategy without forking Alban Gruin
@ 2020-11-16 10:21 ` Alban Gruin
2020-11-16 10:21 ` [PATCH v5 11/12] sequencer: use the "resolve" " Alban Gruin
` (2 subsequent siblings)
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-16 10:21 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
This teaches `git merge' to invoke the "octopus" strategy with a
function call instead of forking.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/builtin/merge.c b/builtin/merge.c
index 3b35aa320c..f3345a582a 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -744,6 +744,9 @@ static int try_merge_strategy(const char *strategy, struct commit_list *common,
} else if (!strcmp(strategy, "resolve")) {
return merge_strategies_resolve(the_repository, common,
head_arg, remoteheads);
+ } else if (!strcmp(strategy, "octopus")) {
+ return merge_strategies_octopus(the_repository, common,
+ head_arg, remoteheads);
} else {
return try_merge_command(the_repository,
strategy, xopts_nr, xopts,
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v5 11/12] sequencer: use the "resolve" strategy without forking
2020-11-16 10:21 ` [PATCH v5 00/12] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (9 preceding siblings ...)
2020-11-16 10:21 ` [PATCH v5 10/12] merge: use the "octopus" " Alban Gruin
@ 2020-11-16 10:21 ` Alban Gruin
2020-11-16 10:21 ` [PATCH v5 12/12] sequencer: use the "octopus" merge " Alban Gruin
2020-11-24 11:53 ` [PATCH v6 00/13] Rewrite the remaining merge strategies from shell to C Alban Gruin
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-16 10:21 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
This teaches the sequencer to invoke the "resolve" strategy with a
function call instead of forking.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
sequencer.c | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/sequencer.c b/sequencer.c
index e8676e965f..ff411d54af 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -33,6 +33,7 @@
#include "commit-reach.h"
#include "rebase-interactive.h"
#include "reset.h"
+#include "merge-strategies.h"
#define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
@@ -2000,9 +2001,15 @@ static int do_pick_commit(struct repository *r,
commit_list_insert(base, &common);
commit_list_insert(next, &remotes);
- res |= try_merge_command(r, opts->strategy,
- opts->xopts_nr, (const char **)opts->xopts,
- common, oid_to_hex(&head), remotes);
+
+ if (!strcmp(opts->strategy, "resolve")) {
+ repo_read_index(r);
+ res |= merge_strategies_resolve(r, common, oid_to_hex(&head), remotes);
+ } else
+ res |= try_merge_command(r, opts->strategy,
+ opts->xopts_nr, (const char **)opts->xopts,
+ common, oid_to_hex(&head), remotes);
+
free_commit_list(common);
free_commit_list(remotes);
}
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v5 12/12] sequencer: use the "octopus" merge strategy without forking
2020-11-16 10:21 ` [PATCH v5 00/12] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (10 preceding siblings ...)
2020-11-16 10:21 ` [PATCH v5 11/12] sequencer: use the "resolve" " Alban Gruin
@ 2020-11-16 10:21 ` Alban Gruin
2020-11-24 11:53 ` [PATCH v6 00/13] Rewrite the remaining merge strategies from shell to C Alban Gruin
12 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-16 10:21 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
This teaches the sequencer to invoke the "octopus" strategy with a
function call instead of forking.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
sequencer.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/sequencer.c b/sequencer.c
index ff411d54af..746afad930 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2005,6 +2005,9 @@ static int do_pick_commit(struct repository *r,
if (!strcmp(opts->strategy, "resolve")) {
repo_read_index(r);
res |= merge_strategies_resolve(r, common, oid_to_hex(&head), remotes);
+ } else if (!strcmp(opts->strategy, "octopus")) {
+ repo_read_index(r);
+ res |= merge_strategies_octopus(r, common, oid_to_hex(&head), remotes);
} else
res |= try_merge_command(r, opts->strategy,
opts->xopts_nr, (const char **)opts->xopts,
--
2.20.1
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v6 00/13] Rewrite the remaining merge strategies from shell to C
2020-11-16 10:21 ` [PATCH v5 00/12] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (11 preceding siblings ...)
2020-11-16 10:21 ` [PATCH v5 12/12] sequencer: use the "octopus" merge " Alban Gruin
@ 2020-11-24 11:53 ` Alban Gruin
2020-11-24 11:53 ` [PATCH v6 01/13] t6407: modernise tests Alban Gruin
` (15 more replies)
12 siblings, 16 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-24 11:53 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
In a effort to reduce the number of shell scripts in git's codebase, I
propose this patch series converting the two remaining merge strategies,
resolve and octopus, from shell to C. This will enable slightly better
performance, better integration with git itself (no more forking to
perform these operations), better portability (Windows and shell scripts
don't mix well).
Three scripts are actually converted: first git-merge-one-file.sh, then
git-merge-resolve.sh, and finally git-merge-octopus.sh. Not only they
are converted, but they also are modified to operate without forking,
and then libified so they can be used by git without spawning another
process.
The first patch is not important to make the whole series work, but I
made this patch while working on it.
This series keeps the commands `git merge-one-file', `git
merge-resolve', and `git merge-octopus', so any script depending on them
should keep working without any changes.
This series is based on 306ee63a70 (Eighteenth batch, 2020-09-29). The
tip is tagged as "rewrite-merge-strategies-v6" at
https://github.com/agrn/git.
Changes since v5:
- [1/13] Change the commit message to reflect the change of the name of
t6027.
- [2/13] Introduce changes in t6060 to avoid potential issues with the
libified version of merge-index, where a conflicted file could be
left unmerged (more details in the commit message).
- [4/13] Fix error handling in do_merge_one_file().
- [5/13] Pass the repository instead of the index to the libified
version of merge-index. This will be useful when merge_three_way()
will be modified to put more information in the merge markers:
instead of passing the repository as context, an array of two strings
will be given to merge_entry()'s callback.
- [5/13] Change error handling in merge_entry(). Instead of returning
-2 when the merge program failed, it will increase the number of
errors, passed by a pointer. This change is introduced so the caller
(namely merge_all_index()) still knows how many times a file was
found in the index with its return value. When not ran in oneshot
mode, the number of errors should remain 0. merge_entry() should
also not print "Merge program failed" when ran in oneshot mode.
- [6/13] Fix the issue described in the second patch.
- [7/13] Pass the repository to merge_all_index().
- [9/13] Set the flag WRITE_TREE_SILENT when calling
write_index_as_tree().
- [9/13] Cleanup of merge_strategies_octopus() (removing redundant
code, removing gotos, etc.).
- [12/13, 13/13] Reformatted an if/else if/else sequence.
Alban Gruin (13):
t6407: modernise tests
t6060: modify multiple files to expose a possible issue with
merge-index
update-index: move add_cacheinfo() to read-cache.c
merge-one-file: rewrite in C
merge-index: libify merge_one_path() and merge_all()
merge-index: don't fork if the requested program is
`git-merge-one-file'
merge-resolve: rewrite in C
merge-recursive: move better_branch_name() to merge.c
merge-octopus: rewrite in C
merge: use the "resolve" strategy without forking
merge: use the "octopus" strategy without forking
sequencer: use the "resolve" strategy without forking
sequencer: use the "octopus" merge strategy without forking
Makefile | 7 +-
builtin.h | 3 +
builtin/merge-index.c | 101 ++----
builtin/merge-octopus.c | 69 ++++
builtin/merge-one-file.c | 94 ++++++
builtin/merge-recursive.c | 16 +-
builtin/merge-resolve.c | 73 ++++
builtin/merge.c | 7 +
builtin/update-index.c | 25 +-
cache.h | 7 +-
git-merge-octopus.sh | 112 -------
git-merge-one-file.sh | 167 ----------
git-merge-resolve.sh | 54 ---
git.c | 3 +
merge-strategies.c | 571 ++++++++++++++++++++++++++++++++
merge-strategies.h | 46 +++
merge.c | 12 +
read-cache.c | 35 ++
sequencer.c | 17 +-
t/t6060-merge-index.sh | 10 +-
t/t6407-merge-binary.sh | 27 +-
t/t6415-merge-dir-to-symlink.sh | 2 +-
22 files changed, 992 insertions(+), 466 deletions(-)
create mode 100644 builtin/merge-octopus.c
create mode 100644 builtin/merge-one-file.c
create mode 100644 builtin/merge-resolve.c
delete mode 100755 git-merge-octopus.sh
delete mode 100755 git-merge-one-file.sh
delete mode 100755 git-merge-resolve.sh
create mode 100644 merge-strategies.c
create mode 100644 merge-strategies.h
Range-diff against v5:
1: 08c7df596a ! 1: 70d6507330 t6027: modernise tests
@@ Metadata
Author: Alban Gruin <alban.gruin@gmail.com>
## Commit message ##
- t6027: modernise tests
+ t6407: modernise tests
- Some tests in t6027 uses a if/then/else to check if a command failed or
+ Some tests in t6407 uses a if/then/else to check if a command failed or
not, but we have the `test_must_fail' function to do it correctly for us
nowadays.
-: ---------- > 2: 25e9c47e41 t6060: modify multiple files to expose a possible issue with merge-index
2: df237da758 = 3: e7ea43c5ff update-index: move add_cacheinfo() to read-cache.c
3: eedddde8ea ! 4: 284fc4227f merge-one-file: rewrite in C
@@ merge-strategies.c (new)
+ return error(_("%s: Not merging symbolic link changes."), path);
+ else if (our_mode == S_IFGITLINK || their_mode == S_IFGITLINK)
+ return error(_("%s: Not merging conflicting submodule changes."), path);
-+ else if (our_mode != their_mode)
-+ return error(_("permission conflict: %o->%o,%o in %s"),
-+ orig_mode, our_mode, their_mode, path);
+
+ if (orig_blob) {
+ printf(_("Auto-merging %s\n"), path);
@@ merge-strategies.c (new)
+ if (ret < 0) {
+ free(result.ptr);
+ return error(_("Failed to execute internal merge"));
-+ } else if (ret > 0 || !orig_blob) {
-+ free(result.ptr);
-+ return error(_("content conflict in %s"), path);
+ }
+
++ if (ret > 0 || !orig_blob)
++ ret = error(_("content conflict in %s"), path);
++ if (our_mode != their_mode)
++ ret = error(_("permission conflict: %o->%o,%o in %s"),
++ orig_mode, our_mode, their_mode, path);
++
+ unlink(path);
+ if ((dest = open(path, O_WRONLY | O_CREAT, our_mode)) < 0) {
+ free(result.ptr);
@@ merge-strategies.c (new)
+
+ if (written < 0)
+ return error_errno(_("failed to write to '%s'"), path);
++ if (ret)
++ return ret;
+
+ return add_file_to_index(istate, path, 0);
+}
4: a9b9942243 ! 5: 54abee902f merge-index: libify merge_one_path() and merge_all()
@@ builtin/merge-index.c: int cmd_merge_index(int argc, const char **argv, const ch
}
if (!strcmp(arg, "-a")) {
- merge_all();
-+ err |= merge_all_index(&the_index, one_shot, quiet,
++ err |= merge_all_index(the_repository, one_shot, quiet,
+ merge_one_file_spawn, (void *)pgm);
continue;
}
die("git merge-index: unknown option %s", arg);
}
- merge_one_path(arg);
-+ err |= merge_index_path(&the_index, one_shot, quiet, arg,
++ err |= merge_index_path(the_repository, one_shot, quiet, arg,
+ merge_one_file_spawn, (void *)pgm);
}
- if (err && !quiet)
@@ merge-strategies.c: int merge_three_way(struct repository *r,
return 0;
}
+
-+int merge_one_file_spawn(const struct object_id *orig_blob,
++int merge_one_file_spawn(struct repository *r,
++ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
@@ merge-strategies.c: int merge_three_way(struct repository *r,
+ return run_command_v_opt(arguments, 0);
+}
+
-+static int merge_entry(struct index_state *istate, int quiet, int pos,
-+ const char *path, merge_fn fn, void *data)
++static int merge_entry(struct repository *r, int quiet, unsigned int pos,
++ const char *path, int *err, merge_fn fn, void *data)
+{
+ int found = 0;
+ const struct object_id *oids[3] = {NULL};
+ unsigned int modes[3] = {0};
+
+ do {
-+ const struct cache_entry *ce = istate->cache[pos];
++ const struct cache_entry *ce = r->index->cache[pos];
+ int stage = ce_stage(ce);
+
+ if (strcmp(ce->name, path))
@@ merge-strategies.c: int merge_three_way(struct repository *r,
+ found++;
+ oids[stage - 1] = &ce->oid;
+ modes[stage - 1] = ce->ce_mode;
-+ } while (++pos < istate->cache_nr);
++ } while (++pos < r->index->cache_nr);
+ if (!found)
+ return error(_("%s is not in the cache"), path);
+
-+ if (fn(oids[0], oids[1], oids[2], path, modes[0], modes[1], modes[2], data)) {
++ if (fn(r, oids[0], oids[1], oids[2], path,
++ modes[0], modes[1], modes[2], data)) {
+ if (!quiet)
+ error(_("Merge program failed"));
-+ return -2;
++ (*err)++;
+ }
+
+ return found;
+}
+
-+int merge_index_path(struct index_state *istate, int oneshot, int quiet,
++int merge_index_path(struct repository *r, int oneshot, int quiet,
+ const char *path, merge_fn fn, void *data)
+{
-+ int pos = index_name_pos(istate, path, strlen(path)), ret;
++ int pos = index_name_pos(r->index, path, strlen(path)), ret, err = 0;
+
+ /*
+ * If it already exists in the cache as stage0, it's
+ * already merged and there is nothing to do.
+ */
+ if (pos < 0) {
-+ ret = merge_entry(istate, quiet, -pos - 1, path, fn, data);
++ ret = merge_entry(r, quiet || oneshot, -pos - 1, path, &err, fn, data);
+ if (ret == -1)
+ return -1;
-+ else if (ret == -2)
++ else if (err)
+ return 1;
+ }
+ return 0;
+}
+
-+int merge_all_index(struct index_state *istate, int oneshot, int quiet,
++int merge_all_index(struct repository *r, int oneshot, int quiet,
+ merge_fn fn, void *data)
+{
-+ int err = 0, i, ret;
-+ for (i = 0; i < istate->cache_nr; i++) {
-+ const struct cache_entry *ce = istate->cache[i];
++ int err = 0, ret;
++ unsigned int i;
++
++ for (i = 0; i < r->index->cache_nr; i++) {
++ const struct cache_entry *ce = r->index->cache[i];
+ if (!ce_stage(ce))
+ continue;
+
-+ ret = merge_entry(istate, quiet, i, ce->name, fn, data);
++ ret = merge_entry(r, quiet || oneshot, i, ce->name, &err, fn, data);
+ if (ret > 0)
+ i += ret - 1;
+ else if (ret == -1)
+ return -1;
-+ else if (ret == -2) {
-+ if (oneshot)
-+ err++;
-+ else
-+ return 1;
-+ }
++
++ if (err && !oneshot)
++ return 1;
+ }
+
+ return err;
@@ merge-strategies.h: int merge_three_way(struct repository *r,
const struct object_id *their_blob, const char *path,
unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode);
-+typedef int (*merge_fn)(const struct object_id *orig_blob,
++typedef int (*merge_fn)(struct repository *r,
++ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
-+int merge_one_file_spawn(const struct object_id *orig_blob,
++int merge_one_file_spawn(struct repository *r,
++ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
-+int merge_index_path(struct index_state *istate, int oneshot, int quiet,
++int merge_index_path(struct repository *r, int oneshot, int quiet,
+ const char *path, merge_fn fn, void *data);
-+int merge_all_index(struct index_state *istate, int oneshot, int quiet,
++int merge_all_index(struct repository *r, int oneshot, int quiet,
+ merge_fn fn, void *data);
+
#endif /* MERGE_STRATEGIES_H */
5: 12775907c5 ! 6: acaf100edd merge-index: don't fork if the requested program is `git-merge-one-file'
@@ Commit message
`merge-index' to call merge_three_way() without forking using a new
callback, merge_one_file_func().
+ To avoid any issue with a shrinking index because of the merge function
+ used (directly in the process or by forking), as described earlier, the
+ iterator of the loop of merge_all_index() is increased by the number of
+ entries with the same name, minus the difference between the number of
+ entries in the index before and after the merge.
+
+ This should handle a shrinking index correctly, but could lead to issues
+ with a growing index. However, this case is not treated, as there is no
+ callback that can produce such a case.
+
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
## builtin/merge-index.c ##
@@ builtin/merge-index.c
{
int i, force_file = 0, err = 0, one_shot = 0, quiet = 0;
const char *pgm;
-+ void *data;
++ void *data = NULL;
+ merge_fn merge_action;
+ struct lock_file lock = LOCK_INIT;
@@ builtin/merge-index.c: int cmd_merge_index(int argc, const char **argv, const ch
}
+
pgm = argv[i++];
++ setup_work_tree();
++
+ if (!strcmp(pgm, "git-merge-one-file")) {
+ merge_action = merge_one_file_func;
-+ data = (void *)the_repository;
-+
-+ setup_work_tree();
+ hold_locked_index(&lock, LOCK_DIE_ON_ERROR);
+ } else {
+ merge_action = merge_one_file_spawn;
@@ builtin/merge-index.c: int cmd_merge_index(int argc, const char **argv, const ch
@@ builtin/merge-index.c: int cmd_merge_index(int argc, const char **argv, const char *prefix)
}
if (!strcmp(arg, "-a")) {
- err |= merge_all_index(&the_index, one_shot, quiet,
+ err |= merge_all_index(the_repository, one_shot, quiet,
- merge_one_file_spawn, (void *)pgm);
+ merge_action, data);
continue;
}
die("git merge-index: unknown option %s", arg);
}
- err |= merge_index_path(&the_index, one_shot, quiet, arg,
+ err |= merge_index_path(the_repository, one_shot, quiet, arg,
- merge_one_file_spawn, (void *)pgm);
+ merge_action, data);
+ }
@@ merge-strategies.c: int merge_three_way(struct repository *r,
return 0;
}
-+int merge_one_file_func(const struct object_id *orig_blob,
++int merge_one_file_func(struct repository *r,
++ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data)
+{
-+ return merge_three_way((struct repository *)data,
++ return merge_three_way(r,
+ orig_blob, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+}
+
- int merge_one_file_spawn(const struct object_id *orig_blob,
+ int merge_one_file_spawn(struct repository *r,
+ const struct object_id *orig_blob,
const struct object_id *our_blob,
- const struct object_id *their_blob, const char *path,
+@@ merge-strategies.c: int merge_all_index(struct repository *r, int oneshot, int quiet,
+ merge_fn fn, void *data)
+ {
+ int err = 0, ret;
+- unsigned int i;
++ unsigned int i, prev_nr;
+
+ for (i = 0; i < r->index->cache_nr; i++) {
+ const struct cache_entry *ce = r->index->cache[i];
+ if (!ce_stage(ce))
+ continue;
+
++ prev_nr = r->index->cache_nr;
+ ret = merge_entry(r, quiet || oneshot, i, ce->name, &err, fn, data);
+- if (ret > 0)
+- i += ret - 1;
+- else if (ret == -1)
++ if (ret > 0) {
++ /* Don't bother handling an index that has
++ grown, since merge_one_file_func() can't grow
++ it, and merge_one_file_spawn() can't change
++ it. */
++ i += ret - (prev_nr - r->index->cache_nr) - 1;
++ } else if (ret == -1)
+ return -1;
+
+ if (err && !oneshot)
## merge-strategies.h ##
-@@ merge-strategies.h: typedef int (*merge_fn)(const struct object_id *orig_blob,
+@@ merge-strategies.h: typedef int (*merge_fn)(struct repository *r,
unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
void *data);
-+int merge_one_file_func(const struct object_id *orig_blob,
++int merge_one_file_func(struct repository *r,
++ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
- int merge_one_file_spawn(const struct object_id *orig_blob,
+ int merge_one_file_spawn(struct repository *r,
+ const struct object_id *orig_blob,
const struct object_id *our_blob,
- const struct object_id *their_blob, const char *path,
6: 54a4a12504 ! 7: 9a9e3faeff merge-resolve: rewrite in C
@@ merge-strategies.c
#include "xdiff-interface.h"
static int checkout_from_index(struct index_state *istate, const char *path,
-@@ merge-strategies.c: int merge_all_index(struct index_state *istate, int oneshot, int quiet,
+@@ merge-strategies.c: int merge_all_index(struct repository *r, int oneshot, int quiet,
return err;
}
@@ merge-strategies.c: int merge_all_index(struct index_state *istate, int oneshot,
+
+ puts(_("Simple merge failed, trying Automatic merge."));
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
-+ ret = merge_all_index(r->index, 0, 0, merge_one_file_func, r);
++ ret = merge_all_index(r, 1, 0, merge_one_file_func, NULL);
+
+ write_locked_index(r->index, &lock, COMMIT_LOCK);
+ return !!ret;
@@ merge-strategies.h
#include "object.h"
int merge_three_way(struct repository *r,
-@@ merge-strategies.h: int merge_index_path(struct index_state *istate, int oneshot, int quiet,
- int merge_all_index(struct index_state *istate, int oneshot, int quiet,
+@@ merge-strategies.h: int merge_index_path(struct repository *r, int oneshot, int quiet,
+ int merge_all_index(struct repository *r, int oneshot, int quiet,
merge_fn fn, void *data);
+int merge_strategies_resolve(struct repository *r,
7: 7c4ad06b95 = 8: 359346229c merge-recursive: move better_branch_name() to merge.c
8: edbe08d41b ! 9: 4dff780212 merge-octopus: rewrite in C
@@ merge-strategies.c: int merge_strategies_resolve(struct repository *r,
+ struct object_id oid;
+ int ret;
+
-+ if (!(ret = write_index_as_tree(&oid, r->index, r->index_file, 0, NULL)))
++ if (!(ret = write_index_as_tree(&oid, r->index, r->index_file,
++ WRITE_TREE_SILENT, NULL)))
+ *reference_tree = lookup_tree(r, &oid);
+
+ return ret;
@@ merge-strategies.c: int merge_strategies_resolve(struct repository *r,
+
+ puts(_("Simple merge did not work, trying automatic merge."));
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
-+ ret = merge_all_index(r->index, 0, 0, merge_one_file_func, r);
++ ret = merge_all_index(r, 1, 0, merge_one_file_func, NULL);
+ write_locked_index(r->index, &lock, COMMIT_LOCK);
+
+ write_tree(r, reference_tree);
@@ merge-strategies.c: int merge_strategies_resolve(struct repository *r,
+ struct commit_list *remotes)
+{
+ int ff_merge = 1, ret = 0, references = 1;
-+ struct commit **reference_commit;
-+ struct tree *reference_tree, *tree_head;
++ struct commit **reference_commit, *head_commit;
++ struct tree *reference_tree, *head_tree;
+ struct commit_list *i;
+ struct object_id head;
+ struct strbuf sb = STRBUF_INIT;
+
+ get_oid(head_arg, &head);
++ head_commit = lookup_commit_reference(r, &head);
++ head_tree = repo_get_commit_tree(r, head_commit);
+
-+ reference_commit = xcalloc(commit_list_count(remotes) + 1,
-+ sizeof(struct commit *));
-+ reference_commit[0] = lookup_commit_reference(r, &head);
-+ reference_tree = repo_get_commit_tree(r, reference_commit[0]);
++ if (parse_tree(head_tree))
++ return 2;
+
-+ tree_head = repo_get_commit_tree(r, reference_commit[0]);
-+ if (parse_tree(tree_head)) {
-+ ret = 2;
-+ goto out;
-+ }
-+
-+ if (repo_index_has_changes(r, reference_tree, &sb)) {
++ if (repo_index_has_changes(r, head_tree, &sb)) {
+ error(_("Your local changes to the following files "
+ "would be overwritten by merge:\n %s"),
+ sb.buf);
+ strbuf_release(&sb);
-+ ret = 2;
-+ goto out;
++ return 2;
+ }
+
++ reference_commit = xcalloc(commit_list_count(remotes) + 1,
++ sizeof(struct commit *));
++ reference_commit[0] = head_commit;
++ reference_tree = head_tree;
++
+ for (i = remotes; i && i->item; i = i->next) {
+ struct commit *c = i->item;
+ struct object_id *oid = &c->object.oid;
@@ merge-strategies.c: int merge_strategies_resolve(struct repository *r,
+ puts(_("Automated merge did not work."));
+ puts(_("Should not be doing an octopus."));
+
-+ ret = 2;
-+ goto out;
++ free(reference_commit);
++ return 2;
+ }
+
+ branch_name = merge_get_better_branch_name(oid_to_hex(oid));
@@ merge-strategies.c: int merge_strategies_resolve(struct repository *r,
+
+ free(branch_name);
+ free_commit_list(common);
++ free(reference_commit);
+
-+ ret = 2;
-+ goto out;
++ return 2;
+ }
+
+ for (j = common; j && !(up_to_date || !ff_merge); j = j->next) {
@@ merge-strategies.c: int merge_strategies_resolve(struct repository *r,
+ }
+
+ if (ff_merge) {
-+ ret = octopus_fast_forward(r, branch_name, tree_head,
++ ret = octopus_fast_forward(r, branch_name, head_tree,
+ current_tree, &reference_tree);
+ references = 0;
+ } else {
@@ merge-strategies.c: int merge_strategies_resolve(struct repository *r,
+ free_commit_list(common);
+
+ if (ret == -1)
-+ goto out;
++ break;
+
+ reference_commit[references++] = c;
+ }
+
-+out:
+ free(reference_commit);
+ return ret;
+}
## merge-strategies.h ##
-@@ merge-strategies.h: int merge_all_index(struct index_state *istate, int oneshot, int quiet,
+@@ merge-strategies.h: int merge_all_index(struct repository *r, int oneshot, int quiet,
int merge_strategies_resolve(struct repository *r,
struct commit_list *bases, const char *head_arg,
struct commit_list *remote);
9: e677b27c06 = 10: 76f02b4531 merge: use the "resolve" strategy without forking
10: 963f316fd6 = 11: c9e0a38d0f merge: use the "octopus" strategy without forking
11: 0ad967a7e5 ! 12: 5b595efa46 sequencer: use the "resolve" strategy without forking
@@ sequencer.c: static int do_pick_commit(struct repository *r,
+ if (!strcmp(opts->strategy, "resolve")) {
+ repo_read_index(r);
+ res |= merge_strategies_resolve(r, common, oid_to_hex(&head), remotes);
-+ } else
++ } else {
+ res |= try_merge_command(r, opts->strategy,
+ opts->xopts_nr, (const char **)opts->xopts,
+ common, oid_to_hex(&head), remotes);
++ }
+
free_commit_list(common);
free_commit_list(remotes);
12: 3814f61717 ! 13: 7eb0f13442 sequencer: use the "octopus" merge strategy without forking
@@ sequencer.c: static int do_pick_commit(struct repository *r,
+ } else if (!strcmp(opts->strategy, "octopus")) {
+ repo_read_index(r);
+ res |= merge_strategies_octopus(r, common, oid_to_hex(&head), remotes);
- } else
+ } else {
res |= try_merge_command(r, opts->strategy,
opts->xopts_nr, (const char **)opts->xopts,
--
2.29.2.260.ge31aba42fb
^ permalink raw reply [flat|nested] 221+ messages in thread
* [PATCH v6 01/13] t6407: modernise tests
2020-11-24 11:53 ` [PATCH v6 00/13] Rewrite the remaining merge strategies from shell to C Alban Gruin
@ 2020-11-24 11:53 ` Alban Gruin
2020-11-24 11:53 ` [PATCH v6 02/13] t6060: modify multiple files to expose a possible issue with merge-index Alban Gruin
` (14 subsequent siblings)
15 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-24 11:53 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
Some tests in t6407 uses a if/then/else to check if a command failed or
not, but we have the `test_must_fail' function to do it correctly for us
nowadays.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
t/t6407-merge-binary.sh | 27 ++++++---------------------
1 file changed, 6 insertions(+), 21 deletions(-)
diff --git a/t/t6407-merge-binary.sh b/t/t6407-merge-binary.sh
index 4e6c7cb77e..071d3f7343 100755
--- a/t/t6407-merge-binary.sh
+++ b/t/t6407-merge-binary.sh
@@ -5,7 +5,6 @@ test_description='ask merge-recursive to merge binary files'
. ./test-lib.sh
test_expect_success setup '
-
cat "$TEST_DIRECTORY"/test-binary-1.png >m &&
git add m &&
git ls-files -s | sed -e "s/ 0 / 1 /" >E1 &&
@@ -35,33 +34,19 @@ test_expect_success setup '
'
test_expect_success resolve '
-
rm -f a* m* &&
git reset --hard anchor &&
-
- if git merge -s resolve master
- then
- echo Oops, should not have succeeded
- false
- else
- git ls-files -s >current
- test_cmp expect current
- fi
+ test_must_fail git merge -s resolve master &&
+ git ls-files -s >current &&
+ test_cmp expect current
'
test_expect_success recursive '
-
rm -f a* m* &&
git reset --hard anchor &&
-
- if git merge -s recursive master
- then
- echo Oops, should not have succeeded
- false
- else
- git ls-files -s >current
- test_cmp expect current
- fi
+ test_must_fail git merge -s recursive master &&
+ git ls-files -s >current &&
+ test_cmp expect current
'
test_done
--
2.29.2.260.ge31aba42fb
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v6 02/13] t6060: modify multiple files to expose a possible issue with merge-index
2020-11-24 11:53 ` [PATCH v6 00/13] Rewrite the remaining merge strategies from shell to C Alban Gruin
2020-11-24 11:53 ` [PATCH v6 01/13] t6407: modernise tests Alban Gruin
@ 2020-11-24 11:53 ` Alban Gruin
2020-11-24 11:53 ` [PATCH v6 03/13] update-index: move add_cacheinfo() to read-cache.c Alban Gruin
` (13 subsequent siblings)
15 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-24 11:53 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
Currently, merge-index iterates over every index entry, skipping stage0
entries. It will then count how many entries following the current one
have the same name, then fork to do the merge. It will then increase
the iterator by the number of entries to skip them. This behaviour is
correct, as even if the subprocess modifies the index, merge-index does
not reload it at all.
But when it will be rewritten to use a function, the index it will use
will be modified and may shrink when a conflict happens or if a file is
removed, so we have to be careful to handle such cases.
Here is an example:
* Merge branches, file1 and file2 are trivially mergeable.
|\
| * Modifies file1 and file2.
* | Modifies file1 and file2.
|/
* Adds file1 and file2.
When the merge happens, the index will look like that:
i -> 0. file1 (stage1)
1. file1 (stage2)
2. file1 (stage3)
3. file2 (stage1)
4. file2 (stage2)
5. file2 (stage3)
merge-index handles `file1' first. As it appears 3 times after the
iterator, it is merged. The index is now stale, `i' is increased by 3,
and the index now looks like this:
0. file1 (stage1)
1. file1 (stage2)
2. file1 (stage3)
i -> 3. file2 (stage1)
4. file2 (stage2)
5. file2 (stage3)
`file2' appears three times too, so it is merged.
With a naive rewrite, the index would look like this:
0. file1 (stage0)
1. file2 (stage1)
2. file2 (stage2)
i -> 3. file2 (stage3)
`file2' appears once at the iterator or after, so it will be added,
_not_ merged. Which is wrong.
A naive rewrite would lead to unproperly merged files, or even files not
handled at all.
This changes t6060 to reproduce this case, by creating 2 files instead
of 1, to check the correctness of the soon-to-be-rewritten merge-index.
The files are identical, which is not really important -- the factors
that could trigger this issue are that they should be separated by at
most one entry in the index, and that the first one in the index should
be trivially mergeable.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
t/t6060-merge-index.sh | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/t/t6060-merge-index.sh b/t/t6060-merge-index.sh
index ddf34f0115..9e15ceb957 100755
--- a/t/t6060-merge-index.sh
+++ b/t/t6060-merge-index.sh
@@ -7,16 +7,19 @@ test_expect_success 'setup diverging branches' '
for i in 1 2 3 4 5 6 7 8 9 10; do
echo $i
done >file &&
- git add file &&
+ cp file file2 &&
+ git add file file2 &&
git commit -m base &&
git tag base &&
sed s/2/two/ <file >tmp &&
mv tmp file &&
+ cp file file2 &&
git commit -a -m two &&
git tag two &&
git checkout -b other HEAD^ &&
sed s/10/ten/ <file >tmp &&
mv tmp file &&
+ cp file file2 &&
git commit -a -m ten &&
git tag ten
'
@@ -35,8 +38,11 @@ ten
EOF
test_expect_success 'read-tree does not resolve content merge' '
+ cat >expect <<-\EOF &&
+ file
+ file2
+ EOF
git read-tree -i -m base ten two &&
- echo file >expect &&
git diff-files --name-only --diff-filter=U >unmerged &&
test_cmp expect unmerged
'
--
2.29.2.260.ge31aba42fb
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v6 03/13] update-index: move add_cacheinfo() to read-cache.c
2020-11-24 11:53 ` [PATCH v6 00/13] Rewrite the remaining merge strategies from shell to C Alban Gruin
2020-11-24 11:53 ` [PATCH v6 01/13] t6407: modernise tests Alban Gruin
2020-11-24 11:53 ` [PATCH v6 02/13] t6060: modify multiple files to expose a possible issue with merge-index Alban Gruin
@ 2020-11-24 11:53 ` Alban Gruin
2020-12-22 20:54 ` Junio C Hamano
2020-11-24 11:53 ` [PATCH v6 04/13] merge-one-file: rewrite in C Alban Gruin
` (12 subsequent siblings)
15 siblings, 1 reply; 221+ messages in thread
From: Alban Gruin @ 2020-11-24 11:53 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
This moves the function add_cacheinfo() that already exists in
update-index.c to update-index.c, renames it add_to_index_cacheinfo(),
and adds an `istate' parameter. The new cache entry is returned through
a pointer passed in the parameters. The return value is either 0
(success), -1 (invalid path), or -2 (failed to add the file in the
index).
This will become useful in the next commit, when the three-way merge
will need to call this function.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/update-index.c | 25 +++++++------------------
cache.h | 5 +++++
read-cache.c | 35 +++++++++++++++++++++++++++++++++++
3 files changed, 47 insertions(+), 18 deletions(-)
diff --git a/builtin/update-index.c b/builtin/update-index.c
index 79087bccea..44862f5e1d 100644
--- a/builtin/update-index.c
+++ b/builtin/update-index.c
@@ -404,27 +404,16 @@ static int process_path(const char *path, struct stat *st, int stat_errno)
static int add_cacheinfo(unsigned int mode, const struct object_id *oid,
const char *path, int stage)
{
- int len, option;
- struct cache_entry *ce;
+ int res;
- if (!verify_path(path, mode))
- return error("Invalid path '%s'", path);
-
- len = strlen(path);
- ce = make_empty_cache_entry(&the_index, len);
-
- oidcpy(&ce->oid, oid);
- memcpy(ce->name, path, len);
- ce->ce_flags = create_ce_flags(stage);
- ce->ce_namelen = len;
- ce->ce_mode = create_ce_mode(mode);
- if (assume_unchanged)
- ce->ce_flags |= CE_VALID;
- option = allow_add ? ADD_CACHE_OK_TO_ADD : 0;
- option |= allow_replace ? ADD_CACHE_OK_TO_REPLACE : 0;
- if (add_cache_entry(ce, option))
+ res = add_to_index_cacheinfo(&the_index, mode, oid, path, stage,
+ allow_add, allow_replace, NULL);
+ if (res == -1)
+ return res;
+ if (res == -2)
return error("%s: cannot add to the index - missing --add option?",
path);
+
report("add '%s'", path);
return 0;
}
diff --git a/cache.h b/cache.h
index c0072d43b1..be16ab3215 100644
--- a/cache.h
+++ b/cache.h
@@ -830,6 +830,11 @@ int remove_file_from_index(struct index_state *, const char *path);
int add_to_index(struct index_state *, const char *path, struct stat *, int flags);
int add_file_to_index(struct index_state *, const char *path, int flags);
+int add_to_index_cacheinfo(struct index_state *, unsigned int mode,
+ const struct object_id *oid, const char *path,
+ int stage, int allow_add, int allow_replace,
+ struct cache_entry **pce);
+
int chmod_index_entry(struct index_state *, struct cache_entry *ce, char flip);
int ce_same_name(const struct cache_entry *a, const struct cache_entry *b);
void set_object_name_for_intent_to_add_entry(struct cache_entry *ce);
diff --git a/read-cache.c b/read-cache.c
index ecf6f68994..c25f951db4 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -1350,6 +1350,41 @@ int add_index_entry(struct index_state *istate, struct cache_entry *ce, int opti
return 0;
}
+int add_to_index_cacheinfo(struct index_state *istate, unsigned int mode,
+ const struct object_id *oid, const char *path,
+ int stage, int allow_add, int allow_replace,
+ struct cache_entry **pce)
+{
+ int len, option;
+ struct cache_entry *ce = NULL;
+
+ if (!verify_path(path, mode))
+ return error(_("Invalid path '%s'"), path);
+
+ len = strlen(path);
+ ce = make_empty_cache_entry(istate, len);
+
+ oidcpy(&ce->oid, oid);
+ memcpy(ce->name, path, len);
+ ce->ce_flags = create_ce_flags(stage);
+ ce->ce_namelen = len;
+ ce->ce_mode = create_ce_mode(mode);
+ if (assume_unchanged)
+ ce->ce_flags |= CE_VALID;
+ option = allow_add ? ADD_CACHE_OK_TO_ADD : 0;
+ option |= allow_replace ? ADD_CACHE_OK_TO_REPLACE : 0;
+
+ if (add_index_entry(istate, ce, option)) {
+ discard_cache_entry(ce);
+ return -2;
+ }
+
+ if (pce)
+ *pce = ce;
+
+ return 0;
+}
+
/*
* "refresh" does not calculate a new sha1 file or bring the
* cache up-to-date for mode/content changes. But what it
--
2.29.2.260.ge31aba42fb
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v6 04/13] merge-one-file: rewrite in C
2020-11-24 11:53 ` [PATCH v6 00/13] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (2 preceding siblings ...)
2020-11-24 11:53 ` [PATCH v6 03/13] update-index: move add_cacheinfo() to read-cache.c Alban Gruin
@ 2020-11-24 11:53 ` Alban Gruin
2020-12-22 21:36 ` Junio C Hamano
2020-11-24 11:53 ` [PATCH v6 05/13] merge-index: libify merge_one_path() and merge_all() Alban Gruin
` (11 subsequent siblings)
15 siblings, 1 reply; 221+ messages in thread
From: Alban Gruin @ 2020-11-24 11:53 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
This rewrites `git merge-one-file' from shell to C. This port is not
completely straightforward: to save precious cycles by avoiding reading
and flushing the index repeatedly, write temporary files when an
operation can be performed in-memory, or allow other function to use the
rewrite without forking nor worrying about the index, the calls to
external processes are replaced by calls to functions in libgit.a:
- calls to `update-index --add --cacheinfo' are replaced by calls to
add_to_index_cacheinfo();
- calls to `update-index --remove' are replaced by calls to
remove_file_from_index();
- calls to `checkout-index -u -f' are replaced by calls to
checkout_entry();
- calls to `unpack-file' and `merge-files' are replaced by calls to
read_mmblob() and xdl_merge(), respectively, to merge files
in-memory;
- calls to `checkout-index -f --stage=2' are removed, as this is needed
to have the correct permission bits on the merged file from the
script, but not in the C version;
- calls to `update-index' are replaced by calls to add_file_to_index().
The bulk of the rewrite is done in a new file in libgit.a,
merge-strategies.c. This will enable the resolve and octopus strategies
to directly call it instead of forking.
This also fixes a bug present in the original script: instead of
checking if a _regular_ file exists when a file exists in the branch to
merge, but not in our branch, the rewritten version checks if a file of
any kind (ie. a directory, ...) exists. This fixes the tests t6035.14,
where the branch to merge had a new file, `a/b', but our branch had a
directory there; it should have failed because a directory exists, but
it did not because there was no regular file called `a/b'. This test is
now marked as successful.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Makefile | 3 +-
builtin.h | 1 +
builtin/merge-one-file.c | 94 +++++++++++++++++
git-merge-one-file.sh | 167 ------------------------------
git.c | 1 +
merge-strategies.c | 178 ++++++++++++++++++++++++++++++++
merge-strategies.h | 12 +++
t/t6415-merge-dir-to-symlink.sh | 2 +-
8 files changed, 289 insertions(+), 169 deletions(-)
create mode 100644 builtin/merge-one-file.c
delete mode 100755 git-merge-one-file.sh
create mode 100644 merge-strategies.c
create mode 100644 merge-strategies.h
diff --git a/Makefile b/Makefile
index de53954590..6dfdb33cb2 100644
--- a/Makefile
+++ b/Makefile
@@ -601,7 +601,6 @@ SCRIPT_SH += git-bisect.sh
SCRIPT_SH += git-difftool--helper.sh
SCRIPT_SH += git-filter-branch.sh
SCRIPT_SH += git-merge-octopus.sh
-SCRIPT_SH += git-merge-one-file.sh
SCRIPT_SH += git-merge-resolve.sh
SCRIPT_SH += git-mergetool.sh
SCRIPT_SH += git-quiltimport.sh
@@ -909,6 +908,7 @@ LIB_OBJS += match-trees.o
LIB_OBJS += mem-pool.o
LIB_OBJS += merge-blobs.o
LIB_OBJS += merge-recursive.o
+LIB_OBJS += merge-strategies.o
LIB_OBJS += merge.o
LIB_OBJS += mergesort.o
LIB_OBJS += midx.o
@@ -1094,6 +1094,7 @@ BUILTIN_OBJS += builtin/mailsplit.o
BUILTIN_OBJS += builtin/merge-base.o
BUILTIN_OBJS += builtin/merge-file.o
BUILTIN_OBJS += builtin/merge-index.o
+BUILTIN_OBJS += builtin/merge-one-file.o
BUILTIN_OBJS += builtin/merge-ours.o
BUILTIN_OBJS += builtin/merge-recursive.o
BUILTIN_OBJS += builtin/merge-tree.o
diff --git a/builtin.h b/builtin.h
index 53fb290963..4d2cd78856 100644
--- a/builtin.h
+++ b/builtin.h
@@ -178,6 +178,7 @@ int cmd_merge_base(int argc, const char **argv, const char *prefix);
int cmd_merge_index(int argc, const char **argv, const char *prefix);
int cmd_merge_ours(int argc, const char **argv, const char *prefix);
int cmd_merge_file(int argc, const char **argv, const char *prefix);
+int cmd_merge_one_file(int argc, const char **argv, const char *prefix);
int cmd_merge_recursive(int argc, const char **argv, const char *prefix);
int cmd_merge_tree(int argc, const char **argv, const char *prefix);
int cmd_mktag(int argc, const char **argv, const char *prefix);
diff --git a/builtin/merge-one-file.c b/builtin/merge-one-file.c
new file mode 100644
index 0000000000..9c21778e1d
--- /dev/null
+++ b/builtin/merge-one-file.c
@@ -0,0 +1,94 @@
+/*
+ * Builtin "git merge-one-file"
+ *
+ * Copyright (c) 2020 Alban Gruin
+ *
+ * Based on git-merge-one-file.sh, written by Linus Torvalds.
+ *
+ * This is the git per-file merge utility, called with
+ *
+ * argv[1] - original file object name (or empty)
+ * argv[2] - file in branch1 object name (or empty)
+ * argv[3] - file in branch2 object name (or empty)
+ * argv[4] - pathname in repository
+ * argv[5] - original file mode (or empty)
+ * argv[6] - file in branch1 mode (or empty)
+ * argv[7] - file in branch2 mode (or empty)
+ *
+ * Handle some trivial cases. The _really_ trivial cases have been
+ * handled already by git read-tree, but that one doesn't do any merges
+ * that might change the tree layout.
+ */
+
+#define USE_THE_INDEX_COMPATIBILITY_MACROS
+#include "cache.h"
+#include "builtin.h"
+#include "lockfile.h"
+#include "merge-strategies.h"
+
+static const char builtin_merge_one_file_usage[] =
+ "git merge-one-file <orig blob> <our blob> <their blob> <path> "
+ "<orig mode> <our mode> <their mode>\n\n"
+ "Blob ids and modes should be empty for missing files.";
+
+static int read_mode(const char *name, const char *arg, unsigned int *mode)
+{
+ char *last;
+ int ret = 0;
+
+ *mode = strtol(arg, &last, 8);
+
+ if (*last)
+ ret = error(_("invalid '%s' mode: expected nothing, got '%c'"), name, *last);
+ else if (!(S_ISREG(*mode) || S_ISDIR(*mode) || S_ISLNK(*mode)))
+ ret = error(_("invalid '%s' mode: %o"), name, *mode);
+
+ return ret;
+}
+
+int cmd_merge_one_file(int argc, const char **argv, const char *prefix)
+{
+ struct object_id orig_blob, our_blob, their_blob,
+ *p_orig_blob = NULL, *p_our_blob = NULL, *p_their_blob = NULL;
+ unsigned int orig_mode = 0, our_mode = 0, their_mode = 0, ret = 0;
+ struct lock_file lock = LOCK_INIT;
+
+ if (argc != 8)
+ usage(builtin_merge_one_file_usage);
+
+ if (read_cache() < 0)
+ die("invalid index");
+
+ hold_locked_index(&lock, LOCK_DIE_ON_ERROR);
+
+ if (!get_oid_hex(argv[1], &orig_blob)) {
+ p_orig_blob = &orig_blob;
+ ret = read_mode("orig", argv[5], &orig_mode);
+ } else if (!*argv[1] && *argv[5])
+ ret = error(_("no 'orig' object id given, but a mode was still given."));
+
+ if (!get_oid_hex(argv[2], &our_blob)) {
+ p_our_blob = &our_blob;
+ ret = read_mode("our", argv[6], &our_mode);
+ } else if (!*argv[2] && *argv[6])
+ ret = error(_("no 'our' object id given, but a mode was still given."));
+
+ if (!get_oid_hex(argv[3], &their_blob)) {
+ p_their_blob = &their_blob;
+ ret = read_mode("their", argv[7], &their_mode);
+ } else if (!*argv[3] && *argv[7])
+ ret = error(_("no 'their' object id given, but a mode was still given."));
+
+ if (ret)
+ return ret;
+
+ ret = merge_three_way(the_repository, p_orig_blob, p_our_blob, p_their_blob,
+ argv[4], orig_mode, our_mode, their_mode);
+
+ if (ret) {
+ rollback_lock_file(&lock);
+ return !!ret;
+ }
+
+ return write_locked_index(&the_index, &lock, COMMIT_LOCK);
+}
diff --git a/git-merge-one-file.sh b/git-merge-one-file.sh
deleted file mode 100755
index f6d9852d2f..0000000000
--- a/git-merge-one-file.sh
+++ /dev/null
@@ -1,167 +0,0 @@
-#!/bin/sh
-#
-# Copyright (c) Linus Torvalds, 2005
-#
-# This is the git per-file merge script, called with
-#
-# $1 - original file SHA1 (or empty)
-# $2 - file in branch1 SHA1 (or empty)
-# $3 - file in branch2 SHA1 (or empty)
-# $4 - pathname in repository
-# $5 - original file mode (or empty)
-# $6 - file in branch1 mode (or empty)
-# $7 - file in branch2 mode (or empty)
-#
-# Handle some trivial cases.. The _really_ trivial cases have
-# been handled already by git read-tree, but that one doesn't
-# do any merges that might change the tree layout.
-
-USAGE='<orig blob> <our blob> <their blob> <path>'
-USAGE="$USAGE <orig mode> <our mode> <their mode>"
-LONG_USAGE="usage: git merge-one-file $USAGE
-
-Blob ids and modes should be empty for missing files."
-
-SUBDIRECTORY_OK=Yes
-. git-sh-setup
-cd_to_toplevel
-require_work_tree
-
-if test $# != 7
-then
- echo "$LONG_USAGE"
- exit 1
-fi
-
-case "${1:-.}${2:-.}${3:-.}" in
-#
-# Deleted in both or deleted in one and unchanged in the other
-#
-"$1.." | "$1.$1" | "$1$1.")
- if { test -z "$6" && test "$5" != "$7"; } ||
- { test -z "$7" && test "$5" != "$6"; }
- then
- echo "ERROR: File $4 deleted on one branch but had its" >&2
- echo "ERROR: permissions changed on the other." >&2
- exit 1
- fi
-
- if test -n "$2"
- then
- echo "Removing $4"
- else
- # read-tree checked that index matches HEAD already,
- # so we know we do not have this path tracked.
- # there may be an unrelated working tree file here,
- # which we should just leave unmolested. Make sure
- # we do not have it in the index, though.
- exec git update-index --remove -- "$4"
- fi
- if test -f "$4"
- then
- rm -f -- "$4" &&
- rmdir -p "$(expr "z$4" : 'z\(.*\)/')" 2>/dev/null || :
- fi &&
- exec git update-index --remove -- "$4"
- ;;
-
-#
-# Added in one.
-#
-".$2.")
- # the other side did not add and we added so there is nothing
- # to be done, except making the path merged.
- exec git update-index --add --cacheinfo "$6" "$2" "$4"
- ;;
-"..$3")
- echo "Adding $4"
- if test -f "$4"
- then
- echo "ERROR: untracked $4 is overwritten by the merge." >&2
- exit 1
- fi
- git update-index --add --cacheinfo "$7" "$3" "$4" &&
- exec git checkout-index -u -f -- "$4"
- ;;
-
-#
-# Added in both, identically (check for same permissions).
-#
-".$3$2")
- if test "$6" != "$7"
- then
- echo "ERROR: File $4 added identically in both branches," >&2
- echo "ERROR: but permissions conflict $6->$7." >&2
- exit 1
- fi
- echo "Adding $4"
- git update-index --add --cacheinfo "$6" "$2" "$4" &&
- exec git checkout-index -u -f -- "$4"
- ;;
-
-#
-# Modified in both, but differently.
-#
-"$1$2$3" | ".$2$3")
-
- case ",$6,$7," in
- *,120000,*)
- echo "ERROR: $4: Not merging symbolic link changes." >&2
- exit 1
- ;;
- *,160000,*)
- echo "ERROR: $4: Not merging conflicting submodule changes." >&2
- exit 1
- ;;
- esac
-
- src1=$(git unpack-file $2)
- src2=$(git unpack-file $3)
- case "$1" in
- '')
- echo "Added $4 in both, but differently."
- orig=$(git unpack-file $(git hash-object /dev/null))
- ;;
- *)
- echo "Auto-merging $4"
- orig=$(git unpack-file $1)
- ;;
- esac
-
- git merge-file "$src1" "$orig" "$src2"
- ret=$?
- msg=
- if test $ret != 0 || test -z "$1"
- then
- msg='content conflict'
- ret=1
- fi
-
- # Create the working tree file, using "our tree" version from the
- # index, and then store the result of the merge.
- git checkout-index -f --stage=2 -- "$4" && cat "$src1" >"$4" || exit 1
- rm -f -- "$orig" "$src1" "$src2"
-
- if test "$6" != "$7"
- then
- if test -n "$msg"
- then
- msg="$msg, "
- fi
- msg="${msg}permissions conflict: $5->$6,$7"
- ret=1
- fi
-
- if test $ret != 0
- then
- echo "ERROR: $msg in $4" >&2
- exit 1
- fi
- exec git update-index -- "$4"
- ;;
-
-*)
- echo "ERROR: $4: Not handling case $1 -> $2 -> $3" >&2
- ;;
-esac
-exit 1
diff --git a/git.c b/git.c
index f1e8b56d99..a4d3f98094 100644
--- a/git.c
+++ b/git.c
@@ -540,6 +540,7 @@ static struct cmd_struct commands[] = {
{ "merge-file", cmd_merge_file, RUN_SETUP_GENTLY },
{ "merge-index", cmd_merge_index, RUN_SETUP | NO_PARSEOPT },
{ "merge-ours", cmd_merge_ours, RUN_SETUP | NO_PARSEOPT },
+ { "merge-one-file", cmd_merge_one_file, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-ours", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-theirs", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
diff --git a/merge-strategies.c b/merge-strategies.c
new file mode 100644
index 0000000000..20a328bf57
--- /dev/null
+++ b/merge-strategies.c
@@ -0,0 +1,178 @@
+#include "cache.h"
+#include "dir.h"
+#include "merge-strategies.h"
+#include "xdiff-interface.h"
+
+static int checkout_from_index(struct index_state *istate, const char *path,
+ struct cache_entry *ce)
+{
+ struct checkout state = CHECKOUT_INIT;
+
+ state.istate = istate;
+ state.force = 1;
+ state.base_dir = "";
+ state.base_dir_len = 0;
+
+ if (checkout_entry(ce, &state, NULL, NULL) < 0)
+ return error(_("%s: cannot checkout file"), path);
+ return 0;
+}
+
+static int merge_one_file_deleted(struct index_state *istate,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
+{
+ if ((our_blob && orig_mode != our_mode) ||
+ (their_blob && orig_mode != their_mode))
+ return error(_("File %s deleted on one branch but had its "
+ "permissions changed on the other."), path);
+
+ if (our_blob) {
+ printf(_("Removing %s\n"), path);
+
+ if (file_exists(path))
+ remove_path(path);
+ }
+
+ if (remove_file_from_index(istate, path))
+ return error("%s: cannot remove from the index", path);
+ return 0;
+}
+
+static int do_merge_one_file(struct index_state *istate,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
+{
+ int ret, i, dest;
+ ssize_t written;
+ mmbuffer_t result = {NULL, 0};
+ mmfile_t mmfs[3];
+ xmparam_t xmp = {{0}};
+
+ if (our_mode == S_IFLNK || their_mode == S_IFLNK)
+ return error(_("%s: Not merging symbolic link changes."), path);
+ else if (our_mode == S_IFGITLINK || their_mode == S_IFGITLINK)
+ return error(_("%s: Not merging conflicting submodule changes."), path);
+
+ if (orig_blob) {
+ printf(_("Auto-merging %s\n"), path);
+ read_mmblob(mmfs + 0, orig_blob);
+ } else {
+ printf(_("Added %s in both, but differently.\n"), path);
+ read_mmblob(mmfs + 0, &null_oid);
+ }
+
+ read_mmblob(mmfs + 1, our_blob);
+ read_mmblob(mmfs + 2, their_blob);
+
+ xmp.level = XDL_MERGE_ZEALOUS_ALNUM;
+ xmp.style = 0;
+ xmp.favor = 0;
+
+ ret = xdl_merge(mmfs + 0, mmfs + 1, mmfs + 2, &xmp, &result);
+
+ for (i = 0; i < 3; i++)
+ free(mmfs[i].ptr);
+
+ if (ret < 0) {
+ free(result.ptr);
+ return error(_("Failed to execute internal merge"));
+ }
+
+ if (ret > 0 || !orig_blob)
+ ret = error(_("content conflict in %s"), path);
+ if (our_mode != their_mode)
+ ret = error(_("permission conflict: %o->%o,%o in %s"),
+ orig_mode, our_mode, their_mode, path);
+
+ unlink(path);
+ if ((dest = open(path, O_WRONLY | O_CREAT, our_mode)) < 0) {
+ free(result.ptr);
+ return error_errno(_("failed to open file '%s'"), path);
+ }
+
+ written = write_in_full(dest, result.ptr, result.size);
+ close(dest);
+
+ free(result.ptr);
+
+ if (written < 0)
+ return error_errno(_("failed to write to '%s'"), path);
+ if (ret)
+ return ret;
+
+ return add_file_to_index(istate, path, 0);
+}
+
+int merge_three_way(struct repository *r,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode)
+{
+ if (orig_blob &&
+ ((!their_blob && our_blob && oideq(orig_blob, our_blob)) ||
+ (!our_blob && their_blob && oideq(orig_blob, their_blob)))) {
+ /* Deleted in both or deleted in one and unchanged in the other. */
+ return merge_one_file_deleted(r->index, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+ } else if (!orig_blob && our_blob && !their_blob) {
+ /*
+ * Added in one. The other side did not add and we
+ * added so there is nothing to be done, except making
+ * the path merged.
+ */
+ return add_to_index_cacheinfo(r->index, our_mode, our_blob,
+ path, 0, 1, 1, NULL);
+ } else if (!orig_blob && !our_blob && their_blob) {
+ struct cache_entry *ce;
+ printf(_("Adding %s\n"), path);
+
+ if (file_exists(path))
+ return error(_("untracked %s is overwritten by the merge."), path);
+
+ if (add_to_index_cacheinfo(r->index, their_mode, their_blob,
+ path, 0, 1, 1, &ce))
+ return -1;
+ return checkout_from_index(r->index, path, ce);
+ } else if (!orig_blob && our_blob && their_blob &&
+ oideq(our_blob, their_blob)) {
+ struct cache_entry *ce;
+
+ /* Added in both, identically (check for same permissions). */
+ if (our_mode != their_mode)
+ return error(_("File %s added identically in both branches, "
+ "but permissions conflict %o->%o."),
+ path, our_mode, their_mode);
+
+ printf(_("Adding %s\n"), path);
+
+ if (add_to_index_cacheinfo(r->index, our_mode, our_blob,
+ path, 0, 1, 1, &ce))
+ return -1;
+ return checkout_from_index(r->index, path, ce);
+ } else if (our_blob && their_blob) {
+ /* Modified in both, but differently. */
+ return do_merge_one_file(r->index,
+ orig_blob, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+ } else {
+ char orig_hex[GIT_MAX_HEXSZ] = {0}, our_hex[GIT_MAX_HEXSZ] = {0},
+ their_hex[GIT_MAX_HEXSZ] = {0};
+
+ if (orig_blob)
+ oid_to_hex_r(orig_hex, orig_blob);
+ if (our_blob)
+ oid_to_hex_r(our_hex, our_blob);
+ if (their_blob)
+ oid_to_hex_r(their_hex, their_blob);
+
+ return error(_("%s: Not handling case %s -> %s -> %s"),
+ path, orig_hex, our_hex, their_hex);
+ }
+
+ return 0;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
new file mode 100644
index 0000000000..e624c4f27c
--- /dev/null
+++ b/merge-strategies.h
@@ -0,0 +1,12 @@
+#ifndef MERGE_STRATEGIES_H
+#define MERGE_STRATEGIES_H
+
+#include "object.h"
+
+int merge_three_way(struct repository *r,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode);
+
+#endif /* MERGE_STRATEGIES_H */
diff --git a/t/t6415-merge-dir-to-symlink.sh b/t/t6415-merge-dir-to-symlink.sh
index 2eddcc7664..5fb74e39a0 100755
--- a/t/t6415-merge-dir-to-symlink.sh
+++ b/t/t6415-merge-dir-to-symlink.sh
@@ -94,7 +94,7 @@ test_expect_success SYMLINKS 'a/b was resolved as symlink' '
test -h a/b
'
-test_expect_failure 'do not lose untracked in merge (resolve)' '
+test_expect_success 'do not lose untracked in merge (resolve)' '
git reset --hard &&
git checkout baseline^0 &&
>a/b/c/e &&
--
2.29.2.260.ge31aba42fb
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v6 05/13] merge-index: libify merge_one_path() and merge_all()
2020-11-24 11:53 ` [PATCH v6 00/13] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (3 preceding siblings ...)
2020-11-24 11:53 ` [PATCH v6 04/13] merge-one-file: rewrite in C Alban Gruin
@ 2020-11-24 11:53 ` Alban Gruin
2021-01-05 15:59 ` Derrick Stolee
2020-11-24 11:53 ` [PATCH v6 06/13] merge-index: don't fork if the requested program is `git-merge-one-file' Alban Gruin
` (10 subsequent siblings)
15 siblings, 1 reply; 221+ messages in thread
From: Alban Gruin @ 2020-11-24 11:53 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
The "resolve" and "octopus" merge strategies do not call directly `git
merge-one-file', they delegate the work to another git command, `git
merge-index', that will loop over files in the index and call the
specified command. Unfortunately, these functions are not part of
libgit.a, which means that once rewritten, the strategies would still
have to invoke `merge-one-file' by spawning a new process first.
To avoid this, this moves and renames merge_one_path(), merge_all(), and
their helpers to merge-strategies.c. They also take a callback to
dictate what they should do for each file. For now, to preserve the
behaviour of `merge-index', only one callback, launching a new process,
is defined.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge-index.c | 77 +++----------------------------
merge-strategies.c | 104 ++++++++++++++++++++++++++++++++++++++++++
merge-strategies.h | 19 ++++++++
3 files changed, 130 insertions(+), 70 deletions(-)
diff --git a/builtin/merge-index.c b/builtin/merge-index.c
index 38ea6ad6ca..d5e5713b25 100644
--- a/builtin/merge-index.c
+++ b/builtin/merge-index.c
@@ -1,74 +1,11 @@
#define USE_THE_INDEX_COMPATIBILITY_MACROS
#include "builtin.h"
-#include "run-command.h"
-
-static const char *pgm;
-static int one_shot, quiet;
-static int err;
-
-static int merge_entry(int pos, const char *path)
-{
- int found;
- const char *arguments[] = { pgm, "", "", "", path, "", "", "", NULL };
- char hexbuf[4][GIT_MAX_HEXSZ + 1];
- char ownbuf[4][60];
-
- if (pos >= active_nr)
- die("git merge-index: %s not in the cache", path);
- found = 0;
- do {
- const struct cache_entry *ce = active_cache[pos];
- int stage = ce_stage(ce);
-
- if (strcmp(ce->name, path))
- break;
- found++;
- oid_to_hex_r(hexbuf[stage], &ce->oid);
- xsnprintf(ownbuf[stage], sizeof(ownbuf[stage]), "%o", ce->ce_mode);
- arguments[stage] = hexbuf[stage];
- arguments[stage + 4] = ownbuf[stage];
- } while (++pos < active_nr);
- if (!found)
- die("git merge-index: %s not in the cache", path);
-
- if (run_command_v_opt(arguments, 0)) {
- if (one_shot)
- err++;
- else {
- if (!quiet)
- die("merge program failed");
- exit(1);
- }
- }
- return found;
-}
-
-static void merge_one_path(const char *path)
-{
- int pos = cache_name_pos(path, strlen(path));
-
- /*
- * If it already exists in the cache as stage0, it's
- * already merged and there is nothing to do.
- */
- if (pos < 0)
- merge_entry(-pos-1, path);
-}
-
-static void merge_all(void)
-{
- int i;
- for (i = 0; i < active_nr; i++) {
- const struct cache_entry *ce = active_cache[i];
- if (!ce_stage(ce))
- continue;
- i += merge_entry(i, ce->name)-1;
- }
-}
+#include "merge-strategies.h"
int cmd_merge_index(int argc, const char **argv, const char *prefix)
{
- int i, force_file = 0;
+ int i, force_file = 0, err = 0, one_shot = 0, quiet = 0;
+ const char *pgm;
/* Without this we cannot rely on waitpid() to tell
* what happened to our children.
@@ -98,14 +35,14 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
continue;
}
if (!strcmp(arg, "-a")) {
- merge_all();
+ err |= merge_all_index(the_repository, one_shot, quiet,
+ merge_one_file_spawn, (void *)pgm);
continue;
}
die("git merge-index: unknown option %s", arg);
}
- merge_one_path(arg);
+ err |= merge_index_path(the_repository, one_shot, quiet, arg,
+ merge_one_file_spawn, (void *)pgm);
}
- if (err && !quiet)
- die("merge program failed");
return err;
}
diff --git a/merge-strategies.c b/merge-strategies.c
index 20a328bf57..6f27e66dfe 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -1,6 +1,7 @@
#include "cache.h"
#include "dir.h"
#include "merge-strategies.h"
+#include "run-command.h"
#include "xdiff-interface.h"
static int checkout_from_index(struct index_state *istate, const char *path,
@@ -176,3 +177,106 @@ int merge_three_way(struct repository *r,
return 0;
}
+
+int merge_one_file_spawn(struct repository *r,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data)
+{
+ char oids[3][GIT_MAX_HEXSZ + 1] = {{0}};
+ char modes[3][10] = {{0}};
+ const char *arguments[] = { (char *)data, oids[0], oids[1], oids[2],
+ path, modes[0], modes[1], modes[2], NULL };
+
+ if (orig_blob) {
+ oid_to_hex_r(oids[0], orig_blob);
+ xsnprintf(modes[0], sizeof(modes[0]), "%06o", orig_mode);
+ }
+
+ if (our_blob) {
+ oid_to_hex_r(oids[1], our_blob);
+ xsnprintf(modes[1], sizeof(modes[1]), "%06o", our_mode);
+ }
+
+ if (their_blob) {
+ oid_to_hex_r(oids[2], their_blob);
+ xsnprintf(modes[2], sizeof(modes[2]), "%06o", their_mode);
+ }
+
+ return run_command_v_opt(arguments, 0);
+}
+
+static int merge_entry(struct repository *r, int quiet, unsigned int pos,
+ const char *path, int *err, merge_fn fn, void *data)
+{
+ int found = 0;
+ const struct object_id *oids[3] = {NULL};
+ unsigned int modes[3] = {0};
+
+ do {
+ const struct cache_entry *ce = r->index->cache[pos];
+ int stage = ce_stage(ce);
+
+ if (strcmp(ce->name, path))
+ break;
+ found++;
+ oids[stage - 1] = &ce->oid;
+ modes[stage - 1] = ce->ce_mode;
+ } while (++pos < r->index->cache_nr);
+ if (!found)
+ return error(_("%s is not in the cache"), path);
+
+ if (fn(r, oids[0], oids[1], oids[2], path,
+ modes[0], modes[1], modes[2], data)) {
+ if (!quiet)
+ error(_("Merge program failed"));
+ (*err)++;
+ }
+
+ return found;
+}
+
+int merge_index_path(struct repository *r, int oneshot, int quiet,
+ const char *path, merge_fn fn, void *data)
+{
+ int pos = index_name_pos(r->index, path, strlen(path)), ret, err = 0;
+
+ /*
+ * If it already exists in the cache as stage0, it's
+ * already merged and there is nothing to do.
+ */
+ if (pos < 0) {
+ ret = merge_entry(r, quiet || oneshot, -pos - 1, path, &err, fn, data);
+ if (ret == -1)
+ return -1;
+ else if (err)
+ return 1;
+ }
+ return 0;
+}
+
+int merge_all_index(struct repository *r, int oneshot, int quiet,
+ merge_fn fn, void *data)
+{
+ int err = 0, ret;
+ unsigned int i;
+
+ for (i = 0; i < r->index->cache_nr; i++) {
+ const struct cache_entry *ce = r->index->cache[i];
+ if (!ce_stage(ce))
+ continue;
+
+ ret = merge_entry(r, quiet || oneshot, i, ce->name, &err, fn, data);
+ if (ret > 0)
+ i += ret - 1;
+ else if (ret == -1)
+ return -1;
+
+ if (err && !oneshot)
+ return 1;
+ }
+
+ return err;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
index e624c4f27c..94c40635c4 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -9,4 +9,23 @@ int merge_three_way(struct repository *r,
const struct object_id *their_blob, const char *path,
unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode);
+typedef int (*merge_fn)(struct repository *r,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
+int merge_one_file_spawn(struct repository *r,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
+int merge_index_path(struct repository *r, int oneshot, int quiet,
+ const char *path, merge_fn fn, void *data);
+int merge_all_index(struct repository *r, int oneshot, int quiet,
+ merge_fn fn, void *data);
+
#endif /* MERGE_STRATEGIES_H */
--
2.29.2.260.ge31aba42fb
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v6 06/13] merge-index: don't fork if the requested program is `git-merge-one-file'
2020-11-24 11:53 ` [PATCH v6 00/13] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (4 preceding siblings ...)
2020-11-24 11:53 ` [PATCH v6 05/13] merge-index: libify merge_one_path() and merge_all() Alban Gruin
@ 2020-11-24 11:53 ` Alban Gruin
2021-01-05 16:11 ` Derrick Stolee
2020-11-24 11:53 ` [PATCH v6 07/13] merge-resolve: rewrite in C Alban Gruin
` (9 subsequent siblings)
15 siblings, 1 reply; 221+ messages in thread
From: Alban Gruin @ 2020-11-24 11:53 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
Since `git-merge-one-file' has been rewritten and libified, this teaches
`merge-index' to call merge_three_way() without forking using a new
callback, merge_one_file_func().
To avoid any issue with a shrinking index because of the merge function
used (directly in the process or by forking), as described earlier, the
iterator of the loop of merge_all_index() is increased by the number of
entries with the same name, minus the difference between the number of
entries in the index before and after the merge.
This should handle a shrinking index correctly, but could lead to issues
with a growing index. However, this case is not treated, as there is no
callback that can produce such a case.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge-index.c | 28 ++++++++++++++++++++++++++--
merge-strategies.c | 25 +++++++++++++++++++++----
merge-strategies.h | 7 +++++++
3 files changed, 54 insertions(+), 6 deletions(-)
diff --git a/builtin/merge-index.c b/builtin/merge-index.c
index d5e5713b25..60fcde579f 100644
--- a/builtin/merge-index.c
+++ b/builtin/merge-index.c
@@ -1,11 +1,15 @@
#define USE_THE_INDEX_COMPATIBILITY_MACROS
#include "builtin.h"
+#include "lockfile.h"
#include "merge-strategies.h"
int cmd_merge_index(int argc, const char **argv, const char *prefix)
{
int i, force_file = 0, err = 0, one_shot = 0, quiet = 0;
const char *pgm;
+ void *data = NULL;
+ merge_fn merge_action;
+ struct lock_file lock = LOCK_INIT;
/* Without this we cannot rely on waitpid() to tell
* what happened to our children.
@@ -26,7 +30,18 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
quiet = 1;
i++;
}
+
pgm = argv[i++];
+ setup_work_tree();
+
+ if (!strcmp(pgm, "git-merge-one-file")) {
+ merge_action = merge_one_file_func;
+ hold_locked_index(&lock, LOCK_DIE_ON_ERROR);
+ } else {
+ merge_action = merge_one_file_spawn;
+ data = (void *)pgm;
+ }
+
for (; i < argc; i++) {
const char *arg = argv[i];
if (!force_file && *arg == '-') {
@@ -36,13 +51,22 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix)
}
if (!strcmp(arg, "-a")) {
err |= merge_all_index(the_repository, one_shot, quiet,
- merge_one_file_spawn, (void *)pgm);
+ merge_action, data);
continue;
}
die("git merge-index: unknown option %s", arg);
}
err |= merge_index_path(the_repository, one_shot, quiet, arg,
- merge_one_file_spawn, (void *)pgm);
+ merge_action, data);
+ }
+
+ if (merge_action == merge_one_file_func) {
+ if (err) {
+ rollback_lock_file(&lock);
+ return err;
+ }
+
+ return write_locked_index(&the_index, &lock, COMMIT_LOCK);
}
return err;
}
diff --git a/merge-strategies.c b/merge-strategies.c
index 6f27e66dfe..542cefcf3d 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -178,6 +178,18 @@ int merge_three_way(struct repository *r,
return 0;
}
+int merge_one_file_func(struct repository *r,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data)
+{
+ return merge_three_way(r,
+ orig_blob, our_blob, their_blob, path,
+ orig_mode, our_mode, their_mode);
+}
+
int merge_one_file_spawn(struct repository *r,
const struct object_id *orig_blob,
const struct object_id *our_blob,
@@ -261,17 +273,22 @@ int merge_all_index(struct repository *r, int oneshot, int quiet,
merge_fn fn, void *data)
{
int err = 0, ret;
- unsigned int i;
+ unsigned int i, prev_nr;
for (i = 0; i < r->index->cache_nr; i++) {
const struct cache_entry *ce = r->index->cache[i];
if (!ce_stage(ce))
continue;
+ prev_nr = r->index->cache_nr;
ret = merge_entry(r, quiet || oneshot, i, ce->name, &err, fn, data);
- if (ret > 0)
- i += ret - 1;
- else if (ret == -1)
+ if (ret > 0) {
+ /* Don't bother handling an index that has
+ grown, since merge_one_file_func() can't grow
+ it, and merge_one_file_spawn() can't change
+ it. */
+ i += ret - (prev_nr - r->index->cache_nr) - 1;
+ } else if (ret == -1)
return -1;
if (err && !oneshot)
diff --git a/merge-strategies.h b/merge-strategies.h
index 94c40635c4..0b74d45431 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -16,6 +16,13 @@ typedef int (*merge_fn)(struct repository *r,
unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
void *data);
+int merge_one_file_func(struct repository *r,
+ const struct object_id *orig_blob,
+ const struct object_id *our_blob,
+ const struct object_id *their_blob, const char *path,
+ unsigned int orig_mode, unsigned int our_mode, unsigned int their_mode,
+ void *data);
+
int merge_one_file_spawn(struct repository *r,
const struct object_id *orig_blob,
const struct object_id *our_blob,
--
2.29.2.260.ge31aba42fb
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v6 07/13] merge-resolve: rewrite in C
2020-11-24 11:53 ` [PATCH v6 00/13] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (5 preceding siblings ...)
2020-11-24 11:53 ` [PATCH v6 06/13] merge-index: don't fork if the requested program is `git-merge-one-file' Alban Gruin
@ 2020-11-24 11:53 ` Alban Gruin
2020-11-24 11:53 ` [PATCH v6 08/13] merge-recursive: move better_branch_name() to merge.c Alban Gruin
` (8 subsequent siblings)
15 siblings, 0 replies; 221+ messages in thread
From: Alban Gruin @ 2020-11-24 11:53 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
This rewrites `git merge-resolve' from shell to C. As for `git
merge-one-file', this port is not completely straightforward and removes
calls to external processes to avoid reading and writing the index over
and over again.
- The call to `update-index -q --refresh' is replaced by a call to
refresh_index().
- The call to `read-tree' is replaced by a call to unpack_trees() (and
all the setup needed).
- The call to `write-tree' is replaced by a call to
write_index_as_tree().
- The call to `merge-index', needed to invoke `git merge-one-file', is
replaced by a call to the new merge_all_index() function.
The index is read in cmd_merge_resolve(), and is wrote back by
merge_strategies_resolve().
The parameters of merge_strategies_resolve() will be surprising at first
glance: why using a commit list for `bases' and `remote', where we could
use an oid array, and a pointer to an oid? Because, in a later commit,
try_merge_strategy() will be able to call merge_strategies_resolve()
directly, and it already uses a commit list for `bases' (`common') and
`remote' (`remoteheads'), and a string for `head_arg'. To reduce
frictions later, merge_strategies_resolve() takes the same types of
parameters.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
Makefile | 2 +-
builtin.h | 1 +
builtin/merge-resolve.c | 73 +++++++++++++++++++++++++++++++
git-merge-resolve.sh | 54 -----------------------
git.c | 1 +
merge-strategies.c | 95 +++++++++++++++++++++++++++++++++++++++++
merge-strategies.h | 5 +++
7 files changed, 176 insertions(+), 55 deletions(-)
create mode 100644 builtin/merge-resolve.c
delete mode 100755 git-merge-resolve.sh
diff --git a/Makefile b/Makefile
index 6dfdb33cb2..3cc6b192f1 100644
--- a/Makefile
+++ b/Makefile
@@ -601,7 +601,6 @@ SCRIPT_SH += git-bisect.sh
SCRIPT_SH += git-difftool--helper.sh
SCRIPT_SH += git-filter-branch.sh
SCRIPT_SH += git-merge-octopus.sh
-SCRIPT_SH += git-merge-resolve.sh
SCRIPT_SH += git-mergetool.sh
SCRIPT_SH += git-quiltimport.sh
SCRIPT_SH += git-request-pull.sh
@@ -1097,6 +1096,7 @@ BUILTIN_OBJS += builtin/merge-index.o
BUILTIN_OBJS += builtin/merge-one-file.o
BUILTIN_OBJS += builtin/merge-ours.o
BUILTIN_OBJS += builtin/merge-recursive.o
+BUILTIN_OBJS += builtin/merge-resolve.o
BUILTIN_OBJS += builtin/merge-tree.o
BUILTIN_OBJS += builtin/merge.o
BUILTIN_OBJS += builtin/mktag.o
diff --git a/builtin.h b/builtin.h
index 4d2cd78856..35e91c16d0 100644
--- a/builtin.h
+++ b/builtin.h
@@ -180,6 +180,7 @@ int cmd_merge_ours(int argc, const char **argv, const char *prefix);
int cmd_merge_file(int argc, const char **argv, const char *prefix);
int cmd_merge_one_file(int argc, const char **argv, const char *prefix);
int cmd_merge_recursive(int argc, const char **argv, const char *prefix);
+int cmd_merge_resolve(int argc, const char **argv, const char *prefix);
int cmd_merge_tree(int argc, const char **argv, const char *prefix);
int cmd_mktag(int argc, const char **argv, const char *prefix);
int cmd_mktree(int argc, const char **argv, const char *prefix);
diff --git a/builtin/merge-resolve.c b/builtin/merge-resolve.c
new file mode 100644
index 0000000000..dca31676b8
--- /dev/null
+++ b/builtin/merge-resolve.c
@@ -0,0 +1,73 @@
+/*
+ * Builtin "git merge-resolve"
+ *
+ * Copyright (c) 2020 Alban Gruin
+ *
+ * Based on git-merge-resolve.sh, written by Linus Torvalds and Junio C
+ * Hamano.
+ *
+ * Resolve two trees, using enhanced multi-base read-tree.
+ */
+
+#define USE_THE_INDEX_COMPATIBILITY_MACROS
+#include "cache.h"
+#include "builtin.h"
+#include "merge-strategies.h"
+
+static const char builtin_merge_resolve_usage[] =
+ "git merge-resolve <bases>... -- <head> <remote>";
+
+int cmd_merge_resolve(int argc, const char **argv, const char *prefix)
+{
+ int i, sep_seen = 0;
+ const char *head = NULL;
+ struct commit_list *bases = NULL, *remote = NULL;
+ struct commit_list **next_base = &bases;
+
+ if (argc < 5)
+ usage(builtin_merge_resolve_usage);
+
+ setup_work_tree();
+ if (read_cache() < 0)
+ die("invalid index");
+
+ /*
+ * The first parameters up to -- are merge bases; the rest are
+ * heads.
+ */
+ for (i = 1; i < argc; i++) {
+ if (!strcmp(argv[i], "--"))
+ sep_seen = 1;
+ else if (!strcmp(argv[i], "-h"))
+ usage(builtin_merge_resolve_usage);
+ else if (sep_seen && !head)
+ head = argv[i];
+ else {
+ struct object_id oid;
+ struct commit *commit;
+
+ if (get_oid(argv[i], &oid))
+ die("object %s not found.", argv[i]);
+
+ commit = lookup_commit_or_die(&oid, argv[i]);
+
+ if (sep_seen)
+ commit_list_insert(commit, &remote);
+ else
+ next_base = commit_list_append(commit, next_base);
+ }
+ }
+
+ /*
+ * Give up if we are given two or more remotes. Not handling
+ * octopus.
+ */
+ if (remote && remote->next)
+ return 2;
+
+ /* Give up if this is a baseless merge. */
+ if (!bases)
+ return 2;
+
+ return merge_strategies_resolve(the_repository, bases, head, remote);
+}
diff --git a/git-merge-resolve.sh b/git-merge-resolve.sh
deleted file mode 100755
index 343fe7bccd..0000000000
--- a/git-merge-resolve.sh
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2005 Linus Torvalds
-# Copyright (c) 2005 Junio C Hamano
-#
-# Resolve two trees, using enhanced multi-base read-tree.
-
-# The first parameters up to -- are merge bases; the rest are heads.
-bases= head= remotes= sep_seen=
-for arg
-do
- case ",$sep_seen,$head,$arg," in
- *,--,)
- sep_seen=yes
- ;;
- ,yes,,*)
- head=$arg
- ;;
- ,yes,*)
- remotes="$remotes$arg "
- ;;
- *)
- bases="$bases$arg "
- ;;
- esac
-done
-
-# Give up if we are given two or more remotes -- not handling octopus.
-case "$remotes" in
-?*' '?*)
- exit 2 ;;
-esac
-
-# Give up if this is a baseless merge.
-if test '' = "$bases"
-then
- exit 2
-fi
-
-git update-index -q --refresh
-git read-tree -u -m --aggressive $bases $head $remotes || exit 2
-echo "Trying simple merge."
-if result_tree=$(git write-tree 2>/dev/null)
-then
- exit 0
-else
- echo "Simple merge failed, trying Automatic merge."
- if git merge-index -o git-merge-one-file -a
- then
- exit 0
- else
- exit 1
- fi
-fi
diff --git a/git.c b/git.c
index a4d3f98094..64a1a1de41 100644
--- a/git.c
+++ b/git.c
@@ -544,6 +544,7 @@ static struct cmd_struct commands[] = {
{ "merge-recursive", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-ours", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-theirs", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
+ { "merge-resolve", cmd_merge_resolve, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-subtree", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-tree", cmd_merge_tree, RUN_SETUP | NO_PARSEOPT },
{ "mktag", cmd_mktag, RUN_SETUP | NO_PARSEOPT },
diff --git a/merge-strategies.c b/merge-strategies.c
index 542cefcf3d..9aa07e91b5 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -1,7 +1,10 @@
#include "cache.h"
+#include "cache-tree.h"
#include "dir.h"
+#include "lockfile.h"
#include "merge-strategies.h"
#include "run-command.h"
+#include "unpack-trees.h"
#include "xdiff-interface.h"
static int checkout_from_index(struct index_state *istate, const char *path,
@@ -297,3 +300,95 @@ int merge_all_index(struct repository *r, int oneshot, int quiet,
return err;
}
+
+static int fast_forward(struct repository *r, struct tree_desc *t,
+ int nr, int aggressive)
+{
+ struct unpack_trees_options opts;
+ struct lock_file lock = LOCK_INIT;
+
+ refresh_index(r->index, REFRESH_QUIET, NULL, NULL, NULL);
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+
+ memset(&opts, 0, sizeof(opts));
+ opts.head_idx = 1;
+ opts.src_index = r->index;
+ opts.dst_index = r->index;
+ opts.merge = 1;
+ opts.update = 1;
+ opts.aggressive = aggressive;
+
+ if (nr == 1)
+ opts.fn = oneway_merge;
+ else if (nr == 2) {
+ opts.fn = twoway_merge;
+ opts.initial_checkout = is_index_unborn(r->index);
+ } else if (nr >= 3) {
+ opts.fn = threeway_merge;
+ opts.head_idx = nr - 1;
+ }
+
+ if (unpack_trees(nr, t, &opts))
+ return -1;
+
+ if (write_locked_index(r->index, &lock, COMMIT_LOCK))
+ return error(_("unable to write new index file"));
+
+ return 0;
+}
+
+static int add_tree(struct tree *tree, struct tree_desc *t)
+{
+ if (parse_tree(tree))
+ return -1;
+
+ init_tree_desc(t, tree->buffer, tree->size);
+ return 0;
+}
+
+int merge_strategies_resolve(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remote)
+{
+ struct tree_desc t[MAX_UNPACK_TREES];
+ struct object_id head, oid;
+ struct commit_list *i;
+ int nr = 0;
+
+ if (head_arg)
+ get_oid(head_arg, &head);
+
+ puts(_("Trying simple merge."));
+
+ for (i = bases; i && i->item; i = i->next) {
+ if (add_tree(repo_get_commit_tree(r, i->item), t + (nr++)))
+ return 2;
+ }
+
+ if (head_arg) {
+ struct tree *tree = parse_tree_indirect(&head);
+ if (add_tree(tree, t + (nr++)))
+ return 2;
+ }
+
+ if (remote && add_tree(repo_get_commit_tree(r, remote->item), t + (nr++)))
+ return 2;
+
+ if (fast_forward(r, t, nr, 1))
+ return 2;
+
+ if (write_index_as_tree(&oid, r->index, r->index_file,
+ WRITE_TREE_SILENT, NULL)) {
+ int ret;
+ struct lock_file lock = LOCK_INIT;
+
+ puts(_("Simple merge failed, trying Automatic merge."));
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+ ret = merge_all_index(r, 1, 0, merge_one_file_func, NULL);
+
+ write_locked_index(r->index, &lock, COMMIT_LOCK);
+ return !!ret;
+ }
+
+ return 0;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
index 0b74d45431..47dcd71ad5 100644
--- a/merge-strategies.h
+++ b/merge-strategies.h
@@ -1,6 +1,7 @@
#ifndef MERGE_STRATEGIES_H
#define MERGE_STRATEGIES_H
+#include "commit.h"
#include "object.h"
int merge_three_way(struct repository *r,
@@ -35,4 +36,8 @@ int merge_index_path(struct repository *r, int oneshot, int quiet,
int merge_all_index(struct repository *r, int oneshot, int quiet,
merge_fn fn, void *data);
+int merge_strategies_resolve(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
+ struct commit_list *remote);
+
#endif /* MERGE_STRATEGIES_H */
--
2.29.2.260.ge31aba42fb
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v6 08/13] merge-recursive: move better_branch_name() to merge.c
2020-11-24 11:53 ` [PATCH v6 00/13] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (6 preceding siblings ...)
2020-11-24 11:53 ` [PATCH v6 07/13] merge-resolve: rewrite in C Alban Gruin
@ 2020-11-24 11:53 ` Alban Gruin
2021-01-05 16:19 ` Derrick Stolee
2020-11-24 11:53 ` [PATCH v6 09/13] merge-octopus: rewrite in C Alban Gruin
` (7 subsequent siblings)
15 siblings, 1 reply; 221+ messages in thread
From: Alban Gruin @ 2020-11-24 11:53 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
better_branch_name() will be used by merge-octopus once it is rewritten
in C, so instead of duplicating it, this moves this function
preventively inside an appropriate file in libgit.a. This function is
also renamed to reflect its usage by merge strategies.
Signed-off-by: Alban Gruin <alban.gruin@gmail.com>
---
builtin/merge-recursive.c | 16 ++--------------
cache.h | 2 +-
merge.c | 12 ++++++++++++
3 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/builtin/merge-recursive.c b/builtin/merge-recursive.c
index a4bfd8fc51..972243b5e9 100644
--- a/builtin/merge-recursive.c
+++ b/builtin/merge-recursive.c
@@ -8,18 +8,6 @@
static const char builtin_merge_recursive_usage[] =
"git %s <base>... -- <head> <remote> ...";
-static char *better_branch_name(const char *branch)
-{
- static char githead_env[8 + GIT_MAX_HEXSZ + 1];
- char *name;
-
- if (strlen(branch) != the_hash_algo->hexsz)
- return xstrdup(branch);
- xsnprintf(githead_env, sizeof(githead_env), "GITHEAD_%s", branch);
- name = getenv(githead_env);
- return xstrdup(name ? name : branch);
-}
-
int cmd_merge_recursive(int argc, const char **argv, const char *prefix)
{
const struct object_id *bases[21];
@@ -75,8 +63,8 @@ int cmd_merge_recursive(int argc, const char **argv, const char *prefix)
if (get_oid(o.branch2, &h2))
die(_("could not resolve ref '%s'"), o.branch2);
- o.branch1 = better1 = better_branch_name(o.branch1);
- o.branch2 = better2 = better_branch_name(o.branch2);
+ o.branch1 = better1 = merge_get_better_branch_name(o.branch1);
+ o.branch2 = better2 = merge_get_better_branch_name(o.branch2);
if (o.verbosity >= 3)
printf(_("Merging %s with %s\n"), o.branch1, o.branch2);
diff --git a/cache.h b/cache.h
index be16ab3215..2d844576ea 100644
--- a/cache.h
+++ b/cache.h
@@ -1933,7 +1933,7 @@ int checkout_fast_forward(struct repository *r,
const struct object_id *from,
const struct object_id *to,
int overwrite_ignore);
-
+char *merge_get_better_branch_name(const char *branch);
int sane_execvp(const char *file, char *const argv[]);
diff --git a/merge.c b/merge.c
index 5fb88af102..801d673c5f 100644
--- a/merge.c
+++ b/merge.c
@@ -109,3 +109,15 @@ int checkout_fast_forward(struct repository *r,
return error(_("unable to write new index file"));
return 0;
}
+
+char *merge_get_better_branch_name(const char *branch)
+{
+ static char githead_env[8 + GIT_MAX_HEXSZ + 1];
+ char *name;
+
+ if (strlen(branch) != the_hash_algo->hexsz)
+ return xstrdup(branch);
+ xsnprintf(githead_env, sizeof(githead_env), "GITHEAD_%s", branch);
+ name = getenv(githead_env);
+ return xstrdup(name ? name : branch);
+}
--
2.29.2.260.ge31aba42fb
^ permalink raw reply related [flat|nested] 221+ messages in thread
* [PATCH v6 09/13] merge-octopus: rewrite in C
2020-11-24 11:53 ` [PATCH v6 00/13] Rewrite the remaining merge strategies from shell to C Alban Gruin
` (7 preceding siblings ...)
2020-11-24 11:53 ` [PATCH v6 08/13] merge-recursive: move better_branch_name() to merge.c Alban Gruin
@ 2020-11-24 11:53 ` Alban Gruin
2021-01-05 16:40 ` Derrick Stolee
2020-11-24 11:53 ` [PATCH v6 10/13] merge: use the "resolve" strategy without forking Alban Gruin
` (6 subsequent siblings)
15 siblings, 1 reply; 221+ messages in thread
From: Alban Gruin @ 2020-11-24 11:53 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Phillip Wood, Alban Gruin
This rewrites `git merge-octopus' from shell to C. As for the two last
conversions, this port removes calls to external processes to avoid
reading and writing the index over and over again.
- Calls to `read-tree -u -m (--aggressive)?' are replaced by calls to
unpack_trees().
- The call to `write-tree' is repla