1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
4
5use sley_config::GitConfig;
6use sley_core::{GitError, ObjectFormat, ObjectId, Result};
7use sley_formats::{
8 Reftable, ReftableLogRecord, ReftableLogUpdate, ReftableLogValue, ReftableRefRecord,
9 ReftableRefValue,
10};
11use std::borrow::Borrow;
12use std::collections::{BTreeMap, BTreeSet, HashMap};
13use std::env;
14use std::fmt;
15use std::fs;
16use std::io::Write;
17use std::ops::Deref;
18use std::path::{Path, PathBuf};
19use std::thread;
20use std::time::Duration;
21use std::time::{SystemTime, UNIX_EPOCH};
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum RefTarget {
25 Direct(ObjectId),
26 Symbolic(String),
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct Ref {
31 pub name: String,
32 pub target: RefTarget,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct RefDelete {
37 pub name: String,
38 pub oid: ObjectId,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct DeleteRef {
43 pub name: String,
44 pub expected_old: Option<ObjectId>,
45 pub reflog: Option<DeleteRefReflog>,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct DeleteRefReflog {
50 pub committer: Vec<u8>,
51 pub message: Vec<u8>,
52}
53
54#[derive(Debug)]
55pub enum RefDeleteError {
56 NotFound,
57 ExpectedMismatch {
58 expected: Option<ObjectId>,
59 actual: Option<ObjectId>,
60 },
61 Locked,
62 InvalidName,
63 Io(std::io::Error),
64}
65
66impl fmt::Display for RefDeleteError {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 match self {
69 Self::NotFound => f.write_str("ref not found"),
70 Self::ExpectedMismatch { expected, actual } => {
71 write!(
72 f,
73 "ref expected old oid mismatch: expected {:?}, actual {:?}",
74 expected, actual
75 )
76 }
77 Self::Locked => f.write_str("ref is locked"),
78 Self::InvalidName => f.write_str("invalid ref name"),
79 Self::Io(err) => write!(f, "io error: {err}"),
80 }
81 }
82}
83
84impl std::error::Error for RefDeleteError {}
85
86impl From<std::io::Error> for RefDeleteError {
87 fn from(value: std::io::Error) -> Self {
88 Self::Io(value)
89 }
90}
91
92fn parse_leading_oid(format: ObjectFormat, value: &str) -> Option<ObjectId> {
95 let hexsz = format.hex_len();
96 let bytes = value.as_bytes();
97 if bytes.len() < hexsz {
98 return None;
99 }
100 if bytes.len() > hexsz && !bytes[hexsz].is_ascii_whitespace() {
101 return None;
102 }
103 ObjectId::from_hex(format, &value[..hexsz]).ok()
104}
105
106fn loose_ref_bytes_are_reftable_sentinel(name: &str, bytes: &[u8]) -> bool {
107 name == "refs/heads" && bytes == b"this repository uses the reftable format\n"
108}
109
110pub fn parse_loose_ref(format: ObjectFormat, name: impl Into<String>, bytes: &[u8]) -> Result<Ref> {
111 let name = name.into();
112 let value = std::str::from_utf8(bytes)
113 .map_err(|err| GitError::InvalidFormat(err.to_string()))?
114 .trim_end_matches('\n');
115 if name == "FETCH_HEAD" {
116 let oid = value
117 .lines()
118 .find_map(|line| line.split_whitespace().next())
119 .ok_or_else(|| GitError::InvalidFormat("FETCH_HEAD is empty".into()))?;
120 return Ok(Ref {
121 name,
122 target: RefTarget::Direct(ObjectId::from_hex(format, oid)?),
123 });
124 }
125 let target = if let Some(symbolic) = value.strip_prefix("ref: ") {
126 RefTarget::Symbolic(symbolic.to_string())
127 } else {
128 let oid = parse_leading_oid(format, value).ok_or_else(|| {
133 GitError::InvalidFormat(format!(
134 "reference {name} has neither a valid OID nor a target"
135 ))
136 })?;
137 RefTarget::Direct(oid)
138 };
139 Ok(Ref { name, target })
140}
141
142pub fn write_loose_ref(reference: &Ref) -> Vec<u8> {
143 match &reference.target {
144 RefTarget::Direct(oid) => format!("{oid}\n").into_bytes(),
145 RefTarget::Symbolic(target) => format!("ref: {target}\n").into_bytes(),
146 }
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct PackedRef {
151 pub reference: Ref,
152 pub peeled: Option<ObjectId>,
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum PackRefDecision {
157 Pack { peeled: Option<ObjectId> },
158 Skip,
159}
160
161pub fn parse_packed_refs(format: ObjectFormat, bytes: &[u8]) -> Result<Vec<PackedRef>> {
162 parse_packed_refs_filtered(format, bytes, |_| true)
163}
164
165fn parse_packed_refs_with_prefix(
166 format: ObjectFormat,
167 bytes: &[u8],
168 prefix: &str,
169) -> Result<Vec<PackedRef>> {
170 if !bytes.is_empty() && !bytes.ends_with(b"\n") {
171 let line_start = bytes
172 .iter()
173 .rposition(|byte| *byte == b'\n')
174 .map_or(0, |index| index + 1);
175 let line = String::from_utf8_lossy(&bytes[line_start..]);
176 return Err(GitError::InvalidFormat(format!(
177 "fatal: unterminated line in .git/packed-refs: {line}"
178 )));
179 }
180 let text =
181 std::str::from_utf8(bytes).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
182 let mut refs: Vec<PackedRef> = Vec::new();
183 let mut saw_ref = false;
184 let mut included_last_ref = false;
185 for raw_line in text.lines() {
186 let line = raw_line.trim_end();
187 if line.is_empty() || line.starts_with('#') {
188 continue;
189 }
190 if let Some(peeled) = line.strip_prefix('^') {
191 if !saw_ref {
192 return Err(GitError::InvalidFormat(
193 "peeled packed ref without preceding ref".into(),
194 ));
195 }
196 if included_last_ref {
197 let oid = ObjectId::from_hex(format, peeled)?;
198 if let Some(last) = refs.last_mut() {
199 last.peeled = Some(oid);
200 }
201 }
202 continue;
203 }
204 let (oid, name) = line
205 .split_once(' ')
206 .ok_or_else(|| packed_refs_unexpected_line(line))?;
207 if oid.len() != format.hex_len()
214 || !oid.bytes().all(|byte| byte.is_ascii_hexdigit())
215 || validate_ref_name_for_read(name).is_err()
216 {
217 return Err(packed_refs_unexpected_line(line));
218 }
219 saw_ref = true;
220 included_last_ref = name.starts_with(prefix);
221 if !included_last_ref {
222 continue;
223 }
224 let oid = ObjectId::from_hex(format, oid)?;
225 refs.push(PackedRef {
226 reference: Ref {
227 name: name.into(),
228 target: RefTarget::Direct(oid),
229 },
230 peeled: None,
231 });
232 }
233 Ok(refs)
234}
235
236fn parse_packed_refs_filtered(
237 format: ObjectFormat,
238 bytes: &[u8],
239 mut include: impl FnMut(&str) -> bool,
240) -> Result<Vec<PackedRef>> {
241 if !bytes.is_empty() && !bytes.ends_with(b"\n") {
242 let line_start = bytes
243 .iter()
244 .rposition(|byte| *byte == b'\n')
245 .map_or(0, |index| index + 1);
246 let line = String::from_utf8_lossy(&bytes[line_start..]);
247 return Err(GitError::InvalidFormat(format!(
248 "fatal: unterminated line in .git/packed-refs: {line}"
249 )));
250 }
251 let text =
252 std::str::from_utf8(bytes).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
253 let mut refs: Vec<PackedRef> = Vec::new();
254 let mut saw_ref = false;
255 let mut included_last_ref = false;
256 for raw_line in text.lines() {
257 let line = raw_line.trim_end();
258 if line.is_empty() || line.starts_with('#') {
259 continue;
260 }
261 if let Some(peeled) = line.strip_prefix('^') {
262 let oid = ObjectId::from_hex(format, peeled)?;
263 if !saw_ref {
264 return Err(GitError::InvalidFormat(
265 "peeled packed ref without preceding ref".into(),
266 ));
267 }
268 if included_last_ref && let Some(last) = refs.last_mut() {
269 last.peeled = Some(oid);
270 }
271 continue;
272 }
273 let (oid, name) = line
274 .split_once(' ')
275 .ok_or_else(|| packed_refs_unexpected_line(line))?;
276 if oid.len() != format.hex_len()
277 || !oid.bytes().all(|byte| byte.is_ascii_hexdigit())
278 || validate_ref_name_for_read(name).is_err()
279 {
280 return Err(packed_refs_unexpected_line(line));
281 }
282 let oid = ObjectId::from_hex(format, oid)?;
283 saw_ref = true;
284 included_last_ref = include(name);
285 if included_last_ref {
286 refs.push(PackedRef {
287 reference: Ref {
288 name: name.into(),
289 target: RefTarget::Direct(oid),
290 },
291 peeled: None,
292 });
293 }
294 }
295 Ok(refs)
296}
297
298fn packed_ref_names_with_prefix(
299 format: ObjectFormat,
300 bytes: &[u8],
301 prefix: &str,
302 strip_prefix: bool,
303 names: &mut Vec<String>,
304) -> Result<()> {
305 if !bytes.is_empty() && !bytes.ends_with(b"\n") {
306 let line_start = bytes
307 .iter()
308 .rposition(|byte| *byte == b'\n')
309 .map_or(0, |index| index + 1);
310 let line = String::from_utf8_lossy(&bytes[line_start..]);
311 return Err(GitError::InvalidFormat(format!(
312 "fatal: unterminated line in .git/packed-refs: {line}"
313 )));
314 }
315 let text =
316 std::str::from_utf8(bytes).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
317 let mut saw_ref = false;
318 for raw_line in text.lines() {
319 let line = raw_line.trim_end();
320 if line.is_empty() || line.starts_with('#') {
321 continue;
322 }
323 if line.starts_with('^') {
324 if !saw_ref {
325 return Err(GitError::InvalidFormat(
326 "peeled packed ref without preceding ref".into(),
327 ));
328 }
329 continue;
330 }
331 let (oid, name) = line
332 .split_once(' ')
333 .ok_or_else(|| packed_refs_unexpected_line(line))?;
334 if oid.len() != format.hex_len()
338 || !oid.bytes().all(|byte| byte.is_ascii_hexdigit())
339 || validate_ref_name_for_read(name).is_err()
340 {
341 return Err(packed_refs_unexpected_line(line));
342 }
343 saw_ref = true;
344 if !name.starts_with(prefix) {
345 continue;
346 }
347 let name = if strip_prefix {
348 &name[prefix.len()..]
349 } else {
350 name
351 };
352 names.push(name.to_owned());
353 }
354 Ok(())
355}
356
357fn packed_refs_unexpected_line(line: &str) -> GitError {
358 GitError::InvalidFormat(format!(
359 "fatal: unexpected line in .git/packed-refs: {line}"
360 ))
361}
362
363fn packed_refs_have_prefix(format: ObjectFormat, bytes: &[u8], prefix: &str) -> Result<bool> {
364 if !bytes.is_empty() && !bytes.ends_with(b"\n") {
365 let line_start = bytes
366 .iter()
367 .rposition(|byte| *byte == b'\n')
368 .map_or(0, |index| index + 1);
369 let line = String::from_utf8_lossy(&bytes[line_start..]);
370 return Err(GitError::InvalidFormat(format!(
371 "fatal: unterminated line in .git/packed-refs: {line}"
372 )));
373 }
374 let text =
375 std::str::from_utf8(bytes).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
376 let mut found = false;
377 let mut saw_ref = false;
378 for raw_line in text.lines() {
379 let line = raw_line.trim_end();
380 if line.is_empty() || line.starts_with('#') {
381 continue;
382 }
383 if let Some(peeled) = line.strip_prefix('^') {
384 ObjectId::from_hex(format, peeled)?;
385 if !saw_ref {
386 return Err(GitError::InvalidFormat(
387 "peeled packed ref without preceding ref".into(),
388 ));
389 }
390 continue;
391 }
392 let (oid, name) = line
393 .split_once(' ')
394 .ok_or_else(|| packed_refs_unexpected_line(line))?;
395 if oid.len() != format.hex_len()
396 || !oid.bytes().all(|byte| byte.is_ascii_hexdigit())
397 || validate_ref_name_for_read(name).is_err()
398 {
399 return Err(packed_refs_unexpected_line(line));
400 }
401 ObjectId::from_hex(format, oid)?;
402 saw_ref = true;
403 found |= name.starts_with(prefix);
404 }
405 Ok(found)
406}
407
408pub fn write_packed_refs(refs: &[PackedRef]) -> Result<Vec<u8>> {
409 let mut refs = refs.to_vec();
410 refs.sort_by(|left, right| left.reference.name.cmp(&right.reference.name));
411 let mut out = b"# pack-refs with: peeled fully-peeled sorted \n".to_vec();
412 for packed in refs {
413 validate_ref_name(&packed.reference.name)?;
414 let RefTarget::Direct(oid) = &packed.reference.target else {
415 return Err(GitError::InvalidFormat(format!(
416 "packed ref {} is symbolic",
417 packed.reference.name
418 )));
419 };
420 out.extend_from_slice(oid.to_hex().as_bytes());
421 out.push(b' ');
422 out.extend_from_slice(packed.reference.name.as_bytes());
423 out.push(b'\n');
424 if let Some(peeled) = packed.peeled {
425 out.push(b'^');
426 out.extend_from_slice(peeled.to_hex().as_bytes());
427 out.push(b'\n');
428 }
429 }
430 Ok(out)
431}
432
433#[derive(Debug, Clone, PartialEq, Eq)]
434pub struct ReflogEntry {
435 pub old_oid: ObjectId,
436 pub new_oid: ObjectId,
437 pub committer: Vec<u8>,
438 pub message: Vec<u8>,
439}
440
441impl ReflogEntry {
442 pub fn to_line(&self) -> Vec<u8> {
443 let mut out = Vec::new();
444 out.extend_from_slice(self.old_oid.to_hex().as_bytes());
445 out.push(b' ');
446 out.extend_from_slice(self.new_oid.to_hex().as_bytes());
447 out.push(b' ');
448 out.extend_from_slice(&self.committer);
449 if !self.message.is_empty() {
450 out.push(b'\t');
451 out.extend_from_slice(&self.message);
452 }
453 out.push(b'\n');
454 out
455 }
456
457 pub fn timestamp_seconds(&self) -> Result<i64> {
458 let committer = std::str::from_utf8(&self.committer)
459 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
460 let Some((before_tz, _tz)) = committer.rsplit_once(' ') else {
461 return Err(GitError::InvalidFormat(
462 "reflog committer is missing timezone".into(),
463 ));
464 };
465 let Some((_identity, timestamp)) = before_tz.rsplit_once(' ') else {
466 return Err(GitError::InvalidFormat(
467 "reflog committer is missing timestamp".into(),
468 ));
469 };
470 timestamp
471 .parse::<i64>()
472 .map_err(|err| GitError::InvalidFormat(err.to_string()))
473 }
474}
475
476pub fn parse_reflog(format: ObjectFormat, bytes: &[u8]) -> Result<Vec<ReflogEntry>> {
477 let text =
478 std::str::from_utf8(bytes).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
479 let mut entries = Vec::new();
480 for line in text.lines() {
481 let mut parts = line.splitn(3, ' ');
482 let old = parts
483 .next()
484 .ok_or_else(|| GitError::InvalidFormat("missing reflog old oid".into()))?;
485 let new = parts
486 .next()
487 .ok_or_else(|| GitError::InvalidFormat("missing reflog new oid".into()))?;
488 let rest = parts
489 .next()
490 .ok_or_else(|| GitError::InvalidFormat("missing reflog committer".into()))?;
491 let (committer, message) = rest.split_once('\t').unwrap_or((rest, ""));
492 entries.push(ReflogEntry {
493 old_oid: ObjectId::from_hex(format, old)?,
494 new_oid: ObjectId::from_hex(format, new)?,
495 committer: committer.as_bytes().to_vec(),
496 message: message.as_bytes().to_vec(),
497 });
498 }
499 Ok(entries)
500}
501
502pub fn expire_reflog(
520 entries: &[ReflogEntry],
521 cutoff_unix: i64,
522 expire_unreachable_cutoff: Option<i64>,
523 is_reachable: impl Fn(&ObjectId) -> bool,
524) -> Result<Vec<ReflogEntry>> {
525 let last_index = entries.len().checked_sub(1);
526 let mut retained = Vec::with_capacity(entries.len());
527 for (index, entry) in entries.iter().enumerate() {
528 if Some(index) == last_index {
531 retained.push(entry.clone());
532 continue;
533 }
534 let timestamp = entry.timestamp_seconds()?;
535 let mut expired = timestamp < cutoff_unix;
536 if let Some(unreachable_cutoff) = expire_unreachable_cutoff
537 && !is_reachable(&entry.new_oid)
538 {
539 expired = expired || timestamp < unreachable_cutoff;
540 }
541 if !expired {
542 retained.push(entry.clone());
543 }
544 }
545 Ok(retained)
546}
547
548#[derive(Debug, Default, Clone)]
549pub struct RefStore {
550 refs: HashMap<String, RefTarget>,
551 reflogs: BTreeMap<String, Vec<ReflogEntry>>,
552}
553
554impl RefStore {
555 pub fn new() -> Self {
556 Self::default()
557 }
558
559 pub fn get(&self, name: &str) -> Option<&RefTarget> {
560 self.refs.get(name)
561 }
562
563 pub fn transaction(&mut self) -> RefTransaction<'_> {
564 RefTransaction {
565 store: self,
566 updates: Vec::new(),
567 }
568 }
569
570 pub fn reflog(&self, name: &str) -> &[ReflogEntry] {
571 self.reflogs
572 .get(name)
573 .map(Vec::as_slice)
574 .unwrap_or_default()
575 }
576}
577
578#[derive(Debug)]
579pub struct RefUpdate {
580 pub name: String,
581 pub expected: Option<RefTarget>,
582 pub new: RefTarget,
583 pub reflog: Option<ReflogEntry>,
584}
585
586#[derive(Debug, Clone, PartialEq, Eq)]
594pub enum RefPrecondition {
595 Any,
597 MustExist,
599 MustNotExist,
601 MustExistAndMatch(RefTarget),
603 ExistingMustMatch(RefTarget),
606}
607
608impl RefPrecondition {
609 fn from_expected(expected: Option<RefTarget>) -> Self {
611 match expected {
612 None => Self::Any,
613 Some(target) => Self::MustExistAndMatch(target),
614 }
615 }
616
617 fn is_satisfied_by(&self, current: Option<&RefTarget>) -> bool {
620 match self {
621 Self::Any => true,
622 Self::MustExist => current.is_some(),
623 Self::MustNotExist => current.is_none(),
624 Self::MustExistAndMatch(target) => current == Some(target),
625 Self::ExistingMustMatch(target) => match current {
626 None => true,
627 Some(current) => current == target,
628 },
629 }
630 }
631
632 fn describe(&self, name: &str) -> String {
634 match self {
635 Self::Any => format!("ref {name} precondition not met"),
636 Self::MustExist => format!("expected ref {name} to exist"),
637 Self::MustNotExist => format!("expected ref {name} to not already exist"),
638 Self::MustExistAndMatch(_) => format!("expected ref {name} to match"),
639 Self::ExistingMustMatch(_) => {
640 format!("expected ref {name} to match its current value")
641 }
642 }
643 }
644}
645
646pub struct RefTransaction<'a> {
647 store: &'a mut RefStore,
648 updates: Vec<RefUpdate>,
649}
650
651impl<'a> RefTransaction<'a> {
652 pub fn update(&mut self, update: RefUpdate) {
653 self.updates.push(update);
654 }
655
656 pub fn commit(self) -> Result<()> {
657 for update in &self.updates {
658 if let Some(expected) = &update.expected
659 && self.store.refs.get(&update.name) != Some(expected)
660 {
661 return Err(GitError::Transaction(format!(
662 "expected ref {} to match",
663 update.name
664 )));
665 }
666 }
667 for update in self.updates {
668 self.store.refs.insert(update.name.clone(), update.new);
669 if let Some(entry) = update.reflog {
670 self.store
671 .reflogs
672 .entry(update.name)
673 .or_default()
674 .push(entry);
675 }
676 }
677 Ok(())
678 }
679}
680
681#[derive(Debug, Clone)]
682pub struct FileRefStore {
683 git_dir: PathBuf,
684 common_dir: PathBuf,
685 storage_dir: PathBuf,
686 format: ObjectFormat,
687 honor_reference_backend_env: bool,
688 reftable_lock_timeout_millis: Option<u64>,
689 combine_reftable_logs: bool,
690}
691
692#[derive(Debug, Clone, PartialEq, Eq)]
693pub struct BranchCreate {
694 pub name: String,
695 pub oid: ObjectId,
696}
697
698#[derive(Debug, Clone, PartialEq, Eq)]
699pub struct BranchDelete {
700 pub name: String,
701 pub oid: ObjectId,
702}
703
704#[derive(Debug, Clone, PartialEq, Eq)]
705pub struct TagCreate {
706 pub name: String,
707 pub oid: ObjectId,
708}
709
710#[derive(Debug, Clone, PartialEq, Eq)]
711pub struct TagDelete {
712 pub name: String,
713 pub oid: ObjectId,
714}
715
716#[derive(Debug, Clone, PartialEq, Eq)]
717pub struct BundleRefUpdate {
718 pub name: String,
719 pub oid: ObjectId,
720}
721
722#[derive(Debug, Clone, PartialEq, Eq)]
723pub struct BundleRefUpdateReflog {
724 pub committer: Vec<u8>,
725 pub message: Vec<u8>,
726}
727
728#[derive(Debug, Clone, PartialEq, Eq)]
729pub struct AppliedBundleRefUpdate {
730 pub name: String,
731 pub old_oid: Option<ObjectId>,
732 pub new_oid: ObjectId,
733}
734
735fn configured_ref_storage_backend(
736 common_dir: &Path,
737 honor_env: bool,
738) -> Option<(RefBackendKind, Option<PathBuf>)> {
739 if honor_env
740 && let Ok(value) = env::var("GIT_REFERENCE_BACKEND")
741 && let Ok((kind, path)) = parse_ref_storage_backend_value(&value)
742 {
743 return Some((
744 kind,
745 path.map(|path| ref_storage_path_from_config(common_dir, path)),
746 ));
747 }
748 let config = GitConfig::read(common_dir.join("config")).ok()?;
749 let value = config.get("extensions", None, "refStorage")?;
750 parse_ref_storage_backend_value(value)
751 .ok()
752 .map(|(kind, path)| {
753 (
754 kind,
755 path.map(|path| ref_storage_path_from_config(common_dir, path)),
756 )
757 })
758}
759
760fn ref_storage_path_from_config(common_dir: &Path, path: PathBuf) -> PathBuf {
761 if path.is_absolute() {
762 path
763 } else {
764 common_dir.join(path)
765 }
766}
767
768fn parse_ref_storage_backend_value(value: &str) -> Result<(RefBackendKind, Option<PathBuf>)> {
769 let (backend, path) = if let Some((backend, path)) = value.split_once("://") {
770 if path.is_empty() {
771 return Err(GitError::InvalidFormat(format!(
772 "invalid value for 'extensions.refstorage': '{value}'"
773 )));
774 }
775 (backend, Some(PathBuf::from(path)))
776 } else {
777 (value, None)
778 };
779 let kind = match backend {
780 "files" => RefBackendKind::Files,
781 "reftable" => RefBackendKind::Reftable,
782 _ => {
783 return Err(GitError::InvalidFormat(format!(
784 "invalid value for 'extensions.refstorage': '{value}'"
785 )));
786 }
787 };
788 Ok((kind, path))
789}
790
791#[derive(Debug, Clone, Copy, PartialEq, Eq)]
792enum RefBackendKind {
793 Files,
794 Reftable,
795}
796
797impl FileRefStore {
798 pub fn new(git_dir: impl Into<PathBuf>, format: ObjectFormat) -> Self {
799 Self::new_with_reference_backend_env(git_dir, format, true)
800 }
801
802 pub fn new_without_reference_backend_env(
803 git_dir: impl Into<PathBuf>,
804 format: ObjectFormat,
805 ) -> Self {
806 Self::new_with_reference_backend_env(git_dir, format, false)
807 }
808
809 fn new_with_reference_backend_env(
810 git_dir: impl Into<PathBuf>,
811 format: ObjectFormat,
812 honor_reference_backend_env: bool,
813 ) -> Self {
814 let git_dir = git_dir.into();
815 let common_dir = repository_common_dir(&git_dir);
816 let configured = configured_ref_storage_backend(&common_dir, honor_reference_backend_env);
817 let storage_dir = match configured {
818 Some((_, Some(path))) => path,
819 Some((RefBackendKind::Reftable, None)) if git_dir != common_dir => git_dir.clone(),
820 _ => common_dir.clone(),
821 };
822 Self {
823 git_dir,
824 common_dir,
825 storage_dir,
826 format,
827 honor_reference_backend_env,
828 reftable_lock_timeout_millis: None,
829 combine_reftable_logs: false,
830 }
831 }
832
833 pub fn with_reftable_lock_timeout_millis(mut self, timeout_millis: Option<u64>) -> Self {
834 self.reftable_lock_timeout_millis = timeout_millis;
835 self
836 }
837
838 pub fn with_reftable_combined_logs(mut self, combine: bool) -> Self {
839 self.combine_reftable_logs = combine;
840 self
841 }
842
843 pub fn read_ref(&self, name: &str) -> Result<Option<RefTarget>> {
844 validate_ref_name_for_read(name)?;
845 self.read_ref_unchecked(name)
846 }
847
848 fn read_ref_unchecked(&self, name: &str) -> Result<Option<RefTarget>> {
849 if self.uses_reftable()? {
850 let (store, name) = self.reftable_store_for_ref(name)?;
851 if let Some(target) = store.read_reftable_ref(&name)? {
852 return Ok(Some(target));
853 }
854 if name != "HEAD" && is_root_ref_syntax(&name) {
855 return Ok(self
856 .read_loose_ref(&name)?
857 .map(|reference| reference.target));
858 }
859 return Ok(None);
860 }
861 if let Some(reference) = self.read_loose_ref(name)? {
862 return Ok(Some(reference.target));
863 }
864 if let Some(reference) = self.read_packed_ref(name)? {
865 return Ok(Some(reference.reference.target));
866 }
867 Ok(None)
868 }
869
870 pub fn raw_ref_exists(&self, name: &str) -> Result<bool> {
880 if self.uses_reftable()? {
881 let (store, name) = self.reftable_store_for_ref(name)?;
882 if store.read_reftable_ref(&name)?.is_some() {
883 return Ok(true);
884 }
885 if name != "HEAD" && is_root_ref_syntax(&name) {
886 return Ok(self.read_loose_ref(&name)?.is_some());
887 }
888 return Ok(false);
889 }
890 let base = if current_worktree_ref(name) {
894 self.current_worktree_storage_dir()
895 } else {
896 self.storage_dir.clone()
897 };
898 let path = base.join(name);
899 match fs::symlink_metadata(&path) {
900 Ok(meta) if meta.is_dir() => {
901 Ok(self.read_packed_ref(name)?.is_some())
904 }
905 Ok(_) => Ok(true),
906 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
907 Ok(self.read_packed_ref(name)?.is_some())
908 }
909 Err(err) => Err(err.into()),
910 }
911 }
912
913 pub fn read_reflog(&self, name: &str) -> Result<Vec<ReflogEntry>> {
914 validate_ref_name_for_read(name)?;
915 if self.uses_reftable()? {
916 return self.read_reftable_logs(name);
917 }
918 let path = self.reflog_path(name);
919 if !path.exists() {
920 return Ok(Vec::new());
921 }
922 parse_reflog(self.format, &fs::read(path)?)
923 }
924
925 pub fn reflog_exists(&self, name: &str) -> Result<bool> {
926 validate_ref_name_for_read(name)?;
927 if self.uses_reftable()? {
928 return self.reftable_log_exists(name);
929 }
930 Ok(self.reflog_path(name).exists())
931 }
932
933 pub fn should_write_reflog_for_update(&self, name: &str, create_reflog: bool) -> Result<bool> {
934 validate_ref_name_for_read(name)?;
935 let reflog_exists = if self.uses_reftable()? {
936 !self.read_reftable_logs(name)?.is_empty()
937 } else {
938 self.reflog_path(name).exists()
939 };
940 if create_reflog || reflog_exists {
941 return Ok(true);
942 }
943 let Ok(config) = GitConfig::read(self.common_dir.join("config")) else {
944 return Ok(false);
945 };
946 if let Some(value) = config.get("core", None, "logAllRefUpdates") {
947 return Ok(log_all_ref_updates_matches(name, value));
948 }
949 if config.get_bool("core", None, "bare").unwrap_or(false) {
950 return Ok(false);
951 }
952 Ok(log_all_ref_updates_matches(name, "true"))
953 }
954
955 pub fn write_reflog(&self, name: &str, entries: &[ReflogEntry]) -> Result<()> {
956 validate_ref_name_for_read(name)?;
957 if self.uses_reftable()? {
958 return self.rewrite_reftable_logs(name, entries, true);
959 }
960 let path = self.reflog_path(name);
961 let parent = path
962 .parent()
963 .ok_or_else(|| GitError::InvalidPath("reflog path has no parent".into()))?;
964 fs::create_dir_all(parent)?;
965 let mut bytes = Vec::new();
966 for entry in entries {
967 bytes.extend_from_slice(&entry.to_line());
968 }
969 write_locked(&path, &bytes)
970 }
971
972 pub fn import_snapshot(
973 &self,
974 refs: &[Ref],
975 reflogs: &[(String, Vec<ReflogEntry>)],
976 pack_files_refs: bool,
977 ) -> Result<()> {
978 if self.uses_reftable()? {
979 let ref_records = refs
980 .iter()
981 .map(|reference| ReftableRefRecord {
982 name: reference.name.clone(),
983 update_index: 0,
984 value: reftable_value_from_ref_target(&reference.target),
985 })
986 .collect::<Vec<_>>();
987 let mut log_records = Vec::new();
988 let mut update_index = 1u64;
989 for (name, entries) in reflogs {
990 for entry in entries {
991 log_records.push(ReftableLogRecord {
992 refname: name.clone(),
993 update_index,
994 value: ReftableLogValue::Update(reftable_update_from_reflog(entry)?),
995 });
996 update_index = update_index.checked_add(1).ok_or_else(|| {
997 GitError::InvalidFormat("reftable update index overflow".into())
998 })?;
999 }
1000 }
1001 if !ref_records.is_empty() || !log_records.is_empty() {
1002 self.append_reftable_table_spanning(ref_records, log_records)?;
1003 }
1004 return Ok(());
1005 }
1006
1007 let mut tx = self.transaction();
1008 for reference in refs {
1009 tx.update(RefUpdate {
1010 name: reference.name.clone(),
1011 expected: None,
1012 new: reference.target.clone(),
1013 reflog: None,
1014 });
1015 }
1016 tx.commit()?;
1017 for (name, entries) in reflogs {
1018 self.write_reflog(name, entries)?;
1019 }
1020 if pack_files_refs {
1021 let _ = self.pack_refs(true)?;
1022 }
1023 Ok(())
1024 }
1025
1026 pub fn expire_reflog_older_than(&self, name: &str, cutoff_seconds: i64) -> Result<usize> {
1027 validate_ref_name_for_read(name)?;
1028 if self.uses_reftable()? {
1029 let entries = self.read_reftable_logs(name)?;
1030 let original_len = entries.len();
1031 let mut retained = Vec::new();
1032 for entry in entries {
1033 if entry.timestamp_seconds()? >= cutoff_seconds {
1034 retained.push(entry);
1035 }
1036 }
1037 let removed = original_len - retained.len();
1038 if removed > 0 {
1039 self.rewrite_reftable_logs(name, &retained, true)?;
1040 }
1041 return Ok(removed);
1042 }
1043 let path = self.reflog_path(name);
1044 if !path.exists() {
1045 return Ok(0);
1046 }
1047 let entries = parse_reflog(self.format, &fs::read(&path)?)?;
1048 let original_len = entries.len();
1049 let mut retained = Vec::new();
1050 for entry in entries {
1051 if entry.timestamp_seconds()? >= cutoff_seconds {
1052 retained.push(entry);
1053 }
1054 }
1055 let mut bytes = Vec::new();
1056 for entry in &retained {
1057 bytes.extend_from_slice(&entry.to_line());
1058 }
1059 write_locked(&path, &bytes)?;
1060 Ok(original_len - retained.len())
1061 }
1062
1063 pub fn expire_reflog_file(
1074 &self,
1075 name: &str,
1076 cutoff_unix: i64,
1077 expire_unreachable_cutoff: Option<i64>,
1078 write: bool,
1079 is_reachable: impl Fn(&ObjectId) -> bool,
1080 ) -> Result<usize> {
1081 validate_ref_name(name)?;
1082 if self.uses_reftable()? {
1083 let entries = self.read_reftable_logs(name)?;
1084 let original_len = entries.len();
1085 let retained = expire_reflog(
1086 &entries,
1087 cutoff_unix,
1088 expire_unreachable_cutoff,
1089 is_reachable,
1090 )?;
1091 let removed = original_len - retained.len();
1092 if write && removed > 0 {
1093 self.rewrite_reftable_logs(name, &retained, true)?;
1094 }
1095 return Ok(removed);
1096 }
1097 let path = self.reflog_path(name);
1098 if !path.exists() {
1099 return Ok(0);
1100 }
1101 let entries = parse_reflog(self.format, &fs::read(&path)?)?;
1102 let original_len = entries.len();
1103 let retained = expire_reflog(
1104 &entries,
1105 cutoff_unix,
1106 expire_unreachable_cutoff,
1107 is_reachable,
1108 )?;
1109 let removed = original_len - retained.len();
1110 if write && removed > 0 {
1111 let mut bytes = Vec::new();
1112 for entry in &retained {
1113 bytes.extend_from_slice(&entry.to_line());
1114 }
1115 write_locked(&path, &bytes)?;
1116 }
1117 Ok(removed)
1118 }
1119
1120 pub fn list_refs(&self) -> Result<Vec<Ref>> {
1121 self.list_refs_with_prefix("refs/")
1122 }
1123
1124 pub fn list_refs_with_prefix(&self, prefix: &str) -> Result<Vec<Ref>> {
1125 if self.uses_reftable()? {
1126 let mut refs = BTreeMap::<String, Ref>::new();
1127 for reference in self
1128 .reftable_store_with_storage(self.shared_reftable_storage_dir())
1129 .list_reftable_refs_with_prefix(prefix)?
1130 {
1131 refs.insert(reference.name.clone(), reference);
1132 }
1133 if self.git_dir != self.common_dir {
1134 for reference in self.list_reftable_refs_with_prefix(prefix)? {
1135 if reftable_current_worktree_ref(&reference.name) {
1136 refs.insert(reference.name.clone(), reference);
1137 }
1138 }
1139 }
1140 return Ok(refs.into_values().collect());
1141 }
1142 let mut refs = Vec::new();
1143 let packed_path = self.storage_dir.join("packed-refs");
1144 if packed_path.exists() {
1145 for packed in
1146 parse_packed_refs_with_prefix(self.format, &fs::read(packed_path)?, prefix)?
1147 {
1148 refs.push(packed.reference);
1149 }
1150 }
1151 let mut loose_refs = BTreeMap::new();
1152 self.collect_loose_refs_with_prefix(prefix, &mut loose_refs)?;
1153 self.collect_current_worktree_loose_refs_with_prefix(prefix, &mut loose_refs)?;
1154 if !loose_refs.is_empty() {
1155 refs.retain(|reference| !loose_refs.contains_key(&reference.name));
1156 refs.extend(loose_refs.into_values());
1157 }
1158 refs.retain(|reference| reference.name.starts_with(prefix));
1159 refs.retain(|reference| {
1160 if validate_ref_name(&reference.name).is_ok() {
1161 true
1162 } else {
1163 warn_broken_ref_name(&reference.name);
1164 false
1165 }
1166 });
1167 refs.sort_by(|left, right| left.name.cmp(&right.name));
1168 Ok(refs)
1169 }
1170
1171 pub fn list_ref_names_with_prefix(&self, prefix: &str) -> Result<Vec<String>> {
1172 if self.uses_reftable()? {
1173 return Ok(self
1174 .list_refs_with_prefix(prefix)?
1175 .into_iter()
1176 .map(|reference| reference.name)
1177 .collect());
1178 }
1179 let mut names = Vec::new();
1180 let packed_path = self.storage_dir.join("packed-refs");
1181 if packed_path.exists() {
1182 packed_ref_names_with_prefix(
1183 self.format,
1184 &fs::read(packed_path)?,
1185 prefix,
1186 false,
1187 &mut names,
1188 )?;
1189 }
1190 let mut loose_names = BTreeSet::new();
1191 self.collect_loose_ref_names_with_prefix(prefix, &mut loose_names)?;
1192 self.collect_current_worktree_loose_ref_names_with_prefix(prefix, &mut loose_names)?;
1193 if !loose_names.is_empty() {
1194 names.retain(|name| !loose_names.contains(name));
1195 names.extend(loose_names);
1196 }
1197 names.retain(|name| name.starts_with(prefix));
1198 names.retain(|name| {
1199 if validate_ref_name(name).is_ok() {
1200 true
1201 } else {
1202 warn_broken_ref_name(name);
1203 false
1204 }
1205 });
1206 names.sort();
1207 names.dedup();
1208 Ok(names)
1209 }
1210
1211 pub fn list_short_ref_names_with_prefix(&self, prefix: &str) -> Result<Vec<String>> {
1212 if self.uses_reftable()? {
1213 let mut names = self
1214 .list_reftable_refs_with_prefix(prefix)?
1215 .into_iter()
1216 .filter_map(|reference| reference.name.strip_prefix(prefix).map(str::to_owned))
1217 .collect::<Vec<_>>();
1218 names.sort();
1219 return Ok(names);
1220 }
1221 let mut names = Vec::new();
1222 let packed_path = self.storage_dir.join("packed-refs");
1223 if packed_path.exists() {
1224 packed_ref_names_with_prefix(
1225 self.format,
1226 &fs::read(packed_path)?,
1227 prefix,
1228 true,
1229 &mut names,
1230 )?;
1231 }
1232 let mut loose_full_names = BTreeSet::new();
1233 self.collect_loose_ref_names_with_prefix(prefix, &mut loose_full_names)?;
1234 self.collect_current_worktree_loose_ref_names_with_prefix(prefix, &mut loose_full_names)?;
1235 let loose_names = loose_full_names
1236 .into_iter()
1237 .filter_map(|name| {
1238 if validate_ref_name(&name).is_ok() {
1239 name.strip_prefix(prefix).map(str::to_owned)
1240 } else {
1241 warn_broken_ref_name(&name);
1242 None
1243 }
1244 })
1245 .collect::<BTreeSet<_>>();
1246 if !loose_names.is_empty() {
1247 names.retain(|name| !loose_names.contains(name));
1248 names.extend(loose_names);
1249 }
1250 names.sort();
1251 names.dedup();
1252 Ok(names)
1253 }
1254
1255 pub fn list_all_refs(&self) -> Result<Vec<Ref>> {
1256 let mut refs = self.list_refs()?;
1257 let mut seen = refs
1258 .iter()
1259 .map(|reference| reference.name.clone())
1260 .collect::<BTreeSet<_>>();
1261 for reference in self.list_root_refs()? {
1262 if seen.insert(reference.name.clone()) {
1263 refs.push(reference);
1264 }
1265 }
1266 refs.sort_by(|left, right| left.name.cmp(&right.name));
1267 Ok(refs)
1268 }
1269
1270 pub fn list_reflog_names(&self) -> Result<Vec<String>> {
1271 let mut names = BTreeSet::new();
1272 if self.uses_reftable()? {
1273 let null = ObjectId::null(self.format);
1274 let mut logs_by_index = BTreeMap::<(String, u64), bool>::new();
1275 for table in self.reftables()? {
1276 for record in table.logs {
1277 let has_visible_entry = match record.value {
1278 ReftableLogValue::Deletion => false,
1279 ReftableLogValue::Update(update) => {
1280 !reftable_log_update_is_empty_marker(&update, null)
1281 }
1282 };
1283 logs_by_index.insert((record.refname, record.update_index), has_visible_entry);
1284 }
1285 }
1286 for ((name, _), has_visible_entry) in logs_by_index {
1287 if has_visible_entry {
1288 names.insert(name);
1289 }
1290 }
1291 return Ok(names.into_iter().collect());
1292 }
1293 self.collect_reflog_names(&self.storage_dir.join("logs"), "logs", &mut names)?;
1294 let worktree_logs = self.git_dir.join("logs");
1295 if worktree_logs != self.storage_dir.join("logs") {
1296 self.collect_reflog_names(&worktree_logs, "logs", &mut names)?;
1297 }
1298 Ok(names.into_iter().collect())
1299 }
1300
1301 pub fn has_refs_with_prefix(&self, prefix: &str) -> Result<bool> {
1302 if self.uses_reftable()? {
1303 return Ok(self
1304 .list_reftable_refs()?
1305 .iter()
1306 .any(|reference| reference.name.starts_with(prefix)));
1307 }
1308 let packed_path = self.storage_dir.join("packed-refs");
1309 if packed_path.exists()
1310 && packed_refs_have_prefix(self.format, &fs::read(&packed_path)?, prefix)?
1311 {
1312 return Ok(true);
1313 }
1314 self.loose_refs_have_prefix(prefix)
1315 }
1316
1317 pub fn write_packed_refs(&self, refs: &[PackedRef]) -> Result<()> {
1318 self.write_packed_refs_with_timeout(refs, 0)
1319 }
1320
1321 pub fn write_packed_refs_with_timeout(
1322 &self,
1323 refs: &[PackedRef],
1324 timeout_millis: u64,
1325 ) -> Result<()> {
1326 let path = self.packed_refs_write_path()?;
1327 write_locked_with_timeout(&path, &write_packed_refs(refs)?, timeout_millis)
1328 }
1329
1330 pub fn pack_refs(&self, prune_loose: bool) -> Result<Vec<PackedRef>> {
1331 self.pack_refs_with_peeler(prune_loose, |_, _| Ok(None))
1332 }
1333
1334 pub fn pack_refs_with_peeler<F>(&self, prune_loose: bool, mut peel: F) -> Result<Vec<PackedRef>>
1335 where
1336 F: FnMut(&str, &ObjectId) -> Result<Option<ObjectId>>,
1337 {
1338 self.pack_refs_selected_with_timeout(
1339 prune_loose,
1340 false,
1341 0,
1342 |_| true,
1343 |name, oid| peel(name, oid).map(|peeled| PackRefDecision::Pack { peeled }),
1344 )
1345 }
1346
1347 pub fn pack_refs_selected_with_timeout<F, S>(
1348 &self,
1349 prune_loose: bool,
1350 auto: bool,
1351 timeout_millis: u64,
1352 mut should_pack: S,
1353 mut decide: F,
1354 ) -> Result<Vec<PackedRef>>
1355 where
1356 F: FnMut(&str, &ObjectId) -> Result<PackRefDecision>,
1357 S: FnMut(&str) -> bool,
1358 {
1359 if self.uses_reftable()? {
1360 self.compact_reftable_stack()?;
1361 return Ok(Vec::new());
1362 }
1363 let mut packed_refs = BTreeMap::new();
1364 let packed_path = self.storage_dir.join("packed-refs");
1365 if packed_path.exists() {
1366 for packed in parse_packed_refs(self.format, &fs::read(&packed_path)?)? {
1367 packed_refs.insert(packed.reference.name.clone(), packed);
1368 }
1369 }
1370
1371 let mut loose_refs = BTreeMap::new();
1372 let refs_dir = self.storage_dir.join("refs");
1373 if refs_dir.exists() {
1374 self.collect_loose_refs(&refs_dir, "refs", &mut loose_refs)?;
1375 }
1376 let loose_refs = loose_refs
1377 .into_values()
1378 .filter(|reference| packable_loose_ref_name(&reference.name))
1379 .filter(|reference| should_pack(&reference.name))
1380 .collect::<Vec<_>>();
1381 if auto && !pack_refs_auto_required_for(&packed_path, loose_refs.len())? {
1382 return Ok(packed_refs.into_values().collect());
1383 }
1384 let mut packed_loose_names = Vec::new();
1385 for reference in loose_refs {
1386 let RefTarget::Direct(oid) = reference.target else {
1387 continue;
1388 };
1389 let peeled = match decide(&reference.name, &oid)? {
1390 PackRefDecision::Pack { peeled } => peeled,
1391 PackRefDecision::Skip => continue,
1392 };
1393 packed_loose_names.push(reference.name.clone());
1394 packed_refs.insert(
1395 reference.name.clone(),
1396 PackedRef {
1397 reference: Ref {
1398 name: reference.name,
1399 target: RefTarget::Direct(oid),
1400 },
1401 peeled,
1402 },
1403 );
1404 }
1405
1406 let refs = packed_refs.into_values().collect::<Vec<_>>();
1407 self.write_packed_refs_with_timeout(&refs, timeout_millis)?;
1408 if prune_loose {
1409 for name in packed_loose_names {
1410 self.delete_loose_ref(&name)?;
1411 }
1412 }
1413 Ok(refs)
1414 }
1415
1416 pub fn pack_refs_auto_required<S>(&self, mut should_pack: S) -> Result<bool>
1417 where
1418 S: FnMut(&str) -> bool,
1419 {
1420 if self.uses_reftable()? {
1421 return Ok(true);
1422 }
1423 let mut loose_refs = BTreeMap::new();
1424 let refs_dir = self.storage_dir.join("refs");
1425 if refs_dir.exists() {
1426 self.collect_loose_refs(&refs_dir, "refs", &mut loose_refs)?;
1427 }
1428 let count = loose_refs
1429 .values()
1430 .filter(|reference| packable_loose_ref_name(&reference.name))
1431 .filter(|reference| matches!(reference.target, RefTarget::Direct(_)))
1432 .filter(|reference| should_pack(&reference.name))
1433 .count();
1434 pack_refs_auto_required_for(&self.storage_dir.join("packed-refs"), count)
1435 }
1436
1437 fn packed_refs_write_path(&self) -> Result<PathBuf> {
1438 let path = self.storage_dir.join("packed-refs");
1439 match fs::symlink_metadata(&path) {
1440 Ok(meta) if meta.file_type().is_symlink() => {
1441 let target = fs::read_link(&path)?;
1442 if target.is_absolute() {
1443 Ok(target)
1444 } else {
1445 let parent = path.parent().ok_or_else(|| {
1446 GitError::InvalidPath("packed-refs path has no parent".into())
1447 })?;
1448 Ok(parent.join(target))
1449 }
1450 }
1451 Ok(_) => Ok(path),
1452 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(path),
1453 Err(err) => Err(err.into()),
1454 }
1455 }
1456
1457 pub fn current_branch_ref(&self) -> Result<Option<String>> {
1458 match self.read_ref("HEAD")? {
1459 Some(RefTarget::Symbolic(name)) if name.starts_with("refs/heads/") => Ok(Some(name)),
1460 _ => Ok(None),
1461 }
1462 }
1463
1464 pub fn current_branch(&self) -> Result<Option<String>> {
1465 Ok(self
1466 .current_branch_ref()?
1467 .and_then(|name| name.strip_prefix("refs/heads/").map(str::to_string)))
1468 }
1469
1470 pub fn transaction(&self) -> FileRefTransaction<'_> {
1471 FileRefTransaction {
1472 store: self,
1473 changes: Vec::new(),
1474 hook: None,
1475 }
1476 }
1477
1478 pub fn uses_alternate_ref_storage(&self) -> bool {
1479 self.storage_dir != self.common_dir
1480 }
1481
1482 pub fn current_worktree_ref_storage_dir(&self) -> PathBuf {
1483 self.current_worktree_storage_dir()
1484 }
1485
1486 pub fn create_branch(
1487 &self,
1488 branch: &str,
1489 start: ObjectId,
1490 committer: Vec<u8>,
1491 message: Vec<u8>,
1492 ) -> Result<BranchCreate> {
1493 let name = branch_ref_name(branch)?;
1494 if self.read_ref(&name)?.is_some() {
1495 return Err(GitError::Transaction(format!(
1496 "branch {branch} already exists"
1497 )));
1498 }
1499 let zero = ObjectId::null(self.format);
1500 let mut tx = self.transaction();
1501 tx.update(RefUpdate {
1502 name: name.clone(),
1503 expected: None,
1504 new: RefTarget::Direct(start),
1505 reflog: Some(ReflogEntry {
1506 old_oid: zero,
1507 new_oid: start,
1508 committer,
1509 message,
1510 }),
1511 });
1512 tx.commit()?;
1513 Ok(BranchCreate { name, oid: start })
1514 }
1515
1516 pub fn delete_branch(&self, branch: &str) -> Result<BranchDelete> {
1517 let name = branch_ref_name_for_read(branch)?;
1518 if matches!(self.read_ref("HEAD")?, Some(RefTarget::Symbolic(head)) if head == name) {
1519 return Err(GitError::Transaction(format!(
1520 "cannot delete branch {branch} checked out at HEAD"
1521 )));
1522 }
1523 let oid = self.delete_direct_ref(&name, "branch", branch)?;
1524 self.remove_reflog_file(&name);
1525 Ok(BranchDelete { name, oid })
1526 }
1527
1528 pub fn move_branch(
1529 &self,
1530 old_branch: &str,
1531 new_branch: &str,
1532 force: bool,
1533 committer: Vec<u8>,
1534 ) -> Result<()> {
1535 self.copy_or_move_branch(old_branch, new_branch, force, false, committer)
1536 }
1537
1538 pub fn copy_branch(
1539 &self,
1540 old_branch: &str,
1541 new_branch: &str,
1542 force: bool,
1543 committer: Vec<u8>,
1544 ) -> Result<()> {
1545 self.copy_or_move_branch(old_branch, new_branch, force, true, committer)
1546 }
1547
1548 fn conflicting_ref_for_path(&self, new_name: &str, exclude: &str) -> Result<Option<String>> {
1555 for reference in self.list_refs()? {
1556 let name = &reference.name;
1557 if name == new_name || name == exclude {
1558 continue;
1559 }
1560 if new_name.starts_with(&format!("{name}/")) {
1562 return Ok(Some(name.clone()));
1563 }
1564 if name.starts_with(&format!("{new_name}/")) {
1566 return Ok(Some(name.clone()));
1567 }
1568 }
1569 Ok(None)
1570 }
1571
1572 fn copy_or_move_branch(
1573 &self,
1574 old_branch: &str,
1575 new_branch: &str,
1576 force: bool,
1577 copy: bool,
1578 committer: Vec<u8>,
1579 ) -> Result<()> {
1580 let old_name = branch_ref_name_for_source(old_branch)?;
1581 let new_name = branch_ref_name(new_branch)?;
1582 if old_name == new_name {
1583 return Ok(());
1584 }
1585 let Some(target) = self.read_ref(&old_name)? else {
1586 return Err(GitError::reference_not_found(format!(
1587 "branch {old_branch}"
1588 )));
1589 };
1590 let RefTarget::Direct(oid) = target else {
1591 return Err(GitError::InvalidFormat(format!(
1592 "branch {old_branch} is symbolic"
1593 )));
1594 };
1595 let conflict_exclude = if copy { "" } else { &old_name };
1603 if let Some(conflict) = self.conflicting_ref_for_path(&new_name, conflict_exclude)? {
1604 return Err(GitError::Transaction(format!(
1605 "'{conflict}' exists; cannot create '{new_name}'"
1606 )));
1607 }
1608 let dest_entry = self.read_ref(&new_name)?;
1612 let dest_resolves = resolve_ref_peeled(self, &new_name)?.is_some();
1613 if dest_resolves && !force {
1614 return Err(GitError::Transaction(format!(
1615 "branch {new_branch} already exists"
1616 )));
1617 }
1618 match dest_entry {
1622 Some(RefTarget::Symbolic(_)) => {
1623 self.delete_symbolic_ref(&new_name)?;
1624 self.remove_reflog_file(&new_name);
1625 }
1626 Some(RefTarget::Direct(_)) => {
1627 let _ = self.delete_direct_ref(&new_name, "branch", new_branch)?;
1628 self.remove_reflog_file(&new_name);
1629 }
1630 None => {}
1631 }
1632
1633 let mut reflog = self.read_reflog(&old_name)?;
1636 reflog.push(ReflogEntry {
1637 old_oid: oid,
1638 new_oid: oid,
1639 committer,
1640 message: if copy {
1641 format!("Branch: copied {old_name} to {new_name}").into_bytes()
1642 } else {
1643 format!("Branch: renamed {old_name} to {new_name}").into_bytes()
1644 },
1645 });
1646
1647 if !copy {
1652 let _ = self.delete_direct_ref(&old_name, "branch", old_branch)?;
1653 self.remove_reflog_file(&old_name);
1654 }
1655
1656 self.write_loose_ref(&Ref {
1657 name: new_name.clone(),
1658 target: RefTarget::Direct(oid),
1659 })?;
1660 self.write_reflog(&new_name, &reflog)?;
1661
1662 if !copy
1663 && matches!(self.read_ref("HEAD")?, Some(RefTarget::Symbolic(head)) if head == old_name)
1664 {
1665 self.write_loose_ref(&Ref {
1666 name: "HEAD".into(),
1667 target: RefTarget::Symbolic(new_name),
1668 })?;
1669 }
1670 Ok(())
1671 }
1672
1673 pub fn create_tag(&self, tag: &str, target: ObjectId) -> Result<TagCreate> {
1674 let name = tag_ref_name(tag)?;
1675 if self.read_ref(&name)?.is_some() {
1676 return Err(GitError::Transaction(format!("tag {tag} already exists")));
1677 }
1678 let mut tx = self.transaction();
1679 tx.update(RefUpdate {
1680 name: name.clone(),
1681 expected: None,
1682 new: RefTarget::Direct(target),
1683 reflog: None,
1684 });
1685 tx.commit()?;
1686 Ok(TagCreate { name, oid: target })
1687 }
1688
1689 pub fn apply_bundle_ref_updates(
1690 &self,
1691 refs: &[BundleRefUpdate],
1692 reflog: Option<BundleRefUpdateReflog>,
1693 ) -> Result<Vec<AppliedBundleRefUpdate>> {
1694 let (updates, applied) = prepare_bundle_ref_updates(refs, reflog.as_ref(), |name, oid| {
1695 if oid.format() != self.format {
1696 return Err(GitError::InvalidObjectId(format!(
1697 "bundle ref {name} has {} object id for {} repository",
1698 oid.format().name(),
1699 self.format.name()
1700 )));
1701 }
1702 self.read_ref(name)
1703 })?;
1704 let mut tx = self.transaction();
1705 for update in updates {
1706 tx.update(update);
1707 }
1708 tx.commit()?;
1709 Ok(applied)
1710 }
1711
1712 pub fn delete_tag(&self, tag: &str) -> Result<TagDelete> {
1713 let name = TagRefNameBuf::from_tag_name_unrestricted(tag)?.into_string();
1714 let oid = self.delete_direct_ref(&name, "tag", tag)?;
1715 self.remove_reflog_file(&name);
1716 Ok(TagDelete { name, oid })
1717 }
1718
1719 pub fn delete_ref(&self, name: &str) -> Result<RefDelete> {
1720 validate_ref_name_for_read(name)?;
1721 let oid = self.delete_direct_ref(name, "ref", name)?;
1722 self.remove_reflog_file(name);
1723 Ok(RefDelete {
1724 name: name.into(),
1725 oid,
1726 })
1727 }
1728
1729 pub fn delete_ref_checked(
1730 &self,
1731 delete: DeleteRef,
1732 ) -> std::result::Result<RefDelete, RefDeleteError> {
1733 validate_ref_name_for_read(&delete.name).map_err(|_| RefDeleteError::InvalidName)?;
1734 if self.uses_reftable().map_err(ref_delete_error_from_git)? {
1735 return self.delete_reftable_ref_checked(delete);
1736 }
1737 self.delete_files_ref_checked(delete)
1738 }
1739
1740 pub fn delete_symbolic_ref(&self, name: &str) -> Result<bool> {
1741 validate_ref_name_for_read(name)?;
1742 if self.uses_reftable()? {
1743 let Some(target) = self.read_ref(name)? else {
1744 return Ok(false);
1745 };
1746 if !matches!(target, RefTarget::Symbolic(_)) {
1747 return Ok(false);
1748 }
1749 self.append_reftable_records(vec![ReftableRefRecord {
1750 name: name.to_string(),
1751 update_index: 0,
1752 value: ReftableRefValue::Deletion,
1753 }])?;
1754 self.remove_reflog_file(name);
1755 return Ok(true);
1756 }
1757 let Some(reference) = self.read_loose_ref(name)? else {
1758 return Ok(false);
1759 };
1760 if !matches!(reference.target, RefTarget::Symbolic(_)) {
1761 return Ok(false);
1762 }
1763 self.delete_loose_ref(name)?;
1764 self.remove_reflog_file(name);
1765 Ok(true)
1766 }
1767
1768 fn delete_direct_ref(&self, name: &str, kind: &str, short_name: &str) -> Result<ObjectId> {
1769 if self.uses_reftable()? {
1770 let Some(target) = self.read_ref(name)? else {
1771 return Err(GitError::reference_not_found(format!(
1772 "{kind} {short_name}"
1773 )));
1774 };
1775 let RefTarget::Direct(oid) = target else {
1776 return Err(GitError::InvalidFormat(format!(
1777 "{kind} {short_name} is symbolic"
1778 )));
1779 };
1780 self.append_reftable_records(vec![ReftableRefRecord {
1781 name: name.to_string(),
1782 update_index: 0,
1783 value: ReftableRefValue::Deletion,
1784 }])?;
1785 self.remove_reflog_file(name);
1788 return Ok(oid);
1789 }
1790 let Some(reference) = self.read_loose_ref(name)? else {
1791 return self.delete_packed_ref(name, kind, short_name);
1792 };
1793 let oid = match reference.target {
1794 RefTarget::Direct(oid) => oid,
1795 RefTarget::Symbolic(target) => {
1796 return Err(GitError::InvalidFormat(format!(
1797 "{kind} {short_name} is symbolic to {target}"
1798 )));
1799 }
1800 };
1801 self.delete_loose_ref(name)?;
1802 match self.delete_packed_ref(name, kind, short_name) {
1808 Ok(_) | Err(GitError::NotFound(_)) => {}
1809 Err(err) => return Err(err),
1810 }
1811 Ok(oid)
1812 }
1813
1814 fn delete_packed_ref(&self, name: &str, kind: &str, short_name: &str) -> Result<ObjectId> {
1815 let path = self.storage_dir.join("packed-refs");
1816 if !path.exists() {
1817 return Err(GitError::reference_not_found(format!(
1818 "{kind} {short_name}"
1819 )));
1820 }
1821 let mut refs = parse_packed_refs(self.format, &fs::read(&path)?)?;
1822 let Some(index) = refs
1823 .iter()
1824 .position(|reference| reference.reference.name == name)
1825 else {
1826 return Err(GitError::reference_not_found(format!(
1827 "{kind} {short_name}"
1828 )));
1829 };
1830 let removed = refs.remove(index);
1831 let RefTarget::Direct(oid) = removed.reference.target else {
1832 return Err(GitError::InvalidFormat(format!(
1833 "{kind} {short_name} is symbolic"
1834 )));
1835 };
1836 self.write_packed_refs(&refs)?;
1837 Ok(oid)
1838 }
1839
1840 fn delete_reftable_ref_checked(
1841 &self,
1842 delete: DeleteRef,
1843 ) -> std::result::Result<RefDelete, RefDeleteError> {
1844 let target = self
1845 .read_ref(&delete.name)
1846 .map_err(ref_delete_error_from_git)?;
1847 let oid = checked_delete_oid(delete.expected_old, target)?;
1848 self.append_reftable_records(vec![ReftableRefRecord {
1849 name: delete.name.clone(),
1850 update_index: 0,
1851 value: ReftableRefValue::Deletion,
1852 }])
1853 .map_err(ref_delete_error_from_git)?;
1854 self.remove_reflog_file(&delete.name);
1857 Ok(RefDelete {
1858 name: delete.name,
1859 oid,
1860 })
1861 }
1862
1863 fn delete_files_ref_checked(
1864 &self,
1865 delete: DeleteRef,
1866 ) -> std::result::Result<RefDelete, RefDeleteError> {
1867 let name = delete.name;
1868 let path = self.ref_path(&name);
1869 let parent = path.parent().ok_or(RefDeleteError::InvalidName)?;
1870 fs::create_dir_all(parent).map_err(RefDeleteError::from)?;
1871
1872 let loose_lock_path = lock_path_for(&path).map_err(|_| RefDeleteError::InvalidName)?;
1873 let _prune_guard = RefDirPruneGuard {
1874 store: self,
1875 name: name.clone(),
1876 };
1877 let loose_lock = DeleteLock::acquire(loose_lock_path)?;
1878
1879 let packed_path = self.storage_dir.join("packed-refs");
1880 let packed_lock_path =
1881 lock_path_for(&packed_path).map_err(|_| RefDeleteError::InvalidName)?;
1882 let mut packed_lock = DeleteLock::acquire(packed_lock_path)?;
1883
1884 let loose_ref = self
1885 .read_loose_ref(&name)
1886 .map_err(ref_delete_error_from_git)?;
1887 let packed_original = match fs::read(&packed_path) {
1888 Ok(bytes) => Some(bytes),
1889 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
1890 Err(err) => return Err(RefDeleteError::Io(err)),
1891 };
1892 let mut packed_refs = match &packed_original {
1893 Some(bytes) => {
1894 parse_packed_refs(self.format, bytes).map_err(ref_delete_error_from_git)?
1895 }
1896 None => Vec::new(),
1897 };
1898 let packed_index = packed_refs
1899 .iter()
1900 .position(|reference| reference.reference.name == name);
1901
1902 let current = if let Some(reference) = loose_ref.as_ref() {
1903 Some(reference.target.clone())
1904 } else {
1905 packed_index.map(|index| packed_refs[index].reference.target.clone())
1906 };
1907 let oid = checked_delete_oid(delete.expected_old, current)?;
1908
1909 let packed_changed = if let Some(index) = packed_index {
1910 packed_refs.remove(index);
1911 true
1912 } else {
1913 false
1914 };
1915
1916 if packed_changed {
1917 let packed_bytes =
1918 write_packed_refs(&packed_refs).map_err(ref_delete_error_from_git)?;
1919 packed_lock.write_all(&packed_bytes)?;
1920 let lock_path = packed_lock.close();
1921 if let Err(err) = fs::rename(&lock_path, &packed_path) {
1922 let _ = fs::remove_file(&lock_path);
1923 return Err(RefDeleteError::Io(err));
1924 }
1925 } else {
1926 packed_lock.remove();
1927 }
1928
1929 if loose_ref.is_some()
1930 && let Err(err) = fs::remove_file(&path)
1931 {
1932 if packed_changed && let Some(bytes) = packed_original.as_ref() {
1933 let _ = restore_file_atomically(&packed_path, bytes);
1934 }
1935 return Err(RefDeleteError::Io(err));
1936 }
1937 loose_lock.remove();
1938
1939 self.remove_reflog_file(&name);
1942 Ok(RefDelete { name, oid })
1943 }
1944
1945 fn read_loose_ref(&self, name: &str) -> Result<Option<Ref>> {
1946 let path = self.ref_path(name);
1947 let metadata = match fs::symlink_metadata(&path) {
1948 Ok(metadata) => metadata,
1949 Err(err)
1950 if err.kind() == std::io::ErrorKind::NotFound
1951 || err.raw_os_error() == Some(20) =>
1952 {
1953 return Ok(None);
1954 }
1955 Err(err) => return Err(err.into()),
1956 };
1957 if metadata.is_dir() {
1958 return Ok(None);
1959 }
1960 if metadata.file_type().is_symlink()
1961 && let Ok(target) = fs::read_link(&path)
1962 {
1963 let target = target.to_string_lossy();
1964 if target.starts_with("refs/") {
1965 return Ok(Some(Ref {
1966 name: name.to_string(),
1967 target: RefTarget::Symbolic(target.into_owned()),
1968 }));
1969 }
1970 }
1971 let bytes = fs::read(path)?;
1972 if loose_ref_bytes_are_reftable_sentinel(name, &bytes) {
1973 return Ok(None);
1974 }
1975 Ok(Some(parse_loose_ref(self.format, name, &bytes)?))
1976 }
1977
1978 fn read_packed_ref(&self, name: &str) -> Result<Option<PackedRef>> {
1979 let path = self.storage_dir.join("packed-refs");
1980 if !path.exists() {
1981 return Ok(None);
1982 }
1983 Ok(parse_packed_refs(self.format, &fs::read(path)?)?
1984 .into_iter()
1985 .find(|reference| reference.reference.name == name))
1986 }
1987
1988 fn read_reftable_ref(&self, name: &str) -> Result<Option<RefTarget>> {
1989 for table in self.reftables()?.into_iter().rev() {
1990 if let Some(record) = table.refs.into_iter().find(|record| record.name == name) {
1991 return reftable_ref_target(record.value);
1992 }
1993 }
1994 Ok(None)
1995 }
1996
1997 fn list_reftable_refs(&self) -> Result<Vec<Ref>> {
1998 self.list_reftable_refs_with_prefix("refs/")
1999 }
2000
2001 fn list_reftable_refs_with_prefix(&self, prefix: &str) -> Result<Vec<Ref>> {
2002 let mut refs = BTreeMap::<String, Ref>::new();
2003 for table in self.reftables()? {
2004 for record in table.refs {
2005 if !record.name.starts_with("refs/") || !record.name.starts_with(prefix) {
2006 continue;
2007 }
2008 match reftable_ref_target(record.value)? {
2009 Some(target) => {
2010 refs.insert(
2011 record.name.clone(),
2012 Ref {
2013 name: record.name,
2014 target,
2015 },
2016 );
2017 }
2018 None => {
2019 refs.remove(&record.name);
2020 }
2021 }
2022 }
2023 }
2024 Ok(refs.into_values().collect())
2025 }
2026
2027 fn list_root_refs(&self) -> Result<Vec<Ref>> {
2028 if self.uses_reftable()? {
2029 let mut refs = BTreeMap::<String, Ref>::new();
2030 for table in self.reftables()? {
2031 for record in table.refs {
2032 if record.name.starts_with("refs/") || !is_root_ref_syntax(&record.name) {
2033 continue;
2034 }
2035 match reftable_ref_target(record.value)? {
2036 Some(target) => {
2037 refs.insert(
2038 record.name.clone(),
2039 Ref {
2040 name: record.name,
2041 target,
2042 },
2043 );
2044 }
2045 None => {
2046 refs.remove(&record.name);
2047 }
2048 }
2049 }
2050 }
2051 return Ok(refs.into_values().collect());
2052 }
2053
2054 let mut refs = Vec::new();
2055 for entry in fs::read_dir(&self.git_dir)? {
2056 let entry = entry?;
2057 if !entry.file_type()?.is_file() {
2058 continue;
2059 }
2060 let name = entry.file_name().to_string_lossy().into_owned();
2061 if !is_root_ref_syntax(&name)
2062 || (name != "HEAD" && !name.ends_with("_HEAD"))
2063 || name == "FETCH_HEAD"
2064 || name == "MERGE_HEAD"
2065 {
2066 continue;
2067 }
2068 if let Ok(reference) = parse_loose_ref(self.format, name, &fs::read(entry.path())?) {
2069 refs.push(reference);
2070 }
2071 }
2072 refs.sort_by(|left, right| left.name.cmp(&right.name));
2073 Ok(refs)
2074 }
2075
2076 fn reftables(&self) -> Result<Vec<Reftable>> {
2077 let reftable_dir = self.storage_dir.join("reftable");
2078 let tables_list = reftable_dir.join("tables.list");
2079 for _ in 0..10 {
2080 let text = match fs::read_to_string(&tables_list) {
2081 Ok(text) => text,
2082 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
2083 if !tables_list.exists() {
2084 return Ok(Vec::new());
2085 }
2086 thread::sleep(Duration::from_millis(10));
2087 continue;
2088 }
2089 Err(err) => return Err(err.into()),
2090 };
2091 let mut tables = Vec::new();
2092 let mut reload = false;
2093 for raw_line in text.lines() {
2094 let line = raw_line.trim();
2095 if line.is_empty() {
2096 continue;
2097 }
2098 if line.contains('/')
2099 || line.contains('\\')
2100 || Path::new(line).components().count() != 1
2101 {
2102 return Err(GitError::InvalidPath(format!(
2103 "invalid reftable table name {line}"
2104 )));
2105 }
2106 let bytes = match fs::read(reftable_dir.join(line)) {
2107 Ok(bytes) => bytes,
2108 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
2109 reload = true;
2110 break;
2111 }
2112 Err(err) => return Err(err.into()),
2113 };
2114 let table = Reftable::parse(&bytes)?;
2115 if table.header.object_format != self.format {
2116 return Err(GitError::InvalidFormat(format!(
2117 "reftable {line} has {} object ids in {} repository",
2118 table.header.object_format.name(),
2119 self.format.name()
2120 )));
2121 }
2122 tables.push(table);
2123 }
2124 if reload {
2125 thread::sleep(Duration::from_millis(10));
2126 continue;
2127 }
2128 return Ok(tables);
2129 }
2130 Err(GitError::Io(format!(
2131 "cannot read stable reftable stack {}",
2132 tables_list.display()
2133 )))
2134 }
2135
2136 fn reftable_store_with_storage(&self, storage_dir: PathBuf) -> FileRefStore {
2137 FileRefStore {
2138 git_dir: self.git_dir.clone(),
2139 common_dir: self.common_dir.clone(),
2140 storage_dir,
2141 format: self.format,
2142 honor_reference_backend_env: self.honor_reference_backend_env,
2143 reftable_lock_timeout_millis: self.reftable_lock_timeout_millis,
2144 combine_reftable_logs: self.combine_reftable_logs,
2145 }
2146 }
2147
2148 fn shared_reftable_storage_dir(&self) -> PathBuf {
2159 match configured_ref_storage_backend(&self.common_dir, self.honor_reference_backend_env) {
2160 Some((_, Some(path))) => path,
2161 _ => self.common_dir.clone(),
2162 }
2163 }
2164
2165 fn current_worktree_storage_dir(&self) -> PathBuf {
2166 if self.storage_dir != self.common_dir {
2167 if self.git_dir != self.common_dir
2168 && let Some(worktree) = self.git_dir.file_name()
2169 {
2170 return self.storage_dir.join("worktrees").join(worktree);
2171 }
2172 return self.storage_dir.clone();
2173 }
2174 self.git_dir.clone()
2175 }
2176
2177 fn reftable_store_for_ref(&self, name: &str) -> Result<(FileRefStore, String)> {
2178 if let Some((worktree, rewritten)) = reftable_other_worktree_ref(name) {
2179 let storage_dir = self
2180 .shared_reftable_storage_dir()
2181 .join("worktrees")
2182 .join(worktree);
2183 return Ok((
2184 self.reftable_store_with_storage(storage_dir),
2185 rewritten.to_string(),
2186 ));
2187 }
2188 if reftable_current_worktree_ref(name) {
2189 return Ok((
2190 self.reftable_store_with_storage(self.current_worktree_storage_dir()),
2191 name.to_string(),
2192 ));
2193 }
2194 Ok((
2195 self.reftable_store_with_storage(self.shared_reftable_storage_dir()),
2196 name.to_string(),
2197 ))
2198 }
2199
2200 pub fn uses_reftable(&self) -> Result<bool> {
2201 if self.honor_reference_backend_env
2202 && let Ok(value) = env::var("GIT_REFERENCE_BACKEND")
2203 {
2204 return Ok(parse_ref_storage_backend_value(&value)?.0 == RefBackendKind::Reftable);
2205 }
2206 let config_path = self.common_dir.join("config");
2207 if !config_path.exists() {
2208 return Ok(false);
2209 }
2210 let config = GitConfig::parse(&fs::read(config_path)?)?;
2211 let Some(value) = config.get("extensions", None, "refStorage") else {
2212 return Ok(false);
2213 };
2214 Ok(parse_ref_storage_backend_value(value)?.0 == RefBackendKind::Reftable)
2215 }
2216
2217 pub fn reftable_table_count(&self) -> Result<usize> {
2218 Ok(self.reftable_table_names()?.len())
2219 }
2220
2221 fn append_reftable_records(&self, records: Vec<ReftableRefRecord>) -> Result<()> {
2222 if records.is_empty() {
2223 return Ok(());
2224 }
2225 self.append_reftable_table(records, Vec::new())?;
2226 Ok(())
2227 }
2228
2229 fn next_reftable_update_index(&self, table_names: &[String]) -> Result<u64> {
2230 let reftable_dir = self.storage_dir.join("reftable");
2231 let mut max_update_index = 0;
2232 for name in table_names {
2233 let table = Reftable::parse(&fs::read(reftable_dir.join(name))?)?;
2234 max_update_index = max_update_index.max(table.header.max_update_index);
2235 }
2236 max_update_index
2237 .checked_add(1)
2238 .ok_or_else(|| GitError::InvalidFormat("reftable update index overflow".into()))
2239 }
2240
2241 fn reftable_table_names(&self) -> Result<Vec<String>> {
2243 self.reftable_table_names_from(&self.storage_dir.join("reftable").join("tables.list"))
2244 }
2245
2246 fn reftable_table_names_from(&self, tables_list: &Path) -> Result<Vec<String>> {
2247 if !tables_list.exists() {
2248 return Ok(Vec::new());
2249 }
2250 Ok(fs::read_to_string(tables_list)?
2251 .lines()
2252 .map(str::trim)
2253 .filter(|line| !line.is_empty())
2254 .map(str::to_string)
2255 .collect())
2256 }
2257
2258 fn reftable_lock_timeout_millis(&self) -> Result<u64> {
2259 if let Some(timeout_millis) = self.reftable_lock_timeout_millis {
2260 return Ok(timeout_millis);
2261 }
2262 let config_path = self.common_dir.join("config");
2263 let Ok(config) = GitConfig::read(config_path) else {
2264 return Ok(0);
2265 };
2266 Ok(config
2267 .get("reftable", None, "lockTimeout")
2268 .and_then(|value| value.parse::<u64>().ok())
2269 .unwrap_or(0))
2270 }
2271
2272 fn acquire_reftable_list_lock(&self, list_path: PathBuf) -> Result<ReftableListLock> {
2273 let lock_path = lock_path_for(&list_path)?;
2274 let timeout_millis = self.reftable_lock_timeout_millis()?;
2275 ReftableListLock::acquire(list_path, lock_path, timeout_millis)
2276 }
2277
2278 fn append_reftable_table(
2283 &self,
2284 mut refs: Vec<ReftableRefRecord>,
2285 mut logs: Vec<ReftableLogRecord>,
2286 ) -> Result<u64> {
2287 let reftable_dir = self.storage_dir.join("reftable");
2288 fs::create_dir_all(&reftable_dir)?;
2289 let list_path = reftable_dir.join("tables.list");
2290 let list_lock = self.acquire_reftable_list_lock(list_path.clone())?;
2291 let mut table_names = self.reftable_table_names_from(&list_path)?;
2292 let update_index = self.next_reftable_update_index(&table_names)?;
2293 for record in &mut refs {
2294 record.update_index = update_index;
2295 }
2296 for record in &mut logs {
2297 record.update_index = update_index;
2298 }
2299 let table_name = reftable_table_name(update_index, update_index);
2300 let bytes = Reftable::write(self.format, update_index, update_index, &refs, &logs)?;
2301 let table_path = reftable_dir.join(&table_name);
2302 write_locked(&table_path, &bytes)?;
2303 self.apply_reftable_shared_file_mode(&table_path)?;
2304 table_names.push(table_name);
2305 let mut list = Vec::new();
2306 for name in &table_names {
2307 list.extend_from_slice(name.as_bytes());
2308 list.push(b'\n');
2309 }
2310 list_lock.commit(&list)?;
2311 self.apply_reftable_shared_file_mode(&list_path)?;
2312 if logs.is_empty() && table_names.len() > 6 {
2313 self.auto_compact_reftable_stack()?;
2314 }
2315 Ok(update_index)
2316 }
2317
2318 pub fn compact_reftable_stack(&self) -> Result<()> {
2319 let old_names = self.reftable_table_names()?;
2320 self.compact_reftable_stack_range(0, old_names.len(), true)
2321 }
2322
2323 fn compact_reftable_stack_range(
2324 &self,
2325 start: usize,
2326 end: usize,
2327 fail_on_locked_table: bool,
2328 ) -> Result<()> {
2329 let reftable_dir = self.storage_dir.join("reftable");
2330 let list_path = reftable_dir.join("tables.list");
2331 let list_lock = self.acquire_reftable_list_lock(list_path.clone())?;
2332 let old_names = self.reftable_table_names_from(&list_path)?;
2333 if start >= end || end > old_names.len() {
2334 return Ok(());
2335 }
2336 let compact_names = old_names[start..end].to_vec();
2337 if compact_names.is_empty() {
2338 return Ok(());
2339 }
2340 if fail_on_locked_table {
2341 for name in &compact_names {
2342 if reftable_dir.join(format!("{name}.lock")).exists() {
2343 return Err(GitError::Io(format!(
2344 "cannot lock references: {}: File exists",
2345 reftable_dir.join(format!("{name}.lock")).display()
2346 )));
2347 }
2348 }
2349 }
2350
2351 let mut refs: BTreeMap<String, ReftableRefRecord> = BTreeMap::new();
2352 let mut logs: BTreeMap<(String, u64), ReftableLogRecord> = BTreeMap::new();
2353 let mut min_index = u64::MAX;
2354 let mut max_index = 0u64;
2355 let drop_tombstones = start == 0;
2356 for name in &compact_names {
2357 let table = Reftable::parse(&fs::read(reftable_dir.join(name))?)?;
2358 for record in table.refs {
2359 match record.value {
2360 ReftableRefValue::Deletion if drop_tombstones => {
2361 refs.remove(&record.name);
2362 }
2363 _ => {
2364 min_index = min_index.min(record.update_index);
2365 max_index = max_index.max(record.update_index);
2366 refs.insert(record.name.clone(), record);
2367 }
2368 }
2369 }
2370 for record in table.logs {
2371 let key = (record.refname.clone(), record.update_index);
2372 match record.value {
2373 ReftableLogValue::Deletion if drop_tombstones => {
2374 logs.remove(&key);
2375 }
2376 _ => {
2377 min_index = min_index.min(record.update_index);
2378 max_index = max_index.max(record.update_index);
2379 logs.insert(key, record);
2380 }
2381 }
2382 }
2383 }
2384
2385 if refs.is_empty() && logs.is_empty() {
2386 min_index = compact_names
2387 .iter()
2388 .filter_map(|name| Reftable::parse(&fs::read(reftable_dir.join(name)).ok()?).ok())
2389 .map(|table| table.header.min_update_index)
2390 .min()
2391 .unwrap_or(1);
2392 max_index = min_index;
2393 }
2394
2395 let table_name = reftable_table_name(min_index, max_index);
2396 let refs = refs.into_values().collect::<Vec<_>>();
2397 let logs = logs.into_values().collect::<Vec<_>>();
2398 let bytes = Reftable::write(self.format, min_index, max_index, &refs, &logs)?;
2399 let table_path = reftable_dir.join(&table_name);
2400 write_locked(&table_path, &bytes)?;
2401 self.apply_reftable_shared_file_mode(&table_path)?;
2402 let mut list = Vec::new();
2403 for name in &old_names[..start] {
2404 list.extend_from_slice(name.as_bytes());
2405 list.push(b'\n');
2406 }
2407 list.extend_from_slice(table_name.as_bytes());
2408 list.push(b'\n');
2409 for name in &old_names[end..] {
2410 list.extend_from_slice(name.as_bytes());
2411 list.push(b'\n');
2412 }
2413 list_lock.commit(&list)?;
2414 self.apply_reftable_shared_file_mode(&list_path)?;
2415 for name in compact_names {
2416 if name != table_name {
2417 let _ = fs::remove_file(reftable_dir.join(name));
2418 }
2419 }
2420 Ok(())
2421 }
2422
2423 fn apply_reftable_shared_file_mode(&self, path: &Path) -> Result<()> {
2424 #[cfg(unix)]
2425 {
2426 use std::os::unix::fs::PermissionsExt;
2427
2428 let config_path = self.common_dir.join("config");
2429 let Ok(config) = GitConfig::read(config_path) else {
2430 return Ok(());
2431 };
2432 let Some(value) = config.get("core", None, "sharedRepository") else {
2433 return Ok(());
2434 };
2435 let mode_or = match value {
2436 "1" | "group" | "true" => 0o660,
2437 "2" | "all" | "world" | "everybody" => 0o664,
2438 _ => return Ok(()),
2439 };
2440 let metadata = fs::metadata(path)?;
2441 let old_mode = metadata.permissions().mode();
2442 let mut permissions = metadata.permissions();
2443 permissions.set_mode((old_mode | mode_or) & 0o7777);
2444 fs::set_permissions(path, permissions)?;
2445 }
2446 #[cfg(not(unix))]
2447 {
2448 let _ = path;
2449 }
2450 Ok(())
2451 }
2452
2453 fn auto_compact_reftable_stack(&self) -> Result<()> {
2454 if reftable_autocompaction_disabled() {
2455 return Ok(());
2456 }
2457 let names = self.reftable_table_names()?;
2458 if names.len() <= 1 {
2459 return Ok(());
2460 }
2461 let reftable_dir = self.storage_dir.join("reftable");
2462 let start = names
2463 .iter()
2464 .rposition(|name| reftable_dir.join(format!("{name}.lock")).exists())
2465 .map_or(0, |index| index + 1);
2466 if names.len().saturating_sub(start) > 1 {
2467 self.compact_reftable_stack_range(start, names.len(), false)?;
2468 }
2469 Ok(())
2470 }
2471
2472 fn read_reftable_logs(&self, name: &str) -> Result<Vec<ReflogEntry>> {
2479 let mut by_index: BTreeMap<u64, Option<ReftableLogUpdate>> = BTreeMap::new();
2482 for table in self.reftables()? {
2483 for record in table.logs {
2484 if record.refname != name {
2485 continue;
2486 }
2487 match record.value {
2488 ReftableLogValue::Deletion => {
2489 by_index.insert(record.update_index, None);
2490 }
2491 ReftableLogValue::Update(update) => {
2492 by_index.insert(record.update_index, Some(update));
2493 }
2494 }
2495 }
2496 }
2497 let null = ObjectId::null(self.format);
2498 let mut entries = Vec::new();
2499 for update in by_index.into_values().flatten() {
2500 if reftable_log_update_is_empty_marker(&update, null) {
2502 continue;
2503 }
2504 entries.push(reflog_entry_from_reftable(update));
2505 }
2506 Ok(entries)
2507 }
2508
2509 fn reftable_log_exists(&self, name: &str) -> Result<bool> {
2510 let mut by_index: BTreeMap<u64, bool> = BTreeMap::new();
2511 for table in self.reftables()? {
2512 for record in table.logs {
2513 if record.refname != name {
2514 continue;
2515 }
2516 match record.value {
2517 ReftableLogValue::Deletion => {
2518 by_index.insert(record.update_index, false);
2519 }
2520 ReftableLogValue::Update(_) => {
2521 by_index.insert(record.update_index, true);
2522 }
2523 }
2524 }
2525 }
2526 Ok(by_index.into_values().any(|exists| exists))
2527 }
2528
2529 fn collect_loose_refs(
2530 &self,
2531 dir: &Path,
2532 prefix: &str,
2533 refs: &mut BTreeMap<String, Ref>,
2534 ) -> Result<()> {
2535 for entry in fs::read_dir(dir)? {
2536 let entry = entry?;
2537 let path = entry.path();
2538 let name = format!("{prefix}/{}", entry.file_name().to_string_lossy());
2539 if path.is_dir() {
2540 self.collect_loose_refs(&path, &name, refs)?;
2541 } else if !name.ends_with(".lock") {
2542 let bytes = fs::read(path)?;
2543 if loose_ref_bytes_are_reftable_sentinel(&name, &bytes) {
2544 continue;
2545 }
2546 match parse_loose_ref(self.format, name.clone(), &bytes) {
2550 Ok(reference) if ref_target_is_broken(&reference.target) => {
2551 warn_broken_ref(&name);
2552 }
2553 Ok(reference) => {
2554 refs.insert(name, reference);
2555 }
2556 Err(_) => warn_broken_ref(&name),
2557 }
2558 }
2559 }
2560 Ok(())
2561 }
2562
2563 fn collect_loose_refs_with_prefix(&self, prefix: &str, refs: &mut BTreeMap<String, Ref>) -> Result<()> {
2564 self.collect_loose_refs_with_prefix_from_base(&self.storage_dir, prefix, refs)
2565 }
2566
2567 fn collect_current_worktree_loose_refs_with_prefix(
2568 &self,
2569 prefix: &str,
2570 refs: &mut BTreeMap<String, Ref>,
2571 ) -> Result<()> {
2572 let base = self.current_worktree_storage_dir();
2573 if base == self.storage_dir {
2574 return Ok(());
2575 }
2576 for namespace in CURRENT_WORKTREE_REF_PREFIXES {
2577 if ref_prefixes_overlap(prefix, namespace) {
2578 let scan_prefix = if prefix.starts_with(namespace) { prefix } else { namespace };
2579 self.collect_loose_refs_with_prefix_from_base(&base, scan_prefix, refs)?;
2580 }
2581 }
2582 Ok(())
2583 }
2584
2585 fn collect_loose_refs_with_prefix_from_base(
2586 &self,
2587 base: &Path,
2588 prefix: &str,
2589 refs: &mut BTreeMap<String, Ref>,
2590 ) -> Result<()> {
2591 if !safe_ref_prefix_for_directory_scan(prefix) {
2592 return self.collect_all_loose_refs_from_base(base, refs);
2593 }
2594
2595 let trimmed = prefix.trim_end_matches('/');
2596 if prefix.ends_with('/') {
2597 let candidate = base.join(trimmed);
2598 match fs::metadata(&candidate) {
2599 Ok(meta) if meta.is_dir() => {
2600 self.collect_loose_refs(&candidate, trimmed, refs)?;
2601 return Ok(());
2602 }
2603 Ok(_) => return Ok(()),
2604 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
2605 Err(err) => return Err(err.into()),
2606 }
2607 }
2608
2609 let Some((parent_prefix, _)) = trimmed.rsplit_once('/') else {
2610 return self.collect_all_loose_refs_from_base(base, refs);
2611 };
2612 let parent = base.join(parent_prefix);
2613 match fs::metadata(&parent) {
2614 Ok(meta) if meta.is_dir() => self.collect_loose_refs(&parent, parent_prefix, refs),
2615 Ok(_) => Ok(()),
2616 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
2617 Err(err) => Err(err.into()),
2618 }
2619 }
2620
2621 fn collect_loose_ref_names(
2622 &self,
2623 dir: &Path,
2624 prefix: &str,
2625 names: &mut BTreeSet<String>,
2626 ) -> Result<()> {
2627 for entry in fs::read_dir(dir)? {
2628 let entry = entry?;
2629 let path = entry.path();
2630 let name = format!("{prefix}/{}", entry.file_name().to_string_lossy());
2631 if path.is_dir() {
2632 self.collect_loose_ref_names(&path, &name, names)?;
2633 } else if !name.ends_with(".lock") {
2634 names.insert(name);
2635 }
2636 }
2637 Ok(())
2638 }
2639
2640 fn collect_loose_ref_names_with_prefix(&self, prefix: &str, names: &mut BTreeSet<String>) -> Result<()> {
2641 self.collect_loose_ref_names_with_prefix_from_base(&self.storage_dir, prefix, names)
2642 }
2643
2644 fn collect_current_worktree_loose_ref_names_with_prefix(
2645 &self,
2646 prefix: &str,
2647 names: &mut BTreeSet<String>,
2648 ) -> Result<()> {
2649 let base = self.current_worktree_storage_dir();
2650 if base == self.storage_dir {
2651 return Ok(());
2652 }
2653 for namespace in CURRENT_WORKTREE_REF_PREFIXES {
2654 if ref_prefixes_overlap(prefix, namespace) {
2655 let scan_prefix = if prefix.starts_with(namespace) { prefix } else { namespace };
2656 self.collect_loose_ref_names_with_prefix_from_base(&base, scan_prefix, names)?;
2657 }
2658 }
2659 Ok(())
2660 }
2661
2662 fn collect_loose_ref_names_with_prefix_from_base(
2663 &self,
2664 base: &Path,
2665 prefix: &str,
2666 names: &mut BTreeSet<String>,
2667 ) -> Result<()> {
2668 if !safe_ref_prefix_for_directory_scan(prefix) {
2669 return self.collect_all_loose_ref_names_from_base(base, names);
2670 }
2671
2672 let trimmed = prefix.trim_end_matches('/');
2673 if prefix.ends_with('/') {
2674 let candidate = base.join(trimmed);
2675 match fs::metadata(&candidate) {
2676 Ok(meta) if meta.is_dir() => {
2677 self.collect_loose_ref_names(&candidate, trimmed, names)?;
2678 return Ok(());
2679 }
2680 Ok(_) => return Ok(()),
2681 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
2682 Err(err) => return Err(err.into()),
2683 }
2684 }
2685
2686 let Some((parent_prefix, _)) = trimmed.rsplit_once('/') else {
2687 return self.collect_all_loose_ref_names_from_base(base, names);
2688 };
2689 let parent = base.join(parent_prefix);
2690 match fs::metadata(&parent) {
2691 Ok(meta) if meta.is_dir() => {
2692 self.collect_loose_ref_names(&parent, parent_prefix, names)
2693 }
2694 Ok(_) => Ok(()),
2695 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
2696 Err(err) => Err(err.into()),
2697 }
2698 }
2699
2700 fn collect_all_loose_ref_names_from_base(
2701 &self,
2702 base: &Path,
2703 names: &mut BTreeSet<String>,
2704 ) -> Result<()> {
2705 let refs_dir = base.join("refs");
2706 if refs_dir.exists() {
2707 self.collect_loose_ref_names(&refs_dir, "refs", names)?;
2708 }
2709 Ok(())
2710 }
2711
2712 fn collect_all_loose_refs_from_base(
2713 &self,
2714 base: &Path,
2715 refs: &mut BTreeMap<String, Ref>,
2716 ) -> Result<()> {
2717 let refs_dir = base.join("refs");
2718 if refs_dir.exists() {
2719 self.collect_loose_refs(&refs_dir, "refs", refs)?;
2720 }
2721 Ok(())
2722 }
2723
2724 fn collect_reflog_names(
2725 &self,
2726 dir: &Path,
2727 prefix: &str,
2728 names: &mut BTreeSet<String>,
2729 ) -> Result<()> {
2730 let Ok(entries) = fs::read_dir(dir) else {
2731 return Ok(());
2732 };
2733 for entry in entries {
2734 let entry = entry?;
2735 let path = entry.path();
2736 let name = format!("{prefix}/{}", entry.file_name().to_string_lossy());
2737 if path.is_dir() {
2738 self.collect_reflog_names(&path, &name, names)?;
2739 } else if let Some(name) = name.strip_prefix("logs/") {
2740 names.insert(name.to_string());
2741 }
2742 }
2743 Ok(())
2744 }
2745
2746 fn loose_refs_have_prefix(&self, prefix: &str) -> Result<bool> {
2747 if !prefix.starts_with("refs/") || !prefix.ends_with('/') {
2748 return Ok(self
2749 .list_refs()?
2750 .iter()
2751 .any(|reference| reference.name.starts_with(prefix)));
2752 }
2753 let loose_prefix = prefix.trim_end_matches('/');
2754 let dir = self.common_dir.join(loose_prefix);
2755 match fs::metadata(&dir) {
2756 Ok(meta) if meta.is_dir() => {
2757 let mut refs = BTreeMap::new();
2758 self.collect_loose_refs(&dir, loose_prefix, &mut refs)?;
2759 Ok(!refs.is_empty())
2760 }
2761 Ok(_) => Ok(false),
2762 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
2763 Err(err) => Err(err.into()),
2764 }
2765 }
2766
2767 fn write_loose_ref(&self, reference: &Ref) -> Result<()> {
2768 if self.uses_reftable()? {
2769 let (store, name) = self.reftable_store_for_ref(&reference.name)?;
2770 store.append_reftable_records(vec![ReftableRefRecord {
2771 name,
2772 update_index: 0,
2773 value: reftable_value_from_ref_target(&reference.target),
2774 }])?;
2775 return Ok(());
2776 }
2777 let path = self.ref_path(&reference.name);
2778 let parent = path
2779 .parent()
2780 .ok_or_else(|| GitError::InvalidPath("ref path has no parent".into()))?;
2781 fs::create_dir_all(parent)?;
2782 write_locked(&path, &write_loose_ref(reference))
2783 }
2784
2785 fn delete_loose_ref(&self, name: &str) -> Result<()> {
2786 let path = self.ref_path(name);
2787 let lock_path = lock_path_for(&path)?;
2788 {
2789 let mut file = fs::OpenOptions::new()
2790 .write(true)
2791 .create_new(true)
2792 .open(&lock_path)?;
2793 file.write_all(b"delete\n")?;
2794 file.sync_all()?;
2795 }
2796 match fs::remove_file(&path) {
2797 Ok(()) => {
2798 fs::remove_file(lock_path)?;
2799 self.prune_empty_ref_dirs(name);
2800 Ok(())
2801 }
2802 Err(err) => {
2803 let _ = fs::remove_file(lock_path);
2804 Err(GitError::Io(err.to_string()))
2805 }
2806 }
2807 }
2808
2809 fn prune_empty_ref_dirs(&self, name: &str) {
2815 let boundary = self.ref_prune_boundary(name);
2816 if let Some(parent) = self.ref_path(name).parent() {
2817 prune_empty_dirs_up_to(parent, &boundary);
2818 }
2819 }
2820
2821 fn ref_prune_boundary(&self, name: &str) -> PathBuf {
2822 let base = self.ref_base_dir(name).to_path_buf();
2823 let mut components = name.split('/');
2824 if components.next() == Some("refs")
2825 && let Some(namespace) = components.next()
2826 {
2827 return base.join("refs").join(namespace);
2828 }
2829 base
2830 }
2831
2832 fn remove_reflog_file(&self, name: &str) {
2838 if matches!(self.uses_reftable(), Ok(true)) {
2842 let _ = self.tombstone_reftable_logs(name);
2843 return;
2844 }
2845 let path = self.reflog_path(name);
2846 let _ = fs::remove_file(&path);
2847 let base = self.ref_base_dir(name).to_path_buf();
2848 let logs_refs_root = base.join("logs").join("refs");
2849 if let Some(parent) = path.parent() {
2850 prune_empty_dirs_up_to(parent, &logs_refs_root);
2851 }
2852 }
2853
2854 fn tombstone_reftable_logs(&self, name: &str) -> Result<()> {
2858 self.rewrite_reftable_logs(name, &[], false)
2859 }
2860
2861 fn head_reflog_mirror(
2873 head_branch: Option<&str>,
2874 reflogs: &[(String, ReflogEntry)],
2875 ) -> Vec<(String, ReflogEntry)> {
2876 let Some(head_branch) = head_branch else {
2877 return Vec::new();
2878 };
2879 if reflogs.iter().any(|(name, _)| name == "HEAD") {
2882 return Vec::new();
2883 }
2884 reflogs
2885 .iter()
2886 .filter(|(name, _)| name == head_branch)
2887 .map(|(_, entry)| ("HEAD".to_string(), entry.clone()))
2888 .collect()
2889 }
2890
2891 fn head_symref_target(&self) -> Option<String> {
2895 match self.read_ref("HEAD") {
2896 Ok(Some(RefTarget::Symbolic(branch))) => Some(branch),
2897 _ => None,
2898 }
2899 }
2900
2901 pub fn append_reflog(&self, name: &str, entry: &ReflogEntry) -> Result<()> {
2902 validate_ref_name_for_read(name)?;
2903 if self.uses_reftable()? {
2904 let update = reftable_update_from_reflog(entry)?;
2905 self.append_reftable_table(
2906 Vec::new(),
2907 vec![ReftableLogRecord {
2908 refname: name.to_string(),
2909 update_index: 0,
2910 value: ReftableLogValue::Update(update),
2911 }],
2912 )?;
2913 self.auto_compact_reftable_stack()?;
2914 return Ok(());
2915 }
2916 let path = self.reflog_path(name);
2917 let parent = path
2918 .parent()
2919 .ok_or_else(|| GitError::InvalidPath("reflog path has no parent".into()))?;
2920 fs::create_dir_all(parent)?;
2921 let mut file = fs::OpenOptions::new()
2922 .create(true)
2923 .append(true)
2924 .open(path)?;
2925 file.write_all(&entry.to_line())?;
2926 Ok(())
2927 }
2928
2929 fn rewrite_reftable_logs(
2937 &self,
2938 name: &str,
2939 entries: &[ReflogEntry],
2940 preserve_empty: bool,
2941 ) -> Result<()> {
2942 let mut live_indexes: BTreeSet<u64> = BTreeSet::new();
2945 let mut deleted_indexes: BTreeSet<u64> = BTreeSet::new();
2946 for table in self.reftables()? {
2947 for record in table.logs {
2948 if record.refname != name {
2949 continue;
2950 }
2951 match record.value {
2952 ReftableLogValue::Deletion => {
2953 deleted_indexes.insert(record.update_index);
2954 live_indexes.remove(&record.update_index);
2955 }
2956 ReftableLogValue::Update(_) => {
2957 live_indexes.insert(record.update_index);
2958 deleted_indexes.remove(&record.update_index);
2959 }
2960 }
2961 }
2962 }
2963
2964 let table_names = self.reftable_table_names()?;
2965 let base = self.next_reftable_update_index(&table_names)?;
2966 let mut logs: Vec<ReftableLogRecord> = Vec::new();
2967 for index in &live_indexes {
2969 logs.push(ReftableLogRecord {
2970 refname: name.to_string(),
2971 update_index: *index,
2972 value: ReftableLogValue::Deletion,
2973 });
2974 }
2975 for (offset, entry) in entries.iter().enumerate() {
2978 let update_index = base
2979 .checked_add(offset as u64)
2980 .ok_or_else(|| GitError::InvalidFormat("reftable update index overflow".into()))?;
2981 logs.push(ReftableLogRecord {
2982 refname: name.to_string(),
2983 update_index,
2984 value: ReftableLogValue::Update(reftable_update_from_reflog(entry)?),
2985 });
2986 }
2987 if entries.is_empty() && preserve_empty {
2988 let null = ObjectId::null(self.format);
2989 logs.push(ReftableLogRecord {
2990 refname: name.to_string(),
2991 update_index: base,
2992 value: ReftableLogValue::Update(ReftableLogUpdate {
2993 old_oid: null,
2994 new_oid: null,
2995 name: String::new(),
2996 email: String::new(),
2997 time: 0,
2998 tz_offset: 0,
2999 message: String::new(),
3000 }),
3001 });
3002 }
3003 if logs.is_empty() {
3004 return Ok(());
3005 }
3006 let leave_empty_rewrite_tombstones_separate = entries.is_empty();
3007 if leave_empty_rewrite_tombstones_separate
3008 && !reftable_autocompaction_disabled()
3009 && self.reftable_table_names()?.len() > 1
3010 {
3011 self.compact_reftable_stack()?;
3012 }
3013 self.append_reftable_table_spanning(Vec::new(), logs)?;
3014 if !leave_empty_rewrite_tombstones_separate {
3015 self.auto_compact_reftable_stack()?;
3016 }
3017 Ok(())
3018 }
3019
3020 fn append_reftable_table_spanning(
3025 &self,
3026 mut refs: Vec<ReftableRefRecord>,
3027 logs: Vec<ReftableLogRecord>,
3028 ) -> Result<u64> {
3029 let reftable_dir = self.storage_dir.join("reftable");
3030 fs::create_dir_all(&reftable_dir)?;
3031 let list_path = reftable_dir.join("tables.list");
3032 let list_lock = self.acquire_reftable_list_lock(list_path.clone())?;
3033 let mut table_names = self.reftable_table_names_from(&list_path)?;
3034 let alloc_index = self.next_reftable_update_index(&table_names)?;
3035 for record in &mut refs {
3036 record.update_index = alloc_index;
3037 }
3038 let mut min_index = alloc_index;
3039 let mut max_index = alloc_index;
3040 for record in &logs {
3041 min_index = min_index.min(record.update_index);
3042 max_index = max_index.max(record.update_index);
3043 }
3044 for record in &refs {
3045 min_index = min_index.min(record.update_index);
3046 max_index = max_index.max(record.update_index);
3047 }
3048 let table_name = reftable_table_name(min_index, max_index);
3049 let bytes = Reftable::write(self.format, min_index, max_index, &refs, &logs)?;
3050 let table_path = reftable_dir.join(&table_name);
3051 write_locked(&table_path, &bytes)?;
3052 self.apply_reftable_shared_file_mode(&table_path)?;
3053 table_names.push(table_name);
3054 let mut list = Vec::new();
3055 for name in &table_names {
3056 list.extend_from_slice(name.as_bytes());
3057 list.push(b'\n');
3058 }
3059 list_lock.commit(&list)?;
3060 self.apply_reftable_shared_file_mode(&list_path)?;
3061 Ok(max_index)
3062 }
3063
3064 fn ref_path(&self, name: &str) -> PathBuf {
3065 self.ref_base_dir(name).join(name)
3066 }
3067
3068 fn reflog_path(&self, name: &str) -> PathBuf {
3069 self.ref_base_dir(name).join("logs").join(name)
3070 }
3071
3072 fn ref_base_dir(&self, name: &str) -> PathBuf {
3073 if current_worktree_ref(name) {
3074 self.current_worktree_storage_dir()
3075 } else {
3076 self.storage_dir.clone()
3077 }
3078 }
3079
3080 fn check_ref_directory_conflict_targeted(&self, name: &str) -> Result<()> {
3081 match self.refname_directory_conflict(name)? {
3082 Some(conflict) => Err(ref_directory_conflict_error(name, &conflict)),
3083 None => Ok(()),
3084 }
3085 }
3086
3087 pub fn refname_directory_conflict(&self, name: &str) -> Result<Option<String>> {
3093 let components = name.split('/').collect::<Vec<_>>();
3094 let mut ancestors = Vec::new();
3095 for index in 1..components.len() {
3096 let ancestor = components[..index].join("/");
3097 if self.loose_ref_file_exists_for_conflict(&ancestor)? {
3098 return Ok(Some(ancestor));
3099 }
3100 ancestors.push(ancestor);
3101 }
3102 let child_prefix = format!("{name}/");
3103 if let Some(existing) = self.first_loose_ref_name_with_prefix(&child_prefix)? {
3104 return Ok(Some(existing));
3105 }
3106 if let Some(existing) =
3107 self.first_packed_ref_directory_conflict(&ancestors, &child_prefix)?
3108 {
3109 return Ok(Some(existing));
3110 }
3111 Ok(None)
3112 }
3113
3114 fn loose_ref_file_exists_for_conflict(&self, name: &str) -> Result<bool> {
3115 let path = self.ref_path(name);
3116 match fs::symlink_metadata(path) {
3117 Ok(meta) if meta.is_dir() => Ok(false),
3118 Ok(_) => {
3119 let path = self.ref_path(name);
3120 if let Ok(bytes) = fs::read(path)
3121 && loose_ref_bytes_are_reftable_sentinel(name, &bytes)
3122 {
3123 return Ok(false);
3124 }
3125 Ok(true)
3126 }
3127 Err(err)
3128 if err.kind() == std::io::ErrorKind::NotFound
3129 || err.kind() == std::io::ErrorKind::NotADirectory =>
3130 {
3131 Ok(false)
3132 }
3133 Err(err) => Err(err.into()),
3134 }
3135 }
3136
3137 fn first_packed_ref_directory_conflict(
3138 &self,
3139 ancestors: &[String],
3140 child_prefix: &str,
3141 ) -> Result<Option<String>> {
3142 let packed_path = self.storage_dir.join("packed-refs");
3143 let text = match fs::read_to_string(packed_path) {
3144 Ok(text) => text,
3145 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
3146 Err(err) => return Err(err.into()),
3147 };
3148 for raw_line in text.lines() {
3149 let line = raw_line.trim_end();
3150 if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
3151 continue;
3152 }
3153 let (_, name) = line
3154 .split_once(' ')
3155 .ok_or_else(|| packed_refs_unexpected_line(line))?;
3156 if ancestors.iter().any(|ancestor| ancestor == name) || name.starts_with(child_prefix) {
3157 return Ok(Some(name.to_owned()));
3158 }
3159 }
3160 Ok(None)
3161 }
3162
3163 fn first_loose_ref_name_with_prefix(&self, prefix: &str) -> Result<Option<String>> {
3164 if !prefix.starts_with("refs/") || !prefix.ends_with('/') {
3165 return Ok(None);
3166 }
3167 let trimmed = prefix.trim_end_matches('/');
3168 let dir = self.ref_base_dir(trimmed).join(trimmed);
3169 self.first_loose_ref_name_in_dir(&dir, trimmed)
3170 }
3171
3172 fn first_loose_ref_name_in_dir(&self, dir: &Path, prefix: &str) -> Result<Option<String>> {
3173 let entries = match fs::read_dir(dir) {
3174 Ok(entries) => entries,
3175 Err(err)
3176 if err.kind() == std::io::ErrorKind::NotFound
3177 || err.kind() == std::io::ErrorKind::NotADirectory =>
3178 {
3179 return Ok(None);
3180 }
3181 Err(err) => return Err(err.into()),
3182 };
3183 for entry in entries {
3184 let entry = entry?;
3185 let path = entry.path();
3186 let name = format!("{prefix}/{}", entry.file_name().to_string_lossy());
3187 if path.is_dir() {
3188 if let Some(found) = self.first_loose_ref_name_in_dir(&path, &name)? {
3189 return Ok(Some(found));
3190 }
3191 } else if !name.ends_with(".lock") {
3192 return Ok(Some(name));
3193 }
3194 }
3195 Ok(None)
3196 }
3197}
3198
3199fn reftable_ref_target(value: ReftableRefValue) -> Result<Option<RefTarget>> {
3200 match value {
3201 ReftableRefValue::Deletion => Ok(None),
3202 ReftableRefValue::Direct(oid) | ReftableRefValue::Peeled { target: oid, .. } => {
3203 Ok(Some(RefTarget::Direct(oid)))
3204 }
3205 ReftableRefValue::Symbolic(target) => Ok(Some(RefTarget::Symbolic(target))),
3206 }
3207}
3208
3209fn reftable_value_from_ref_target(target: &RefTarget) -> ReftableRefValue {
3210 match target {
3211 RefTarget::Direct(oid) => ReftableRefValue::Direct(*oid),
3212 RefTarget::Symbolic(target) => ReftableRefValue::Symbolic(target.clone()),
3213 }
3214}
3215
3216fn reflog_entry_from_reftable(update: ReftableLogUpdate) -> ReflogEntry {
3223 let committer = format!(
3224 "{} <{}> {} {}",
3225 update.name,
3226 update.email,
3227 update.time,
3228 format_reflog_tz(update.tz_offset),
3229 );
3230 let mut message = update.message.into_bytes();
3235 if message.last() == Some(&b'\n') {
3236 message.pop();
3237 }
3238 ReflogEntry {
3239 old_oid: update.old_oid,
3240 new_oid: update.new_oid,
3241 committer: committer.into_bytes(),
3242 message,
3243 }
3244}
3245
3246fn reftable_log_update_is_empty_marker(update: &ReftableLogUpdate, null: ObjectId) -> bool {
3247 update.old_oid == null && update.new_oid == null
3248}
3249
3250fn reftable_update_from_reflog(entry: &ReflogEntry) -> Result<ReftableLogUpdate> {
3253 let committer = std::str::from_utf8(&entry.committer)
3254 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
3255 let (name, email, time, tz_offset) = split_committer_ident(committer)?;
3256 let mut message = std::str::from_utf8(&entry.message)
3257 .map_err(|err| GitError::InvalidFormat(err.to_string()))?
3258 .to_string();
3259 if !message.ends_with('\n') {
3263 message.push('\n');
3264 }
3265 Ok(ReftableLogUpdate {
3266 old_oid: entry.old_oid,
3267 new_oid: entry.new_oid,
3268 name,
3269 email,
3270 time,
3271 tz_offset,
3272 message,
3273 })
3274}
3275
3276fn split_committer_ident(committer: &str) -> Result<(String, String, u64, i16)> {
3281 let open = committer.find(" <").ok_or_else(|| {
3282 GitError::InvalidFormat("reflog committer is missing email opener".into())
3283 })?;
3284 let name = committer[..open].to_string();
3285 let after_open = open + 2;
3286 let close = committer[after_open..].find('>').ok_or_else(|| {
3287 GitError::InvalidFormat("reflog committer is missing email closer".into())
3288 })?;
3289 let email = committer[after_open..after_open + close].to_string();
3290 let rest = committer[after_open + close + 1..].trim();
3291 let (time_str, tz_str) = rest.split_once(' ').ok_or_else(|| {
3292 GitError::InvalidFormat("reflog committer is missing timestamp/timezone".into())
3293 })?;
3294 let time = time_str
3295 .trim()
3296 .parse::<u64>()
3297 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
3298 let tz_offset = parse_reflog_tz(tz_str.trim())?;
3299 Ok((name, email, time, tz_offset))
3300}
3301
3302fn format_reflog_tz(tz_offset: i16) -> String {
3305 let sign = if tz_offset < 0 { '-' } else { '+' };
3306 let magnitude = tz_offset.unsigned_abs();
3307 format!("{sign}{magnitude:04}")
3308}
3309
3310fn parse_reflog_tz(tz: &str) -> Result<i16> {
3313 let (sign, digits) = match tz.strip_prefix('-') {
3314 Some(rest) => (-1i16, rest),
3315 None => (1i16, tz.strip_prefix('+').unwrap_or(tz)),
3316 };
3317 let magnitude = digits
3318 .parse::<i16>()
3319 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
3320 Ok(sign * magnitude)
3321}
3322
3323fn reftable_table_name(min_update_index: u64, max_update_index: u64) -> String {
3329 let nanos = SystemTime::now()
3330 .duration_since(UNIX_EPOCH)
3331 .map(|duration| duration.as_nanos())
3332 .unwrap_or(0);
3333 let salt = (nanos as u64) ^ (u64::from(std::process::id()) << 16);
3336 format!(
3337 "0x{min_update_index:012x}-0x{max_update_index:012x}-{:08x}.ref",
3338 salt as u32
3339 )
3340}
3341
3342fn reftable_autocompaction_disabled() -> bool {
3343 std::env::var("GIT_TEST_REFTABLE_AUTOCOMPACTION")
3344 .is_ok_and(|value| value.eq_ignore_ascii_case("false"))
3345}
3346
3347#[cfg(test)]
3352fn reftable_table_name_is_valid(name: &str) -> bool {
3353 fn hex_prefix(s: &str) -> Option<&str> {
3354 let body = s
3356 .strip_prefix("0x")
3357 .or_else(|| s.strip_prefix("0X"))
3358 .unwrap_or(s);
3359 let consumed = body
3360 .find(|c: char| !c.is_ascii_hexdigit())
3361 .unwrap_or(body.len());
3362 if consumed == 0 {
3363 return None;
3364 }
3365 Some(&body[consumed..])
3366 }
3367 let Some(rest) = hex_prefix(name) else {
3368 return false;
3369 };
3370 let Some(rest) = rest.strip_prefix('-') else {
3371 return false;
3372 };
3373 let Some(rest) = hex_prefix(rest) else {
3374 return false;
3375 };
3376 let Some(rest) = rest.strip_prefix('-') else {
3377 return false;
3378 };
3379 let Some(rest) = hex_prefix(rest) else {
3380 return false;
3381 };
3382 rest == ".ref" || rest == ".log"
3383}
3384
3385fn repository_common_dir(git_dir: &Path) -> PathBuf {
3386 if let Some(common_dir) = std::env::var_os("GIT_COMMON_DIR") {
3387 return PathBuf::from(common_dir);
3388 }
3389 let commondir = git_dir.join("commondir");
3390 if let Ok(value) = fs::read_to_string(&commondir) {
3391 let path = PathBuf::from(value.trim());
3392 let common = if path.is_absolute() {
3393 path
3394 } else {
3395 git_dir.join(path)
3396 };
3397 return fs::canonicalize(&common).unwrap_or(common);
3398 }
3399 git_dir.to_path_buf()
3400}
3401
3402fn log_all_ref_updates_matches(name: &str, value: &str) -> bool {
3403 if value.eq_ignore_ascii_case("always") {
3404 return true;
3405 }
3406 if !sley_config::parse_config_bool(value).unwrap_or(false) {
3407 return false;
3408 }
3409 name == "HEAD"
3410 || name.starts_with("refs/heads/")
3411 || name.starts_with("refs/remotes/")
3412 || name.starts_with("refs/notes/")
3413}
3414
3415#[derive(Clone, Copy, PartialEq, Eq, Debug)]
3419pub enum RefTransactionPhase {
3420 Preparing,
3423 Prepared,
3426 Committed,
3428 Aborted,
3430}
3431
3432impl RefTransactionPhase {
3433 pub fn as_str(self) -> &'static str {
3435 match self {
3436 RefTransactionPhase::Preparing => "preparing",
3437 RefTransactionPhase::Prepared => "prepared",
3438 RefTransactionPhase::Committed => "committed",
3439 RefTransactionPhase::Aborted => "aborted",
3440 }
3441 }
3442}
3443
3444#[derive(Clone, Debug)]
3450pub struct RefTransactionHookUpdate {
3451 pub old_value: String,
3452 pub new_value: String,
3453 pub refname: String,
3454}
3455
3456pub trait ReferenceTransactionHook {
3468 fn run(&self, phase: RefTransactionPhase, updates: &[RefTransactionHookUpdate])
3469 -> Result<bool>;
3470}
3471
3472pub struct FileRefTransaction<'a> {
3473 store: &'a FileRefStore,
3474 changes: Vec<QueuedRefChange>,
3475 hook: Option<&'a dyn ReferenceTransactionHook>,
3476}
3477
3478struct QueuedUpdate {
3481 name: String,
3482 precondition: RefPrecondition,
3483 new: RefTarget,
3484 reflog: Option<ReflogEntry>,
3485}
3486
3487struct QueuedDelete {
3488 name: String,
3489 precondition: RefDeletePrecondition,
3490}
3491
3492enum QueuedRefChange {
3493 Update(QueuedUpdate),
3494 Delete(QueuedDelete),
3495}
3496
3497#[derive(Debug, Clone, PartialEq, Eq)]
3499pub enum RefDeletePrecondition {
3500 Any,
3502 Immediate(RefTarget),
3504 Direct(Option<ObjectId>),
3506 Peeled(ObjectId),
3508}
3509
3510impl<'a> FileRefTransaction<'a> {
3511 pub fn with_hook(mut self, hook: &'a dyn ReferenceTransactionHook) -> Self {
3517 self.hook = Some(hook);
3518 self
3519 }
3520
3521 pub fn update(&mut self, update: RefUpdate) {
3526 self.changes.push(QueuedRefChange::Update(QueuedUpdate {
3527 name: update.name,
3528 precondition: RefPrecondition::from_expected(update.expected),
3529 new: update.new,
3530 reflog: update.reflog,
3531 }));
3532 }
3533
3534 pub fn update_to(
3540 &mut self,
3541 name: impl Into<String>,
3542 new: RefTarget,
3543 precondition: RefPrecondition,
3544 reflog: Option<ReflogEntry>,
3545 ) {
3546 self.changes.push(QueuedRefChange::Update(QueuedUpdate {
3547 name: name.into(),
3548 precondition,
3549 new,
3550 reflog,
3551 }));
3552 }
3553
3554 pub fn delete(&mut self, delete: DeleteRef) {
3559 self.delete_with_precondition(
3560 delete.name,
3561 RefDeletePrecondition::Direct(delete.expected_old),
3562 delete.reflog,
3563 );
3564 }
3565
3566 pub fn delete_with_precondition(
3572 &mut self,
3573 name: impl Into<String>,
3574 precondition: RefDeletePrecondition,
3575 _reflog: Option<DeleteRefReflog>,
3576 ) {
3577 self.changes.push(QueuedRefChange::Delete(QueuedDelete {
3578 name: name.into(),
3579 precondition,
3580 }));
3581 }
3582
3583 pub fn commit(self) -> Result<()> {
3604 if env::var_os("GIT_QUARANTINE_PATH").is_some() {
3605 return Err(GitError::Transaction(
3606 "ref updates forbidden inside quarantine environment".into(),
3607 ));
3608 }
3609 let FileRefTransaction {
3610 store,
3611 changes,
3612 hook,
3613 } = self;
3614 let changes = coalesce_ref_changes(changes)?;
3615 let hook_updates = hook.map(|_| hook_updates_for_changes(store.format, &changes));
3620 if let (Some(hook), Some(updates)) = (hook, hook_updates.as_ref())
3621 && hook.run(RefTransactionPhase::Preparing, updates)?
3622 {
3623 return Err(ref_transaction_hook_abort(RefTransactionPhase::Preparing));
3624 }
3625 if store.uses_reftable()? {
3626 store.commit_reftable(changes)
3627 } else {
3628 store.commit_loose_hooked(changes, hook, hook_updates.as_deref())
3629 }
3630 }
3631}
3632
3633fn ref_transaction_hook_abort(phase: RefTransactionPhase) -> GitError {
3636 GitError::Transaction(format!(
3637 "in '{}' phase, update aborted by the reference-transaction hook",
3638 phase.as_str()
3639 ))
3640}
3641
3642fn hook_updates_for_changes(
3648 format: ObjectFormat,
3649 changes: &[CoalescedRefChange],
3650) -> Vec<RefTransactionHookUpdate> {
3651 let zero = ObjectId::null(format).to_string();
3652 changes
3653 .iter()
3654 .map(|change| match change {
3655 CoalescedRefChange::Update(update) => RefTransactionHookUpdate {
3656 old_value: hook_old_value(&zero, &update.precondition),
3657 new_value: hook_target_value(&zero, Some(&update.new)),
3658 refname: update.name.clone(),
3659 },
3660 CoalescedRefChange::Delete(delete) => RefTransactionHookUpdate {
3661 old_value: hook_delete_old_value(&zero, &delete.precondition),
3662 new_value: zero.clone(),
3663 refname: delete.name.clone(),
3664 },
3665 })
3666 .collect()
3667}
3668
3669fn hook_old_value(zero: &str, precondition: &RefPrecondition) -> String {
3673 match precondition {
3674 RefPrecondition::Any | RefPrecondition::MustExist => zero.to_string(),
3675 RefPrecondition::MustNotExist => zero.to_string(),
3676 RefPrecondition::MustExistAndMatch(target) | RefPrecondition::ExistingMustMatch(target) => {
3677 hook_target_value(zero, Some(target))
3678 }
3679 }
3680}
3681
3682fn hook_delete_old_value(zero: &str, precondition: &RefDeletePrecondition) -> String {
3685 match precondition {
3686 RefDeletePrecondition::Any => zero.to_string(),
3687 RefDeletePrecondition::Immediate(target) => hook_target_value(zero, Some(target)),
3688 RefDeletePrecondition::Direct(Some(oid)) | RefDeletePrecondition::Peeled(oid) => {
3689 oid.to_string()
3690 }
3691 RefDeletePrecondition::Direct(None) => zero.to_string(),
3692 }
3693}
3694
3695fn hook_target_value(zero: &str, target: Option<&RefTarget>) -> String {
3698 match target {
3699 None => zero.to_string(),
3700 Some(RefTarget::Direct(oid)) => oid.to_string(),
3701 Some(RefTarget::Symbolic(name)) => format!("ref:{name}"),
3702 }
3703}
3704
3705impl FileRefStore {
3706 fn commit_reftable(&self, changes: Vec<CoalescedRefChange>) -> Result<()> {
3707 let routed = self.route_reftable_changes(changes)?;
3708 if routed.len() != 1 || routed[0].0.storage_dir != self.storage_dir {
3709 for (store, changes) in routed {
3710 store.commit_reftable_local(changes)?;
3711 }
3712 return Ok(());
3713 }
3714 let mut routed = routed;
3715 if let Some((_, changes)) = routed.pop() {
3716 self.commit_reftable_local(changes)
3717 } else {
3718 Ok(())
3719 }
3720 }
3721
3722 fn commit_reftable_local(&self, changes: Vec<CoalescedRefChange>) -> Result<()> {
3723 let has_reflogs = changes.iter().any(|change| {
3725 matches!(change, CoalescedRefChange::Update(update) if !update.reflog.is_empty())
3726 });
3727 let head_branch = if has_reflogs {
3728 self.head_symref_target()
3729 } else {
3730 None
3731 };
3732 if changes
3733 .iter()
3734 .any(|change| matches!(change, CoalescedRefChange::Update(_)))
3735 {
3736 let mut names = self
3737 .list_refs()?
3738 .into_iter()
3739 .map(|reference| reference.name)
3740 .collect::<BTreeSet<_>>();
3741 for change in &changes {
3742 if let CoalescedRefChange::Update(update) = change {
3743 names.insert(update.name.clone());
3744 }
3745 }
3746 for change in &changes {
3747 if let CoalescedRefChange::Update(update) = change {
3748 check_ref_directory_conflict_in_names(&update.name, &names)?;
3749 }
3750 }
3751 }
3752 let mut records = Vec::with_capacity(changes.len());
3753 let mut reflogs = Vec::new();
3754 let mut delete_names = Vec::new();
3755 for change in changes {
3756 match change {
3757 CoalescedRefChange::Update(update) => {
3758 if !matches!(update.precondition, RefPrecondition::Any) {
3759 let current = self.read_ref(&update.name)?;
3760 if !update.precondition.is_satisfied_by(current.as_ref()) {
3761 return Err(GitError::Transaction(
3762 update.precondition.describe(&update.name),
3763 ));
3764 }
3765 }
3766 records.push(ReftableRefRecord {
3767 name: update.name.clone(),
3768 update_index: 0,
3769 value: reftable_value_from_ref_target(&update.new),
3770 });
3771 for entry in update.reflog {
3772 reflogs.push((update.name.clone(), entry));
3773 }
3774 }
3775 CoalescedRefChange::Delete(delete) => {
3776 let current = self.read_ref(&delete.name)?;
3777 verify_delete_precondition(
3781 self,
3782 &delete.name,
3783 current.as_ref(),
3784 &delete.precondition,
3785 )?;
3786 records.push(ReftableRefRecord {
3787 name: delete.name.clone(),
3788 update_index: 0,
3789 value: ReftableRefValue::Deletion,
3790 });
3791 delete_names.push(delete.name.clone());
3792 }
3793 }
3794 }
3795 if self.combine_reftable_logs || self.git_dir != self.common_dir {
3796 let head_mirror = Self::head_reflog_mirror(head_branch.as_deref(), &reflogs);
3797 reflogs.extend(head_mirror);
3798 let log_records = reflogs
3799 .into_iter()
3800 .map(|(name, entry)| {
3801 Ok(ReftableLogRecord {
3802 refname: name,
3803 update_index: 0,
3804 value: ReftableLogValue::Update(reftable_update_from_reflog(&entry)?),
3805 })
3806 })
3807 .collect::<Result<Vec<_>>>()?;
3808 self.append_reftable_table(records, log_records)?;
3809 for name in &delete_names {
3810 self.remove_reflog_file(name);
3811 }
3812 return Ok(());
3813 }
3814 self.append_reftable_records(records)?;
3815 for name in &delete_names {
3819 self.remove_reflog_file(name);
3820 }
3821 let head_mirror = Self::head_reflog_mirror(head_branch.as_deref(), &reflogs);
3822 reflogs.extend(head_mirror);
3823 for (name, entry) in reflogs {
3824 self.append_reflog(&name, &entry)?;
3825 }
3826 Ok(())
3827 }
3828
3829 fn route_reftable_changes(
3830 &self,
3831 changes: Vec<CoalescedRefChange>,
3832 ) -> Result<Vec<(FileRefStore, Vec<CoalescedRefChange>)>> {
3833 let mut grouped = BTreeMap::<PathBuf, (FileRefStore, Vec<CoalescedRefChange>)>::new();
3834 for change in changes {
3835 let name = match &change {
3836 CoalescedRefChange::Update(update) => update.name.as_str(),
3837 CoalescedRefChange::Delete(delete) => delete.name.as_str(),
3838 };
3839 let (store, rewritten) = self.reftable_store_for_ref(name)?;
3840 let rewritten_change = match change {
3841 CoalescedRefChange::Update(mut update) => {
3842 update.name = rewritten;
3843 CoalescedRefChange::Update(update)
3844 }
3845 CoalescedRefChange::Delete(mut delete) => {
3846 delete.name = rewritten;
3847 CoalescedRefChange::Delete(delete)
3848 }
3849 };
3850 grouped
3851 .entry(store.storage_dir.clone())
3852 .or_insert_with(|| (store, Vec::new()))
3853 .1
3854 .push(rewritten_change);
3855 }
3856 Ok(grouped.into_values().collect())
3857 }
3858
3859 #[allow(dead_code)]
3862 fn commit_loose(&self, changes: Vec<CoalescedRefChange>) -> Result<()> {
3863 self.commit_loose_hooked(changes, None, None)
3864 }
3865
3866 fn commit_loose_hooked(
3872 &self,
3873 changes: Vec<CoalescedRefChange>,
3874 hook: Option<&dyn ReferenceTransactionHook>,
3875 hook_updates: Option<&[RefTransactionHookUpdate]>,
3876 ) -> Result<()> {
3877 let has_reflogs = changes.iter().any(|change| {
3879 matches!(change, CoalescedRefChange::Update(update) if !update.reflog.is_empty())
3880 });
3881 let head_branch = if has_reflogs {
3882 self.head_symref_target()
3883 } else {
3884 None
3885 };
3886 let has_delete = changes
3887 .iter()
3888 .any(|change| matches!(change, CoalescedRefChange::Delete(_)));
3889 let update_count = changes
3890 .iter()
3891 .filter(|change| matches!(change, CoalescedRefChange::Update(_)))
3892 .count();
3893 let targeted_conflict_check = update_count == 1 && !has_delete;
3894 let conflict_names = if update_count > 0 && !targeted_conflict_check {
3895 let mut names = self
3896 .list_refs()?
3897 .into_iter()
3898 .map(|reference| reference.name)
3899 .collect::<BTreeSet<_>>();
3900 for change in &changes {
3901 if let CoalescedRefChange::Update(update) = change {
3902 names.insert(update.name.clone());
3903 }
3904 }
3905 Some(names)
3906 } else {
3907 None
3908 };
3909 let mut pending = Vec::with_capacity(changes.len() + usize::from(has_delete));
3910 for change in &changes {
3912 let name = change.name();
3913 if matches!(change, CoalescedRefChange::Update(_)) {
3914 let conflict_result = if targeted_conflict_check {
3915 self.check_ref_directory_conflict_targeted(name)
3916 } else if let Some(conflict_names) = conflict_names.as_ref() {
3917 check_ref_directory_conflict_in_names(name, conflict_names)
3918 } else {
3919 Ok(())
3920 };
3921 if let Err(err) = conflict_result {
3922 release_pending_locks(&pending);
3923 return Err(err);
3924 }
3925 }
3926 let path = self.ref_path(name);
3927 let parent = path
3928 .parent()
3929 .ok_or_else(|| GitError::InvalidPath("ref path has no parent".into()))?;
3930 if let Err(err) = fs::create_dir_all(parent) {
3931 release_pending_locks(&pending);
3932 if err.kind() == std::io::ErrorKind::NotADirectory {
3933 return Err(ref_directory_conflict_error(
3934 name,
3935 &parent_to_ref_name(&self.ref_base_dir(name), parent),
3936 ));
3937 }
3938 return Err(GitError::Io(err.to_string()));
3939 }
3940 let lock_path = match lock_path_for(&path) {
3941 Ok(lock_path) => lock_path,
3942 Err(err) => {
3943 release_pending_locks(&pending);
3944 return Err(err);
3945 }
3946 };
3947 if let Err(err) = fs::OpenOptions::new()
3948 .write(true)
3949 .create_new(true)
3950 .open(&lock_path)
3951 {
3952 release_pending_locks(&pending);
3953 return Err(GitError::Io(format!("could not lock ref {name}: {err}")));
3954 }
3955 let action = match change {
3956 CoalescedRefChange::Update(update) => PendingPathAction::Write {
3957 contents: write_loose_ref(&Ref {
3958 name: update.name.clone(),
3959 target: update.new.clone(),
3960 }),
3961 },
3962 CoalescedRefChange::Delete(_) => PendingPathAction::Delete,
3963 };
3964 pending.push(PendingPathChange {
3965 name: name.to_string(),
3966 path,
3967 lock_path,
3968 original: None,
3969 action,
3970 });
3971 }
3972
3973 let packed_path = self.storage_dir.join("packed-refs");
3974 let mut packed_refs = Vec::new();
3975 let mut use_packed_snapshot = false;
3976 if has_delete {
3977 let packed_lock_path = match lock_path_for(&packed_path) {
3978 Ok(lock_path) => lock_path,
3979 Err(err) => {
3980 release_pending_locks(&pending);
3981 return Err(err);
3982 }
3983 };
3984 if let Err(err) = fs::OpenOptions::new()
3985 .write(true)
3986 .create_new(true)
3987 .open(&packed_lock_path)
3988 {
3989 release_pending_locks(&pending);
3990 return Err(GitError::Io(format!("could not lock packed-refs: {err}")));
3991 }
3992 let packed_original = match fs::read(&packed_path) {
3993 Ok(bytes) => Some(bytes),
3994 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
3995 Err(err) => {
3996 release_pending_locks(&pending);
3997 let _ = fs::remove_file(&packed_lock_path);
3998 return Err(GitError::Io(err.to_string()));
3999 }
4000 };
4001 packed_refs = match &packed_original {
4002 Some(bytes) => match parse_packed_refs(self.format, bytes) {
4003 Ok(refs) => refs,
4004 Err(err) => {
4005 release_pending_locks(&pending);
4006 let _ = fs::remove_file(&packed_lock_path);
4007 return Err(err);
4008 }
4009 },
4010 None => Vec::new(),
4011 };
4012 use_packed_snapshot = true;
4013 pending.push(PendingPathChange {
4014 name: "packed-refs".into(),
4015 path: packed_path.clone(),
4016 lock_path: packed_lock_path,
4017 original: packed_original,
4018 action: PendingPathAction::ReleaseLock,
4019 });
4020 } else if packed_path.exists() {
4021 packed_refs = parse_packed_refs(self.format, &fs::read(&packed_path)?)?;
4022 use_packed_snapshot = true;
4023 }
4024 let packed_ref_targets = packed_refs
4025 .iter()
4026 .map(|reference| {
4027 (
4028 reference.reference.name.clone(),
4029 reference.reference.target.clone(),
4030 )
4031 })
4032 .collect::<HashMap<_, _>>();
4033
4034 let mut reflogs = Vec::new();
4038 let mut delete_names = BTreeSet::new();
4039 for index in 0..changes.len() {
4040 match &changes[index] {
4041 CoalescedRefChange::Update(update) => {
4042 let current = if use_packed_snapshot {
4043 match self.read_ref_from_packed_snapshot(&update.name, &packed_ref_targets)
4044 {
4045 Ok(current) => current,
4046 Err(err) => {
4047 release_pending_locks(&pending);
4048 return Err(err);
4049 }
4050 }
4051 } else {
4052 match self.read_ref(&update.name) {
4053 Ok(current) => current,
4054 Err(err) => {
4055 release_pending_locks(&pending);
4056 return Err(err);
4057 }
4058 }
4059 };
4060 if !matches!(update.precondition, RefPrecondition::Any)
4061 && !update.precondition.is_satisfied_by(current.as_ref())
4062 {
4063 release_pending_locks(&pending);
4064 return Err(GitError::Transaction(
4065 update.precondition.describe(&update.name),
4066 ));
4067 }
4068 pending[index].original = match read_optional_file(&pending[index].path) {
4069 Ok(original) => original,
4070 Err(err) => {
4071 release_pending_locks(&pending);
4072 return Err(err);
4073 }
4074 };
4075 if pending[index].original.is_none() && current.as_ref() == Some(&update.new) {
4076 pending[index].action = PendingPathAction::ReleaseLock;
4077 }
4078 for entry in &update.reflog {
4079 reflogs.push((update.name.clone(), entry.clone()));
4080 }
4081 }
4082 CoalescedRefChange::Delete(delete) => {
4083 let state = match self.read_locked_ref_state(&delete.name, &packed_ref_targets)
4084 {
4085 Ok(state) => state,
4086 Err(err) => {
4087 release_pending_locks(&pending);
4088 return Err(err);
4089 }
4090 };
4091 if let Err(err) = verify_delete_precondition(
4095 self,
4096 &delete.name,
4097 state.current.as_ref(),
4098 &delete.precondition,
4099 ) {
4100 release_pending_locks(&pending);
4101 return Err(err);
4102 }
4103 pending[index].original = if state.has_loose {
4104 match read_optional_file(&pending[index].path) {
4105 Ok(original) => original,
4106 Err(err) => {
4107 release_pending_locks(&pending);
4108 return Err(err);
4109 }
4110 }
4111 } else {
4112 None
4113 };
4114 delete_names.insert(delete.name.clone());
4115 }
4116 }
4117 }
4118
4119 if has_delete {
4120 let old_len = packed_refs.len();
4121 packed_refs.retain(|reference| !delete_names.contains(&reference.reference.name));
4122 if packed_refs.len() != old_len {
4123 let packed_bytes = match write_packed_refs(&packed_refs) {
4124 Ok(bytes) => bytes,
4125 Err(err) => {
4126 release_pending_locks(&pending);
4127 return Err(err);
4128 }
4129 };
4130 if let Some(packed) = pending.last_mut() {
4131 packed.action = PendingPathAction::Write {
4132 contents: packed_bytes,
4133 };
4134 }
4135 }
4136 }
4137
4138 for change in &pending {
4141 if let Err(err) = stage_pending_change(change) {
4142 release_pending_locks(&pending);
4143 return Err(err);
4144 }
4145 }
4146
4147 if let (Some(hook), Some(updates)) = (hook, hook_updates)
4151 && hook.run(RefTransactionPhase::Prepared, updates)?
4152 {
4153 release_pending_locks(&pending);
4154 return Err(ref_transaction_hook_abort(RefTransactionPhase::Prepared));
4155 }
4156
4157 for index in 0..pending.len() {
4160 if let Err(err) = maybe_fail_loose_commit_action(index) {
4161 rollback_after_apply(&pending, index);
4162 return Err(err);
4163 }
4164 if let Err(err) = apply_pending_change(&pending[index]) {
4165 rollback_after_apply(&pending, index + 1);
4166 return Err(err);
4167 }
4168 if matches!(pending[index].action, PendingPathAction::Delete) {
4169 self.prune_empty_ref_dirs(&pending[index].name);
4170 }
4171 }
4172
4173 for name in &delete_names {
4177 self.remove_reflog_file(name);
4178 }
4179 let head_mirror = Self::head_reflog_mirror(head_branch.as_deref(), &reflogs);
4182 reflogs.extend(head_mirror);
4183 for (name, entry) in reflogs {
4185 self.append_reflog(&name, &entry)?;
4186 }
4187 if let (Some(hook), Some(updates)) = (hook, hook_updates) {
4190 hook.run(RefTransactionPhase::Committed, updates)?;
4191 }
4192 Ok(())
4193 }
4194
4195 fn read_ref_from_packed_snapshot(
4196 &self,
4197 name: &str,
4198 packed_refs: &HashMap<String, RefTarget>,
4199 ) -> Result<Option<RefTarget>> {
4200 let state = self.read_locked_ref_state(name, packed_refs)?;
4201 Ok(state.current)
4202 }
4203
4204 fn read_locked_ref_state(
4205 &self,
4206 name: &str,
4207 packed_refs: &HashMap<String, RefTarget>,
4208 ) -> Result<LockedRefState> {
4209 let loose = self.read_loose_ref(name)?;
4210 let current = if let Some(reference) = loose.as_ref() {
4211 Some(reference.target.clone())
4212 } else {
4213 packed_refs.get(name).cloned()
4214 };
4215 Ok(LockedRefState {
4216 current,
4217 has_loose: loose.is_some(),
4218 })
4219 }
4220}
4221
4222struct LockedRefState {
4223 current: Option<RefTarget>,
4224 has_loose: bool,
4225}
4226
4227enum CoalescedRefChange {
4228 Update(CoalescedRefUpdate),
4229 Delete(CoalescedRefDelete),
4230}
4231
4232impl CoalescedRefChange {
4233 fn name(&self) -> &str {
4234 match self {
4235 Self::Update(update) => &update.name,
4236 Self::Delete(delete) => &delete.name,
4237 }
4238 }
4239}
4240
4241struct CoalescedRefUpdate {
4243 name: String,
4244 precondition: RefPrecondition,
4245 new: RefTarget,
4246 reflog: Vec<ReflogEntry>,
4247}
4248
4249struct CoalescedRefDelete {
4250 name: String,
4251 precondition: RefDeletePrecondition,
4252}
4253
4254fn coalesce_ref_changes(changes: Vec<QueuedRefChange>) -> Result<Vec<CoalescedRefChange>> {
4255 let has_delete = changes
4256 .iter()
4257 .any(|change| matches!(change, QueuedRefChange::Delete(_)));
4258 if !has_delete {
4259 let updates = changes
4260 .into_iter()
4261 .map(|change| match change {
4262 QueuedRefChange::Update(update) => update,
4263 QueuedRefChange::Delete(_) => unreachable!("has_delete was false"),
4264 })
4265 .collect::<Vec<_>>();
4266 return coalesce_ref_updates(updates).map(|updates| {
4267 updates
4268 .into_iter()
4269 .map(CoalescedRefChange::Update)
4270 .collect()
4271 });
4272 }
4273
4274 let mut seen = BTreeSet::new();
4275 let mut coalesced = Vec::with_capacity(changes.len());
4276 for change in changes {
4277 let name = match &change {
4278 QueuedRefChange::Update(update) => &update.name,
4279 QueuedRefChange::Delete(delete) => &delete.name,
4280 };
4281 match &change {
4282 QueuedRefChange::Update(update) => validate_ref_name_for_update(&update.name)?,
4283 QueuedRefChange::Delete(delete) => validate_ref_name_for_read(&delete.name)?,
4284 }
4285 if !seen.insert(name.clone()) {
4286 return Err(GitError::Transaction(format!(
4287 "ref {name} appears more than once in transaction"
4288 )));
4289 }
4290 coalesced.push(match change {
4291 QueuedRefChange::Update(update) => CoalescedRefChange::Update(CoalescedRefUpdate {
4292 name: update.name,
4293 precondition: update.precondition,
4294 new: update.new,
4295 reflog: update.reflog.into_iter().collect(),
4296 }),
4297 QueuedRefChange::Delete(delete) => CoalescedRefChange::Delete(CoalescedRefDelete {
4298 name: delete.name,
4299 precondition: delete.precondition,
4300 }),
4301 });
4302 }
4303 Ok(coalesced)
4304}
4305
4306fn coalesce_ref_updates(updates: Vec<QueuedUpdate>) -> Result<Vec<CoalescedRefUpdate>> {
4311 let mut order: Vec<String> = Vec::new();
4312 let mut by_name: HashMap<String, CoalescedRefUpdate> = HashMap::new();
4313 for update in updates {
4314 validate_ref_name_for_update(&update.name)?;
4315 match by_name.get_mut(&update.name) {
4316 Some(existing) => {
4317 existing.new = update.new;
4318 if let Some(entry) = update.reflog {
4319 existing.reflog.push(entry);
4320 }
4321 }
4322 None => {
4323 order.push(update.name.clone());
4324 by_name.insert(
4325 update.name.clone(),
4326 CoalescedRefUpdate {
4327 name: update.name,
4328 precondition: update.precondition,
4329 new: update.new,
4330 reflog: update.reflog.into_iter().collect(),
4331 },
4332 );
4333 }
4334 }
4335 }
4336 let mut coalesced = Vec::with_capacity(order.len());
4337 for name in order {
4338 if let Some(update) = by_name.remove(&name) {
4339 coalesced.push(update);
4340 }
4341 }
4342 Ok(coalesced)
4343}
4344
4345struct PendingPathChange {
4348 name: String,
4349 path: PathBuf,
4350 lock_path: PathBuf,
4351 original: Option<Vec<u8>>,
4352 action: PendingPathAction,
4353}
4354
4355enum PendingPathAction {
4356 Write { contents: Vec<u8> },
4357 Delete,
4358 ReleaseLock,
4359}
4360
4361struct RefDirPruneGuard<'a> {
4362 store: &'a FileRefStore,
4363 name: String,
4364}
4365
4366impl Drop for RefDirPruneGuard<'_> {
4367 fn drop(&mut self) {
4368 self.store.prune_empty_ref_dirs(&self.name);
4369 }
4370}
4371
4372struct DeleteLock {
4373 path: PathBuf,
4374 file: Option<fs::File>,
4375 active: bool,
4376}
4377
4378impl DeleteLock {
4379 fn acquire(path: PathBuf) -> std::result::Result<Self, RefDeleteError> {
4380 match fs::OpenOptions::new()
4381 .write(true)
4382 .create_new(true)
4383 .open(&path)
4384 {
4385 Ok(file) => Ok(Self {
4386 path,
4387 file: Some(file),
4388 active: true,
4389 }),
4390 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
4391 Err(RefDeleteError::Locked)
4392 }
4393 Err(err) => Err(RefDeleteError::Io(err)),
4394 }
4395 }
4396
4397 fn write_all(&mut self, bytes: &[u8]) -> std::result::Result<(), RefDeleteError> {
4398 let Some(file) = self.file.as_mut() else {
4399 return Err(RefDeleteError::Io(std::io::Error::other(
4400 "lock file is already closed",
4401 )));
4402 };
4403 file.set_len(0)?;
4404 file.write_all(bytes)?;
4405 file.sync_all()?;
4406 Ok(())
4407 }
4408
4409 fn close(mut self) -> PathBuf {
4410 self.active = false;
4411 let _ = self.file.take();
4412 self.path.clone()
4413 }
4414
4415 fn remove(mut self) {
4416 self.active = false;
4417 let _ = self.file.take();
4418 let _ = fs::remove_file(&self.path);
4419 }
4420}
4421
4422impl Drop for DeleteLock {
4423 fn drop(&mut self) {
4424 if self.active {
4425 let _ = self.file.take();
4426 let _ = fs::remove_file(&self.path);
4427 }
4428 }
4429}
4430
4431struct ReftableListLock {
4432 list_path: PathBuf,
4433 lock_path: PathBuf,
4434 file: Option<fs::File>,
4435 active: bool,
4436}
4437
4438impl ReftableListLock {
4439 fn acquire(list_path: PathBuf, lock_path: PathBuf, timeout_millis: u64) -> Result<Self> {
4440 let start = SystemTime::now();
4441 loop {
4442 match fs::OpenOptions::new()
4443 .write(true)
4444 .create_new(true)
4445 .open(&lock_path)
4446 {
4447 Ok(file) => {
4448 return Ok(Self {
4449 list_path,
4450 lock_path,
4451 file: Some(file),
4452 active: true,
4453 });
4454 }
4455 Err(err)
4456 if err.kind() == std::io::ErrorKind::AlreadyExists && timeout_millis > 0 =>
4457 {
4458 let elapsed = start
4459 .elapsed()
4460 .unwrap_or_else(|_| Duration::from_millis(timeout_millis + 1));
4461 if elapsed.as_millis() as u64 >= timeout_millis {
4462 return Err(GitError::Io(format!(
4463 "cannot lock references: {}: File exists",
4464 lock_path.display()
4465 )));
4466 }
4467 thread::sleep(Duration::from_millis(50));
4468 }
4469 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
4470 return Err(GitError::Io(format!(
4471 "cannot lock references: {}: File exists",
4472 lock_path.display()
4473 )));
4474 }
4475 Err(err) => return Err(err.into()),
4476 }
4477 }
4478 }
4479
4480 fn commit(mut self, bytes: &[u8]) -> Result<()> {
4481 let Some(mut file) = self.file.take() else {
4482 return Err(GitError::Io("reftable list lock is already closed".into()));
4483 };
4484 file.set_len(0)?;
4485 file.write_all(bytes)?;
4486 file.sync_all()?;
4487 drop(file);
4488 fs::rename(&self.lock_path, &self.list_path)
4489 .map_err(|err| GitError::Io(err.to_string()))?;
4490 self.active = false;
4491 Ok(())
4492 }
4493}
4494
4495impl Drop for ReftableListLock {
4496 fn drop(&mut self) {
4497 if self.active {
4498 let _ = self.file.take();
4499 let _ = fs::remove_file(&self.lock_path);
4500 }
4501 }
4502}
4503
4504fn checked_delete_oid(
4505 expected: Option<ObjectId>,
4506 current: Option<RefTarget>,
4507) -> std::result::Result<ObjectId, RefDeleteError> {
4508 let Some(current) = current else {
4509 return Err(RefDeleteError::NotFound);
4510 };
4511 let RefTarget::Direct(actual) = current else {
4512 return Err(RefDeleteError::ExpectedMismatch {
4513 expected,
4514 actual: None,
4515 });
4516 };
4517 if let Some(expected_oid) = expected
4518 && expected_oid != actual
4519 {
4520 return Err(RefDeleteError::ExpectedMismatch {
4521 expected: Some(expected_oid),
4522 actual: Some(actual),
4523 });
4524 }
4525 Ok(actual)
4526}
4527
4528fn verify_delete_precondition(
4534 store: &FileRefStore,
4535 name: &str,
4536 current: Option<&RefTarget>,
4537 precondition: &RefDeletePrecondition,
4538) -> Result<()> {
4539 let Some(current) = current else {
4540 return Err(GitError::Transaction(format!("ref {name} not found")));
4541 };
4542 match precondition {
4543 RefDeletePrecondition::Any => {
4544 peeled_oid_for_delete(store, current)?;
4545 Ok(())
4546 }
4547 RefDeletePrecondition::Immediate(expected) if current == expected => {
4548 peeled_oid_for_delete(store, current)?;
4549 Ok(())
4550 }
4551 RefDeletePrecondition::Immediate(_) => Err(delete_precondition_mismatch(name)),
4552 RefDeletePrecondition::Direct(expected) => {
4553 let RefTarget::Direct(actual) = current else {
4554 return Err(delete_precondition_mismatch(name));
4555 };
4556 if let Some(expected) = expected
4557 && expected != actual
4558 {
4559 return Err(delete_precondition_mismatch(name));
4560 }
4561 Ok(())
4562 }
4563 RefDeletePrecondition::Peeled(expected) => {
4564 let actual = peeled_oid_for_delete(store, current)?;
4565 if actual == Some(*expected) {
4566 Ok(())
4567 } else {
4568 Err(delete_precondition_mismatch(name))
4569 }
4570 }
4571 }
4572}
4573
4574fn peeled_oid_for_delete(store: &FileRefStore, target: &RefTarget) -> Result<Option<ObjectId>> {
4575 match target {
4576 RefTarget::Direct(oid) => Ok(Some(*oid)),
4577 RefTarget::Symbolic(name) => resolve_ref_peeled(store, name),
4578 }
4579}
4580
4581fn delete_precondition_mismatch(name: &str) -> GitError {
4582 GitError::Transaction(format!("expected ref {name} to match"))
4583}
4584
4585fn ref_delete_error_from_git(err: GitError) -> RefDeleteError {
4586 match err {
4587 GitError::InvalidPath(_) => RefDeleteError::InvalidName,
4588 GitError::NotFound(_) => RefDeleteError::NotFound,
4589 GitError::Io(message) if message.contains("File exists") => RefDeleteError::Locked,
4590 GitError::Io(message) if message.contains("could not lock") => RefDeleteError::Locked,
4591 GitError::Transaction(message) if message.contains("could not lock") => {
4592 RefDeleteError::Locked
4593 }
4594 other => RefDeleteError::Io(std::io::Error::other(other.to_string())),
4595 }
4596}
4597
4598fn read_optional_file(path: &Path) -> Result<Option<Vec<u8>>> {
4599 match fs::read(path) {
4600 Ok(bytes) => Ok(Some(bytes)),
4601 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
4602 Err(err) if err.kind() == std::io::ErrorKind::IsADirectory => Ok(None),
4603 Err(err) => Err(GitError::Io(err.to_string())),
4604 }
4605}
4606
4607fn remove_empty_dir_tree(path: &Path) -> std::io::Result<()> {
4611 let meta = match fs::symlink_metadata(path) {
4612 Ok(meta) => meta,
4613 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
4614 Err(err) => return Err(err),
4615 };
4616 if !meta.is_dir() {
4617 return Ok(());
4618 }
4619 for entry in fs::read_dir(path)? {
4620 let entry = entry?;
4621 if entry.file_type()?.is_dir() {
4622 remove_empty_dir_tree(&entry.path())?;
4623 } else {
4624 return Err(std::io::Error::other("directory not empty"));
4625 }
4626 }
4627 fs::remove_dir(path)
4628}
4629
4630fn stage_lock_file(lock_path: &Path, contents: &[u8]) -> Result<()> {
4631 let mut file = fs::OpenOptions::new()
4632 .write(true)
4633 .truncate(true)
4634 .open(lock_path)?;
4635 file.write_all(contents)?;
4636 Ok(())
4637}
4638
4639fn stage_pending_change(change: &PendingPathChange) -> Result<()> {
4640 match &change.action {
4641 PendingPathAction::Write { contents } => stage_lock_file(&change.lock_path, contents),
4642 PendingPathAction::Delete => stage_lock_file(&change.lock_path, b"delete\n"),
4643 PendingPathAction::ReleaseLock => Ok(()),
4644 }
4645}
4646
4647fn apply_pending_change(change: &PendingPathChange) -> Result<()> {
4648 match &change.action {
4649 PendingPathAction::Write { .. } => {
4650 remove_empty_dir_tree(&change.path).map_err(|err| GitError::Io(err.to_string()))?;
4654 fs::rename(&change.lock_path, &change.path).map_err(|err| GitError::Io(err.to_string()))
4655 }
4656 PendingPathAction::Delete => {
4657 if change.original.is_some() {
4658 fs::remove_file(&change.path).map_err(|err| GitError::Io(err.to_string()))?;
4659 }
4660 fs::remove_file(&change.lock_path).map_err(|err| GitError::Io(err.to_string()))
4661 }
4662 PendingPathAction::ReleaseLock => {
4663 fs::remove_file(&change.lock_path).map_err(|err| GitError::Io(err.to_string()))
4664 }
4665 }
4666}
4667
4668fn release_pending_locks(pending: &[PendingPathChange]) {
4671 for change in pending {
4672 let _ = fs::remove_file(&change.lock_path);
4673 }
4674}
4675
4676fn rollback_after_apply(pending: &[PendingPathChange], applied: usize) {
4680 for change in pending.iter().take(applied) {
4681 if matches!(change.action, PendingPathAction::ReleaseLock) {
4682 let _ = fs::remove_file(&change.lock_path);
4683 continue;
4684 }
4685 match &change.original {
4686 Some(bytes) => {
4687 let _ = restore_file_atomically(&change.path, bytes);
4688 }
4689 None => {
4690 let _ = fs::remove_file(&change.path);
4691 }
4692 }
4693 let _ = fs::remove_file(&change.lock_path);
4694 }
4695 for change in pending.iter().skip(applied) {
4696 let _ = fs::remove_file(&change.lock_path);
4697 }
4698}
4699
4700#[cfg(test)]
4701thread_local! {
4702 static FAIL_LOOSE_COMMIT_ACTION: std::cell::Cell<Option<usize>> =
4703 const { std::cell::Cell::new(None) };
4704}
4705
4706#[cfg(test)]
4707fn set_fail_loose_commit_action_for_test(index: Option<usize>) {
4708 FAIL_LOOSE_COMMIT_ACTION.with(|cell| cell.set(index));
4709}
4710
4711#[cfg(test)]
4712fn maybe_fail_loose_commit_action(index: usize) -> Result<()> {
4713 let should_fail = FAIL_LOOSE_COMMIT_ACTION.with(|cell| cell.get() == Some(index));
4714 if should_fail {
4715 FAIL_LOOSE_COMMIT_ACTION.with(|cell| cell.set(None));
4716 return Err(GitError::Io(format!(
4717 "injected loose ref transaction failure at action {index}"
4718 )));
4719 }
4720 Ok(())
4721}
4722
4723#[cfg(not(test))]
4724fn maybe_fail_loose_commit_action(_index: usize) -> Result<()> {
4725 Ok(())
4726}
4727
4728fn restore_file_atomically(path: &Path, bytes: &[u8]) -> Result<()> {
4731 if let Some(parent) = path.parent() {
4732 fs::create_dir_all(parent)?;
4733 }
4734 write_locked(path, bytes)
4735}
4736
4737#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
4738pub struct FullRefName<'a> {
4739 name: &'a str,
4740}
4741
4742impl<'a> FullRefName<'a> {
4743 pub fn new(name: &'a str) -> Result<Self> {
4744 validate_ref_name(name)?;
4745 Ok(Self { name })
4746 }
4747
4748 pub fn as_str(&self) -> &str {
4749 self.name
4750 }
4751
4752 pub fn into_str(self) -> &'a str {
4753 self.name
4754 }
4755
4756 pub fn to_owned(&self) -> FullRefNameBuf {
4757 FullRefNameBuf {
4758 name: self.name.to_string(),
4759 }
4760 }
4761
4762 pub fn as_branch(&self) -> Result<BranchRefName<'a>> {
4763 BranchRefName::from_full_ref(*self)
4764 }
4765
4766 pub fn as_tag(&self) -> Result<TagRefName<'a>> {
4767 TagRefName::from_full_ref(*self)
4768 }
4769
4770 pub fn as_remote(&self) -> Result<RemoteRefName<'a>> {
4771 RemoteRefName::from_full_ref(*self)
4772 }
4773}
4774
4775impl AsRef<str> for FullRefName<'_> {
4776 fn as_ref(&self) -> &str {
4777 self.as_str()
4778 }
4779}
4780
4781impl fmt::Display for FullRefName<'_> {
4782 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4783 f.write_str(self.as_str())
4784 }
4785}
4786
4787#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4788pub struct FullRefNameBuf {
4789 name: String,
4790}
4791
4792impl FullRefNameBuf {
4793 pub fn new(name: impl Into<String>) -> Result<Self> {
4794 let name = name.into();
4795 validate_ref_name(&name)?;
4796 Ok(Self { name })
4797 }
4798
4799 pub fn as_ref_name(&self) -> FullRefName<'_> {
4800 FullRefName { name: &self.name }
4801 }
4802
4803 pub fn as_str(&self) -> &str {
4804 &self.name
4805 }
4806
4807 pub fn into_string(self) -> String {
4808 self.name
4809 }
4810
4811 pub fn as_branch(&self) -> Result<BranchRefName<'_>> {
4812 self.as_ref_name().as_branch()
4813 }
4814
4815 pub fn as_tag(&self) -> Result<TagRefName<'_>> {
4816 self.as_ref_name().as_tag()
4817 }
4818
4819 pub fn as_remote(&self) -> Result<RemoteRefName<'_>> {
4820 self.as_ref_name().as_remote()
4821 }
4822}
4823
4824impl AsRef<str> for FullRefNameBuf {
4825 fn as_ref(&self) -> &str {
4826 self.as_str()
4827 }
4828}
4829
4830impl Borrow<str> for FullRefNameBuf {
4831 fn borrow(&self) -> &str {
4832 self.as_str()
4833 }
4834}
4835
4836impl Deref for FullRefNameBuf {
4837 type Target = str;
4838
4839 fn deref(&self) -> &Self::Target {
4840 self.as_str()
4841 }
4842}
4843
4844impl fmt::Display for FullRefNameBuf {
4845 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4846 f.write_str(self.as_str())
4847 }
4848}
4849
4850#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
4851pub struct BranchRefName<'a> {
4852 name: &'a str,
4853}
4854
4855impl<'a> BranchRefName<'a> {
4856 pub const PREFIX: &'static str = "refs/heads/";
4857
4858 pub fn from_full(name: &'a str) -> Result<Self> {
4859 let full = FullRefName::new(name)?;
4860 Self::from_full_ref(full)
4861 }
4862
4863 pub fn from_full_ref(name: FullRefName<'a>) -> Result<Self> {
4864 validate_namespaced_ref(name.as_str(), Self::PREFIX, "branch")?;
4865 Ok(Self {
4866 name: name.into_str(),
4867 })
4868 }
4869
4870 pub fn as_full_ref_name(&self) -> FullRefName<'a> {
4871 FullRefName { name: self.name }
4872 }
4873
4874 pub fn as_str(&self) -> &str {
4875 self.name
4876 }
4877
4878 pub fn branch_name(&self) -> &str {
4879 self.short_name()
4880 }
4881
4882 pub fn short_name(&self) -> &str {
4883 &self.name[Self::PREFIX.len()..]
4884 }
4885
4886 pub fn into_str(self) -> &'a str {
4887 self.name
4888 }
4889
4890 pub fn to_owned(&self) -> BranchRefNameBuf {
4891 BranchRefNameBuf {
4892 name: self.name.to_string(),
4893 }
4894 }
4895}
4896
4897impl AsRef<str> for BranchRefName<'_> {
4898 fn as_ref(&self) -> &str {
4899 self.as_str()
4900 }
4901}
4902
4903impl fmt::Display for BranchRefName<'_> {
4904 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4905 f.write_str(self.as_str())
4906 }
4907}
4908
4909impl<'a> From<BranchRefName<'a>> for FullRefName<'a> {
4910 fn from(name: BranchRefName<'a>) -> Self {
4911 name.as_full_ref_name()
4912 }
4913}
4914
4915#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4916pub struct BranchRefNameBuf {
4917 name: String,
4918}
4919
4920impl BranchRefNameBuf {
4921 pub fn from_branch_name(branch: &str) -> Result<Self> {
4922 validate_short_ref_name("branch", branch)?;
4923 let name = format!("{}{}", BranchRefName::PREFIX, branch);
4924 Self::from_full(name)
4925 }
4926
4927 pub fn from_full(name: impl Into<String>) -> Result<Self> {
4928 let name = name.into();
4929 BranchRefName::from_full(&name)?;
4930 Ok(Self { name })
4931 }
4932
4933 pub fn as_ref_name(&self) -> BranchRefName<'_> {
4934 BranchRefName { name: &self.name }
4935 }
4936
4937 pub fn as_full_ref_name(&self) -> FullRefName<'_> {
4938 FullRefName { name: &self.name }
4939 }
4940
4941 pub fn as_str(&self) -> &str {
4942 &self.name
4943 }
4944
4945 pub fn branch_name(&self) -> &str {
4946 self.short_name()
4947 }
4948
4949 pub fn short_name(&self) -> &str {
4950 &self.name[BranchRefName::PREFIX.len()..]
4951 }
4952
4953 pub fn into_string(self) -> String {
4954 self.name
4955 }
4956}
4957
4958impl AsRef<str> for BranchRefNameBuf {
4959 fn as_ref(&self) -> &str {
4960 self.as_str()
4961 }
4962}
4963
4964impl Borrow<str> for BranchRefNameBuf {
4965 fn borrow(&self) -> &str {
4966 self.as_str()
4967 }
4968}
4969
4970impl Deref for BranchRefNameBuf {
4971 type Target = str;
4972
4973 fn deref(&self) -> &Self::Target {
4974 self.as_str()
4975 }
4976}
4977
4978impl fmt::Display for BranchRefNameBuf {
4979 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4980 f.write_str(self.as_str())
4981 }
4982}
4983
4984impl From<BranchRefNameBuf> for FullRefNameBuf {
4985 fn from(name: BranchRefNameBuf) -> Self {
4986 Self { name: name.name }
4987 }
4988}
4989
4990#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
4991pub struct TagRefName<'a> {
4992 name: &'a str,
4993}
4994
4995impl<'a> TagRefName<'a> {
4996 pub const PREFIX: &'static str = "refs/tags/";
4997
4998 pub fn from_full(name: &'a str) -> Result<Self> {
4999 let full = FullRefName::new(name)?;
5000 Self::from_full_ref(full)
5001 }
5002
5003 pub fn from_full_ref(name: FullRefName<'a>) -> Result<Self> {
5004 validate_namespaced_ref(name.as_str(), Self::PREFIX, "tag")?;
5005 Ok(Self {
5006 name: name.into_str(),
5007 })
5008 }
5009
5010 pub fn as_full_ref_name(&self) -> FullRefName<'a> {
5011 FullRefName { name: self.name }
5012 }
5013
5014 pub fn as_str(&self) -> &str {
5015 self.name
5016 }
5017
5018 pub fn tag_name(&self) -> &str {
5019 self.short_name()
5020 }
5021
5022 pub fn short_name(&self) -> &str {
5023 &self.name[Self::PREFIX.len()..]
5024 }
5025
5026 pub fn into_str(self) -> &'a str {
5027 self.name
5028 }
5029
5030 pub fn to_owned(&self) -> TagRefNameBuf {
5031 TagRefNameBuf {
5032 name: self.name.to_string(),
5033 }
5034 }
5035}
5036
5037impl AsRef<str> for TagRefName<'_> {
5038 fn as_ref(&self) -> &str {
5039 self.as_str()
5040 }
5041}
5042
5043impl fmt::Display for TagRefName<'_> {
5044 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5045 f.write_str(self.as_str())
5046 }
5047}
5048
5049impl<'a> From<TagRefName<'a>> for FullRefName<'a> {
5050 fn from(name: TagRefName<'a>) -> Self {
5051 name.as_full_ref_name()
5052 }
5053}
5054
5055#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
5056pub struct TagRefNameBuf {
5057 name: String,
5058}
5059
5060impl TagRefNameBuf {
5061 pub fn from_tag_name(tag: &str) -> Result<Self> {
5062 if tag.starts_with('-') || tag == "HEAD" {
5065 return Err(GitError::InvalidPath(format!("invalid tag name {tag}")));
5066 }
5067 Self::from_tag_name_unrestricted(tag)
5068 }
5069
5070 pub fn from_tag_name_unrestricted(tag: &str) -> Result<Self> {
5075 let name = format!("{}{}", TagRefName::PREFIX, tag);
5076 check_refname_format(&name, false)?;
5077 Ok(Self { name })
5078 }
5079
5080 pub fn from_full(name: impl Into<String>) -> Result<Self> {
5081 let name = name.into();
5082 TagRefName::from_full(&name)?;
5083 Ok(Self { name })
5084 }
5085
5086 pub fn as_ref_name(&self) -> TagRefName<'_> {
5087 TagRefName { name: &self.name }
5088 }
5089
5090 pub fn as_full_ref_name(&self) -> FullRefName<'_> {
5091 FullRefName { name: &self.name }
5092 }
5093
5094 pub fn as_str(&self) -> &str {
5095 &self.name
5096 }
5097
5098 pub fn tag_name(&self) -> &str {
5099 self.short_name()
5100 }
5101
5102 pub fn short_name(&self) -> &str {
5103 &self.name[TagRefName::PREFIX.len()..]
5104 }
5105
5106 pub fn into_string(self) -> String {
5107 self.name
5108 }
5109}
5110
5111impl AsRef<str> for TagRefNameBuf {
5112 fn as_ref(&self) -> &str {
5113 self.as_str()
5114 }
5115}
5116
5117impl Borrow<str> for TagRefNameBuf {
5118 fn borrow(&self) -> &str {
5119 self.as_str()
5120 }
5121}
5122
5123impl Deref for TagRefNameBuf {
5124 type Target = str;
5125
5126 fn deref(&self) -> &Self::Target {
5127 self.as_str()
5128 }
5129}
5130
5131impl fmt::Display for TagRefNameBuf {
5132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5133 f.write_str(self.as_str())
5134 }
5135}
5136
5137impl From<TagRefNameBuf> for FullRefNameBuf {
5138 fn from(name: TagRefNameBuf) -> Self {
5139 Self { name: name.name }
5140 }
5141}
5142
5143#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
5144pub struct RemoteRefName<'a> {
5145 name: &'a str,
5146}
5147
5148impl<'a> RemoteRefName<'a> {
5149 pub const PREFIX: &'static str = "refs/remotes/";
5150
5151 pub fn from_full(name: &'a str) -> Result<Self> {
5152 let full = FullRefName::new(name)?;
5153 Self::from_full_ref(full)
5154 }
5155
5156 pub fn from_full_ref(name: FullRefName<'a>) -> Result<Self> {
5157 validate_namespaced_ref(name.as_str(), Self::PREFIX, "remote")?;
5158 Ok(Self {
5159 name: name.into_str(),
5160 })
5161 }
5162
5163 pub fn as_full_ref_name(&self) -> FullRefName<'a> {
5164 FullRefName { name: self.name }
5165 }
5166
5167 pub fn as_str(&self) -> &str {
5168 self.name
5169 }
5170
5171 pub fn short_name(&self) -> &str {
5172 &self.name[Self::PREFIX.len()..]
5173 }
5174
5175 pub fn remote_name(&self) -> &str {
5176 match self.short_name().split_once('/') {
5177 Some((remote, _branch)) => remote,
5178 None => self.short_name(),
5179 }
5180 }
5181
5182 pub fn remote_branch(&self) -> Option<&str> {
5183 self.short_name()
5184 .split_once('/')
5185 .map(|(_remote, branch)| branch)
5186 }
5187
5188 pub fn into_str(self) -> &'a str {
5189 self.name
5190 }
5191
5192 pub fn to_owned(&self) -> RemoteRefNameBuf {
5193 RemoteRefNameBuf {
5194 name: self.name.to_string(),
5195 }
5196 }
5197}
5198
5199impl AsRef<str> for RemoteRefName<'_> {
5200 fn as_ref(&self) -> &str {
5201 self.as_str()
5202 }
5203}
5204
5205impl fmt::Display for RemoteRefName<'_> {
5206 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5207 f.write_str(self.as_str())
5208 }
5209}
5210
5211impl<'a> From<RemoteRefName<'a>> for FullRefName<'a> {
5212 fn from(name: RemoteRefName<'a>) -> Self {
5213 name.as_full_ref_name()
5214 }
5215}
5216
5217#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
5218pub struct RemoteRefNameBuf {
5219 name: String,
5220}
5221
5222impl RemoteRefNameBuf {
5223 pub fn from_short_name(name: &str) -> Result<Self> {
5224 validate_short_ref_name("remote ref", name)?;
5225 let name = format!("{}{}", RemoteRefName::PREFIX, name);
5226 Self::from_full(name)
5227 }
5228
5229 pub fn from_remote_branch(remote: &str, branch: &str) -> Result<Self> {
5230 validate_remote_name(remote)?;
5231 validate_short_ref_name("remote branch", branch)?;
5232 let name = format!("{}{}/{}", RemoteRefName::PREFIX, remote, branch);
5233 Self::from_full(name)
5234 }
5235
5236 pub fn from_full(name: impl Into<String>) -> Result<Self> {
5237 let name = name.into();
5238 RemoteRefName::from_full(&name)?;
5239 Ok(Self { name })
5240 }
5241
5242 pub fn as_ref_name(&self) -> RemoteRefName<'_> {
5243 RemoteRefName { name: &self.name }
5244 }
5245
5246 pub fn as_full_ref_name(&self) -> FullRefName<'_> {
5247 FullRefName { name: &self.name }
5248 }
5249
5250 pub fn as_str(&self) -> &str {
5251 &self.name
5252 }
5253
5254 pub fn short_name(&self) -> &str {
5255 &self.name[RemoteRefName::PREFIX.len()..]
5256 }
5257
5258 pub fn remote_name(&self) -> &str {
5259 match self.short_name().split_once('/') {
5260 Some((remote, _branch)) => remote,
5261 None => self.short_name(),
5262 }
5263 }
5264
5265 pub fn remote_branch(&self) -> Option<&str> {
5266 self.short_name()
5267 .split_once('/')
5268 .map(|(_remote, branch)| branch)
5269 }
5270
5271 pub fn into_string(self) -> String {
5272 self.name
5273 }
5274}
5275
5276impl AsRef<str> for RemoteRefNameBuf {
5277 fn as_ref(&self) -> &str {
5278 self.as_str()
5279 }
5280}
5281
5282impl Borrow<str> for RemoteRefNameBuf {
5283 fn borrow(&self) -> &str {
5284 self.as_str()
5285 }
5286}
5287
5288impl Deref for RemoteRefNameBuf {
5289 type Target = str;
5290
5291 fn deref(&self) -> &Self::Target {
5292 self.as_str()
5293 }
5294}
5295
5296impl fmt::Display for RemoteRefNameBuf {
5297 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5298 f.write_str(self.as_str())
5299 }
5300}
5301
5302impl From<RemoteRefNameBuf> for FullRefNameBuf {
5303 fn from(name: RemoteRefNameBuf) -> Self {
5304 Self { name: name.name }
5305 }
5306}
5307
5308pub fn branch_ref_name(branch: &str) -> Result<String> {
5309 BranchRefNameBuf::from_branch_name(branch).map(BranchRefNameBuf::into_string)
5310}
5311
5312pub fn branch_ref_name_for_read(branch: &str) -> Result<String> {
5313 let name = format!("{}{}", BranchRefName::PREFIX, branch);
5314 if validate_ref_name(&name).is_err() {
5315 if name.contains("..") {
5316 validate_ref_path_safe_for_read(&name)?;
5317 } else {
5318 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
5319 }
5320 }
5321 Ok(name)
5322}
5323
5324pub fn branch_ref_name_for_source(branch: &str) -> Result<String> {
5325 if branch.starts_with("--") {
5326 return Err(GitError::InvalidPath(format!(
5327 "invalid branch name {branch}"
5328 )));
5329 }
5330 let name = format!("{}{}", BranchRefName::PREFIX, branch);
5331 check_refname_format(&name, false)?;
5332 Ok(name)
5333}
5334
5335pub fn tag_ref_name(tag: &str) -> Result<String> {
5336 TagRefNameBuf::from_tag_name(tag).map(TagRefNameBuf::into_string)
5337}
5338
5339fn write_locked(path: &Path, bytes: &[u8]) -> Result<()> {
5340 write_locked_with_timeout(path, bytes, 0)
5341}
5342
5343fn write_locked_with_timeout(path: &Path, bytes: &[u8], timeout_millis: u64) -> Result<()> {
5344 let lock_path = lock_path_for(path)?;
5345 let start = SystemTime::now();
5346 loop {
5347 match fs::OpenOptions::new()
5348 .write(true)
5349 .create_new(true)
5350 .open(&lock_path)
5351 {
5352 Ok(mut file) => {
5353 file.write_all(bytes)?;
5354 file.sync_all()?;
5355 break;
5356 }
5357 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists && timeout_millis > 0 => {
5358 let elapsed = start
5359 .elapsed()
5360 .unwrap_or_else(|_| Duration::from_millis(timeout_millis + 1));
5361 if elapsed.as_millis() as u64 >= timeout_millis {
5362 return Err(GitError::Io(format!(
5363 "could not lock {}: File exists",
5364 path.display()
5365 )));
5366 }
5367 thread::sleep(Duration::from_millis(50));
5368 }
5369 Err(err) => return Err(err.into()),
5370 }
5371 }
5372 match fs::rename(&lock_path, path) {
5373 Ok(()) => Ok(()),
5374 Err(err) => {
5375 let _ = fs::remove_file(lock_path);
5376 Err(GitError::Io(err.to_string()))
5377 }
5378 }
5379}
5380
5381fn lock_path_for(path: &Path) -> Result<PathBuf> {
5382 let file_name = path
5383 .file_name()
5384 .ok_or_else(|| GitError::InvalidPath("ref path has no filename".into()))?;
5385 let mut lock_name = file_name.to_os_string();
5386 lock_name.push(".lock");
5387 Ok(path.with_file_name(lock_name))
5388}
5389
5390pub fn check_refname_format(name: &str, allow_onelevel: bool) -> Result<()> {
5392 if name.is_empty()
5393 || name == "@"
5394 || name.starts_with('/')
5395 || name.ends_with('/')
5396 || name.ends_with('.')
5397 || name.contains("..")
5398 || name.contains("//")
5399 || name.contains("@{")
5400 || (!allow_onelevel && !name.contains('/'))
5401 {
5402 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
5403 }
5404 for component in name.split('/') {
5405 if component.is_empty() || component.starts_with('.') || component.ends_with(".lock") {
5406 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
5407 }
5408 for (idx, byte) in component.bytes().enumerate() {
5409 if byte <= b' '
5410 || byte == 0x7f
5411 || matches!(byte, b'~' | b'^' | b':' | b'?' | b'*' | b'[' | b'\\')
5412 {
5413 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
5414 }
5415 if byte == b'.' && component.as_bytes().get(idx + 1) == Some(&b'.') {
5416 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
5417 }
5418 if byte == b'@' && component.as_bytes().get(idx + 1) == Some(&b'{') {
5419 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
5420 }
5421 }
5422 }
5423 Ok(())
5424}
5425
5426pub fn validate_symref_name(name: &str) -> Result<()> {
5428 if name == "HEAD" {
5429 return Ok(());
5430 }
5431 check_refname_format(name, true)
5432}
5433
5434pub fn validate_symref_target(name: &str) -> Result<()> {
5436 check_refname_format(name, true)
5437}
5438
5439fn prune_empty_dirs_up_to(start: &Path, boundary: &Path) {
5444 let mut dir = start.to_path_buf();
5445 while dir.starts_with(boundary) && dir != *boundary {
5446 if fs::remove_dir(&dir).is_err() {
5447 break;
5448 }
5449 dir = match dir.parent() {
5450 Some(parent) => parent.to_path_buf(),
5451 None => break,
5452 };
5453 }
5454}
5455
5456fn packable_loose_ref_name(name: &str) -> bool {
5457 name.starts_with("refs/")
5458 && !name.starts_with("refs/bisect/")
5459 && !name.starts_with("refs/worktree/")
5460 && !name.starts_with("refs/rewritten/")
5461}
5462
5463fn pack_refs_auto_required_for(packed_path: &Path, loose_count: usize) -> Result<bool> {
5464 let packed_size = match fs::metadata(packed_path) {
5465 Ok(meta) => meta.len() as usize,
5466 Err(err) if err.kind() == std::io::ErrorKind::NotFound => 0,
5467 Err(err) => return Err(err.into()),
5468 };
5469 let estimated_packed_refs = packed_size / 100;
5470 let log2 = if estimated_packed_refs == 0 {
5471 0
5472 } else {
5473 usize::BITS as usize - estimated_packed_refs.leading_zeros() as usize - 1
5474 };
5475 let limit = (log2 * 5).max(16);
5476 Ok(loose_count >= limit)
5477}
5478
5479pub fn resolve_ref_peeled(store: &FileRefStore, name: &str) -> Result<Option<ObjectId>> {
5480 let mut current = name.to_string();
5481 for _ in 0..16 {
5482 match store.read_ref(¤t)? {
5483 Some(RefTarget::Direct(oid)) => return Ok(Some(oid)),
5484 Some(RefTarget::Symbolic(next)) => {
5485 if validate_ref_name_for_read(&next).is_err() {
5486 return Ok(None);
5487 }
5488 current = next;
5489 }
5490 None => return Ok(None),
5491 }
5492 }
5493 Ok(None)
5494}
5495
5496pub fn validate_ref_name_for_read(name: &str) -> Result<()> {
5497 if validate_ref_name(name).is_ok() {
5498 return Ok(());
5499 }
5500 if is_root_ref_syntax(name) {
5501 return Ok(());
5502 }
5503 if check_refname_format(name, true).is_ok() {
5504 return Ok(());
5505 }
5506 validate_ref_path_safe_for_read(name)
5507}
5508
5509pub fn validate_ref_name_for_update(name: &str) -> Result<()> {
5510 if validate_ref_name(name).is_ok() {
5511 return Ok(());
5512 }
5513 if is_root_ref_syntax(name) {
5514 return Ok(());
5515 }
5516 check_refname_format(name, true)
5517}
5518
5519pub fn refname_is_safe(refname: &str) -> bool {
5533 if let Some(rest) = refname.strip_prefix("refs/") {
5534 if rest.is_empty() || rest.starts_with('/') || rest.ends_with('/') || rest.contains('\\') {
5535 return false;
5536 }
5537 let path = Path::new(rest);
5538 !path.is_absolute()
5539 && !path.components().any(|component| {
5540 matches!(
5541 component,
5542 std::path::Component::ParentDir
5543 | std::path::Component::Prefix(_)
5544 | std::path::Component::RootDir
5545 )
5546 })
5547 } else {
5548 !refname.is_empty() && refname.bytes().all(|b| b.is_ascii_uppercase() || b == b'_')
5549 }
5550}
5551
5552fn is_root_ref_syntax(name: &str) -> bool {
5557 !name.is_empty()
5558 && name
5559 .bytes()
5560 .all(|b| b.is_ascii_uppercase() || b == b'-' || b == b'_')
5561}
5562
5563const CURRENT_WORKTREE_REF_PREFIXES: &[&str] = &[
5564 "refs/bisect/",
5565 "refs/worktree/",
5566 "refs/rewritten/",
5567];
5568
5569fn current_worktree_ref(name: &str) -> bool {
5570 is_root_ref_syntax(name)
5571 || CURRENT_WORKTREE_REF_PREFIXES
5572 .iter()
5573 .any(|prefix| name.starts_with(prefix))
5574}
5575
5576fn ref_prefixes_overlap(prefix: &str, namespace: &str) -> bool {
5577 let namespace = namespace.trim_end_matches('/');
5578 prefix == "refs/"
5579 || prefix == namespace
5580 || prefix.starts_with(&format!("{namespace}/"))
5581 || namespace.starts_with(prefix.trim_end_matches('/'))
5582}
5583
5584fn reftable_current_worktree_ref(name: &str) -> bool {
5585 current_worktree_ref(name)
5586}
5587
5588fn reftable_other_worktree_ref(name: &str) -> Option<(&str, &str)> {
5589 let rest = name.strip_prefix("worktrees/")?;
5590 let (worktree, rewritten) = rest.split_once('/')?;
5591 if worktree.is_empty() || rewritten.is_empty() {
5592 return None;
5593 }
5594 Some((worktree, rewritten))
5595}
5596
5597pub fn validate_ref_name(name: &str) -> Result<()> {
5598 if name == "HEAD" {
5599 return Ok(());
5600 }
5601 if !name.starts_with("refs/") || check_refname_format(name, false).is_err() {
5602 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
5603 }
5604 Ok(())
5605}
5606
5607fn validate_ref_path_safe_for_read(name: &str) -> Result<()> {
5608 let path = Path::new(name);
5609 if !name.starts_with("refs/")
5610 || name.starts_with('/')
5611 || name.contains('\\')
5612 || path.is_absolute()
5613 || path.components().any(|component| {
5614 matches!(
5615 component,
5616 std::path::Component::ParentDir | std::path::Component::Prefix(_)
5617 )
5618 })
5619 {
5620 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
5621 }
5622 Ok(())
5623}
5624
5625fn safe_ref_prefix_for_directory_scan(prefix: &str) -> bool {
5626 let path = Path::new(prefix);
5627 prefix.starts_with("refs/")
5628 && !prefix.starts_with('/')
5629 && !prefix.contains('\\')
5630 && !path.is_absolute()
5631 && !path.components().any(|component| {
5632 matches!(
5633 component,
5634 std::path::Component::ParentDir | std::path::Component::Prefix(_)
5635 )
5636 })
5637}
5638
5639fn warn_broken_ref_name(name: &str) {
5640 eprintln!("warning: ignoring ref with broken name {name}");
5641}
5642
5643fn warn_broken_ref(name: &str) {
5644 eprintln!("warning: ignoring broken ref {name}");
5645}
5646
5647fn ref_target_is_broken(target: &RefTarget) -> bool {
5649 matches!(target, RefTarget::Direct(oid) if oid.is_null())
5650}
5651
5652fn ref_directory_conflict_error(new_ref: &str, existing_ref: &str) -> GitError {
5653 GitError::Transaction(format!(
5654 "cannot lock ref '{new_ref}': '{existing_ref}' exists; cannot create '{new_ref}'"
5655 ))
5656}
5657
5658fn check_ref_directory_conflict_in_names(name: &str, names: &BTreeSet<String>) -> Result<()> {
5659 let components = name.split('/').collect::<Vec<_>>();
5660 for index in 1..components.len() {
5661 let ancestor = components[..index].join("/");
5662 if names.contains(&ancestor) {
5663 return Err(ref_directory_conflict_error(name, &ancestor));
5664 }
5665 }
5666 let child_prefix = format!("{name}/");
5667 if let Some(existing) = names.range(child_prefix.clone()..).next()
5668 && existing.starts_with(&child_prefix)
5669 {
5670 return Err(ref_directory_conflict_error(name, existing));
5671 }
5672 Ok(())
5673}
5674
5675fn parent_to_ref_name(base: &Path, parent: &Path) -> String {
5676 match parent.strip_prefix(base) {
5677 Ok(suffix) => suffix.to_string_lossy().replace('\\', "/"),
5678 Err(_) => parent.to_string_lossy().into_owned(),
5679 }
5680}
5681
5682fn validate_namespaced_ref(name: &str, prefix: &str, kind: &str) -> Result<()> {
5683 validate_ref_name(name)?;
5684 if name
5685 .strip_prefix(prefix)
5686 .is_none_or(|short_name| short_name.is_empty())
5687 {
5688 return Err(GitError::InvalidPath(format!(
5689 "invalid {kind} ref name {name}"
5690 )));
5691 }
5692 Ok(())
5693}
5694
5695fn validate_short_ref_name(kind: &str, name: &str) -> Result<()> {
5696 if name.is_empty()
5697 || name.starts_with('-')
5698 || name.starts_with('/')
5699 || name.ends_with('/')
5700 || name.contains(' ')
5701 || name.contains('\\')
5702 {
5703 return Err(GitError::InvalidPath(format!("invalid {kind} name {name}")));
5704 }
5705 Ok(())
5706}
5707
5708fn validate_remote_name(remote: &str) -> Result<()> {
5709 validate_short_ref_name("remote", remote)?;
5710 if remote.contains('/') {
5711 return Err(GitError::InvalidPath(format!(
5712 "invalid remote name {remote}"
5713 )));
5714 }
5715 Ok(())
5716}
5717
5718fn prepare_bundle_ref_updates<F>(
5719 refs: &[BundleRefUpdate],
5720 reflog: Option<&BundleRefUpdateReflog>,
5721 mut read_ref: F,
5722) -> Result<(Vec<RefUpdate>, Vec<AppliedBundleRefUpdate>)>
5723where
5724 F: FnMut(&str, &ObjectId) -> Result<Option<RefTarget>>,
5725{
5726 let mut seen = BTreeSet::new();
5727 let mut updates = Vec::with_capacity(refs.len());
5728 let mut applied = Vec::with_capacity(refs.len());
5729 for bundle_ref in refs {
5730 validate_ref_name(&bundle_ref.name)?;
5731 if !seen.insert(bundle_ref.name.clone()) {
5732 return Err(GitError::Transaction(format!(
5733 "duplicate bundle ref {}",
5734 bundle_ref.name
5735 )));
5736 }
5737 let old_oid = match read_ref(&bundle_ref.name, &bundle_ref.oid)? {
5738 Some(RefTarget::Direct(oid)) => Some(oid),
5739 Some(RefTarget::Symbolic(target)) => {
5740 return Err(GitError::Transaction(format!(
5741 "bundle ref {} would overwrite symbolic ref {target}",
5742 bundle_ref.name
5743 )));
5744 }
5745 None => None,
5746 };
5747 let reflog = match reflog {
5748 Some(reflog) => Some(ReflogEntry {
5749 old_oid: match &old_oid {
5750 Some(oid) => *oid,
5751 None => null_oid(bundle_ref.oid.format())?,
5752 },
5753 new_oid: bundle_ref.oid,
5754 committer: reflog.committer.clone(),
5755 message: reflog.message.clone(),
5756 }),
5757 None => None,
5758 };
5759 updates.push(RefUpdate {
5760 name: bundle_ref.name.clone(),
5761 expected: old_oid.map(RefTarget::Direct),
5762 new: RefTarget::Direct(bundle_ref.oid),
5763 reflog,
5764 });
5765 applied.push(AppliedBundleRefUpdate {
5766 name: bundle_ref.name.clone(),
5767 old_oid,
5768 new_oid: bundle_ref.oid,
5769 });
5770 }
5771 Ok((updates, applied))
5772}
5773
5774fn null_oid(format: ObjectFormat) -> Result<ObjectId> {
5775 Ok(ObjectId::null(format))
5776}
5777
5778#[cfg(test)]
5779mod tests {
5780 use super::*;
5781 use std::sync::atomic::{AtomicU64, Ordering};
5782
5783 static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
5784
5785 #[test]
5786 fn loose_ref_round_trips_direct() {
5787 let oid = "ce013625030ba8dba906f756967f9e9ca394464a";
5788 let reference = parse_loose_ref(ObjectFormat::Sha1, "refs/heads/main", oid.as_bytes())
5789 .expect("test operation should succeed");
5790 assert_eq!(write_loose_ref(&reference), format!("{oid}\n").into_bytes());
5791 }
5792
5793 #[test]
5794 fn loose_fetch_head_reads_first_object_id() {
5795 let oid = ObjectId::from_hex(
5796 ObjectFormat::Sha1,
5797 "ce013625030ba8dba906f756967f9e9ca394464a",
5798 )
5799 .expect("test operation should succeed");
5800 let bytes = b"ce013625030ba8dba906f756967f9e9ca394464a\t\tbranch 'main' of ../sub\n";
5801 let reference = parse_loose_ref(ObjectFormat::Sha1, "FETCH_HEAD", bytes)
5802 .expect("test operation should succeed");
5803 assert_eq!(reference.target, RefTarget::Direct(oid));
5804 }
5805
5806 #[test]
5807 fn symref_names_allow_onelevel_pseudo_refs() {
5808 for name in ["NOTHEAD", "FOO", "ORIG_HEAD", "TEST_SYMREF"] {
5809 validate_symref_name(name).expect("symref name should be valid");
5810 }
5811 assert!(validate_ref_name("NOTHEAD").is_err());
5812 assert!(validate_symref_target("refs/heads/foo").is_ok());
5813 assert!(validate_symref_target("ORIG_HEAD").is_ok());
5814 assert!(validate_symref_target("foo..bar").is_err());
5815 }
5816
5817 #[test]
5818 fn resolve_ref_peeled_follows_symref_chains() {
5819 let git_dir = temp_git_dir();
5820 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5821 let oid = ObjectId::from_hex(
5822 ObjectFormat::Sha1,
5823 "ce013625030ba8dba906f756967f9e9ca394464a",
5824 )
5825 .expect("test operation should succeed");
5826 let mut tx = store.transaction();
5827 tx.update(RefUpdate {
5828 name: "refs/heads/target".into(),
5829 expected: None,
5830 new: RefTarget::Direct(oid),
5831 reflog: None,
5832 });
5833 tx.commit().expect("seed target ref");
5834 let mut tx = store.transaction();
5835 tx.update(RefUpdate {
5836 name: "refs/heads/alias".into(),
5837 expected: None,
5838 new: RefTarget::Symbolic("refs/heads/target".into()),
5839 reflog: None,
5840 });
5841 tx.commit().expect("seed alias ref");
5842 let mut tx = store.transaction();
5843 tx.update(RefUpdate {
5844 name: "ORIG_HEAD".into(),
5845 expected: None,
5846 new: RefTarget::Symbolic("refs/heads/alias".into()),
5847 reflog: None,
5848 });
5849 tx.commit().expect("seed ORIG_HEAD symref");
5850 assert_eq!(
5851 resolve_ref_peeled(&store, "ORIG_HEAD").expect("resolve ORIG_HEAD"),
5852 Some(oid)
5853 );
5854 let _ = fs::remove_dir_all(git_dir);
5855 }
5856
5857 #[test]
5858 fn symref_directory_conflict_is_reported_gracefully() {
5859 let git_dir = temp_git_dir();
5860 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5861 let oid = ObjectId::from_hex(
5862 ObjectFormat::Sha1,
5863 "ce013625030ba8dba906f756967f9e9ca394464a",
5864 )
5865 .expect("test operation should succeed");
5866 let mut tx = store.transaction();
5867 tx.update(RefUpdate {
5868 name: "refs/heads/df".into(),
5869 expected: None,
5870 new: RefTarget::Direct(oid),
5871 reflog: None,
5872 });
5873 tx.commit().expect("seed branch ref");
5874
5875 let mut tx = store.transaction();
5876 tx.update(RefUpdate {
5877 name: "refs/heads/df/conflict".into(),
5878 expected: None,
5879 new: RefTarget::Symbolic("refs/heads/df".into()),
5880 reflog: None,
5881 });
5882 let err = tx.commit().expect_err("child ref should conflict");
5883 assert!(
5884 matches!(err, GitError::Transaction(message) if message.contains(
5885 "cannot lock ref 'refs/heads/df/conflict'"
5886 ) && message.contains("refs/heads/df"))
5887 );
5888 let _ = fs::remove_dir_all(git_dir);
5889 }
5890
5891 #[test]
5892 fn transaction_checks_expected_value() {
5893 let oid = ObjectId::from_hex(
5894 ObjectFormat::Sha1,
5895 "ce013625030ba8dba906f756967f9e9ca394464a",
5896 )
5897 .expect("test operation should succeed");
5898 let mut store = RefStore::new();
5899 let mut tx = store.transaction();
5900 tx.update(RefUpdate {
5901 name: "refs/heads/main".into(),
5902 expected: None,
5903 new: RefTarget::Direct(oid),
5904 reflog: None,
5905 });
5906 tx.commit().expect("test operation should succeed");
5907 assert_eq!(store.get("refs/heads/main"), Some(&RefTarget::Direct(oid)));
5908 }
5909
5910 #[test]
5911 fn packed_refs_parse_peeled_refs() {
5912 let packed = b"# pack-refs with: peeled fully-peeled sorted \n\
5913ce013625030ba8dba906f756967f9e9ca394464a refs/tags/v1\n\
5914^e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\n";
5915 let refs =
5916 parse_packed_refs(ObjectFormat::Sha1, packed).expect("test operation should succeed");
5917 assert_eq!(refs.len(), 1);
5918 assert_eq!(refs[0].reference.name, "refs/tags/v1");
5919 assert_eq!(
5920 refs[0]
5921 .peeled
5922 .as_ref()
5923 .expect("test operation should succeed")
5924 .to_hex(),
5925 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"
5926 );
5927 }
5928
5929 #[test]
5930 fn packed_refs_write_sorted_with_peeled_refs() {
5931 let head_oid = ObjectId::from_hex(
5932 ObjectFormat::Sha1,
5933 "ce013625030ba8dba906f756967f9e9ca394464a",
5934 )
5935 .expect("test operation should succeed");
5936 let tag_oid = ObjectId::from_hex(
5937 ObjectFormat::Sha1,
5938 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
5939 )
5940 .expect("test operation should succeed");
5941 let peeled_oid = ObjectId::from_hex(
5942 ObjectFormat::Sha1,
5943 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
5944 )
5945 .expect("test operation should succeed");
5946 let refs = vec![
5947 PackedRef {
5948 reference: Ref {
5949 name: "refs/tags/v1".into(),
5950 target: RefTarget::Direct(tag_oid),
5951 },
5952 peeled: Some(peeled_oid),
5953 },
5954 PackedRef {
5955 reference: Ref {
5956 name: "refs/heads/main".into(),
5957 target: RefTarget::Direct(head_oid),
5958 },
5959 peeled: None,
5960 },
5961 ];
5962 let bytes = write_packed_refs(&refs).expect("test operation should succeed");
5963 let expected = format!(
5964 "# pack-refs with: peeled fully-peeled sorted \n\
5965{head_oid} refs/heads/main\n\
5966{tag_oid} refs/tags/v1\n\
5967^{peeled_oid}\n"
5968 );
5969 assert_eq!(
5970 String::from_utf8(bytes.clone()).expect("test operation should succeed"),
5971 expected
5972 );
5973 let parsed =
5974 parse_packed_refs(ObjectFormat::Sha1, &bytes).expect("test operation should succeed");
5975 assert_eq!(parsed[0], refs[1]);
5976 assert_eq!(parsed[1], refs[0]);
5977 }
5978
5979 #[test]
5980 fn full_ref_name_validates_and_round_trips_owned() {
5981 let full = FullRefName::new("refs/heads/main").expect("valid full branch ref");
5982 assert_eq!(full.as_str(), "refs/heads/main");
5983 assert_eq!(full.to_string(), "refs/heads/main");
5984 assert_eq!(full.to_owned().into_string(), "refs/heads/main");
5985
5986 let head = FullRefNameBuf::new("HEAD").expect("valid HEAD ref");
5987 assert_eq!(head.as_ref_name().into_str(), "HEAD");
5988
5989 assert!(FullRefName::new("main").is_err());
5990 assert!(FullRefNameBuf::new("refs/heads/bad.lock").is_err());
5991 }
5992
5993 #[test]
5994 fn branch_ref_name_helpers_validate_short_and_full_names() {
5995 let branch =
5996 BranchRefNameBuf::from_branch_name("feature/topic").expect("valid branch short name");
5997 assert_eq!(branch.as_str(), "refs/heads/feature/topic");
5998 assert_eq!(branch.branch_name(), "feature/topic");
5999 assert_eq!(
6000 branch.as_full_ref_name().as_str(),
6001 "refs/heads/feature/topic"
6002 );
6003 assert_eq!(
6004 branch_ref_name("feature/topic").expect("valid branch short name"),
6005 branch.as_str()
6006 );
6007
6008 let borrowed = BranchRefName::from_full("refs/heads/main").expect("valid full branch ref");
6009 assert_eq!(borrowed.branch_name(), "main");
6010 assert_eq!(borrowed.to_owned().into_string(), "refs/heads/main");
6011 assert_eq!(
6012 FullRefName::new("refs/heads/main")
6013 .expect("valid full branch ref")
6014 .as_branch()
6015 .expect("full ref is a branch")
6016 .branch_name(),
6017 "main"
6018 );
6019
6020 assert!(BranchRefName::from_full("refs/tags/main").is_err());
6021 assert!(BranchRefName::from_full("refs/heads").is_err());
6022 assert!(BranchRefNameBuf::from_branch_name("-bad").is_err());
6023 }
6024
6025 #[test]
6026 fn tag_ref_name_helpers_validate_short_and_full_names() {
6027 let tag = TagRefNameBuf::from_tag_name("v1.0").expect("valid tag short name");
6028 assert_eq!(tag.as_str(), "refs/tags/v1.0");
6029 assert_eq!(tag.tag_name(), "v1.0");
6030 assert_eq!(tag.as_full_ref_name().as_str(), "refs/tags/v1.0");
6031 assert_eq!(
6032 tag_ref_name("v1.0").expect("valid tag short name"),
6033 tag.as_str()
6034 );
6035
6036 let borrowed = TagRefName::from_full("refs/tags/release/1").expect("valid full tag ref");
6037 assert_eq!(borrowed.tag_name(), "release/1");
6038 assert_eq!(borrowed.to_owned().into_string(), "refs/tags/release/1");
6039 assert_eq!(
6040 FullRefName::new("refs/tags/release/1")
6041 .expect("valid full tag ref")
6042 .as_tag()
6043 .expect("full ref is a tag")
6044 .tag_name(),
6045 "release/1"
6046 );
6047
6048 assert!(TagRefName::from_full("refs/heads/v1.0").is_err());
6049 assert!(TagRefName::from_full("refs/tags").is_err());
6050 assert!(TagRefNameBuf::from_tag_name("bad tag").is_err());
6051 }
6052
6053 #[test]
6054 fn remote_ref_name_helpers_validate_namespace_and_components() {
6055 let remote = RemoteRefNameBuf::from_remote_branch("origin", "feature/topic")
6056 .expect("valid remote branch ref");
6057 assert_eq!(remote.as_str(), "refs/remotes/origin/feature/topic");
6058 assert_eq!(remote.short_name(), "origin/feature/topic");
6059 assert_eq!(remote.remote_name(), "origin");
6060 assert_eq!(remote.remote_branch(), Some("feature/topic"));
6061 assert_eq!(
6062 remote.as_full_ref_name().as_str(),
6063 "refs/remotes/origin/feature/topic"
6064 );
6065
6066 let head =
6067 RemoteRefName::from_full("refs/remotes/origin/HEAD").expect("valid remote HEAD ref");
6068 assert_eq!(head.remote_name(), "origin");
6069 assert_eq!(head.remote_branch(), Some("HEAD"));
6070 assert_eq!(
6071 FullRefName::new("refs/remotes/upstream/main")
6072 .expect("valid full remote ref")
6073 .as_remote()
6074 .expect("full ref is remote-tracking")
6075 .remote_name(),
6076 "upstream"
6077 );
6078
6079 let short =
6080 RemoteRefNameBuf::from_short_name("origin/main").expect("valid remote short ref");
6081 assert_eq!(short.as_str(), "refs/remotes/origin/main");
6082
6083 assert!(RemoteRefName::from_full("refs/heads/origin/main").is_err());
6084 assert!(RemoteRefName::from_full("refs/remotes/").is_err());
6085 assert!(RemoteRefNameBuf::from_remote_branch("origin/fork", "main").is_err());
6086 }
6087
6088 #[test]
6089 fn file_ref_store_writes_ref_and_reflog() {
6090 let git_dir = temp_git_dir();
6091 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6092 let oid = ObjectId::from_hex(
6093 ObjectFormat::Sha1,
6094 "ce013625030ba8dba906f756967f9e9ca394464a",
6095 )
6096 .expect("test operation should succeed");
6097 let mut tx = store.transaction();
6098 tx.update(RefUpdate {
6099 name: "refs/heads/main".into(),
6100 expected: None,
6101 new: RefTarget::Direct(oid),
6102 reflog: Some(ReflogEntry {
6103 old_oid: zero_oid(ObjectFormat::Sha1).expect("test operation should succeed"),
6104 new_oid: oid,
6105 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
6106 message: b"update by test".to_vec(),
6107 }),
6108 });
6109 tx.commit().expect("test operation should succeed");
6110 assert_eq!(
6111 store
6112 .read_ref("refs/heads/main")
6113 .expect("test operation should succeed"),
6114 Some(RefTarget::Direct(oid))
6115 );
6116 let log = store
6117 .read_reflog("refs/heads/main")
6118 .expect("test operation should succeed");
6119 assert_eq!(log.len(), 1);
6120 assert_eq!(log[0].message, b"update by test");
6121 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6122 }
6123
6124 #[test]
6125 fn file_ref_store_applies_bundle_refs_with_reflog() {
6126 let git_dir = temp_git_dir();
6127 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6128 let old_main = ObjectId::from_hex(
6129 ObjectFormat::Sha1,
6130 "ce013625030ba8dba906f756967f9e9ca394464a",
6131 )
6132 .expect("test operation should succeed");
6133 let new_main = ObjectId::from_hex(
6134 ObjectFormat::Sha1,
6135 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
6136 )
6137 .expect("test operation should succeed");
6138 let tag_oid = ObjectId::from_hex(
6139 ObjectFormat::Sha1,
6140 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
6141 )
6142 .expect("test operation should succeed");
6143 let mut tx = store.transaction();
6144 tx.update(RefUpdate {
6145 name: "refs/heads/main".into(),
6146 expected: None,
6147 new: RefTarget::Direct(old_main.clone()),
6148 reflog: None,
6149 });
6150 tx.commit().expect("test operation should succeed");
6151
6152 let applied = store
6153 .apply_bundle_ref_updates(
6154 &[
6155 BundleRefUpdate {
6156 name: "refs/heads/main".into(),
6157 oid: new_main.clone(),
6158 },
6159 BundleRefUpdate {
6160 name: "refs/tags/v1.0".into(),
6161 oid: tag_oid,
6162 },
6163 ],
6164 Some(BundleRefUpdateReflog {
6165 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
6166 message: b"bundle: import refs".to_vec(),
6167 }),
6168 )
6169 .expect("test operation should succeed");
6170
6171 assert_eq!(
6172 applied,
6173 vec![
6174 AppliedBundleRefUpdate {
6175 name: "refs/heads/main".into(),
6176 old_oid: Some(old_main.clone()),
6177 new_oid: new_main.clone(),
6178 },
6179 AppliedBundleRefUpdate {
6180 name: "refs/tags/v1.0".into(),
6181 old_oid: None,
6182 new_oid: tag_oid,
6183 }
6184 ]
6185 );
6186 assert_eq!(
6187 store
6188 .read_ref("refs/heads/main")
6189 .expect("test operation should succeed"),
6190 Some(RefTarget::Direct(new_main.clone()))
6191 );
6192 assert_eq!(
6193 store
6194 .read_ref("refs/tags/v1.0")
6195 .expect("test operation should succeed"),
6196 Some(RefTarget::Direct(tag_oid))
6197 );
6198 let main_log = store
6199 .read_reflog("refs/heads/main")
6200 .expect("test operation should succeed");
6201 assert_eq!(main_log.len(), 1);
6202 assert_eq!(main_log[0].old_oid, old_main);
6203 assert_eq!(main_log[0].new_oid, new_main);
6204 assert_eq!(main_log[0].message, b"bundle: import refs");
6205 let tag_log = store
6206 .read_reflog("refs/tags/v1.0")
6207 .expect("test operation should succeed");
6208 assert_eq!(tag_log.len(), 1);
6209 assert_eq!(
6210 tag_log[0].old_oid,
6211 zero_oid(ObjectFormat::Sha1).expect("test operation should succeed")
6212 );
6213 assert_eq!(tag_log[0].new_oid, tag_oid);
6214 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6215 }
6216
6217 #[test]
6218 fn file_ref_store_rejects_bad_bundle_ref_before_writing() {
6219 let git_dir = temp_git_dir();
6220 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6221 let oid = ObjectId::from_hex(
6222 ObjectFormat::Sha1,
6223 "ce013625030ba8dba906f756967f9e9ca394464a",
6224 )
6225 .expect("test operation should succeed");
6226
6227 let result = store.apply_bundle_ref_updates(
6228 &[
6229 BundleRefUpdate {
6230 name: "refs/heads/main".into(),
6231 oid,
6232 },
6233 BundleRefUpdate {
6234 name: "refs/heads/bad.lock".into(),
6235 oid,
6236 },
6237 ],
6238 None,
6239 );
6240
6241 assert!(result.is_err());
6242 assert_eq!(
6243 store
6244 .read_ref("refs/heads/main")
6245 .expect("test operation should succeed"),
6246 None
6247 );
6248 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6249 }
6250
6251 #[test]
6252 fn file_ref_store_rejects_bundle_ref_over_symbolic_ref() {
6253 let git_dir = temp_git_dir();
6254 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6255 let oid = ObjectId::from_hex(
6256 ObjectFormat::Sha1,
6257 "ce013625030ba8dba906f756967f9e9ca394464a",
6258 )
6259 .expect("test operation should succeed");
6260 let mut tx = store.transaction();
6261 tx.update(RefUpdate {
6262 name: "refs/heads/main".into(),
6263 expected: None,
6264 new: RefTarget::Symbolic("refs/heads/base".into()),
6265 reflog: None,
6266 });
6267 tx.commit().expect("test operation should succeed");
6268
6269 let result = store.apply_bundle_ref_updates(
6270 &[BundleRefUpdate {
6271 name: "refs/heads/main".into(),
6272 oid,
6273 }],
6274 None,
6275 );
6276
6277 assert!(result.is_err());
6278 assert_eq!(
6279 store
6280 .read_ref("refs/heads/main")
6281 .expect("test operation should succeed"),
6282 Some(RefTarget::Symbolic("refs/heads/base".into()))
6283 );
6284 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6285 }
6286
6287 #[test]
6288 fn file_ref_store_expires_reflog_entries_by_timestamp() {
6289 let git_dir = temp_git_dir();
6290 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6291 let first = ObjectId::from_hex(
6292 ObjectFormat::Sha1,
6293 "ce013625030ba8dba906f756967f9e9ca394464a",
6294 )
6295 .expect("test operation should succeed");
6296 let second = ObjectId::from_hex(
6297 ObjectFormat::Sha1,
6298 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
6299 )
6300 .expect("test operation should succeed");
6301 let mut tx = store.transaction();
6302 tx.update(RefUpdate {
6303 name: "refs/heads/main".into(),
6304 expected: None,
6305 new: RefTarget::Direct(first.clone()),
6306 reflog: Some(ReflogEntry {
6307 old_oid: zero_oid(ObjectFormat::Sha1).expect("test operation should succeed"),
6308 new_oid: first.clone(),
6309 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
6310 message: b"old".to_vec(),
6311 }),
6312 });
6313 tx.update(RefUpdate {
6314 name: "refs/heads/main".into(),
6315 expected: None,
6316 new: RefTarget::Direct(second.clone()),
6317 reflog: Some(ReflogEntry {
6318 old_oid: first,
6319 new_oid: second.clone(),
6320 committer: b"Git Rs <sley@example.invalid> 100 +0000".to_vec(),
6321 message: b"new".to_vec(),
6322 }),
6323 });
6324 tx.commit().expect("test operation should succeed");
6325
6326 let removed = store
6327 .expire_reflog_older_than("refs/heads/main", 50)
6328 .expect("test operation should succeed");
6329 assert_eq!(removed, 1);
6330 let log = store
6331 .read_reflog("refs/heads/main")
6332 .expect("test operation should succeed");
6333 assert_eq!(log.len(), 1);
6334 assert_eq!(log[0].new_oid, second);
6335 assert_eq!(log[0].message, b"new");
6336 assert!(
6337 !git_dir
6338 .join("logs")
6339 .join("refs")
6340 .join("heads")
6341 .join("main.lock")
6342 .exists()
6343 );
6344 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6345 }
6346
6347 #[test]
6348 fn file_ref_store_creates_branch() {
6349 let git_dir = temp_git_dir();
6350 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6351 let oid = ObjectId::from_hex(
6352 ObjectFormat::Sha1,
6353 "ce013625030ba8dba906f756967f9e9ca394464a",
6354 )
6355 .expect("test operation should succeed");
6356 let branch = store
6357 .create_branch(
6358 "feature",
6359 oid,
6360 b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
6361 b"branch: Created from main".to_vec(),
6362 )
6363 .expect("test operation should succeed");
6364 assert_eq!(branch.name, "refs/heads/feature");
6365 assert_eq!(
6366 store
6367 .read_ref("refs/heads/feature")
6368 .expect("test operation should succeed"),
6369 Some(RefTarget::Direct(oid))
6370 );
6371 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6372 }
6373
6374 #[test]
6375 fn file_ref_store_deletes_loose_branch() {
6376 let git_dir = temp_git_dir();
6377 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6378 let oid = ObjectId::from_hex(
6379 ObjectFormat::Sha1,
6380 "ce013625030ba8dba906f756967f9e9ca394464a",
6381 )
6382 .expect("test operation should succeed");
6383 store
6384 .create_branch(
6385 "feature",
6386 oid,
6387 b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
6388 b"branch: Created from main".to_vec(),
6389 )
6390 .expect("test operation should succeed");
6391 let deleted = store
6392 .delete_branch("feature")
6393 .expect("test operation should succeed");
6394 assert_eq!(deleted.name, "refs/heads/feature");
6395 assert_eq!(deleted.oid, oid);
6396 assert_eq!(
6397 store
6398 .read_ref("refs/heads/feature")
6399 .expect("test operation should succeed"),
6400 None
6401 );
6402 assert!(!git_dir.join("refs").join("heads").join("feature").exists());
6403 assert!(
6404 !git_dir
6405 .join("logs")
6406 .join("refs")
6407 .join("heads")
6408 .join("feature")
6409 .exists()
6410 );
6411 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6412 }
6413
6414 #[test]
6415 fn file_ref_store_deletes_generic_loose_ref() {
6416 let git_dir = temp_git_dir();
6417 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6418 let oid = ObjectId::from_hex(
6419 ObjectFormat::Sha1,
6420 "ce013625030ba8dba906f756967f9e9ca394464a",
6421 )
6422 .expect("test operation should succeed");
6423 let mut tx = store.transaction();
6424 tx.update(RefUpdate {
6425 name: "refs/heads/topic".into(),
6426 expected: None,
6427 new: RefTarget::Direct(oid),
6428 reflog: Some(ReflogEntry {
6429 old_oid: zero_oid(ObjectFormat::Sha1).expect("test operation should succeed"),
6430 new_oid: oid,
6431 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
6432 message: b"update by test".to_vec(),
6433 }),
6434 });
6435 tx.commit().expect("test operation should succeed");
6436 let deleted = store
6437 .delete_ref("refs/heads/topic")
6438 .expect("test operation should succeed");
6439 assert_eq!(deleted.name, "refs/heads/topic");
6440 assert_eq!(deleted.oid, oid);
6441 assert_eq!(
6442 store
6443 .read_ref("refs/heads/topic")
6444 .expect("test operation should succeed"),
6445 None
6446 );
6447 assert!(!git_dir.join("refs").join("heads").join("topic").exists());
6448 assert!(
6449 !git_dir
6450 .join("logs")
6451 .join("refs")
6452 .join("heads")
6453 .join("topic")
6454 .exists()
6455 );
6456 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6457 }
6458
6459 #[test]
6460 fn file_ref_store_delete_prunes_below_namespace_only() {
6461 let git_dir = temp_git_dir();
6462 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6463 let oid = ObjectId::from_hex(
6464 ObjectFormat::Sha1,
6465 "ce013625030ba8dba906f756967f9e9ca394464a",
6466 )
6467 .expect("test operation should succeed");
6468 for name in ["refs/heads/l/m", "refs/foo/bar"] {
6469 let mut tx = store.transaction();
6470 tx.update(RefUpdate {
6471 name: name.into(),
6472 expected: None,
6473 new: RefTarget::Direct(oid),
6474 reflog: None,
6475 });
6476 tx.commit().expect("test operation should succeed");
6477 store.delete_ref(name).expect("test operation should succeed");
6478 }
6479 assert!(!git_dir.join("refs").join("heads").join("l").exists());
6480 assert!(git_dir.join("refs").join("heads").exists());
6481 assert!(!git_dir.join("refs").join("foo").join("bar").exists());
6482 assert!(git_dir.join("refs").join("foo").exists());
6483 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6484 }
6485
6486 #[test]
6487 fn file_ref_store_delete_ref_checked_removes_reflog() {
6488 let git_dir = temp_git_dir();
6489 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6490 let oid = ObjectId::from_hex(
6491 ObjectFormat::Sha1,
6492 "ce013625030ba8dba906f756967f9e9ca394464a",
6493 )
6494 .expect("test operation should succeed");
6495 let mut tx = store.transaction();
6499 tx.update(RefUpdate {
6500 name: "refs/heads/main".into(),
6501 expected: None,
6502 new: RefTarget::Direct(oid),
6503 reflog: Some(ReflogEntry {
6504 old_oid: zero_oid(ObjectFormat::Sha1).expect("test operation should succeed"),
6505 new_oid: oid,
6506 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
6507 message: b"create main".to_vec(),
6508 }),
6509 });
6510 tx.commit().expect("test operation should succeed");
6511 assert!(
6512 git_dir
6513 .join("logs")
6514 .join("refs")
6515 .join("heads")
6516 .join("main")
6517 .exists(),
6518 "reflog file should exist before the checked delete"
6519 );
6520
6521 let deleted = store
6522 .delete_ref_checked(DeleteRef {
6523 name: "refs/heads/main".into(),
6524 expected_old: Some(oid),
6525 reflog: Some(DeleteRefReflog {
6526 committer: b"Git Rs <sley@example.invalid> 123 +0000".to_vec(),
6527 message: b"delete main".to_vec(),
6528 }),
6529 })
6530 .expect("test operation should succeed");
6531
6532 assert_eq!(deleted.name, "refs/heads/main");
6533 assert_eq!(deleted.oid, oid);
6534 assert_eq!(
6535 store
6536 .read_ref("refs/heads/main")
6537 .expect("test operation should succeed"),
6538 None
6539 );
6540 assert!(
6543 !git_dir
6544 .join("logs")
6545 .join("refs")
6546 .join("heads")
6547 .join("main")
6548 .exists(),
6549 "reflog file should be removed by the checked delete"
6550 );
6551 assert!(
6552 store
6553 .read_reflog("refs/heads/main")
6554 .expect("test operation should succeed")
6555 .is_empty()
6556 );
6557 assert!(
6558 !git_dir
6559 .join("refs")
6560 .join("heads")
6561 .join("main.lock")
6562 .exists()
6563 );
6564 assert!(!git_dir.join("packed-refs.lock").exists());
6565 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6566 }
6567
6568 #[test]
6569 fn file_ref_store_delete_ref_checked_stale_expected_leaves_ref_untouched() {
6570 let git_dir = temp_git_dir();
6571 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6572 let actual = ObjectId::from_hex(
6573 ObjectFormat::Sha1,
6574 "ce013625030ba8dba906f756967f9e9ca394464a",
6575 )
6576 .expect("test operation should succeed");
6577 let expected = ObjectId::from_hex(
6578 ObjectFormat::Sha1,
6579 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
6580 )
6581 .expect("test operation should succeed");
6582 let mut tx = store.transaction();
6583 tx.update(RefUpdate {
6584 name: "refs/heads/main".into(),
6585 expected: None,
6586 new: RefTarget::Direct(actual),
6587 reflog: None,
6588 });
6589 tx.commit().expect("test operation should succeed");
6590
6591 let err = store
6592 .delete_ref_checked(DeleteRef {
6593 name: "refs/heads/main".into(),
6594 expected_old: Some(expected),
6595 reflog: None,
6596 })
6597 .expect_err("stale expected must fail");
6598
6599 assert!(matches!(
6600 err,
6601 RefDeleteError::ExpectedMismatch {
6602 expected: Some(got_expected),
6603 actual: Some(got_actual),
6604 } if got_expected == expected && got_actual == actual
6605 ));
6606 assert_eq!(
6607 store
6608 .read_ref("refs/heads/main")
6609 .expect("test operation should succeed"),
6610 Some(RefTarget::Direct(actual))
6611 );
6612 assert!(
6613 !git_dir
6614 .join("refs")
6615 .join("heads")
6616 .join("main.lock")
6617 .exists()
6618 );
6619 assert!(!git_dir.join("packed-refs.lock").exists());
6620 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6621 }
6622
6623 #[test]
6624 fn file_ref_store_delete_ref_checked_missing_returns_not_found() {
6625 let git_dir = temp_git_dir();
6626 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6627
6628 let err = store
6629 .delete_ref_checked(DeleteRef {
6630 name: "refs/heads/missing".into(),
6631 expected_old: None,
6632 reflog: None,
6633 })
6634 .expect_err("missing ref must fail");
6635
6636 assert!(matches!(err, RefDeleteError::NotFound));
6637 assert!(
6638 !git_dir
6639 .join("refs")
6640 .join("heads")
6641 .join("missing.lock")
6642 .exists()
6643 );
6644 assert!(!git_dir.join("packed-refs.lock").exists());
6645 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6646 }
6647
6648 #[test]
6649 fn file_ref_store_delete_ref_checked_removes_packed_ref() {
6650 let git_dir = temp_git_dir();
6651 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6652 let oid = ObjectId::from_hex(
6653 ObjectFormat::Sha1,
6654 "ce013625030ba8dba906f756967f9e9ca394464a",
6655 )
6656 .expect("test operation should succeed");
6657 let other = ObjectId::from_hex(
6658 ObjectFormat::Sha1,
6659 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
6660 )
6661 .expect("test operation should succeed");
6662 store
6663 .write_packed_refs(&[
6664 PackedRef {
6665 reference: Ref {
6666 name: "refs/heads/main".into(),
6667 target: RefTarget::Direct(oid),
6668 },
6669 peeled: None,
6670 },
6671 PackedRef {
6672 reference: Ref {
6673 name: "refs/heads/other".into(),
6674 target: RefTarget::Direct(other),
6675 },
6676 peeled: None,
6677 },
6678 ])
6679 .expect("test operation should succeed");
6680
6681 store
6682 .delete_ref_checked(DeleteRef {
6683 name: "refs/heads/main".into(),
6684 expected_old: Some(oid),
6685 reflog: None,
6686 })
6687 .expect("test operation should succeed");
6688
6689 assert_eq!(
6690 store
6691 .read_ref("refs/heads/main")
6692 .expect("test operation should succeed"),
6693 None
6694 );
6695 assert_eq!(
6696 store
6697 .read_ref("refs/heads/other")
6698 .expect("test operation should succeed"),
6699 Some(RefTarget::Direct(other))
6700 );
6701 let packed =
6702 fs::read_to_string(git_dir.join("packed-refs")).expect("test operation should succeed");
6703 assert!(!packed.contains("refs/heads/main"));
6704 assert!(packed.contains("refs/heads/other"));
6705 assert!(
6706 !git_dir
6707 .join("refs")
6708 .join("heads")
6709 .join("main.lock")
6710 .exists()
6711 );
6712 assert!(!git_dir.join("packed-refs.lock").exists());
6713 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6714 }
6715
6716 #[test]
6717 fn file_ref_store_delete_ref_checked_lock_conflict_returns_locked() {
6718 let git_dir = temp_git_dir();
6719 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6720 let oid = ObjectId::from_hex(
6721 ObjectFormat::Sha1,
6722 "ce013625030ba8dba906f756967f9e9ca394464a",
6723 )
6724 .expect("test operation should succeed");
6725 let mut tx = store.transaction();
6726 tx.update(RefUpdate {
6727 name: "refs/heads/main".into(),
6728 expected: None,
6729 new: RefTarget::Direct(oid),
6730 reflog: None,
6731 });
6732 tx.commit().expect("test operation should succeed");
6733 fs::write(
6734 git_dir.join("refs").join("heads").join("main.lock"),
6735 b"held\n",
6736 )
6737 .expect("test operation should succeed");
6738
6739 let err = store
6740 .delete_ref_checked(DeleteRef {
6741 name: "refs/heads/main".into(),
6742 expected_old: Some(oid),
6743 reflog: None,
6744 })
6745 .expect_err("held lock must fail");
6746
6747 assert!(matches!(err, RefDeleteError::Locked));
6748 assert_eq!(
6749 store
6750 .read_ref("refs/heads/main")
6751 .expect("test operation should succeed"),
6752 Some(RefTarget::Direct(oid))
6753 );
6754 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6755 }
6756
6757 #[test]
6758 fn file_ref_store_reports_current_branch() {
6759 let git_dir = temp_git_dir();
6760 fs::write(git_dir.join("HEAD"), b"ref: refs/heads/main\n")
6761 .expect("test operation should succeed");
6762 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6763 assert_eq!(
6764 store
6765 .current_branch_ref()
6766 .expect("test operation should succeed"),
6767 Some("refs/heads/main".into())
6768 );
6769 assert_eq!(
6770 store
6771 .current_branch()
6772 .expect("test operation should succeed"),
6773 Some("main".into())
6774 );
6775 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6776 }
6777
6778 #[test]
6779 fn file_ref_store_resolves_linked_worktree_head_through_common_refs() {
6780 let common = temp_git_dir();
6781 let admin = common.join("worktrees").join("linked");
6782 fs::create_dir_all(&admin).expect("test operation should succeed");
6783 fs::write(admin.join("commondir"), "../..\n").expect("test operation should succeed");
6784 fs::write(admin.join("HEAD"), b"ref: refs/heads/topic\n")
6785 .expect("test operation should succeed");
6786 let oid = ObjectId::from_hex(
6787 ObjectFormat::Sha256,
6788 "08ffba112b648c22b5425f01bec2c37ffc524c4d48ef04337779df3973733050",
6789 )
6790 .expect("test operation should succeed");
6791 fs::create_dir_all(common.join("refs").join("heads"))
6792 .expect("test operation should succeed");
6793 fs::write(
6794 common.join("refs").join("heads").join("topic"),
6795 format!("{oid}\n"),
6796 )
6797 .expect("test operation should succeed");
6798
6799 let store = FileRefStore::new(&admin, ObjectFormat::Sha256);
6800 assert_eq!(
6801 store
6802 .read_ref("HEAD")
6803 .expect("test operation should succeed"),
6804 Some(RefTarget::Symbolic("refs/heads/topic".into()))
6805 );
6806 assert_eq!(
6807 store
6808 .read_ref("refs/heads/topic")
6809 .expect("test operation should succeed"),
6810 Some(RefTarget::Direct(oid))
6811 );
6812
6813 fs::remove_dir_all(common).expect("test operation should succeed");
6814 }
6815
6816 #[test]
6817 fn file_ref_store_creates_tag() {
6818 let git_dir = temp_git_dir();
6819 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6820 let oid = ObjectId::from_hex(
6821 ObjectFormat::Sha1,
6822 "ce013625030ba8dba906f756967f9e9ca394464a",
6823 )
6824 .expect("test operation should succeed");
6825 let tag = store
6826 .create_tag("v1.0", oid)
6827 .expect("test operation should succeed");
6828 assert_eq!(tag.name, "refs/tags/v1.0");
6829 assert_eq!(
6830 store
6831 .read_ref("refs/tags/v1.0")
6832 .expect("test operation should succeed"),
6833 Some(RefTarget::Direct(oid))
6834 );
6835 assert!(
6836 store
6837 .read_reflog("refs/tags/v1.0")
6838 .expect("test operation should succeed")
6839 .is_empty()
6840 );
6841 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6842 }
6843
6844 #[test]
6845 fn file_ref_store_deletes_loose_tag() {
6846 let git_dir = temp_git_dir();
6847 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6848 let oid = ObjectId::from_hex(
6849 ObjectFormat::Sha1,
6850 "ce013625030ba8dba906f756967f9e9ca394464a",
6851 )
6852 .expect("test operation should succeed");
6853 store
6854 .create_tag("v1.0", oid)
6855 .expect("test operation should succeed");
6856 let deleted = store
6857 .delete_tag("v1.0")
6858 .expect("test operation should succeed");
6859 assert_eq!(deleted.name, "refs/tags/v1.0");
6860 assert_eq!(deleted.oid, oid);
6861 assert_eq!(
6862 store
6863 .read_ref("refs/tags/v1.0")
6864 .expect("test operation should succeed"),
6865 None
6866 );
6867 assert!(!git_dir.join("refs").join("tags").join("v1.0").exists());
6868 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6869 }
6870
6871 #[test]
6872 fn file_ref_store_reads_packed_ref() {
6873 let git_dir = temp_git_dir();
6874 fs::write(
6875 git_dir.join("packed-refs"),
6876 b"ce013625030ba8dba906f756967f9e9ca394464a refs/heads/main\n",
6877 )
6878 .expect("test operation should succeed");
6879 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6880 assert!(matches!(
6881 store
6882 .read_ref("refs/heads/main")
6883 .expect("test operation should succeed"),
6884 Some(RefTarget::Direct(_))
6885 ));
6886 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6887 }
6888
6889 #[test]
6890 fn file_ref_store_lists_loose_refs_over_packed_refs() {
6891 let git_dir = temp_git_dir();
6892 fs::write(
6893 git_dir.join("packed-refs"),
6894 b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 refs/heads/main\n",
6895 )
6896 .expect("test operation should succeed");
6897 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6898 let oid = ObjectId::from_hex(
6899 ObjectFormat::Sha1,
6900 "ce013625030ba8dba906f756967f9e9ca394464a",
6901 )
6902 .expect("test operation should succeed");
6903 let mut tx = store.transaction();
6904 tx.update(RefUpdate {
6905 name: "refs/heads/main".into(),
6906 expected: None,
6907 new: RefTarget::Direct(oid),
6908 reflog: None,
6909 });
6910 tx.commit().expect("test operation should succeed");
6911 let refs = store.list_refs().expect("test operation should succeed");
6912 assert_eq!(refs.len(), 1);
6913 assert_eq!(refs[0].target, RefTarget::Direct(oid));
6914 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6915 }
6916
6917 #[test]
6918 fn file_ref_store_lists_refs_with_prefix_and_preserves_loose_shadowing() {
6919 let git_dir = temp_git_dir();
6920 fs::write(
6921 git_dir.join("packed-refs"),
6922 b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 refs/heads/main\n\
6923 18f002b4484b838b205a48b1e9e6763ba5e3a607 refs/heads/topic\n\
6924 ce013625030ba8dba906f756967f9e9ca394464a refs/tags/v1.0\n",
6925 )
6926 .expect("test operation should succeed");
6927 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6928 let loose_main = ObjectId::from_hex(
6929 ObjectFormat::Sha1,
6930 "ce013625030ba8dba906f756967f9e9ca394464a",
6931 )
6932 .expect("test operation should succeed");
6933 let packed_topic = ObjectId::from_hex(
6934 ObjectFormat::Sha1,
6935 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
6936 )
6937 .expect("test operation should succeed");
6938 let mut tx = store.transaction();
6939 tx.update(RefUpdate {
6940 name: "refs/heads/main".into(),
6941 expected: None,
6942 new: RefTarget::Direct(loose_main),
6943 reflog: None,
6944 });
6945 tx.commit().expect("test operation should succeed");
6946
6947 assert_eq!(
6948 store
6949 .list_refs_with_prefix("refs/heads/")
6950 .expect("test operation should succeed"),
6951 vec![
6952 Ref {
6953 name: "refs/heads/main".into(),
6954 target: RefTarget::Direct(loose_main),
6955 },
6956 Ref {
6957 name: "refs/heads/topic".into(),
6958 target: RefTarget::Direct(packed_topic),
6959 },
6960 ]
6961 );
6962 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6963 }
6964
6965 #[test]
6966 fn file_ref_store_writes_packed_refs() {
6967 let git_dir = temp_git_dir();
6968 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6969 let oid = ObjectId::from_hex(
6970 ObjectFormat::Sha1,
6971 "ce013625030ba8dba906f756967f9e9ca394464a",
6972 )
6973 .expect("test operation should succeed");
6974 store
6975 .write_packed_refs(&[PackedRef {
6976 reference: Ref {
6977 name: "refs/heads/main".into(),
6978 target: RefTarget::Direct(oid),
6979 },
6980 peeled: None,
6981 }])
6982 .expect("test operation should succeed");
6983 assert_eq!(
6984 store
6985 .read_ref("refs/heads/main")
6986 .expect("test operation should succeed"),
6987 Some(RefTarget::Direct(oid))
6988 );
6989 let refs = store.list_refs().expect("test operation should succeed");
6990 assert_eq!(refs.len(), 1);
6991 assert_eq!(refs[0].target, RefTarget::Direct(oid));
6992 assert!(git_dir.join("packed-refs").exists());
6993 assert!(!git_dir.join("packed-refs.lock").exists());
6994 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6995 }
6996
6997 #[test]
6998 fn file_ref_store_checks_ref_prefix_in_packed_refs() {
6999 let git_dir = temp_git_dir();
7000 fs::write(
7001 git_dir.join("packed-refs"),
7002 b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 refs/heads/main\n\
7003 ce013625030ba8dba906f756967f9e9ca394464a refs/replace/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\n",
7004 )
7005 .expect("test operation should succeed");
7006 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7007 assert!(
7008 store
7009 .has_refs_with_prefix("refs/replace/")
7010 .expect("test operation should succeed")
7011 );
7012 assert!(
7013 !store
7014 .has_refs_with_prefix("refs/notes/")
7015 .expect("test operation should succeed")
7016 );
7017 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7018 }
7019
7020 #[test]
7021 fn file_ref_store_checks_ref_prefix_in_loose_refs() {
7022 let git_dir = temp_git_dir();
7023 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7024 let oid = ObjectId::from_hex(
7025 ObjectFormat::Sha1,
7026 "ce013625030ba8dba906f756967f9e9ca394464a",
7027 )
7028 .expect("test operation should succeed");
7029 let mut tx = store.transaction();
7030 tx.update(RefUpdate {
7031 name: "refs/replace/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391".into(),
7032 expected: None,
7033 new: RefTarget::Direct(oid),
7034 reflog: None,
7035 });
7036 tx.commit().expect("test operation should succeed");
7037 assert!(
7038 store
7039 .has_refs_with_prefix("refs/replace/")
7040 .expect("test operation should succeed")
7041 );
7042 assert!(
7043 !store
7044 .has_refs_with_prefix("refs/notes/")
7045 .expect("test operation should succeed")
7046 );
7047 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7048 }
7049
7050 #[test]
7051 fn file_ref_store_reads_reftable_stack_and_ignores_dummy_head() {
7052 let git_dir = temp_git_dir();
7053 write_reftable_config(&git_dir);
7054 fs::write(git_dir.join("HEAD"), b"ref: refs/heads/.invalid\n")
7055 .expect("test operation should succeed");
7056 let head_oid = ObjectId::from_hex(
7057 ObjectFormat::Sha1,
7058 "ce013625030ba8dba906f756967f9e9ca394464a",
7059 )
7060 .expect("test operation should succeed");
7061 let tag_oid = ObjectId::from_hex(
7062 ObjectFormat::Sha1,
7063 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
7064 )
7065 .expect("test operation should succeed");
7066 let peeled_oid = ObjectId::from_hex(
7067 ObjectFormat::Sha1,
7068 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7069 )
7070 .expect("test operation should succeed");
7071 write_reftable_stack(
7072 &git_dir,
7073 &[(
7074 "0x000000000001-0x000000000001-00000000.ref",
7075 vec![
7076 sley_formats::ReftableRefRecord {
7077 name: "HEAD".into(),
7078 update_index: 1,
7079 value: ReftableRefValue::Symbolic("refs/heads/main".into()),
7080 },
7081 sley_formats::ReftableRefRecord {
7082 name: "refs/heads/main".into(),
7083 update_index: 1,
7084 value: ReftableRefValue::Direct(head_oid),
7085 },
7086 sley_formats::ReftableRefRecord {
7087 name: "refs/tags/v1.0".into(),
7088 update_index: 1,
7089 value: ReftableRefValue::Peeled {
7090 target: tag_oid,
7091 peeled: peeled_oid,
7092 },
7093 },
7094 ],
7095 )],
7096 );
7097
7098 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7099 assert_eq!(
7100 store
7101 .read_ref("HEAD")
7102 .expect("test operation should succeed"),
7103 Some(RefTarget::Symbolic("refs/heads/main".into()))
7104 );
7105 assert_eq!(
7106 store
7107 .read_ref("refs/heads/main")
7108 .expect("test operation should succeed"),
7109 Some(RefTarget::Direct(head_oid))
7110 );
7111 assert_eq!(
7112 store
7113 .read_ref("refs/tags/v1.0")
7114 .expect("test operation should succeed"),
7115 Some(RefTarget::Direct(tag_oid))
7116 );
7117 let refs = store.list_refs().expect("test operation should succeed");
7118 assert_eq!(
7119 refs,
7120 vec![
7121 Ref {
7122 name: "refs/heads/main".into(),
7123 target: RefTarget::Direct(head_oid),
7124 },
7125 Ref {
7126 name: "refs/tags/v1.0".into(),
7127 target: RefTarget::Direct(tag_oid),
7128 },
7129 ]
7130 );
7131 assert_eq!(
7132 store
7133 .list_refs_with_prefix("refs/tags/")
7134 .expect("test operation should succeed"),
7135 vec![Ref {
7136 name: "refs/tags/v1.0".into(),
7137 target: RefTarget::Direct(tag_oid),
7138 }]
7139 );
7140
7141 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7142 }
7143
7144 #[test]
7145 fn file_ref_store_reads_loose_fetch_head_in_reftable_repo() {
7146 let git_dir = temp_git_dir();
7147 write_reftable_config(&git_dir);
7148 fs::write(git_dir.join("HEAD"), b"ref: refs/heads/.invalid\n")
7149 .expect("test operation should succeed");
7150 fs::create_dir_all(git_dir.join("reftable")).expect("test operation should succeed");
7151 fs::write(git_dir.join("reftable").join("tables.list"), b"")
7152 .expect("test operation should succeed");
7153 let oid = ObjectId::from_hex(
7154 ObjectFormat::Sha1,
7155 "ce013625030ba8dba906f756967f9e9ca394464a",
7156 )
7157 .expect("test operation should succeed");
7158 fs::write(
7159 git_dir.join("FETCH_HEAD"),
7160 b"ce013625030ba8dba906f756967f9e9ca394464a\t\tbranch 'main' of ../sub\n",
7161 )
7162 .expect("test operation should succeed");
7163
7164 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7165 assert_eq!(
7166 store
7167 .read_ref("FETCH_HEAD")
7168 .expect("test operation should succeed"),
7169 Some(RefTarget::Direct(oid))
7170 );
7171 assert!(
7172 store
7173 .raw_ref_exists("FETCH_HEAD")
7174 .expect("test operation should succeed")
7175 );
7176
7177 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7178 }
7179
7180 #[test]
7181 fn file_ref_store_empty_reftable_reflog_rewrite_keeps_marker() {
7182 let git_dir = temp_git_dir();
7183 write_reftable_config(&git_dir);
7184 fs::create_dir_all(git_dir.join("reftable")).expect("test operation should succeed");
7185 fs::write(git_dir.join("reftable").join("tables.list"), b"")
7186 .expect("test operation should succeed");
7187 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7188
7189 store
7190 .write_reflog("refs/heads/main", &[])
7191 .expect("test operation should succeed");
7192
7193 assert!(
7194 store
7195 .read_reflog("refs/heads/main")
7196 .expect("test operation should succeed")
7197 .is_empty()
7198 );
7199 let tables = store.reftables().expect("test operation should succeed");
7200 let marker = tables
7201 .iter()
7202 .flat_map(|table| table.logs.iter())
7203 .find(|record| record.refname == "refs/heads/main")
7204 .expect("empty reflog marker should exist");
7205 let ReftableLogValue::Update(update) = &marker.value else {
7206 panic!("empty reflog marker should be an update");
7207 };
7208 let null = ObjectId::null(ObjectFormat::Sha1);
7209 assert_eq!(update.old_oid, null);
7210 assert_eq!(update.new_oid, null);
7211
7212 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7213 }
7214
7215 #[test]
7216 fn file_ref_store_lists_only_visible_reftable_reflog_names() {
7217 let git_dir = temp_git_dir();
7218 write_reftable_config(&git_dir);
7219 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7220 let oid = ObjectId::from_hex(
7221 ObjectFormat::Sha1,
7222 "ce013625030ba8dba906f756967f9e9ca394464a",
7223 )
7224 .expect("test operation should succeed");
7225
7226 store
7227 .write_reflog("refs/heads/empty", &[])
7228 .expect("test operation should succeed");
7229 store
7230 .append_reflog("refs/heads/main", &reflog_entry(&oid, 1, "create main"))
7231 .expect("test operation should succeed");
7232
7233 assert_eq!(
7234 store
7235 .list_reflog_names()
7236 .expect("test operation should succeed"),
7237 vec!["refs/heads/main".to_string()]
7238 );
7239
7240 store
7241 .write_reflog("refs/heads/main", &[])
7242 .expect("test operation should succeed");
7243 assert!(
7244 store
7245 .list_reflog_names()
7246 .expect("test operation should succeed")
7247 .is_empty()
7248 );
7249
7250 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7251 }
7252
7253 #[test]
7254 fn file_ref_store_applies_reftable_stack_overrides_and_deletions() {
7255 let git_dir = temp_git_dir();
7256 write_reftable_config(&git_dir);
7257 let first = ObjectId::from_hex(
7258 ObjectFormat::Sha1,
7259 "ce013625030ba8dba906f756967f9e9ca394464a",
7260 )
7261 .expect("test operation should succeed");
7262 let second = ObjectId::from_hex(
7263 ObjectFormat::Sha1,
7264 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7265 )
7266 .expect("test operation should succeed");
7267 write_reftable_stack(
7268 &git_dir,
7269 &[
7270 (
7271 "0x000000000001-0x000000000001-00000000.ref",
7272 vec![
7273 sley_formats::ReftableRefRecord {
7274 name: "refs/heads/main".into(),
7275 update_index: 1,
7276 value: ReftableRefValue::Direct(first),
7277 },
7278 sley_formats::ReftableRefRecord {
7279 name: "refs/heads/topic".into(),
7280 update_index: 1,
7281 value: ReftableRefValue::Direct(second.clone()),
7282 },
7283 ],
7284 ),
7285 (
7286 "000000000002-000000000002-tip.ref",
7287 vec![
7288 sley_formats::ReftableRefRecord {
7289 name: "refs/heads/main".into(),
7290 update_index: 2,
7291 value: ReftableRefValue::Direct(second.clone()),
7292 },
7293 sley_formats::ReftableRefRecord {
7294 name: "refs/heads/topic".into(),
7295 update_index: 2,
7296 value: ReftableRefValue::Deletion,
7297 },
7298 ],
7299 ),
7300 ],
7301 );
7302
7303 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7304 assert_eq!(
7305 store
7306 .read_ref("refs/heads/main")
7307 .expect("test operation should succeed"),
7308 Some(RefTarget::Direct(second.clone()))
7309 );
7310 assert_eq!(
7311 store
7312 .read_ref("refs/heads/topic")
7313 .expect("test operation should succeed"),
7314 None
7315 );
7316 assert_eq!(
7317 store.list_refs().expect("test operation should succeed"),
7318 vec![Ref {
7319 name: "refs/heads/main".into(),
7320 target: RefTarget::Direct(second),
7321 }]
7322 );
7323
7324 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7325 }
7326
7327 #[test]
7328 fn file_ref_store_writes_reftable_transaction_table() {
7329 let git_dir = temp_git_dir();
7330 write_reftable_config(&git_dir);
7331 let first = ObjectId::from_hex(
7332 ObjectFormat::Sha1,
7333 "ce013625030ba8dba906f756967f9e9ca394464a",
7334 )
7335 .expect("test operation should succeed");
7336 let second = ObjectId::from_hex(
7337 ObjectFormat::Sha1,
7338 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7339 )
7340 .expect("test operation should succeed");
7341 write_reftable_stack(
7342 &git_dir,
7343 &[(
7344 "0x000000000001-0x000000000001-00000000.ref",
7345 vec![sley_formats::ReftableRefRecord {
7346 name: "refs/heads/main".into(),
7347 update_index: 1,
7348 value: ReftableRefValue::Direct(first),
7349 }],
7350 )],
7351 );
7352
7353 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7354 let mut tx = store.transaction();
7355 tx.update(RefUpdate {
7356 name: "HEAD".into(),
7357 expected: None,
7358 new: RefTarget::Symbolic("refs/heads/main".into()),
7359 reflog: None,
7360 });
7361 tx.update(RefUpdate {
7362 name: "refs/heads/main".into(),
7363 expected: None,
7364 new: RefTarget::Direct(second.clone()),
7365 reflog: None,
7366 });
7367 tx.commit().expect("test operation should succeed");
7368
7369 assert_eq!(
7370 store
7371 .read_ref("HEAD")
7372 .expect("test operation should succeed"),
7373 Some(RefTarget::Symbolic("refs/heads/main".into()))
7374 );
7375 assert_eq!(
7376 store
7377 .read_ref("refs/heads/main")
7378 .expect("test operation should succeed"),
7379 Some(RefTarget::Direct(second.clone()))
7380 );
7381 assert_eq!(
7382 store
7383 .list_refs()
7384 .expect("test operation should succeed")
7385 .len(),
7386 1
7387 );
7388 assert!(!git_dir.join("HEAD").exists());
7389 let tables = fs::read_to_string(git_dir.join("reftable").join("tables.list"))
7390 .expect("test operation should succeed");
7391 assert_eq!(tables.lines().count(), 2);
7392 let last = tables
7393 .lines()
7394 .last()
7395 .expect("test operation should succeed");
7396 assert!(
7400 last.starts_with("0x") && last.ends_with(".ref"),
7401 "expected git-format reftable name in tables.list, got {tables}"
7402 );
7403 assert!(
7404 reftable_table_name_is_valid(last),
7405 "rust-written reftable name must parse as git's hex format, got {last}"
7406 );
7407
7408 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7409 }
7410
7411 #[test]
7412 fn file_ref_store_deletes_reftable_refs_with_tombstones() {
7413 let git_dir = temp_git_dir();
7414 write_reftable_config(&git_dir);
7415 let oid = ObjectId::from_hex(
7416 ObjectFormat::Sha1,
7417 "ce013625030ba8dba906f756967f9e9ca394464a",
7418 )
7419 .expect("test operation should succeed");
7420 write_reftable_stack(
7421 &git_dir,
7422 &[(
7423 "0x000000000001-0x000000000001-00000000.ref",
7424 vec![
7425 sley_formats::ReftableRefRecord {
7426 name: "refs/heads/main".into(),
7427 update_index: 1,
7428 value: ReftableRefValue::Direct(oid),
7429 },
7430 sley_formats::ReftableRefRecord {
7431 name: "refs/alias/main".into(),
7432 update_index: 1,
7433 value: ReftableRefValue::Symbolic("refs/heads/main".into()),
7434 },
7435 ],
7436 )],
7437 );
7438
7439 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7440 assert!(
7441 store
7442 .delete_symbolic_ref("refs/alias/main")
7443 .expect("test operation should succeed")
7444 );
7445 assert_eq!(
7446 store
7447 .read_ref("refs/alias/main")
7448 .expect("test operation should succeed"),
7449 None
7450 );
7451 let deleted = store
7452 .delete_ref("refs/heads/main")
7453 .expect("test operation should succeed");
7454 assert_eq!(deleted.oid, oid);
7455 assert_eq!(
7456 store
7457 .read_ref("refs/heads/main")
7458 .expect("test operation should succeed"),
7459 None
7460 );
7461 assert!(
7462 store
7463 .list_refs()
7464 .expect("test operation should succeed")
7465 .is_empty()
7466 );
7467 let tables = fs::read_to_string(git_dir.join("reftable").join("tables.list"))
7468 .expect("test operation should succeed");
7469 assert_eq!(tables.lines().count(), 3);
7470
7471 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7472 }
7473
7474 #[test]
7475 fn file_ref_store_deletes_packed_branch() {
7476 let git_dir = temp_git_dir();
7477 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7478 let branch_oid = ObjectId::from_hex(
7479 ObjectFormat::Sha1,
7480 "ce013625030ba8dba906f756967f9e9ca394464a",
7481 )
7482 .expect("test operation should succeed");
7483 let tag_oid = ObjectId::from_hex(
7484 ObjectFormat::Sha1,
7485 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7486 )
7487 .expect("test operation should succeed");
7488 store
7489 .write_packed_refs(&[
7490 PackedRef {
7491 reference: Ref {
7492 name: "refs/heads/feature".into(),
7493 target: RefTarget::Direct(branch_oid),
7494 },
7495 peeled: None,
7496 },
7497 PackedRef {
7498 reference: Ref {
7499 name: "refs/tags/v1.0".into(),
7500 target: RefTarget::Direct(tag_oid),
7501 },
7502 peeled: None,
7503 },
7504 ])
7505 .expect("test operation should succeed");
7506 let deleted = store
7507 .delete_branch("feature")
7508 .expect("test operation should succeed");
7509 assert_eq!(deleted.name, "refs/heads/feature");
7510 assert_eq!(deleted.oid, branch_oid);
7511 assert_eq!(
7512 store
7513 .read_ref("refs/heads/feature")
7514 .expect("test operation should succeed"),
7515 None
7516 );
7517 assert_eq!(
7518 store
7519 .read_ref("refs/tags/v1.0")
7520 .expect("test operation should succeed"),
7521 Some(RefTarget::Direct(tag_oid))
7522 );
7523 assert!(!git_dir.join("packed-refs.lock").exists());
7524 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7525 }
7526
7527 #[test]
7528 fn file_ref_store_deletes_packed_tag() {
7529 let git_dir = temp_git_dir();
7530 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7531 let oid = ObjectId::from_hex(
7532 ObjectFormat::Sha1,
7533 "ce013625030ba8dba906f756967f9e9ca394464a",
7534 )
7535 .expect("test operation should succeed");
7536 store
7537 .write_packed_refs(&[PackedRef {
7538 reference: Ref {
7539 name: "refs/tags/v1.0".into(),
7540 target: RefTarget::Direct(oid),
7541 },
7542 peeled: None,
7543 }])
7544 .expect("test operation should succeed");
7545 let deleted = store
7546 .delete_tag("v1.0")
7547 .expect("test operation should succeed");
7548 assert_eq!(deleted.name, "refs/tags/v1.0");
7549 assert_eq!(deleted.oid, oid);
7550 assert_eq!(
7551 store
7552 .read_ref("refs/tags/v1.0")
7553 .expect("test operation should succeed"),
7554 None
7555 );
7556 assert!(!git_dir.join("packed-refs.lock").exists());
7557 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7558 }
7559
7560 #[test]
7561 fn file_ref_store_packs_loose_refs_and_prunes() {
7562 let git_dir = temp_git_dir();
7563 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7564 let main_oid = ObjectId::from_hex(
7565 ObjectFormat::Sha1,
7566 "ce013625030ba8dba906f756967f9e9ca394464a",
7567 )
7568 .expect("test operation should succeed");
7569 let tag_oid = ObjectId::from_hex(
7570 ObjectFormat::Sha1,
7571 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7572 )
7573 .expect("test operation should succeed");
7574 let mut tx = store.transaction();
7575 tx.update(RefUpdate {
7576 name: "refs/heads/main".into(),
7577 expected: None,
7578 new: RefTarget::Direct(main_oid),
7579 reflog: None,
7580 });
7581 tx.update(RefUpdate {
7582 name: "refs/tags/v1.0".into(),
7583 expected: None,
7584 new: RefTarget::Direct(tag_oid),
7585 reflog: None,
7586 });
7587 tx.commit().expect("test operation should succeed");
7588
7589 let packed = store
7590 .pack_refs(true)
7591 .expect("test operation should succeed");
7592 assert_eq!(packed.len(), 2);
7593 assert_eq!(
7594 store
7595 .read_ref("refs/heads/main")
7596 .expect("test operation should succeed"),
7597 Some(RefTarget::Direct(main_oid))
7598 );
7599 assert_eq!(
7600 store
7601 .read_ref("refs/tags/v1.0")
7602 .expect("test operation should succeed"),
7603 Some(RefTarget::Direct(tag_oid))
7604 );
7605 assert!(!git_dir.join("refs").join("heads").join("main").exists());
7606 assert!(!git_dir.join("refs").join("tags").join("v1.0").exists());
7607 assert!(git_dir.join("packed-refs").exists());
7608 assert!(!git_dir.join("packed-refs.lock").exists());
7609 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7610 }
7611
7612 #[test]
7613 fn file_ref_store_packs_loose_refs_without_pruning() {
7614 let git_dir = temp_git_dir();
7615 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7616 let oid = ObjectId::from_hex(
7617 ObjectFormat::Sha1,
7618 "ce013625030ba8dba906f756967f9e9ca394464a",
7619 )
7620 .expect("test operation should succeed");
7621 let mut tx = store.transaction();
7622 tx.update(RefUpdate {
7623 name: "refs/heads/main".into(),
7624 expected: None,
7625 new: RefTarget::Direct(oid),
7626 reflog: None,
7627 });
7628 tx.commit().expect("test operation should succeed");
7629
7630 let packed = store
7631 .pack_refs(false)
7632 .expect("test operation should succeed");
7633 assert_eq!(packed.len(), 1);
7634 assert!(git_dir.join("refs").join("heads").join("main").exists());
7635 assert_eq!(
7636 store
7637 .read_ref("refs/heads/main")
7638 .expect("test operation should succeed"),
7639 Some(RefTarget::Direct(oid))
7640 );
7641 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7642 }
7643
7644 #[test]
7645 fn file_ref_store_packs_loose_refs_with_peeled_ids() {
7646 let git_dir = temp_git_dir();
7647 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7648 let tag_oid = ObjectId::from_hex(
7649 ObjectFormat::Sha1,
7650 "ce013625030ba8dba906f756967f9e9ca394464a",
7651 )
7652 .expect("test operation should succeed");
7653 let peeled_oid = ObjectId::from_hex(
7654 ObjectFormat::Sha1,
7655 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7656 )
7657 .expect("test operation should succeed");
7658 let mut tx = store.transaction();
7659 tx.update(RefUpdate {
7660 name: "refs/tags/v1.0".into(),
7661 expected: None,
7662 new: RefTarget::Direct(tag_oid),
7663 reflog: None,
7664 });
7665 tx.commit().expect("test operation should succeed");
7666
7667 let packed = store
7668 .pack_refs_with_peeler(true, |name, oid| {
7669 if name == "refs/tags/v1.0" && oid == &tag_oid {
7670 Ok(Some(peeled_oid))
7671 } else {
7672 Ok(None)
7673 }
7674 })
7675 .expect("test operation should succeed");
7676 assert_eq!(packed.len(), 1);
7677 assert_eq!(packed[0].peeled, Some(peeled_oid));
7678 let bytes =
7679 fs::read_to_string(git_dir.join("packed-refs")).expect("test operation should succeed");
7680 assert!(bytes.contains(&format!("^{peeled_oid}\n")));
7681 assert!(!git_dir.join("refs").join("tags").join("v1.0").exists());
7682 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7683 }
7684
7685 fn reflog_entry(new_oid: &ObjectId, timestamp: i64, message: &str) -> ReflogEntry {
7686 ReflogEntry {
7687 old_oid: zero_oid(new_oid.format()).expect("test operation should succeed"),
7688 new_oid: *new_oid,
7689 committer: format!("Git Rs <sley@example.invalid> {timestamp} +0000").into_bytes(),
7690 message: message.as_bytes().to_vec(),
7691 }
7692 }
7693
7694 #[test]
7695 fn expire_reflog_drops_old_entries_and_keeps_latest() {
7696 let oid_a = ObjectId::from_hex(
7697 ObjectFormat::Sha1,
7698 "ce013625030ba8dba906f756967f9e9ca394464a",
7699 )
7700 .expect("test operation should succeed");
7701 let oid_b = ObjectId::from_hex(
7702 ObjectFormat::Sha1,
7703 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7704 )
7705 .expect("test operation should succeed");
7706 let oid_c = ObjectId::from_hex(
7707 ObjectFormat::Sha1,
7708 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
7709 )
7710 .expect("test operation should succeed");
7711 let entries = vec![
7712 reflog_entry(&oid_a, 10, "oldest"),
7713 reflog_entry(&oid_b, 100, "middle"),
7714 reflog_entry(&oid_c, 20, "latest"),
7715 ];
7716
7717 let retained =
7720 expire_reflog(&entries, 50, None, |_| true).expect("test operation should succeed");
7721 assert_eq!(retained.len(), 2);
7722 assert_eq!(retained[0].message, b"middle");
7723 assert_eq!(retained[1].message, b"latest");
7724 }
7725
7726 #[test]
7727 fn expire_reflog_applies_stricter_unreachable_cutoff() {
7728 let reachable = ObjectId::from_hex(
7729 ObjectFormat::Sha1,
7730 "ce013625030ba8dba906f756967f9e9ca394464a",
7731 )
7732 .expect("test operation should succeed");
7733 let unreachable = ObjectId::from_hex(
7734 ObjectFormat::Sha1,
7735 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7736 )
7737 .expect("test operation should succeed");
7738 let tip = ObjectId::from_hex(
7739 ObjectFormat::Sha1,
7740 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
7741 )
7742 .expect("test operation should succeed");
7743 let entries = vec![
7746 reflog_entry(&reachable, 100, "reachable"),
7747 reflog_entry(&unreachable, 100, "unreachable"),
7748 reflog_entry(&tip, 200, "tip"),
7749 ];
7750 let retained = expire_reflog(&entries, 50, Some(150), |oid| {
7751 oid == &reachable || oid == &tip
7752 })
7753 .expect("test operation should succeed");
7754 assert_eq!(retained.len(), 2);
7755 assert_eq!(retained[0].message, b"reachable");
7756 assert_eq!(retained[1].message, b"tip");
7757 }
7758
7759 #[test]
7760 fn expire_reflog_keeps_single_entry_below_cutoff() {
7761 let oid = ObjectId::from_hex(
7762 ObjectFormat::Sha1,
7763 "ce013625030ba8dba906f756967f9e9ca394464a",
7764 )
7765 .expect("test operation should succeed");
7766 let entries = vec![reflog_entry(&oid, 1, "only")];
7767 let retained = expire_reflog(&entries, i64::MAX, Some(i64::MAX), |_| false)
7768 .expect("test operation should succeed");
7769 assert_eq!(retained.len(), 1);
7770 assert_eq!(retained[0].message, b"only");
7771 }
7772
7773 #[test]
7774 fn file_ref_store_expire_reflog_file_rewrites_and_dry_runs() {
7775 let git_dir = temp_git_dir();
7776 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7777 let first = ObjectId::from_hex(
7778 ObjectFormat::Sha1,
7779 "ce013625030ba8dba906f756967f9e9ca394464a",
7780 )
7781 .expect("test operation should succeed");
7782 let second = ObjectId::from_hex(
7783 ObjectFormat::Sha1,
7784 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7785 )
7786 .expect("test operation should succeed");
7787 store
7788 .write_reflog(
7789 "refs/heads/main",
7790 &[
7791 reflog_entry(&first, 10, "old"),
7792 reflog_entry(&second, 100, "new"),
7793 ],
7794 )
7795 .expect("test operation should succeed");
7796
7797 let would_remove = store
7799 .expire_reflog_file("refs/heads/main", 50, None, false, |_| true)
7800 .expect("test operation should succeed");
7801 assert_eq!(would_remove, 1);
7802 assert_eq!(
7803 store
7804 .read_reflog("refs/heads/main")
7805 .expect("test operation should succeed")
7806 .len(),
7807 2
7808 );
7809
7810 let removed = store
7812 .expire_reflog_file("refs/heads/main", 50, None, true, |_| true)
7813 .expect("test operation should succeed");
7814 assert_eq!(removed, 1);
7815 let log = store
7816 .read_reflog("refs/heads/main")
7817 .expect("test operation should succeed");
7818 assert_eq!(log.len(), 1);
7819 assert_eq!(log[0].new_oid, second);
7820 assert!(
7821 !git_dir
7822 .join("logs")
7823 .join("refs")
7824 .join("heads")
7825 .join("main.lock")
7826 .exists()
7827 );
7828 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7829 }
7830
7831 #[test]
7832 fn file_ref_transaction_commits_all_refs_atomically() {
7833 let git_dir = temp_git_dir();
7834 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7835 let main_oid = ObjectId::from_hex(
7836 ObjectFormat::Sha1,
7837 "ce013625030ba8dba906f756967f9e9ca394464a",
7838 )
7839 .expect("test operation should succeed");
7840 let topic_oid = ObjectId::from_hex(
7841 ObjectFormat::Sha1,
7842 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7843 )
7844 .expect("test operation should succeed");
7845 let tag_oid = ObjectId::from_hex(
7846 ObjectFormat::Sha1,
7847 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
7848 )
7849 .expect("test operation should succeed");
7850 let mut tx = store.transaction();
7851 tx.update(RefUpdate {
7852 name: "refs/heads/main".into(),
7853 expected: None,
7854 new: RefTarget::Direct(main_oid),
7855 reflog: Some(reflog_entry(&main_oid, 0, "create main")),
7856 });
7857 tx.update(RefUpdate {
7858 name: "refs/heads/topic".into(),
7859 expected: None,
7860 new: RefTarget::Direct(topic_oid),
7861 reflog: None,
7862 });
7863 tx.update(RefUpdate {
7864 name: "refs/tags/v1.0".into(),
7865 expected: None,
7866 new: RefTarget::Direct(tag_oid),
7867 reflog: None,
7868 });
7869 tx.commit().expect("test operation should succeed");
7870
7871 assert_eq!(
7872 store
7873 .read_ref("refs/heads/main")
7874 .expect("test operation should succeed"),
7875 Some(RefTarget::Direct(main_oid))
7876 );
7877 assert_eq!(
7878 store
7879 .read_ref("refs/heads/topic")
7880 .expect("test operation should succeed"),
7881 Some(RefTarget::Direct(topic_oid))
7882 );
7883 assert_eq!(
7884 store
7885 .read_ref("refs/tags/v1.0")
7886 .expect("test operation should succeed"),
7887 Some(RefTarget::Direct(tag_oid))
7888 );
7889 let main_log = store
7890 .read_reflog("refs/heads/main")
7891 .expect("test operation should succeed");
7892 assert_eq!(main_log.len(), 1);
7893 assert_eq!(main_log[0].new_oid, main_oid);
7894 assert!(
7896 !git_dir
7897 .join("refs")
7898 .join("heads")
7899 .join("main.lock")
7900 .exists()
7901 );
7902 assert!(
7903 !git_dir
7904 .join("refs")
7905 .join("heads")
7906 .join("topic.lock")
7907 .exists()
7908 );
7909 assert!(!git_dir.join("refs").join("tags").join("v1.0.lock").exists());
7910 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7911 }
7912
7913 #[test]
7914 fn file_ref_transaction_rolls_back_all_refs_on_expected_mismatch() {
7915 let git_dir = temp_git_dir();
7916 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7917 let old_topic = ObjectId::from_hex(
7918 ObjectFormat::Sha1,
7919 "ce013625030ba8dba906f756967f9e9ca394464a",
7920 )
7921 .expect("test operation should succeed");
7922 let new_main = ObjectId::from_hex(
7923 ObjectFormat::Sha1,
7924 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7925 )
7926 .expect("test operation should succeed");
7927 let new_tag = ObjectId::from_hex(
7928 ObjectFormat::Sha1,
7929 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
7930 )
7931 .expect("test operation should succeed");
7932 let wrong_expected = ObjectId::from_hex(
7933 ObjectFormat::Sha1,
7934 "0000000000000000000000000000000000000001",
7935 )
7936 .expect("test operation should succeed");
7937
7938 let mut seed = store.transaction();
7941 seed.update(RefUpdate {
7942 name: "refs/heads/topic".into(),
7943 expected: None,
7944 new: RefTarget::Direct(old_topic.clone()),
7945 reflog: None,
7946 });
7947 seed.commit().expect("test operation should succeed");
7948
7949 let mut tx = store.transaction();
7950 tx.update(RefUpdate {
7952 name: "refs/heads/main".into(),
7953 expected: None,
7954 new: RefTarget::Direct(new_main.clone()),
7955 reflog: Some(reflog_entry(&new_main, 0, "create main")),
7956 });
7957 tx.update(RefUpdate {
7959 name: "refs/heads/topic".into(),
7960 expected: Some(RefTarget::Direct(wrong_expected)),
7961 new: RefTarget::Direct(new_main.clone()),
7962 reflog: None,
7963 });
7964 tx.update(RefUpdate {
7966 name: "refs/tags/v1.0".into(),
7967 expected: None,
7968 new: RefTarget::Direct(new_tag),
7969 reflog: None,
7970 });
7971 let result = tx.commit();
7972 assert!(result.is_err());
7973
7974 assert_eq!(
7977 store
7978 .read_ref("refs/heads/main")
7979 .expect("test operation should succeed"),
7980 None
7981 );
7982 assert_eq!(
7983 store
7984 .read_ref("refs/heads/topic")
7985 .expect("test operation should succeed"),
7986 Some(RefTarget::Direct(old_topic))
7987 );
7988 assert_eq!(
7989 store
7990 .read_ref("refs/tags/v1.0")
7991 .expect("test operation should succeed"),
7992 None
7993 );
7994 assert!(
7995 store
7996 .read_reflog("refs/heads/main")
7997 .expect("test operation should succeed")
7998 .is_empty()
7999 );
8000
8001 assert!(
8003 !git_dir
8004 .join("refs")
8005 .join("heads")
8006 .join("main.lock")
8007 .exists()
8008 );
8009 assert!(
8010 !git_dir
8011 .join("refs")
8012 .join("heads")
8013 .join("topic.lock")
8014 .exists()
8015 );
8016 assert!(!git_dir.join("refs").join("tags").join("v1.0.lock").exists());
8017 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8018 }
8019
8020 #[test]
8021 fn file_ref_transaction_mixes_update_and_delete() {
8022 let git_dir = temp_git_dir();
8023 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8024 let old_main = ObjectId::from_hex(
8025 ObjectFormat::Sha1,
8026 "ce013625030ba8dba906f756967f9e9ca394464a",
8027 )
8028 .expect("test operation should succeed");
8029 let new_topic = ObjectId::from_hex(
8030 ObjectFormat::Sha1,
8031 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
8032 )
8033 .expect("test operation should succeed");
8034 let mut seed = store.transaction();
8035 seed.update(RefUpdate {
8036 name: "refs/heads/main".into(),
8037 expected: None,
8038 new: RefTarget::Direct(old_main),
8039 reflog: None,
8040 });
8041 seed.commit().expect("test operation should succeed");
8042
8043 let mut tx = store.transaction();
8044 tx.update(RefUpdate {
8045 name: "refs/heads/topic".into(),
8046 expected: None,
8047 new: RefTarget::Direct(new_topic),
8048 reflog: None,
8049 });
8050 tx.delete_with_precondition(
8051 "refs/heads/main",
8052 RefDeletePrecondition::Direct(Some(old_main)),
8053 None,
8054 );
8055 tx.commit().expect("test operation should succeed");
8056
8057 assert_eq!(
8058 store
8059 .read_ref("refs/heads/main")
8060 .expect("test operation should succeed"),
8061 None
8062 );
8063 assert_eq!(
8064 store
8065 .read_ref("refs/heads/topic")
8066 .expect("test operation should succeed"),
8067 Some(RefTarget::Direct(new_topic))
8068 );
8069 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8070 }
8071
8072 #[test]
8073 fn file_ref_transaction_rejects_deleted_descendant_parent_create() {
8074 let git_dir = temp_git_dir();
8075 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8076 let old_conflict = ObjectId::from_hex(
8077 ObjectFormat::Sha1,
8078 "ce013625030ba8dba906f756967f9e9ca394464a",
8079 )
8080 .expect("test operation should succeed");
8081 let new_parent = ObjectId::from_hex(
8082 ObjectFormat::Sha1,
8083 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
8084 )
8085 .expect("test operation should succeed");
8086 let mut seed = store.transaction();
8087 seed.update(RefUpdate {
8088 name: "refs/heads/branch/conflict".into(),
8089 expected: None,
8090 new: RefTarget::Direct(old_conflict),
8091 reflog: None,
8092 });
8093 seed.commit().expect("test operation should succeed");
8094
8095 let mut tx = store.transaction();
8096 tx.delete_with_precondition(
8097 "refs/heads/branch/conflict",
8098 RefDeletePrecondition::Direct(Some(old_conflict)),
8099 None,
8100 );
8101 tx.update(RefUpdate {
8102 name: "refs/heads/branch".into(),
8103 expected: None,
8104 new: RefTarget::Direct(new_parent),
8105 reflog: None,
8106 });
8107 let err = tx
8108 .commit()
8109 .expect_err("D/F-conflicting delete plus create must fail");
8110 assert_eq!(
8111 err.to_string(),
8112 "transaction failed: cannot lock ref 'refs/heads/branch': 'refs/heads/branch/conflict' exists; cannot create 'refs/heads/branch'"
8113 );
8114
8115 assert_eq!(
8116 store
8117 .read_ref("refs/heads/branch/conflict")
8118 .expect("test operation should succeed"),
8119 Some(RefTarget::Direct(old_conflict))
8120 );
8121 assert_eq!(
8122 store
8123 .read_ref("refs/heads/branch")
8124 .expect("test operation should succeed"),
8125 None
8126 );
8127 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8128 }
8129
8130 #[test]
8131 fn file_ref_transaction_stale_delete_rolls_back_update() {
8132 let git_dir = temp_git_dir();
8133 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8134 let old_oid = ObjectId::from_hex(
8135 ObjectFormat::Sha1,
8136 "ce013625030ba8dba906f756967f9e9ca394464a",
8137 )
8138 .expect("test operation should succeed");
8139 let new_oid = ObjectId::from_hex(
8140 ObjectFormat::Sha1,
8141 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
8142 )
8143 .expect("test operation should succeed");
8144 let mut seed = store.transaction();
8145 for name in ["refs/heads/main", "refs/heads/topic"] {
8146 seed.update(RefUpdate {
8147 name: name.into(),
8148 expected: None,
8149 new: RefTarget::Direct(old_oid),
8150 reflog: None,
8151 });
8152 }
8153 seed.commit().expect("test operation should succeed");
8154
8155 let mut tx = store.transaction();
8156 tx.update(RefUpdate {
8157 name: "refs/heads/topic".into(),
8158 expected: None,
8159 new: RefTarget::Direct(new_oid),
8160 reflog: None,
8161 });
8162 tx.delete_with_precondition(
8163 "refs/heads/main",
8164 RefDeletePrecondition::Direct(Some(new_oid)),
8165 None,
8166 );
8167 let err = tx.commit().expect_err("stale delete must abort");
8168 assert!(err.to_string().contains("expected ref refs/heads/main"));
8169
8170 assert_eq!(
8171 store
8172 .read_ref("refs/heads/main")
8173 .expect("test operation should succeed"),
8174 Some(RefTarget::Direct(old_oid))
8175 );
8176 assert_eq!(
8177 store
8178 .read_ref("refs/heads/topic")
8179 .expect("test operation should succeed"),
8180 Some(RefTarget::Direct(old_oid))
8181 );
8182 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8183 }
8184
8185 #[test]
8186 fn file_ref_transaction_rejects_duplicate_mixed_ref() {
8187 let git_dir = temp_git_dir();
8188 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8189 let oid = ObjectId::from_hex(
8190 ObjectFormat::Sha1,
8191 "ce013625030ba8dba906f756967f9e9ca394464a",
8192 )
8193 .expect("test operation should succeed");
8194 let mut tx = store.transaction();
8195 tx.update(RefUpdate {
8196 name: "refs/heads/main".into(),
8197 expected: None,
8198 new: RefTarget::Direct(oid),
8199 reflog: None,
8200 });
8201 tx.delete_with_precondition("refs/heads/main", RefDeletePrecondition::Any, None);
8202
8203 let err = tx.commit().expect_err("duplicate ref must fail");
8204 assert!(err.to_string().contains("refs/heads/main"));
8205 assert_eq!(
8206 store
8207 .read_ref("refs/heads/main")
8208 .expect("test operation should succeed"),
8209 None
8210 );
8211 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8212 }
8213
8214 #[test]
8215 fn file_ref_transaction_deletes_symbolic_ref_with_immediate_expectation() {
8216 let git_dir = temp_git_dir();
8217 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8218 let oid = ObjectId::from_hex(
8219 ObjectFormat::Sha1,
8220 "ce013625030ba8dba906f756967f9e9ca394464a",
8221 )
8222 .expect("test operation should succeed");
8223 let mut seed = store.transaction();
8224 seed.update(RefUpdate {
8225 name: "refs/heads/main".into(),
8226 expected: None,
8227 new: RefTarget::Direct(oid),
8228 reflog: None,
8229 });
8230 seed.update(RefUpdate {
8231 name: "refs/aliases/main".into(),
8232 expected: None,
8233 new: RefTarget::Symbolic("refs/heads/main".into()),
8234 reflog: None,
8235 });
8236 seed.commit().expect("test operation should succeed");
8237
8238 let mut tx = store.transaction();
8239 tx.delete_with_precondition(
8240 "refs/aliases/main",
8241 RefDeletePrecondition::Immediate(RefTarget::Symbolic("refs/heads/main".into())),
8242 None,
8243 );
8244 tx.commit().expect("test operation should succeed");
8245
8246 assert_eq!(
8247 store
8248 .read_ref("refs/aliases/main")
8249 .expect("test operation should succeed"),
8250 None
8251 );
8252 assert_eq!(
8253 store
8254 .read_ref("refs/heads/main")
8255 .expect("test operation should succeed"),
8256 Some(RefTarget::Direct(oid))
8257 );
8258 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8259 }
8260
8261 #[test]
8262 fn file_ref_transaction_rolls_back_delete_after_late_write_failure() {
8263 let git_dir = temp_git_dir();
8264 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8265 let old_oid = ObjectId::from_hex(
8266 ObjectFormat::Sha1,
8267 "ce013625030ba8dba906f756967f9e9ca394464a",
8268 )
8269 .expect("test operation should succeed");
8270 let new_oid = ObjectId::from_hex(
8271 ObjectFormat::Sha1,
8272 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
8273 )
8274 .expect("test operation should succeed");
8275 let mut seed = store.transaction();
8276 for name in ["refs/heads/main", "refs/heads/topic"] {
8277 seed.update(RefUpdate {
8278 name: name.into(),
8279 expected: None,
8280 new: RefTarget::Direct(old_oid),
8281 reflog: None,
8282 });
8283 }
8284 seed.commit().expect("test operation should succeed");
8285
8286 set_fail_loose_commit_action_for_test(Some(1));
8287 let mut tx = store.transaction();
8288 tx.delete_with_precondition(
8289 "refs/heads/main",
8290 RefDeletePrecondition::Direct(Some(old_oid)),
8291 None,
8292 );
8293 tx.update(RefUpdate {
8294 name: "refs/heads/topic".into(),
8295 expected: None,
8296 new: RefTarget::Direct(new_oid),
8297 reflog: None,
8298 });
8299 let err = tx.commit().expect_err("injected failure must abort");
8300 assert!(
8301 err.to_string()
8302 .contains("injected loose ref transaction failure")
8303 );
8304
8305 assert_eq!(
8306 store
8307 .read_ref("refs/heads/main")
8308 .expect("test operation should succeed"),
8309 Some(RefTarget::Direct(old_oid))
8310 );
8311 assert_eq!(
8312 store
8313 .read_ref("refs/heads/topic")
8314 .expect("test operation should succeed"),
8315 Some(RefTarget::Direct(old_oid))
8316 );
8317 assert!(
8318 !git_dir
8319 .join("refs")
8320 .join("heads")
8321 .join("main.lock")
8322 .exists()
8323 );
8324 assert!(
8325 !git_dir
8326 .join("refs")
8327 .join("heads")
8328 .join("topic.lock")
8329 .exists()
8330 );
8331 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8332 }
8333
8334 fn temp_git_dir() -> PathBuf {
8335 let path = std::env::temp_dir().join(format!(
8336 "sley-refs-{}-{}",
8337 std::process::id(),
8338 TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
8339 ));
8340 fs::create_dir_all(&path).expect("test operation should succeed");
8341 path
8342 }
8343
8344 fn zero_oid(format: ObjectFormat) -> Result<ObjectId> {
8345 Ok(ObjectId::null(format))
8346 }
8347
8348 fn write_reftable_config(git_dir: &Path) {
8349 fs::write(
8350 git_dir.join("config"),
8351 b"[core]\n\trepositoryformatversion = 1\n[extensions]\n\trefStorage = reftable\n",
8352 )
8353 .expect("test operation should succeed");
8354 }
8355
8356 fn write_reftable_stack(
8357 git_dir: &Path,
8358 tables: &[(&str, Vec<sley_formats::ReftableRefRecord>)],
8359 ) {
8360 let reftable_dir = git_dir.join("reftable");
8361 fs::create_dir_all(&reftable_dir).expect("test operation should succeed");
8362 let mut list = String::new();
8363 for (idx, (name, refs)) in tables.iter().enumerate() {
8364 let update_index = (idx + 1) as u64;
8365 let bytes = sley_formats::Reftable::write_ref_only(
8366 ObjectFormat::Sha1,
8367 update_index,
8368 update_index,
8369 refs,
8370 )
8371 .expect("test operation should succeed");
8372 fs::write(reftable_dir.join(name), bytes).expect("test operation should succeed");
8373 list.push_str(name);
8374 list.push('\n');
8375 }
8376 fs::write(reftable_dir.join("tables.list"), list).expect("test operation should succeed");
8377 }
8378}