Skip to content

Commit 96c9a71

Browse files
gitsterdscho
authored andcommitted
Merge branch 'kn/refs-files-case-insensitive' into jch
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
2 parents 8592f15 + 948b2ab commit 96c9a71

File tree

6 files changed

+262
-17
lines changed

6 files changed

+262
-17
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: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1223,7 +1223,7 @@ int ref_transaction_maybe_set_rejected(struct ref_transaction *transaction,
12231223
return 0;
12241224

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

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

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);
1241+
12351242
transaction->updates[update_idx]->rejection_err = err;
12361243
ALLOC_GROW(transaction->rejections->update_indices,
12371244
transaction->rejections->nr + 1,
@@ -3327,6 +3334,8 @@ const char *ref_transaction_error_msg(enum ref_transaction_error err)
33273334
return "invalid new value provided";
33283335
case REF_TRANSACTION_ERROR_EXPECTED_SYMREF:
33293336
return "expected symref but found regular ref";
3337+
case REF_TRANSACTION_ERROR_CASE_CONFLICT:
3338+
return "reference conflict due to case-insensitive filesystem";
33303339
default:
33313340
return "unknown failure";
33323341
}

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: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,26 @@ static void unlock_ref(struct ref_lock *lock)
653653
}
654654
}
655655

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+
656676
/*
657677
* Lock refname, without following symrefs, and set *lock_p to point
658678
* at a newly-allocated lock object. Fill in lock->old_oid, referent,
@@ -683,16 +703,17 @@ static void unlock_ref(struct ref_lock *lock)
683703
* - Generate informative error messages in the case of failure
684704
*/
685705
static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs,
686-
struct ref_update *update,
706+
struct ref_transaction *transaction,
687707
size_t update_idx,
688708
int mustexist,
689709
struct string_list *refnames_to_check,
690-
const struct string_list *extras,
691710
struct ref_lock **lock_p,
692711
struct strbuf *referent,
693712
struct strbuf *err)
694713
{
695714
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;
696717
const char *refname = update->refname;
697718
unsigned int *type = &update->type;
698719
struct ref_lock *lock;
@@ -782,6 +803,24 @@ static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs,
782803
goto retry;
783804
} else {
784805
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+
785824
goto error_return;
786825
}
787826
}
@@ -837,21 +876,22 @@ static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs,
837876
goto error_return;
838877
} else if (remove_dir_recursively(&ref_file,
839878
REMOVE_DIR_EMPTY_ONLY)) {
879+
ret = REF_TRANSACTION_ERROR_NAME_CONFLICT;
840880
if (refs_verify_refname_available(
841881
&refs->base, refname,
842882
extras, NULL, 0, err)) {
843883
/*
844884
* The error message set by
845885
* verify_refname_available() is OK.
846886
*/
847-
ret = REF_TRANSACTION_ERROR_NAME_CONFLICT;
848887
goto error_return;
849888
} else {
850889
/*
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.
855895
*/
856896
strbuf_addf(err, "there is a non-empty directory '%s' "
857897
"blocking reference '%s'",
@@ -873,8 +913,23 @@ static enum ref_transaction_error lock_raw_ref(struct files_ref_store *refs,
873913
* If the ref did not exist and we are creating it, we have to
874914
* make sure there is no existing packed ref that conflicts
875915
* 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.
876920
*/
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+
878933
item->util = xmalloc(sizeof(update_idx));
879934
memcpy(item->util, &update_idx, sizeof(update_idx));
880935
}
@@ -2590,9 +2645,8 @@ static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re
25902645
if (lock) {
25912646
lock->count++;
25922647
} 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);
25962650
if (ret) {
25972651
char *reason;
25982652

@@ -2830,7 +2884,7 @@ static int files_transaction_prepare(struct ref_store *ref_store,
28302884
"ref_transaction_prepare");
28312885
size_t i;
28322886
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;
28342888
char *head_ref = NULL;
28352889
int head_type;
28362890
struct files_transaction_backend_data *backend_data;

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" &&

0 commit comments

Comments
 (0)