Git fork

Merge branch 'kn/refs-files-case-insensitive' into maint-2.51

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
··· 1223 1223 return 0; 1224 1224 1225 1225 if (!transaction->rejections) 1226 - BUG("transaction not inititalized with failure support"); 1226 + BUG("transaction not initialized with failure support"); 1227 1227 1228 1228 /* 1229 1229 * Don't accept generic errors, since these errors are not user ··· 1231 1231 */ 1232 1232 if (err == REF_TRANSACTION_ERROR_GENERIC) 1233 1233 return 0; 1234 + 1235 + /* 1236 + * Rejected refnames shouldn't be considered in the availability 1237 + * checks, so remove them from the list. 1238 + */ 1239 + string_list_remove(&transaction->refnames, 1240 + transaction->updates[update_idx]->refname, 0); 1234 1241 1235 1242 transaction->updates[update_idx]->rejection_err = err; 1236 1243 ALLOC_GROW(transaction->rejections->update_indices, ··· 3327 3334 return "invalid new value provided"; 3328 3335 case REF_TRANSACTION_ERROR_EXPECTED_SYMREF: 3329 3336 return "expected symref but found regular ref"; 3337 + case REF_TRANSACTION_ERROR_CASE_CONFLICT: 3338 + return "reference conflict due to case-insensitive filesystem"; 3330 3339 default: 3331 3340 return "unknown failure"; 3332 3341 }
+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 } ··· 2590 2645 if (lock) { 2591 2646 lock->count++; 2592 2647 } else { 2593 - ret = lock_raw_ref(refs, update, update_idx, mustexist, 2594 - refnames_to_check, &transaction->refnames, 2595 - &lock, &referent, err); 2648 + ret = lock_raw_ref(refs, transaction, update_idx, mustexist, 2649 + refnames_to_check, &lock, &referent, err); 2596 2650 if (ret) { 2597 2651 char *reason; 2598 2652 ··· 2830 2884 "ref_transaction_prepare"); 2831 2885 size_t i; 2832 2886 int ret = 0; 2833 - struct string_list refnames_to_check = STRING_LIST_INIT_NODUP; 2887 + struct string_list refnames_to_check = STRING_LIST_INIT_DUP; 2834 2888 char *head_ref = NULL; 2835 2889 int head_type; 2836 2890 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
··· 47 47 git config set branch.main.merge refs/heads/one 48 48 ) && 49 49 git clone . bundle && 50 - git clone . seven 50 + git clone . seven && 51 + git clone --ref-format=reftable . case_sensitive && 52 + ( 53 + cd case_sensitive && 54 + git branch branch1 && 55 + git branch bRanch1 56 + ) && 57 + git clone --ref-format=reftable . case_sensitive_fd && 58 + ( 59 + cd case_sensitive_fd && 60 + git branch foo/bar && 61 + git branch Foo 62 + ) && 63 + git clone --ref-format=reftable . case_sensitive_df && 64 + ( 65 + cd case_sensitive_df && 66 + git branch Foo/bar && 67 + git branch foo 68 + ) 51 69 ' 52 70 53 71 test_expect_success "fetch test" ' ··· 1524 1542 git clone df-conflict clone 2>err && 1525 1543 test_grep ! WHOOPS err && 1526 1544 test_path_is_missing whoops 1545 + ' 1546 + 1547 + test_expect_success CASE_INSENSITIVE_FS,REFFILES 'existing references in a case insensitive filesystem' ' 1548 + test_when_finished rm -rf case_insensitive && 1549 + ( 1550 + git init --bare case_insensitive && 1551 + cd case_insensitive && 1552 + git remote add origin -- ../case_sensitive && 1553 + test_must_fail git fetch -f origin "refs/heads/*:refs/heads/*" 2>err && 1554 + test_grep "You${SQ}re on a case-insensitive filesystem" err && 1555 + git rev-parse refs/heads/main >expect && 1556 + git rev-parse refs/heads/branch1 >actual && 1557 + test_cmp expect actual 1558 + ) 1559 + ' 1560 + 1561 + test_expect_success REFFILES 'existing reference lock in repo' ' 1562 + test_when_finished rm -rf base repo && 1563 + ( 1564 + git init --ref-format=reftable base && 1565 + cd base && 1566 + echo >file update && 1567 + git add . && 1568 + git commit -m "updated" && 1569 + git branch -M main && 1570 + 1571 + git update-ref refs/heads/foo @ && 1572 + git update-ref refs/heads/branch @ && 1573 + cd .. && 1574 + 1575 + git init --ref-format=files --bare repo && 1576 + cd repo && 1577 + git remote add origin ../base && 1578 + touch refs/heads/foo.lock && 1579 + test_must_fail git fetch -f origin "refs/heads/*:refs/heads/*" 2>err && 1580 + test_grep "error: fetching ref refs/heads/foo failed: reference already exists" err && 1581 + git rev-parse refs/heads/main >expect && 1582 + git rev-parse refs/heads/branch >actual && 1583 + test_cmp expect actual 1584 + ) 1585 + ' 1586 + 1587 + test_expect_success CASE_INSENSITIVE_FS,REFFILES 'F/D conflict on case insensitive filesystem' ' 1588 + test_when_finished rm -rf case_insensitive && 1589 + ( 1590 + git init --bare case_insensitive && 1591 + cd case_insensitive && 1592 + git remote add origin -- ../case_sensitive_fd && 1593 + test_must_fail git fetch -f origin "refs/heads/*:refs/heads/*" 2>err && 1594 + test_grep "failed: refname conflict" err && 1595 + git rev-parse refs/heads/main >expect && 1596 + git rev-parse refs/heads/foo/bar >actual && 1597 + test_cmp expect actual 1598 + ) 1599 + ' 1600 + 1601 + test_expect_success CASE_INSENSITIVE_FS,REFFILES 'D/F conflict on case insensitive filesystem' ' 1602 + test_when_finished rm -rf case_insensitive && 1603 + ( 1604 + git init --bare case_insensitive && 1605 + cd case_insensitive && 1606 + git remote add origin -- ../case_sensitive_df && 1607 + test_must_fail git fetch -f origin "refs/heads/*:refs/heads/*" 2>err && 1608 + test_grep "failed: refname conflict" err && 1609 + git rev-parse refs/heads/main >expect && 1610 + git rev-parse refs/heads/Foo/bar >actual && 1611 + test_cmp expect actual 1612 + ) 1613 + ' 1614 + 1615 + test_expect_success REFFILES 'D/F conflict on case sensitive filesystem with lock' ' 1616 + ( 1617 + git init --ref-format=reftable base && 1618 + cd base && 1619 + echo >file update && 1620 + git add . && 1621 + git commit -m "updated" && 1622 + git branch -M main && 1623 + 1624 + git update-ref refs/heads/foo @ && 1625 + git update-ref refs/heads/branch @ && 1626 + cd .. && 1627 + 1628 + git init --ref-format=files --bare repo && 1629 + cd repo && 1630 + git remote add origin ../base && 1631 + mkdir refs/heads/foo && 1632 + touch refs/heads/foo/random.lock && 1633 + test_must_fail git fetch origin "refs/heads/*:refs/heads/*" 2>err && 1634 + test_grep "some local refs could not be updated; try running" err && 1635 + git rev-parse refs/heads/main >expect && 1636 + git rev-parse refs/heads/branch >actual && 1637 + test_cmp expect actual 1638 + ) 1527 1639 ' 1528 1640 1529 1641 . "$TEST_DIRECTORY"/lib-httpd.sh