directory rename detection: testcases exploring possibly suboptimal merges

Reviewed-by: Stefan Beller <sbeller@google.com> Signed-off-by: Elijah Newren <newren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Elijah Newren committed Apr 19, 2018 at 10:57 UTC 362ab315acce6a4bf9849ffb6de76b3ebe381371
1 file changed +404
t/t6043-merge-rename-directories.sh
+404
@@ -1912,4 +1912,408 @@ test_expect_failure '7e-check: transitive rename in rename/delete AND dirs in th
1912 )
1913 '
1914
1915 +###########################################################################
1916 +# SECTION 8: Suboptimal merges
1917 +#
1918 +# As alluded to in the last section, the ruleset we have built up for
1919 +# detecting directory renames unfortunately has some special cases where it
1920 +# results in slightly suboptimal or non-intuitive behavior. This section
1921 +# explores these cases.
1922 +#
1923 +# To be fair, we already had non-intuitive or suboptimal behavior for most
1924 +# of these cases in git before introducing implicit directory rename
1925 +# detection, but it'd be nice if there was a modified ruleset out there
1926 +# that handled these cases a bit better.
1927 +###########################################################################
1928 +
1929 +# Testcase 8a, Dual-directory rename, one into the others' way
1930 +# Commit O. x/{a,b}, y/{c,d}
1931 +# Commit A. x/{a,b,e}, y/{c,d,f}
1932 +# Commit B. y/{a,b}, z/{c,d}
1933 +#
1934 +# Possible Resolutions:
1935 +# w/o dir-rename detection: y/{a,b,f}, z/{c,d}, x/e
1936 +# Currently expected: y/{a,b,e,f}, z/{c,d}
1937 +# Optimal: y/{a,b,e}, z/{c,d,f}
1938 +#
1939 +# Note: Both x and y got renamed and it'd be nice to detect both, and we do
1940 +# better with directory rename detection than git did without, but the
1941 +# simple rule from section 5 prevents me from handling this as optimally as
1942 +# we potentially could.
1943 +
1944 +test_expect_success '8a-setup: Dual-directory rename, one into the others way' '
1945 + test_create_repo 8a &&
1946 + (
1947 + cd 8a &&
1948 +
1949 + mkdir x &&
1950 + mkdir y &&
1951 + echo a >x/a &&
1952 + echo b >x/b &&
1953 + echo c >y/c &&
1954 + echo d >y/d &&
1955 + git add x y &&
1956 + test_tick &&
1957 + git commit -m "O" &&
1958 +
1959 + git branch O &&
1960 + git branch A &&
1961 + git branch B &&
1962 +
1963 + git checkout A &&
1964 + echo e >x/e &&
1965 + echo f >y/f &&
1966 + git add x/e y/f &&
1967 + test_tick &&
1968 + git commit -m "A" &&
1969 +
1970 + git checkout B &&
1971 + git mv y z &&
1972 + git mv x y &&
1973 + test_tick &&
1974 + git commit -m "B"
1975 + )
1976 +'
1977 +
1978 +test_expect_failure '8a-check: Dual-directory rename, one into the others way' '
1979 + (
1980 + cd 8a &&
1981 +
1982 + git checkout A^0 &&
1983 +
1984 + git merge -s recursive B^0 &&
1985 +
1986 + git ls-files -s >out &&
1987 + test_line_count = 6 out &&
1988 + git ls-files -u >out &&
1989 + test_line_count = 0 out &&
1990 + git ls-files -o >out &&
1991 + test_line_count = 1 out &&
1992 +
1993 + git rev-parse >actual \
1994 + HEAD:y/a HEAD:y/b HEAD:y/e HEAD:y/f HEAD:z/c HEAD:z/d &&
1995 + git rev-parse >expect \
1996 + O:x/a O:x/b A:x/e A:y/f O:y/c O:y/d &&
1997 + test_cmp expect actual
1998 + )
1999 +'
2000 +
2001 +# Testcase 8b, Dual-directory rename, one into the others' way, with conflicting filenames
2002 +# Commit O. x/{a_1,b_1}, y/{a_2,b_2}
2003 +# Commit A. x/{a_1,b_1,e_1}, y/{a_2,b_2,e_2}
2004 +# Commit B. y/{a_1,b_1}, z/{a_2,b_2}
2005 +#
2006 +# w/o dir-rename detection: y/{a_1,b_1,e_2}, z/{a_2,b_2}, x/e_1
2007 +# Currently expected: <same>
2008 +# Scary: y/{a_1,b_1}, z/{a_2,b_2}, CONFLICT(add/add, e_1 vs. e_2)
2009 +# Optimal: y/{a_1,b_1,e_1}, z/{a_2,b_2,e_2}
2010 +#
2011 +# Note: Very similar to 8a, except instead of 'e' and 'f' in directories x and
2012 +# y, both are named 'e'. Without directory rename detection, neither file
2013 +# moves directories. Implement directory rename detection suboptimally, and
2014 +# you get an add/add conflict, but both files were added in commit A, so this
2015 +# is an add/add conflict where one side of history added both files --
2016 +# something we can't represent in the index. Obviously, we'd prefer the last
2017 +# resolution, but our previous rules are too coarse to allow it. Using both
2018 +# the rules from section 4 and section 5 save us from the Scary resolution,
2019 +# making us fall back to pre-directory-rename-detection behavior for both
2020 +# e_1 and e_2.
2021 +
2022 +test_expect_success '8b-setup: Dual-directory rename, one into the others way, with conflicting filenames' '
2023 + test_create_repo 8b &&
2024 + (
2025 + cd 8b &&
2026 +
2027 + mkdir x &&
2028 + mkdir y &&
2029 + echo a1 >x/a &&
2030 + echo b1 >x/b &&
2031 + echo a2 >y/a &&
2032 + echo b2 >y/b &&
2033 + git add x y &&
2034 + test_tick &&
2035 + git commit -m "O" &&
2036 +
2037 + git branch O &&
2038 + git branch A &&
2039 + git branch B &&
2040 +
2041 + git checkout A &&
2042 + echo e1 >x/e &&
2043 + echo e2 >y/e &&
2044 + git add x/e y/e &&
2045 + test_tick &&
2046 + git commit -m "A" &&
2047 +
2048 + git checkout B &&
2049 + git mv y z &&
2050 + git mv x y &&
2051 + test_tick &&
2052 + git commit -m "B"
2053 + )
2054 +'
2055 +
2056 +test_expect_success '8b-check: Dual-directory rename, one into the others way, with conflicting filenames' '
2057 + (
2058 + cd 8b &&
2059 +
2060 + git checkout A^0 &&
2061 +
2062 + git merge -s recursive B^0 &&
2063 +
2064 + git ls-files -s >out &&
2065 + test_line_count = 6 out &&
2066 + git ls-files -u >out &&
2067 + test_line_count = 0 out &&
2068 + git ls-files -o >out &&
2069 + test_line_count = 1 out &&
2070 +
2071 + git rev-parse >actual \
2072 + HEAD:y/a HEAD:y/b HEAD:z/a HEAD:z/b HEAD:x/e HEAD:y/e &&
2073 + git rev-parse >expect \
2074 + O:x/a O:x/b O:y/a O:y/b A:x/e A:y/e &&
2075 + test_cmp expect actual
2076 + )
2077 +'
2078 +
2079 +# Testcase 8c, rename+modify/delete
2080 +# (Related to testcases 5b and 8d)
2081 +# Commit O: z/{b,c,d}
2082 +# Commit A: y/{b,c}
2083 +# Commit B: z/{b,c,d_modified,e}
2084 +# Expected: y/{b,c,e}, CONFLICT(rename+modify/delete: x/d -> y/d or deleted)
2085 +#
2086 +# Note: This testcase doesn't present any concerns for me...until you
2087 +# compare it with testcases 5b and 8d. See notes in 8d for more
2088 +# details.
2089 +
2090 +test_expect_success '8c-setup: rename+modify/delete' '
2091 + test_create_repo 8c &&
2092 + (
2093 + cd 8c &&
2094 +
2095 + mkdir z &&
2096 + echo b >z/b &&
2097 + echo c >z/c &&
2098 + test_seq 1 10 >z/d &&
2099 + git add z &&
2100 + test_tick &&
2101 + git commit -m "O" &&
2102 +
2103 + git branch O &&
2104 + git branch A &&
2105 + git branch B &&
2106 +
2107 + git checkout A &&
2108 + git rm z/d &&
2109 + git mv z y &&
2110 + test_tick &&
2111 + git commit -m "A" &&
2112 +
2113 + git checkout B &&
2114 + echo 11 >z/d &&
2115 + test_chmod +x z/d &&
2116 + echo e >z/e &&
2117 + git add z/d z/e &&
2118 + test_tick &&
2119 + git commit -m "B"
2120 + )
2121 +'
2122 +
2123 +test_expect_failure '8c-check: rename+modify/delete' '
2124 + (
2125 + cd 8c &&
2126 +
2127 + git checkout A^0 &&
2128 +
2129 + test_must_fail git merge -s recursive B^0 >out &&
2130 + test_i18ngrep "CONFLICT (rename/delete).* z/d.*y/d" out &&
2131 +
2132 + git ls-files -s >out &&
2133 + test_line_count = 4 out &&
2134 + git ls-files -u >out &&
2135 + test_line_count = 1 out &&
2136 + git ls-files -o >out &&
2137 + test_line_count = 1 out &&
2138 +
2139 + git rev-parse >actual \
2140 + :0:y/b :0:y/c :0:y/e :3:y/d &&
2141 + git rev-parse >expect \
2142 + O:z/b O:z/c B:z/e B:z/d &&
2143 + test_cmp expect actual &&
2144 +
2145 + test_must_fail git rev-parse :1:y/d &&
2146 + test_must_fail git rev-parse :2:y/d &&
2147 + git ls-files -s y/d | grep ^100755 &&
2148 + test_path_is_file y/d
2149 + )
2150 +'
2151 +
2152 +# Testcase 8d, rename/delete...or not?
2153 +# (Related to testcase 5b; these may appear slightly inconsistent to users;
2154 +# Also related to testcases 7d and 7e)
2155 +# Commit O: z/{b,c,d}
2156 +# Commit A: y/{b,c}
2157 +# Commit B: z/{b,c,d,e}
2158 +# Expected: y/{b,c,e}
2159 +#
2160 +# Note: It would also be somewhat reasonable to resolve this as
2161 +# y/{b,c,e}, CONFLICT(rename/delete: x/d -> y/d or deleted)
2162 +# The logic being that the only difference between this testcase and 8c
2163 +# is that there is no modification to d. That suggests that instead of a
2164 +# rename/modify vs. delete conflict, we should just have a rename/delete
2165 +# conflict, otherwise we are being inconsistent.
2166 +#
2167 +# However...as far as consistency goes, we didn't report a conflict for
2168 +# path d_1 in testcase 5b due to a different file being in the way. So,
2169 +# we seem to be forced to have cases where users can change things
2170 +# slightly and get what they may perceive as inconsistent results. It
2171 +# would be nice to avoid that, but I'm not sure I see how.
2172 +#
2173 +# In this case, I'm leaning towards: commit A was the one that deleted z/d
2174 +# and it did the rename of z to y, so the two "conflicts" (rename vs.
2175 +# delete) are both coming from commit A, which is illogical. Conflicts
2176 +# during merging are supposed to be about opposite sides doing things
2177 +# differently.
2178 +
2179 +test_expect_success '8d-setup: rename/delete...or not?' '
2180 + test_create_repo 8d &&
2181 + (
2182 + cd 8d &&
2183 +
2184 + mkdir z &&
2185 + echo b >z/b &&
2186 + echo c >z/c &&
2187 + test_seq 1 10 >z/d &&
2188 + git add z &&
2189 + test_tick &&
2190 + git commit -m "O" &&
2191 +
2192 + git branch O &&
2193 + git branch A &&
2194 + git branch B &&
2195 +
2196 + git checkout A &&
2197 + git rm z/d &&
2198 + git mv z y &&
2199 + test_tick &&
2200 + git commit -m "A" &&
2201 +
2202 + git checkout B &&
2203 + echo e >z/e &&
2204 + git add z/e &&
2205 + test_tick &&
2206 + git commit -m "B"
2207 + )
2208 +'
2209 +
2210 +test_expect_failure '8d-check: rename/delete...or not?' '
2211 + (
2212 + cd 8d &&
2213 +
2214 + git checkout A^0 &&
2215 +
2216 + git merge -s recursive B^0 &&
2217 +
2218 + git ls-files -s >out &&
2219 + test_line_count = 3 out &&
2220 +
2221 + git rev-parse >actual \
2222 + HEAD:y/b HEAD:y/c HEAD:y/e &&
2223 + git rev-parse >expect \
2224 + O:z/b O:z/c B:z/e &&
2225 + test_cmp expect actual
2226 + )
2227 +'
2228 +
2229 +# Testcase 8e, Both sides rename, one side adds to original directory
2230 +# Commit O: z/{b,c}
2231 +# Commit A: y/{b,c}
2232 +# Commit B: w/{b,c}, z/d
2233 +#
2234 +# Possible Resolutions:
2235 +# w/o dir-rename detection: z/d, CONFLICT(z/b -> y/b vs. w/b),
2236 +# CONFLICT(z/c -> y/c vs. w/c)
2237 +# Currently expected: y/d, CONFLICT(z/b -> y/b vs. w/b),
2238 +# CONFLICT(z/c -> y/c vs. w/c)
2239 +# Optimal: ??
2240 +#
2241 +# Notes: In commit A, directory z got renamed to y. In commit B, directory z
2242 +# did NOT get renamed; the directory is still present; instead it is
2243 +# considered to have just renamed a subset of paths in directory z
2244 +# elsewhere. Therefore, the directory rename done in commit A to z/
2245 +# applies to z/d and maps it to y/d.
2246 +#
2247 +# It's possible that users would get confused about this, but what
2248 +# should we do instead? Silently leaving at z/d seems just as bad or
2249 +# maybe even worse. Perhaps we could print a big warning about z/d
2250 +# and how we're moving to y/d in this case, but when I started thinking
2251 +# about the ramifications of doing that, I didn't know how to rule out
2252 +# that opening other weird edge and corner cases so I just punted.
2253 +
2254 +test_expect_success '8e-setup: Both sides rename, one side adds to original directory' '
2255 + test_create_repo 8e &&
2256 + (
2257 + cd 8e &&
2258 +
2259 + mkdir z &&
2260 + echo b >z/b &&
2261 + echo c >z/c &&
2262 + git add z &&
2263 + test_tick &&
2264 + git commit -m "O" &&
2265 +
2266 + git branch O &&
2267 + git branch A &&
2268 + git branch B &&
2269 +
2270 + git checkout A &&
2271 + git mv z y &&
2272 + test_tick &&
2273 + git commit -m "A" &&
2274 +
2275 + git checkout B &&
2276 + git mv z w &&
2277 + mkdir z &&
2278 + echo d >z/d &&
2279 + git add z/d &&
2280 + test_tick &&
2281 + git commit -m "B"
2282 + )
2283 +'
2284 +
2285 +test_expect_failure '8e-check: Both sides rename, one side adds to original directory' '
2286 + (
2287 + cd 8e &&
2288 +
2289 + git checkout A^0 &&
2290 +
2291 + test_must_fail git merge -s recursive B^0 >out 2>err &&
2292 + test_i18ngrep CONFLICT.*rename/rename.*z/c.*y/c.*w/c out &&
2293 + test_i18ngrep CONFLICT.*rename/rename.*z/b.*y/b.*w/b out &&
2294 +
2295 + git ls-files -s >out &&
2296 + test_line_count = 7 out &&
2297 + git ls-files -u >out &&
2298 + test_line_count = 6 out &&
2299 + git ls-files -o >out &&
2300 + test_line_count = 2 out &&
2301 +
2302 + git rev-parse >actual \
2303 + :1:z/b :2:y/b :3:w/b :1:z/c :2:y/c :3:w/c :0:y/d &&
2304 + git rev-parse >expect \
2305 + O:z/b O:z/b O:z/b O:z/c O:z/c O:z/c B:z/d &&
2306 + test_cmp expect actual &&
2307 +
2308 + git hash-object >actual \
2309 + y/b w/b y/c w/c &&
2310 + git rev-parse >expect \
2311 + O:z/b O:z/b O:z/c O:z/c &&
2312 + test_cmp expect actual &&
2313 +
2314 + test_path_is_missing z/b &&
2315 + test_path_is_missing z/c
2316 + )
2317 +'
2318 +
2319 test_done