Re: [PATCH v3 1/4] refs/files: catch conflicts on case-insensitive file-systems
From: Karthik Nayak <hidden>
Date: 2025-09-17 07:33:26
Justin Tobler [off-list ref] writes:
On 25/09/13 10:54PM, Karthik Nayak wrote:quoted
During the 'prepare' phase of reference transaction in the filess/reference/a reference/
I thought 'phase of reference transaction' without an 'a' is an accepted form.. I'll add this locally for now.
quoted
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/fooOk so this is only a problem now because the reference updates are performed in a single transaction and the resulting error causes the entire transaction to be aborted.
Yup, exactly. So conflicting references within the same transaction become an issue since they try to lock the same file.
quoted
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: 2eefd1150e06d8fca1ddfa684dec016f36bf4e56Makes sense.quoted
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.So does this mean that we return `REF_TRANSACTION_ERROR_CASE_CONFLICT` in all cases where a a lockfile already exists for a reference? Or do we only actually care about scenarios where the lockfile already exists specific to case-insensitive filesystems?
This patch specifically only returns when there is a conflict in case-insensitive systems.
quoted
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-updatesJust to clarify, is this saying that this new error is not ignored in a standard reference transaction? Only the above operations?
Yes, only batched updates care about error types. Regular transactions treat all errors the same.
quoted
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 pro-active and notify users on case-insensitives/pro-active/proactive/
Will change.
quoted
@@ -1657,11 +1658,25 @@ static void ref_transaction_rejection_handler(const char *refname, { struct ref_rejection_data *data = cb_data; - if (err == REF_TRANSACTION_ERROR_NAME_CONFLICT && !data->conflict_msg_shown) { + if (err == REF_TRANSACTION_ERROR_CASE_CONFLICT && ignore_case && + !data->case_sensitive_msg_shown) { + error(_("You're on a case-insensitive filesystem, and the remote you are\n" + "trying to fetch from has references that only differ in casing. It\n" + "is impossible to store such references with the 'files' backend. You\n" + "can either accept this as-is, in which case you won't be able to\n" + "store all remote references on disk. Or you can alternatively\n" + "migrate your repository to use the 'reftable' backend with the\n" + "following command:\n\n git refs migrate --ref-format=reftable\n\n" + "Please keep in mind that not all implementations of Git support this\n" + "new format yet. So if you use tools other than Git to access this\n" + "repository it may not be an option to migrate to reftables.\n"));Nice error message.
All thanks to Patrick!
quoted
+ /* * Lock refname, without following symrefs, and set *lock_p to point * at a newly-allocated lock object. Fill in lock->old_oid, referent,@@ -677,16 +697,17 @@ static void unlock_ref(struct ref_lock *lock) * - Generate informative error messages in the case of failure */ static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs, - struct ref_update *update, + struct ref_transaction *transaction, size_t update_idx, int mustexist, struct string_list *refnames_to_check, - const struct string_list *extras, struct ref_lock **lock_p, struct strbuf *referent, struct strbuf *err) { enum ref_transaction_error ret = REF_TRANSACTION_ERROR_GENERIC; + struct ref_update *update = transaction->updates[update_idx]; + const struct string_list *extras = &transaction->refnames; const char *refname = update->refname; unsigned int *type = &update->type; struct ref_lock *lock;@@ -776,6 +797,9 @@ static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs, goto retry; } else { unable_to_lock_message(ref_file.buf, myerr, err);huh, so if if we have a lockfile error due to a case-insensitve filesystem, does this mean we print the error message from `unable_to_lock_message()` and the new message? If so, I wonder if we would be better off skipping the former since it could be a bit misleading.
I would say both are necessary. The errors added here are more technical and really talk about why we faced an issue. The error in 'builtin/fetch.c' is more about guidance to how to overcome that issue. Also this error is client agnostic, so we'd add the error here for users of both regular transactions and batched updates. The error in 'builtin/fetch.c' is very specific to users of 'git-fetch(1)'. So I think both hold value.
quoted
+ if (myerr == EEXIST && ignore_case && + transaction_has_case_conflicting_update(transaction, update)) + ret = REF_TRANSACTION_ERROR_CASE_CONFLICT; goto error_return; } }-Justin
Attachments
- signature.asc [application/pgp-signature] 690 bytes