Git fork

Merge branch 'kn/refs-files-case-insensitive'

Deal more gracefully with directory / file conflicts when the files
backend is used for ref storage, by failing only the ones that are
involved in the conflict while allowing others.

* kn/refs-files-case-insensitive:
refs/files: handle D/F conflicts during locking
refs/files: handle F/D conflicts in case-insensitive FS
refs/files: use correct error type when lock exists
refs/files: catch conflicts on case-insensitive file-systems

+262 -17
+18 -3
builtin/fetch.c
··· 1643 1643 1644 1644 struct ref_rejection_data { 1645 1645 int *retcode; 1646 - int conflict_msg_shown; 1646 + bool conflict_msg_shown; 1647 + bool case_sensitive_msg_shown; 1647 1648 const char *remote_name; 1648 1649 }; 1649 1650 ··· 1657 1658 { 1658 1659 struct ref_rejection_data *data = cb_data; 1659 1660 1660 - if (err == REF_TRANSACTION_ERROR_NAME_CONFLICT && !data->conflict_msg_shown) { 1661 + if (err == REF_TRANSACTION_ERROR_CASE_CONFLICT && ignore_case && 1662 + !data->case_sensitive_msg_shown) { 1663 + error(_("You're on a case-insensitive filesystem, and the remote you are\n" 1664 + "trying to fetch from has references that only differ in casing. It\n" 1665 + "is impossible to store such references with the 'files' backend. You\n" 1666 + "can either accept this as-is, in which case you won't be able to\n" 1667 + "store all remote references on disk. Or you can alternatively\n" 1668 + "migrate your repository to use the 'reftable' backend with the\n" 1669 + "following command:\n\n git refs migrate --ref-format=reftable\n\n" 1670 + "Please keep in mind that not all implementations of Git support this\n" 1671 + "new format yet. So if you use tools other than Git to access this\n" 1672 + "repository it may not be an option to migrate to reftables.\n")); 1673 + data->case_sensitive_msg_shown = true; 1674 + } else if (err == REF_TRANSACTION_ERROR_NAME_CONFLICT && 1675 + !data->conflict_msg_shown) { 1661 1676 error(_("some local refs could not be updated; try running\n" 1662 1677 " 'git remote prune %s' to remove any old, conflicting " 1663 1678 "branches"), data->remote_name); 1664 - data->conflict_msg_shown = 1; 1679 + data->conflict_msg_shown = true; 1665 1680 } else { 1666 1681 const char *reason = ref_transaction_error_msg(err); 1667 1682
+10 -1
refs.c
··· 1237 1237 return 0; 1238 1238 1239 1239 if (!transaction->rejections) 1240 - BUG("transaction not inititalized with failure support"); 1240 + BUG("transaction not initialized with failure support"); 1241 1241 1242 1242 /* 1243 1243 * Don't accept generic errors, since these errors are not user ··· 1245 1245 */ 1246 1246 if (err == REF_TRANSACTION_ERROR_GENERIC) 1247 1247 return 0; 1248 + 1249 + /* 1250 + * Rejected refnames shouldn't be considered in the availability 1251 + * checks, so remove them from the list. 1252 + */ 1253 + string_list_remove(&transaction->refnames, 1254 + transaction->updates[update_idx]->refname, 0); 1248 1255 1249 1256 transaction->updates[update_idx]->rejection_err = err; 1250 1257 ALLOC_GROW(transaction->rejections->update_indices, ··· 3330 3337 return "invalid new value provided"; 3331 3338 case REF_TRANSACTION_ERROR_EXPECTED_SYMREF: 3332 3339 return "expected symref but found regular ref"; 3340 + case REF_TRANSACTION_ERROR_CASE_CONFLICT: 3341 + return "reference conflict due to case-insensitive filesystem"; 3333 3342 default: 3334 3343 return "unknown failure"; 3335 3344 }
+2
refs.h
··· 31 31 REF_TRANSACTION_ERROR_INVALID_NEW_VALUE = -6, 32 32 /* Expected ref to be symref, but is a regular ref */ 33 33 REF_TRANSACTION_ERROR_EXPECTED_SYMREF = -7, 34 + /* Cannot create ref due to case-insensitive filesystem */ 35 + REF_TRANSACTION_ERROR_CASE_CONFLICT = -8, 34 36 }; 35 37 36 38 /*
+66 -12
refs/files-backend.c
··· 654 654 } 655 655 656 656 /* 657 + * Check if the transaction has another update with a case-insensitive refname 658 + * match. 659 + * 660 + * If the update is part of the transaction, we only check up to that index. 661 + * Further updates are expected to call this function to match previous indices. 662 + */ 663 + static bool transaction_has_case_conflicting_update(struct ref_transaction *transaction, 664 + struct ref_update *update) 665 + { 666 + for (size_t i = 0; i < transaction->nr; i++) { 667 + if (transaction->updates[i] == update) 668 + break; 669 + 670 + if (!strcasecmp(transaction->updates[i]->refname, update->refname)) 671 + return true; 672 + } 673 + return false; 674 + } 675 + 676 + /* 657 677 * Lock refname, without following symrefs, and set *lock_p to point 658 678 * at a newly-allocated lock object. Fill in lock->old_oid, referent, 659 679 * and type similarly to read_raw_ref(). ··· 683 703 * - Generate informative error messages in the case of failure 684 704 */ 685 705 static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs, 686 - struct ref_update *update, 706 + struct ref_transaction *transaction, 687 707 size_t update_idx, 688 708 int mustexist, 689 709 struct string_list *refnames_to_check, 690 - const struct string_list *extras, 691 710 struct ref_lock **lock_p, 692 711 struct strbuf *referent, 693 712 struct strbuf *err) 694 713 { 695 714 enum ref_transaction_error ret = REF_TRANSACTION_ERROR_GENERIC; 715 + struct ref_update *update = transaction->updates[update_idx]; 716 + const struct string_list *extras = &transaction->refnames; 696 717 const char *refname = update->refname; 697 718 unsigned int *type = &update->type; 698 719 struct ref_lock *lock; ··· 782 803 goto retry; 783 804 } else { 784 805 unable_to_lock_message(ref_file.buf, myerr, err); 806 + if (myerr == EEXIST) { 807 + if (ignore_case && 808 + transaction_has_case_conflicting_update(transaction, update)) { 809 + /* 810 + * In case-insensitive filesystems, ensure that conflicts within a 811 + * given transaction are handled. Pre-existing refs on a 812 + * case-insensitive system will be overridden without any issue. 813 + */ 814 + ret = REF_TRANSACTION_ERROR_CASE_CONFLICT; 815 + } else { 816 + /* 817 + * Pre-existing case-conflicting reference locks should also be 818 + * specially categorized to avoid failing all batched updates. 819 + */ 820 + ret = REF_TRANSACTION_ERROR_CREATE_EXISTS; 821 + } 822 + } 823 + 785 824 goto error_return; 786 825 } 787 826 } ··· 837 876 goto error_return; 838 877 } else if (remove_dir_recursively(&ref_file, 839 878 REMOVE_DIR_EMPTY_ONLY)) { 879 + ret = REF_TRANSACTION_ERROR_NAME_CONFLICT; 840 880 if (refs_verify_refname_available( 841 881 &refs->base, refname, 842 882 extras, NULL, 0, err)) { ··· 844 884 * The error message set by 845 885 * verify_refname_available() is OK. 846 886 */ 847 - ret = REF_TRANSACTION_ERROR_NAME_CONFLICT; 848 887 goto error_return; 849 888 } else { 850 889 /* 851 - * We can't delete the directory, 852 - * but we also don't know of any 853 - * references that it should 854 - * contain. 890 + * Directory conflicts can occur if there 891 + * is an existing lock file in the directory 892 + * or if the filesystem is case-insensitive 893 + * and the directory contains a valid reference 894 + * but conflicts with the update. 855 895 */ 856 896 strbuf_addf(err, "there is a non-empty directory '%s' " 857 897 "blocking reference '%s'", ··· 873 913 * If the ref did not exist and we are creating it, we have to 874 914 * make sure there is no existing packed ref that conflicts 875 915 * with refname. This check is deferred so that we can batch it. 916 + * 917 + * For case-insensitive filesystems, we should also check for F/D 918 + * conflicts between 'foo' and 'Foo/bar'. So let's lowercase 919 + * the refname. 876 920 */ 877 - item = string_list_append(refnames_to_check, refname); 921 + if (ignore_case) { 922 + struct strbuf lower = STRBUF_INIT; 923 + 924 + strbuf_addstr(&lower, refname); 925 + strbuf_tolower(&lower); 926 + 927 + item = string_list_append_nodup(refnames_to_check, 928 + strbuf_detach(&lower, NULL)); 929 + } else { 930 + item = string_list_append(refnames_to_check, refname); 931 + } 932 + 878 933 item->util = xmalloc(sizeof(update_idx)); 879 934 memcpy(item->util, &update_idx, sizeof(update_idx)); 880 935 } ··· 2616 2671 if (lock) { 2617 2672 lock->count++; 2618 2673 } else { 2619 - ret = lock_raw_ref(refs, update, update_idx, mustexist, 2620 - refnames_to_check, &transaction->refnames, 2621 - &lock, &referent, err); 2674 + ret = lock_raw_ref(refs, transaction, update_idx, mustexist, 2675 + refnames_to_check, &lock, &referent, err); 2622 2676 if (ret) { 2623 2677 char *reason; 2624 2678 ··· 2858 2912 "ref_transaction_prepare"); 2859 2913 size_t i; 2860 2914 int ret = 0; 2861 - struct string_list refnames_to_check = STRING_LIST_INIT_NODUP; 2915 + struct string_list refnames_to_check = STRING_LIST_INIT_DUP; 2862 2916 char *head_ref = NULL; 2863 2917 int head_type; 2864 2918 struct files_transaction_backend_data *backend_data;
+53
t/t1400-update-ref.sh
··· 2294 2294 ) 2295 2295 ' 2296 2296 2297 + test_expect_success CASE_INSENSITIVE_FS,REFFILES "stdin $type batch-updates existing reference" ' 2298 + git init repo && 2299 + test_when_finished "rm -fr repo" && 2300 + ( 2301 + cd repo && 2302 + test_commit one && 2303 + old_head=$(git rev-parse HEAD) && 2304 + test_commit two && 2305 + head=$(git rev-parse HEAD) && 2306 + 2307 + { 2308 + format_command $type "create refs/heads/foo" "$head" && 2309 + format_command $type "create refs/heads/ref" "$old_head" && 2310 + format_command $type "create refs/heads/Foo" "$old_head" 2311 + } >stdin && 2312 + git update-ref $type --stdin --batch-updates <stdin >stdout && 2313 + 2314 + echo $head >expect && 2315 + git rev-parse refs/heads/foo >actual && 2316 + echo $old_head >expect && 2317 + git rev-parse refs/heads/ref >actual && 2318 + test_cmp expect actual && 2319 + test_grep -q "reference conflict due to case-insensitive filesystem" stdout 2320 + ) 2321 + ' 2322 + 2323 + test_expect_success CASE_INSENSITIVE_FS "stdin $type batch-updates existing reference" ' 2324 + git init --ref-format=reftable repo && 2325 + test_when_finished "rm -fr repo" && 2326 + ( 2327 + cd repo && 2328 + test_commit one && 2329 + old_head=$(git rev-parse HEAD) && 2330 + test_commit two && 2331 + head=$(git rev-parse HEAD) && 2332 + 2333 + { 2334 + format_command $type "create refs/heads/foo" "$head" && 2335 + format_command $type "create refs/heads/ref" "$old_head" && 2336 + format_command $type "create refs/heads/Foo" "$old_head" 2337 + } >stdin && 2338 + git update-ref $type --stdin --batch-updates <stdin >stdout && 2339 + 2340 + echo $head >expect && 2341 + git rev-parse refs/heads/foo >actual && 2342 + echo $old_head >expect && 2343 + git rev-parse refs/heads/ref >actual && 2344 + test_cmp expect actual && 2345 + git rev-parse refs/heads/Foo >actual && 2346 + test_cmp expect actual 2347 + ) 2348 + ' 2349 + 2297 2350 test_expect_success "stdin $type batch-updates delete incorrect symbolic ref" ' 2298 2351 git init repo && 2299 2352 test_when_finished "rm -fr repo" &&
+113 -1
t/t5510-fetch.sh
··· 45 45 git config set branch.main.merge refs/heads/one 46 46 ) && 47 47 git clone . bundle && 48 - git clone . seven 48 + git clone . seven && 49 + git clone --ref-format=reftable . case_sensitive && 50 + ( 51 + cd case_sensitive && 52 + git branch branch1 && 53 + git branch bRanch1 54 + ) && 55 + git clone --ref-format=reftable . case_sensitive_fd && 56 + ( 57 + cd case_sensitive_fd && 58 + git branch foo/bar && 59 + git branch Foo 60 + ) && 61 + git clone --ref-format=reftable . case_sensitive_df && 62 + ( 63 + cd case_sensitive_df && 64 + git branch Foo/bar && 65 + git branch foo 66 + ) 49 67 ' 50 68 51 69 test_expect_success "fetch test" ' ··· 1463 1481 git clone df-conflict clone 2>err && 1464 1482 test_grep ! WHOOPS err && 1465 1483 test_path_is_missing whoops 1484 + ' 1485 + 1486 + test_expect_success CASE_INSENSITIVE_FS,REFFILES 'existing references in a case insensitive filesystem' ' 1487 + test_when_finished rm -rf case_insensitive && 1488 + ( 1489 + git init --bare case_insensitive && 1490 + cd case_insensitive && 1491 + git remote add origin -- ../case_sensitive && 1492 + test_must_fail git fetch -f origin "refs/heads/*:refs/heads/*" 2>err && 1493 + test_grep "You${SQ}re on a case-insensitive filesystem" err && 1494 + git rev-parse refs/heads/main >expect && 1495 + git rev-parse refs/heads/branch1 >actual && 1496 + test_cmp expect actual 1497 + ) 1498 + ' 1499 + 1500 + test_expect_success REFFILES 'existing reference lock in repo' ' 1501 + test_when_finished rm -rf base repo && 1502 + ( 1503 + git init --ref-format=reftable base && 1504 + cd base && 1505 + echo >file update && 1506 + git add . && 1507 + git commit -m "updated" && 1508 + git branch -M main && 1509 + 1510 + git update-ref refs/heads/foo @ && 1511 + git update-ref refs/heads/branch @ && 1512 + cd .. && 1513 + 1514 + git init --ref-format=files --bare repo && 1515 + cd repo && 1516 + git remote add origin ../base && 1517 + touch refs/heads/foo.lock && 1518 + test_must_fail git fetch -f origin "refs/heads/*:refs/heads/*" 2>err && 1519 + test_grep "error: fetching ref refs/heads/foo failed: reference already exists" err && 1520 + git rev-parse refs/heads/main >expect && 1521 + git rev-parse refs/heads/branch >actual && 1522 + test_cmp expect actual 1523 + ) 1524 + ' 1525 + 1526 + test_expect_success CASE_INSENSITIVE_FS,REFFILES 'F/D conflict on case insensitive filesystem' ' 1527 + test_when_finished rm -rf case_insensitive && 1528 + ( 1529 + git init --bare case_insensitive && 1530 + cd case_insensitive && 1531 + git remote add origin -- ../case_sensitive_fd && 1532 + test_must_fail git fetch -f origin "refs/heads/*:refs/heads/*" 2>err && 1533 + test_grep "failed: refname conflict" err && 1534 + git rev-parse refs/heads/main >expect && 1535 + git rev-parse refs/heads/foo/bar >actual && 1536 + test_cmp expect actual 1537 + ) 1538 + ' 1539 + 1540 + test_expect_success CASE_INSENSITIVE_FS,REFFILES 'D/F conflict on case insensitive filesystem' ' 1541 + test_when_finished rm -rf case_insensitive && 1542 + ( 1543 + git init --bare case_insensitive && 1544 + cd case_insensitive && 1545 + git remote add origin -- ../case_sensitive_df && 1546 + test_must_fail git fetch -f origin "refs/heads/*:refs/heads/*" 2>err && 1547 + test_grep "failed: refname conflict" err && 1548 + git rev-parse refs/heads/main >expect && 1549 + git rev-parse refs/heads/Foo/bar >actual && 1550 + test_cmp expect actual 1551 + ) 1552 + ' 1553 + 1554 + test_expect_success REFFILES 'D/F conflict on case sensitive filesystem with lock' ' 1555 + ( 1556 + git init --ref-format=reftable base && 1557 + cd base && 1558 + echo >file update && 1559 + git add . && 1560 + git commit -m "updated" && 1561 + git branch -M main && 1562 + 1563 + git update-ref refs/heads/foo @ && 1564 + git update-ref refs/heads/branch @ && 1565 + cd .. && 1566 + 1567 + git init --ref-format=files --bare repo && 1568 + cd repo && 1569 + git remote add origin ../base && 1570 + mkdir refs/heads/foo && 1571 + touch refs/heads/foo/random.lock && 1572 + test_must_fail git fetch origin "refs/heads/*:refs/heads/*" 2>err && 1573 + test_grep "some local refs could not be updated; try running" err && 1574 + git rev-parse refs/heads/main >expect && 1575 + git rev-parse refs/heads/branch >actual && 1576 + test_cmp expect actual 1577 + ) 1466 1578 ' 1467 1579 1468 1580 . "$TEST_DIRECTORY"/lib-httpd.sh