Skip to content

Commit 1de70db

Browse files
newrengitster
authored andcommitted
merge-recursive: fix check for skipability of working tree updates
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 4ab9a15 ("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 b2c8c0a ("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 5b448b8 ("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 c5b761f ("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 <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 05cf21e commit 1de70db

File tree

4 files changed

+39
-23
lines changed

4 files changed

+39
-23
lines changed

merge-recursive.c

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,25 @@ static int dir_in_way(const char *path, int check_working_copy, int empty_ok)
779779
!(empty_ok && is_empty_dir(path));
780780
}
781781

782+
/*
783+
* Returns whether path was tracked in the index before the merge started,
784+
* and its oid and mode match the specified values
785+
*/
786+
static int was_tracked_and_matches(struct merge_options *o, const char *path,
787+
const struct object_id *oid, unsigned mode)
788+
{
789+
int pos = index_name_pos(&o->orig_index, path, strlen(path));
790+
struct cache_entry *ce;
791+
792+
if (0 > pos)
793+
/* we were not tracking this path before the merge */
794+
return 0;
795+
796+
/* See if the file we were tracking before matches */
797+
ce = o->orig_index.cache[pos];
798+
return (oid_eq(&ce->oid, oid) && ce->ce_mode == mode);
799+
}
800+
782801
/*
783802
* Returns whether path was tracked in the index before the merge started
784803
*/
@@ -2821,23 +2840,20 @@ static int merge_content(struct merge_options *o,
28212840
o->branch2, path2, &mfi))
28222841
return -1;
28232842

2824-
if (mfi.clean && !df_conflict_remains &&
2825-
oid_eq(&mfi.oid, a_oid) && mfi.mode == a_mode) {
2826-
int path_renamed_outside_HEAD;
2843+
/*
2844+
* We can skip updating the working tree file iff:
2845+
* a) The merge is clean
2846+
* b) The merge matches what was in HEAD (content, mode, pathname)
2847+
* c) The target path is usable (i.e. not involved in D/F conflict)
2848+
*/
2849+
if (mfi.clean &&
2850+
was_tracked_and_matches(o, path, &mfi.oid, mfi.mode) &&
2851+
!df_conflict_remains) {
28272852
output(o, 3, _("Skipped %s (merged same as existing)"), path);
2828-
/*
2829-
* The content merge resulted in the same file contents we
2830-
* already had. We can return early if those file contents
2831-
* are recorded at the correct path (which may not be true
2832-
* if the merge involves a rename).
2833-
*/
2834-
path_renamed_outside_HEAD = !path2 || !strcmp(path, path2);
2835-
if (!path_renamed_outside_HEAD) {
2836-
if (add_cacheinfo(o, mfi.mode, &mfi.oid, path,
2837-
0, (!o->call_depth && !is_dirty), 0))
2838-
return -1;
2839-
return mfi.clean;
2840-
}
2853+
if (add_cacheinfo(o, mfi.mode, &mfi.oid, path,
2854+
0, (!o->call_depth && !is_dirty), 0))
2855+
return -1;
2856+
return mfi.clean;
28412857
}
28422858

28432859
if (!mfi.clean) {

t/t6022-merge-rename.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ test_expect_success 'merge of identical changes in a renamed file' '
247247
git reset --hard HEAD^ &&
248248
git checkout change &&
249249
GIT_MERGE_VERBOSITY=3 git merge change+rename >out &&
250-
test_i18ngrep "^Skipped B" out
250+
test_i18ngrep ! "^Skipped B" out
251251
'
252252

253253
test_expect_success 'setup for rename + d/f conflicts' '

t/t6043-merge-rename-directories.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3884,7 +3884,7 @@ test_expect_success '12b-setup: Moving one directory hierarchy into another' '
38843884
)
38853885
'
38863886

3887-
test_expect_failure '12b-check: Moving one directory hierarchy into another' '
3887+
test_expect_success '12b-check: Moving one directory hierarchy into another' '
38883888
(
38893889
cd 12b &&
38903890

t/t6046-merge-skip-unneeded-updates.sh

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ test_expect_success '1a-setup: Modify(A)/Modify(B), change on B subset of A' '
6464
)
6565
'
6666

67-
test_expect_failure '1a-check-L: Modify(A)/Modify(B), change on B subset of A' '
67+
test_expect_success '1a-check-L: Modify(A)/Modify(B), change on B subset of A' '
6868
test_when_finished "git -C 1a reset --hard" &&
6969
test_when_finished "git -C 1a clean -fd" &&
7070
(
@@ -160,7 +160,7 @@ test_expect_success '2a-setup: Modify(A)/rename(B)' '
160160
)
161161
'
162162

163-
test_expect_failure '2a-check-L: Modify/rename, merge into modify side' '
163+
test_expect_success '2a-check-L: Modify/rename, merge into modify side' '
164164
test_when_finished "git -C 2a reset --hard" &&
165165
test_when_finished "git -C 2a clean -fd" &&
166166
(
@@ -360,7 +360,7 @@ test_expect_success '2c-setup: Modify b & add c VS rename b->c' '
360360
)
361361
'
362362

363-
test_expect_failure '2c-check: Modify b & add c VS rename b->c' '
363+
test_expect_success '2c-check: Modify b & add c VS rename b->c' '
364364
(
365365
cd 2c &&
366366
@@ -456,7 +456,7 @@ test_expect_success '3a-setup: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
456456
)
457457
'
458458

459-
test_expect_failure '3a-check-L: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
459+
test_expect_success '3a-check-L: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
460460
test_when_finished "git -C 3a reset --hard" &&
461461
test_when_finished "git -C 3a clean -fd" &&
462462
(
@@ -579,7 +579,7 @@ test_expect_success '3b-check-L: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
579579
)
580580
'
581581

582-
test_expect_failure '3b-check-R: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
582+
test_expect_success '3b-check-R: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
583583
test_when_finished "git -C 3b reset --hard" &&
584584
test_when_finished "git -C 3b clean -fd" &&
585585
(

0 commit comments

Comments
 (0)