Skip to content

Commit 8c8e5bd

Browse files
newrengitster
authored andcommitted
merge-recursive: switch directory rename detection default
When all of x/a, x/b, and x/c have moved to z/a, z/b, and z/c on one branch, there is a question about whether x/d added on a different branch should remain at x/d or appear at z/d when the two branches are merged. There are different possible viewpoints here: A) The file was placed at x/d; it's unrelated to the other files in x/ so it doesn't matter that all the files from x/ moved to z/ on one branch; x/d should still remain at x/d. B) x/d is related to the other files in x/, and x/ was renamed to z/; therefore x/d should be moved to z/d. Since there was no ability to detect directory renames prior to git-2.18, users experienced (A) regardless of context. Choice (B) was implemented in git-2.18, with no option to go back to (A), and has been in use since. However, one user reported that the merge results did not match their expectations, making the change of default problematic, especially since there was no notice printed when directory rename detection moved files. Note that there is also a third possibility here: C) There are different answers depending on the context and content that cannot be determined by git, so this is a conflict. Use a higher stage in the index to record the conflict and notify the user of the potential issue instead of silently selecting a resolution for them. Add an option for users to specify their preference for whether to use directory rename detection, and default to (C). Even when directory rename detection is on, add notice messages about files moved into new directories. As a sidenote, x/d did not have to be a new file here; it could have already existed at some other path and been renamed to x/d, with directory rename detection just renaming it again to z/d. Thus, it's not just new files, but also a modification to all rename types (normal renames, rename/add, rename/delete, rename/rename(1to1), rename/rename(1to2), and rename/rename(2to1)). Signed-off-by: Elijah Newren <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent e62d112 commit 8c8e5bd

File tree

5 files changed

+552
-87
lines changed

5 files changed

+552
-87
lines changed

Documentation/config/merge.txt

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,22 @@ merge.renameLimit::
3939
is turned off.
4040

4141
merge.renames::
42-
Whether and how Git detects renames. If set to "false",
43-
rename detection is disabled. If set to "true", basic rename
44-
detection is enabled. Defaults to the value of diff.renames.
42+
Whether Git detects renames. If set to "false", rename detection
43+
is disabled. If set to "true", basic rename detection is enabled.
44+
Defaults to the value of diff.renames.
45+
46+
merge.directoryRenames::
47+
Whether Git detects directory renames, affecting what happens at
48+
merge time to new files added to a directory on one side of
49+
history when that directory was renamed on the other side of
50+
history. If merge.directoryRenames is set to "false", directory
51+
rename detection is disabled, meaning that such new files will be
52+
left behind in the old directory. If set to "true", directory
53+
rename detection is enabled, meaning that such new files will be
54+
moved into the new directory. If set to "conflict", a conflict
55+
will be reported for such paths. If merge.renames is false,
56+
merge.directoryRenames is ignored and treated as false. Defaults
57+
to "conflict".
4558

4659
merge.renormalize::
4760
Tell Git that canonical representation of files in the

merge-recursive.c

Lines changed: 123 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1370,30 +1370,39 @@ static int handle_rename_via_dir(struct merge_options *opt,
13701370
*/
13711371
const struct rename *ren = ci->ren1;
13721372
const struct diff_filespec *dest = ren->pair->two;
1373+
char *file_path = dest->path;
1374+
int mark_conflicted = (opt->detect_directory_renames == 1);
1375+
assert(ren->dir_rename_original_dest);
13731376

13741377
if (!opt->call_depth && would_lose_untracked(opt, dest->path)) {
1375-
char *alt_path = unique_path(opt, dest->path, ren->branch);
1376-
1378+
mark_conflicted = 1;
1379+
file_path = unique_path(opt, dest->path, ren->branch);
13771380
output(opt, 1, _("Error: Refusing to lose untracked file at %s; "
1378-
"writing to %s instead."),
1379-
dest->path, alt_path);
1381+
"writing to %s instead."),
1382+
dest->path, file_path);
1383+
}
1384+
1385+
if (mark_conflicted) {
13801386
/*
1381-
* Write the file in worktree at alt_path, but not in the
1382-
* index. Instead, write to dest->path for the index but
1383-
* only at the higher appropriate stage.
1387+
* Write the file in worktree at file_path. In the index,
1388+
* only record the file at dest->path in the appropriate
1389+
* higher stage.
13841390
*/
1385-
if (update_file(opt, 0, dest, alt_path))
1391+
if (update_file(opt, 0, dest, file_path))
13861392
return -1;
1387-
free(alt_path);
1388-
return update_stages(opt, dest->path, NULL,
1389-
ren->branch == opt->branch1 ? dest : NULL,
1390-
ren->branch == opt->branch1 ? NULL : dest);
1393+
if (file_path != dest->path)
1394+
free(file_path);
1395+
if (update_stages(opt, dest->path, NULL,
1396+
ren->branch == opt->branch1 ? dest : NULL,
1397+
ren->branch == opt->branch1 ? NULL : dest))
1398+
return -1;
1399+
return 0; /* not clean, but conflicted */
1400+
} else {
1401+
/* Update dest->path both in index and in worktree */
1402+
if (update_file(opt, 1, dest, dest->path))
1403+
return -1;
1404+
return 1; /* clean */
13911405
}
1392-
1393-
/* Update dest->path both in index and in worktree */
1394-
if (update_file(opt, 1, dest, dest->path))
1395-
return -1;
1396-
return 0;
13971406
}
13981407

