merge-recursive: fix merging a subdirectory into the root directory
We allow renaming all entries in e.g. a directory named z/ into a directory named y/ to be detected as a z/ -> y/ rename, so that if the other side of history adds any files to the directory z/ in the mean time, we can provide the hint that they should be moved to y/. There is no reason to not allow 'y/' to be the root directory, but the code did not handle that case correctly. Add a testcase and the necessary special checks to support this case. Signed-off-by: Elijah Newren <newren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Elijah Newren committed
Oct 22, 2019 at 21:22 UTC
49b8133a9ece199a17db8bb2545202c6eac67485
2 files changed
+163
-3
merge-recursive.c
+49
-3
@@ -1931,6 +1931,16 @@ static char *apply_dir_rename(struct dir_rename_entry *entry,
1931
return NULL;
1932
1933
oldlen = strlen(entry->dir);
1934
+ if (entry->new_dir.len == 0)
1935
+ /*
1936
+ * If someone renamed/merged a subdirectory into the root
1937
+ * directory (e.g. 'some/subdir' -> ''), then we want to
1938
+ * avoid returning
1939
+ * '' + '/filename'
1940
+ * as the rename; we need to make old_path + oldlen advance
1941
+ * past the '/' character.
1942
+ */
1943
+ oldlen++;
1944
newlen = entry->new_dir.len + (strlen(old_path) - oldlen) + 1;
1945
strbuf_grow(&new_path, newlen);
1946
strbuf_addbuf(&new_path, &entry->new_dir);
@@ -1963,8 +1973,26 @@ static void get_renamed_dir_portion(const char *old_path, const char *new_path,
1973
*/
1974
end_of_old = strrchr(old_path, '/');
1975
end_of_new = strrchr(new_path, '/');
1966
- if (end_of_old == NULL || end_of_new == NULL)
1967
- return; /* We haven't modified *old_dir or *new_dir yet. */
1976
+
1977
+ /*
1978
+ * If end_of_old is NULL, old_path wasn't in a directory, so there
1979
+ * could not be a directory rename (our rule elsewhere that a
1980
+ * directory which still exists is not considered to have been
1981
+ * renamed means the root directory can never be renamed -- because
1982
+ * the root directory always exists).
1983
+ */
1984
+ if (end_of_old == NULL)
1985
+ return; /* Note: *old_dir and *new_dir are still NULL */
1986
+
1987
+ /*
1988
+ * If new_path contains no directory (end_of_new is NULL), then we
1989
+ * have a rename of old_path's directory to the root directory.
1990
+ */
1991
+ if (end_of_new == NULL) {
1992
+ *old_dir = xstrndup(old_path, end_of_old - old_path);
1993
+ *new_dir = xstrdup("");
1994
+ return;
1995
+ }
1996
1997
/* Find the first non-matching character traversing backwards */
1998
while (*--end_of_new == *--end_of_old &&
@@ -1978,7 +2006,25 @@ static void get_renamed_dir_portion(const char *old_path, const char *new_path,
2006
*/
2007
if (end_of_old == old_path && end_of_new == new_path &&
2008
*end_of_old == *end_of_new)
1981
- return; /* We haven't modified *old_dir or *new_dir yet. */
2009
+ return; /* Note: *old_dir and *new_dir are still NULL */
2010
+
2011
+ /*
2012
+ * If end_of_new got back to the beginning of its string, and
2013
+ * end_of_old got back to the beginning of some subdirectory, then
2014
+ * we have a rename/merge of a subdirectory into the root, which
2015
+ * needs slightly special handling.
2016
+ *
2017
+ * Note: There is no need to consider the opposite case, with a
2018
+ * rename/merge of the root directory into some subdirectory
2019
+ * because as noted above the root directory always exists so it
2020
+ * cannot be considered to be renamed.
2021
+ */
2022
+ if (end_of_new == new_path &&
2023
+ end_of_old != old_path && end_of_old[-1] == '/') {
2024
+ *old_dir = xstrndup(old_path, --end_of_old - old_path);
2025
+ *new_dir = xstrdup("");
2026
+ return;
2027
+ }
2028
2029
/*
2030
* We've found the first non-matching character in the directory
t/t6043-merge-rename-directories.sh
+114
@@ -4051,6 +4051,120 @@ test_expect_success '12c-check: Moving one directory hierarchy into another w/ c
4051
)
4052
'
4053
4054
+# Testcase 12d, Rename/merge of subdirectory into the root
4055
+# Commit O: a/b/subdir/foo
4056
+# Commit A: subdir/foo
4057
+# Commit B: a/b/subdir/foo, a/b/bar
4058
+# Expected: subdir/foo, bar
4059
+
4060
+test_expect_success '12d-setup: Rename/merge subdir into the root, variant 1' '
4061
+ test_create_repo 12d &&
4062
+ (
4063
+ cd 12d &&
4064
+
4065
+ mkdir -p a/b/subdir &&
4066
+ test_commit a/b/subdir/foo &&
4067
+
4068
+ git branch O &&
4069
+ git branch A &&
4070
+ git branch B &&
4071
+
4072
+ git checkout A &&
4073
+ mkdir subdir &&
4074
+ git mv a/b/subdir/foo.t subdir/foo.t &&
4075
+ test_tick &&
4076
+ git commit -m "A" &&
4077
+
4078
+ git checkout B &&
4079
+ test_commit a/b/bar
4080
+ )
4081
+'
4082
+
4083
+test_expect_success '12d-check: Rename/merge subdir into the root, variant 1' '
4084
+ (
4085
+ cd 12d &&
4086
+
4087
+ git checkout A^0 &&
4088
+
4089
+ git -c merge.directoryRenames=true merge -s recursive B^0 &&
4090
+
4091
+ git ls-files -s >out &&
4092
+ test_line_count = 2 out &&
4093
+
4094
+ git rev-parse >actual \
4095
+ HEAD:subdir/foo.t HEAD:bar.t &&
4096
+ git rev-parse >expect \
4097
+ O:a/b/subdir/foo.t B:a/b/bar.t &&
4098
+ test_cmp expect actual &&
4099
+
4100
+ git hash-object bar.t >actual &&
4101
+ git rev-parse B:a/b/bar.t >expect &&
4102
+ test_cmp expect actual &&
4103
+
4104
+ test_must_fail git rev-parse HEAD:a/b/subdir/foo.t &&
4105
+ test_must_fail git rev-parse HEAD:a/b/bar.t &&
4106
+ test_path_is_missing a/ &&
4107
+ test_path_is_file bar.t
4108
+ )
4109
+'
4110
+
4111
+# Testcase 12e, Rename/merge of subdirectory into the root
4112
+# Commit O: a/b/foo
4113
+# Commit A: foo
4114
+# Commit B: a/b/foo, a/b/bar
4115
+# Expected: foo, bar
4116
+
4117
+test_expect_success '12e-setup: Rename/merge subdir into the root, variant 2' '
4118
+ test_create_repo 12e &&
4119
+ (
4120
+ cd 12e &&
4121
+
4122
+ mkdir -p a/b &&
4123
+ test_commit a/b/foo &&
4124
+
4125
+ git branch O &&
4126
+ git branch A &&
4127
+ git branch B &&
4128
+
4129
+ git checkout A &&
4130
+ mkdir subdir &&
4131
+ git mv a/b/foo.t foo.t &&
4132
+ test_tick &&
4133
+ git commit -m "A" &&
4134
+
4135
+ git checkout B &&
4136
+ test_commit a/b/bar
4137
+ )
4138
+'
4139
+
4140
+test_expect_success '12e-check: Rename/merge subdir into the root, variant 2' '
4141
+ (
4142
+ cd 12e &&
4143
+
4144
+ git checkout A^0 &&
4145
+
4146
+ git -c merge.directoryRenames=true merge -s recursive B^0 &&
4147
+
4148
+ git ls-files -s >out &&
4149
+ test_line_count = 2 out &&
4150
+
4151
+ git rev-parse >actual \
4152
+ HEAD:foo.t HEAD:bar.t &&
4153
+ git rev-parse >expect \
4154
+ O:a/b/foo.t B:a/b/bar.t &&
4155
+ test_cmp expect actual &&
4156
+
4157
+ git hash-object bar.t >actual &&
4158
+ git rev-parse B:a/b/bar.t >expect &&
4159
+ test_cmp expect actual &&
4160
+
4161
+ test_must_fail git rev-parse HEAD:a/b/foo.t &&
4162
+ test_must_fail git rev-parse HEAD:a/b/bar.t &&
4163
+ test_path_is_missing a/ &&
4164
+ test_path_is_file bar.t
4165
+ )
4166
+'
4167
+
4168
###########################################################################
4169
# SECTION 13: Checking informational and conflict messages
4170
#