fast-import: allow unquoted empty path for root

Ever since filerename was added in f39a946a1f (Support wholesale directory renames in fast-import, 2007-07-09) and filecopy in b6f3481bb4 (Teach fast-import to recursively copy files/directories, 2007-07-15), both have produced an error when the destination path is empty. Later, when support for targeting the root directory with an empty string was added in 2794ad5244 (fast-import: Allow filemodify to set the root, 2010-10-10), this had the effect of allowing the quoted empty string (`""`), but forbidding its unquoted variant (``). This seems to have been intended as simple data validation for parsing two paths, rather than a syntax restriction, because it was not extended to the other operations. All other occurrences of paths (in filemodify, filedelete, the source of filecopy and filerename, and ls) allow both. For most of this feature's lifetime, the documentation has not prescribed the use of quoted empty strings. In e5959106d6 (Documentation/fast-import: put explanation of M 040000 <dataref> "" in context, 2011-01-15), its documentation was changed from “`<path>` may also be an empty string (`""`) to specify the root of the tree” to “The root of the tree can be represented by an empty string as `<path>`”. Thus, we should assume that some front-ends have depended on this behavior. Remove this restriction for the destination paths of filecopy and filerename and change tests targeting the root to test `""` and ``. Signed-off-by: Thalia Archibald <thalia@archibald.dev> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Thalia Archibald committed Apr 14, 2024 at 01:11 UTC b5062f752ef039b6fa8b3a7491072c2f1dfe3cf2
2 files changed +190 -176
builtin/fast-import.c
-3
@@ -2423,9 +2423,6 @@ static void file_change_cr(const char *p, struct branch *b, int rename)
2423
2424 strbuf_reset(&source);
2425 parse_path_space(&source, p, &p, "source");
2426 -
2427 - if (!*p)
2428 - die("Missing dest: %s", command_buf.buf);
2426 strbuf_reset(&dest);
2427 parse_path_eol(&dest, p, "dest");
2428
t/t9300-fast-import.sh
+190 -173
@@ -1059,30 +1059,33 @@ test_expect_success 'M: rename subdirectory to new subdirectory' '
1059 compare_diff_raw expect actual
1060 '
1061
1062 -test_expect_success 'M: rename root to subdirectory' '
1063 - cat >input <<-INPUT_END &&
1064 - commit refs/heads/M4
1065 - committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
1066 - data <<COMMIT
1067 - rename root
1068 - COMMIT
1062 +for root in '""' ''
1063 +do
1064 + test_expect_success "M: rename root ($root) to subdirectory" '
1065 + cat >input <<-INPUT_END &&
1066 + commit refs/heads/M4
1067 + committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
1068 + data <<COMMIT
1069 + rename root
1070 + COMMIT
1071
1070 - from refs/heads/M2^0
1071 - R "" sub
1072 + from refs/heads/M2^0
1073 + R $root sub
1074
1073 - INPUT_END
1075 + INPUT_END
1076
1075 - cat >expect <<-EOF &&
1076 - :100644 100644 $oldf $oldf R100 file2/oldf sub/file2/oldf
1077 - :100755 100755 $f4id $f4id R100 file4 sub/file4
1078 - :100755 100755 $newf $newf R100 i/am/new/to/you sub/i/am/new/to/you
1079 - :100755 100755 $f6id $f6id R100 newdir/exec.sh sub/newdir/exec.sh
1080 - :100644 100644 $f5id $f5id R100 newdir/interesting sub/newdir/interesting
1081 - EOF
1082 - git fast-import <input &&
1083 - git diff-tree -M -r M4^ M4 >actual &&
1084 - compare_diff_raw expect actual
1085 -'
1077 + cat >expect <<-EOF &&
1078 + :100644 100644 $oldf $oldf R100 file2/oldf sub/file2/oldf
1079 + :100755 100755 $f4id $f4id R100 file4 sub/file4
1080 + :100755 100755 $newf $newf R100 i/am/new/to/you sub/i/am/new/to/you
1081 + :100755 100755 $f6id $f6id R100 newdir/exec.sh sub/newdir/exec.sh
1082 + :100644 100644 $f5id $f5id R100 newdir/interesting sub/newdir/interesting
1083 + EOF
1084 + git fast-import <input &&
1085 + git diff-tree -M -r M4^ M4 >actual &&
1086 + compare_diff_raw expect actual
1087 + '
1088 +done
1089
1090 ###
1091 ### series N
@@ -1259,49 +1262,52 @@ test_expect_success PIPE 'N: empty directory reads as missing' '
1262 test_cmp expect actual
1263 '
1264
1262 -test_expect_success 'N: copy root directory by tree hash' '
1263 - cat >expect <<-EOF &&
1264 - :100755 000000 $newf $zero D file3/newf
1265 - :100644 000000 $oldf $zero D file3/oldf
1266 - EOF
1267 - root=$(git rev-parse refs/heads/branch^0^{tree}) &&
1268 - cat >input <<-INPUT_END &&
1269 - commit refs/heads/N6
1270 - committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
1271 - data <<COMMIT
1272 - copy root directory by tree hash
1273 - COMMIT
1265 +for root in '""' ''
1266 +do
1267 + test_expect_success "N: copy root ($root) by tree hash" '
1268 + cat >expect <<-EOF &&
1269 + :100755 000000 $newf $zero D file3/newf
1270 + :100644 000000 $oldf $zero D file3/oldf
1271 + EOF
1272 + root_tree=$(git rev-parse refs/heads/branch^0^{tree}) &&
1273 + cat >input <<-INPUT_END &&
1274 + commit refs/heads/N6
1275 + committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
1276 + data <<COMMIT
1277 + copy root directory by tree hash
1278 + COMMIT
1279
1275 - from refs/heads/branch^0
1276 - M 040000 $root ""
1277 - INPUT_END
1278 - git fast-import <input &&
1279 - git diff-tree -C --find-copies-harder -r N4 N6 >actual &&
1280 - compare_diff_raw expect actual
1281 -'
1280 + from refs/heads/branch^0
1281 + M 040000 $root_tree $root
1282 + INPUT_END
1283 + git fast-import <input &&
1284 + git diff-tree -C --find-copies-harder -r N4 N6 >actual &&
1285 + compare_diff_raw expect actual
1286 + '
1287
1283 -test_expect_success 'N: copy root by path' '
1284 - cat >expect <<-EOF &&
1285 - :100755 100755 $newf $newf C100 file2/newf oldroot/file2/newf
1286 - :100644 100644 $oldf $oldf C100 file2/oldf oldroot/file2/oldf
1287 - :100755 100755 $f4id $f4id C100 file4 oldroot/file4
1288 - :100755 100755 $f6id $f6id C100 newdir/exec.sh oldroot/newdir/exec.sh
1289 - :100644 100644 $f5id $f5id C100 newdir/interesting oldroot/newdir/interesting
1290 - EOF
1291 - cat >input <<-INPUT_END &&
1292 - commit refs/heads/N-copy-root-path
1293 - committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
1294 - data <<COMMIT
1295 - copy root directory by (empty) path
1296 - COMMIT
1288 + test_expect_success "N: copy root ($root) by path" '
1289 + cat >expect <<-EOF &&
1290 + :100755 100755 $newf $newf C100 file2/newf oldroot/file2/newf
1291 + :100644 100644 $oldf $oldf C100 file2/oldf oldroot/file2/oldf
1292 + :100755 100755 $f4id $f4id C100 file4 oldroot/file4
1293 + :100755 100755 $f6id $f6id C100 newdir/exec.sh oldroot/newdir/exec.sh
1294 + :100644 100644 $f5id $f5id C100 newdir/interesting oldroot/newdir/interesting
1295 + EOF
1296 + cat >input <<-INPUT_END &&
1297 + commit refs/heads/N-copy-root-path
1298 + committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
1299 + data <<COMMIT
1300 + copy root directory by (empty) path
1301 + COMMIT
1302
1298 - from refs/heads/branch^0
1299 - C "" oldroot
1300 - INPUT_END
1301 - git fast-import <input &&
1302 - git diff-tree -C --find-copies-harder -r branch N-copy-root-path >actual &&
1303 - compare_diff_raw expect actual
1304 -'
1303 + from refs/heads/branch^0
1304 + C $root oldroot
1305 + INPUT_END
1306 + git fast-import <input &&
1307 + git diff-tree -C --find-copies-harder -r branch N-copy-root-path >actual &&
1308 + compare_diff_raw expect actual
1309 + '
1310 +done
1311
1312 test_expect_success 'N: delete directory by copying' '
1313 cat >expect <<-\EOF &&
@@ -1431,98 +1437,102 @@ test_expect_success 'N: reject foo/ syntax in ls argument' '
1437 INPUT_END
1438 '
1439
1434 -test_expect_success 'N: copy to root by id and modify' '
1435 - echo "hello, world" >expect.foo &&
1436 - echo hello >expect.bar &&
1437 - git fast-import <<-SETUP_END &&
1438 - commit refs/heads/N7
1439 - committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
1440 - data <<COMMIT
1441 - hello, tree
1442 - COMMIT
1440 +for root in '""' ''
1441 +do
1442 + test_expect_success "N: copy to root ($root) by id and modify" '
1443 + echo "hello, world" >expect.foo &&
1444 + echo hello >expect.bar &&
1445 + git fast-import <<-SETUP_END &&
1446 + commit refs/heads/N7
1447 + committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
1448 + data <<COMMIT
1449 + hello, tree
1450 + COMMIT
1451
1444 - deleteall
1445 - M 644 inline foo/bar
1446 - data <<EOF
1447 - hello
1448 - EOF
1449 - SETUP_END
1452 + deleteall
1453 + M 644 inline foo/bar
1454 + data <<EOF
1455 + hello
1456 + EOF
1457 + SETUP_END
1458
1451 - tree=$(git rev-parse --verify N7:) &&
1452 - git fast-import <<-INPUT_END &&
1453 - commit refs/heads/N8
1454 - committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
1455 - data <<COMMIT
1456 - copy to root by id and modify
1457 - COMMIT
1459 + tree=$(git rev-parse --verify N7:) &&
1460 + git fast-import <<-INPUT_END &&
1461 + commit refs/heads/N8
1462 + committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
1463 + data <<COMMIT
1464 + copy to root by id and modify
1465 + COMMIT
1466
1459 - M 040000 $tree ""
1460 - M 644 inline foo/foo
1461 - data <<EOF
1462 - hello, world
1463 - EOF
1464 - INPUT_END
1465 - git show N8:foo/foo >actual.foo &&
1466 - git show N8:foo/bar >actual.bar &&
1467 - test_cmp expect.foo actual.foo &&
1468 - test_cmp expect.bar actual.bar
1469 -'
1467 + M 040000 $tree $root
1468 + M 644 inline foo/foo
1469 + data <<EOF
1470 + hello, world
1471 + EOF
1472 + INPUT_END
1473 + git show N8:foo/foo >actual.foo &&
1474 + git show N8:foo/bar >actual.bar &&
1475 + test_cmp expect.foo actual.foo &&
1476 + test_cmp expect.bar actual.bar
1477 + '
1478
1471 -test_expect_success 'N: extract subtree' '
1472 - branch=$(git rev-parse --verify refs/heads/branch^{tree}) &&
1473 - cat >input <<-INPUT_END &&
1474 - commit refs/heads/N9
1475 - committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
1476 - data <<COMMIT
1477 - extract subtree branch:newdir
1478 - COMMIT
1479 + test_expect_success "N: extract subtree to the root ($root)" '
1480 + branch=$(git rev-parse --verify refs/heads/branch^{tree}) &&
1481 + cat >input <<-INPUT_END &&
1482 + commit refs/heads/N9
1483 + committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
1484 + data <<COMMIT
1485 + extract subtree branch:newdir
1486 + COMMIT
1487
1480 - M 040000 $branch ""
1481 - C "newdir" ""
1482 - INPUT_END
1483 - git fast-import <input &&
1484 - git diff --exit-code branch:newdir N9
1485 -'
1488 + M 040000 $branch $root
1489 + C "newdir" $root
1490 + INPUT_END
1491 + git fast-import <input &&
1492 + git diff --exit-code branch:newdir N9
1493 + '
1494
1487 -test_expect_success 'N: modify subtree, extract it, and modify again' '
1488 - echo hello >expect.baz &&
1489 - echo hello, world >expect.qux &&
1490 - git fast-import <<-SETUP_END &&
1491 - commit refs/heads/N10
1492 - committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
1493 - data <<COMMIT
1494 - hello, tree
1495 - COMMIT
1495 + test_expect_success "N: modify subtree, extract it to the root ($root), and modify again" '
1496 + echo hello >expect.baz &&
1497 + echo hello, world >expect.qux &&
1498 + git fast-import <<-SETUP_END &&
1499 + commit refs/heads/N10
1500 + committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
1501 + data <<COMMIT
1502 + hello, tree
1503 + COMMIT
1504
1497 - deleteall
1498 - M 644 inline foo/bar/baz
1499 - data <<EOF
1500 - hello
1501 - EOF
1502 - SETUP_END
1505 + deleteall
1506 + M 644 inline foo/bar/baz
1507 + data <<EOF
1508 + hello
1509 + EOF
1510 + SETUP_END
1511
1504 - tree=$(git rev-parse --verify N10:) &&
1505 - git fast-import <<-INPUT_END &&
1506 - commit refs/heads/N11
1507 - committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
1508 - data <<COMMIT
1509 - copy to root by id and modify
1510 - COMMIT
1512 + tree=$(git rev-parse --verify N10:) &&
1513 + git fast-import <<-INPUT_END &&
1514 + commit refs/heads/N11
1515 + committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
1516 + data <<COMMIT
1517 + copy to root by id and modify
1518 + COMMIT
1519
1512 - M 040000 $tree ""
1513 - M 100644 inline foo/bar/qux
1514 - data <<EOF
1515 - hello, world
1516 - EOF
1517 - R "foo" ""
1518 - C "bar/qux" "bar/quux"
1519 - INPUT_END
1520 - git show N11:bar/baz >actual.baz &&
1521 - git show N11:bar/qux >actual.qux &&
1522 - git show N11:bar/quux >actual.quux &&
1523 - test_cmp expect.baz actual.baz &&
1524 - test_cmp expect.qux actual.qux &&
1525 - test_cmp expect.qux actual.quux'
1520 + M 040000 $tree $root
1521 + M 100644 inline foo/bar/qux
1522 + data <<EOF
1523 + hello, world
1524 + EOF
1525 + R "foo" $root
1526 + C "bar/qux" "bar/quux"
1527 + INPUT_END
1528 + git show N11:bar/baz >actual.baz &&
1529 + git show N11:bar/qux >actual.qux &&
1530 + git show N11:bar/quux >actual.quux &&
1531 + test_cmp expect.baz actual.baz &&
1532 + test_cmp expect.qux actual.qux &&
1533 + test_cmp expect.qux actual.quux
1534 + '
1535 +done
1536
1537 ###
1538 ### series O
@@ -3067,6 +3077,7 @@ test_expect_success 'S: ls with garbage after sha1 must fail' '
3077 # There are two sorts of ways a path can be parsed, depending on whether it is
3078 # the last field on the line. Additionally, ls without a <dataref> has a special
3079 # case. Test every occurrence of <path> in the grammar against every error case.
3080 +# Paths for the root (empty strings) are tested elsewhere.
3081 #
3082
3083 #
@@ -3321,16 +3332,19 @@ test_path_eol_quoted_fail 'ls (without dataref in commit)' 'ls ' path
3332 ###
3333 # Setup is carried over from series S.
3334
3324 -test_expect_success 'T: ls root tree' '
3325 - sed -e "s/Z\$//" >expect <<-EOF &&
3326 - 040000 tree $(git rev-parse S^{tree}) Z
3327 - EOF
3328 - sha1=$(git rev-parse --verify S) &&
3329 - git fast-import --import-marks=marks <<-EOF >actual &&
3330 - ls $sha1 ""
3331 - EOF
3332 - test_cmp expect actual
3333 -'
3335 +for root in '""' ''
3336 +do
3337 + test_expect_success "T: ls root ($root) tree" '
3338 + sed -e "s/Z\$//" >expect <<-EOF &&
3339 + 040000 tree $(git rev-parse S^{tree}) Z
3340 + EOF
3341 + sha1=$(git rev-parse --verify S) &&
3342 + git fast-import --import-marks=marks <<-EOF >actual &&
3343 + ls $sha1 $root
3344 + EOF
3345 + test_cmp expect actual
3346 + '
3347 +done
3348
3349 test_expect_success 'T: delete branch' '
3350 git branch to-delete &&
@@ -3432,30 +3446,33 @@ test_expect_success 'U: validate directory delete result' '
3446 compare_diff_raw expect actual
3447 '
3448
3435 -test_expect_success 'U: filedelete root succeeds' '
3436 - cat >input <<-INPUT_END &&
3437 - commit refs/heads/U
3438 - committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
3439 - data <<COMMIT
3440 - must succeed
3441 - COMMIT
3442 - from refs/heads/U^0
3443 - D ""
3449 +for root in '""' ''
3450 +do
3451 + test_expect_success "U: filedelete root ($root) succeeds" '
3452 + cat >input <<-INPUT_END &&
3453 + commit refs/heads/U-delete-root
3454 + committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
3455 + data <<COMMIT
3456 + must succeed
3457 + COMMIT
3458 + from refs/heads/U^0
3459 + D $root
3460
3445 - INPUT_END
3461 + INPUT_END
3462
3447 - git fast-import <input
3448 -'
3463 + git fast-import <input
3464 + '
3465
3450 -test_expect_success 'U: validate root delete result' '
3451 - cat >expect <<-EOF &&
3452 - :100644 000000 $f7id $ZERO_OID D hello.c
3453 - EOF
3466 + test_expect_success "U: validate root ($root) delete result" '
3467 + cat >expect <<-EOF &&
3468 + :100644 000000 $f7id $ZERO_OID D hello.c
3469 + EOF
3470
3455 - git diff-tree -M -r U^1 U >actual &&
3471 + git diff-tree -M -r U U-delete-root >actual &&
3472
3457 - compare_diff_raw expect actual
3458 -'
3473 + compare_diff_raw expect actual
3474 + '
3475 +done
3476
3477 ###
3478 ### series V (checkpoint)