13991408
static int handle_change_delete(struct merge_options *opt,
@@ -3090,10 +3099,88 @@ static int handle_rename_normal(struct merge_options *opt,
30903099
const struct diff_filespec *b,
30913100
struct rename_conflict_info *ci)
30923101
{
3093-
/* Merge the content and write it out */
3102+
struct rename *ren = ci->ren1;
30943103
struct merge_file_info mfi;
3095-
return handle_content_merge(&mfi, opt, path, was_dirty(opt, path),
3096-
o, a, b, ci);
3104+
int clean;
3105+
int side = (ren->branch == opt->branch1 ? 2 : 3);
3106+
3107+
/* Merge the content and write it out */
3108+
clean = handle_content_merge(&mfi, opt, path, was_dirty(opt, path),
3109+
o, a, b, ci);
3110+
3111+
if (clean && opt->detect_directory_renames == 1 &&
3112+
ren->dir_rename_original_dest) {
3113+
if (update_stages(opt, path,
3114+
NULL,
3115+
side == 2 ? &mfi.blob : NULL,
3116+
side == 2 ? NULL : &mfi.blob))
3117+
return -1;
3118+
clean = 0; /* not clean, but conflicted */
3119+
}
3120+
return clean;
3121+
}
3122+
3123+
static void dir_rename_warning(const char *msg,
3124+
int is_add,
3125+
int clean,
3126+
struct merge_options *opt,
3127+
struct rename *ren)
3128+
{
3129+
const char *other_branch;
3130+
other_branch = (ren->branch == opt->branch1 ?
3131+
opt->branch2 : opt->branch1);
3132+
if (is_add) {
3133+
output(opt, clean ? 2 : 1, msg,
3134+
ren->pair->one->path, ren->branch,
3135+
other_branch, ren->pair->two->path);
3136+
return;
3137+
}
3138+
output(opt, clean ? 2 : 1, msg,
3139+
ren->pair->one->path, ren->dir_rename_original_dest, ren->branch,
3140+
other_branch, ren->pair->two->path);
3141+
}
3142+
static int warn_about_dir_renamed_entries(struct merge_options *opt,
3143+
struct rename *ren)
3144+
{
3145+
const char *msg;
3146+
int clean = 1, is_add;
3147+
3148+
if (!ren)
3149+
return clean;
3150+
3151+
/* Return early if ren was not affected/created by a directory rename */
3152+
if (!ren->dir_rename_original_dest)
3153+
return clean;
3154+
3155+
/* Sanity checks */
3156+
assert(opt->detect_directory_renames > 0);
3157+
assert(ren->dir_rename_original_type == 'A' ||
3158+
ren->dir_rename_original_type == 'R');
3159+
3160+
/* Check whether to treat directory renames as a conflict */
3161+
clean = (opt->detect_directory_renames == 2);
3162+
3163+
is_add = (ren->dir_rename_original_type == 'A');
3164+
if (ren->dir_rename_original_type == 'A' && clean) {
3165+
msg = _("Path updated: %s added in %s inside a "
3166+
"directory that was renamed in %s; moving it to %s.");
3167+
} else if (ren->dir_rename_original_type == 'A' && !clean) {
3168+
msg = _("CONFLICT (file location): %s added in %s "
3169+
"inside a directory that was renamed in %s, "
3170+
"suggesting it should perhaps be moved to %s.");
3171+
} else if (ren->dir_rename_original_type == 'R' && clean) {
3172+
msg = _("Path updated: %s renamed to %s in %s, inside a "
3173+
"directory that was renamed in %s; moving it to %s.");
3174+
} else if (ren->dir_rename_original_type == 'R' && !clean) {
3175+
msg = _("CONFLICT (file location): %s renamed to %s in %s, "
3176+
"inside a directory that was renamed in %s, "
3177+
"suggesting it should perhaps be moved to %s.");
3178+
} else {
3179+
BUG("Impossible dir_rename_original_type/clean combination");
3180+
}
3181+
dir_rename_warning(msg, is_add, clean, opt, ren);
3182+
3183+
return clean;
30973184
}
30983185

30993186
/* Per entry merge function */
@@ -3115,6 +3202,10 @@ static int process_entry(struct merge_options *opt,
31153202
if (entry->rename_conflict_info) {
31163203
struct rename_conflict_info *ci = entry->rename_conflict_info;
31173204
struct diff_filespec *temp;
3205+
int path_clean;
3206+
3207+
path_clean = warn_about_dir_renamed_entries(opt, ci->ren1);
3208+
path_clean &= warn_about_dir_renamed_entries(opt, ci->ren2);
31183209

31193210
/*
31203211
* For cases with a single rename, {o,a,b}->path have all been
@@ -3135,9 +3226,7 @@ static int process_entry(struct merge_options *opt,
31353226
ci);
31363227
break;
31373228
case RENAME_VIA_DIR:
3138-
clean_merge = 1;
3139-
if (handle_rename_via_dir(opt, ci))
3140-
clean_merge = -1;
3229+
clean_merge = handle_rename_via_dir(opt, ci);
31413230
break;
31423231
case RENAME_ADD:
31433232
/*
@@ -3187,6 +3276,8 @@ static int process_entry(struct merge_options *opt,
31873276
entry->processed = 0;
31883277
break;
31893278
}
3279+
if (path_clean < clean_merge)
3280+
clean_merge = path_clean;
31903281
} else if (o_valid && (!a_valid || !b_valid)) {
31913282
/* Case A: Deleted in one */
31923283
if ((!a_valid && !b_valid) ||
@@ -3558,6 +3649,15 @@ static void merge_recursive_config(struct merge_options *opt)
35583649
opt->merge_detect_rename = git_config_rename("merge.renames", value);
35593650
free(value);
35603651
}
3652+
if (!git_config_get_string("merge.directoryrenames", &value)) {
3653+
int boolval = git_parse_maybe_bool(value);
3654+
if (0 <= boolval) {
3655+
opt->detect_directory_renames = boolval ? 2 : 0;
3656+
} else if (!strcasecmp(value, "conflict")) {
3657+
opt->detect_directory_renames = 1;
3658+
} /* avoid erroring on values from future versions of git */
3659+
free(value);
3660+
}
35613661
git_config(git_xmerge_config, NULL);
35623662
}
35633663

t/t3401-rebase-and-am-rename.sh

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ test_expect_success 'rebase --interactive: directory rename detected' '
4242
git checkout B^0 &&
4343
4444
set_fake_editor &&
45-
FAKE_LINES="1" git rebase --interactive A &&
45+
FAKE_LINES="1" git -c merge.directoryRenames=true rebase --interactive A &&
4646
4747
git ls-files -s >out &&
4848
test_line_count = 5 out &&
@@ -58,7 +58,7 @@ test_expect_failure 'rebase (am): directory rename detected' '
5858
5959
git checkout B^0 &&
6060
61-
git rebase A &&
61+
git -c merge.directoryRenames=true rebase A &&
6262
6363
git ls-files -s >out &&
6464
test_line_count = 5 out &&
@@ -74,7 +74,7 @@ test_expect_success 'rebase --merge: directory rename detected' '
7474
7575
git checkout B^0 &&
7676
77-
git rebase --merge A &&
77+
git -c merge.directoryRenames=true rebase --merge A &&
7878
7979
git ls-files -s >out &&
8080
test_line_count = 5 out &&
@@ -92,7 +92,7 @@ test_expect_failure 'am: directory rename detected' '
9292
9393
git format-patch -1 B &&
9494
95-
git am --3way 0001*.patch &&
95+
git -c merge.directoryRenames=true am --3way 0001*.patch &&
9696
9797
git ls-files -s >out &&
9898
test_line_count = 5 out &&

0 commit comments

Comments
 (0)