Skip to content

Commit 584b705

Browse files
committed
fix: assure symrefs don't get deleted when moving refs to packed-refs. (#595)
Previously it was possible for symbolic refs to be deleted right after they have been created or updated as they were included in the set of refs that was assumed to be part of packed-refs, which isn't the case for symbolic refs.
1 parent 130d13b commit 584b705

File tree

6 files changed

+44
-29
lines changed

6 files changed

+44
-29
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ impl<'s, 'p> Transaction<'s, 'p> {
7979
// Don't do anything else while keeping the lock after potentially updating the reflog.
8080
// We delay deletion of the reference and dropping the lock to after the packed-refs were
8181
// safely written.
82-
if delete_loose_refs {
82+
if delete_loose_refs && matches!(new, Target::Peeled(_)) {
8383
change.lock = lock;
8484
continue;
8585
}
@@ -145,8 +145,9 @@ impl<'s, 'p> Transaction<'s, 'p> {
145145
let take_lock_and_delete = match &change.update.change {
146146
Change::Update {
147147
log: LogChange { mode, .. },
148+
new,
148149
..
149-
} => delete_loose_refs && *mode == RefLog::AndReference,
150+
} => delete_loose_refs && *mode == RefLog::AndReference && matches!(new, Target::Peeled(_)),
150151
Change::Delete { log: mode, .. } => *mode == RefLog::AndReference,
151152
};
152153
if take_lock_and_delete {

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

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ use std::fmt::Formatter;
55
use crate::{
66
store_impl::{file, file::Transaction},
77
transaction::RefEdit,
8-
Target,
98
};
109

1110
/// A function receiving an object id to resolve, returning its decompressed bytes,
@@ -26,6 +25,7 @@ pub enum PackedRefs<'a> {
2625
DeletionsAndNonSymbolicUpdates(Box<FindObjectFn<'a>>),
2726
/// Propagate deletions as well as updates to references which are peeled, that is contain an object id. Furthermore delete the
2827
/// reference which is originally updated if it exists. If it doesn't, the new value will be written into the packed ref right away.
28+
/// Note that this doesn't affect symbolic references at all, which can't be placed into packed refs.
2929
DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(Box<FindObjectFn<'a>>),
3030
}
3131

@@ -53,14 +53,6 @@ impl Edit {
5353
}
5454
}
5555

56-
fn new_would_change_existing(new: &Target, existing: &Target) -> bool {
57-
match (new, existing) {
58-
(Target::Peeled(new), Target::Peeled(old)) => old != new,
59-
(Target::Symbolic(new), Target::Symbolic(old)) => old != new,
60-
(_, _) => true,
61-
}
62-
}
63-
6456
impl std::borrow::Borrow<RefEdit> for Edit {
6557
fn borrow(&self) -> &RefEdit {
6658
&self.update

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ use crate::{
1212
FullName, FullNameRef, Reference, Target,
1313
};
1414

15+
use crate::{packed::transaction::buffer_into_transaction, transaction::PreviousValue};
16+
1517
impl<'s, 'p> Transaction<'s, 'p> {
1618
fn lock_ref_and_apply_change(
1719
store: &file::Store,
@@ -161,15 +163,24 @@ impl<'s, 'p> Transaction<'s, 'p> {
161163
}
162164
};
163165

164-
let is_effective = if let Some(existing) = existing_ref {
165-
let effective = new_would_change_existing(new, &existing.target);
166+
fn new_would_change_existing(new: &Target, existing: &Target) -> (bool, bool) {
167+
match (new, existing) {
168+
(Target::Peeled(new), Target::Peeled(old)) => (old != new, false),
169+
(Target::Symbolic(new), Target::Symbolic(old)) => (old != new, true),
170+
(Target::Peeled(_), _) => (true, false),
171+
(Target::Symbolic(_), _) => (true, true),
172+
}
173+
}
174+
175+
let (is_effective, is_symbolic) = if let Some(existing) = existing_ref {
176+
let (effective, is_symbolic) = new_would_change_existing(new, &existing.target);
166177
*expected = PreviousValue::MustExistAndMatch(existing.target);
167-
effective
178+
(effective, is_symbolic)
168179
} else {
169-
true
180+
(true, matches!(new, Target::Symbolic(_)))
170181
};
171182

172-
if is_effective && !direct_to_packed_refs {
183+
if (is_effective && !direct_to_packed_refs) || is_symbolic {
173184
let mut lock = lock.take().map(Ok).unwrap_or_else(obtain_lock)?;
174185

175186
lock.with_mut(|file| match new {
@@ -466,6 +477,3 @@ mod error {
466477
}
467478

468479
pub use error::Error;
469-
470-
use crate::file::transaction::new_would_change_existing;
471-
use crate::{packed::transaction::buffer_into_transaction, transaction::PreviousValue};

git-ref/tests/file/store/iter.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ mod with_namespace {
99
use git_object::bstr::{BString, ByteSlice};
1010

1111
use crate::file::store_at;
12+
use crate::file::transaction::prepare_and_commit::empty_store;
13+
14+
#[test]
15+
fn missing_refs_dir_yields_empty_iteration() -> crate::Result {
16+
let (_dir, store) = empty_store()?;
17+
assert_eq!(store.iter()?.all()?.count(), 0);
18+
assert_eq!(store.loose_iter()?.count(), 0);
19+
Ok(())
20+
}
1221

1322
#[test]
1423
fn iteration_can_trivially_use_namespaces_as_prefixes() -> crate::Result {

git-ref/tests/file/transaction/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub(crate) mod prepare_and_commit {
1717
Ok(res)
1818
}
1919

20-
fn empty_store() -> crate::Result<(tempfile::TempDir, file::Store)> {
20+
pub(crate) fn empty_store() -> crate::Result<(tempfile::TempDir, file::Store)> {
2121
let dir = tempfile::TempDir::new().unwrap();
2222
let store = file::Store::at(dir.path(), git_ref::store::WriteReflog::Normal, git_hash::Kind::Sha1);
2323
Ok((dir, store))

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::file::transaction::prepare_and_commit::{committer, create_at, delete_at, empty_store};
1+
use crate::file::transaction::prepare_and_commit::{committer, create_at, create_symbolic_at, delete_at, empty_store};
22
use git_lock::acquire::Fail;
33
use git_ref::file::transaction::PackedRefs;
44
use git_ref::transaction::{Change, LogChange, PreviousValue, RefEdit};
@@ -89,7 +89,11 @@ fn conflicting_creation_into_packed_refs() -> crate::Result {
8989
Box::new(|_, _| Ok(Some(git_object::Kind::Commit))),
9090
))
9191
.prepare(
92-
[create_at("refs/a"), create_at("refs/A")],
92+
[
93+
create_at("refs/a"),
94+
create_at("refs/A"),
95+
create_symbolic_at("refs/symbolic", "refs/heads/target"),
96+
],
9397
Fail::Immediately,
9498
Fail::Immediately,
9599
)?
@@ -102,11 +106,12 @@ fn conflicting_creation_into_packed_refs() -> crate::Result {
102106
);
103107
assert_eq!(
104108
store.loose_iter()?.count(),
105-
0,
106-
"refs/ directory isn't present as there is no loose ref - it removed every up to the base dir"
109+
1,
110+
"symbolic refs can't be packed and stay loose"
107111
);
108112
assert!(store.reflog_exists("refs/a")?);
109113
assert!(store.reflog_exists("refs/A")?);
114+
assert!(!store.reflog_exists("refs/symbolic")?, "and they can't have reflogs");
110115

111116
// The following works because locks aren't actually obtained if there would be no change.
112117
// Otherwise there would be a conflict on case-insensitive filesystems
@@ -142,7 +147,7 @@ fn conflicting_creation_into_packed_refs() -> crate::Result {
142147
Fail::Immediately,
143148
)?
144149
.commit(committer().to_ref())?;
145-
assert_eq!(store.iter()?.all()?.count(), 2);
150+
assert_eq!(store.iter()?.all()?.count(), 3);
146151

147152
{
148153
let _ongoing = store
@@ -179,7 +184,7 @@ fn conflicting_creation_into_packed_refs() -> crate::Result {
179184
}
180185

181186
// Create a loose ref at a path
182-
assert_eq!(store.loose_iter()?.count(), 0);
187+
assert_eq!(store.loose_iter()?.count(), 1, "a symref");
183188
store
184189
.transaction()
185190
.prepare(
@@ -198,14 +203,14 @@ fn conflicting_creation_into_packed_refs() -> crate::Result {
198203
.commit(committer().to_ref())?;
199204
assert_eq!(
200205
store.loose_iter()?.count(),
201-
1,
202-
"we created a loose ref, overlaying the packed one"
206+
2,
207+
"we created a loose ref, overlaying the packed one, and have a symbolic one"
203208
);
204209

205210
store
206211
.transaction()
207212
.prepare(
208-
[delete_at("refs/a"), delete_at("refs/A")],
213+
[delete_at("refs/a"), delete_at("refs/A"), delete_at("refs/symbolic")],
209214
Fail::Immediately,
210215
Fail::Immediately,
211216
)?

0 commit comments

Comments
 (0)