refs/files: handle F/D conflicts in case-insensitive FS

When using the files-backend on case-insensitive filesystems, there is
possibility of hitting F/D conflicts when creating references within a
single transaction, such as:

  - 'refs/heads/foo'
  - 'refs/heads/Foo/bar'

Ideally such conflicts are caught in `refs_verify_refnames_available()`
which is responsible for checking F/D conflicts within a given
transaction. This utility function is shared across the reference
backends. As such, it doesn't consider the issues of using a
case-insensitive file system, which only affects the files-backend.

While one solution would be to make the function aware of such issues,
this feels like leaking implementation details of file-backend specific
issues into the utility function. So opt for the more simpler option, of
lowercasing all references sent to this function when on a
case-insensitive filesystem and operating on the files-backend.

To do this, simply use a `struct strbuf` to convert the refname to
lowercase and append it to the list of refnames to be checked. Since we
use a `struct strbuf` and the memory is cleared right after, make sure
that the string list duplicates all provided string.

Without this change, the user would simply be left with a repository
with '.lock' files which were created in the 'prepare' phase of the
transaction, as the 'commit' phase would simply abort and not do the
necessary cleanup.

Reported-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Karthik Nayak <karthik.188@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
maint
Karthik Nayak 2025-09-17 17:25:13 +02:00 committed by Junio C Hamano
parent 9b62a67bdb
commit 770f389b2d
2 changed files with 37 additions and 2 deletions

View File

@ -906,8 +906,23 @@ retry:
* If the ref did not exist and we are creating it, we have to
* make sure there is no existing packed ref that conflicts
* with refname. This check is deferred so that we can batch it.
*
* For case-insensitive filesystems, we should also check for F/D
* conflicts between 'foo' and 'Foo/bar'. So let's lowercase
* the refname.
*/
item = string_list_append(refnames_to_check, refname);
if (ignore_case) {
struct strbuf lower = STRBUF_INIT;

strbuf_addstr(&lower, refname);
strbuf_tolower(&lower);

item = string_list_append_nodup(refnames_to_check,
strbuf_detach(&lower, NULL));
} else {
item = string_list_append(refnames_to_check, refname);
}

item->util = xmalloc(sizeof(update_idx));
memcpy(item->util, &update_idx, sizeof(update_idx));
}
@ -2832,7 +2847,7 @@ static int files_transaction_prepare(struct ref_store *ref_store,
"ref_transaction_prepare");
size_t i;
int ret = 0;
struct string_list refnames_to_check = STRING_LIST_INIT_NODUP;
struct string_list refnames_to_check = STRING_LIST_INIT_DUP;
char *head_ref = NULL;
int head_type;
struct files_transaction_backend_data *backend_data;

View File

@ -53,6 +53,12 @@ test_expect_success "clone and setup child repos" '
cd case_sensitive &&
git branch branch1 &&
git branch bRanch1
) &&
git clone --ref-format=reftable . case_sensitive_fd &&
(
cd case_sensitive_fd &&
git branch foo/bar &&
git branch Foo
)
'

@ -1572,6 +1578,20 @@ test_expect_success REFFILES 'existing reference lock in repo' '
)
'

test_expect_success CASE_INSENSITIVE_FS,REFFILES 'F/D conflict on case insensitive filesystem' '
test_when_finished rm -rf case_insensitive &&
(
git init --bare case_insensitive &&
cd case_insensitive &&
git remote add origin -- ../case_sensitive_fd &&
test_must_fail git fetch -f origin "refs/heads/*:refs/heads/*" 2>err &&
test_grep "failed: refname conflict" err &&
git rev-parse refs/heads/main >expect &&
git rev-parse refs/heads/foo/bar >actual &&
test_cmp expect actual
)
'

. "$TEST_DIRECTORY"/lib-httpd.sh
start_httpd