Skip to contentSkip to conversation

Complete merge-ort implementation...almost

PATCH Superseded Elijah Newren via GitGitGadget 4 participants 44 messages Mar 5, 2021 – Mar 17, 2021

lore
Share & tools
lore
#0 lore raw

cover Complete merge-ort implementation...almost

In order to help Ævar test his tree-walk changes against merge-ort[1], this series completes the merge-ort implementation and cleans up testsuite failures...EXCEPT for some t6423 failures. It also leaves out a lot of performance work, which incidentally will fix the t6423 failures and is being reviewed independently[2].

This 11-patch series could be submitted as 7 independent series, 1-4 patches in length each, but it's probably easier for Ævar if we can merge just one more thing and it's only 11 total patches. This series sub-divides as follows:

* Patch 1: Fix bug in already-merged portion of merge-ort affecting rename/rename conflicts on platforms where qsort isn't stable. (Could be considered for merging before 2.31 since it is a new bug in the 2.31 cycle that I just learned of last night, but not sure it matters since merge-ort wasn't complete anyway and we're not even mentioning merge-ort in the release notes.)
* Patches 2-5: Add support for renormalization
* Patch 6: Add support for subtree shifting
* Patch 7-8: Add test and support for conflicts affecting sparse-checkout entries
* Patch 9: Update submodule related merge tests to note the ones that merge-ort fixes relative to merge-recursive
* Patch 10: New feature -- allow "git diff AUTO_MERGE" during conflict resolution to let user review changes they made since merge/rebase/cherry-pick/revert stopped and informed them of conflicts
* Patch 11: Add comments noting various bugs in merge-recursive

The last two patches aren't needed by Ævar, so they could be left out and submitted later. I just figured that it was only two more patches and they were part of "completing the merge-ort implementation" in my view.

[1] https://lore.kernel.org/git/[email protected]/ [2] See
https://lore.kernel.org/git/[email protected]/;
there are five more waiting after that -- viewable by the curious at
https://github.com/gitgitgadget/git/pulls?q=is%3Apr+author%3Anewren+Optimization+batch

Elijah Newren (11):
merge-ort: use STABLE_QSORT instead of QSORT where required
merge-ort: add a special minimal index just for renormalization
merge-ort: add a function for initializing our special attr_index
merge-ort: have ll_merge() calls use the attr_index for
renormalization
merge-ort: let renormalization change modify/delete into clean delete
merge-ort: support subtree shifting
t6428: new test for SKIP_WORKTREE handling and conflicts
merge-ort: implement CE_SKIP_WORKTREE handling with conflicted entries
t: mark several submodule merging tests as fixed under merge-ort
merge-ort: write $GIT_DIR/AUTO_MERGE whenever we hit a conflict
merge-recursive: add a bunch of FIXME comments documenting known bugs

 branch.c                                      |   1 +
 builtin/rebase.c                              |   1 +
 merge-ort.c                                   | 230 ++++++++++++++++--
 merge-recursive.c                             |  37 +++
 path.c                                        |   1 +
 path.h                                        |   2 +
 sequencer.c                                   |   5 +
 t/t3512-cherry-pick-submodule.sh              |   9 +-
 t/t3513-revert-submodule.sh                   |   7 +-
 t/t5572-pull-submodule.sh                     |   9 +-
 t/t6428-merge-conflicts-sparse.sh             | 158 ++++++++++++
 t/t6437-submodule-merge.sh                    |   5 +-
 t/t6438-submodule-directory-file-conflicts.sh |   9 +-
 13 files changed, 449 insertions(+), 25 deletions(-)
 create mode 100755 t/t6428-merge-conflicts-sparse.sh

base-commit: f01623b2c9d14207e497b21ebc6b3ec4afaf4b46
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-973%2Fnewren%2Fort-remainder-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-973/newren/ort-remainder-v1
Pull-Request: https://github.com/git/git/pull/973

Signature · 1 line
gitgitgadget
#1 lore raw

PATCH 1/11 merge-ort: use STABLE_QSORT instead of QSORT where required

From: Elijah Newren <[email protected]>

rename/rename conflict handling depends on the fact that if both sides renamed the same path, that the one on the MERGE_SIDE1 will appear first in the combined diff_queue_struct passed to process_renames(). Since we add all pairs from MERGE_SIDE1 to combined first, and then all pairs from MERGE_SIDE2, and then sort based on filename, this will only be true if the sort used is stable. This was found due to the fact that Mac, unlike Linux, apparently has a system-defined qsort that is not stable.

While we are at it, review the other callers of QSORT and add comments about why they can remain as calls to QSORT instead of being modified to call STABLE_QSORT.

---
 merge-ort.c | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)
