@matheusbsbs / matheus-bagliano / commits / 49b8133a9e
Fork 0

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 #