Skip to content

Commit 3c07063

Browse files
KarthikNayakgitster
authored andcommitted
refs/files: catch conflicts on case-insensitive file-systems
During the 'prepare' phase of a reference transaction in the files backend, we create the lock files for references to be created. When using batched updates on case-insensitive filesystems, the entire batched updates would be aborted if there are conflicting names such as: refs/heads/Foo refs/heads/foo This affects all commands which were migrated to use batched updates in Git 2.51, including 'git-fetch(1)' and 'git-receive-pack(1)'. Before that, reference updates would be applied serially with one transaction used per update. When users fetched multiple references on case-insensitive systems, subsequent references would simply overwrite any earlier references. So when fetching: refs/heads/foo: 5f34ec0bfeac225b1c854340257a65b106f70ea6 refs/heads/Foo: ec3053b0977e83d9b67fc32c4527a117953994f3 refs/heads/sample: 2eefd1150e06d8fca1ddfa684dec016f36bf4e56 The user would simply end up with: refs/heads/foo: ec3053b0977e83d9b67fc32c4527a117953994f3 refs/heads/sample: 2eefd1150e06d8fca1ddfa684dec016f36bf4e56 This is buggy behavior since the user is never informed about the overrides performed and missing references. Nevertheless, the user is left with a working repository with a subset of the references. Since Git 2.51, in such situations fetches would simply fail without updating any references. Which is also buggy behavior and worse off since the user is left without any references. The error is triggered in `lock_raw_ref()` where the files backend attempts to create a lock file. When a lock file already exists the function returns a 'REF_TRANSACTION_ERROR_GENERIC'. When this happens, the entire batched updates, not individual operation, is aborted as if it were in a transaction. Change this to return 'REF_TRANSACTION_ERROR_CASE_CONFLICT' instead to aid the batched update mechanism to simply reject such errors. The change only affects batched updates since batched updates will reject individual updates with non-generic errors. So specifically this would only affect: 1. git fetch 2. git receive-pack 3. git update-ref --batch-updates This bubbles the error type up to `files_transaction_prepare()` which tries to lock each reference update. So if the locking fails, we check if the rejection type can be ignored, which is done by calling `ref_transaction_maybe_set_rejected()`. As the error type is now 'REF_TRANSACTION_ERROR_CASE_CONFLICT', the specific reference update would simply be rejected, while other updates in the transaction would continue to be applied. This allows partial application of references in case-insensitive filesystems when fetching colliding references. While the earlier implementation allowed the last reference to be applied overriding the initial references, this change would allow the first reference to be applied while rejecting consequent collisions. This should be an okay compromise since with the files backend, there is no scenario possible where we would retain all colliding references. Let's also be more proactive and notify users on case-insensitive filesystems about such problems by providing a brief about the issue while also recommending using the reftable backend, which doesn't have the same issue. Reported-by: Joe Drew <[email protected]> Helped-by: Patrick Steinhardt <[email protected]> Signed-off-by: Karthik Nayak <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent c44beea commit 3c07063

File tree

6 files changed

+124
-9
lines changed

6 files changed

+124
-9
lines changed

builtin/fetch.c

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1643,7 +1643,8 @@ static int set_head(const struct ref *remote_refs, struct remote *remote)
16431643

16441644
struct ref_rejection_data {
16451645
int *retcode;
1646-
int conflict_msg_shown;
1646+
bool conflict_msg_shown;
1647+
bool case_sensitive_msg_shown;
16471648
const char *remote_name;
16481649
};
16491650