merge-ort.c +7 -1
index 603d30c52170..5309488fd9d8 100644
@@ -2289,7 +2289,7 @@ static int detect_and_process_renames(struct merge_options *opt,
 	clean &= collect_renames(opt, &combined, MERGE_SIDE2,
 				 &renames->dir_renames[1],
 				 &renames->dir_renames[2]);
-	QSORT(combined.queue, combined.nr, compare_pairs);
+	STABLE_QSORT(combined.queue, combined.nr, compare_pairs);
 	trace2_region_leave("merge", "directory renames", opt->repo);
 
 	trace2_region_enter("merge", "process renames", opt->repo);
@@ -2415,6 +2415,7 @@ static void write_tree(struct object_id *result_oid,
 	 */
 	relevant_entries.items = versions->items + offset;
 	relevant_entries.nr = versions->nr - offset;
+	/* No need for STABLE_QSORT -- filenames must be unique */
 	QSORT(relevant_entries.items, relevant_entries.nr, tree_entry_order);
 
 	/* Pre-allocate some space in buf */
@@ -3190,6 +3191,11 @@ static int record_conflicted_index_entries(struct merge_options *opt,
 	 * entries we added to the end into their right locations.
 	 */
 	remove_marked_cache_entries(index, 1);
+	/*
+	 * No need for STABLE_QSORT -- cmp_cache_name_compare sorts primarily
+	 * on filename and secondarily on stage, and (name, stage #) are a
+	 * unique tuple.
+	 */
 	QSORT(index->cache, index->cache_nr, cmp_cache_name_compare);
 
 	return errs;
Signature · 1 line
gitgitgadget
#2 lore raw

PATCH 2/11 merge-ort: add a special minimal index just for renormalization

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

renormalize_buffer() requires an index_state, which is something that merge-ort does not operate with. However, all the renormalization code needs is an index with a .gitattributes file...plus a little bit of setup. Create such an index, along with the deallocation and attr_direction handling.

A subsequent commit will add a function to finish the initialization of this index.

---
 merge-ort.c | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)
merge-ort.c +20 -0
index 5309488fd9d8..d91b66a052b6 100644
@@ -18,6 +18,7 @@
 #include "merge-ort.h"
 
 #include "alloc.h"
+#include "attr.h"
 #include "blob.h"
 #include "cache-tree.h"
 #include "commit.h"
@@ -170,6 +171,16 @@ struct merge_options_internal {
 	 */
 	struct rename_info renames;
 
+	/*
+	 * attr_index: hacky minimal index used for renormalization
+	 *
+	 * renormalization code _requires_ an index, though it only needs to
+	 * find a .gitattributes file within the index.  So, when
+	 * renormalization is important, we create a special index with just
+	 * that one file.
+	 */
+	struct index_state attr_index;
+
 	/*
 	 * current_dir_name, toplevel_dir: temporary vars
 	 *
@@ -349,6 +360,9 @@ static void clear_or_reinit_internal_opts(struct merge_options_internal *opti,
 	string_list_clear(&opti->paths_to_free, 0);
 	opti->paths_to_free.strdup_strings = 0;
 
+	if (opti->attr_index.cache_nr)
+		discard_index(&opti->attr_index);
+
 	/* Free memory used by various renames maps */
 	for (i = MERGE_SIDE1; i <= MERGE_SIDE2; ++i) {
 		struct hashmap_iter iter;
@@ -3272,6 +3286,8 @@ void merge_finalize(struct merge_options *opt,
 {
 	struct merge_options_internal *opti = result->priv;
 
+	if (opt->renormalize)
+		git_attr_set_direction(GIT_ATTR_CHECKIN);
 	assert(opt->priv == NULL);
 
 	clear_or_reinit_internal_opts(opti, 0);
@@ -3347,6 +3363,10 @@ static void merge_start(struct merge_options *opt, struct merge_result *result)
 	/* Default to histogram diff.  Actually, just hardcode it...for now. */
 	opt->xdl_opts = DIFF_WITH_ALG(opt, HISTOGRAM_DIFF);
 
+	/* Handle attr direction stuff for renormalization */
+	if (opt->renormalize)
+		git_attr_set_direction(GIT_ATTR_CHECKOUT);
+
 	/* Initialization of opt->priv, our internal merge data */
 	trace2_region_enter("merge", "allocate/init", opt->repo);
 	if (opt->priv) {
Signature · 1 line
gitgitgadget
#3 lore raw

PATCH 3/11 merge-ort: add a function for initializing our special attr_index

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Add a function which can be called to populate the attr_index with the appropriate .gitattributes contents when necessary. Make it return early if the attr_index is already initialized or if we are not renormalizing files.

NOTE 1: Even if the user has a working copy or a real index (which is not a given as merge-ort can be used in bare repositories), we explicitly ignore any .gitattributes file from either of these locations. merge-ort can be used to merge two branches that are unrelated to HEAD, so .gitattributes from the working copy and current index should not be considered relevant.

NOTE 2: Since we are in the middle of merging, there is a risk that .gitattributes itself is conflicted...leaving us with an ill-defined situation about how to perform the rest of the merge. It could be that the .gitattributes file does not even exist on one of the sides of the merge, or that it has been modified on both sides. If it's been modified on both sides, it's possible that it could itself be merged cleanly, though it's also possible that it only merges cleanly if you use the right version of the .gitattributes file to drive the merge. It gets kind of complicated. The only test we ever had that attempted to test behavior in this area was seemingly unaware of the undefined behavior, but knew the test wouldn't work for lack of attribute handling support, marked it as test_expect_failure from the beginning, but managed to fail for several reasons unrelated to attribute handling. See commit 6f6e7cfb52 ("t6038: remove problematic test", 2020-08-03) for details. So there are probably various ways to improve what initialize_attr_index() picks in the case of a conflicted .gitattributes but for now I just implemented something simple -- look for whatever .gitattributes file we can find in any of the higher order stages and use it.

---
 merge-ort.c | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 61 insertions(+)
merge-ort.c +61 -0
index d91b66a052b6..028d1adcd2c9 100644
@@ -988,6 +988,67 @@ static int merge_submodule(struct merge_options *opt,
 	return 0;
 }
 
+MAYBE_UNUSED
+static void initialize_attr_index(struct merge_options *opt)
+{
+	/*
+	 * The renormalize_buffer() functions require attributes, and
+	 * annoyingly those can only be read from the working tree or from
+	 * an index_state.  merge-ort doesn't have an index_state, so we
+	 * generate a fake one containing only attribute information.
+	 */
+	struct merged_info *mi;
+	struct index_state *attr_index = &opt->priv->attr_index;
+	struct cache_entry *ce;
+
+	if (!opt->renormalize)
+		return;
+
+	if (attr_index->initialized)
+		return;
+	attr_index->initialized = 1;
+
+	mi = strmap_get(&opt->priv->paths, GITATTRIBUTES_FILE);
+	if (!mi)
+		return;
+
+	if (mi->clean) {
+		int len = strlen(GITATTRIBUTES_FILE);
+		ce = make_empty_cache_entry(attr_index, len);
+		ce->ce_mode = create_ce_mode(mi->result.mode);
+		ce->ce_flags = create_ce_flags(0);
+		ce->ce_namelen = len;
+		oidcpy(&ce->oid, &mi->result.oid);
+		memcpy(ce->name, GITATTRIBUTES_FILE, len);
+		add_index_entry(attr_index, ce,
+				ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE);
+		get_stream_filter(attr_index, GITATTRIBUTES_FILE, &ce->oid);
+	}
+	else {
+		int stage, len;
+		struct conflict_info *ci;
+
+		ASSIGN_AND_VERIFY_CI(ci, mi);
+		for (stage=0; stage<3; ++stage) {
+			unsigned stage_mask = (1 << stage);
+
+			if (!(ci->filemask & stage_mask))
+				continue;
+			len = strlen(GITATTRIBUTES_FILE);
+			ce = make_empty_cache_entry(attr_index, len);
+			ce->ce_mode = create_ce_mode(ci->stages[stage].mode);
+			ce->ce_flags = create_ce_flags(stage);
+			ce->ce_namelen = len;
+			oidcpy(&ce->oid, &ci->stages[stage].oid);
+			memcpy(ce->name, GITATTRIBUTES_FILE, len);
+			add_index_entry(attr_index, ce,
+					ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE);
+			get_stream_filter(attr_index, GITATTRIBUTES_FILE,
+					  &ce->oid);
+		}
+	}
+}
+
 static int merge_3way(struct merge_options *opt,
 		      const char *path,
 		      const struct object_id *o,
Signature · 1 line
gitgitgadget
#4 lore raw

Re: [PATCH 03/11] merge-ort: add a function for initializing our special attr_index

Quoted 44 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Add a function which can be called to populate the attr_index with the appropriate .gitattributes contents when necessary. Make it return early if the attr_index is already initialized or if we are not renormalizing files.

NOTE 1: Even if the user has a working copy or a real index (which is not a given as merge-ort can be used in bare repositories), we explicitly ignore any .gitattributes file from either of these locations. merge-ort can be used to merge two branches that are unrelated to HEAD, so .gitattributes from the working copy and current index should not be considered relevant.

NOTE 2: Since we are in the middle of merging, there is a risk that .gitattributes itself is conflicted...leaving us with an ill-defined situation about how to perform the rest of the merge. It could be that the .gitattributes file does not even exist on one of the sides of the merge, or that it has been modified on both sides. If it's been modified on both sides, it's possible that it could itself be merged cleanly, though it's also possible that it only merges cleanly if you use the right version of the .gitattributes file to drive the merge. It gets kind of complicated. The only test we ever had that attempted to test behavior in this area was seemingly unaware of the undefined behavior, but knew the test wouldn't work for lack of attribute handling support, marked it as test_expect_failure from the beginning, but managed to fail for several reasons unrelated to attribute handling. See commit 6f6e7cfb52 ("t6038: remove problematic test", 2020-08-03) for details. So there are probably various ways to improve what initialize_attr_index() picks in the case of a conflicted .gitattributes but for now I just implemented something simple -- look for whatever .gitattributes file we can find in any of the higher order stages and use it.

---
 merge-ort.c | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 61 insertions(+)
merge-ort.c +0 -0
index d91b66a052b6..028d1adcd2c9 100644

merge-ort.c static int merge_submodule(struct merge_options *opt, · line 991

Quoted 4 lines
merge-ort.c +1 -0
@@ -988,6 +988,67 @@ static int merge_submodule(struct merge_options *opt,
 	return 0;
 }
 
+MAYBE_UNUSED

As with the lst series you had I also think this is better squashed with 04/11.

Quoted 7 lines
merge-ort.c +7 -0
+static void initialize_attr_index(struct merge_options *opt)
+{
+	/*
+	 * The renormalize_buffer() functions require attributes, and
+	 * annoyingly those can only be read from the working tree or from
+	 * an index_state.  merge-ort doesn't have an index_state, so we
+	 * generate a fake one containing only attribute information.

merge-ort.c static int merge_submodule(struct merge_options *opt, · line 1008

Quoted 10 lines
merge-ort.c +10 -0
+	 */
+	struct merged_info *mi;
+	struct index_state *attr_index = &opt->priv->attr_index;
+	struct cache_entry *ce;
+
+	if (!opt->renormalize)
+		return;
+
+	if (attr_index->initialized)
+		return;

Will comment on this in 04/11.

Quoted 9 lines
merge-ort.c +9 -0
+	attr_index->initialized = 1;
+
+	mi = strmap_get(&opt->priv->paths, GITATTRIBUTES_FILE);
+	if (!mi)
+		return;
+
+	if (mi->clean) {
+		int len = strlen(GITATTRIBUTES_FILE);
+		ce = make_empty_cache_entry(attr_index, len);

merge-ort.c static int merge_submodule(struct merge_options *opt, · line 1027

Quoted 10 lines
merge-ort.c +10 -0
+		ce->ce_mode = create_ce_mode(mi->result.mode);
+		ce->ce_flags = create_ce_flags(0);
+		ce->ce_namelen = len;
+		oidcpy(&ce->oid, &mi->result.oid);
+		memcpy(ce->name, GITATTRIBUTES_FILE, len);
+		add_index_entry(attr_index, ce,
+				ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE);
+		get_stream_filter(attr_index, GITATTRIBUTES_FILE, &ce->oid);
+	}
+	else {

Style nit: } else {

merge-ort.c static int merge_submodule(struct merge_options *opt, · line 1032

Quoted 5 lines
merge-ort.c +5 -0
+		int stage, len;
+		struct conflict_info *ci;
+
+		ASSIGN_AND_VERIFY_CI(ci, mi);
+		for (stage=0; stage<3; ++stage) {

Style nit: stage < 3

Style nit: I find just stage++ to be more readable in for-loops, makes no difference to the compiler, just more idiomatic.

Quoted 19 lines
merge-ort.c +19 -0
+			unsigned stage_mask = (1 << stage);
+
+			if (!(ci->filemask & stage_mask))
+				continue;
+			len = strlen(GITATTRIBUTES_FILE);
+			ce = make_empty_cache_entry(attr_index, len);
+			ce->ce_mode = create_ce_mode(ci->stages[stage].mode);
+			ce->ce_flags = create_ce_flags(stage);
+			ce->ce_namelen = len;
+			oidcpy(&ce->oid, &ci->stages[stage].oid);
+			memcpy(ce->name, GITATTRIBUTES_FILE, len);
+			add_index_entry(attr_index, ce,
+					ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE);
+			get_stream_filter(attr_index, GITATTRIBUTES_FILE,
+					  &ce->oid);
+		}
+	}
+}
+
#5 lore raw

PATCH 4/11 merge-ort: have ll_merge() calls use the attr_index for renormalization

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

ll_merge() needs an index when renormalization is requested. Give it the special one we created exactly for that purpose. This fixes t6418.4 and t6418.5 under GIT_TEST_MERGE_ALGORITHM=ort.

---
 merge-ort.c | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)
merge-ort.c +4 -3
index 028d1adcd2c9..87c553c0882c 100644
@@ -360,7 +360,7 @@ static void clear_or_reinit_internal_opts(struct merge_options_internal *opti,
 	string_list_clear(&opti->paths_to_free, 0);
 	opti->paths_to_free.strdup_strings = 0;
 
-	if (opti->attr_index.cache_nr)
+	if (opti->attr_index.cache_nr) /* true iff opt->renormalize */
 		discard_index(&opti->attr_index);
 
 	/* Free memory used by various renames maps */
@@ -988,7 +988,6 @@ static int merge_submodule(struct merge_options *opt,
 	return 0;
 }
 
-MAYBE_UNUSED
 static void initialize_attr_index(struct merge_options *opt)
 {
 	/*
@@ -1063,6 +1062,8 @@ static int merge_3way(struct merge_options *opt,
 	char *base, *name1, *name2;
 	int merge_status;
 
+	initialize_attr_index(opt);
+
 	ll_opts.renormalize = opt->renormalize;
 	ll_opts.extra_marker_size = extra_marker_size;
 	ll_opts.xdl_opts = opt->xdl_opts;
@@ -1101,7 +1102,7 @@ static int merge_3way(struct merge_options *opt,
 
 	merge_status = ll_merge(result_buf, path, &orig, base,
 				&src1, name1, &src2, name2,
-				opt->repo->index, &ll_opts);
+				&opt->priv->attr_index, &ll_opts);
 
 	free(base);
 	free(name1);
Signature · 1 line
gitgitgadget
#6 lore raw

Re: [PATCH 04/11] merge-ort: have ll_merge() calls use the attr_index for renormalization

Quoted 16 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

ll_merge() needs an index when renormalization is requested. Give it the special one we created exactly for that purpose. This fixes t6418.4 and t6418.5 under GIT_TEST_MERGE_ALGORITHM=ort.

---
 merge-ort.c | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)
merge-ort.c +0 -0
index 028d1adcd2c9..87c553c0882c 100644

merge-ort.c static void clear_or_reinit_internal_opts(struct merge_options_internal *opti, · line 364

Quoted 6 lines
merge-ort.c +1 -1
@@ -360,7 +360,7 @@ static void clear_or_reinit_internal_opts(struct merge_options_internal *opti,
 	string_list_clear(&opti->paths_to_free, 0);
 	opti->paths_to_free.strdup_strings = 0;
 
-	if (opti->attr_index.cache_nr)
+	if (opti->attr_index.cache_nr) /* true iff opt->renormalize */
 		discard_index(&opti->attr_index);

Perhaps instead of a comment, in that "if":

    assert(opt->renormalize);
Quoted 10 lines
merge-ort.c +0 -1
 	/* Free memory used by various renames maps */
@@ -988,7 +988,6 @@ static int merge_submodule(struct merge_options *opt,
 	return 0;
 }
 
-MAYBE_UNUSED
 static void initialize_attr_index(struct merge_options *opt)
 {
 	/*

merge-ort.c static int merge_3way(struct merge_options *opt, · line 1065

Quoted 4 lines
merge-ort.c +1 -0
@@ -1063,6 +1062,8 @@ static int merge_3way(struct merge_options *opt,
 	char *base, *name1, *name2;
 	int merge_status;
 
+	initialize_attr_index(opt);

Subjective, but I think it's more readable to move the "initialized" check in initialize_attr_index() here, so:

    if (!attr_index->initialized)
        initialize_attr_index(opt);

Saves the reader a trip to the function to see that it doesn't do anything except exit early on that flag.

Quoted 12 lines
merge-ort.c +1 -1
 	ll_opts.renormalize = opt->renormalize;
 	ll_opts.extra_marker_size = extra_marker_size;
 	ll_opts.xdl_opts = opt->xdl_opts;
@@ -1101,7 +1102,7 @@ static int merge_3way(struct merge_options *opt,
 
 	merge_status = ll_merge(result_buf, path, &orig, base,
 				&src1, name1, &src2, name2,
-				opt->repo->index, &ll_opts);
+				&opt->priv->attr_index, &ll_opts);
 
 	free(base);
 	free(name1);
#7 lore raw

Re: [PATCH 04/11] merge-ort: have ll_merge() calls use the attr_index for renormalization

Quoted 23 lines · Ævar Arnfjörð Bjarmason
Quoted 19 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

ll_merge() needs an index when renormalization is requested. Give it the special one we created exactly for that purpose. This fixes t6418.4 and t6418.5 under GIT_TEST_MERGE_ALGORITHM=ort.

---
 merge-ort.c | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)
merge-ort.c +0 -0
index 028d1adcd2c9..87c553c0882c 100644
@@ -360,7 +360,7 @@ static void clear_or_reinit_internal_opts(struct merge_options_internal *opti,
      string_list_clear(&opti->paths_to_free, 0);
      opti->paths_to_free.strdup_strings = 0;
 
Quoted 7 lines

merge-ort.c static void clear_or_reinit_internal_opts(struct merge_options_internal *opti, · line 364

Quoted 3 lines
merge-ort.c +1 -1
-     if (opti->attr_index.cache_nr)
+     if (opti->attr_index.cache_nr) /* true iff opt->renormalize */
              discard_index(&opti->attr_index);

Perhaps instead of a comment, in that "if":

    assert(opt->renormalize);

I would, but opt isn't defined here, and passing it in merely for an assertion seems overboard.

Quoted 18 lines
Quoted 10 lines
merge-ort.c +0 -1
      /* Free memory used by various renames maps */
@@ -988,7 +988,6 @@ static int merge_submodule(struct merge_options *opt,
      return 0;
 }
 
-MAYBE_UNUSED
 static void initialize_attr_index(struct merge_options *opt)
 {
      /*

merge-ort.c static int merge_3way(struct merge_options *opt, · line 1065

Quoted 4 lines
merge-ort.c +1 -0
@@ -1063,6 +1062,8 @@ static int merge_3way(struct merge_options *opt,
      char *base, *name1, *name2;
      int merge_status;
 
+     initialize_attr_index(opt);

Subjective, but I think it's more readable to move the "initialized" check in initialize_attr_index() here, so:

Quoted 5 lines
    if (!attr_index->initialized)
        initialize_attr_index(opt);

Saves the reader a trip to the function to see that it doesn't do anything except exit early on that flag.

Makes sense; I'll fix this up.

Quoted 14 lines
Quoted 13 lines
merge-ort.c +1 -1
      ll_opts.renormalize = opt->renormalize;
      ll_opts.extra_marker_size = extra_marker_size;
      ll_opts.xdl_opts = opt->xdl_opts;
@@ -1101,7 +1102,7 @@ static int merge_3way(struct merge_options *opt,
 
      merge_status = ll_merge(result_buf, path, &orig, base,
                              &src1, name1, &src2, name2,
-                             opt->repo->index, &ll_opts);
+                             &opt->priv->attr_index, &ll_opts);
 
      free(base);
      free(name1);
#8 lore raw

PATCH 5/11 merge-ort: let renormalization change modify/delete into clean delete

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

When we have a modify/delete conflict, but the only change to the modification is e.g. change of line endings, then if renormalization is requested then we should be able to recognize such a case as a not-modified/delete and resolve the conflict automatically.

This fixes t6418.10 under GIT_TEST_MERGE_ALGORITHM=ort.

---
 merge-ort.c | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 61 insertions(+), 2 deletions(-)
merge-ort.c +61 -2
index 87c553c0882c..c4bd88b9d3db 100644
@@ -2416,6 +2416,60 @@ static int string_list_df_name_compare(const char *one, const char *two)
 	return onelen - twolen;
 }
 
+static int read_oid_strbuf(struct merge_options *opt,
+			   const struct object_id *oid,
+			   struct strbuf *dst)
+{
+	void *buf;
+	enum object_type type;
+	unsigned long size;
+	buf = read_object_file(oid, &type, &size);
+	if (!buf)
+		return err(opt, _("cannot read object %s"), oid_to_hex(oid));
+	if (type != OBJ_BLOB) {
+		free(buf);
+		return err(opt, _("object %s is not a blob"), oid_to_hex(oid));
+	}
+	strbuf_attach(dst, buf, size, size + 1);
+	return 0;
+}
+
+static int blob_unchanged(struct merge_options *opt,
+			  const struct version_info *base,
+			  const struct version_info *side,
+			  const char *path)
+{
+	struct strbuf basebuf = STRBUF_INIT;
+	struct strbuf sidebuf = STRBUF_INIT;
+	int ret = 0; /* assume changed for safety */
+	const struct index_state *idx = &opt->priv->attr_index;
+
+	initialize_attr_index(opt);
+
+	if (base->mode != side->mode)
+		return 0;
+	if (oideq(&base->oid, &side->oid))
+		return 1;
+
+	if (read_oid_strbuf(opt, &base->oid, &basebuf) ||
+	    read_oid_strbuf(opt, &side->oid, &sidebuf))
+		goto error_return;
+	/*
+	 * Note: binary | is used so that both renormalizations are
+	 * performed.  Comparison can be skipped if both files are
+	 * unchanged since their sha1s have already been compared.
+	 */
+	if (renormalize_buffer(idx, path, basebuf.buf, basebuf.len, &basebuf) |
+	    renormalize_buffer(idx, path, sidebuf.buf, sidebuf.len, &sidebuf))
+		ret = (basebuf.len == sidebuf.len &&
+		       !memcmp(basebuf.buf, sidebuf.buf, basebuf.len));
+
+error_return:
+	strbuf_release(&basebuf);
+	strbuf_release(&sidebuf);
+	return ret;
+}
+
 struct directory_versions {
 	/*
 	 * versions: list of (basename -> version_info)
@@ -3003,8 +3057,13 @@ static void process_entry(struct merge_options *opt,
 		modify_branch = (side == 1) ? opt->branch1 : opt->branch2;
 		delete_branch = (side == 1) ? opt->branch2 : opt->branch1;
 
-		if (ci->path_conflict &&
-		    oideq(&ci->stages[0].oid, &ci->stages[side].oid)) {
+		if (opt->renormalize &&
+		    blob_unchanged(opt, &ci->stages[0], &ci->stages[side],
+				   path)) {
+			ci->merged.is_null = 1;
+			ci->merged.clean = 1;
+		} else if (ci->path_conflict &&
+			   oideq(&ci->stages[0].oid, &ci->stages[side].oid)) {
 			/*
 			 * This came from a rename/delete; no action to take,
 			 * but avoid printing "modify/delete" conflict notice
Signature · 1 line
gitgitgadget
#9 lore raw

Re: [PATCH 05/11] merge-ort: let renormalization change modify/delete into clean delete

Quoted 25 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

When we have a modify/delete conflict, but the only change to the modification is e.g. change of line endings, then if renormalization is requested then we should be able to recognize such a case as a not-modified/delete and resolve the conflict automatically.

This fixes t6418.10 under GIT_TEST_MERGE_ALGORITHM=ort.

---
 merge-ort.c | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 61 insertions(+), 2 deletions(-)
merge-ort.c +3 -0
index 87c553c0882c..c4bd88b9d3db 100644
@@ -2416,6 +2416,60 @@ static int string_list_df_name_compare(const char *one, const char *two)
 	return onelen - twolen;
 }
 
+static int read_oid_strbuf(struct merge_options *opt,
+			   const struct object_id *oid,
+			   struct strbuf *dst)

merge-ort.c static int string_list_df_name_compare(const char *one, const char *two) · line 2431

Quoted 10 lines
merge-ort.c +10 -0
+{
+	void *buf;
+	enum object_type type;
+	unsigned long size;
+	buf = read_object_file(oid, &type, &size);
+	if (!buf)
+		return err(opt, _("cannot read object %s"), oid_to_hex(oid));
+	if (type != OBJ_BLOB) {
+		free(buf);
+		return err(opt, _("object %s is not a blob"), oid_to_hex(oid));

As an aside I've got another series I'll submit soon which refactors all these "object is not xyz" calls to a utility function, so in this case we'd also say what it was other than a blob.

Fine to keep this here, just a #leftoverbits note to myself to eventually migrate this.

Quoted 48 lines
merge-ort.c +41 -0
+	}
+	strbuf_attach(dst, buf, size, size + 1);
+	return 0;
+}
+
+static int blob_unchanged(struct merge_options *opt,
+			  const struct version_info *base,
+			  const struct version_info *side,
+			  const char *path)
+{
+	struct strbuf basebuf = STRBUF_INIT;
+	struct strbuf sidebuf = STRBUF_INIT;
+	int ret = 0; /* assume changed for safety */
+	const struct index_state *idx = &opt->priv->attr_index;
+
+	initialize_attr_index(opt);
+
+	if (base->mode != side->mode)
+		return 0;
+	if (oideq(&base->oid, &side->oid))
+		return 1;
+
+	if (read_oid_strbuf(opt, &base->oid, &basebuf) ||
+	    read_oid_strbuf(opt, &side->oid, &sidebuf))
+		goto error_return;
+	/*
+	 * Note: binary | is used so that both renormalizations are
+	 * performed.  Comparison can be skipped if both files are
+	 * unchanged since their sha1s have already been compared.
+	 */
+	if (renormalize_buffer(idx, path, basebuf.buf, basebuf.len, &basebuf) |
+	    renormalize_buffer(idx, path, sidebuf.buf, sidebuf.len, &sidebuf))
+		ret = (basebuf.len == sidebuf.len &&
+		       !memcmp(basebuf.buf, sidebuf.buf, basebuf.len));
+
+error_return:
+	strbuf_release(&basebuf);
+	strbuf_release(&sidebuf);
+	return ret;
+}
+
 
 struct directory_versions {
 	/*
merge-ort.c +0 -0
 	 * versions: list of (basename -> version_info)
@@ -3003,8 +3057,13 @@ static void process_entry(struct merge_options *opt,
 		modify_branch = (side == 1) ? opt->branch1 : opt->branch2;
 		delete_branch = (side == 1) ? opt->branch2 : opt->branch1;

merge-ort.c static void process_entry(struct merge_options *opt, · line 3066

Quoted 10 lines
merge-ort.c +7 -2
 
-		if (ci->path_conflict &&
-		    oideq(&ci->stages[0].oid, &ci->stages[side].oid)) {
+		if (opt->renormalize &&
+		    blob_unchanged(opt, &ci->stages[0], &ci->stages[side],
+				   path)) {
+			ci->merged.is_null = 1;
+			ci->merged.clean = 1;
+		} else if (ci->path_conflict &&
+			   oideq(&ci->stages[0].oid, &ci->stages[side].oid)) {

Small note (no need for re-roll or whatever) on having read a bit of merge-ort.c code recently: I'd find this thing a bit easier on the eyes if ci->stages[0] and ci->stages[side] were split into a variable before the if/else, i.e. used as "side_0.oid and side_n.oid" and "side_0 and side_n" in this case..

That would also avoid the wrapping of at least one argument list here.

#10 lore raw

PATCH 6/11 merge-ort: support subtree shifting

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

merge-recursive has some simple code to support subtree shifting; copy it over to merge-ort. This fixes t6409.12 under GIT_TEST_MERGE_ALGORITHM=ort.

---
 merge-ort.c | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)
merge-ort.c +24 -0
index c4bd88b9d3db..a998f843a1da 100644
@@ -3417,6 +3417,23 @@ void merge_finalize(struct merge_options *opt,
 
 /*** Function Grouping: helper functions for merge_incore_*() ***/
 
+static struct tree *shift_tree_object(struct repository *repo,
+				      struct tree *one, struct tree *two,
+				      const char *subtree_shift)
+{
+	struct object_id shifted;
+
+	if (!*subtree_shift) {
+		shift_tree(repo, &one->object.oid, &two->object.oid, &shifted, 0);
+	} else {
+		shift_tree_by(repo, &one->object.oid, &two->object.oid, &shifted,
+			      subtree_shift);
+	}
+	if (oideq(&two->object.oid, &shifted))
+		return two;
+	return lookup_tree(repo, &shifted);
+}
+
 static inline void set_commit_tree(struct commit *c, struct tree *t)
 {
 	c->maybe_tree = t;
@@ -3544,6 +3561,13 @@ static void merge_ort_nonrecursive_internal(struct merge_options *opt,
 {
 	struct object_id working_tree_oid;
 
+	if (opt->subtree_shift) {
+		side2 = shift_tree_object(opt->repo, side1, side2,
+					  opt->subtree_shift);
+		merge_base = shift_tree_object(opt->repo, side1, merge_base,
+					       opt->subtree_shift);
+	}
+
 	trace2_region_enter("merge", "collect_merge_info", opt->repo);
 	if (collect_merge_info(opt, merge_base, side1, side2) != 0) {
 		/*
Signature · 1 line
gitgitgadget
#11 lore raw

PATCH 7/11 t6428: new test for SKIP_WORKTREE handling and conflicts

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

If there is a conflict during a merge for a SKIP_WORKTREE entry, we expect that file to be written to the working copy and have the SKIP_WORKTREE bit cleared in the index. If the user had manually created a file in the working tree despite SKIP_WORKTREE being set, we do not want to clobber their changes to that file, but want to move it out of the way. Add tests that check for these behaviors.

---
 t/t6428-merge-conflicts-sparse.sh | 158 ++++++++++++++++++++++++++++++
 1 file changed, 158 insertions(+)
 create mode 100755 t/t6428-merge-conflicts-sparse.sh
t/t6428-merge-conflicts-sparse.sh new file +158 -0
new file mode 100755
index 000000000000..1bb52ff6f38c
@@ -0,0 +1,158 @@
+#!/bin/sh
+
+test_description="merge cases"
+
+# The setup for all of them, pictorially, is:
+#
+#      A
+#      o
+#     / \
+#  O o   ?
+#     \ /
+#      o
+#      B
+#
+# To help make it easier to follow the flow of tests, they have been
+# divided into sections and each test will start with a quick explanation
+# of what commits O, A, and B contain.
+#
+# Notation:
+#    z/{b,c}   means  files z/b and z/c both exist
+#    x/d_1     means  file x/d exists with content d1.  (Purpose of the
+#                     underscore notation is to differentiate different
+#                     files that might be renamed into each other's paths.)
+
+. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-merge.sh
+
+
+# Testcase basic, conflicting changes in 'numerals'
+
+test_setup_numerals () {
+	test_create_repo numerals_$1 &&
+	(
+		cd numerals_$1 &&
+
+		>README &&
+		test_write_lines I II III >numerals &&
+		git add README numerals &&
+		test_tick &&
+		git commit -m "O" &&
+
+		git branch O &&
+		git branch A &&
+		git branch B &&
+
+		git checkout A &&
+		test_write_lines I II III IIII >numerals &&
+		git add numerals &&
+		test_tick &&
+		git commit -m "A" &&
+
+		git checkout B &&
+		test_write_lines I II III IV >numerals &&
+		git add numerals &&
+		test_tick &&
+		git commit -m "B" &&
+
+		cat <<-EOF >expected-index &&
+		H README
+		M numerals
+		M numerals
+		M numerals
+		EOF
+
+		cat <<-EOF >expected-merge
+		I
+		II
+		III
+		<<<<<<< HEAD
+		IIII
+		=======
+		IV
+		>>>>>>> B^0
+		EOF
+
+	)
+}
+
+test_expect_merge_algorithm success failure 'conflicting entries written to worktree even if sparse' '
+	test_setup_numerals plain &&
+	(
+		cd numerals_plain &&
+
+		git checkout A^0 &&
+
+		test_path_is_file README &&
+		test_path_is_file numerals &&
+
+		git sparse-checkout init &&
+		git sparse-checkout set README &&
+
+		test_path_is_file README &&
+		test_path_is_missing numerals &&
+
+		test_must_fail git merge -s recursive B^0 &&
+
+		git ls-files -t >index_files &&
+		test_cmp expected-index index_files &&
+
+		test_path_is_file README &&
+		test_path_is_file numerals &&
+
+		test_cmp expected-merge numerals &&
+
+		# 4 other files:
+		#   * expected-merge
+		#   * expected-index
+		#   * index_files
+		#   * others
+		git ls-files -o >others &&
+		test_line_count = 4 others
+	)
+'
+
+test_expect_merge_algorithm failure failure 'present-despite-SKIP_WORKTREE handled reasonably' '
+	test_setup_numerals in_the_way &&
+	(
+		cd numerals_in_the_way &&
+
+		git checkout A^0 &&
+
+		test_path_is_file README &&
+		test_path_is_file numerals &&
+
+		git sparse-checkout init &&
+		git sparse-checkout set README &&
+
+		test_path_is_file README &&
+		test_path_is_missing numerals &&
+
+		echo foobar >numerals &&
+
+		test_must_fail git merge -s recursive B^0 &&
+
+		git ls-files -t >index_files &&
+		test_cmp expected-index index_files &&
+
+		test_path_is_file README &&
+		test_path_is_file numerals &&
+
+		test_cmp expected-merge numerals &&
+
+		# There should still be a file with "foobar" in it
+		grep foobar * &&
+
+		# 5 other files:
+		#   * expected-merge
+		#   * expected-index
+		#   * index_files
+		#   * others
+		#   * whatever name was given to the numerals file that had
+		#     "foobar" in it
+		git ls-files -o >others &&
+		test_line_count = 5 others
+	)
+'
+
+test_done
Signature · 1 line
gitgitgadget
#12 lore raw

Re: [PATCH 07/11] t6428: new test for SKIP_WORKTREE handling and conflicts

Quoted 51 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

If there is a conflict during a merge for a SKIP_WORKTREE entry, we expect that file to be written to the working copy and have the SKIP_WORKTREE bit cleared in the index. If the user had manually created a file in the working tree despite SKIP_WORKTREE being set, we do not want to clobber their changes to that file, but want to move it out of the way. Add tests that check for these behaviors.

---
 t/t6428-merge-conflicts-sparse.sh | 158 ++++++++++++++++++++++++++++++
 1 file changed, 158 insertions(+)
 create mode 100755 t/t6428-merge-conflicts-sparse.sh
t/t6428-merge-conflicts-sparse.sh new file +30 -0
new file mode 100755
index 000000000000..1bb52ff6f38c
@@ -0,0 +1,158 @@
+#!/bin/sh
+
+test_description="merge cases"
+
+# The setup for all of them, pictorially, is:
+#
+#      A
+#      o
+#     / \
+#  O o   ?
+#     \ /
+#      o
+#      B
+#
+# To help make it easier to follow the flow of tests, they have been
+# divided into sections and each test will start with a quick explanation
+# of what commits O, A, and B contain.
+#
+# Notation:
+#    z/{b,c}   means  files z/b and z/c both exist
+#    x/d_1     means  file x/d exists with content d1.  (Purpose of the
+#                     underscore notation is to differentiate different
+#                     files that might be renamed into each other's paths.)
+
+. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-merge.sh
+
+
+# Testcase basic, conflicting changes in 'numerals'
+

t/t6428-merge-conflicts-sparse.sh line 40

Quoted 10 lines
t/t6428-merge-conflicts-sparse.sh new file +10 -0
+test_setup_numerals () {
+	test_create_repo numerals_$1 &&
+	(
+		cd numerals_$1 &&
+
+		>README &&
+		test_write_lines I II III >numerals &&
+		git add README numerals &&
+		test_tick &&
+		git commit -m "O" &&

As an aside this could use the --printf option to test_commit I've got in next, but that's also a bit painful to use since you can't use test_write_lines.

I've wanted to just support something like this for this use-case of using an existing file:

    test_write_lines A B C D >lines &&
    test_commit --add O lines &&
Quoted 30 lines
t/t6428-merge-conflicts-sparse.sh new file +30 -0
+
+		git branch O &&
+		git branch A &&
+		git branch B &&
+
+		git checkout A &&
+		test_write_lines I II III IIII >numerals &&
+		git add numerals &&
+		test_tick &&
+		git commit -m "A" &&
+
+		git checkout B &&
+		test_write_lines I II III IV >numerals &&
+		git add numerals &&
+		test_tick &&
+		git commit -m "B" &&
+
+		cat <<-EOF >expected-index &&
+		H README
+		M numerals
+		M numerals
+		M numerals
+		EOF
+
+		cat <<-EOF >expected-merge
+		I
+		II
+		III
+		<<<<<<< HEAD
+		IIII

t/t6428-merge-conflicts-sparse.sh line 80

Quoted 10 lines
t/t6428-merge-conflicts-sparse.sh new file +10 -0
+		=======
+		IV
+		>>>>>>> B^0
+		EOF
+
+	)
+}
+
+test_expect_merge_algorithm success failure 'conflicting entries written to worktree even if sparse' '
+	test_setup_numerals plain &&

A small nit, but makes it easier to debug things: I think having what you have in "test_setup_numerals" above in a test_expect_success is a better pattern, then if it fails we can see where exactly.

Then instead of calling "test_setup_numerals" here you'd do:

    cp -R template plain &&

To just copy over that existing setup template, or re-use it and have have the tests call a small helper to "test_when_finish" reset --hard back as appropriate.

#13 lore raw

Re: [PATCH 07/11] t6428: new test for SKIP_WORKTREE handling and conflicts

Quoted 122 lines · Ævar Arnfjörð Bjarmason
Quoted 51 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

If there is a conflict during a merge for a SKIP_WORKTREE entry, we expect that file to be written to the working copy and have the SKIP_WORKTREE bit cleared in the index. If the user had manually created a file in the working tree despite SKIP_WORKTREE being set, we do not want to clobber their changes to that file, but want to move it out of the way. Add tests that check for these behaviors.

---
 t/t6428-merge-conflicts-sparse.sh | 158 ++++++++++++++++++++++++++++++
 1 file changed, 158 insertions(+)
 create mode 100755 t/t6428-merge-conflicts-sparse.sh
t/t6428-merge-conflicts-sparse.sh new file +30 -0
new file mode 100755
index 000000000000..1bb52ff6f38c
@@ -0,0 +1,158 @@
+#!/bin/sh
+
+test_description="merge cases"
+
+# The setup for all of them, pictorially, is:
+#
+#      A
+#      o
+#     / \
+#  O o   ?
+#     \ /
+#      o
+#      B
+#
+# To help make it easier to follow the flow of tests, they have been
+# divided into sections and each test will start with a quick explanation
+# of what commits O, A, and B contain.
+#
+# Notation:
+#    z/{b,c}   means  files z/b and z/c both exist
+#    x/d_1     means  file x/d exists with content d1.  (Purpose of the
+#                     underscore notation is to differentiate different
+#                     files that might be renamed into each other's paths.)
+
+. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-merge.sh
+
+
+# Testcase basic, conflicting changes in 'numerals'
+

t/t6428-merge-conflicts-sparse.sh line 40

Quoted 10 lines
t/t6428-merge-conflicts-sparse.sh new file +10 -0
+test_setup_numerals () {
+     test_create_repo numerals_$1 &&
+     (
+             cd numerals_$1 &&
+
+             >README &&
+             test_write_lines I II III >numerals &&
+             git add README numerals &&
+             test_tick &&
+             git commit -m "O" &&

As an aside this could use the --printf option to test_commit I've got in next, but that's also a bit painful to use since you can't use test_write_lines.

I've wanted to just support something like this for this use-case of using an existing file:

    test_write_lines A B C D >lines &&
    test_commit --add O lines &&
Quoted 30 lines
t/t6428-merge-conflicts-sparse.sh new file +30 -0
+
+             git branch O &&
+             git branch A &&
+             git branch B &&
+
+             git checkout A &&
+             test_write_lines I II III IIII >numerals &&
+             git add numerals &&
+             test_tick &&
+             git commit -m "A" &&
+
+             git checkout B &&
+             test_write_lines I II III IV >numerals &&
+             git add numerals &&
+             test_tick &&
+             git commit -m "B" &&
+
+             cat <<-EOF >expected-index &&
+             H README
+             M numerals
+             M numerals
+             M numerals
+             EOF
+
+             cat <<-EOF >expected-merge
+             I
+             II
+             III
+             <<<<<<< HEAD
+             IIII

t/t6428-merge-conflicts-sparse.sh line 80

Quoted 10 lines
t/t6428-merge-conflicts-sparse.sh new file +10 -0
+             =======
+             IV
+             >>>>>>> B^0
+             EOF
+
+     )
+}
+
+test_expect_merge_algorithm success failure 'conflicting entries written to worktree even if sparse' '
+     test_setup_numerals plain &&

A small nit, but makes it easier to debug things: I think having what you have in "test_setup_numerals" above in a test_expect_success is a better pattern, then if it fails we can see where exactly.

Quoted 7 lines

Then instead of calling "test_setup_numerals" here you'd do:

    cp -R template plain &&

To just copy over that existing setup template, or re-use it and have have the tests call a small helper to "test_when_finish" reset --hard back as appropriate.

I used to have tests follow that pattern, but Dscho objected strenuously to it; see e.g.
https://lore.kernel.org/git/[email protected]/

I went through and replaced most of the merge-recursive-related ones to the current style to help him out.

#14 lore raw

PATCH 8/11 merge-ort: implement CE_SKIP_WORKTREE handling with conflicted entries

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

When merge conflicts occur in paths removed by a sparse-checkout, we need to unsparsify those paths (clear the SKIP_WORKTREE bit), and write out the conflicted file to the working copy. In the very unlikely case that someone manually put a file into the working copy at the location of the SKIP_WORKTREE file, we need to avoid overwriting whatever edits they have made and move that file to a different location first.

---
 merge-ort.c                       | 43 +++++++++++++++++++++----------
 t/t6428-merge-conflicts-sparse.sh |  4 +--
 2 files changed, 32 insertions(+), 15 deletions(-)
merge-ort.c +30 -13
index a998f843a1da..37b69cbe0f9a 100644
@@ -3235,23 +3235,27 @@ static int checkout(struct merge_options *opt,
 	return ret;
 }
 
-static int record_conflicted_index_entries(struct merge_options *opt,
-					   struct index_state *index,
-					   struct strmap *paths,
-					   struct strmap *conflicted)
+static int record_conflicted_index_entries(struct merge_options *opt)
 {
 	struct hashmap_iter iter;
 	struct strmap_entry *e;
+	struct index_state *index = opt->repo->index;
+	struct checkout state = CHECKOUT_INIT;
 	int errs = 0;
 	int original_cache_nr;
 
-	if (strmap_empty(conflicted))
+	if (strmap_empty(&opt->priv->conflicted))
 		return 0;
 
+	/* If any entries have skip_worktree set, we'll have to check 'em out */
+	state.force = 1;
+	state.quiet = 1;
+	state.refresh_cache = 1;
+	state.istate = index;
 	original_cache_nr = index->cache_nr;
 
 	/* Put every entry from paths into plist, then sort */
-	strmap_for_each_entry(conflicted, &iter, e) {
+	strmap_for_each_entry(&opt->priv->conflicted, &iter, e) {
 		const char *path = e->key;
 		struct conflict_info *ci = e->value;
 		int pos;
@@ -3292,9 +3296,23 @@ static int record_conflicted_index_entries(struct merge_options *opt,
 			 * the higher order stages.  Thus, we need override
 			 * the CE_SKIP_WORKTREE bit and manually write those
 			 * files to the working disk here.
-			 *
-			 * TODO: Implement this CE_SKIP_WORKTREE fixup.
 			 */
+			if (ce_skip_worktree(ce)) {
+				struct stat st;
+
+				if (!lstat(path, &st)) {
+					char *new_name = unique_path(&opt->priv->paths,
+								     path,
+								     "cruft");
+
+					path_msg(opt, path, 1,
+						 _("Note: %s not up to date and in way of checking out conflicted version; old copy renamed to %s"),
+						 path, new_name);
+					errs |= rename(path, new_name);
+					free(new_name);
+				}
+				errs |= checkout_entry(ce, &state, NULL, NULL);
+			}
 
 			/*
 			 * Mark this cache entry for removal and instead add
@@ -3344,8 +3362,6 @@ void merge_switch_to_result(struct merge_options *opt,
 {
 	assert(opt->priv == NULL);
 	if (result->clean >= 0 && update_worktree_and_index) {
-		struct merge_options_internal *opti = result->priv;
-
 		trace2_region_enter("merge", "checkout", opt->repo);
 		if (checkout(opt, head, result->tree)) {
 			/* failure to function */
@@ -3355,13 +3371,14 @@ void merge_switch_to_result(struct merge_options *opt,
 		trace2_region_leave("merge", "checkout", opt->repo);
 
 		trace2_region_enter("merge", "record_conflicted", opt->repo);
-		if (record_conflicted_index_entries(opt, opt->repo->index,
-						    &opti->paths,
-						    &opti->conflicted)) {
+		opt->priv = result->priv;
+		if (record_conflicted_index_entries(opt)) {
 			/* failure to function */
+			opt->priv = NULL;
 			result->clean = -1;
 			return;
 		}
+		opt->priv = NULL;
 		trace2_region_leave("merge", "record_conflicted", opt->repo);
 	}
 
t/t6428-merge-conflicts-sparse.sh +2 -2
index 1bb52ff6f38c..7e8bf497f821 100755
@@ -76,7 +76,7 @@ test_setup_numerals () {
 	)
 }
 
-test_expect_merge_algorithm success failure 'conflicting entries written to worktree even if sparse' '
+test_expect_success 'conflicting entries written to worktree even if sparse' '
 	test_setup_numerals plain &&
 	(
 		cd numerals_plain &&
@@ -112,7 +112,7 @@ test_expect_merge_algorithm success failure 'conflicting entries written to work
 	)
 '
 
-test_expect_merge_algorithm failure failure 'present-despite-SKIP_WORKTREE handled reasonably' '
+test_expect_merge_algorithm failure success 'present-despite-SKIP_WORKTREE handled reasonably' '
 	test_setup_numerals in_the_way &&
 	(
 		cd numerals_in_the_way &&
Signature · 1 line
gitgitgadget
#15 lore raw

Re: [PATCH 08/11] merge-ort: implement CE_SKIP_WORKTREE handling with conflicted entries

Quoted 61 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

When merge conflicts occur in paths removed by a sparse-checkout, we need to unsparsify those paths (clear the SKIP_WORKTREE bit), and write out the conflicted file to the working copy. In the very unlikely case that someone manually put a file into the working copy at the location of the SKIP_WORKTREE file, we need to avoid overwriting whatever edits they have made and move that file to a different location first.

---
 merge-ort.c                       | 43 +++++++++++++++++++++----------
 t/t6428-merge-conflicts-sparse.sh |  4 +--
 2 files changed, 32 insertions(+), 15 deletions(-)
merge-ort.c +11 -8
index a998f843a1da..37b69cbe0f9a 100644
@@ -3235,23 +3235,27 @@ static int checkout(struct merge_options *opt,
 	return ret;
 }
 
-static int record_conflicted_index_entries(struct merge_options *opt,
-					   struct index_state *index,
-					   struct strmap *paths,
-					   struct strmap *conflicted)
+static int record_conflicted_index_entries(struct merge_options *opt)
 {
 	struct hashmap_iter iter;
 	struct strmap_entry *e;
+	struct index_state *index = opt->repo->index;
+	struct checkout state = CHECKOUT_INIT;
 	int errs = 0;
 	int original_cache_nr;
 
-	if (strmap_empty(conflicted))
+	if (strmap_empty(&opt->priv->conflicted))
 		return 0;
 
+	/* If any entries have skip_worktree set, we'll have to check 'em out */
+	state.force = 1;
+	state.quiet = 1;
+	state.refresh_cache = 1;
+	state.istate = index;
 	original_cache_nr = index->cache_nr;
 
 	/* Put every entry from paths into plist, then sort */
-	strmap_for_each_entry(conflicted, &iter, e) {
+	strmap_for_each_entry(&opt->priv->conflicted, &iter, e) {
 		const char *path = e->key;
 		struct conflict_info *ci = e->value;
 		int pos;
@@ -3292,9 +3296,23 @@ static int record_conflicted_index_entries(struct merge_options *opt,
 			 * the higher order stages.  Thus, we need override
 			 * the CE_SKIP_WORKTREE bit and manually write those
 			 * files to the working disk here.
-			 *
-			 * TODO: Implement this CE_SKIP_WORKTREE fixup.
 			 */
+			if (ce_skip_worktree(ce)) {

merge-ort.c static int record_conflicted_index_entries(struct merge_options *opt, · line 3310

Quoted 10 lines
merge-ort.c +10 -0
+				struct stat st;
+
+				if (!lstat(path, &st)) {
+					char *new_name = unique_path(&opt->priv->paths,
+								     path,
+								     "cruft");
+
+					path_msg(opt, path, 1,
+						 _("Note: %s not up to date and in way of checking out conflicted version; old copy renamed to %s"),
+						 path, new_name);

I see this follows existing uses in merge-ort.c, but I wonder if this won't be quite unreadable on long paths, i.e.:

<long x> renamed to <long x.new>

As opposed to:

We had to rename your thing:
from: <long x>
to: <long x.new>

#16 lore raw

Re: [PATCH 08/11] merge-ort: implement CE_SKIP_WORKTREE handling with conflicted entries

Quoted 79 lines · Ævar Arnfjörð Bjarmason
Quoted 61 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

When merge conflicts occur in paths removed by a sparse-checkout, we need to unsparsify those paths (clear the SKIP_WORKTREE bit), and write out the conflicted file to the working copy. In the very unlikely case that someone manually put a file into the working copy at the location of the SKIP_WORKTREE file, we need to avoid overwriting whatever edits they have made and move that file to a different location first.

---
 merge-ort.c                       | 43 +++++++++++++++++++++----------
 t/t6428-merge-conflicts-sparse.sh |  4 +--
 2 files changed, 32 insertions(+), 15 deletions(-)
merge-ort.c +11 -8
index a998f843a1da..37b69cbe0f9a 100644
@@ -3235,23 +3235,27 @@ static int checkout(struct merge_options *opt,
      return ret;
 }
 
-static int record_conflicted_index_entries(struct merge_options *opt,
-                                        struct index_state *index,
-                                        struct strmap *paths,
-                                        struct strmap *conflicted)
+static int record_conflicted_index_entries(struct merge_options *opt)
 {
      struct hashmap_iter iter;
      struct strmap_entry *e;
+     struct index_state *index = opt->repo->index;
+     struct checkout state = CHECKOUT_INIT;
      int errs = 0;
      int original_cache_nr;
 
-     if (strmap_empty(conflicted))
+     if (strmap_empty(&opt->priv->conflicted))
              return 0;
 
+     /* If any entries have skip_worktree set, we'll have to check 'em out */
+     state.force = 1;
+     state.quiet = 1;
+     state.refresh_cache = 1;
+     state.istate = index;
      original_cache_nr = index->cache_nr;
 
      /* Put every entry from paths into plist, then sort */
-     strmap_for_each_entry(conflicted, &iter, e) {
+     strmap_for_each_entry(&opt->priv->conflicted, &iter, e) {
              const char *path = e->key;
              struct conflict_info *ci = e->value;
              int pos;
@@ -3292,9 +3296,23 @@ static int record_conflicted_index_entries(struct merge_options *opt,
                       * the higher order stages.  Thus, we need override
                       * the CE_SKIP_WORKTREE bit and manually write those
                       * files to the working disk here.
-                      *
-                      * TODO: Implement this CE_SKIP_WORKTREE fixup.
                       */
+                     if (ce_skip_worktree(ce)) {

merge-ort.c static int record_conflicted_index_entries(struct merge_options *opt, · line 3310

Quoted 10 lines
merge-ort.c +10 -0
+                             struct stat st;
+
+                             if (!lstat(path, &st)) {
+                                     char *new_name = unique_path(&opt->priv->paths,
+                                                                  path,
+                                                                  "cruft");
+
+                                     path_msg(opt, path, 1,
+                                              _("Note: %s not up to date and in way of checking out conflicted version; old copy renamed to %s"),
+                                              path, new_name);

I see this follows existing uses in merge-ort.c, but I wonder if this won't be quite unreadable on long paths, i.e.:

Quoted 7 lines

<long x> renamed to <long x.new>

As opposed to:

We had to rename your thing:
from: <long x>
to: <long x.new>

Makes sense, but it seems like something we'd want to do to a lot of messages rather than just this one. For now, especially given that I expect this particular message to be *very* rare, I think I'll leave this one as-is for now but we can address this idea in a subsequent series or as #leftoverbits.

#17 lore raw

PATCH 9/11 t: mark several submodule merging tests as fixed under merge-ort

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

merge-ort handles submodules (and directory/file conflicts in general) differently than merge-recursive does; it basically puts all the special handling for different filetypes into one place in the codebase instead of needing special handling for different filetypes in many different code paths. This one code path in merge-ort could perhaps use some work still (there are still test_expect_failure cases in the testsuite), but it passes all the tests that merge-recursive does as well as 12 additional ones that merge-recursive fails. Mark those 12 tests as test_expect_success under merge-ort.

---
 t/t3512-cherry-pick-submodule.sh              | 9 +++++++--
 t/t3513-revert-submodule.sh                   | 7 ++++++-
 t/t5572-pull-submodule.sh                     | 9 +++++++--
 t/t6437-submodule-merge.sh                    | 5 +++--
 t/t6438-submodule-directory-file-conflicts.sh | 9 +++++++--
 5 files changed, 30 insertions(+), 9 deletions(-)
t/t3512-cherry-pick-submodule.sh +7 -2
index 822f2d4bfbd5..eb7a47b9d98a 100755
@@ -8,8 +8,13 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
 . ./test-lib.sh
 . "$TEST_DIRECTORY"/lib-submodule-update.sh
 
-KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
-KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
+if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+then
+	:  # No special additional KNOWN_FAILURE knobs to set
+else
+	KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
+	KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
+fi
 test_submodule_switch "cherry-pick"
 
 test_expect_success 'unrelated submodule/file conflict is ignored' '
t/t3513-revert-submodule.sh +6 -1
index a759f12cbb1d..1f8157ad5ba5 100755
@@ -30,7 +30,12 @@ git_revert () {
 	git revert HEAD
 }
 
-KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
+if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+then
+	:  # No special additional KNOWN_FAILURE knobs to set
+else
+	KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
+fi
 test_submodule_switch_func "git_revert"
 
 test_done
t/t5572-pull-submodule.sh +7 -2
index 29537f4798ef..4b4495613d04 100755
@@ -42,8 +42,13 @@ git_pull_noff () {
 	$2 git pull --no-ff
 }
 
-KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
-KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
+if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+then
+	:  # No special additional KNOWN_FAILURE knobs to set
+else
+	KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
+	KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
+fi
 test_submodule_switch_func "git_pull_noff"
 
 test_expect_success 'pull --recurse-submodule setup' '
t/t6437-submodule-merge.sh +3 -2
index 0f92bcf326c8..e5e89c2045e7 100755
@@ -6,6 +6,7 @@ GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
 export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
 
 . ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-merge.sh
 
 #
 # history
@@ -328,7 +329,7 @@ test_expect_success 'setup file/submodule conflict' '
 	)
 '
 
-test_expect_failure 'file/submodule conflict' '
+test_expect_merge_algorithm failure success 'file/submodule conflict' '
 	test_when_finished "git -C file-submodule reset --hard" &&
 	(
 		cd file-submodule &&
@@ -437,7 +438,7 @@ test_expect_failure 'directory/submodule conflict; keep submodule clean' '
 	)
 '
 
-test_expect_failure !FAIL_PREREQS 'directory/submodule conflict; should not treat submodule files as untracked or in the way' '
+test_expect_merge_algorithm failure success !FAIL_PREREQS 'directory/submodule conflict; should not treat submodule files as untracked or in the way' '
 	test_when_finished "git -C directory-submodule/path reset --hard" &&
 	test_when_finished "git -C directory-submodule reset --hard" &&
 	(
t/t6438-submodule-directory-file-conflicts.sh +7 -2
index 04bf4be7d792..abfa59d3684c 100755
@@ -12,8 +12,13 @@ test_submodule_switch "merge --ff"
 
 test_submodule_switch "merge --ff-only"
 
-KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
-KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
+if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+then
+	:  # No special additional KNOWN_FAILURE knobs to set
+else
+	KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
+	KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
+fi
 test_submodule_switch "merge --no-ff"
 
 test_done
Signature · 1 line
gitgitgadget
#18 lore raw

Re: [PATCH 09/11] t: mark several submodule merging tests as fixed under merge-ort

Quoted 28 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

merge-ort handles submodules (and directory/file conflicts in general) differently than merge-recursive does; it basically puts all the special handling for different filetypes into one place in the codebase instead of needing special handling for different filetypes in many different code paths. This one code path in merge-ort could perhaps use some work still (there are still test_expect_failure cases in the testsuite), but it passes all the tests that merge-recursive does as well as 12 additional ones that merge-recursive fails. Mark those 12 tests as test_expect_success under merge-ort.

---
 t/t3512-cherry-pick-submodule.sh              | 9 +++++++--
 t/t3513-revert-submodule.sh                   | 7 ++++++-
 t/t5572-pull-submodule.sh                     | 9 +++++++--
 t/t6437-submodule-merge.sh                    | 5 +++--
 t/t6438-submodule-directory-file-conflicts.sh | 9 +++++++--
 5 files changed, 30 insertions(+), 9 deletions(-)
t/t3512-cherry-pick-submodule.sh +0 -0
index 822f2d4bfbd5..eb7a47b9d98a 100755
@@ -8,8 +8,13 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
 . ./test-lib.sh
 . "$TEST_DIRECTORY"/lib-submodule-update.sh

t/t3512-cherry-pick-submodule.sh export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME · line 17

Quoted 10 lines
t/t3512-cherry-pick-submodule.sh +7 -2
 
-KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
-KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
+if test "$GIT_TEST_MERGE_ALGORITHM" = ort
+then
+	:  # No special additional KNOWN_FAILURE knobs to set
+else
+	KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
+	KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
+fi

nit: I think this and the rest would be just as readable/obvious without the comment as:

    if test "$x" != ort
    then
            some_vars=xyz
    fi
#19 lore raw

PATCH 10/11 merge-ort: write $GIT_DIR/AUTO_MERGE whenever we hit a conflict

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

There are a variety of questions users might ask while resolving conflicts:
* What changes have been made since the previous (first) parent?
* What changes are staged?
* What is still unstaged? (or what is still conflicted?)
* What changes did I make to resolve conflicts so far?
The first three of these have simple answers:
* git diff HEAD
* git diff --cached
* git diff
There was no way to answer the final question previously. Adding one is trivial in merge-ort, since it works by creating a tree representing what should be written to the working copy complete with conflict markers. Simply write that tree to .git/AUTO_MERGE, allowing users to answer the fourth question with
* git diff AUTO_MERGE

I avoided using a name like "MERGE_AUTO", because that would be merge-specific (much like MERGE_HEAD, REBASE_HEAD, REVERT_HEAD, CHERRY_PICK_HEAD) and I wanted a name that didn't change depending on which type of operation the merge was part of.

Ensure that paths which clean out other temporary operation-specific files (e.g. CHERRY_PICK_HEAD, MERGE_MSG, rebase-merge/ state directory) also clean out this AUTO_MERGE file.

---
 branch.c         |  1 +
 builtin/rebase.c |  1 +
 merge-ort.c      | 10 ++++++++++
 path.c           |  1 +
 path.h           |  2 ++
 sequencer.c      |  5 +++++
 6 files changed, 20 insertions(+)
branch.c +1 -0
index 9c9dae1eae32..b71a2de29dbe 100644
@@ -344,6 +344,7 @@ void remove_merge_branch_state(struct repository *r)
 	unlink(git_path_merge_rr(r));
 	unlink(git_path_merge_msg(r));
 	unlink(git_path_merge_mode(r));
+	unlink(git_path_auto_merge(r));
 	save_autostash(git_path_merge_autostash(r));
 }
 
builtin/rebase.c +1 -0
index de400f9a1973..7d9afe118fd4 100644
@@ -739,6 +739,7 @@ static int finish_rebase(struct rebase_options *opts)
 	int ret = 0;
 
 	delete_ref(NULL, "REBASE_HEAD", NULL, REF_NO_DEREF);
+	unlink(git_path_auto_merge(the_repository));
 	apply_autostash(state_dir_path("autostash", opts));
 	close_object_store(the_repository->objects);
 	/*
merge-ort.c +10 -0
index 37b69cbe0f9a..cf927cd160e1 100644
@@ -3362,6 +3362,9 @@ void merge_switch_to_result(struct merge_options *opt,
 {
 	assert(opt->priv == NULL);
 	if (result->clean >= 0 && update_worktree_and_index) {
+		const char *filename;
+		FILE *fp;
+
 		trace2_region_enter("merge", "checkout", opt->repo);
 		if (checkout(opt, head, result->tree)) {
 			/* failure to function */
@@ -3380,6 +3383,13 @@ void merge_switch_to_result(struct merge_options *opt,
 		}
 		opt->priv = NULL;
 		trace2_region_leave("merge", "record_conflicted", opt->repo);
+
+		trace2_region_enter("merge", "write_auto_merge", opt->repo);
+		filename = git_path_auto_merge(opt->repo);
+		fp = xfopen(filename, "w");
+		fprintf(fp, "%s\n", oid_to_hex(&result->tree->object.oid));
+		fclose(fp);
+		trace2_region_leave("merge", "write_auto_merge", opt->repo);
 	}
 
 	if (display_update_msgs) {
path.c +1 -0
index 7b385e5eb282..9e883eb52446 100644
@@ -1534,5 +1534,6 @@ REPO_GIT_PATH_FUNC(merge_rr, "MERGE_RR")
 REPO_GIT_PATH_FUNC(merge_mode, "MERGE_MODE")
 REPO_GIT_PATH_FUNC(merge_head, "MERGE_HEAD")
 REPO_GIT_PATH_FUNC(merge_autostash, "MERGE_AUTOSTASH")
+REPO_GIT_PATH_FUNC(auto_merge, "AUTO_MERGE")
 REPO_GIT_PATH_FUNC(fetch_head, "FETCH_HEAD")
 REPO_GIT_PATH_FUNC(shallow, "shallow")
path.h +2 -0
index e7e77da6aaa5..251c78d98000 100644
@@ -176,6 +176,7 @@ struct path_cache {
 	const char *merge_mode;
 	const char *merge_head;
 	const char *merge_autostash;
+	const char *auto_merge;
 	const char *fetch_head;
 	const char *shallow;
 };
@@ -191,6 +192,7 @@ const char *git_path_merge_rr(struct repository *r);
 const char *git_path_merge_mode(struct repository *r);
 const char *git_path_merge_head(struct repository *r);
 const char *git_path_merge_autostash(struct repository *r);
+const char *git_path_auto_merge(struct repository *r);
 const char *git_path_fetch_head(struct repository *r);
 const char *git_path_shallow(struct repository *r);
 
sequencer.c +5 -0
index d2332d3e1787..472cdd8c620d 100644
@@ -2096,6 +2096,7 @@ static int do_pick_commit(struct repository *r,
 		refs_delete_ref(get_main_ref_store(r), "", "CHERRY_PICK_HEAD",
 				NULL, 0);
 		unlink(git_path_merge_msg(r));
+		unlink(git_path_auto_merge(r));
 		fprintf(stderr,
 			_("dropping %s %s -- patch contents already upstream\n"),
 			oid_to_hex(&commit->object.oid), msg.subject);
@@ -2451,6 +2452,8 @@ void sequencer_post_commit_cleanup(struct repository *r, int verbose)
 		need_cleanup = 1;
 	}
 
+	unlink(git_path_auto_merge(r));
+
 	if (!need_cleanup)
 		return;
 
@@ -4111,6 +4114,7 @@ static int pick_commits(struct repository *r,
 			unlink(rebase_path_stopped_sha());
 			unlink(rebase_path_amend());
 			unlink(git_path_merge_head(r));
+			unlink(git_path_auto_merge(r));
 			delete_ref(NULL, "REBASE_HEAD", NULL, REF_NO_DEREF);
 
 			if (item->command == TODO_BREAK) {
@@ -4505,6 +4509,7 @@ static int commit_staged_changes(struct repository *r,
 		return error(_("could not commit staged changes."));
 	unlink(rebase_path_amend());
 	unlink(git_path_merge_head(r));
+	unlink(git_path_auto_merge(r));
 	if (final_fixup) {
 		unlink(rebase_path_fixup_msg());
 		unlink(rebase_path_squash_msg());
Signature · 1 line
gitgitgadget
#20 lore raw

Re: [PATCH 10/11] merge-ort: write $GIT_DIR/AUTO_MERGE whenever we hit a conflict

Quoted 19 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

There are a variety of questions users might ask while resolving conflicts:
* What changes have been made since the previous (first) parent?
* What changes are staged?
* What is still unstaged? (or what is still conflicted?)
* What changes did I make to resolve conflicts so far?
The first three of these have simple answers:
* git diff HEAD
* git diff --cached
* git diff
There was no way to answer the final question previously. Adding one is trivial in merge-ort, since it works by creating a tree representing what should be written to the working copy complete with conflict markers. Simply write that tree to .git/AUTO_MERGE, allowing users to answer the fourth question with
* git diff AUTO_MERGE

Quoted 4 lines

I avoided using a name like "MERGE_AUTO", because that would be merge-specific (much like MERGE_HEAD, REBASE_HEAD, REVERT_HEAD, CHERRY_PICK_HEAD) and I wanted a name that didn't change depending on which type of operation the merge was part of.

That's a really cool feature. I'm starting to like this "ort" thing :)

(After knowing almost nothing about it until a few days ago...)

Quoted 53 lines

Ensure that paths which clean out other temporary operation-specific files (e.g. CHERRY_PICK_HEAD, MERGE_MSG, rebase-merge/ state directory) also clean out this AUTO_MERGE file.

---
 branch.c         |  1 +
 builtin/rebase.c |  1 +
 merge-ort.c      | 10 ++++++++++
 path.c           |  1 +
 path.h           |  2 ++
 sequencer.c      |  5 +++++
 6 files changed, 20 insertions(+)
branch.c +1 -0
index 9c9dae1eae32..b71a2de29dbe 100644
@@ -344,6 +344,7 @@ void remove_merge_branch_state(struct repository *r)
 	unlink(git_path_merge_rr(r));
 	unlink(git_path_merge_msg(r));
 	unlink(git_path_merge_mode(r));
+	unlink(git_path_auto_merge(r));
 	save_autostash(git_path_merge_autostash(r));
 }
 
builtin/rebase.c +1 -0
index de400f9a1973..7d9afe118fd4 100644
@@ -739,6 +739,7 @@ static int finish_rebase(struct rebase_options *opts)
 	int ret = 0;
 
 	delete_ref(NULL, "REBASE_HEAD", NULL, REF_NO_DEREF);
+	unlink(git_path_auto_merge(the_repository));
 	apply_autostash(state_dir_path("autostash", opts));
 	close_object_store(the_repository->objects);
 	/*
merge-ort.c +3 -0
index 37b69cbe0f9a..cf927cd160e1 100644
@@ -3362,6 +3362,9 @@ void merge_switch_to_result(struct merge_options *opt,
 {
 	assert(opt->priv == NULL);
 	if (result->clean >= 0 && update_worktree_and_index) {
+		const char *filename;
+		FILE *fp;
+
 		trace2_region_enter("merge", "checkout", opt->repo);
 		if (checkout(opt, head, result->tree)) {
 			/* failure to function */

merge-ort.c void merge_switch_to_result(struct merge_options *opt, · line 3392

Quoted 10 lines
merge-ort.c +7 -0
@@ -3380,6 +3383,13 @@ void merge_switch_to_result(struct merge_options *opt,
 		}
 		opt->priv = NULL;
 		trace2_region_leave("merge", "record_conflicted", opt->repo);
+
+		trace2_region_enter("merge", "write_auto_merge", opt->repo);
+		filename = git_path_auto_merge(opt->repo);
+		fp = xfopen(filename, "w");
+		fprintf(fp, "%s\n", oid_to_hex(&result->tree->object.oid));
+		fclose(fp);
+		trace2_region_leave("merge", "write_auto_merge", opt->repo);

This isn't a new problem since you're just folling an existing pattern, but here you (rightly) do xopen()< and the:n

Quoted 39 lines
merge-ort.c +0 -0
 	}
 
 	if (display_update_msgs) {
path.c +1 -0
index 7b385e5eb282..9e883eb52446 100644
@@ -1534,5 +1534,6 @@ REPO_GIT_PATH_FUNC(merge_rr, "MERGE_RR")
 REPO_GIT_PATH_FUNC(merge_mode, "MERGE_MODE")
 REPO_GIT_PATH_FUNC(merge_head, "MERGE_HEAD")
 REPO_GIT_PATH_FUNC(merge_autostash, "MERGE_AUTOSTASH")
+REPO_GIT_PATH_FUNC(auto_merge, "AUTO_MERGE")
 REPO_GIT_PATH_FUNC(fetch_head, "FETCH_HEAD")
 REPO_GIT_PATH_FUNC(shallow, "shallow")
path.h +2 -0
index e7e77da6aaa5..251c78d98000 100644
@@ -176,6 +176,7 @@ struct path_cache {
 	const char *merge_mode;
 	const char *merge_head;
 	const char *merge_autostash;
+	const char *auto_merge;
 	const char *fetch_head;
 	const char *shallow;
 };
@@ -191,6 +192,7 @@ const char *git_path_merge_rr(struct repository *r);
 const char *git_path_merge_mode(struct repository *r);
 const char *git_path_merge_head(struct repository *r);
 const char *git_path_merge_autostash(struct repository *r);
+const char *git_path_auto_merge(struct repository *r);
 const char *git_path_fetch_head(struct repository *r);
 const char *git_path_shallow(struct repository *r);
 
sequencer.c +0 -0
index d2332d3e1787..472cdd8c620d 100644

sequencer.c static int do_pick_commit(struct repository *r, · line 2099

Quoted 4 lines
sequencer.c +1 -0
@@ -2096,6 +2096,7 @@ static int do_pick_commit(struct repository *r,
 		refs_delete_ref(get_main_ref_store(r), "", "CHERRY_PICK_HEAD",
 				NULL, 0);
 		unlink(git_path_merge_msg(r));
+		unlink(git_path_auto_merge(r));

Shouldn't this & the rest ideally be at least unlink_or_warn()?

Quoted 28 lines
sequencer.c +4 -0
 		fprintf(stderr,
 			_("dropping %s %s -- patch contents already upstream\n"),
 			oid_to_hex(&commit->object.oid), msg.subject);
@@ -2451,6 +2452,8 @@ void sequencer_post_commit_cleanup(struct repository *r, int verbose)
 		need_cleanup = 1;
 	}
 
+	unlink(git_path_auto_merge(r));
+
 	if (!need_cleanup)
 		return;
 
@@ -4111,6 +4114,7 @@ static int pick_commits(struct repository *r,
 			unlink(rebase_path_stopped_sha());
 			unlink(rebase_path_amend());
 			unlink(git_path_merge_head(r));
+			unlink(git_path_auto_merge(r));
 			delete_ref(NULL, "REBASE_HEAD", NULL, REF_NO_DEREF);
 
 			if (item->command == TODO_BREAK) {
@@ -4505,6 +4509,7 @@ static int commit_staged_changes(struct repository *r,
 		return error(_("could not commit staged changes."));
 	unlink(rebase_path_amend());
 	unlink(git_path_merge_head(r));
+	unlink(git_path_auto_merge(r));
 	if (final_fixup) {
 		unlink(rebase_path_fixup_msg());
 		unlink(rebase_path_squash_msg());
#21 lore raw

Re: [PATCH 10/11] merge-ort: write $GIT_DIR/AUTO_MERGE whenever we hit a conflict

Quoted 90 lines · Ævar Arnfjörð Bjarmason
Quoted 19 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

There are a variety of questions users might ask while resolving conflicts:
* What changes have been made since the previous (first) parent?
* What changes are staged?
* What is still unstaged? (or what is still conflicted?)
* What changes did I make to resolve conflicts so far?
The first three of these have simple answers:
* git diff HEAD
* git diff --cached
* git diff
There was no way to answer the final question previously. Adding one is trivial in merge-ort, since it works by creating a tree representing what should be written to the working copy complete with conflict markers. Simply write that tree to .git/AUTO_MERGE, allowing users to answer the fourth question with
* git diff AUTO_MERGE

Quoted 4 lines

I avoided using a name like "MERGE_AUTO", because that would be merge-specific (much like MERGE_HEAD, REBASE_HEAD, REVERT_HEAD, CHERRY_PICK_HEAD) and I wanted a name that didn't change depending on which type of operation the merge was part of.

That's a really cool feature. I'm starting to like this "ort" thing :)

(After knowing almost nothing about it until a few days ago...)

Quoted 58 lines

Ensure that paths which clean out other temporary operation-specific files (e.g. CHERRY_PICK_HEAD, MERGE_MSG, rebase-merge/ state directory) also clean out this AUTO_MERGE file.

---
 branch.c         |  1 +
 builtin/rebase.c |  1 +
 merge-ort.c      | 10 ++++++++++
 path.c           |  1 +
 path.h           |  2 ++
 sequencer.c      |  5 +++++
 6 files changed, 20 insertions(+)
branch.c +1 -0
index 9c9dae1eae32..b71a2de29dbe 100644
@@ -344,6 +344,7 @@ void remove_merge_branch_state(struct repository *r)
      unlink(git_path_merge_rr(r));
      unlink(git_path_merge_msg(r));
      unlink(git_path_merge_mode(r));
+     unlink(git_path_auto_merge(r));
      save_autostash(git_path_merge_autostash(r));
 }
 
builtin/rebase.c +1 -0
index de400f9a1973..7d9afe118fd4 100644
@@ -739,6 +739,7 @@ static int finish_rebase(struct rebase_options *opts)
      int ret = 0;
 
      delete_ref(NULL, "REBASE_HEAD", NULL, REF_NO_DEREF);
+     unlink(git_path_auto_merge(the_repository));
      apply_autostash(state_dir_path("autostash", opts));
      close_object_store(the_repository->objects);
      /*
merge-ort.c +5 -0
index 37b69cbe0f9a..cf927cd160e1 100644
@@ -3362,6 +3362,9 @@ void merge_switch_to_result(struct merge_options *opt,
 {
      assert(opt->priv == NULL);
      if (result->clean >= 0 && update_worktree_and_index) {
+             const char *filename;
+             FILE *fp;
+
              trace2_region_enter("merge", "checkout", opt->repo);
              if (checkout(opt, head, result->tree)) {
                      /* failure to function */
@@ -3380,6 +3383,13 @@ void merge_switch_to_result(struct merge_options *opt,
              }
              opt->priv = NULL;
              trace2_region_leave("merge", "record_conflicted", opt->repo);
+
+             trace2_region_enter("merge", "write_auto_merge", opt->repo);
Quoted 8 lines

merge-ort.c void merge_switch_to_result(struct merge_options *opt, · line 3392

Quoted 5 lines
merge-ort.c +5 -0
+             filename = git_path_auto_merge(opt->repo);
+             fp = xfopen(filename, "w");
+             fprintf(fp, "%s\n", oid_to_hex(&result->tree->object.oid));
+             fclose(fp);
+             trace2_region_leave("merge", "write_auto_merge", opt->repo);

This isn't a new problem since you're just folling an existing pattern, but here you (rightly) do xopen()< and the:n

Looks like your comment got garbled/truncated. Do you remember the rest of what you were going to say here?

Quoted 34 lines
Quoted 34 lines
merge-ort.c +0 -0
      }
 
      if (display_update_msgs) {
path.c +1 -0
index 7b385e5eb282..9e883eb52446 100644
@@ -1534,5 +1534,6 @@ REPO_GIT_PATH_FUNC(merge_rr, "MERGE_RR")
 REPO_GIT_PATH_FUNC(merge_mode, "MERGE_MODE")
 REPO_GIT_PATH_FUNC(merge_head, "MERGE_HEAD")
 REPO_GIT_PATH_FUNC(merge_autostash, "MERGE_AUTOSTASH")
+REPO_GIT_PATH_FUNC(auto_merge, "AUTO_MERGE")
 REPO_GIT_PATH_FUNC(fetch_head, "FETCH_HEAD")
 REPO_GIT_PATH_FUNC(shallow, "shallow")
path.h +2 -0
index e7e77da6aaa5..251c78d98000 100644
@@ -176,6 +176,7 @@ struct path_cache {
      const char *merge_mode;
      const char *merge_head;
      const char *merge_autostash;
+     const char *auto_merge;
      const char *fetch_head;
      const char *shallow;
 };
@@ -191,6 +192,7 @@ const char *git_path_merge_rr(struct repository *r);
 const char *git_path_merge_mode(struct repository *r);
 const char *git_path_merge_head(struct repository *r);
 const char *git_path_merge_autostash(struct repository *r);
+const char *git_path_auto_merge(struct repository *r);
 const char *git_path_fetch_head(struct repository *r);
 const char *git_path_shallow(struct repository *r);
 
Quoted 11 lines

sequencer.c static int do_pick_commit(struct repository *r, · line 2099

Quoted 9 lines
sequencer.c +1 -0
index d2332d3e1787..472cdd8c620d 100644
@@ -2096,6 +2096,7 @@ static int do_pick_commit(struct repository *r,
              refs_delete_ref(get_main_ref_store(r), "", "CHERRY_PICK_HEAD",
                              NULL, 0);
              unlink(git_path_merge_msg(r));
+             unlink(git_path_auto_merge(r));

Shouldn't this & the rest ideally be at least unlink_or_warn()?

Perhaps, but I think that should be a follow-on series or #leftoverbits. I'm having enough trouble getting reviews (I think I'm burning Stolee out after the last half year) without making my series longer for tangential cleanups. :-)

I'm starting to worry that despite having all the remaining patches ready (and most have been ready for months), that we won't be able to get merge-ort done before git-2.32 -- the -rc0 for it is now just under three months away.

Quoted 28 lines
Quoted 28 lines
sequencer.c +4 -0
              fprintf(stderr,
                      _("dropping %s %s -- patch contents already upstream\n"),
                      oid_to_hex(&commit->object.oid), msg.subject);
@@ -2451,6 +2452,8 @@ void sequencer_post_commit_cleanup(struct repository *r, int verbose)
              need_cleanup = 1;
      }
 
+     unlink(git_path_auto_merge(r));
+
      if (!need_cleanup)
              return;
 
@@ -4111,6 +4114,7 @@ static int pick_commits(struct repository *r,
                      unlink(rebase_path_stopped_sha());
                      unlink(rebase_path_amend());
                      unlink(git_path_merge_head(r));
+                     unlink(git_path_auto_merge(r));
                      delete_ref(NULL, "REBASE_HEAD", NULL, REF_NO_DEREF);
 
                      if (item->command == TODO_BREAK) {
@@ -4505,6 +4509,7 @@ static int commit_staged_changes(struct repository *r,
              return error(_("could not commit staged changes."));
      unlink(rebase_path_amend());
      unlink(git_path_merge_head(r));
+     unlink(git_path_auto_merge(r));
      if (final_fixup) {
              unlink(rebase_path_fixup_msg());
              unlink(rebase_path_squash_msg());
#22 lore raw

PATCH 11/11 merge-recursive: add a bunch of FIXME comments documenting known bugs

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

The plan is to just delete merge-recursive, but not until everyone is comfortable with merge-ort as a replacement. Given that I haven't switched all callers of merge-recursive over yet (e.g. git-am still uses merge-recursive), maybe there's some value documenting known bugs in the algorithm in case we end up keeping it or someone wants to dig it up in the future.

---
 merge-recursive.c | 37 +++++++++++++++++++++++++++++++++++++
 1 file changed, 37 insertions(+)
merge-recursive.c +37 -0
index b052974f191c..99a197597db5 100644
@@ -1075,6 +1075,11 @@ static int merge_3way(struct merge_options *opt,
 	read_mmblob(&src1, &a->oid);
 	read_mmblob(&src2, &b->oid);
 
+	/*
+	 * FIXME: Using a->path for normalization rules in ll_merge could be
+	 * wrong if we renamed from a->path to b->path.  We should use the
+	 * target path for where the file will be written.
+	 */
 	merge_status = ll_merge(result_buf, a->path, &orig, base,
 				&src1, name1, &src2, name2,
 				opt->repo->index, &ll_opts);
@@ -1154,6 +1159,8 @@ static void print_commit(struct commit *commit)
 	struct strbuf sb = STRBUF_INIT;
 	struct pretty_print_context ctx = {0};
 	ctx.date_mode.type = DATE_NORMAL;
+	/* FIXME: Merge this with output_commit_title() */
+	assert(!merge_remote_util(commit));
 	format_commit_message(commit, " %h: %m %s", &sb, &ctx);
 	fprintf(stderr, "%s\n", sb.buf);
 	strbuf_release(&sb);
@@ -1177,6 +1184,11 @@ static int merge_submodule(struct merge_options *opt,
 	int search = !opt->priv->call_depth;
 
 	/* store a in result in case we fail */
+	/* FIXME: This is the WRONG resolution for the recursive case when
+	 * we need to be careful to avoid accidentally matching either side.
+	 * Should probably use o instead there, much like we do for merging
+	 * binaries.
+	 */
 	oidcpy(result, a);
 
 	/* we can not handle deletion conflicts */
@@ -1301,6 +1313,13 @@ static int merge_mode_and_contents(struct merge_options *opt,
 
 	if ((S_IFMT & a->mode) != (S_IFMT & b->mode)) {
 		result->clean = 0;
+		/*
+		 * FIXME: This is a bad resolution for recursive case; for
+		 * the recursive case we want something that is unlikely to
+		 * accidentally match either side.  Also, while it makes
+		 * sense to prefer regular files over symlinks, it doesn't
+		 * make sense to prefer regular files over submodules.
+		 */
 		if (S_ISREG(a->mode)) {
 			result->blob.mode = a->mode;
 			oidcpy(&result->blob.oid, &a->oid);
@@ -1349,6 +1368,7 @@ static int merge_mode_and_contents(struct merge_options *opt,
 			free(result_buf.ptr);
 			if (ret)
 				return ret;
+			/* FIXME: bug, what if modes didn't match? */
 			result->clean = (merge_status == 0);
 		} else if (S_ISGITLINK(a->mode)) {
 			result->clean = merge_submodule(opt, &result->blob.oid,
@@ -2664,6 +2684,14 @@ static int process_renames(struct merge_options *opt,
 	struct string_list b_by_dst = STRING_LIST_INIT_NODUP;
 	const struct rename *sre;
 
+	/*
+	 * FIXME: As string-list.h notes, it's O(n^2) to build a sorted
+	 * string_list one-by-one, but O(n log n) to build it unsorted and
+	 * then sort it.  Note that as we build the list, we do not need to
+	 * check if the existing destination path is already in the list,
+	 * because the structure of diffcore_rename guarantees we won't
+	 * have duplicates.
+	 */
 	for (i = 0; i < a_renames->nr; i++) {
 		sre = a_renames->items[i].util;
 		string_list_insert(&a_by_dst, sre->pair->two->path)->util
@@ -3602,6 +3630,15 @@ static int merge_recursive_internal(struct merge_options *opt,
 			return err(opt, _("merge returned no commit"));
 	}
 
+	/*
+	 * FIXME: Since merge_recursive_internal() is only ever called by
+	 * places that ensure the index is loaded first
+	 * (e.g. builtin/merge.c, rebase/sequencer, etc.), in the common
+	 * case where the merge base was unique that means when we get here
+	 * we immediately discard the index and re-read it, which is a
+	 * complete waste of time.  We should only be discarding and
+	 * re-reading if we were forced to recurse.
+	 */
 	discard_index(opt->repo->index);
 	if (!opt->priv->call_depth)
 		repo_read_index(opt->repo);
Signature · 1 line
gitgitgadget
#23 lore raw

Re: [PATCH 11/11] merge-recursive: add a bunch of FIXME comments documenting known bugs

Quoted 8 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

The plan is to just delete merge-recursive, but not until everyone is comfortable with merge-ort as a replacement. Given that I haven't switched all callers of merge-recursive over yet (e.g. git-am still uses merge-recursive), maybe there's some value documenting known bugs in the algorithm in case we end up keeping it or someone wants to dig it up in the future.

Yes, I think that's very useful.

#24 lore raw
In reply to Elijah Newren via GitGitGadget
Quoted 5 lines · Elijah Newren via GitGitGadget

In order to help Ævar test his tree-walk changes against merge-ort[1], this series completes the merge-ort implementation and cleans up testsuite failures...EXCEPT for some t6423 failures. It also leaves out a lot of performance work, which incidentally will fix the t6423 failures and is being reviewed independently[2].

For those testing this in combination with their work, the expected failures are these specific tests:

    ./t6423-merge-rename-directories.sh (Wstat: 256 Tests: 68 Failed: 4)
      Failed tests:  7, 53, 55, 59

Perhaps I'm missing something but why not have [1] below on top of this series? It makes that test pass in both modes, and means we could e.g. follow-up by running CI with "ort".

That CI step seems worth doing sooner than later, even if it needs some GIT_TEST_SKIP for now...

Quoted 22 lines

This 11-patch series could be submitted as 7 independent series, 1-4 patches in length each, but it's probably easier for Ævar if we can merge just one more thing and it's only 11 total patches. This series sub-divides as follows:

* Patch 1: Fix bug in already-merged portion of merge-ort affecting rename/rename conflicts on platforms where qsort isn't stable. (Could be considered for merging before 2.31 since it is a new bug in the 2.31 cycle that I just learned of last night, but not sure it matters since merge-ort wasn't complete anyway and we're not even mentioning merge-ort in the release notes.)
* Patches 2-5: Add support for renormalization
* Patch 6: Add support for subtree shifting
* Patch 7-8: Add test and support for conflicts affecting sparse-checkout entries
* Patch 9: Update submodule related merge tests to note the ones that merge-ort fixes relative to merge-recursive
* Patch 10: New feature -- allow "git diff AUTO_MERGE" during conflict resolution to let user review changes they made since merge/rebase/cherry-pick/revert stopped and informed them of conflicts
* Patch 11: Add comments noting various bugs in merge-recursive

Quoted 3 lines

The last two patches aren't needed by Ævar, so they could be left out and submitted later. I just figured that it was only two more patches and they were part of "completing the merge-ort implementation" in my view.

This whole thing was a pleasant read, and helped me catch a subtle regression in my WIP "mode" work (which I'm now about to submit).

Reviewing this series suffered from the problem you have with writing code that's clearly good enough: Mostly all I had were minor nits, suggestions to re-arrange code differently etc.

That being said I'm not all that familiar with the guts of the merge logic, so I may have missed other issues...

Quoted 36 lines

[1] https://lore.kernel.org/git/[email protected]/ [2] See
https://lore.kernel.org/git/[email protected]/;
there are five more waiting after that -- viewable by the curious at
https://github.com/gitgitgadget/git/pulls?q=is%3Apr+author%3Anewren+Optimization+batch

Elijah Newren (11):
merge-ort: use STABLE_QSORT instead of QSORT where required
merge-ort: add a special minimal index just for renormalization
merge-ort: add a function for initializing our special attr_index
merge-ort: have ll_merge() calls use the attr_index for
renormalization
merge-ort: let renormalization change modify/delete into clean delete
merge-ort: support subtree shifting
t6428: new test for SKIP_WORKTREE handling and conflicts
merge-ort: implement CE_SKIP_WORKTREE handling with conflicted entries
t: mark several submodule merging tests as fixed under merge-ort
merge-ort: write $GIT_DIR/AUTO_MERGE whenever we hit a conflict
merge-recursive: add a bunch of FIXME comments documenting known bugs

 branch.c                                      |   1 +
 builtin/rebase.c                              |   1 +
 merge-ort.c                                   | 230 ++++++++++++++++--
 merge-recursive.c                             |  37 +++
 path.c                                        |   1 +
 path.h                                        |   2 +
 sequencer.c                                   |   5 +
 t/t3512-cherry-pick-submodule.sh              |   9 +-
 t/t3513-revert-submodule.sh                   |   7 +-
 t/t5572-pull-submodule.sh                     |   9 +-
 t/t6428-merge-conflicts-sparse.sh             | 158 ++++++++++++
 t/t6437-submodule-merge.sh                    |   5 +-
 t/t6438-submodule-directory-file-conflicts.sh |   9 +-
 13 files changed, 449 insertions(+), 25 deletions(-)
 create mode 100755 t/t6428-merge-conflicts-sparse.sh
Quoted 4 lines

base-commit: f01623b2c9d14207e497b21ebc6b3ec4afaf4b46
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-973%2Fnewren%2Fort-remainder-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-973/newren/ort-remainder-v1
Pull-Request: https://github.com/git/git/pull/973

1.:

t/t6423-merge-rename-directories.sh +5 -5
index 5d3b711fe68..4f6ead11e51 100755
@@ -507,7 +507,7 @@ test_setup_2a () {
 	)
 }
 
-test_expect_success '2a: Directory split into two on one side, with equal numbers of paths' '
+test_expect_merge_algorithm success failure '2a: Directory split into two on one side, with equal numbers of paths' '
 	test_setup_2a &&
 	(
 		cd 2a &&
@@ -3060,7 +3060,7 @@ test_setup_9g () {
 	)
 }
 
-test_expect_failure '9g: Renamed directory that only contained immediate subdirs, immediate subdirs renamed' '
+test_expect_merge_algorithm failure failure '9g: Renamed directory that only contained immediate subdirs, immediate subdirs renamed' '
 	test_setup_9g &&
 	(
 		cd 9g &&
@@ -4267,7 +4267,7 @@ test_setup_12b1 () {
 	)
 }
 
-test_expect_merge_algorithm failure success '12b1: Moving two directory hierarchies into each other' '
+test_expect_merge_algorithm failure failure '12b1: Moving two directory hierarchies into each other' '
 	test_setup_12b1 &&
 	(
 		cd 12b1 &&
@@ -4435,7 +4435,7 @@ test_setup_12c1 () {
 	)
 }
 
-test_expect_merge_algorithm failure success '12c1: Moving one directory hierarchy into another w/ content merge' '
+test_expect_merge_algorithm failure failure '12c1: Moving one directory hierarchy into another w/ content merge' '
 	test_setup_12c1 &&
 	(
 		cd 12c1 &&
@@ -4797,7 +4797,7 @@ test_setup_12f () {
 	)
 }
 
-test_expect_merge_algorithm failure success '12f: Trivial directory resolve, caching, all kinds of fun' '
+test_expect_merge_algorithm failure failure '12f: Trivial directory resolve, caching, all kinds of fun' '
 	test_setup_12f &&
 	(
 		cd 12f &&
#25 lore raw
Quoted 16 lines · Ævar Arnfjörð Bjarmason
Quoted 5 lines · Elijah Newren via GitGitGadget

In order to help Ævar test his tree-walk changes against merge-ort[1], this series completes the merge-ort implementation and cleans up testsuite failures...EXCEPT for some t6423 failures. It also leaves out a lot of performance work, which incidentally will fix the t6423 failures and is being reviewed independently[2].

For those testing this in combination with their work, the expected failures are these specific tests:

    ./t6423-merge-rename-directories.sh (Wstat: 256 Tests: 68 Failed: 4)
      Failed tests:  7, 53, 55, 59
Quoted 6 lines

Perhaps I'm missing something but why not have [1] below on top of this series? It makes that test pass in both modes, and means we could e.g. follow-up by running CI with "ort".

That CI step seems worth doing sooner than later, even if it needs some GIT_TEST_SKIP for now...

Oh, um, mostly it was history and the fact that I just never took a step back. When I started adding merge-ort, there were 2200+ tests that were failing, and marking them all as test_expect_fail was just a useless code churn; we knew it'd fail when the implemenation basically just called die(). At some point I got it down to 8 tests failing, but felt I needed to quickly switch tracks to upstream the diffcore-rename work since we had an Outreachy intern that was going to be buildiing on it and/or making changes that might conflict with it. I was still in the mode of fixing them through completing the implementation rather than stepping back and saying, "Hey, we only have a few tests left failing; let's mark them as such.".

However, all that said, making this change _now_ would semantically conflict with ort-perf-batch-9[1] (which was submitted first), since it fixes one of the four tests. It'd also mean a similar semantic conflict for ort-perf-batch-10 (which fixes another) and ort-perf-batch-12 (which fixes the last 2), or those series will have to depend on a merge of ort-perf-batch-9 and this series. Relying on special merges for some of the earlier patches for merge-ort seems like it was a bit of a pain and I'd rather not put Junio through that. Also, if we do it, and don't have Junio mark the fixes as things are merged, then we have to decide on a topic order. So, Junio: thoughts?

[1] https://lore.kernel.org/git/[email protected]/

Quoted 22 lines
Quoted 22 lines

This 11-patch series could be submitted as 7 independent series, 1-4 patches in length each, but it's probably easier for Ævar if we can merge just one more thing and it's only 11 total patches. This series sub-divides as follows:

* Patch 1: Fix bug in already-merged portion of merge-ort affecting rename/rename conflicts on platforms where qsort isn't stable. (Could be considered for merging before 2.31 since it is a new bug in the 2.31 cycle that I just learned of last night, but not sure it matters since merge-ort wasn't complete anyway and we're not even mentioning merge-ort in the release notes.)
* Patches 2-5: Add support for renormalization
* Patch 6: Add support for subtree shifting
* Patch 7-8: Add test and support for conflicts affecting sparse-checkout entries
* Patch 9: Update submodule related merge tests to note the ones that merge-ort fixes relative to merge-recursive
* Patch 10: New feature -- allow "git diff AUTO_MERGE" during conflict resolution to let user review changes they made since merge/rebase/cherry-pick/revert stopped and informed them of conflicts
* Patch 11: Add comments noting various bugs in merge-recursive

Quoted 6 lines
Quoted 3 lines

The last two patches aren't needed by Ævar, so they could be left out and submitted later. I just figured that it was only two more patches and they were part of "completing the merge-ort implementation" in my view.

This whole thing was a pleasant read, and helped me catch a subtle regression in my WIP "mode" work (which I'm now about to submit).

Wahoo for helping catch regressions! :-)

Quoted 3 lines

Reviewing this series suffered from the problem you have with writing code that's clearly good enough: Mostly all I had were minor nits, suggestions to re-arrange code differently etc.

Thanks for taking a look. You had lots of good small suggestions that I'll incorporate in a re-roll. There were a couple that sounded like they should be split off into their own sets of cleanup patches that I'll defer for now.

Quoted 96 lines

That being said I'm not all that familiar with the guts of the merge logic, so I may have missed other issues...

Quoted 36 lines

[1] https://lore.kernel.org/git/[email protected]/ [2] See
https://lore.kernel.org/git/[email protected]/;
there are five more waiting after that -- viewable by the curious at
https://github.com/gitgitgadget/git/pulls?q=is%3Apr+author%3Anewren+Optimization+batch

Elijah Newren (11):
merge-ort: use STABLE_QSORT instead of QSORT where required
merge-ort: add a special minimal index just for renormalization
merge-ort: add a function for initializing our special attr_index
merge-ort: have ll_merge() calls use the attr_index for
renormalization
merge-ort: let renormalization change modify/delete into clean delete
merge-ort: support subtree shifting
t6428: new test for SKIP_WORKTREE handling and conflicts
merge-ort: implement CE_SKIP_WORKTREE handling with conflicted entries
t: mark several submodule merging tests as fixed under merge-ort
merge-ort: write $GIT_DIR/AUTO_MERGE whenever we hit a conflict
merge-recursive: add a bunch of FIXME comments documenting known bugs

 branch.c                                      |   1 +
 builtin/rebase.c                              |   1 +
 merge-ort.c                                   | 230 ++++++++++++++++--
 merge-recursive.c                             |  37 +++
 path.c                                        |   1 +
 path.h                                        |   2 +
 sequencer.c                                   |   5 +
 t/t3512-cherry-pick-submodule.sh              |   9 +-
 t/t3513-revert-submodule.sh                   |   7 +-
 t/t5572-pull-submodule.sh                     |   9 +-
 t/t6428-merge-conflicts-sparse.sh             | 158 ++++++++++++
 t/t6437-submodule-merge.sh                    |   5 +-
 t/t6438-submodule-directory-file-conflicts.sh |   9 +-
 13 files changed, 449 insertions(+), 25 deletions(-)
 create mode 100755 t/t6428-merge-conflicts-sparse.sh
Quoted 4 lines

base-commit: f01623b2c9d14207e497b21ebc6b3ec4afaf4b46
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-973%2Fnewren%2Fort-remainder-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-973/newren/ort-remainder-v1
Pull-Request: https://github.com/git/git/pull/973

1.:

t/t6423-merge-rename-directories.sh +5 -5
index 5d3b711fe68..4f6ead11e51 100755
@@ -507,7 +507,7 @@ test_setup_2a () {
        )
 }
 
-test_expect_success '2a: Directory split into two on one side, with equal numbers of paths' '
+test_expect_merge_algorithm success failure '2a: Directory split into two on one side, with equal numbers of paths' '
        test_setup_2a &&
        (
                cd 2a &&
@@ -3060,7 +3060,7 @@ test_setup_9g () {
        )
 }
 
-test_expect_failure '9g: Renamed directory that only contained immediate subdirs, immediate subdirs renamed' '
+test_expect_merge_algorithm failure failure '9g: Renamed directory that only contained immediate subdirs, immediate subdirs renamed' '
        test_setup_9g &&
        (
                cd 9g &&
@@ -4267,7 +4267,7 @@ test_setup_12b1 () {
        )
 }
 
-test_expect_merge_algorithm failure success '12b1: Moving two directory hierarchies into each other' '
+test_expect_merge_algorithm failure failure '12b1: Moving two directory hierarchies into each other' '
        test_setup_12b1 &&
        (
                cd 12b1 &&
@@ -4435,7 +4435,7 @@ test_setup_12c1 () {
        )
 }
 
-test_expect_merge_algorithm failure success '12c1: Moving one directory hierarchy into another w/ content merge' '
+test_expect_merge_algorithm failure failure '12c1: Moving one directory hierarchy into another w/ content merge' '
        test_setup_12c1 &&
        (
                cd 12c1 &&
@@ -4797,7 +4797,7 @@ test_setup_12f () {
        )
 }
 
-test_expect_merge_algorithm failure success '12f: Trivial directory resolve, caching, all kinds of fun' '
+test_expect_merge_algorithm failure failure '12f: Trivial directory resolve, caching, all kinds of fun' '
        test_setup_12f &&
        (
                cd 12f &&
#26 lore raw

v2 cover Complete merge-ort implementation...almost

In reply to Elijah Newren via GitGitGadget

This series (nearly) completes the merge-ort implementation, cleaning up testsuite failures. The exceptions are some t6423 failures being addressed independently[1].

Changes since v1, to address feedback from Ævar:

* Squashed patches 3 & 4
* Various style fixes
* Check attr_index->initialized before calling initialize_attr_index() instead of inside initialize_attr_index()
* Simplify GIT_TEST_MERGE_ALGORITHM comparison to only have if-then block

Stuff not included in v2:

* Ævar suggested patching the test expectation on the 4 known failing tests so that all tests passed under GIT_TEST_MERGE_ALGORITHM=ort. Seems reasonable, but the semantic conflicts with other series might make it more trouble than it's worth and other series will fix all 4 tests. Leaving as-is for now to avoid putting more burden on Junio; see
https://lore.kernel.org/git/CABPp-BHeR6m4-M=nSX5NZtA2js3E3EVbAyDSMtp3-rN2QnUjqw@mail.gmail.com/
* Ævar noted a few bigger cleanups to surrounding code that could also be done, even if orthogonal to this series. I'll leave those for other series to address.

[1] See
https://lore.kernel.org/git/[email protected]/
and batch 10 and 12 from
https://github.com/gitgitgadget/git/pulls?q=is%3Apr+author%3Anewren+Optimization+batch

Elijah Newren (10):
merge-ort: use STABLE_QSORT instead of QSORT where required
merge-ort: add a special minimal index just for renormalization
merge-ort: have ll_merge() use a special attr_index for
renormalization
merge-ort: let renormalization change modify/delete into clean delete
merge-ort: support subtree shifting
t6428: new test for SKIP_WORKTREE handling and conflicts
merge-ort: implement CE_SKIP_WORKTREE handling with conflicted entries
t: mark several submodule merging tests as fixed under merge-ort
merge-ort: write $GIT_DIR/AUTO_MERGE whenever we hit a conflict
merge-recursive: add a bunch of FIXME comments documenting known bugs

 branch.c                                      |   1 +
 builtin/rebase.c                              |   1 +
 merge-ort.c                                   | 229 ++++++++++++++++--
 merge-recursive.c                             |  37 +++
 path.c                                        |   1 +
 path.h                                        |   2 +
 sequencer.c                                   |   5 +
 t/t3512-cherry-pick-submodule.sh              |   7 +-
 t/t3513-revert-submodule.sh                   |   5 +-
 t/t5572-pull-submodule.sh                     |   7 +-
 t/t6428-merge-conflicts-sparse.sh             | 158 ++++++++++++
 t/t6437-submodule-merge.sh                    |   5 +-
 t/t6438-submodule-directory-file-conflicts.sh |   7 +-
 13 files changed, 440 insertions(+), 25 deletions(-)
 create mode 100755 t/t6428-merge-conflicts-sparse.sh

base-commit: f01623b2c9d14207e497b21ebc6b3ec4afaf4b46
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-973%2Fnewren%2Fort-remainder-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-973/newren/ort-remainder-v2
Pull-Request: https://github.com/git/git/pull/973

Range-diff vs v1:

  1:  3ca16a5e3466 =  1:  3ca16a5e3466 merge-ort: use STABLE_QSORT instead of QSORT where required
  2:  24454e67b186 =  2:  24454e67b186 merge-ort: add a special minimal index just for renormalization
  3:  815af5d30ebd !  3:  386cb3230b67 merge-ort: add a function for initializing our special attr_index
     @@ Metadata
      Author: Elijah Newren <[email protected]>
      
       ## Commit message ##
     -    merge-ort: add a function for initializing our special attr_index
     +    merge-ort: have ll_merge() use a special attr_index for renormalization
      
     -    Add a function which can be called to populate the attr_index with the
     -    appropriate .gitattributes contents when necessary.  Make it return
     -    early if the attr_index is already initialized or if we are not
     -    renormalizing files.
     +    ll_merge() needs an index when renormalization is requested.  Create one
     +    specifically for just this purpose with just the one needed entry.  This
     +    fixes t6418.4 and t6418.5 under GIT_TEST_MERGE_ALGORITHM=ort.
      
          NOTE 1: Even if the user has a working copy or a real index (which is
          not a given as merge-ort can be used in bare repositories), we
     @@ Commit message
          Signed-off-by: Elijah Newren <[email protected]>
      
       ## merge-ort.c ##
     +@@ merge-ort.c: static void clear_or_reinit_internal_opts(struct merge_options_internal *opti,
     + 	string_list_clear(&opti->paths_to_free, 0);
     + 	opti->paths_to_free.strdup_strings = 0;
     + 
     +-	if (opti->attr_index.cache_nr)
     ++	if (opti->attr_index.cache_nr) /* true iff opt->renormalize */
     + 		discard_index(&opti->attr_index);
     + 
     + 	/* Free memory used by various renames maps */
      @@ merge-ort.c: static int merge_submodule(struct merge_options *opt,
       	return 0;
       }
       
     -+MAYBE_UNUSED
      +static void initialize_attr_index(struct merge_options *opt)
      +{
      +	/*
     @@ merge-ort.c: static int merge_submodule(struct merge_options *opt,
      +	struct index_state *attr_index = &opt->priv->attr_index;
      +	struct cache_entry *ce;
      +
     -+	if (!opt->renormalize)
     -+		return;
     ++	attr_index->initialized = 1;
      +
     -+	if (attr_index->initialized)
     ++	if (!opt->renormalize)
      +		return;
     -+	attr_index->initialized = 1;
      +
      +	mi = strmap_get(&opt->priv->paths, GITATTRIBUTES_FILE);
      +	if (!mi)
     @@ merge-ort.c: static int merge_submodule(struct merge_options *opt,
      +		add_index_entry(attr_index, ce,
      +				ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE);
      +		get_stream_filter(attr_index, GITATTRIBUTES_FILE, &ce->oid);
     -+	}
     -+	else {
     ++	} else {
      +		int stage, len;
      +		struct conflict_info *ci;
      +
      +		ASSIGN_AND_VERIFY_CI(ci, mi);
     -+		for (stage=0; stage<3; ++stage) {
     ++		for (stage = 0; stage < 3; stage++) {
      +			unsigned stage_mask = (1 << stage);
      +
      +			if (!(ci->filemask & stage_mask))
     @@ merge-ort.c: static int merge_submodule(struct merge_options *opt,
       static int merge_3way(struct merge_options *opt,
       		      const char *path,
       		      const struct object_id *o,
     +@@ merge-ort.c: static int merge_3way(struct merge_options *opt,
     + 	char *base, *name1, *name2;
     + 	int merge_status;
     + 
     ++	if (!opt->priv->attr_index.initialized)
     ++		initialize_attr_index(opt);
     ++
     + 	ll_opts.renormalize = opt->renormalize;
     + 	ll_opts.extra_marker_size = extra_marker_size;
     + 	ll_opts.xdl_opts = opt->xdl_opts;
     +@@ merge-ort.c: static int merge_3way(struct merge_options *opt,
     + 
     + 	merge_status = ll_merge(result_buf, path, &orig, base,
     + 				&src1, name1, &src2, name2,
     +-				opt->repo->index, &ll_opts);
     ++				&opt->priv->attr_index, &ll_opts);
     + 
     + 	free(base);
     + 	free(name1);
  4:  cb035ac5fe4a <  -:  ------------ merge-ort: have ll_merge() calls use the attr_index for renormalization
  5:  b70ef4d7000a !  4:  7fcabded5016 merge-ort: let renormalization change modify/delete into clean delete
     @@ merge-ort.c: static int string_list_df_name_compare(const char *one, const char
      +	int ret = 0; /* assume changed for safety */
      +	const struct index_state *idx = &opt->priv->attr_index;
      +
     -+	initialize_attr_index(opt);
     ++	if (!idx->initialized)
     ++		initialize_attr_index(opt);
      +
      +	if (base->mode != side->mode)
      +		return 0;
  6:  d04ddabde124 =  5:  e21eea71e707 merge-ort: support subtree shifting
  7:  6ccb24b557fc =  6:  d1d8c017b23f t6428: new test for SKIP_WORKTREE handling and conflicts
  8:  100c0187bdfe =  7:  90a57ade629d merge-ort: implement CE_SKIP_WORKTREE handling with conflicted entries
  9:  95a6c0abe558 !  8:  fcce88c5ac3d t: mark several submodule merging tests as fixed under merge-ort
     @@ t/t3512-cherry-pick-submodule.sh: export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
       
      -KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
      -KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
     -+if test "$GIT_TEST_MERGE_ALGORITHM" = ort
     ++if test "$GIT_TEST_MERGE_ALGORITHM" != ort
      +then
     -+	:  # No special additional KNOWN_FAILURE knobs to set
     -+else
      +	KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
      +	KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
      +fi
     @@ t/t3513-revert-submodule.sh: git_revert () {
       }
       
      -KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
     -+if test "$GIT_TEST_MERGE_ALGORITHM" = ort
     ++if test "$GIT_TEST_MERGE_ALGORITHM" != ort
      +then
     -+	:  # No special additional KNOWN_FAILURE knobs to set
     -+else
      +	KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
      +fi
       test_submodule_switch_func "git_revert"
     @@ t/t5572-pull-submodule.sh: git_pull_noff () {
       
      -KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
      -KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
     -+if test "$GIT_TEST_MERGE_ALGORITHM" = ort
     ++if test "$GIT_TEST_MERGE_ALGORITHM" != ort
      +then
     -+	:  # No special additional KNOWN_FAILURE knobs to set
     -+else
      +	KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
      +	KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
      +fi
     @@ t/t6438-submodule-directory-file-conflicts.sh: test_submodule_switch "merge --ff
       
      -KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
      -KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
     -+if test "$GIT_TEST_MERGE_ALGORITHM" = ort
     ++if test "$GIT_TEST_MERGE_ALGORITHM" != ort
      +then
     -+	:  # No special additional KNOWN_FAILURE knobs to set
     -+else
      +	KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
      +	KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
      +fi
 10:  d8c6eb39aa7c =  9:  93b241c2f213 merge-ort: write $GIT_DIR/AUTO_MERGE whenever we hit a conflict
 11:  0409118d4ff7 = 10:  a9a95bb0391f merge-recursive: add a bunch of FIXME comments documenting known bugs
Signature · 1 line
gitgitgadget
#27 lore raw

PATCH v2 1/10 merge-ort: use STABLE_QSORT instead of QSORT where required

From: Elijah Newren <[email protected]>

rename/rename conflict handling depends on the fact that if both sides renamed the same path, that the one on the MERGE_SIDE1 will appear first in the combined diff_queue_struct passed to process_renames(). Since we add all pairs from MERGE_SIDE1 to combined first, and then all pairs from MERGE_SIDE2, and then sort based on filename, this will only be true if the sort used is stable. This was found due to the fact that Mac, unlike Linux, apparently has a system-defined qsort that is not stable.

While we are at it, review the other callers of QSORT and add comments about why they can remain as calls to QSORT instead of being modified to call STABLE_QSORT.

---
 merge-ort.c | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)
merge-ort.c +7 -1
index 603d30c52170..5309488fd9d8 100644
@@ -2289,7 +2289,7 @@ static int detect_and_process_renames(struct merge_options *opt,
 	clean &= collect_renames(opt, &combined, MERGE_SIDE2,
 				 &renames->dir_renames[1],
 				 &renames->dir_renames[2]);
-	QSORT(combined.queue, combined.nr, compare_pairs);
+	STABLE_QSORT(combined.queue, combined.nr, compare_pairs);
 	trace2_region_leave("merge", "directory renames", opt->repo);
 
 	trace2_region_enter("merge", "process renames", opt->repo);
@@ -2415,6 +2415,7 @@ static void write_tree(struct object_id *result_oid,
 	 */
 	relevant_entries.items = versions->items + offset;
 	relevant_entries.nr = versions->nr - offset;
+	/* No need for STABLE_QSORT -- filenames must be unique */
 	QSORT(relevant_entries.items, relevant_entries.nr, tree_entry_order);
 
 	/* Pre-allocate some space in buf */
@@ -3190,6 +3191,11 @@ static int record_conflicted_index_entries(struct merge_options *opt,
 	 * entries we added to the end into their right locations.
 	 */
 	remove_marked_cache_entries(index, 1);
+	/*
+	 * No need for STABLE_QSORT -- cmp_cache_name_compare sorts primarily
+	 * on filename and secondarily on stage, and (name, stage #) are a
+	 * unique tuple.
+	 */
 	QSORT(index->cache, index->cache_nr, cmp_cache_name_compare);
 
 	return errs;
Signature · 1 line
gitgitgadget
#28 lore raw

PATCH v2 2/10 merge-ort: add a special minimal index just for renormalization

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

renormalize_buffer() requires an index_state, which is something that merge-ort does not operate with. However, all the renormalization code needs is an index with a .gitattributes file...plus a little bit of setup. Create such an index, along with the deallocation and attr_direction handling.

A subsequent commit will add a function to finish the initialization of this index.

---
 merge-ort.c | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)
merge-ort.c +20 -0
index 5309488fd9d8..d91b66a052b6 100644
@@ -18,6 +18,7 @@
 #include "merge-ort.h"
 
 #include "alloc.h"
+#include "attr.h"
 #include "blob.h"
 #include "cache-tree.h"
 #include "commit.h"
@@ -170,6 +171,16 @@ struct merge_options_internal {
 	 */
 	struct rename_info renames;
 
+	/*
+	 * attr_index: hacky minimal index used for renormalization
+	 *
+	 * renormalization code _requires_ an index, though it only needs to
+	 * find a .gitattributes file within the index.  So, when
+	 * renormalization is important, we create a special index with just
+	 * that one file.
+	 */
+	struct index_state attr_index;
+
 	/*
 	 * current_dir_name, toplevel_dir: temporary vars
 	 *
@@ -349,6 +360,9 @@ static void clear_or_reinit_internal_opts(struct merge_options_internal *opti,
 	string_list_clear(&opti->paths_to_free, 0);
 	opti->paths_to_free.strdup_strings = 0;
 
+	if (opti->attr_index.cache_nr)
+		discard_index(&opti->attr_index);
+
 	/* Free memory used by various renames maps */
 	for (i = MERGE_SIDE1; i <= MERGE_SIDE2; ++i) {
 		struct hashmap_iter iter;
@@ -3272,6 +3286,8 @@ void merge_finalize(struct merge_options *opt,
 {
 	struct merge_options_internal *opti = result->priv;
 
+	if (opt->renormalize)
+		git_attr_set_direction(GIT_ATTR_CHECKIN);
 	assert(opt->priv == NULL);
 
 	clear_or_reinit_internal_opts(opti, 0);
@@ -3347,6 +3363,10 @@ static void merge_start(struct merge_options *opt, struct merge_result *result)
 	/* Default to histogram diff.  Actually, just hardcode it...for now. */
 	opt->xdl_opts = DIFF_WITH_ALG(opt, HISTOGRAM_DIFF);
 
+	/* Handle attr direction stuff for renormalization */
+	if (opt->renormalize)
+		git_attr_set_direction(GIT_ATTR_CHECKOUT);
+
 	/* Initialization of opt->priv, our internal merge data */
 	trace2_region_enter("merge", "allocate/init", opt->repo);
 	if (opt->priv) {
Signature · 1 line
gitgitgadget
#29 lore raw

Re: [PATCH v2 02/10] merge-ort: add a special minimal index just for renormalization

Quoted 10 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

renormalize_buffer() requires an index_state, which is something that merge-ort does not operate with. However, all the renormalization code needs is an index with a .gitattributes file...plus a little bit of setup. Create such an index, along with the deallocation and attr_direction handling.

A subsequent commit will add a function to finish the initialization of this index.

Is this the best way to solve this problem? Shouldn't we extract the logic that interacts with the .gitattributes file from renormalize_buffer() (say, to renormalize_buffer_attributes()) and just pass the .gitattributes file instead of creating a fake index?

It probably moves more code to do it the way I recommend, but it helps to separate the concerns a bit better. Of course, I haven't tried doing it myself, so maybe it's harder than I think.

Thanks,
-Stolee

#30 lore raw

PATCH v2 3/10 merge-ort: have ll_merge() use a special attr_index for renormalization

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

ll_merge() needs an index when renormalization is requested. Create one specifically for just this purpose with just the one needed entry. This fixes t6418.4 and t6418.5 under GIT_TEST_MERGE_ALGORITHM=ort.

NOTE 1: Even if the user has a working copy or a real index (which is not a given as merge-ort can be used in bare repositories), we explicitly ignore any .gitattributes file from either of these locations. merge-ort can be used to merge two branches that are unrelated to HEAD, so .gitattributes from the working copy and current index should not be considered relevant.

NOTE 2: Since we are in the middle of merging, there is a risk that .gitattributes itself is conflicted...leaving us with an ill-defined situation about how to perform the rest of the merge. It could be that the .gitattributes file does not even exist on one of the sides of the merge, or that it has been modified on both sides. If it's been modified on both sides, it's possible that it could itself be merged cleanly, though it's also possible that it only merges cleanly if you use the right version of the .gitattributes file to drive the merge. It gets kind of complicated. The only test we ever had that attempted to test behavior in this area was seemingly unaware of the undefined behavior, but knew the test wouldn't work for lack of attribute handling support, marked it as test_expect_failure from the beginning, but managed to fail for several reasons unrelated to attribute handling. See commit 6f6e7cfb52 ("t6038: remove problematic test", 2020-08-03) for details. So there are probably various ways to improve what initialize_attr_index() picks in the case of a conflicted .gitattributes but for now I just implemented something simple -- look for whatever .gitattributes file we can find in any of the higher order stages and use it.

---
 merge-ort.c | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 62 insertions(+), 2 deletions(-)
merge-ort.c +62 -2
index d91b66a052b6..ea720eb51971 100644
@@ -360,7 +360,7 @@ static void clear_or_reinit_internal_opts(struct merge_options_internal *opti,
 	string_list_clear(&opti->paths_to_free, 0);
 	opti->paths_to_free.strdup_strings = 0;
 
-	if (opti->attr_index.cache_nr)
+	if (opti->attr_index.cache_nr) /* true iff opt->renormalize */
 		discard_index(&opti->attr_index);
 
 	/* Free memory used by various renames maps */
@@ -988,6 +988,63 @@ static int merge_submodule(struct merge_options *opt,
 	return 0;
 }
 
+static void initialize_attr_index(struct merge_options *opt)
+{
+	/*
+	 * The renormalize_buffer() functions require attributes, and
+	 * annoyingly those can only be read from the working tree or from
+	 * an index_state.  merge-ort doesn't have an index_state, so we
+	 * generate a fake one containing only attribute information.
+	 */
+	struct merged_info *mi;
+	struct index_state *attr_index = &opt->priv->attr_index;
+	struct cache_entry *ce;
+
+	attr_index->initialized = 1;
+
+	if (!opt->renormalize)
+		return;
+
+	mi = strmap_get(&opt->priv->paths, GITATTRIBUTES_FILE);
+	if (!mi)
+		return;
+
+	if (mi->clean) {
+		int len = strlen(GITATTRIBUTES_FILE);
+		ce = make_empty_cache_entry(attr_index, len);
+		ce->ce_mode = create_ce_mode(mi->result.mode);
+		ce->ce_flags = create_ce_flags(0);
+		ce->ce_namelen = len;
+		oidcpy(&ce->oid, &mi->result.oid);
+		memcpy(ce->name, GITATTRIBUTES_FILE, len);
+		add_index_entry(attr_index, ce,
+				ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE);
+		get_stream_filter(attr_index, GITATTRIBUTES_FILE, &ce->oid);
+	} else {
+		int stage, len;
+		struct conflict_info *ci;
+
+		ASSIGN_AND_VERIFY_CI(ci, mi);
+		for (stage = 0; stage < 3; stage++) {
+			unsigned stage_mask = (1 << stage);
+
+			if (!(ci->filemask & stage_mask))
+				continue;
+			len = strlen(GITATTRIBUTES_FILE);
+			ce = make_empty_cache_entry(attr_index, len);
+			ce->ce_mode = create_ce_mode(ci->stages[stage].mode);
+			ce->ce_flags = create_ce_flags(stage);
+			ce->ce_namelen = len;
+			oidcpy(&ce->oid, &ci->stages[stage].oid);
+			memcpy(ce->name, GITATTRIBUTES_FILE, len);
+			add_index_entry(attr_index, ce,
+					ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE);
+			get_stream_filter(attr_index, GITATTRIBUTES_FILE,
+					  &ce->oid);
+		}
+	}
+}
+
 static int merge_3way(struct merge_options *opt,
 		      const char *path,
 		      const struct object_id *o,
@@ -1002,6 +1059,9 @@ static int merge_3way(struct merge_options *opt,
 	char *base, *name1, *name2;
 	int merge_status;
 
+	if (!opt->priv->attr_index.initialized)
+		initialize_attr_index(opt);
+
 	ll_opts.renormalize = opt->renormalize;
 	ll_opts.extra_marker_size = extra_marker_size;
 	ll_opts.xdl_opts = opt->xdl_opts;
@@ -1040,7 +1100,7 @@ static int merge_3way(struct merge_options *opt,
 
 	merge_status = ll_merge(result_buf, path, &orig, base,
 				&src1, name1, &src2, name2,
-				opt->repo->index, &ll_opts);
+				&opt->priv->attr_index, &ll_opts);
 
 	free(base);
 	free(name1);
Signature · 1 line
gitgitgadget
#31 lore raw

Re: [PATCH v2 03/10] merge-ort: have ll_merge() use a special attr_index for renormalization

Quoted 5 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

ll_merge() needs an index when renormalization is requested. Create one specifically for just this purpose with just the one needed entry. This fixes t6418.4 and t6418.5 under GIT_TEST_MERGE_ALGORITHM=ort.

I have similar concerns here. This strategy of creating an index with only one entry is adding some deep coupling between the ORT code and these normalization methods. It would be a better pattern to extract the logic that normalizes based on some attributes file without caring where it came from.

Thanks,
-Stolee

#32 lore raw

PATCH v2 4/10 merge-ort: let renormalization change modify/delete into clean delete

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

When we have a modify/delete conflict, but the only change to the modification is e.g. change of line endings, then if renormalization is requested then we should be able to recognize such a case as a not-modified/delete and resolve the conflict automatically.

This fixes t6418.10 under GIT_TEST_MERGE_ALGORITHM=ort.

---
 merge-ort.c | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 62 insertions(+), 2 deletions(-)
merge-ort.c +62 -2
index ea720eb51971..db46f496cb82 100644
@@ -2414,6 +2414,61 @@ static int string_list_df_name_compare(const char *one, const char *two)
 	return onelen - twolen;
 }
 
+static int read_oid_strbuf(struct merge_options *opt,
+			   const struct object_id *oid,
+			   struct strbuf *dst)
+{
+	void *buf;
+	enum object_type type;
+	unsigned long size;
+	buf = read_object_file(oid, &type, &size);
+	if (!buf)
+		return err(opt, _("cannot read object %s"), oid_to_hex(oid));
+	if (type != OBJ_BLOB) {
+		free(buf);
+		return err(opt, _("object %s is not a blob"), oid_to_hex(oid));
+	}
+	strbuf_attach(dst, buf, size, size + 1);
+	return 0;
+}
+
+static int blob_unchanged(struct merge_options *opt,
+			  const struct version_info *base,
+			  const struct version_info *side,
+			  const char *path)
+{
+	struct strbuf basebuf = STRBUF_INIT;
+	struct strbuf sidebuf = STRBUF_INIT;
+	int ret = 0; /* assume changed for safety */
+	const struct index_state *idx = &opt->priv->attr_index;
+
+	if (!idx->initialized)
+		initialize_attr_index(opt);
+
+	if (base->mode != side->mode)
+		return 0;
+	if (oideq(&base->oid, &side->oid))
+		return 1;
+
+	if (read_oid_strbuf(opt, &base->oid, &basebuf) ||
+	    read_oid_strbuf(opt, &side->oid, &sidebuf))
+		goto error_return;
+	/*
+	 * Note: binary | is used so that both renormalizations are
+	 * performed.  Comparison can be skipped if both files are
+	 * unchanged since their sha1s have already been compared.
+	 */
+	if (renormalize_buffer(idx, path, basebuf.buf, basebuf.len, &basebuf) |
+	    renormalize_buffer(idx, path, sidebuf.buf, sidebuf.len, &sidebuf))
+		ret = (basebuf.len == sidebuf.len &&
+		       !memcmp(basebuf.buf, sidebuf.buf, basebuf.len));
+
+error_return:
+	strbuf_release(&basebuf);
+	strbuf_release(&sidebuf);
+	return ret;
+}
+
 struct directory_versions {
 	/*
 	 * versions: list of (basename -> version_info)
@@ -3001,8 +3056,13 @@ static void process_entry(struct merge_options *opt,
 		modify_branch = (side == 1) ? opt->branch1 : opt->branch2;
 		delete_branch = (side == 1) ? opt->branch2 : opt->branch1;
 
-		if (ci->path_conflict &&
-		    oideq(&ci->stages[0].oid, &ci->stages[side].oid)) {
+		if (opt->renormalize &&
+		    blob_unchanged(opt, &ci->stages[0], &ci->stages[side],
+				   path)) {
+			ci->merged.is_null = 1;
+			ci->merged.clean = 1;
+		} else if (ci->path_conflict &&
+			   oideq(&ci->stages[0].oid, &ci->stages[side].oid)) {
 			/*
 			 * This came from a rename/delete; no action to take,
 			 * but avoid printing "modify/delete" conflict notice
Signature · 1 line
gitgitgadget
#33 lore raw

PATCH v2 5/10 merge-ort: support subtree shifting

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

merge-recursive has some simple code to support subtree shifting; copy it over to merge-ort. This fixes t6409.12 under GIT_TEST_MERGE_ALGORITHM=ort.

---
 merge-ort.c | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)
merge-ort.c +24 -0
index db46f496cb82..cfd956a3f435 100644
@@ -3416,6 +3416,23 @@ void merge_finalize(struct merge_options *opt,
 
 /*** Function Grouping: helper functions for merge_incore_*() ***/
 
+static struct tree *shift_tree_object(struct repository *repo,
+				      struct tree *one, struct tree *two,
+				      const char *subtree_shift)
+{
+	struct object_id shifted;
+
+	if (!*subtree_shift) {
+		shift_tree(repo, &one->object.oid, &two->object.oid, &shifted, 0);
+	} else {
+		shift_tree_by(repo, &one->object.oid, &two->object.oid, &shifted,
+			      subtree_shift);
+	}
+	if (oideq(&two->object.oid, &shifted))
+		return two;
+	return lookup_tree(repo, &shifted);
+}
+
 static inline void set_commit_tree(struct commit *c, struct tree *t)
 {
 	c->maybe_tree = t;
@@ -3543,6 +3560,13 @@ static void merge_ort_nonrecursive_internal(struct merge_options *opt,
 {
 	struct object_id working_tree_oid;
 
+	if (opt->subtree_shift) {
+		side2 = shift_tree_object(opt->repo, side1, side2,
+					  opt->subtree_shift);
+		merge_base = shift_tree_object(opt->repo, side1, merge_base,
+					       opt->subtree_shift);
+	}
+
 	trace2_region_enter("merge", "collect_merge_info", opt->repo);
 	if (collect_merge_info(opt, merge_base, side1, side2) != 0) {
 		/*
Signature · 1 line
gitgitgadget
#34 lore raw

PATCH v2 6/10 t6428: new test for SKIP_WORKTREE handling and conflicts

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

If there is a conflict during a merge for a SKIP_WORKTREE entry, we expect that file to be written to the working copy and have the SKIP_WORKTREE bit cleared in the index. If the user had manually created a file in the working tree despite SKIP_WORKTREE being set, we do not want to clobber their changes to that file, but want to move it out of the way. Add tests that check for these behaviors.

---
 t/t6428-merge-conflicts-sparse.sh | 158 ++++++++++++++++++++++++++++++
 1 file changed, 158 insertions(+)
 create mode 100755 t/t6428-merge-conflicts-sparse.sh
t/t6428-merge-conflicts-sparse.sh new file +158 -0
new file mode 100755
index 000000000000..1bb52ff6f38c
@@ -0,0 +1,158 @@
+#!/bin/sh
+
+test_description="merge cases"
+
+# The setup for all of them, pictorially, is:
+#
+#      A
+#      o
+#     / \
+#  O o   ?
+#     \ /
+#      o
+#      B
+#
+# To help make it easier to follow the flow of tests, they have been
+# divided into sections and each test will start with a quick explanation
+# of what commits O, A, and B contain.
+#
+# Notation:
+#    z/{b,c}   means  files z/b and z/c both exist
+#    x/d_1     means  file x/d exists with content d1.  (Purpose of the
+#                     underscore notation is to differentiate different
+#                     files that might be renamed into each other's paths.)
+
+. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-merge.sh
+
+
+# Testcase basic, conflicting changes in 'numerals'
+
+test_setup_numerals () {
+	test_create_repo numerals_$1 &&
+	(
+		cd numerals_$1 &&
+
+		>README &&
+		test_write_lines I II III >numerals &&
+		git add README numerals &&
+		test_tick &&
+		git commit -m "O" &&
+
+		git branch O &&
+		git branch A &&
+		git branch B &&
+
+		git checkout A &&
+		test_write_lines I II III IIII >numerals &&
+		git add numerals &&
+		test_tick &&
+		git commit -m "A" &&
+
+		git checkout B &&
+		test_write_lines I II III IV >numerals &&
+		git add numerals &&
+		test_tick &&
+		git commit -m "B" &&
+
+		cat <<-EOF >expected-index &&
+		H README
+		M numerals
+		M numerals
+		M numerals
+		EOF
+
+		cat <<-EOF >expected-merge
+		I
+		II
+		III
+		<<<<<<< HEAD
+		IIII
+		=======
+		IV
+		>>>>>>> B^0
+		EOF
+
+	)
+}
+
+test_expect_merge_algorithm success failure 'conflicting entries written to worktree even if sparse' '
+	test_setup_numerals plain &&
+	(
+		cd numerals_plain &&
+
+		git checkout A^0 &&
+
+		test_path_is_file README &&
+		test_path_is_file numerals &&
+
+		git sparse-checkout init &&
+		git sparse-checkout set README &&
+
+		test_path_is_file README &&
+		test_path_is_missing numerals &&
+
+		test_must_fail git merge -s recursive B^0 &&
+
+		git ls-files -t >index_files &&
+		test_cmp expected-index index_files &&
+
+		test_path_is_file README &&
+		test_path_is_file numerals &&
+
+		test_cmp expected-merge numerals &&
+
+		# 4 other files:
+		#   * expected-merge
+		#   * expected-index
+		#   * index_files
+		#   * others
+		git ls-files -o >others &&
+		test_line_count = 4 others
+	)
+'
+
+test_expect_merge_algorithm failure failure 'present-despite-SKIP_WORKTREE handled reasonably' '
+	test_setup_numerals in_the_way &&
+	(
+		cd numerals_in_the_way &&
+
+		git checkout A^0 &&
+
+		test_path_is_file README &&
+		test_path_is_file numerals &&
+
+		git sparse-checkout init &&
+		git sparse-checkout set README &&
+
+		test_path_is_file README &&
+		test_path_is_missing numerals &&
+
+		echo foobar >numerals &&
+
+		test_must_fail git merge -s recursive B^0 &&
+
+		git ls-files -t >index_files &&
+		test_cmp expected-index index_files &&
+
+		test_path_is_file README &&
+		test_path_is_file numerals &&
+
+		test_cmp expected-merge numerals &&
+
+		# There should still be a file with "foobar" in it
+		grep foobar * &&
+
+		# 5 other files:
+		#   * expected-merge
+		#   * expected-index
+		#   * index_files
+		#   * others
+		#   * whatever name was given to the numerals file that had
+		#     "foobar" in it
+		git ls-files -o >others &&
+		test_line_count = 5 others
+	)
+'
+
+test_done
Signature · 1 line
gitgitgadget
#35 lore raw

Re: [PATCH v2 06/10] t6428: new test for SKIP_WORKTREE handling and conflicts

Quoted 8 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

If there is a conflict during a merge for a SKIP_WORKTREE entry, we expect that file to be written to the working copy and have the SKIP_WORKTREE bit cleared in the index. If the user had manually created a file in the working tree despite SKIP_WORKTREE being set, we do not want to clobber their changes to that file, but want to move it out of the way. Add tests that check for these behaviors.

Thank you for this test!

-Stolee

#36 lore raw

PATCH v2 7/10 merge-ort: implement CE_SKIP_WORKTREE handling with conflicted entries

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

When merge conflicts occur in paths removed by a sparse-checkout, we need to unsparsify those paths (clear the SKIP_WORKTREE bit), and write out the conflicted file to the working copy. In the very unlikely case that someone manually put a file into the working copy at the location of the SKIP_WORKTREE file, we need to avoid overwriting whatever edits they have made and move that file to a different location first.

---
 merge-ort.c                       | 43 +++++++++++++++++++++----------
 t/t6428-merge-conflicts-sparse.sh |  4 +--
 2 files changed, 32 insertions(+), 15 deletions(-)
merge-ort.c +30 -13
index cfd956a3f435..040287e3a998 100644
@@ -3234,23 +3234,27 @@ static int checkout(struct merge_options *opt,
 	return ret;
 }
 
-static int record_conflicted_index_entries(struct merge_options *opt,
-					   struct index_state *index,
-					   struct strmap *paths,
-					   struct strmap *conflicted)
+static int record_conflicted_index_entries(struct merge_options *opt)
 {
 	struct hashmap_iter iter;
 	struct strmap_entry *e;
+	struct index_state *index = opt->repo->index;
+	struct checkout state = CHECKOUT_INIT;
 	int errs = 0;
 	int original_cache_nr;
 
-	if (strmap_empty(conflicted))
+	if (strmap_empty(&opt->priv->conflicted))
 		return 0;
 
+	/* If any entries have skip_worktree set, we'll have to check 'em out */
+	state.force = 1;
+	state.quiet = 1;
+	state.refresh_cache = 1;
+	state.istate = index;
 	original_cache_nr = index->cache_nr;
 
 	/* Put every entry from paths into plist, then sort */
-	strmap_for_each_entry(conflicted, &iter, e) {
+	strmap_for_each_entry(&opt->priv->conflicted, &iter, e) {
 		const char *path = e->key;
 		struct conflict_info *ci = e->value;
 		int pos;
@@ -3291,9 +3295,23 @@ static int record_conflicted_index_entries(struct merge_options *opt,
 			 * the higher order stages.  Thus, we need override
 			 * the CE_SKIP_WORKTREE bit and manually write those
 			 * files to the working disk here.
-			 *
-			 * TODO: Implement this CE_SKIP_WORKTREE fixup.
 			 */
+			if (ce_skip_worktree(ce)) {
+				struct stat st;
+
+				if (!lstat(path, &st)) {
+					char *new_name = unique_path(&opt->priv->paths,
+								     path,
+								     "cruft");
+
+					path_msg(opt, path, 1,
+						 _("Note: %s not up to date and in way of checking out conflicted version; old copy renamed to %s"),
+						 path, new_name);
+					errs |= rename(path, new_name);
+					free(new_name);
+				}
+				errs |= checkout_entry(ce, &state, NULL, NULL);
+			}
 
 			/*
 			 * Mark this cache entry for removal and instead add
@@ -3343,8 +3361,6 @@ void merge_switch_to_result(struct merge_options *opt,
 {
 	assert(opt->priv == NULL);
 	if (result->clean >= 0 && update_worktree_and_index) {
-		struct merge_options_internal *opti = result->priv;
-
 		trace2_region_enter("merge", "checkout", opt->repo);
 		if (checkout(opt, head, result->tree)) {
 			/* failure to function */
@@ -3354,13 +3370,14 @@ void merge_switch_to_result(struct merge_options *opt,
 		trace2_region_leave("merge", "checkout", opt->repo);
 
 		trace2_region_enter("merge", "record_conflicted", opt->repo);
-		if (record_conflicted_index_entries(opt, opt->repo->index,
-						    &opti->paths,
-						    &opti->conflicted)) {
+		opt->priv = result->priv;
+		if (record_conflicted_index_entries(opt)) {
 			/* failure to function */
+			opt->priv = NULL;
 			result->clean = -1;
 			return;
 		}
+		opt->priv = NULL;
 		trace2_region_leave("merge", "record_conflicted", opt->repo);
 	}
 
t/t6428-merge-conflicts-sparse.sh +2 -2
index 1bb52ff6f38c..7e8bf497f821 100755
@@ -76,7 +76,7 @@ test_setup_numerals () {
 	)
 }
 
-test_expect_merge_algorithm success failure 'conflicting entries written to worktree even if sparse' '
+test_expect_success 'conflicting entries written to worktree even if sparse' '
 	test_setup_numerals plain &&
 	(
 		cd numerals_plain &&
@@ -112,7 +112,7 @@ test_expect_merge_algorithm success failure 'conflicting entries written to work
 	)
 '
 
-test_expect_merge_algorithm failure failure 'present-despite-SKIP_WORKTREE handled reasonably' '
+test_expect_merge_algorithm failure success 'present-despite-SKIP_WORKTREE handled reasonably' '
 	test_setup_numerals in_the_way &&
 	(
 		cd numerals_in_the_way &&
Signature · 1 line
gitgitgadget
#37 lore raw

PATCH v2 8/10 t: mark several submodule merging tests as fixed under merge-ort

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

merge-ort handles submodules (and directory/file conflicts in general) differently than merge-recursive does; it basically puts all the special handling for different filetypes into one place in the codebase instead of needing special handling for different filetypes in many different code paths. This one code path in merge-ort could perhaps use some work still (there are still test_expect_failure cases in the testsuite), but it passes all the tests that merge-recursive does as well as 12 additional ones that merge-recursive fails. Mark those 12 tests as test_expect_success under merge-ort.

---
 t/t3512-cherry-pick-submodule.sh              | 7 +++++--
 t/t3513-revert-submodule.sh                   | 5 ++++-
 t/t5572-pull-submodule.sh                     | 7 +++++--
 t/t6437-submodule-merge.sh                    | 5 +++--
 t/t6438-submodule-directory-file-conflicts.sh | 7 +++++--
 5 files changed, 22 insertions(+), 9 deletions(-)
t/t3512-cherry-pick-submodule.sh +5 -2
index 822f2d4bfbd5..c657840db33b 100755
@@ -8,8 +8,11 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
 . ./test-lib.sh
 . "$TEST_DIRECTORY"/lib-submodule-update.sh
 
-KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
-KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
+if test "$GIT_TEST_MERGE_ALGORITHM" != ort
+then
+	KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
+	KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
+fi
 test_submodule_switch "cherry-pick"
 
 test_expect_success 'unrelated submodule/file conflict is ignored' '
t/t3513-revert-submodule.sh +4 -1
index a759f12cbb1d..74cd96e58223 100755
@@ -30,7 +30,10 @@ git_revert () {
 	git revert HEAD
 }
 
-KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
+if test "$GIT_TEST_MERGE_ALGORITHM" != ort
+then
+	KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
+fi
 test_submodule_switch_func "git_revert"
 
 test_done
t/t5572-pull-submodule.sh +5 -2
index 29537f4798ef..4f92a116e1f0 100755
@@ -42,8 +42,11 @@ git_pull_noff () {
 	$2 git pull --no-ff
 }
 
-KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
-KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
+if test "$GIT_TEST_MERGE_ALGORITHM" != ort
+then
+	KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
+	KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
+fi
 test_submodule_switch_func "git_pull_noff"
 
 test_expect_success 'pull --recurse-submodule setup' '
t/t6437-submodule-merge.sh +3 -2
index 0f92bcf326c8..e5e89c2045e7 100755
@@ -6,6 +6,7 @@ GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
 export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
 
 . ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-merge.sh
 
 #
 # history
@@ -328,7 +329,7 @@ test_expect_success 'setup file/submodule conflict' '
 	)
 '
 
-test_expect_failure 'file/submodule conflict' '
+test_expect_merge_algorithm failure success 'file/submodule conflict' '
 	test_when_finished "git -C file-submodule reset --hard" &&
 	(
 		cd file-submodule &&
@@ -437,7 +438,7 @@ test_expect_failure 'directory/submodule conflict; keep submodule clean' '
 	)
 '
 
-test_expect_failure !FAIL_PREREQS 'directory/submodule conflict; should not treat submodule files as untracked or in the way' '
+test_expect_merge_algorithm failure success !FAIL_PREREQS 'directory/submodule conflict; should not treat submodule files as untracked or in the way' '
 	test_when_finished "git -C directory-submodule/path reset --hard" &&
 	test_when_finished "git -C directory-submodule reset --hard" &&
 	(
t/t6438-submodule-directory-file-conflicts.sh +5 -2
index 04bf4be7d792..8df67a0ef99d 100755
@@ -12,8 +12,11 @@ test_submodule_switch "merge --ff"
 
 test_submodule_switch "merge --ff-only"
 
-KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
-KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
+if test "$GIT_TEST_MERGE_ALGORITHM" != ort
+then
+	KNOWN_FAILURE_NOFF_MERGE_DOESNT_CREATE_EMPTY_SUBMODULE_DIR=1
+	KNOWN_FAILURE_NOFF_MERGE_ATTEMPTS_TO_MERGE_REMOVED_SUBMODULE_FILES=1
+fi
 test_submodule_switch "merge --no-ff"
 
 test_done
Signature · 1 line
gitgitgadget
#38 lore raw

Re: [PATCH v2 08/10] t: mark several submodule merging tests as fixed under merge-ort

Quoted 11 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

merge-ort handles submodules (and directory/file conflicts in general) differently than merge-recursive does; it basically puts all the special handling for different filetypes into one place in the codebase instead of needing special handling for different filetypes in many different code paths. This one code path in merge-ort could perhaps use some work still (there are still test_expect_failure cases in the testsuite), but it passes all the tests that merge-recursive does as well as 12 additional ones that merge-recursive fails. Mark those 12 tests as test_expect_success under merge-ort.

Nice!
-Stolee

#39 lore raw

PATCH v2 9/10 merge-ort: write $GIT_DIR/AUTO_MERGE whenever we hit a conflict

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

There are a variety of questions users might ask while resolving conflicts:
* What changes have been made since the previous (first) parent?
* What changes are staged?
* What is still unstaged? (or what is still conflicted?)
* What changes did I make to resolve conflicts so far?
The first three of these have simple answers:
* git diff HEAD
* git diff --cached
* git diff
There was no way to answer the final question previously. Adding one is trivial in merge-ort, since it works by creating a tree representing what should be written to the working copy complete with conflict markers. Simply write that tree to .git/AUTO_MERGE, allowing users to answer the fourth question with
* git diff AUTO_MERGE

I avoided using a name like "MERGE_AUTO", because that would be merge-specific (much like MERGE_HEAD, REBASE_HEAD, REVERT_HEAD, CHERRY_PICK_HEAD) and I wanted a name that didn't change depending on which type of operation the merge was part of.

Ensure that paths which clean out other temporary operation-specific files (e.g. CHERRY_PICK_HEAD, MERGE_MSG, rebase-merge/ state directory) also clean out this AUTO_MERGE file.

---
 branch.c         |  1 +
 builtin/rebase.c |  1 +
 merge-ort.c      | 10 ++++++++++
 path.c           |  1 +
 path.h           |  2 ++
 sequencer.c      |  5 +++++
 6 files changed, 20 insertions(+)
branch.c +1 -0
index 9c9dae1eae32..b71a2de29dbe 100644
@@ -344,6 +344,7 @@ void remove_merge_branch_state(struct repository *r)
 	unlink(git_path_merge_rr(r));
 	unlink(git_path_merge_msg(r));
 	unlink(git_path_merge_mode(r));
+	unlink(git_path_auto_merge(r));
 	save_autostash(git_path_merge_autostash(r));
 }
 
builtin/rebase.c +1 -0
index de400f9a1973..7d9afe118fd4 100644
@@ -739,6 +739,7 @@ static int finish_rebase(struct rebase_options *opts)
 	int ret = 0;
 
 	delete_ref(NULL, "REBASE_HEAD", NULL, REF_NO_DEREF);
+	unlink(git_path_auto_merge(the_repository));
 	apply_autostash(state_dir_path("autostash", opts));
 	close_object_store(the_repository->objects);
 	/*
merge-ort.c +10 -0
index 040287e3a998..1a07e39d2db4 100644
@@ -3361,6 +3361,9 @@ void merge_switch_to_result(struct merge_options *opt,
 {
 	assert(opt->priv == NULL);
 	if (result->clean >= 0 && update_worktree_and_index) {
+		const char *filename;
+		FILE *fp;
+
 		trace2_region_enter("merge", "checkout", opt->repo);
 		if (checkout(opt, head, result->tree)) {
 			/* failure to function */
@@ -3379,6 +3382,13 @@ void merge_switch_to_result(struct merge_options *opt,
 		}
 		opt->priv = NULL;
 		trace2_region_leave("merge", "record_conflicted", opt->repo);
+
+		trace2_region_enter("merge", "write_auto_merge", opt->repo);
+		filename = git_path_auto_merge(opt->repo);
+		fp = xfopen(filename, "w");
+		fprintf(fp, "%s\n", oid_to_hex(&result->tree->object.oid));
+		fclose(fp);
+		trace2_region_leave("merge", "write_auto_merge", opt->repo);
 	}
 
 	if (display_update_msgs) {
path.c +1 -0
index 7b385e5eb282..9e883eb52446 100644
@@ -1534,5 +1534,6 @@ REPO_GIT_PATH_FUNC(merge_rr, "MERGE_RR")
 REPO_GIT_PATH_FUNC(merge_mode, "MERGE_MODE")
 REPO_GIT_PATH_FUNC(merge_head, "MERGE_HEAD")
 REPO_GIT_PATH_FUNC(merge_autostash, "MERGE_AUTOSTASH")
+REPO_GIT_PATH_FUNC(auto_merge, "AUTO_MERGE")
 REPO_GIT_PATH_FUNC(fetch_head, "FETCH_HEAD")
 REPO_GIT_PATH_FUNC(shallow, "shallow")
path.h +2 -0
index e7e77da6aaa5..251c78d98000 100644
@@ -176,6 +176,7 @@ struct path_cache {
 	const char *merge_mode;
 	const char *merge_head;
 	const char *merge_autostash;
+	const char *auto_merge;
 	const char *fetch_head;
 	const char *shallow;
 };
@@ -191,6 +192,7 @@ const char *git_path_merge_rr(struct repository *r);
 const char *git_path_merge_mode(struct repository *r);
 const char *git_path_merge_head(struct repository *r);
 const char *git_path_merge_autostash(struct repository *r);
+const char *git_path_auto_merge(struct repository *r);
 const char *git_path_fetch_head(struct repository *r);
 const char *git_path_shallow(struct repository *r);
 
sequencer.c +5 -0
index d2332d3e1787..472cdd8c620d 100644
@@ -2096,6 +2096,7 @@ static int do_pick_commit(struct repository *r,
 		refs_delete_ref(get_main_ref_store(r), "", "CHERRY_PICK_HEAD",
 				NULL, 0);
 		unlink(git_path_merge_msg(r));
+		unlink(git_path_auto_merge(r));
 		fprintf(stderr,
 			_("dropping %s %s -- patch contents already upstream\n"),
 			oid_to_hex(&commit->object.oid), msg.subject);
@@ -2451,6 +2452,8 @@ void sequencer_post_commit_cleanup(struct repository *r, int verbose)
 		need_cleanup = 1;
 	}
 
+	unlink(git_path_auto_merge(r));
+
 	if (!need_cleanup)
 		return;
 
@@ -4111,6 +4114,7 @@ static int pick_commits(struct repository *r,
 			unlink(rebase_path_stopped_sha());
 			unlink(rebase_path_amend());
 			unlink(git_path_merge_head(r));
+			unlink(git_path_auto_merge(r));
 			delete_ref(NULL, "REBASE_HEAD", NULL, REF_NO_DEREF);
 
 			if (item->command == TODO_BREAK) {
@@ -4505,6 +4509,7 @@ static int commit_staged_changes(struct repository *r,
 		return error(_("could not commit staged changes."));
 	unlink(rebase_path_amend());
 	unlink(git_path_merge_head(r));
+	unlink(git_path_auto_merge(r));
 	if (final_fixup) {
 		unlink(rebase_path_fixup_msg());
 		unlink(rebase_path_squash_msg());
Signature · 1 line
gitgitgadget
#40 lore raw

PATCH v2 10/10 merge-recursive: add a bunch of FIXME comments documenting known bugs

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

The plan is to just delete merge-recursive, but not until everyone is comfortable with merge-ort as a replacement. Given that I haven't switched all callers of merge-recursive over yet (e.g. git-am still uses merge-recursive), maybe there's some value documenting known bugs in the algorithm in case we end up keeping it or someone wants to dig it up in the future.

---
 merge-recursive.c | 37 +++++++++++++++++++++++++++++++++++++
 1 file changed, 37 insertions(+)
merge-recursive.c +37 -0
index b052974f191c..99a197597db5 100644
@@ -1075,6 +1075,11 @@ static int merge_3way(struct merge_options *opt,
 	read_mmblob(&src1, &a->oid);
 	read_mmblob(&src2, &b->oid);
 
+	/*
+	 * FIXME: Using a->path for normalization rules in ll_merge could be
+	 * wrong if we renamed from a->path to b->path.  We should use the
+	 * target path for where the file will be written.
+	 */
 	merge_status = ll_merge(result_buf, a->path, &orig, base,
 				&src1, name1, &src2, name2,
 				opt->repo->index, &ll_opts);
@@ -1154,6 +1159,8 @@ static void print_commit(struct commit *commit)
 	struct strbuf sb = STRBUF_INIT;
 	struct pretty_print_context ctx = {0};
 	ctx.date_mode.type = DATE_NORMAL;
+	/* FIXME: Merge this with output_commit_title() */
+	assert(!merge_remote_util(commit));
 	format_commit_message(commit, " %h: %m %s", &sb, &ctx);
 	fprintf(stderr, "%s\n", sb.buf);
 	strbuf_release(&sb);
@@ -1177,6 +1184,11 @@ static int merge_submodule(struct merge_options *opt,
 	int search = !opt->priv->call_depth;
 
 	/* store a in result in case we fail */
+	/* FIXME: This is the WRONG resolution for the recursive case when
+	 * we need to be careful to avoid accidentally matching either side.
+	 * Should probably use o instead there, much like we do for merging
+	 * binaries.
+	 */
 	oidcpy(result, a);
 
 	/* we can not handle deletion conflicts */
@@ -1301,6 +1313,13 @@ static int merge_mode_and_contents(struct merge_options *opt,
 
 	if ((S_IFMT & a->mode) != (S_IFMT & b->mode)) {
 		result->clean = 0;
+		/*
+		 * FIXME: This is a bad resolution for recursive case; for
+		 * the recursive case we want something that is unlikely to
+		 * accidentally match either side.  Also, while it makes
+		 * sense to prefer regular files over symlinks, it doesn't
+		 * make sense to prefer regular files over submodules.
+		 */
 		if (S_ISREG(a->mode)) {
 			result->blob.mode = a->mode;
 			oidcpy(&result->blob.oid, &a->oid);
@@ -1349,6 +1368,7 @@ static int merge_mode_and_contents(struct merge_options *opt,
 			free(result_buf.ptr);
 			if (ret)
 				return ret;
+			/* FIXME: bug, what if modes didn't match? */
 			result->clean = (merge_status == 0);
 		} else if (S_ISGITLINK(a->mode)) {
 			result->clean = merge_submodule(opt, &result->blob.oid,
@@ -2664,6 +2684,14 @@ static int process_renames(struct merge_options *opt,
 	struct string_list b_by_dst = STRING_LIST_INIT_NODUP;
 	const struct rename *sre;
 
+	/*
+	 * FIXME: As string-list.h notes, it's O(n^2) to build a sorted
+	 * string_list one-by-one, but O(n log n) to build it unsorted and
+	 * then sort it.  Note that as we build the list, we do not need to
+	 * check if the existing destination path is already in the list,
+	 * because the structure of diffcore_rename guarantees we won't
+	 * have duplicates.
+	 */
 	for (i = 0; i < a_renames->nr; i++) {
 		sre = a_renames->items[i].util;
 		string_list_insert(&a_by_dst, sre->pair->two->path)->util
@@ -3602,6 +3630,15 @@ static int merge_recursive_internal(struct merge_options *opt,
 			return err(opt, _("merge returned no commit"));
 	}
 
+	/*
+	 * FIXME: Since merge_recursive_internal() is only ever called by
+	 * places that ensure the index is loaded first
+	 * (e.g. builtin/merge.c, rebase/sequencer, etc.), in the common
+	 * case where the merge base was unique that means when we get here
+	 * we immediately discard the index and re-read it, which is a
+	 * complete waste of time.  We should only be discarding and
+	 * re-reading if we were forced to recurse.
+	 */
 	discard_index(opt->repo->index);
 	if (!opt->priv->call_depth)
 		repo_read_index(opt->repo);
Signature · 1 line
gitgitgadget
#41 lore raw
In reply to Elijah Newren via GitGitGadget
Quoted 3 lines · Elijah Newren via GitGitGadget

This series (nearly) completes the merge-ort implementation, cleaning up testsuite failures. The exceptions are some t6423 failures being addressed independently[1].

I like that most of the patches here are small and straight-forward, and are backed up by tests.

I'm concerned about the coupling of the "index with one entry" mechanism. I'd like to see that built differently by creating methods that can operate on an attributes file directly. If this method extraction is truly too difficult, then I'm willing to concede this point. It is worth some effort to try, though.

Thanks,
-Stolee

#42 lore raw
Quoted 6 lines · Derrick Stolee
Quoted 3 lines · Elijah Newren via GitGitGadget

This series (nearly) completes the merge-ort implementation, cleaning up testsuite failures. The exceptions are some t6423 failures being addressed independently[1].

Quoted 8 lines

I like that most of the patches here are small and straight-forward, and are backed up by tests.

I'm concerned about the coupling of the "index with one entry" mechanism. I'd like to see that built differently by creating methods that can operate on an attributes file directly. If this method extraction is truly too difficult, then I'm willing to concede this point. It is worth some effort to try, though.

Oh, I agree, I hate the "index with one entry" mechanism to activate the renormalization code. It feels like a capitulation on merge-ort's design and is such a hack. I tried to figure out how to do it a different way, and failed pretty hard. I was close to the point of throwing my hands in the air and leaving the renormalization tests failing ("who uses that stuff anyway?"), but it was the _only_ test that merge-recursive passed that merge-ort didn't, so I kept trying anyway. At some point I realized that a hack along these lines might work. Although, to be honest, I still don't understand this code or why my patches work -- in particular, the get_stream_filter() call has me flummoxed. It returns a value, but I throw it away -- yet the code fails unless I call it. What is it actually doing? Clearly it has some kind of side-effect somewhere, but reading the code didn't seem to help me. I discovered the function and others via various debugging and trial-and-error attempts, and ended up finding out that just calling that function is enough. I know it works, but I don't know why. I understand everything else in merge-ort; I hate having a magic corner here. I'm not happy with it.

I'm certain that with enough effort the renormalization stuff could be fixed and made callable without an istate. Perhaps there's even a simple way to extract the relevant logic and I'm just being dense, but I got very lost in that code and I'm a bit afraid of it now.

#43 lore raw
In reply to Elijah Newren via GitGitGadget
Quoted 4 lines · Elijah Newren via GitGitGadget

This series (nearly) completes the merge-ort implementation, cleaning up testsuite failures. The exceptions are some t6423 failures being addressed independently[1].

This series was subsumed into
https://lore.kernel.org/git/[email protected]/.
So, ignore this topic and look for the changes over there.

(Reasons for the curious: This series had fixed t6409 and t6418 and the submodule TODO passes, while the other series fixed t6423. The other series could not have been included when this was submitted because it depended on the not-yet-submitted ort-perf-batch-10. Why not just wait? Well Ævar wanted a way to test his changes against merge-ort, this early submission provided that and helped him find a bug -- see https://lore.kernel.org/git/[email protected]/.
Now that we can fix all the tests, I'm withdrawing this one and including it with the other fixes.)