git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v10 36/36] merge-recursive: fix check for skipability of working tree updates

From
Elijah Newren <newren@gmail.com>
Date
Apr 19, 2018, 17:58 UTC
Message-ID
<20180419175823.7946-37-newren@gmail.com>
In-Reply-To
<20180419175823.7946-1-newren@gmail.com>
The can-working-tree-updates-be-skipped check has had a long and blemished
history.  The update can be skipped iff:
  a) The merge is clean
  b) The merge matches what was in HEAD (content, mode, pathname)
  c) The target path is usable (i.e. not involved in D/F conflict)
Traditionally, we split b into parts:
  b1) The merged result matches the content and mode found in HEAD
  b2) The merged target path existed in HEAD

Steps a & b1 are easy to check; we have always gotten those right. While it is easy to overlook step c, this was fixed seven years ago with commit 4ab9a157d069 ("merge_content(): Check whether D/F conflicts are still present", 2010-09-20). merge-recursive didn't have a readily available way to directly check step b2, so various approximations were used:

  * In commit b2c8c0a76274 ("merge-recursive: When we detect we can skip
    an update, actually skip it", 2011-02-28), it was noted that although
    the code claimed it was skipping the update, it did not actually skip
    the update.  The code was made to skip it, but used lstat(path, ...)
    as an approximation to path-was-tracked-in-index-before-merge.
  * In commit 5b448b853030 ("merge-recursive: When we detect we can skip
    an update, actually skip it", 2011-08-11), the problem with using
    lstat was noted.  It was changed to the approximation
       path2 && strcmp(path, path2)
    which is also wrong.  !path2 || strcmp(path, path2) would have been
    better, but would have fallen short with directory renames.
  * In c5b761fb2711 ("merge-recursive: ensure we write updates for
    directory-renamed file", 2018-02-14), the problem with the previous
    approximation was noted and changed to
       was_tracked(path)
    That looks close to what we were trying to answer, but was_tracked()
    as implemented at the time should have been named is_tracked(); it
    returned something different than what we were looking for.
  * To make matters more complex, fixing was_tracked() isn't sufficient
    because the splitting of b into b1 and b2 is wrong.  Consider the
    following merge with a rename/add conflict:
       side A: modify foo, add unrelated bar
       side B: rename foo->bar (but don't modify the mode or contents)
    In this case, the three-way merge of original foo, A's foo, and B's
    bar will result in a desired pathname of bar with the same
    mode/contents that A had for foo.  Thus, A had the right mode and
    contents for the file, and it had the right pathname present (namely,
    bar), but the bar that was present was unrelated to the contents, so
    the working tree update was not skippable.
Fix this by introducing a new function:
   was_tracked_and_matches(o, path, &mfi.oid, mfi.mode)
and use it to directly check for condition b.
Signed-off-by: Elijah Newren <newren@gmail.com>
---
 merge-recursive.c                      | 48 +++++++++++++++++---------
 t/t6022-merge-rename.sh                |  2 +-
 t/t6043-merge-rename-directories.sh    |  2 +-
 t/t6046-merge-skip-unneeded-updates.sh | 10 +++---
 4 files changed, 39 insertions(+), 23 deletions(-)
diff --git a/merge-recursive.c b/merge-recursive.c
index 212d34d268..1de8dc1c53 100644
--- a/merge-recursive.c
+++ b/merge-recursive.c
@@ -779,6 +779,25 @@ static int dir_in_way(const char *path, int check_working_copy, int empty_ok)
 		!(empty_ok && is_empty_dir(path));
 }
 
+/*
+ * Returns whether path was tracked in the index before the merge started,
+ * and its oid and mode match the specified values
+ */
+static int was_tracked_and_matches(struct merge_options *o, const char *path,
+				   const struct object_id *oid, unsigned mode)
+{
+	int pos = index_name_pos(&o->orig_index, path, strlen(path));
+	struct cache_entry *ce;
+
+	if (0 > pos)
+		/* we were not tracking this path before the merge */
+		return 0;
+
+	/* See if the file we were tracking before matches */
+	ce = o->orig_index.cache[pos];
+	return (oid_eq(&ce->oid, oid) && ce->ce_mode == mode);
+}
+
 /*
  * Returns whether path was tracked in the index before the merge started
  */
@@ -2821,23 +2840,20 @@ static int merge_content(struct merge_options *o,
 				       o->branch2, path2, &mfi))
 		return -1;
 
-	if (mfi.clean && !df_conflict_remains &&
-	    oid_eq(&mfi.oid, a_oid) && mfi.mode == a_mode) {
-		int path_renamed_outside_HEAD;
+	/*
+	 * We can skip updating the working tree file iff:
+	 *   a) The merge is clean
+	 *   b) The merge matches what was in HEAD (content, mode, pathname)
+	 *   c) The target path is usable (i.e. not involved in D/F conflict)
+	 */
+	if (mfi.clean &&
+	    was_tracked_and_matches(o, path, &mfi.oid, mfi.mode) &&
+	    !df_conflict_remains) {
 		output(o, 3, _("Skipped %s (merged same as existing)"), path);
-		/*
-		 * The content merge resulted in the same file contents we
-		 * already had.  We can return early if those file contents
-		 * are recorded at the correct path (which may not be true
-		 * if the merge involves a rename).
-		 */
-		path_renamed_outside_HEAD = !path2 || !strcmp(path, path2);
-		if (!path_renamed_outside_HEAD) {
-			if (add_cacheinfo(o, mfi.mode, &mfi.oid, path,
-					  0, (!o->call_depth && !is_dirty), 0))
-				return -1;
-			return mfi.clean;
-		}
+		if (add_cacheinfo(o, mfi.mode, &mfi.oid, path,
+				  0, (!o->call_depth && !is_dirty), 0))
+			return -1;
+		return mfi.clean;
 	}
 
 	if (!mfi.clean) {
diff --git a/t/t6022-merge-rename.sh b/t/t6022-merge-rename.sh
index a1fad6980b..6df2650c03 100755
--- a/t/t6022-merge-rename.sh
+++ b/t/t6022-merge-rename.sh
@@ -247,7 +247,7 @@ test_expect_success 'merge of identical changes in a renamed file' '
 	git reset --hard HEAD^ &&
 	git checkout change &&
 	GIT_MERGE_VERBOSITY=3 git merge change+rename >out &&
-	test_i18ngrep "^Skipped B" out
+	test_i18ngrep ! "^Skipped B" out
 '
 
 test_expect_success 'setup for rename + d/f conflicts' '
diff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh
index 45f620633f..2e28f2908d 100755
--- a/t/t6043-merge-rename-directories.sh
+++ b/t/t6043-merge-rename-directories.sh
@@ -3884,7 +3884,7 @@ test_expect_success '12b-setup: Moving one directory hierarchy into another' '
 	)
 '
 
-test_expect_failure '12b-check: Moving one directory hierarchy into another' '
+test_expect_success '12b-check: Moving one directory hierarchy into another' '
 	(
 		cd 12b &&
 
diff --git a/t/t6046-merge-skip-unneeded-updates.sh b/t/t6046-merge-skip-unneeded-updates.sh
index 911e2f87a4..880cd782d7 100755
--- a/t/t6046-merge-skip-unneeded-updates.sh
+++ b/t/t6046-merge-skip-unneeded-updates.sh
@@ -64,7 +64,7 @@ test_expect_success '1a-setup: Modify(A)/Modify(B), change on B subset of A' '
 	)
 '
 
-test_expect_failure '1a-check-L: Modify(A)/Modify(B), change on B subset of A' '
+test_expect_success '1a-check-L: Modify(A)/Modify(B), change on B subset of A' '
 	test_when_finished "git -C 1a reset --hard" &&
 	test_when_finished "git -C 1a clean -fd" &&
 	(
@@ -160,7 +160,7 @@ test_expect_success '2a-setup: Modify(A)/rename(B)' '
 	)
 '
 
-test_expect_failure '2a-check-L: Modify/rename, merge into modify side' '
+test_expect_success '2a-check-L: Modify/rename, merge into modify side' '
 	test_when_finished "git -C 2a reset --hard" &&
 	test_when_finished "git -C 2a clean -fd" &&
 	(
@@ -360,7 +360,7 @@ test_expect_success '2c-setup: Modify b & add c VS rename b->c' '
 	)
 '
 
-test_expect_failure '2c-check: Modify b & add c VS rename b->c' '
+test_expect_success '2c-check: Modify b & add c VS rename b->c' '
 	(
 		cd 2c &&
 
@@ -456,7 +456,7 @@ test_expect_success '3a-setup: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
 	)
 '
 
-test_expect_failure '3a-check-L: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
+test_expect_success '3a-check-L: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
 	test_when_finished "git -C 3a reset --hard" &&
 	test_when_finished "git -C 3a clean -fd" &&
 	(
@@ -579,7 +579,7 @@ test_expect_success '3b-check-L: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
 	)
 '
 
-test_expect_failure '3b-check-R: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
+test_expect_success '3b-check-R: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
 	test_when_finished "git -C 3b reset --hard" &&
 	test_when_finished "git -C 3b clean -fd" &&
 	(
-- 
2.17.0.290.ge988e9ce2a
Previous: Elijah NewrenNext: Elijah Newren
Message 2 of 78 in “Add directory rename detection to git”
  1. 00/36 Add directory rename detection to gitElijah Newren, Apr 19, 2018
  2. 36/36 merge-recursive: fix check for skipability of working tree updatesElijah Newren, Apr 19, 2018
  3. 29/36 merge-recursive: improve add_cacheinfo error handlingElijah Newren, Apr 19, 2018
  4. 19/36 merge-recursive: check for directory level conflictsElijah Newren, Apr 19, 2018
  5. 07/36 directory rename detection: more involved edge/corner testcasesElijah Newren, Apr 19, 2018
  6. 26/36 merge-recursive: fix remaining directory rename + dirty overwrite casesElijah Newren, Apr 19, 2018
  7. 14/36 merge-recursive: fix leaks of allocated renames and diff_filepairsElijah Newren, Apr 19, 2018
  8. 33/36 merge-recursive: fix was_tracked() to quit lying with some renamed pathsElijah Newren, Apr 19, 2018
  9. Martin ÅgrenApr 19, 2018
  10. Elijah NewrenApr 19, 2018
  11. SZEDER GáborApr 20, 2018
  12. Elijah NewrenApr 20, 2018
  13. 5/36 unpack_trees: fix memory corruption with split_index when src != dstElijah Newren, Apr 21, 2018
  14. Elijah NewrenApr 21, 2018
  15. Duy NguyenApr 22, 2018
  16. Elijah NewrenApr 23, 2018
  17. Duy NguyenApr 23, 2018
  18. Elijah NewrenApr 23, 2018
  19. unpack_trees: fix breakage when o->src_index != o->dst_indexElijah Newren, Apr 24, 2018
  20. Junio C HamanoApr 24, 2018
  21. Junio C HamanoApr 24, 2018
  22. unpack_trees: fix breakage when o->src_index != o->dst_indexElijah Newren, Apr 24, 2018
  23. Duy NguyenApr 29, 2018
  24. Johannes SchindelinApr 29, 2018
  25. Duy NguyenApr 30, 2018
  26. Duy NguyenApr 30, 2018
  27. Elijah NewrenApr 30, 2018
  28. Duy NguyenApr 30, 2018
  29. 24/36 merge-recursive: avoid clobbering untracked files with directory renamesElijah Newren, Apr 19, 2018
  30. 34/36 merge-recursive: fix remainder of was_dirty() to use original indexElijah Newren, Apr 19, 2018
  31. 31/36 merge-recursive: avoid triggering add_cacheinfo error with dirty modElijah Newren, Apr 19, 2018
  32. 20/36 merge-recursive: add computation of collisions due to dir rename & mergingElijah Newren, Apr 19, 2018
  33. 32/36 t6046: testcases checking whether updates can be skipped in a mergeElijah Newren, Apr 19, 2018
  34. SZEDER GáborApr 19, 2018
  35. Elijah NewrenApr 19, 2018
  36. 35/36 merge-recursive: make "Auto-merging" comment show for other mergesElijah Newren, Apr 19, 2018
  37. 23/36 merge-recursive: apply necessary modifications for directory renamesElijah Newren, Apr 19, 2018
  38. 30/36 merge-recursive: move more is_dirty handling to merge_contentElijah Newren, Apr 19, 2018
  39. 28/36 merge-recursive: avoid spurious rename/rename conflict from dir renamesElijah Newren, Apr 19, 2018
  40. 18/36 merge-recursive: add get_directory_renames()Elijah Newren, Apr 19, 2018
  41. SZEDER GáborMay 6, 2018
  42. fixup! merge-recursive: add get_directory_renames()Elijah Newren, May 7, 2018
  43. Johannes SchindelinOct 9, 2019
  44. Elijah NewrenOct 11, 2019
  45. Johannes SchindelinOct 12, 2019
  46. 25/36 merge-recursive: fix overwriting dirty files involved in renamesElijah Newren, Apr 19, 2018
  47. Martin ÅgrenApr 19, 2018
  48. Martin ÅgrenApr 19, 2018
  49. Elijah NewrenApr 19, 2018
  50. 22/36 merge-recursive: when comparing files, don't include treesElijah Newren, Apr 19, 2018
  51. 11/36 directory rename detection: tests for handling overwriting dirty filesElijah Newren, Apr 19, 2018
  52. 06/36 directory rename detection: testcases checking which side did the renameElijah Newren, Apr 19, 2018
  53. 13/36 merge-recursive: introduce new functions to handle rename logicElijah Newren, Apr 19, 2018
  54. 10/36 directory rename detection: tests for handling overwriting untracked filesElijah Newren, Apr 19, 2018
  55. 21/36 merge-recursive: check for file level conflicts then get new nameElijah Newren, Apr 19, 2018
  56. 17/36 merge-recursive: make a helper function for cleanup for handle_renamesElijah Newren, Apr 19, 2018
  57. 16/36 merge-recursive: split out code for determining diff_filepairsElijah Newren, Apr 19, 2018
  58. 15/36 merge-recursive: make !o->detect_rename codepath more obviousElijah Newren, Apr 19, 2018
  59. 27/36 directory rename detection: new testcases showcasing a pair of bugsElijah Newren, Apr 19, 2018
  60. 01/36 directory rename detection: basic testcasesElijah Newren, Apr 19, 2018
  61. 08/36 directory rename detection: testcases exploring possibly suboptimal mergesElijah Newren, Apr 19, 2018
  62. 04/36 directory rename detection: partially renamed directory testcase/discussionElijah Newren, Apr 19, 2018
  63. 09/36 directory rename detection: miscellaneous testcases to complete coverageElijah Newren, Apr 19, 2018
  64. 02/36 directory rename detection: directory splitting testcasesElijah Newren, Apr 19, 2018
  65. 12/36 merge-recursive: move the get_renames() functionElijah Newren, Apr 19, 2018
  66. 03/36 directory rename detection: testcases to avoid taking detection too farElijah Newren, Apr 19, 2018
  67. 05/36 directory rename detection: files/directories in the way of some renamesElijah Newren, Apr 19, 2018
  68. Elijah NewrenApr 19, 2018
  69. Stefan BellerApr 19, 2018
  70. Derrick StoleeApr 19, 2018
  71. Elijah NewrenApr 19, 2018
  72. Junio C HamanoApr 20, 2018
  73. Elijah NewrenApr 23, 2018
  74. 1/2 fixup! merge-recursive: fix was_tracked() to quit lying with some renamed pathsElijah Newren, Apr 24, 2018
  75. 2/2 fixup! t6046: testcases checking whether updates can be skipped in a mergeElijah Newren, Apr 24, 2018
  76. Elijah NewrenApr 23, 2018
  77. Junio C HamanoApr 23, 2018
  78. Elijah NewrenApr 24, 2018

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.