@@ -1657,11 +1658,25 @@ static void ref_transaction_rejection_handler(const char *refname,
16571658
{
16581659
struct ref_rejection_data *data = cb_data;
16591660

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) {
16611676
error(_("some local refs could not be updated; try running\n"
16621677
" 'git remote prune %s' to remove any old, conflicting "
16631678
"branches"), data->remote_name);
1664-
data->conflict_msg_shown = 1;
1679+
data->conflict_msg_shown = true;
16651680
} else {
16661681
const char *reason = ref_transaction_error_msg(err);
16671682

refs.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3321,6 +3321,8 @@ const char *ref_transaction_error_msg(enum ref_transaction_error err)
33213321
return "invalid new value provided";
33223322
case REF_TRANSACTION_ERROR_EXPECTED_SYMREF:
33233323
return "expected symref but found regular ref";
3324+
case REF_TRANSACTION_ERROR_CASE_CONFLICT:
3325+
return "reference conflict due to case-insensitive filesystem";
33243326
default:
33253327
return "unknown failure";
33263328
}

refs.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ enum ref_transaction_error {
3131
REF_TRANSACTION_ERROR_INVALID_NEW_VALUE = -6,
3232
/* Expected ref to be symref, but is a regular ref */
3333
REF_TRANSACTION_ERROR_EXPECTED_SYMREF = -7,
34+
/* Cannot create ref due to case-insensitive filesystem */
35+
REF_TRANSACTION_ERROR_CASE_CONFLICT = -8,
3436
};
3537

3638
/*

refs/files-backend.c

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,26 @@ static void unlock_ref(struct ref_lock *lock)
647647
}
648648
}
649649

650+
/*
651+
* Check if the transaction has another update with a case-insensitive refname
652+
* match.
653+
*
654+
* If the update is part of the transaction, we only check up to that index.
655+
* Further updates are expected to call this function to match previous indices.
656+
*/
657+
static bool transaction_has_case_conflicting_update(struct ref_transaction *transaction,
658+
struct ref_update *update)
659+
{
660+
for (size_t i = 0; i < transaction->nr; i++) {
661+
if (transaction->updates[i] == update)
662+
break;
663+
664+
if (!strcasecmp(transaction->updates[i]->refname, update->refname))
665+
return true;
666+
}
667+
return false;
668+
}
669+
650670
/*
651671
* Lock refname, without following symrefs, and set *lock_p to point
652672
* at a newly-allocated lock object. Fill in lock->old_oid, referent,
@@ -677,16 +697,17 @@ static void unlock_ref(struct ref_lock *lock)
677697
* - Generate informative error messages in the case of failure
678698
*/
679699
static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs,
680-
struct ref_update *update,
700+
struct ref_transaction *transaction,
681701
size_t update_idx,
682702
int mustexist,
683703
struct string_list *refnames_to_check,
684-
const struct string_list *extras,
685704
struct ref_lock **lock_p,
686705
struct strbuf *referent,
687706
struct strbuf *err)
688707
{
689708
enum ref_transaction_error ret = REF_TRANSACTION_ERROR_GENERIC;
709+
struct ref_update *update = transaction->updates[update_idx];
710+
const struct string_list *extras = &transaction->refnames;
690711
const char *refname = update->refname;
691712
unsigned int *type = &update->type;
692713
struct ref_lock *lock;
@@ -776,6 +797,9 @@ static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs,
776797
goto retry;
777798
} else {
778799
unable_to_lock_message(ref_file.buf, myerr, err);
800+
if (myerr == EEXIST && ignore_case &&
801+
transaction_has_case_conflicting_update(transaction, update))
802+
ret = REF_TRANSACTION_ERROR_CASE_CONFLICT;
779803
goto error_return;
780804
}
781805
}
@@ -2583,9 +2607,8 @@ static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re
25832607
if (lock) {
25842608
lock->count++;
25852609
} else {
2586-
ret = lock_raw_ref(refs, update, update_idx, mustexist,
2587-
refnames_to_check, &transaction->refnames,
2588-
&lock, &referent, err);
2610+
ret = lock_raw_ref(refs, transaction, update_idx, mustexist,
2611+
refnames_to_check, &lock, &referent, err);
25892612
if (ret) {
25902613
char *reason;
25912614

t/t1400-update-ref.sh

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2294,6 +2294,59 @@ do
22942294
)
22952295
'
22962296

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+
22972350
test_expect_success "stdin $type batch-updates delete incorrect symbolic ref" '
22982351
git init repo &&
22992352
test_when_finished "rm -fr repo" &&

t/t5510-fetch.sh

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,13 @@ test_expect_success "clone and setup child repos" '
4747
git config set branch.main.merge refs/heads/one
4848
) &&
4949
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+
)
5157
'
5258

5359
test_expect_success "fetch test" '
@@ -1526,6 +1532,20 @@ test_expect_success SYMLINKS 'clone does not get confused by a D/F conflict' '
15261532
test_path_is_missing whoops
15271533
'
15281534

1535+
test_expect_success CASE_INSENSITIVE_FS,REFFILES 'existing references in a case insensitive filesystem' '
1536+
test_when_finished rm -rf case_insensitive &&
1537+
(
1538+
git init --bare case_insensitive &&
1539+
cd case_insensitive &&
1540+
git remote add origin -- ../case_sensitive &&
1541+
test_must_fail git fetch -f origin "refs/heads/*:refs/heads/*" 2>err &&
1542+
test_grep "You${SQ}re on a case-insensitive filesystem" err &&
1543+
git rev-parse refs/heads/main >expect &&
1544+
git rev-parse refs/heads/branch1 >actual &&
1545+
test_cmp expect actual
1546+
)
1547+
'
1548+
15291549
. "$TEST_DIRECTORY"/lib-httpd.sh
15301550
start_httpd
15311551

0 commit comments

Comments
 (0)