Hi Alban
On 09/08/2022 19:54, Alban Gruin wrote:
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
cache_tree_update(). This call is wrapped in a new function,
write_tree(). It is made to mimick write_index_as_tree() with
WRITE_TREE_SILENT flag, but without locking the index; this is taken
care directly in merge_strategies_resolve().
- 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 the new merge_all_index() function.
The index is read in cmd_merge_resolve(), and is wrote back by
merge_strategies_resolve(). This is to accomodate future applications:
in `git-merge', the index has already been read when the merge strategy
is called, so it would be redundant to read it again when the builtin
will be able to use merge_strategies_resolve() directly.
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.
git-merge-resolve will happily merge three trees, unfortunately using
lists of commits will break that.
merge_strategies_resolve() locks the index only once, at the beginning
of the merge, and releases it when the merge has been completed.
Signed-off-by: Alban Gruin <alban.gruin@xxxxxxxxx>
---
diff --git a/builtin/merge-resolve.c b/builtin/merge-resolve.c
new file mode 100644
index 0000000000..a51158ebf8
--- /dev/null
+++ b/builtin/merge-resolve.c
@@ -0,0 +1,63 @@
+/*
+ * 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, sep_seen = 0;
+ const char *head = NULL;
+ struct commit_list *bases = NULL, *remote = NULL;
+ struct commit_list **next_base = &bases;
+ struct repository *r = the_repository;
+
+ if (argc < 5)
+ usage(builtin_merge_resolve_usage);
I think it would be better to call parse_options() and then check argc.
That would give better error messages for unknown options and supports
'-h' for free.
I think we also need to call git_config(). I see that read-tree respects
submodule.recurse so I think we need the same here. I suspect we should
also be reading the merge config to respect merge.conflictStyle.
+ setup_work_tree();
+ if (repo_read_index(r) < 0)
+ die("invalid index");
This should probably be marked for translation.
+
+ /*
+ * 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]);
translation here as well.
+ commit = oideq(&oid, r->hash_algo->empty_tree) ?
+ NULL : lookup_commit_or_die(&oid, argv[i]);
As I said above, git-merge-resolve should be callable with trees I think.
+
+ if (sep_seen)
+ commit_list_insert(commit, &remote);
+ else
+ next_base = commit_list_append(commit, next_base);
+ }
+ }
+
+ return merge_strategies_resolve(r, bases, head, remote);
+}
diff --git a/merge-strategies.c b/merge-strategies.c
index 373b69c10b..30f225ae5f 100644
--- a/merge-strategies.c
+++ b/merge-strategies.c
@@ -1,9 +1,34 @@
#include "cache.h"
+#include "cache-tree.h"
#include "dir.h"
#include "entry.h"
+#include "lockfile.h"
#include "merge-strategies.h"
+#include "unpack-trees.h"
#include "xdiff-interface.h"
+static int check_index_is_head(struct repository *r, const char *head_arg)
+{
+ struct commit *head_commit;
+ struct tree *head_tree;
+ 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);
Can this all be replaced by a call to parse_tree_indirect(), we should
also handle an invalid HEAD.
+
+ 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);
This matches the script but I wonder why that did not check for unstaged
changes.
+int merge_strategies_resolve(struct repository *r,
+ struct commit_list *bases, const char *head_arg,
As well as the commit vs tree comments above I think that we should be
getting the callers to parse head rather than passing a string. Both
builtin/merge.c and sequencer.c have a struct commit we can use.
+ struct commit_list *remote)
+{
+ struct tree_desc t[MAX_UNPACK_TREES];
+ struct commit_list *i;
+ struct lock_file lock = LOCK_INIT;
+ int nr = 0, ret = 0;
+
+ /* Abort if index does not match head */
+ if (check_index_is_head(r, head_arg))
+ return 2;
+
+ /*
+ * 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;
+
+ 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++)))
This needs to check that we're not overrunning the end of t as
builtin/read-tree.c:list_trees() does.
Except for the tree issue the conversion of the script looks correct to
me and you have been careful to preserve the exit values.
Best Wishes
Phillip
+ return 2;
+ }
+
+ if (head_arg) {
+ struct object_id head;
+ struct tree *tree;
+
+ get_oid(head_arg, &head);
+ 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;
+
+ repo_hold_locked_index(r, &lock, LOCK_DIE_ON_ERROR);
+
+ if (merge_trees(r, t, nr, 1)) {
+ rollback_lock_file(&lock);
+ return 2;
+ }
+
+ if (write_tree(r)) {
+ puts(_("Simple merge failed, trying Automatic merge."));
+ ret = merge_all_index(r->index, 1, 0, merge_one_file_func, NULL);
+ }
+
+ if (write_locked_index(r->index, &lock, COMMIT_LOCK))
+ return !!error(_("unable to write new index file"));
+ return !!ret;
+}
diff --git a/merge-strategies.h b/merge-strategies.h
index 8705a550ca..bba4bf999c 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 index_state *istate,
@@ -28,4 +29,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 */