Subject: Re: [RFC PATCH] sequencer - tipped merge strategy

From: Junio C Hamano <[email-masked]>
Date: Fri, 03 Mar 2023 08:45:32 -0800

Edmundo Carmona Antoranz <[email-masked]> writes:

> When rebasing merge commits and dealing with conflicts, having the
> original merge commit as a reference can help us avoid some of
> them.
>
> With this patch, we leverage the original merge commit to handle the most
> obvious case:
> - HEAD tree has to match the tree of the first parent of the original merge
>   commit.
> - MERGE_HEAD tree has to match the tree of the second parent of the original
>   merge commit.
> - At least one tree in the merge bases of HEAD/MERGE_HEAD has to match
>   a tree in the merge bases of the parent commits of the original merge
>   commit.

The first two conditions look a bit too restrictive for the purpose
of reusing previous conflict resolution, while I am not sure ...

> If all of those conditions are met, we can safely use the tree of the
> original merge commit as the resulting tree of this merge that is being
> attempted at the time.

...if the "at least one" in the last condition is a safe and
sensible loosening; if it introduces a mismerge by ignoring bases
that are different from the original, then it is a bit too bold to
declare that we can safely use the tree of the original.

What was the motivating usecase behind this new feature?  Was it
more about reusing the structural merge conflict resolution, or
about the textual merge conflict resolution?  For the latter, after
doing the usual three-way file-level merge and seeing a conflicted
textual merge, requiring the match of the blob objects for only these
conflicted paths and taking the previous merge result may give you a
safe way to raising the chance to find more reusable merges.

> +static void load_tree_oids(struct oid_array *oids, struct commit_list *bases)
> +{
> +	struct commit_list *i;
> +	for (i = bases; i; i = i->next)

Using 'i' for a pointer looks novel.  Don't.

> +static int find_oid(const struct object_id *oid,
> +		void *data)

The result of unfolding these two lines would not be overly long, I suspect?

> +{
> +	struct oid_array *other_list = (struct oid_array *) data;
> +	int pos = oid_array_lookup(other_list, oid);
> +	return pos >= 0 ? 1 : 0;

That's an unusual way to say 

	return pos >= 0;

or even

	return 0 <= oid_array_lookup(other_list, oid);

without otherwise unused variable.

> +static int run_tms_merge(struct tms_options *options)
> +{
> +	struct commit *head, *merge_head, *tip;
> +	struct commit_list *i;
> +
> +	head = lookup_commit_reference_by_name("HEAD");
> +	merge_head = lookup_commit_reference_by_name(options->merge_head);
> +	tip = lookup_commit_reference_by_name(options->tip);
> +
> +	if (!(head && merge_head && tip)) {
> +		return 2;
> +	}

Unnecessary {} around a single statement block.

> +	if (commit_list_count(tip->parents) != 2)
> +		return 2;
> +
> +	for (i = tip->parents; i; i = i->next)
> +		parse_commit(i->item);
> +	if (!oideq(get_commit_tree_oid(head),
> +		   get_commit_tree_oid(tip->parents->item)))
> +		return 2;
> +	if (!oideq(get_commit_tree_oid(merge_head),
> +		   get_commit_tree_oid(tip->parents->next->item)))
> +		return 2;
> +
> +	if (!base_match(tip, head, merge_head))
> +		return 2;
> +
> +	if (restore(tip))
> +		return 2;

I somehow thought that reverting the trashed working tree and the
index to their original state was not the responsibility for a merge
strategy but for the caller?  Shouldn't this restoration be on the
caller's side?

Oh, has this code even touched anything in the working tree and the
index at this point?  It looks more like everything we did above in
order to punt by returning 2 was to see if the condition for us to
reuse the resulting tree holds and nothing else.

Ah, "restore()" is misnamed, perhaps?  I thought it was about "oh,
we made a mess and need to go back to the state that was given to us
before failing", but is this the real "ok, the condition holds and
we can just reuse the tree from the previous merge"?  Then it makes
sense for the code to attempt to check out that tree and return 2
when it fails.  Only the function name was misleading.

> +	if (!opts->strategy || !strcmp(opts->strategy, "tms")) {
> +		rollback_lock_file(&lock);
> +		ret = try_tms_merge(opts, commit, to_merge->item);
> +		if (ret) {
> +			discard_index(r->index);
> +			if (repo_read_index(r) < 0) {
> +				ret = error(_("could not read index"));
> +				goto leave_merge;
> +			}
> +			goto ran_merge;
> +		}
> +		// regain lock to go into recursive

No // comments here.

> +		if (repo_hold_locked_index(r, &lock, LOCK_REPORT_ON_ERROR) < 0) {