Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions builtin/fetch.c
Original file line number Diff line number Diff line change
Expand Up @@ -1643,7 +1643,8 @@ static int set_head(const struct ref *remote_refs, struct remote *remote)

struct ref_rejection_data {
int *retcode;
int conflict_msg_shown;
bool conflict_msg_shown;
bool case_sensitive_msg_shown;
const char *remote_name;
};

Expand All @@ -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"));
data->case_sensitive_msg_shown = true;
} else if (err == REF_TRANSACTION_ERROR_NAME_CONFLICT &&
!data->conflict_msg_shown) {
error(_("some local refs could not be updated; try running\n"
" 'git remote prune %s' to remove any old, conflicting "
"branches"), data->remote_name);
data->conflict_msg_shown = 1;
data->conflict_msg_shown = true;
} else {
const char *reason = ref_transaction_error_msg(err);

Expand Down
11 changes: 10 additions & 1 deletion refs.c
Original file line number Diff line number Diff line change
Expand Up @@ -1223,7 +1223,7 @@ int ref_transaction_maybe_set_rejected(struct ref_transaction *transaction,
return 0;

if (!transaction->rejections)
BUG("transaction not inititalized with failure support");
BUG("transaction not initialized with failure support");

/*
* Don't accept generic errors, since these errors are not user
Expand All @@ -1232,6 +1232,13 @@ int ref_transaction_maybe_set_rejected(struct ref_transaction *transaction,
if (err == REF_TRANSACTION_ERROR_GENERIC)
return 0;

/*
* Rejected refnames shouldn't be considered in the availability
* checks, so remove them from the list.
*/
string_list_remove(&transaction->refnames,
transaction->updates[update_idx]->refname, 0);

transaction->updates[update_idx]->rejection_err = err;
ALLOC_GROW(transaction->rejections->update_indices,
transaction->rejections->nr + 1,
Expand Down Expand Up @@ -3321,6 +3328,8 @@ const char *ref_transaction_error_msg(enum ref_transaction_error err)
return "invalid new value provided";
case REF_TRANSACTION_ERROR_EXPECTED_SYMREF:
return "expected symref but found regular ref";
case REF_TRANSACTION_ERROR_CASE_CONFLICT:
return "reference conflict due to case-insensitive filesystem";
default:
return "unknown failure";
}
Expand Down
2 changes: 2 additions & 0 deletions refs.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ enum ref_transaction_error {
REF_TRANSACTION_ERROR_INVALID_NEW_VALUE = -6,
/* Expected ref to be symref, but is a regular ref */
REF_TRANSACTION_ERROR_EXPECTED_SYMREF = -7,
/* Cannot create ref due to case-insensitive filesystem */
REF_TRANSACTION_ERROR_CASE_CONFLICT = -8,
};

/*
Expand Down
78 changes: 66 additions & 12 deletions refs/files-backend.c
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,26 @@ static void unlock_ref(struct ref_lock *lock)
}
}

/*
* Check if the transaction has another update with a case-insensitive refname
* match.
*
* If the update is part of the transaction, we only check up to that index.
* Further updates are expected to call this function to match previous indices.
*/
static bool transaction_has_case_conflicting_update(struct ref_transaction *transaction,
struct ref_update *update)
{
for (size_t i = 0; i < transaction->nr; i++) {
if (transaction->updates[i] == update)
break;

if (!strcasecmp(transaction->updates[i]->refname, update->refname))
return true;
}
return false;
}

/*
* Lock refname, without following symrefs, and set *lock_p to point
* at a newly-allocated lock object. Fill in lock->old_oid, referent,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -776,6 +797,24 @@ 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);
if (myerr == EEXIST) {
if (ignore_case &&
transaction_has_case_conflicting_update(transaction, update)) {
/*
* In case-insensitive filesystems, ensure that conflicts within a
* given transaction are handled. Pre-existing refs on a
* case-insensitive system will be overridden without any issue.
*/
ret = REF_TRANSACTION_ERROR_CASE_CONFLICT;
} else {
/*
* Pre-existing case-conflicting reference locks should also be
* specially categorized to avoid failing all batched updates.
*/
ret = REF_TRANSACTION_ERROR_CREATE_EXISTS;
}
}

