Skip to content

Commit 9f84850

Browse files
committed
fix: case-insentively conflicting references can be created even on case-insensitie filesystems*. (#595)
The asterisk indicates that this only works if packed-refs are present and these references are written straight to packed references without ever trying to handle the otherwise conflicting loose reference files. This is done by leveraging the fact that in presence of packed-refs or a pending creation of packed-refs, there is no need to create per-file locks as concurrent transactions also have to obtain the packed-refs lock and fail (or wait) until it's done.
1 parent e9853dd commit 9f84850

File tree

6 files changed

+197
-49
lines changed

6 files changed

+197
-49
lines changed

git-ref/src/store/file/packed.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ impl file::Store {
4444
pub fn packed_refs_path(&self) -> PathBuf {
4545
self.common_dir_resolved().join("packed-refs")
4646
}
47+
48+
pub(crate) fn packed_refs_lock_path(&self) -> PathBuf {
49+
let mut p = self.packed_refs_path();
50+
p.set_extension("lock");
51+
p
52+
}
4753
}
4854

4955
///

git-ref/src/store/file/transaction/commit.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ impl<'s, 'p> Transaction<'s, 'p> {
3636
match &change.update.change {
3737
// reflog first, then reference
3838
Change::Update { log, new, expected } => {
39-
let lock = change.lock.take().expect("each ref is locked");
39+
let lock = match change.lock.take() {
40+
Some(l) => l,
41+
// Some updates are never locked as they are no-ops
42+
None => continue,
43+
};
4044
let (update_ref, update_reflog) = match log.mode {
4145
RefLog::Only => (false, true),
4246
RefLog::AndReference => (true, true),
@@ -151,7 +155,7 @@ impl<'s, 'p> Transaction<'s, 'p> {
151155
Change::Delete { log: mode, .. } => *mode == RefLog::AndReference,
152156
};
153157
if take_lock_and_delete {
154-
let lock = change.lock.take().expect("lock must still be present in delete mode");
158+
let lock = change.lock.take();
155159
let reference_path = self.store.reference_path(change.update.name.as_ref());
156160
if let Err(err) = std::fs::remove_file(reference_path) {
157161
if err.kind() != std::io::ErrorKind::NotFound {

git-ref/src/store/file/transaction/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use git_hash::ObjectId;
22
use git_object::bstr::BString;
3+
use std::fmt::Formatter;
34

45
use crate::transaction::{Change, PreviousValue};
56
use crate::{
@@ -112,6 +113,15 @@ impl<'s, 'p> Transaction<'s, 'p> {
112113
}
113114
}
114115

116+
impl std::fmt::Debug for Transaction<'_, '_> {
117+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
118+
f.debug_struct("Transaction")
119+
.field("store", self.store)
120+
.field("edits", &self.updates.as_ref().map(|u| u.len()))
121+
.finish_non_exhaustive()
122+
}
123+
}
124+
115125
///
116126
pub mod prepare;
117127

git-ref/src/store/file/transaction/prepare.rs

Lines changed: 54 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ impl<'s, 'p> Transaction<'s, 'p> {
1818
lock_fail_mode: git_lock::acquire::Fail,
1919
packed: Option<&packed::Buffer>,
2020
change: &mut Edit,
21+
has_global_lock: bool,
22+
direct_to_packed_refs: bool,
2123
) -> Result<(), Error> {
2224
use std::io::Write;
2325
assert!(
@@ -94,19 +96,22 @@ impl<'s, 'p> Transaction<'s, 'p> {
9496
*expected = PreviousValue::MustExistAndMatch(existing.target);
9597
}
9698

97-
lock
99+
Some(lock)
98100
}
99101
Change::Update { expected, new, .. } => {
100102
let (base, relative_path) = store.reference_path_with_base(change.update.name.as_ref());
101-
let mut lock = git_lock::File::acquire_to_update_resource(
102-
base.join(relative_path),
103-
lock_fail_mode,
104-
Some(base.into_owned()),
105-
)
106-
.map_err(|err| Error::LockAcquire {
107-
source: err,
108-
full_name: "borrowchk won't allow change.name() and this will be corrected by caller".into(),
109-
})?;
103+
let obtain_lock = || {
104+
git_lock::File::acquire_to_update_resource(
105+
base.join(relative_path.as_ref()),
106+
lock_fail_mode,
107+
Some(base.clone().into_owned()),
108+
)
109+
.map_err(|err| Error::LockAcquire {
110+
source: err,
111+
full_name: "borrowchk won't allow change.name() and this will be corrected by caller".into(),
112+
})
113+
};
114+
let mut lock = (!has_global_lock).then(|| obtain_lock()).transpose()?;
110115

111116
let existing_ref = existing_ref?;
112117
match (&expected, &existing_ref) {
@@ -158,17 +163,20 @@ impl<'s, 'p> Transaction<'s, 'p> {
158163
true
159164
};
160165

161-
if is_effective {
166+
if is_effective && !direct_to_packed_refs {
167+
let mut lock = lock.take().map(Ok).unwrap_or_else(obtain_lock)?;
168+
162169
lock.with_mut(|file| match new {
163170
Target::Peeled(oid) => write!(file, "{}", oid),
164171
Target::Symbolic(name) => write!(file, "ref: {}", name.0),
165172
})?;
173+
Some(lock.close()?)
174+
} else {
175+
None
166176
}
167-
168-
lock.close()?
169177
}
170178
};
171-
change.lock = Some(lock);
179+
change.lock = lock;
172180
Ok(())
173181
}
174182
}
@@ -219,7 +227,10 @@ impl<'s, 'p> Transaction<'s, 'p> {
219227
| PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(_) => Some(0_usize),
220228
PackedRefs::DeletionsOnly => None,
221229
};
222-
if maybe_updates_for_packed_refs.is_some() || self.store.packed_refs_path().is_file() {
230+
if maybe_updates_for_packed_refs.is_some()
231+
|| self.store.packed_refs_path().is_file()
232+
|| self.store.packed_refs_lock_path().is_file()
233+
{
223234
let mut edits_for_packed_transaction = Vec::<RefEdit>::new();
224235
let mut needs_packed_refs_lookups = false;
225236
for edit in updates.iter() {
@@ -271,28 +282,29 @@ impl<'s, 'p> Transaction<'s, 'p> {
271282
// What follows means that we will only create a transaction if we have to access packed refs for looking
272283
// up current ref values, or that we definitely have a transaction if we need to make updates. Otherwise
273284
// we may have no transaction at all which isn't required if we had none and would only try making deletions.
274-
let packed_transaction: Option<_> = if maybe_updates_for_packed_refs.unwrap_or(0) > 0 {
275-
// We have to create a packed-ref even if it doesn't exist
276-
self.store
277-
.packed_transaction(packed_refs_lock_fail_mode)
278-
.map_err(|err| match err {
279-
file::packed::transaction::Error::BufferOpen(err) => Error::from(err),
280-
file::packed::transaction::Error::TransactionLock(err) => {
281-
Error::PackedTransactionAcquire(err)
282-
}
283-
})?
284-
.into()
285-
} else {
286-
// A packed transaction is optional - we only have deletions that can't be made if
287-
// no packed-ref file exists anyway
288-
self.store
289-
.assure_packed_refs_uptodate()?
290-
.map(|p| {
291-
buffer_into_transaction(p, packed_refs_lock_fail_mode)
292-
.map_err(Error::PackedTransactionAcquire)
293-
})
294-
.transpose()?
295-
};
285+
let packed_transaction: Option<_> =
286+
if maybe_updates_for_packed_refs.unwrap_or(0) > 0 || self.store.packed_refs_lock_path().is_file() {
287+
// We have to create a packed-ref even if it doesn't exist
288+
self.store
289+
.packed_transaction(packed_refs_lock_fail_mode)
290+
.map_err(|err| match err {
291+
file::packed::transaction::Error::BufferOpen(err) => Error::from(err),
292+
file::packed::transaction::Error::TransactionLock(err) => {
293+
Error::PackedTransactionAcquire(err)
294+
}
295+
})?
296+
.into()
297+
} else {
298+
// A packed transaction is optional - we only have deletions that can't be made if
299+
// no packed-ref file exists anyway
300+
self.store
301+
.assure_packed_refs_uptodate()?
302+
.map(|p| {
303+
buffer_into_transaction(p, packed_refs_lock_fail_mode)
304+
.map_err(Error::PackedTransactionAcquire)
305+
})
306+
.transpose()?
307+
};
296308
if let Some(transaction) = packed_transaction {
297309
self.packed_transaction = Some(match &mut self.packed_refs {
298310
PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(f)
@@ -315,6 +327,11 @@ impl<'s, 'p> Transaction<'s, 'p> {
315327
ref_files_lock_fail_mode,
316328
self.packed_transaction.as_ref().and_then(|t| t.buffer()),
317329
change,
330+
self.packed_transaction.is_some(),
331+
matches!(
332+
self.packed_refs,
333+
PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(_)
334+
),
318335
) {
319336
let err = match err {
320337
Error::LockAcquire {

git-ref/src/store/packed/transaction.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::fmt::Formatter;
12
use std::io::Write;
23

34
use crate::{
@@ -24,6 +25,15 @@ impl packed::Transaction {
2425
}
2526
}
2627

28+
impl std::fmt::Debug for packed::Transaction {
29+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
30+
f.debug_struct("packed::Transaction")
31+
.field("edits", &self.edits.as_ref().map(|e| e.len()))
32+
.field("lock", &self.lock)
33+
.finish_non_exhaustive()
34+
}
35+
}
36+
2737
/// Access
2838
impl packed::Transaction {
2939
/// Returns our packed buffer

git-ref/tests/file/transaction/prepare_and_commit/create_or_update.rs

Lines changed: 111 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,20 @@ use crate::file::{
2222
};
2323

2424
mod collisions {
25-
use crate::file::transaction::prepare_and_commit::{committer, create_at, empty_store};
25+
use crate::file::transaction::prepare_and_commit::{committer, create_at, delete_at, empty_store};
2626
use git_lock::acquire::Fail;
2727
use git_ref::file::transaction::PackedRefs;
28+
use git_ref::transaction::{Change, LogChange, PreviousValue, RefEdit};
29+
use git_ref::Target;
30+
use std::convert::TryInto;
2831

2932
fn case_sensitive(tmp_dir: &std::path::Path) -> bool {
3033
std::fs::write(tmp_dir.join("config"), "").expect("can create file once");
3134
!git_worktree::fs::Capabilities::probe(tmp_dir).ignore_case
3235
}
3336

3437
#[test]
35-
fn conflicting_creation_without_packedrefs() -> crate::Result {
38+
fn conflicting_creation_without_packed_refs() -> crate::Result {
3639
let (dir, store) = empty_store()?;
3740
let res = store.transaction().prepare(
3841
[create_at("refs/a"), create_at("refs/A")],
@@ -57,33 +60,131 @@ mod collisions {
5760
}
5861

5962
#[test]
60-
fn conflicting_creation_into_packed_refs() -> crate::Result {
63+
fn non_conflicting_creation_without_packed_refs_work() -> crate::Result {
6164
let (_dir, store) = empty_store()?;
65+
let ongoing = store
66+
.transaction()
67+
.prepare([create_at("refs/new")], Fail::Immediately, Fail::Immediately)
68+
.unwrap();
69+
70+
let t2 = store.transaction().prepare(
71+
[create_at("refs/non-conflicting")],
72+
Fail::Immediately,
73+
Fail::Immediately,
74+
)?;
75+
76+
t2.commit(committer().to_ref())?;
77+
ongoing.commit(committer().to_ref())?;
78+
79+
Ok(())
80+
}
81+
82+
#[test]
83+
fn packed_refs_lock_is_mandatory_for_multiple_ongoing_transactions_even_if_one_does_not_need_it() -> crate::Result {
84+
let (_dir, store) = empty_store()?;
85+
let ref_name = "refs/a";
86+
let _t1 = store
87+
.transaction()
88+
.packed_refs(PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(
89+
Box::new(|_, _| Ok(Some(git_object::Kind::Commit))),
90+
))
91+
.prepare([create_at(ref_name)], Fail::Immediately, Fail::Immediately)?;
92+
93+
let t2res = store
94+
.transaction()
95+
.prepare([delete_at(ref_name)], Fail::Immediately, Fail::Immediately);
96+
assert_eq!(&t2res.unwrap_err().to_string()[..51], "The lock for the packed-ref file could not be obtai", "if packed-refs are about to be created, other transactions always acquire a packed-refs lock as to not miss anything");
97+
Ok(())
98+
}
99+
100+
#[test]
101+
fn conflicting_creation_into_packed_refs() {
102+
let (_dir, store) = empty_store().unwrap();
62103
store
63104
.transaction()
64105
.packed_refs(PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(
65106
Box::new(|_, _| Ok(Some(git_object::Kind::Commit))),
66107
))
67108
.prepare(
68-
[create_at("refs/a"), create_at("refs/Ab")],
109+
[create_at("refs/a"), create_at("refs/A")],
69110
Fail::Immediately,
70111
Fail::Immediately,
71-
)?
72-
.commit(committer().to_ref())?;
112+
)
113+
.unwrap()
114+
.commit(committer().to_ref())
115+
.unwrap();
73116

74117
assert_eq!(
75-
store.cached_packed_buffer()?.expect("created").iter()?.count(),
118+
store
119+
.cached_packed_buffer()
120+
.unwrap()
121+
.expect("created")
122+
.iter()
123+
.unwrap()
124+
.count(),
76125
2,
77126
"packed-refs can store everything in case-insensitive manner"
78127
);
79-
80128
assert_eq!(
81-
store.loose_iter()?.count(),
129+
store.loose_iter().unwrap().count(),
82130
0,
83131
"refs/ directory isn't present as there is no loose ref - it removed every up to the base dir"
84132
);
85133

86-
Ok(())
134+
// The following works because locks aren't actually obtained if there would be no change.
135+
store
136+
.transaction()
137+
.packed_refs(PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(
138+
Box::new(|_, _| Ok(Some(git_object::Kind::Commit))),
139+
))
140+
.prepare(
141+
[
142+
RefEdit {
143+
change: Change::Update {
144+
log: LogChange::default(),
145+
expected: PreviousValue::Any,
146+
new: Target::Peeled(git_hash::Kind::Sha1.null()),
147+
},
148+
name: "refs/a".try_into().expect("valid"),
149+
deref: false,
150+
},
151+
RefEdit {
152+
change: Change::Update {
153+
log: LogChange::default(),
154+
expected: PreviousValue::MustExistAndMatch(Target::Peeled(git_hash::Kind::Sha1.null())),
155+
new: Target::Peeled(git_hash::Kind::Sha1.null()),
156+
},
157+
name: "refs/A".try_into().expect("valid"),
158+
deref: false,
159+
},
160+
],
161+
Fail::Immediately,
162+
Fail::Immediately,
163+
)
164+
.unwrap()
165+
.commit(committer().to_ref())
166+
.unwrap();
167+
168+
{
169+
let _ongoing = store
170+
.transaction()
171+
.prepare([create_at("refs/new")], Fail::Immediately, Fail::Immediately)
172+
.unwrap();
173+
174+
let t2res = store.transaction().prepare(
175+
[create_at("refs/non-conflicting")],
176+
Fail::Immediately,
177+
Fail::Immediately,
178+
);
179+
180+
assert_eq!(
181+
&t2res.unwrap_err().to_string()[..40],
182+
"The lock for the packed-ref file could n",
183+
"packed-refs files will always be locked if they are present as we have to look up their content"
184+
);
185+
}
186+
187+
// TODO: parallel deletion
87188
}
88189
}
89190

0 commit comments

Comments
 (0)