goto error_return;
}
}
Expand Down Expand Up @@ -831,21 +870,22 @@ static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs,
goto error_return;
} else if (remove_dir_recursively(&ref_file,
REMOVE_DIR_EMPTY_ONLY)) {
ret = REF_TRANSACTION_ERROR_NAME_CONFLICT;
if (refs_verify_refname_available(
&refs->base, refname,
extras, NULL, 0, err)) {
/*
* The error message set by
* verify_refname_available() is OK.
*/
ret = REF_TRANSACTION_ERROR_NAME_CONFLICT;
goto error_return;
} else {
/*
* We can't delete the directory,
* but we also don't know of any
* references that it should
* contain.
* Directory conflicts can occur if there
* is an existing lock file in the directory
* or if the filesystem is case-insensitive
* and the directory contains a valid reference
* but conflicts with the update.
*/
strbuf_addf(err, "there is a non-empty directory '%s' "
"blocking reference '%s'",
Expand All @@ -867,8 +907,23 @@ static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs,
* 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));
}
Expand Down Expand Up @@ -2583,9 +2638,8 @@ static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re
if (lock) {
lock->count++;
} else {
ret = lock_raw_ref(refs, update, update_idx, mustexist,
refnames_to_check, &transaction->refnames,
&lock, &referent, err);
ret = lock_raw_ref(refs, transaction, update_idx, mustexist,
refnames_to_check, &lock, &referent, err);
if (ret) {
char *reason;

Expand Down Expand Up @@ -2794,7 +2848,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;
Expand Down
53 changes: 53 additions & 0 deletions t/t1400-update-ref.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2294,6 +2294,59 @@ do
)
'

test_expect_success CASE_INSENSITIVE_FS,REFFILES "stdin $type batch-updates existing reference" '
git init repo &&
test_when_finished "rm -fr repo" &&
(
cd repo &&
test_commit one &&
old_head=$(git rev-parse HEAD) &&
test_commit two &&
head=$(git rev-parse HEAD) &&

{
format_command $type "create refs/heads/foo" "$head" &&
format_command $type "create refs/heads/ref" "$old_head" &&
format_command $type "create refs/heads/Foo" "$old_head"
} >stdin &&
git update-ref $type --stdin --batch-updates <stdin >stdout &&

echo $head >expect &&
git rev-parse refs/heads/foo >actual &&
echo $old_head >expect &&
git rev-parse refs/heads/ref >actual &&
test_cmp expect actual &&
test_grep -q "reference conflict due to case-insensitive filesystem" stdout
)
'

test_expect_success CASE_INSENSITIVE_FS "stdin $type batch-updates existing reference" '
git init --ref-format=reftable repo &&
test_when_finished "rm -fr repo" &&
(
cd repo &&
test_commit one &&
old_head=$(git rev-parse HEAD) &&
test_commit two &&
head=$(git rev-parse HEAD) &&

{
format_command $type "create refs/heads/foo" "$head" &&
format_command $type "create refs/heads/ref" "$old_head" &&
format_command $type "create refs/heads/Foo" "$old_head"
} >stdin &&
git update-ref $type --stdin --batch-updates <stdin >stdout &&

echo $head >expect &&
git rev-parse refs/heads/foo >actual &&
echo $old_head >expect &&
git rev-parse refs/heads/ref >actual &&
test_cmp expect actual &&
git rev-parse refs/heads/Foo >actual &&
test_cmp expect actual
)
'

test_expect_success "stdin $type batch-updates delete incorrect symbolic ref" '
git init repo &&
test_when_finished "rm -fr repo" &&
Expand Down
Loading
Loading