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, ReftableWriteOptions,
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
23pub mod branch;
24pub mod migration;
25
26pub fn refname_pattern_matches_case(pattern: &str, name: &str, ignore_case: bool) -> bool {
31 fn matches_from(pattern: &[u8], name: &[u8]) -> bool {
32 match pattern {
33 [] => name.is_empty(),
34 [b'*', rest @ ..] => {
35 matches_from(rest, name) || (!name.is_empty() && matches_from(pattern, &name[1..]))
36 }
37 [b'?', rest @ ..] => !name.is_empty() && matches_from(rest, &name[1..]),
38 [b'\\', escaped, rest @ ..] => {
39 matches!(name, [first, ..] if first == escaped) && matches_from(rest, &name[1..])
40 }
41 [b'[', rest @ ..] => {
42 if let Some((matched, consumed)) =
43 match_refname_pattern_class(rest, name.first().copied())
44 {
45 !name.is_empty() && matched && matches_from(&rest[consumed..], &name[1..])
46 } else {
47 matches!(name, [b'[', ..]) && matches_from(rest, &name[1..])
48 }
49 }
50 [literal, rest @ ..] => {
51 matches!(name, [first, ..] if first == literal) && matches_from(rest, &name[1..])
52 }
53 }
54 }
55
56 if ignore_case {
57 let pattern = pattern
58 .as_bytes()
59 .iter()
60 .map(u8::to_ascii_lowercase)
61 .collect::<Vec<_>>();
62 let name = name
63 .as_bytes()
64 .iter()
65 .map(u8::to_ascii_lowercase)
66 .collect::<Vec<_>>();
67 matches_from(&pattern, &name)
68 } else {
69 matches_from(pattern.as_bytes(), name.as_bytes())
70 }
71}
72
73fn match_refname_pattern_class(class: &[u8], name: Option<u8>) -> Option<(bool, usize)> {
74 let mut idx = 0;
75 let negated = matches!(class.first(), Some(b'!' | b'^'));
76 if negated {
77 idx += 1;
78 }
79
80 let mut matched = false;
81 let mut saw_member = false;
82 while idx < class.len() {
83 if class[idx] == b']' && saw_member {
84 return Some((if negated { !matched } else { matched }, idx + 1));
85 }
86 let start = class[idx];
87 if start == b'\\' && idx + 1 < class.len() {
88 idx += 1;
89 }
90 let start = class[idx];
91 saw_member = true;
92 if idx + 2 < class.len() && class[idx + 1] == b'-' && class[idx + 2] != b']' {
93 let mut end_idx = idx + 2;
94 if class[end_idx] == b'\\' && end_idx + 1 < class.len() {
95 end_idx += 1;
96 }
97 let end = class[end_idx];
98 if let Some(value) = name {
99 matched |= start <= value && value <= end;
100 }
101 idx = end_idx + 1;
102 } else {
103 matched |= name == Some(start);
104 idx += 1;
105 }
106 }
107 None
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub enum RefTarget {
112 Direct(ObjectId),
113 Symbolic(String),
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
117pub struct Ref {
118 pub name: String,
119 pub target: RefTarget,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum RefReadSnapshotValue {
125 Target(RefTarget),
126 Broken,
127}
128
129#[derive(Debug, Clone, Default)]
135pub struct RefReadSnapshot {
136 entries: BTreeMap<String, RefReadSnapshotValue>,
137}
138
139impl RefReadSnapshot {
140 #[must_use]
141 pub fn get(&self, name: &str) -> Option<&RefReadSnapshotValue> {
142 self.entries.get(name)
143 }
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct RefSnapshot {
150 pub refs: Vec<Ref>,
151 pub reflogs: Vec<(String, Vec<ReflogEntry>)>,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct RefDelete {
156 pub name: String,
157 pub oid: ObjectId,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct DeleteRef {
162 pub name: String,
163 pub expected_old: Option<ObjectId>,
164 pub reflog: Option<DeleteRefReflog>,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct DeleteRefReflog {
169 pub committer: Vec<u8>,
170 pub message: Vec<u8>,
171}
172
173#[derive(Debug)]
174pub enum RefDeleteError {
175 NotFound,
176 ExpectedMismatch {
177 expected: Option<ObjectId>,
178 actual: Option<ObjectId>,
179 },
180 Locked,
181 InvalidName,
182 Io(std::io::Error),
183}
184
185impl fmt::Display for RefDeleteError {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 match self {
188 Self::NotFound => f.write_str("ref not found"),
189 Self::ExpectedMismatch { expected, actual } => {
190 write!(
191 f,
192 "ref expected old oid mismatch: expected {:?}, actual {:?}",
193 expected, actual
194 )
195 }
196 Self::Locked => f.write_str("ref is locked"),
197 Self::InvalidName => f.write_str("invalid ref name"),
198 Self::Io(err) => write!(f, "io error: {err}"),
199 }
200 }
201}
202
203impl std::error::Error for RefDeleteError {}
204
205impl From<std::io::Error> for RefDeleteError {
206 fn from(value: std::io::Error) -> Self {
207 Self::Io(value)
208 }
209}
210
211fn parse_leading_oid(format: ObjectFormat, value: &str) -> Option<ObjectId> {
214 let hexsz = format.hex_len();
215 let bytes = value.as_bytes();
216 if bytes.len() < hexsz {
217 return None;
218 }
219 if bytes.len() > hexsz && !bytes[hexsz].is_ascii_whitespace() {
220 return None;
221 }
222 ObjectId::from_hex(format, &value[..hexsz]).ok()
223}
224
225fn loose_ref_bytes_are_reftable_sentinel(name: &str, bytes: &[u8]) -> bool {
226 name == "refs/heads" && bytes == b"this repository uses the reftable format\n"
227}
228
229pub fn parse_loose_ref(format: ObjectFormat, name: impl Into<String>, bytes: &[u8]) -> Result<Ref> {
230 let name = name.into();
231 let value = std::str::from_utf8(bytes)
232 .map_err(|err| GitError::InvalidFormat(err.to_string()))?
233 .trim_end_matches('\n');
234 if name == "FETCH_HEAD" {
235 let oid = value
236 .lines()
237 .find_map(|line| line.split_whitespace().next())
238 .ok_or_else(|| GitError::InvalidFormat("FETCH_HEAD is empty".into()))?;
239 return Ok(Ref {
240 name,
241 target: RefTarget::Direct(ObjectId::from_hex(format, oid)?),
242 });
243 }
244 let target = if let Some(symbolic) = value.strip_prefix("ref: ") {
245 RefTarget::Symbolic(symbolic.to_string())
246 } else {
247 let oid = parse_leading_oid(format, value).ok_or_else(|| {
252 GitError::InvalidFormat(format!(
253 "reference {name} has neither a valid OID nor a target"
254 ))
255 })?;
256 RefTarget::Direct(oid)
257 };
258 Ok(Ref { name, target })
259}
260
261pub fn write_loose_ref(reference: &Ref) -> Vec<u8> {
262 match &reference.target {
263 RefTarget::Direct(oid) => format!("{oid}\n").into_bytes(),
264 RefTarget::Symbolic(target) => format!("ref: {target}\n").into_bytes(),
265 }
266}
267
268#[derive(Debug, Clone, PartialEq, Eq)]
269pub struct PackedRef {
270 pub reference: Ref,
271 pub peeled: Option<ObjectId>,
272}
273
274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum PackRefDecision {
276 Pack { peeled: Option<ObjectId> },
277 Skip,
278}
279
280pub fn parse_packed_refs(format: ObjectFormat, bytes: &[u8]) -> Result<Vec<PackedRef>> {
281 parse_packed_refs_filtered(format, bytes, |_| true)
282}
283
284fn parse_packed_refs_with_prefix(
285 format: ObjectFormat,
286 bytes: &[u8],
287 prefix: &str,
288) -> Result<Vec<PackedRef>> {
289 if !bytes.is_empty() && !bytes.ends_with(b"\n") {
290 let line_start = bytes
291 .iter()
292 .rposition(|byte| *byte == b'\n')
293 .map_or(0, |index| index + 1);
294 let line = String::from_utf8_lossy(&bytes[line_start..]);
295 return Err(GitError::InvalidFormat(format!(
296 "fatal: unterminated line in .git/packed-refs: {line}"
297 )));
298 }
299 let text =
300 std::str::from_utf8(bytes).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
301 let mut refs: Vec<PackedRef> = Vec::new();
302 let mut saw_ref = false;
303 let mut included_last_ref = false;
304 for raw_line in text.lines() {
305 let line = raw_line.trim_end();
306 if line.is_empty() || line.starts_with('#') {
307 continue;
308 }
309 if let Some(peeled) = line.strip_prefix('^') {
310 if !saw_ref {
311 return Err(GitError::InvalidFormat(
312 "peeled packed ref without preceding ref".into(),
313 ));
314 }
315 if included_last_ref {
316 let oid = ObjectId::from_hex(format, peeled)?;
317 if let Some(last) = refs.last_mut() {
318 last.peeled = Some(oid);
319 }
320 }
321 continue;
322 }
323 let (oid, name) = line
324 .split_once(' ')
325 .ok_or_else(|| packed_refs_unexpected_line(line))?;
326 if oid.len() != format.hex_len()
333 || !oid.bytes().all(|byte| byte.is_ascii_hexdigit())
334 || validate_ref_name_for_read(name).is_err()
335 {
336 return Err(packed_refs_unexpected_line(line));
337 }
338 saw_ref = true;
339 included_last_ref = name.starts_with(prefix);
340 if !included_last_ref {
341 continue;
342 }
343 let oid = ObjectId::from_hex(format, oid)?;
344 refs.push(PackedRef {
345 reference: Ref {
346 name: name.into(),
347 target: RefTarget::Direct(oid),
348 },
349 peeled: None,
350 });
351 }
352 Ok(refs)
353}
354
355fn parse_packed_refs_filtered(
356 format: ObjectFormat,
357 bytes: &[u8],
358 mut include: impl FnMut(&str) -> bool,
359) -> Result<Vec<PackedRef>> {
360 if !bytes.is_empty() && !bytes.ends_with(b"\n") {
361 let line_start = bytes
362 .iter()
363 .rposition(|byte| *byte == b'\n')
364 .map_or(0, |index| index + 1);
365 let line = String::from_utf8_lossy(&bytes[line_start..]);
366 return Err(GitError::InvalidFormat(format!(
367 "fatal: unterminated line in .git/packed-refs: {line}"
368 )));
369 }
370 let text =
371 std::str::from_utf8(bytes).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
372 let mut refs: Vec<PackedRef> = Vec::new();
373 let mut saw_ref = false;
374 let mut included_last_ref = false;
375 for raw_line in text.lines() {
376 let line = raw_line.trim_end();
377 if line.is_empty() || line.starts_with('#') {
378 continue;
379 }
380 if let Some(peeled) = line.strip_prefix('^') {
381 let oid = ObjectId::from_hex(format, peeled)?;
382 if !saw_ref {
383 return Err(GitError::InvalidFormat(
384 "peeled packed ref without preceding ref".into(),
385 ));
386 }
387 if included_last_ref && let Some(last) = refs.last_mut() {
388 last.peeled = Some(oid);
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 let oid = ObjectId::from_hex(format, oid)?;
402 saw_ref = true;
403 included_last_ref = include(name);
404 if included_last_ref {
405 refs.push(PackedRef {
406 reference: Ref {
407 name: name.into(),
408 target: RefTarget::Direct(oid),
409 },
410 peeled: None,
411 });
412 }
413 }
414 Ok(refs)
415}
416
417fn packed_ref_names_with_prefix(
418 format: ObjectFormat,
419 bytes: &[u8],
420 prefix: &str,
421 strip_prefix: bool,
422 names: &mut Vec<String>,
423) -> Result<()> {
424 if !bytes.is_empty() && !bytes.ends_with(b"\n") {
425 let line_start = bytes
426 .iter()
427 .rposition(|byte| *byte == b'\n')
428 .map_or(0, |index| index + 1);
429 let line = String::from_utf8_lossy(&bytes[line_start..]);
430 return Err(GitError::InvalidFormat(format!(
431 "fatal: unterminated line in .git/packed-refs: {line}"
432 )));
433 }
434 let text =
435 std::str::from_utf8(bytes).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
436 let mut saw_ref = false;
437 for raw_line in text.lines() {
438 let line = raw_line.trim_end();
439 if line.is_empty() || line.starts_with('#') {
440 continue;
441 }
442 if line.starts_with('^') {
443 if !saw_ref {
444 return Err(GitError::InvalidFormat(
445 "peeled packed ref without preceding ref".into(),
446 ));
447 }
448 continue;
449 }
450 let (oid, name) = line
451 .split_once(' ')
452 .ok_or_else(|| packed_refs_unexpected_line(line))?;
453 if oid.len() != format.hex_len()
457 || !oid.bytes().all(|byte| byte.is_ascii_hexdigit())
458 || validate_ref_name_for_read(name).is_err()
459 {
460 return Err(packed_refs_unexpected_line(line));
461 }
462 saw_ref = true;
463 if !name.starts_with(prefix) {
464 continue;
465 }
466 let name = if strip_prefix {
467 &name[prefix.len()..]
468 } else {
469 name
470 };
471 names.push(name.to_owned());
472 }
473 Ok(())
474}
475
476fn packed_refs_unexpected_line(line: &str) -> GitError {
477 GitError::InvalidFormat(format!(
478 "fatal: unexpected line in .git/packed-refs: {line}"
479 ))
480}
481
482fn packed_refs_have_prefix(format: ObjectFormat, bytes: &[u8], prefix: &str) -> Result<bool> {
483 if !bytes.is_empty() && !bytes.ends_with(b"\n") {
484 let line_start = bytes
485 .iter()
486 .rposition(|byte| *byte == b'\n')
487 .map_or(0, |index| index + 1);
488 let line = String::from_utf8_lossy(&bytes[line_start..]);
489 return Err(GitError::InvalidFormat(format!(
490 "fatal: unterminated line in .git/packed-refs: {line}"
491 )));
492 }
493 let text =
494 std::str::from_utf8(bytes).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
495 let mut found = false;
496 let mut saw_ref = false;
497 for raw_line in text.lines() {
498 let line = raw_line.trim_end();
499 if line.is_empty() || line.starts_with('#') {
500 continue;
501 }
502 if let Some(peeled) = line.strip_prefix('^') {
503 ObjectId::from_hex(format, peeled)?;
504 if !saw_ref {
505 return Err(GitError::InvalidFormat(
506 "peeled packed ref without preceding ref".into(),
507 ));
508 }
509 continue;
510 }
511 let (oid, name) = line
512 .split_once(' ')
513 .ok_or_else(|| packed_refs_unexpected_line(line))?;
514 if oid.len() != format.hex_len()
515 || !oid.bytes().all(|byte| byte.is_ascii_hexdigit())
516 || validate_ref_name_for_read(name).is_err()
517 {
518 return Err(packed_refs_unexpected_line(line));
519 }
520 ObjectId::from_hex(format, oid)?;
521 saw_ref = true;
522 found |= name.starts_with(prefix);
523 }
524 Ok(found)
525}
526
527pub fn write_packed_refs(refs: &[PackedRef]) -> Result<Vec<u8>> {
528 let mut refs = refs.to_vec();
529 refs.sort_by(|left, right| left.reference.name.cmp(&right.reference.name));
530 let mut out = b"# pack-refs with: peeled fully-peeled sorted \n".to_vec();
531 for packed in refs {
532 validate_ref_name(&packed.reference.name)?;
533 let RefTarget::Direct(oid) = &packed.reference.target else {
534 return Err(GitError::InvalidFormat(format!(
535 "packed ref {} is symbolic",
536 packed.reference.name
537 )));
538 };
539 out.extend_from_slice(oid.to_hex().as_bytes());
540 out.push(b' ');
541 out.extend_from_slice(packed.reference.name.as_bytes());
542 out.push(b'\n');
543 if let Some(peeled) = packed.peeled {
544 out.push(b'^');
545 out.extend_from_slice(peeled.to_hex().as_bytes());
546 out.push(b'\n');
547 }
548 }
549 Ok(out)
550}
551
552#[derive(Debug, Clone, PartialEq, Eq)]
553pub struct ReflogEntry {
554 pub old_oid: ObjectId,
555 pub new_oid: ObjectId,
556 pub committer: Vec<u8>,
557 pub message: Vec<u8>,
558}
559
560impl ReflogEntry {
561 pub fn to_line(&self) -> Vec<u8> {
562 let mut out = Vec::new();
563 out.extend_from_slice(self.old_oid.to_hex().as_bytes());
564 out.push(b' ');
565 out.extend_from_slice(self.new_oid.to_hex().as_bytes());
566 out.push(b' ');
567 out.extend_from_slice(&self.committer);
568 if !self.message.is_empty() {
569 out.push(b'\t');
570 out.extend_from_slice(&self.message);
571 }
572 out.push(b'\n');
573 out
574 }
575
576 pub fn timestamp_seconds(&self) -> Result<i64> {
577 let committer = std::str::from_utf8(&self.committer)
578 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
579 let Some((before_tz, _tz)) = committer.rsplit_once(' ') else {
580 return Err(GitError::InvalidFormat(
581 "reflog committer is missing timezone".into(),
582 ));
583 };
584 let Some((_identity, timestamp)) = before_tz.rsplit_once(' ') else {
585 return Err(GitError::InvalidFormat(
586 "reflog committer is missing timestamp".into(),
587 ));
588 };
589 timestamp
590 .parse::<i64>()
591 .map_err(|err| GitError::InvalidFormat(err.to_string()))
592 }
593}
594
595pub fn parse_reflog(format: ObjectFormat, bytes: &[u8]) -> Result<Vec<ReflogEntry>> {
596 let text =
597 std::str::from_utf8(bytes).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
598 let mut entries = Vec::new();
599 for line in text.lines() {
600 let mut parts = line.splitn(3, ' ');
601 let old = parts
602 .next()
603 .ok_or_else(|| GitError::InvalidFormat("missing reflog old oid".into()))?;
604 let new = parts
605 .next()
606 .ok_or_else(|| GitError::InvalidFormat("missing reflog new oid".into()))?;
607 let rest = parts
608 .next()
609 .ok_or_else(|| GitError::InvalidFormat("missing reflog committer".into()))?;
610 let (committer, message) = rest.split_once('\t').unwrap_or((rest, ""));
611 entries.push(ReflogEntry {
612 old_oid: ObjectId::from_hex(format, old)?,
613 new_oid: ObjectId::from_hex(format, new)?,
614 committer: committer.as_bytes().to_vec(),
615 message: message.as_bytes().to_vec(),
616 });
617 }
618 Ok(entries)
619}
620
621pub fn expire_reflog(
639 entries: &[ReflogEntry],
640 cutoff_unix: i64,
641 expire_unreachable_cutoff: Option<i64>,
642 is_reachable: impl Fn(&ObjectId) -> bool,
643) -> Result<Vec<ReflogEntry>> {
644 let last_index = entries.len().checked_sub(1);
645 let mut retained = Vec::with_capacity(entries.len());
646 for (index, entry) in entries.iter().enumerate() {
647 if Some(index) == last_index {
650 retained.push(entry.clone());
651 continue;
652 }
653 let timestamp = entry.timestamp_seconds()?;
654 let mut expired = timestamp < cutoff_unix;
655 if let Some(unreachable_cutoff) = expire_unreachable_cutoff
656 && !is_reachable(&entry.new_oid)
657 {
658 expired = expired || timestamp < unreachable_cutoff;
659 }
660 if !expired {
661 retained.push(entry.clone());
662 }
663 }
664 Ok(retained)
665}
666
667#[derive(Debug, Default, Clone)]
668pub struct RefStore {
669 refs: HashMap<String, RefTarget>,
670 reflogs: BTreeMap<String, Vec<ReflogEntry>>,
671}
672
673impl RefStore {
674 pub fn new() -> Self {
675 Self::default()
676 }
677
678 pub fn get(&self, name: &str) -> Option<&RefTarget> {
679 self.refs.get(name)
680 }
681
682 pub fn transaction(&mut self) -> RefTransaction<'_> {
683 RefTransaction {
684 store: self,
685 updates: Vec::new(),
686 }
687 }
688
689 pub fn reflog(&self, name: &str) -> &[ReflogEntry] {
690 self.reflogs
691 .get(name)
692 .map(Vec::as_slice)
693 .unwrap_or_default()
694 }
695}
696
697#[derive(Debug)]
698pub struct RefUpdate {
699 pub name: String,
700 pub expected: Option<RefTarget>,
701 pub new: RefTarget,
702 pub reflog: Option<ReflogEntry>,
703}
704
705#[derive(Debug, Clone, PartialEq, Eq)]
713pub enum RefPrecondition {
714 Any,
716 MustExist,
718 MustNotExist,
720 MustExistAndMatch(RefTarget),
722 ExistingMustMatch(RefTarget),
725}
726
727impl RefPrecondition {
728 fn from_expected(expected: Option<RefTarget>) -> Self {
730 match expected {
731 None => Self::Any,
732 Some(target) => Self::MustExistAndMatch(target),
733 }
734 }
735
736 fn is_satisfied_by(&self, current: Option<&RefTarget>) -> bool {
739 match self {
740 Self::Any => true,
741 Self::MustExist => current.is_some(),
742 Self::MustNotExist => current.is_none(),
743 Self::MustExistAndMatch(target) => current == Some(target),
744 Self::ExistingMustMatch(target) => match current {
745 None => true,
746 Some(current) => current == target,
747 },
748 }
749 }
750
751 fn describe(&self, name: &str) -> String {
753 match self {
754 Self::Any => format!("ref {name} precondition not met"),
755 Self::MustExist => format!("expected ref {name} to exist"),
756 Self::MustNotExist => format!("expected ref {name} to not already exist"),
757 Self::MustExistAndMatch(_) => format!("expected ref {name} to match"),
758 Self::ExistingMustMatch(_) => {
759 format!("expected ref {name} to match its current value")
760 }
761 }
762 }
763}
764
765pub struct RefTransaction<'a> {
766 store: &'a mut RefStore,
767 updates: Vec<RefUpdate>,
768}
769
770impl<'a> RefTransaction<'a> {
771 pub fn update(&mut self, update: RefUpdate) {
772 self.updates.push(update);
773 }
774
775 pub fn commit(self) -> Result<()> {
776 for update in &self.updates {
777 if let Some(expected) = &update.expected
778 && self.store.refs.get(&update.name) != Some(expected)
779 {
780 return Err(GitError::Transaction(format!(
781 "expected ref {} to match",
782 update.name
783 )));
784 }
785 }
786 for update in self.updates {
787 self.store.refs.insert(update.name.clone(), update.new);
788 if let Some(entry) = update.reflog {
789 self.store
790 .reflogs
791 .entry(update.name)
792 .or_default()
793 .push(entry);
794 }
795 }
796 Ok(())
797 }
798}
799
800#[derive(Debug, Clone)]
801pub struct FileRefStore {
802 git_dir: PathBuf,
803 common_dir: PathBuf,
804 storage_dir: PathBuf,
805 format: ObjectFormat,
806 ref_storage: RefStorageConfiguration,
807 packed_refs_lock_timeout_millis: u64,
808 reftable_lock_timeout_millis: Option<u64>,
809 reftable_write_options: ReftableWriteOptions,
810 combine_reftable_logs: bool,
811 prefer_symlink_refs: bool,
812 reference_fsync: ReferenceFsyncPolicy,
813 shared_repository: sley_formats::SharedRepositoryPermissions,
814}
815
816#[derive(Debug, Clone)]
817struct RefStorageConfiguration {
818 backend: Option<(RefBackendKind, Option<PathBuf>)>,
819 invalid_value: Option<String>,
820}
821
822#[derive(Debug, Clone, Copy, PartialEq, Eq)]
823enum ReferenceFsyncMethod {
824 Fsync,
825 WriteoutOnly,
826 Batch,
827}
828
829impl ReferenceFsyncMethod {
830 const fn platform_default() -> Self {
831 #[cfg(target_os = "windows")]
832 {
833 Self::Batch
834 }
835 #[cfg(all(not(target_os = "windows"), target_os = "macos"))]
836 {
837 Self::WriteoutOnly
838 }
839 #[cfg(not(any(target_os = "windows", target_os = "macos")))]
840 {
841 Self::Fsync
842 }
843 }
844
845 fn from_config(value: Option<&str>) -> Self {
846 match value {
847 Some("fsync") => Self::Fsync,
848 Some("writeout-only") => Self::WriteoutOnly,
849 Some("batch") => Self::Batch,
850 _ => Self::platform_default(),
851 }
852 }
853}
854
855#[derive(Debug, Clone, Copy, PartialEq, Eq)]
856struct ReferenceFsyncPolicy {
857 enabled: bool,
858 method: ReferenceFsyncMethod,
859}
860
861impl ReferenceFsyncPolicy {
862 fn from_config(core_fsync: Option<&str>, core_fsync_method: Option<&str>) -> Self {
863 let enabled =
864 core_fsync.is_some_and(core_fsync_includes_reference) && git_test_fsync_enabled();
865 Self {
866 enabled,
867 method: ReferenceFsyncMethod::from_config(core_fsync_method),
868 }
869 }
870
871 fn method_if_enabled(self) -> Option<ReferenceFsyncMethod> {
872 self.enabled.then_some(self.method)
873 }
874}
875
876#[derive(Debug, Clone, PartialEq, Eq)]
877pub struct BranchCreate {
878 pub name: String,
879 pub oid: ObjectId,
880}
881
882#[derive(Debug, Clone, PartialEq, Eq)]
883pub struct BranchDelete {
884 pub name: String,
885 pub oid: ObjectId,
886}
887
888#[derive(Debug, Clone, PartialEq, Eq)]
889pub struct TagCreate {
890 pub name: String,
891 pub oid: ObjectId,
892}
893
894#[derive(Debug, Clone, PartialEq, Eq)]
895pub struct TagDelete {
896 pub name: String,
897 pub oid: ObjectId,
898}
899
900#[derive(Debug, Clone, PartialEq, Eq)]
901pub struct BundleRefUpdate {
902 pub name: String,
903 pub oid: ObjectId,
904}
905
906#[derive(Debug, Clone, PartialEq, Eq)]
907pub struct BundleRefUpdateReflog {
908 pub committer: Vec<u8>,
909 pub message: Vec<u8>,
910}
911
912#[derive(Debug, Clone, PartialEq, Eq)]
913pub struct AppliedBundleRefUpdate {
914 pub name: String,
915 pub old_oid: Option<ObjectId>,
916 pub new_oid: ObjectId,
917}
918
919fn configured_ref_storage_backend(common_dir: &Path, honor_env: bool) -> RefStorageConfiguration {
920 if honor_env && let Ok(value) = env::var("GIT_REFERENCE_BACKEND") {
921 return match parse_ref_storage_backend_value(&value) {
922 Ok((kind, path)) => RefStorageConfiguration {
923 backend: Some((
924 kind,
925 path.map(|path| ref_storage_path_from_config(common_dir, path)),
926 )),
927 invalid_value: None,
928 },
929 Err(_) => RefStorageConfiguration {
930 backend: None,
931 invalid_value: Some(value),
932 },
933 };
934 }
935 let value = GitConfig::read(common_dir.join("config"))
936 .ok()
937 .and_then(|config| {
938 config
939 .get("extensions", None, "refStorage")
940 .map(str::to_string)
941 });
942 let Some(value) = value else {
943 return RefStorageConfiguration {
944 backend: None,
945 invalid_value: None,
946 };
947 };
948 match parse_ref_storage_backend_value(&value) {
949 Ok((kind, path)) => RefStorageConfiguration {
950 backend: Some((
951 kind,
952 path.map(|path| ref_storage_path_from_config(common_dir, path)),
953 )),
954 invalid_value: None,
955 },
956 Err(_) => RefStorageConfiguration {
957 backend: None,
958 invalid_value: Some(value),
959 },
960 }
961}
962
963fn configured_reference_fsync(common_dir: &Path) -> ReferenceFsyncPolicy {
964 let context = sley_config::ConfigIncludeContext::new(
965 Some(sley_config::git_dir_for_include_context(common_dir)),
966 sley_config::repo_current_branch_name(common_dir),
967 );
968 let config = sley_config::load_effective_config(common_dir, &context).ok();
969 ReferenceFsyncPolicy::from_config(
970 config
971 .as_ref()
972 .and_then(|config| config.get("core", None, "fsync")),
973 config
974 .as_ref()
975 .and_then(|config| config.get("core", None, "fsyncMethod")),
976 )
977}
978
979fn core_fsync_includes_reference(value: &str) -> bool {
980 const COMPONENTS: [(&str, bool); 11] = [
981 ("loose-object", false),
982 ("pack", false),
983 ("pack-metadata", false),
984 ("commit-graph", false),
985 ("index", false),
986 ("objects", false),
987 ("reference", true),
988 ("derived-metadata", false),
989 ("committed", true),
990 ("added", true),
991 ("all", true),
992 ];
993 let mut positive = false;
994 let mut negative = false;
995 for raw_component in value.split(',') {
996 let component = raw_component.trim();
997 if component == "none" || component.is_empty() {
998 continue;
999 }
1000 let (negated, component) = component
1001 .strip_prefix('-')
1002 .map_or((false, component), |component| (true, component));
1003 if component.is_empty() {
1004 continue;
1005 }
1006 let matches_reference = COMPONENTS
1007 .iter()
1008 .any(|(name, includes_reference)| *includes_reference && name.starts_with(component));
1009 if matches_reference {
1010 if negated {
1011 negative = true;
1012 } else {
1013 positive = true;
1014 }
1015 }
1016 }
1017 (platform_default_includes_reference_fsync() && !negative) || positive
1021}
1022
1023fn platform_default_includes_reference_fsync() -> bool {
1024 false
1028}
1029
1030fn git_test_fsync_enabled() -> bool {
1031 let Ok(value) = env::var("GIT_TEST_FSYNC") else {
1032 return true;
1033 };
1034 !matches!(
1035 value.to_ascii_lowercase().as_str(),
1036 "0" | "false" | "no" | "off" | ""
1037 )
1038}
1039
1040fn ref_storage_path_from_config(common_dir: &Path, path: PathBuf) -> PathBuf {
1041 if path.is_absolute() {
1042 path
1043 } else {
1044 common_dir.join(path)
1045 }
1046}
1047
1048fn parse_ref_storage_backend_value(value: &str) -> Result<(RefBackendKind, Option<PathBuf>)> {
1049 let (backend, path) = if let Some((backend, path)) = value.split_once("://") {
1050 if path.is_empty() {
1051 return Err(GitError::InvalidFormat(format!(
1052 "invalid value for 'extensions.refstorage': '{value}'"
1053 )));
1054 }
1055 (backend, Some(PathBuf::from(path)))
1056 } else {
1057 (value, None)
1058 };
1059 let kind = match backend {
1060 "files" => RefBackendKind::Files,
1061 "reftable" => RefBackendKind::Reftable,
1062 _ => {
1063 return Err(GitError::InvalidFormat(format!(
1064 "invalid value for 'extensions.refstorage': '{value}'"
1065 )));
1066 }
1067 };
1068 Ok((kind, path))
1069}
1070
1071#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1072enum RefBackendKind {
1073 Files,
1074 Reftable,
1075}
1076
1077impl FileRefStore {
1078 pub fn new(git_dir: impl Into<PathBuf>, format: ObjectFormat) -> Self {
1079 Self::new_with_reference_backend_env(git_dir, format, true)
1080 }
1081
1082 pub fn new_without_reference_backend_env(
1083 git_dir: impl Into<PathBuf>,
1084 format: ObjectFormat,
1085 ) -> Self {
1086 Self::new_with_reference_backend_env(git_dir, format, false)
1087 }
1088
1089 #[must_use]
1091 pub fn git_dir(&self) -> &Path {
1092 &self.git_dir
1093 }
1094
1095 #[must_use]
1097 pub const fn object_format(&self) -> ObjectFormat {
1098 self.format
1099 }
1100
1101 fn new_with_reference_backend_env(
1102 git_dir: impl Into<PathBuf>,
1103 format: ObjectFormat,
1104 honor_reference_backend_env: bool,
1105 ) -> Self {
1106 let git_dir = git_dir.into();
1107 let common_dir = repository_common_dir(&git_dir);
1108 let configured = configured_ref_storage_backend(&common_dir, honor_reference_backend_env);
1109 let reference_fsync = configured_reference_fsync(&common_dir);
1110 let shared_repository =
1111 sley_formats::SharedRepositoryPermissions::from_git_dir(&common_dir);
1112 let prefer_symlink_refs = GitConfig::read(common_dir.join("config"))
1113 .ok()
1114 .and_then(|config| config.get_bool("core", None, "preferSymlinkRefs"))
1115 .unwrap_or(false);
1116 let storage_dir = match configured.backend.as_ref() {
1117 Some((_, Some(path))) => path.clone(),
1118 Some((RefBackendKind::Reftable, None)) if git_dir != common_dir => git_dir.clone(),
1119 _ => common_dir.clone(),
1120 };
1121 Self {
1122 git_dir,
1123 common_dir,
1124 storage_dir,
1125 format,
1126 ref_storage: configured,
1127 packed_refs_lock_timeout_millis: 1_000,
1128 reftable_lock_timeout_millis: None,
1129 reftable_write_options: ReftableWriteOptions::default(),
1130 combine_reftable_logs: false,
1131 prefer_symlink_refs,
1132 reference_fsync,
1133 shared_repository,
1134 }
1135 }
1136
1137 pub fn with_reftable_lock_timeout_millis(mut self, timeout_millis: Option<u64>) -> Self {
1138 self.reftable_lock_timeout_millis = timeout_millis;
1139 self
1140 }
1141
1142 #[must_use]
1149 pub fn with_packed_refs_lock_timeout_millis(mut self, timeout_millis: u64) -> Self {
1150 self.packed_refs_lock_timeout_millis = timeout_millis;
1151 self
1152 }
1153
1154 pub fn with_reftable_write_options(mut self, options: ReftableWriteOptions) -> Self {
1157 self.reftable_write_options = options;
1158 self
1159 }
1160
1161 pub fn with_reftable_combined_logs(mut self, combine: bool) -> Self {
1162 self.combine_reftable_logs = combine;
1163 self
1164 }
1165
1166 #[must_use]
1169 pub const fn prefers_symlink_refs(&self) -> bool {
1170 self.prefer_symlink_refs
1171 }
1172
1173 #[must_use]
1176 pub fn with_prefer_symlink_refs(mut self, prefer: bool) -> Self {
1177 self.prefer_symlink_refs = prefer;
1178 self
1179 }
1180
1181 pub fn with_reference_fsync_config(
1187 mut self,
1188 core_fsync: Option<&str>,
1189 core_fsync_method: Option<&str>,
1190 ) -> Self {
1191 if core_fsync.is_some() || core_fsync_method.is_some() {
1192 let enabled = core_fsync
1193 .map(core_fsync_includes_reference)
1194 .unwrap_or(self.reference_fsync.enabled)
1195 && git_test_fsync_enabled();
1196 let method = core_fsync_method.map_or(self.reference_fsync.method, |value| {
1197 ReferenceFsyncMethod::from_config(Some(value))
1198 });
1199 self.reference_fsync = ReferenceFsyncPolicy { enabled, method };
1200 }
1201 self
1202 }
1203
1204 pub fn read_ref(&self, name: &str) -> Result<Option<RefTarget>> {
1205 validate_ref_name_for_read(name)?;
1206 self.read_ref_unchecked(name)
1207 }
1208
1209 fn read_ref_unchecked(&self, name: &str) -> Result<Option<RefTarget>> {
1210 if self.uses_reftable()? {
1211 let (store, name) = self.reftable_store_for_ref(name)?;
1212 if let Some(target) = store.read_reftable_ref(&name)? {
1213 return Ok(Some(target));
1214 }
1215 if name != "HEAD" && is_root_ref_syntax(&name) {
1216 return Ok(self
1217 .read_loose_ref(&name)?
1218 .map(|reference| reference.target));
1219 }
1220 return Ok(None);
1221 }
1222 if let Some(reference) = self.read_loose_ref(name)? {
1223 return Ok(Some(reference.target));
1224 }
1225 if let Some(reference) = self.read_packed_ref(name)? {
1226 return Ok(Some(reference.reference.target));
1227 }
1228 Ok(None)
1229 }
1230
1231 pub fn raw_ref_exists(&self, name: &str) -> Result<bool> {
1241 if self.uses_reftable()? {
1242 let (store, name) = self.reftable_store_for_ref(name)?;
1243 if store.read_reftable_ref(&name)?.is_some() {
1244 return Ok(true);
1245 }
1246 if name != "HEAD" && is_root_ref_syntax(&name) {
1247 return Ok(self.read_loose_ref(&name)?.is_some());
1248 }
1249 return Ok(false);
1250 }
1251 let base = if current_worktree_ref(name) {
1255 self.current_worktree_storage_dir()
1256 } else {
1257 self.storage_dir.clone()
1258 };
1259 let path = base.join(name);
1260 match fs::symlink_metadata(&path) {
1261 Ok(meta) if meta.is_dir() => {
1262 Ok(self.read_packed_ref(name)?.is_some())
1265 }
1266 Ok(_) => Ok(true),
1267 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
1268 Ok(self.read_packed_ref(name)?.is_some())
1269 }
1270 Err(err) => Err(err.into()),
1271 }
1272 }
1273
1274 pub fn read_reflog(&self, name: &str) -> Result<Vec<ReflogEntry>> {
1275 validate_ref_name_for_read(name)?;
1276 if self.uses_reftable()? {
1277 return self.read_reftable_logs(name);
1278 }
1279 let path = self.reflog_path(name);
1280 if !path.exists() {
1281 return Ok(Vec::new());
1282 }
1283 parse_reflog(self.format, &fs::read(path)?)
1284 }
1285
1286 pub fn read_reflog_for_expiry(&self, name: &str) -> Result<Vec<ReflogEntry>> {
1292 validate_ref_name_for_read(name)?;
1293 if self.uses_reftable()? {
1294 return self.read_reftable_logs(name);
1295 }
1296 let path = self.reflog_path(name);
1297 let bytes = match fs::read(path) {
1298 Ok(bytes) => bytes,
1299 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
1300 Err(err) => return Err(err.into()),
1301 };
1302 let mut entries = Vec::new();
1303 for line in bytes.split_inclusive(|byte| *byte == b'\n') {
1304 if let Ok(mut parsed) = parse_reflog(self.format, line) {
1305 entries.append(&mut parsed);
1306 }
1307 }
1308 Ok(entries)
1309 }
1310
1311 pub fn adjust_reflog_permissions(&self, name: &str) -> Result<()> {
1314 validate_ref_name_for_read(name)?;
1315 if self.uses_reftable()? {
1316 return Ok(());
1317 }
1318 let path = self.reflog_path(name);
1319 if path.exists() {
1320 self.shared_repository.adjust_file(&path)?;
1321 }
1322 Ok(())
1323 }
1324
1325 pub fn reflog_exists(&self, name: &str) -> Result<bool> {
1326 validate_ref_name_for_read(name)?;
1327 if self.uses_reftable()? {
1328 return self.reftable_log_exists(name);
1329 }
1330 Ok(self.reflog_path(name).exists())
1331 }
1332
1333 pub fn should_write_reflog_for_update(&self, name: &str, create_reflog: bool) -> Result<bool> {
1334 validate_ref_name_for_read(name)?;
1335 let reflog_exists = if self.uses_reftable()? {
1336 !self.read_reftable_logs(name)?.is_empty()
1337 } else {
1338 self.reflog_path(name).exists()
1339 };
1340 if create_reflog || reflog_exists {
1341 return Ok(true);
1342 }
1343 let context = sley_config::ConfigIncludeContext::new(
1344 Some(sley_config::git_dir_for_include_context(&self.common_dir)),
1345 sley_config::repo_current_branch_name(&self.common_dir),
1346 );
1347 let Ok(config) = sley_config::load_effective_config(&self.common_dir, &context) else {
1348 return Ok(false);
1349 };
1350 if let Some(value) = config.get("core", None, "logAllRefUpdates") {
1351 return Ok(log_all_ref_updates_matches(name, value));
1352 }
1353 if config.get_bool("core", None, "bare").unwrap_or(false) {
1354 return Ok(false);
1355 }
1356 Ok(log_all_ref_updates_matches(name, "true"))
1357 }
1358
1359 pub fn write_reflog(&self, name: &str, entries: &[ReflogEntry]) -> Result<()> {
1360 validate_ref_name_for_read(name)?;
1361 if self.uses_reftable()? {
1362 return self.rewrite_reftable_logs(name, entries, true);
1363 }
1364 let path = self.reflog_path(name);
1365 let parent = path
1366 .parent()
1367 .ok_or_else(|| GitError::InvalidPath("reflog path has no parent".into()))?;
1368 self.shared_repository.create_dir_all(parent)?;
1369 let mut bytes = Vec::new();
1370 for entry in entries {
1371 bytes.extend_from_slice(&entry.to_line());
1372 }
1373 write_locked(&path, &bytes, self.reference_fsync.method_if_enabled())?;
1374 self.shared_repository.adjust_file(&path)
1375 }
1376
1377 pub fn export_snapshot(&self, include_reflogs: bool) -> Result<RefSnapshot> {
1385 if self.uses_reftable()? {
1386 let mut refs = BTreeMap::<String, Ref>::new();
1387 let mut logs = BTreeMap::<(String, u64), Option<ReftableLogUpdate>>::new();
1388 for table in self.reftables()? {
1389 for record in table.refs {
1390 let is_migratable_root = !record.name.starts_with("refs/")
1391 && is_root_ref_syntax(&record.name)
1392 && !matches!(record.name.as_str(), "FETCH_HEAD" | "MERGE_HEAD");
1393 if !record.name.starts_with("refs/") && !is_migratable_root {
1394 continue;
1395 }
1396 match reftable_ref_target(record.value)? {
1397 Some(target) => {
1398 refs.insert(
1399 record.name.clone(),
1400 Ref {
1401 name: record.name,
1402 target,
1403 },
1404 );
1405 }
1406 None => {
1407 refs.remove(&record.name);
1408 }
1409 }
1410 }
1411 if include_reflogs {
1412 for record in table.logs {
1413 let value = match record.value {
1414 ReftableLogValue::Deletion => None,
1415 ReftableLogValue::Update(update) => Some(update),
1416 };
1417 logs.insert((record.refname, record.update_index), value);
1418 }
1419 }
1420 }
1421
1422 let mut reflogs = BTreeMap::<String, Vec<ReflogEntry>>::new();
1423 if include_reflogs {
1424 let null = ObjectId::null(self.format);
1425 for ((name, _), update) in logs {
1426 let Some(update) = update else {
1427 continue;
1428 };
1429 if !reftable_log_update_is_empty_marker(&update, null) {
1430 reflogs
1431 .entry(name)
1432 .or_default()
1433 .push(reflog_entry_from_reftable(update));
1434 }
1435 }
1436 }
1437 return Ok(RefSnapshot {
1438 refs: refs.into_values().collect(),
1439 reflogs: reflogs.into_iter().collect(),
1440 });
1441 }
1442
1443 let refs = self.list_all_refs()?;
1444 if !include_reflogs {
1445 return Ok(RefSnapshot {
1446 refs,
1447 reflogs: Vec::new(),
1448 });
1449 }
1450 let names = self.list_reflog_names()?;
1456 let mut reflogs = Vec::new();
1457 for name in names {
1458 let entries = self.read_reflog(&name)?;
1459 if !entries.is_empty() {
1460 reflogs.push((name, entries));
1461 }
1462 }
1463 Ok(RefSnapshot { refs, reflogs })
1464 }
1465
1466 pub fn import_snapshot(
1467 &self,
1468 refs: &[Ref],
1469 reflogs: &[(String, Vec<ReflogEntry>)],
1470 pack_files_refs: bool,
1471 ) -> Result<()> {
1472 if self.uses_reftable()? {
1473 let ref_records = refs
1474 .iter()
1475 .map(|reference| ReftableRefRecord {
1476 name: reference.name.clone(),
1477 update_index: 0,
1478 value: reftable_value_from_ref_target(&reference.target),
1479 })
1480 .collect::<Vec<_>>();
1481 let mut log_records = Vec::new();
1482 let mut update_index = 1u64;
1483 for (name, entries) in reflogs {
1484 for entry in entries {
1485 log_records.push(ReftableLogRecord {
1486 refname: name.clone(),
1487 update_index,
1488 value: ReftableLogValue::Update(self.reftable_update_from_reflog(entry)?),
1489 });
1490 update_index = update_index.checked_add(1).ok_or_else(|| {
1491 GitError::InvalidFormat("reftable update index overflow".into())
1492 })?;
1493 }
1494 }
1495 if !ref_records.is_empty() || !log_records.is_empty() {
1496 self.append_reftable_table_spanning(ref_records, log_records)?;
1497 }
1498 return Ok(());
1499 }
1500
1501 if pack_files_refs {
1502 let mut packed = Vec::new();
1503 let mut tx = self.transaction();
1504 for reference in refs {
1505 if packable_loose_ref_name(&reference.name)
1506 && matches!(&reference.target, RefTarget::Direct(_))
1507 {
1508 packed.push(PackedRef {
1509 reference: reference.clone(),
1510 peeled: None,
1511 });
1512 } else {
1513 tx.update(RefUpdate {
1514 name: reference.name.clone(),
1515 expected: None,
1516 new: reference.target.clone(),
1517 reflog: None,
1518 });
1519 }
1520 }
1521 self.write_packed_refs(&packed)?;
1526 tx.commit()?;
1527 } else {
1528 let mut tx = self.transaction();
1529 for reference in refs {
1530 tx.update(RefUpdate {
1531 name: reference.name.clone(),
1532 expected: None,
1533 new: reference.target.clone(),
1534 reflog: None,
1535 });
1536 }
1537 tx.commit()?;
1538 }
1539 for (name, entries) in reflogs {
1540 self.write_reflog(name, entries)?;
1541 }
1542 Ok(())
1543 }
1544
1545 pub fn expire_reflog_older_than(&self, name: &str, cutoff_seconds: i64) -> Result<usize> {
1546 validate_ref_name_for_read(name)?;
1547 if self.uses_reftable()? {
1548 let entries = self.read_reftable_logs(name)?;
1549 let original_len = entries.len();
1550 let mut retained = Vec::new();
1551 for entry in entries {
1552 if entry.timestamp_seconds()? >= cutoff_seconds {
1553 retained.push(entry);
1554 }
1555 }
1556 let removed = original_len - retained.len();
1557 if removed > 0 {
1558 self.rewrite_reftable_logs(name, &retained, true)?;
1559 }
1560 return Ok(removed);
1561 }
1562 let path = self.reflog_path(name);
1563 if !path.exists() {
1564 return Ok(0);
1565 }
1566 let entries = parse_reflog(self.format, &fs::read(&path)?)?;
1567 let original_len = entries.len();
1568 let mut retained = Vec::new();
1569 for entry in entries {
1570 if entry.timestamp_seconds()? >= cutoff_seconds {
1571 retained.push(entry);
1572 }
1573 }
1574 let mut bytes = Vec::new();
1575 for entry in &retained {
1576 bytes.extend_from_slice(&entry.to_line());
1577 }
1578 write_locked(&path, &bytes, self.reference_fsync.method_if_enabled())?;
1579 Ok(original_len - retained.len())
1580 }
1581
1582 pub fn expire_reflog_file(
1593 &self,
1594 name: &str,
1595 cutoff_unix: i64,
1596 expire_unreachable_cutoff: Option<i64>,
1597 write: bool,
1598 is_reachable: impl Fn(&ObjectId) -> bool,
1599 ) -> Result<usize> {
1600 validate_ref_name(name)?;
1601 if self.uses_reftable()? {
1602 let entries = self.read_reftable_logs(name)?;
1603 let original_len = entries.len();
1604 let retained = expire_reflog(
1605 &entries,
1606 cutoff_unix,
1607 expire_unreachable_cutoff,
1608 is_reachable,
1609 )?;
1610 let removed = original_len - retained.len();
1611 if write && removed > 0 {
1612 self.rewrite_reftable_logs(name, &retained, true)?;
1613 }
1614 return Ok(removed);
1615 }
1616 let path = self.reflog_path(name);
1617 if !path.exists() {
1618 return Ok(0);
1619 }
1620 let entries = parse_reflog(self.format, &fs::read(&path)?)?;
1621 let original_len = entries.len();
1622 let retained = expire_reflog(
1623 &entries,
1624 cutoff_unix,
1625 expire_unreachable_cutoff,
1626 is_reachable,
1627 )?;
1628 let removed = original_len - retained.len();
1629 if write && removed > 0 {
1630 let mut bytes = Vec::new();
1631 for entry in &retained {
1632 bytes.extend_from_slice(&entry.to_line());
1633 }
1634 write_locked(&path, &bytes, self.reference_fsync.method_if_enabled())?;
1635 }
1636 Ok(removed)
1637 }
1638
1639 pub fn list_refs(&self) -> Result<Vec<Ref>> {
1640 self.list_refs_with_prefix("refs/")
1641 }
1642
1643 pub fn read_snapshot_for_update(&self) -> Result<RefReadSnapshot> {
1649 if self.uses_reftable()? {
1650 let entries = self
1651 .list_refs()?
1652 .into_iter()
1653 .map(|reference| {
1654 (
1655 reference.name,
1656 RefReadSnapshotValue::Target(reference.target),
1657 )
1658 })
1659 .collect();
1660 return Ok(RefReadSnapshot { entries });
1661 }
1662
1663 let mut entries = BTreeMap::new();
1664 let packed_path = self.storage_dir.join("packed-refs");
1665 if packed_path.exists() {
1666 for packed in parse_packed_refs(self.format, &fs::read(packed_path)?)? {
1667 entries.insert(
1668 packed.reference.name,
1669 RefReadSnapshotValue::Target(packed.reference.target),
1670 );
1671 }
1672 }
1673 self.collect_loose_ref_snapshot(&self.storage_dir.join("refs"), "refs", &mut entries)?;
1674 let worktree_base = self.current_worktree_storage_dir();
1675 if worktree_base != self.storage_dir {
1676 for namespace in CURRENT_WORKTREE_REF_PREFIXES {
1677 let prefix = namespace.trim_end_matches('/');
1678 self.collect_loose_ref_snapshot(&worktree_base.join(prefix), prefix, &mut entries)?;
1679 }
1680 }
1681 Ok(RefReadSnapshot { entries })
1682 }
1683
1684 pub fn list_refs_with_prefix(&self, prefix: &str) -> Result<Vec<Ref>> {
1685 if self.uses_reftable()? {
1686 let mut refs = BTreeMap::<String, Ref>::new();
1687 for reference in self
1688 .reftable_store_with_storage(self.shared_reftable_storage_dir())
1689 .list_reftable_refs_with_prefix(prefix)?
1690 {
1691 refs.insert(reference.name.clone(), reference);
1692 }
1693 if self.git_dir != self.common_dir {
1694 for reference in self.list_reftable_refs_with_prefix(prefix)? {
1695 if reftable_current_worktree_ref(&reference.name) {
1696 refs.insert(reference.name.clone(), reference);
1697 }
1698 }
1699 }
1700 return Ok(refs.into_values().collect());
1701 }
1702 let mut refs = Vec::new();
1703 let packed_path = self.storage_dir.join("packed-refs");
1704 if packed_path.exists() {
1705 for packed in
1706 parse_packed_refs_with_prefix(self.format, &fs::read(packed_path)?, prefix)?
1707 {
1708 refs.push(packed.reference);
1709 }
1710 }
1711 let mut loose_refs = BTreeMap::new();
1712 self.collect_loose_refs_with_prefix(prefix, &mut loose_refs)?;
1713 self.collect_current_worktree_loose_refs_with_prefix(prefix, &mut loose_refs)?;
1714 if !loose_refs.is_empty() {
1715 refs.retain(|reference| !loose_refs.contains_key(&reference.name));
1716 refs.extend(loose_refs.into_values());
1717 }
1718 refs.retain(|reference| reference.name.starts_with(prefix));
1719 refs.retain(|reference| {
1720 if validate_ref_name(&reference.name).is_ok() {
1721 true
1722 } else {
1723 warn_broken_ref_name(&reference.name);
1724 false
1725 }
1726 });
1727 refs.sort_by(|left, right| left.name.cmp(&right.name));
1728 Ok(refs)
1729 }
1730
1731 pub fn list_ref_names_with_prefix(&self, prefix: &str) -> Result<Vec<String>> {
1732 if self.uses_reftable()? {
1733 return Ok(self
1734 .list_refs_with_prefix(prefix)?
1735 .into_iter()
1736 .map(|reference| reference.name)
1737 .collect());
1738 }
1739 let mut names = Vec::new();
1740 let packed_path = self.storage_dir.join("packed-refs");
1741 if packed_path.exists() {
1742 packed_ref_names_with_prefix(
1743 self.format,
1744 &fs::read(packed_path)?,
1745 prefix,
1746 false,
1747 &mut names,
1748 )?;
1749 }
1750 let mut loose_names = BTreeSet::new();
1751 self.collect_loose_ref_names_with_prefix(prefix, &mut loose_names)?;
1752 self.collect_current_worktree_loose_ref_names_with_prefix(prefix, &mut loose_names)?;
1753 if !loose_names.is_empty() {
1754 names.retain(|name| !loose_names.contains(name));
1755 names.extend(loose_names);
1756 }
1757 names.retain(|name| name.starts_with(prefix));
1758 names.retain(|name| {
1759 if validate_ref_name(name).is_ok() {
1760 true
1761 } else {
1762 warn_broken_ref_name(name);
1763 false
1764 }
1765 });
1766 names.sort();
1767 names.dedup();
1768 Ok(names)
1769 }
1770
1771 pub fn list_short_ref_names_with_prefix(&self, prefix: &str) -> Result<Vec<String>> {
1772 if self.uses_reftable()? {
1773 let mut names = self
1774 .list_reftable_refs_with_prefix(prefix)?
1775 .into_iter()
1776 .filter_map(|reference| reference.name.strip_prefix(prefix).map(str::to_owned))
1777 .collect::<Vec<_>>();
1778 names.sort();
1779 return Ok(names);
1780 }
1781 let mut names = Vec::new();
1782 let packed_path = self.storage_dir.join("packed-refs");
1783 if packed_path.exists() {
1784 packed_ref_names_with_prefix(
1785 self.format,
1786 &fs::read(packed_path)?,
1787 prefix,
1788 true,
1789 &mut names,
1790 )?;
1791 }
1792 let mut loose_full_names = BTreeSet::new();
1793 self.collect_loose_ref_names_with_prefix(prefix, &mut loose_full_names)?;
1794 self.collect_current_worktree_loose_ref_names_with_prefix(prefix, &mut loose_full_names)?;
1795 let loose_names = loose_full_names
1796 .into_iter()
1797 .filter_map(|name| {
1798 if validate_ref_name(&name).is_ok() {
1799 name.strip_prefix(prefix).map(str::to_owned)
1800 } else {
1801 warn_broken_ref_name(&name);
1802 None
1803 }
1804 })
1805 .collect::<BTreeSet<_>>();
1806 if !loose_names.is_empty() {
1807 names.retain(|name| !loose_names.contains(name));
1808 names.extend(loose_names);
1809 }
1810 names.sort();
1811 names.dedup();
1812 Ok(names)
1813 }
1814
1815 pub fn list_all_refs(&self) -> Result<Vec<Ref>> {
1816 let mut refs = self.list_refs()?;
1817 let mut seen = refs
1818 .iter()
1819 .map(|reference| reference.name.clone())
1820 .collect::<BTreeSet<_>>();
1821 for reference in self.list_root_refs()? {
1822 if seen.insert(reference.name.clone()) {
1823 refs.push(reference);
1824 }
1825 }
1826 refs.sort_by(|left, right| left.name.cmp(&right.name));
1827 Ok(refs)
1828 }
1829
1830 pub fn list_reflog_names(&self) -> Result<Vec<String>> {
1831 let mut names = BTreeSet::new();
1832 if self.uses_reftable()? {
1833 let null = ObjectId::null(self.format);
1834 let mut logs_by_index = BTreeMap::<(String, u64), bool>::new();
1835 for table in self.reftables()? {
1836 for record in table.logs {
1837 let has_visible_entry = match record.value {
1838 ReftableLogValue::Deletion => false,
1839 ReftableLogValue::Update(update) => {
1840 !reftable_log_update_is_empty_marker(&update, null)
1841 }
1842 };
1843 logs_by_index.insert((record.refname, record.update_index), has_visible_entry);
1844 }
1845 }
1846 for ((name, _), has_visible_entry) in logs_by_index {
1847 if has_visible_entry {
1848 names.insert(name);
1849 }
1850 }
1851 return Ok(names.into_iter().collect());
1852 }
1853 self.collect_reflog_names(&self.storage_dir.join("logs"), "logs", &mut names)?;
1854 let worktree_logs = self.git_dir.join("logs");
1855 if worktree_logs != self.storage_dir.join("logs") {
1856 self.collect_reflog_names(&worktree_logs, "logs", &mut names)?;
1857 }
1858 Ok(names.into_iter().collect())
1859 }
1860
1861 pub fn has_refs_with_prefix(&self, prefix: &str) -> Result<bool> {
1862 if self.uses_reftable()? {
1863 return Ok(self
1864 .list_reftable_refs()?
1865 .iter()
1866 .any(|reference| reference.name.starts_with(prefix)));
1867 }
1868 let packed_path = self.storage_dir.join("packed-refs");
1869 if packed_path.exists()
1870 && packed_refs_have_prefix(self.format, &fs::read(&packed_path)?, prefix)?
1871 {
1872 return Ok(true);
1873 }
1874 self.loose_refs_have_prefix(prefix)
1875 }
1876
1877 pub fn write_packed_refs(&self, refs: &[PackedRef]) -> Result<()> {
1878 self.write_packed_refs_with_timeout(refs, 0)
1879 }
1880
1881 pub fn write_packed_refs_with_timeout(
1882 &self,
1883 refs: &[PackedRef],
1884 timeout_millis: u64,
1885 ) -> Result<()> {
1886 let path = self.packed_refs_write_path()?;
1887 write_locked_with_timeout(
1888 &path,
1889 &write_packed_refs(refs)?,
1890 timeout_millis,
1891 self.reference_fsync.method_if_enabled(),
1892 )
1893 }
1894
1895 pub fn pack_refs(&self, prune_loose: bool) -> Result<Vec<PackedRef>> {
1896 self.pack_refs_with_peeler(prune_loose, |_, _| Ok(None))
1897 }
1898
1899 pub fn pack_refs_with_peeler<F>(&self, prune_loose: bool, mut peel: F) -> Result<Vec<PackedRef>>
1900 where
1901 F: FnMut(&str, &ObjectId) -> Result<Option<ObjectId>>,
1902 {
1903 self.pack_refs_selected_with_timeout(
1904 prune_loose,
1905 false,
1906 0,
1907 |_| true,
1908 |name, oid| peel(name, oid).map(|peeled| PackRefDecision::Pack { peeled }),
1909 )
1910 }
1911
1912 pub fn pack_refs_selected_with_timeout<F, S>(
1913 &self,
1914 prune_loose: bool,
1915 auto: bool,
1916 timeout_millis: u64,
1917 mut should_pack: S,
1918 mut decide: F,
1919 ) -> Result<Vec<PackedRef>>
1920 where
1921 F: FnMut(&str, &ObjectId) -> Result<PackRefDecision>,
1922 S: FnMut(&str) -> bool,
1923 {
1924 if self.uses_reftable()? {
1925 self.compact_reftable_stack()?;
1926 return Ok(Vec::new());
1927 }
1928 let mut packed_refs = BTreeMap::new();
1929 let packed_path = self.storage_dir.join("packed-refs");
1930 if packed_path.exists() {
1931 for packed in parse_packed_refs(self.format, &fs::read(&packed_path)?)? {
1932 packed_refs.insert(packed.reference.name.clone(), packed);
1933 }
1934 }
1935
1936 let mut loose_refs = BTreeMap::new();
1937 let refs_dir = self.storage_dir.join("refs");
1938 if refs_dir.exists() {
1939 self.collect_loose_refs(&refs_dir, "refs", &mut loose_refs)?;
1940 }
1941 let loose_refs = loose_refs
1942 .into_values()
1943 .filter(|reference| packable_loose_ref_name(&reference.name))
1944 .filter(|reference| should_pack(&reference.name))
1945 .collect::<Vec<_>>();
1946 if auto && !pack_refs_auto_required_for(&packed_path, loose_refs.len())? {
1947 return Ok(packed_refs.into_values().collect());
1948 }
1949 let mut packed_loose_refs = Vec::new();
1950 for reference in loose_refs {
1951 let RefTarget::Direct(oid) = reference.target else {
1952 continue;
1953 };
1954 let peeled = match decide(&reference.name, &oid)? {
1955 PackRefDecision::Pack { peeled } => peeled,
1956 PackRefDecision::Skip => continue,
1957 };
1958 packed_loose_refs.push((reference.name.clone(), oid));
1959 packed_refs.insert(
1960 reference.name.clone(),
1961 PackedRef {
1962 reference: Ref {
1963 name: reference.name,
1964 target: RefTarget::Direct(oid),
1965 },
1966 peeled,
1967 },
1968 );
1969 }
1970
1971 let refs = packed_refs.into_values().collect::<Vec<_>>();
1972 self.write_packed_refs_with_timeout(&refs, timeout_millis)?;
1973 if prune_loose {
1974 for (name, oid) in packed_loose_refs {
1975 self.prune_loose_ref_after_pack(&name, &oid)?;
1976 }
1977 }
1978 Ok(refs)
1979 }
1980
1981 pub fn pack_refs_auto_required<S>(&self, mut should_pack: S) -> Result<bool>
1982 where
1983 S: FnMut(&str) -> bool,
1984 {
1985 if self.uses_reftable()? {
1986 return Ok(true);
1987 }
1988 let mut loose_refs = BTreeMap::new();
1989 let refs_dir = self.storage_dir.join("refs");
1990 if refs_dir.exists() {
1991 self.collect_loose_refs(&refs_dir, "refs", &mut loose_refs)?;
1992 }
1993 let count = loose_refs
1994 .values()
1995 .filter(|reference| packable_loose_ref_name(&reference.name))
1996 .filter(|reference| matches!(reference.target, RefTarget::Direct(_)))
1997 .filter(|reference| should_pack(&reference.name))
1998 .count();
1999 pack_refs_auto_required_for(&self.storage_dir.join("packed-refs"), count)
2000 }
2001
2002 fn packed_refs_write_path(&self) -> Result<PathBuf> {
2003 let path = self.storage_dir.join("packed-refs");
2004 match fs::symlink_metadata(&path) {
2005 Ok(meta) if meta.file_type().is_symlink() => {
2006 let target = fs::read_link(&path)?;
2007 if target.is_absolute() {
2008 Ok(target)
2009 } else {
2010 let parent = path.parent().ok_or_else(|| {
2011 GitError::InvalidPath("packed-refs path has no parent".into())
2012 })?;
2013 Ok(parent.join(target))
2014 }
2015 }
2016 Ok(_) => Ok(path),
2017 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(path),
2018 Err(err) => Err(err.into()),
2019 }
2020 }
2021
2022 pub fn current_branch_ref(&self) -> Result<Option<String>> {
2023 match self.read_ref("HEAD")? {
2024 Some(RefTarget::Symbolic(name)) if name.starts_with("refs/heads/") => Ok(Some(name)),
2025 _ => Ok(None),
2026 }
2027 }
2028
2029 pub fn current_branch(&self) -> Result<Option<String>> {
2030 Ok(self
2031 .current_branch_ref()?
2032 .and_then(|name| name.strip_prefix("refs/heads/").map(str::to_string)))
2033 }
2034
2035 pub fn transaction(&self) -> FileRefTransaction<'_> {
2036 FileRefTransaction {
2037 store: self,
2038 changes: Vec::new(),
2039 hook: None,
2040 }
2041 }
2042
2043 pub fn uses_alternate_ref_storage(&self) -> bool {
2044 self.storage_dir != self.common_dir
2045 }
2046
2047 pub fn current_worktree_ref_storage_dir(&self) -> PathBuf {
2048 self.current_worktree_storage_dir()
2049 }
2050
2051 pub fn create_branch(
2052 &self,
2053 branch: &str,
2054 start: ObjectId,
2055 committer: Vec<u8>,
2056 message: Vec<u8>,
2057 ) -> Result<BranchCreate> {
2058 let name = branch_ref_name(branch)?;
2059 if self.read_ref(&name)?.is_some() {
2060 return Err(GitError::Transaction(format!(
2061 "branch {branch} already exists"
2062 )));
2063 }
2064 let zero = ObjectId::null(self.format);
2065 let mut tx = self.transaction();
2066 tx.update(RefUpdate {
2067 name: name.clone(),
2068 expected: None,
2069 new: RefTarget::Direct(start),
2070 reflog: Some(ReflogEntry {
2071 old_oid: zero,
2072 new_oid: start,
2073 committer,
2074 message,
2075 }),
2076 });
2077 tx.commit()?;
2078 Ok(BranchCreate { name, oid: start })
2079 }
2080
2081 pub fn delete_branch(&self, branch: &str) -> Result<BranchDelete> {
2082 let name = branch_ref_name_for_read(branch)?;
2083 if matches!(self.read_ref("HEAD")?, Some(RefTarget::Symbolic(head)) if head == name) {
2084 return Err(GitError::Transaction(format!(
2085 "cannot delete branch {branch} checked out at HEAD"
2086 )));
2087 }
2088 let oid = self.delete_direct_ref(&name, "branch", branch)?;
2089 self.remove_reflog_file(&name);
2090 Ok(BranchDelete { name, oid })
2091 }
2092
2093 pub fn move_branch(
2094 &self,
2095 old_branch: &str,
2096 new_branch: &str,
2097 force: bool,
2098 committer: Vec<u8>,
2099 ) -> Result<()> {
2100 self.copy_or_move_branch(old_branch, new_branch, force, false, committer)
2101 }
2102
2103 pub fn copy_branch(
2104 &self,
2105 old_branch: &str,
2106 new_branch: &str,
2107 force: bool,
2108 committer: Vec<u8>,
2109 ) -> Result<()> {
2110 self.copy_or_move_branch(old_branch, new_branch, force, true, committer)
2111 }
2112
2113 fn conflicting_ref_for_path(&self, new_name: &str, exclude: &str) -> Result<Option<String>> {
2120 for reference in self.list_refs()? {
2121 let name = &reference.name;
2122 if name == new_name || name == exclude {
2123 continue;
2124 }
2125 if new_name.starts_with(&format!("{name}/")) {
2127 return Ok(Some(name.clone()));
2128 }
2129 if name.starts_with(&format!("{new_name}/")) {
2131 return Ok(Some(name.clone()));
2132 }
2133 }
2134 Ok(None)
2135 }
2136
2137 fn copy_or_move_branch(
2138 &self,
2139 old_branch: &str,
2140 new_branch: &str,
2141 force: bool,
2142 copy: bool,
2143 committer: Vec<u8>,
2144 ) -> Result<()> {
2145 let old_name = branch_ref_name_for_source(old_branch)?;
2146 let new_name = branch_ref_name(new_branch)?;
2147 if old_name == new_name {
2148 return Ok(());
2149 }
2150 let Some(target) = self.read_ref(&old_name)? else {
2151 return Err(GitError::reference_not_found(format!(
2152 "branch {old_branch}"
2153 )));
2154 };
2155 let RefTarget::Direct(oid) = target else {
2156 return Err(GitError::InvalidFormat(format!(
2157 "branch {old_branch} is symbolic"
2158 )));
2159 };
2160 if !copy
2161 && fs::symlink_metadata(self.reflog_path(&old_name))
2162 .is_ok_and(|metadata| metadata.file_type().is_symlink())
2163 {
2164 return Err(GitError::Transaction(format!(
2165 "reflog for '{old_name}' is a symlink"
2166 )));
2167 }
2168 let conflict_exclude = if copy { "" } else { &old_name };
2176 if let Some(conflict) = self.conflicting_ref_for_path(&new_name, conflict_exclude)? {
2177 return Err(GitError::Transaction(format!(
2178 "'{conflict}' exists; cannot create '{new_name}'"
2179 )));
2180 }
2181 let dest_entry = self.read_ref(&new_name)?;
2185 let dest_resolves = resolve_ref_peeled(self, &new_name)?.is_some();
2186 if dest_resolves && !force {
2187 return Err(GitError::Transaction(format!(
2188 "branch {new_branch} already exists"
2189 )));
2190 }
2191 match dest_entry {
2195 Some(RefTarget::Symbolic(_)) => {
2196 self.delete_symbolic_ref(&new_name)?;
2197 self.remove_reflog_file(&new_name);
2198 }
2199 Some(RefTarget::Direct(_)) => {
2200 let _ = self.delete_direct_ref(&new_name, "branch", new_branch)?;
2201 self.remove_reflog_file(&new_name);
2202 }
2203 None => {}
2204 }
2205
2206 let mut reflog = self.read_reflog(&old_name)?;
2209 reflog.push(ReflogEntry {
2210 old_oid: oid,
2211 new_oid: oid,
2212 committer,
2213 message: if copy {
2214 format!("Branch: copied {old_name} to {new_name}").into_bytes()
2215 } else {
2216 format!("Branch: renamed {old_name} to {new_name}").into_bytes()
2217 },
2218 });
2219
2220 if !copy {
2225 let _ = self.delete_direct_ref(&old_name, "branch", old_branch)?;
2226 self.remove_reflog_file(&old_name);
2227 }
2228
2229 self.write_loose_ref(&Ref {
2230 name: new_name.clone(),
2231 target: RefTarget::Direct(oid),
2232 })?;
2233 self.write_reflog(&new_name, &reflog)?;
2234
2235 if !copy
2236 && matches!(self.read_ref("HEAD")?, Some(RefTarget::Symbolic(head)) if head == old_name)
2237 {
2238 self.write_loose_ref(&Ref {
2239 name: "HEAD".into(),
2240 target: RefTarget::Symbolic(new_name),
2241 })?;
2242 }
2243 Ok(())
2244 }
2245
2246 pub fn create_tag(&self, tag: &str, target: ObjectId) -> Result<TagCreate> {
2247 let name = tag_ref_name(tag)?;
2248 if self.read_ref(&name)?.is_some() {
2249 return Err(GitError::Transaction(format!("tag {tag} already exists")));
2250 }
2251 let mut tx = self.transaction();
2252 tx.update(RefUpdate {
2253 name: name.clone(),
2254 expected: None,
2255 new: RefTarget::Direct(target),
2256 reflog: None,
2257 });
2258 tx.commit()?;
2259 Ok(TagCreate { name, oid: target })
2260 }
2261
2262 pub fn apply_bundle_ref_updates(
2263 &self,
2264 refs: &[BundleRefUpdate],
2265 reflog: Option<BundleRefUpdateReflog>,
2266 ) -> Result<Vec<AppliedBundleRefUpdate>> {
2267 let (updates, applied) = prepare_bundle_ref_updates(refs, reflog.as_ref(), |name, oid| {
2268 if oid.format() != self.format {
2269 return Err(GitError::InvalidObjectId(format!(
2270 "bundle ref {name} has {} object id for {} repository",
2271 oid.format().name(),
2272 self.format.name()
2273 )));
2274 }
2275 self.read_ref(name)
2276 })?;
2277 let mut tx = self.transaction();
2278 for update in updates {
2279 tx.update(update);
2280 }
2281 tx.commit()?;
2282 Ok(applied)
2283 }
2284
2285 pub fn delete_tag(&self, tag: &str) -> Result<TagDelete> {
2286 let name = TagRefNameBuf::from_tag_name_unrestricted(tag)?.into_string();
2287 let oid = self.delete_direct_ref(&name, "tag", tag)?;
2288 self.remove_reflog_file(&name);
2289 Ok(TagDelete { name, oid })
2290 }
2291
2292 pub fn delete_ref(&self, name: &str) -> Result<RefDelete> {
2293 validate_ref_name_for_read(name)?;
2294 let oid = self.delete_direct_ref(name, "ref", name)?;
2295 self.remove_reflog_file(name);
2296 Ok(RefDelete {
2297 name: name.into(),
2298 oid,
2299 })
2300 }
2301
2302 pub fn delete_ref_checked(
2303 &self,
2304 delete: DeleteRef,
2305 ) -> std::result::Result<RefDelete, RefDeleteError> {
2306 validate_ref_name_for_read(&delete.name).map_err(|_| RefDeleteError::InvalidName)?;
2307 if self.uses_reftable().map_err(ref_delete_error_from_git)? {
2308 return self.delete_reftable_ref_checked(delete);
2309 }
2310 self.delete_files_ref_checked(delete)
2311 }
2312
2313 pub fn delete_symbolic_ref(&self, name: &str) -> Result<bool> {
2314 validate_ref_name_for_read(name)?;
2315 if self.uses_reftable()? {
2316 let Some(target) = self.read_ref(name)? else {
2317 return Ok(false);
2318 };
2319 if !matches!(target, RefTarget::Symbolic(_)) {
2320 return Ok(false);
2321 }
2322 self.append_reftable_records(vec![ReftableRefRecord {
2323 name: name.to_string(),
2324 update_index: 0,
2325 value: ReftableRefValue::Deletion,
2326 }])?;
2327 self.remove_reflog_file(name);
2328 return Ok(true);
2329 }
2330 let Some(reference) = self.read_loose_ref(name)? else {
2331 return Ok(false);
2332 };
2333 if !matches!(reference.target, RefTarget::Symbolic(_)) {
2334 return Ok(false);
2335 }
2336 self.delete_loose_ref(name)?;
2337 self.remove_reflog_file(name);
2338 Ok(true)
2339 }
2340
2341 fn delete_direct_ref(&self, name: &str, kind: &str, short_name: &str) -> Result<ObjectId> {
2342 if self.uses_reftable()? {
2343 let Some(target) = self.read_ref(name)? else {
2344 return Err(GitError::reference_not_found(format!(
2345 "{kind} {short_name}"
2346 )));
2347 };
2348 let RefTarget::Direct(oid) = target else {
2349 return Err(GitError::InvalidFormat(format!(
2350 "{kind} {short_name} is symbolic"
2351 )));
2352 };
2353 self.append_reftable_records(vec![ReftableRefRecord {
2354 name: name.to_string(),
2355 update_index: 0,
2356 value: ReftableRefValue::Deletion,
2357 }])?;
2358 self.remove_reflog_file(name);
2361 return Ok(oid);
2362 }
2363 let target = self
2364 .read_ref(name)?
2365 .ok_or_else(|| GitError::reference_not_found(format!("{kind} {short_name}")))?;
2366 let RefTarget::Direct(oid) = target else {
2367 return Err(GitError::InvalidFormat(format!(
2368 "{kind} {short_name} is symbolic"
2369 )));
2370 };
2371
2372 let mut tx = self.transaction();
2377 tx.delete_with_precondition(
2378 name.to_string(),
2379 RefDeletePrecondition::Direct(Some(oid)),
2380 None,
2381 );
2382 tx.commit()?;
2383 Ok(oid)
2384 }
2385
2386 fn delete_reftable_ref_checked(
2387 &self,
2388 delete: DeleteRef,
2389 ) -> std::result::Result<RefDelete, RefDeleteError> {
2390 let target = self
2391 .read_ref(&delete.name)
2392 .map_err(ref_delete_error_from_git)?;
2393 let oid = checked_delete_oid(delete.expected_old, target)?;
2394 self.append_reftable_records(vec![ReftableRefRecord {
2395 name: delete.name.clone(),
2396 update_index: 0,
2397 value: ReftableRefValue::Deletion,
2398 }])
2399 .map_err(ref_delete_error_from_git)?;
2400 self.remove_reflog_file(&delete.name);
2403 Ok(RefDelete {
2404 name: delete.name,
2405 oid,
2406 })
2407 }
2408
2409 fn delete_files_ref_checked(
2410 &self,
2411 delete: DeleteRef,
2412 ) -> std::result::Result<RefDelete, RefDeleteError> {
2413 let name = delete.name;
2414 let path = self.ref_path(&name);
2415 let parent = path.parent().ok_or(RefDeleteError::InvalidName)?;
2416 fs::create_dir_all(parent).map_err(RefDeleteError::from)?;
2417
2418 let loose_lock_path = lock_path_for(&path).map_err(|_| RefDeleteError::InvalidName)?;
2419 let _prune_guard = RefDirPruneGuard {
2420 store: self,
2421 name: name.clone(),
2422 };
2423 let loose_lock = DeleteLock::acquire(loose_lock_path)?;
2424
2425 let packed_path = self.storage_dir.join("packed-refs");
2426 let packed_lock_path =
2427 lock_path_for(&packed_path).map_err(|_| RefDeleteError::InvalidName)?;
2428 let mut packed_lock = DeleteLock::acquire(packed_lock_path)?;
2429
2430 let loose_ref = self
2431 .read_loose_ref(&name)
2432 .map_err(ref_delete_error_from_git)?;
2433 let packed_original = match fs::read(&packed_path) {
2434 Ok(bytes) => Some(bytes),
2435 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
2436 Err(err) => return Err(RefDeleteError::Io(err)),
2437 };
2438 let mut packed_refs = match &packed_original {
2439 Some(bytes) => {
2440 parse_packed_refs(self.format, bytes).map_err(ref_delete_error_from_git)?
2441 }
2442 None => Vec::new(),
2443 };
2444 let packed_index = packed_refs
2445 .iter()
2446 .position(|reference| reference.reference.name == name);
2447
2448 let current = if let Some(reference) = loose_ref.as_ref() {
2449 Some(reference.target.clone())
2450 } else {
2451 packed_index.map(|index| packed_refs[index].reference.target.clone())
2452 };
2453 let oid = checked_delete_oid(delete.expected_old, current)?;
2454
2455 let packed_changed = if let Some(index) = packed_index {
2456 packed_refs.remove(index);
2457 true
2458 } else {
2459 false
2460 };
2461
2462 if packed_changed {
2463 let packed_bytes =
2464 write_packed_refs(&packed_refs).map_err(ref_delete_error_from_git)?;
2465 packed_lock.write_all(&packed_bytes, self.reference_fsync.method_if_enabled())?;
2466 let lock_path = packed_lock.close();
2467 if let Err(err) = fs::rename(&lock_path, &packed_path) {
2468 let _ = fs::remove_file(&lock_path);
2469 return Err(RefDeleteError::Io(err));
2470 }
2471 } else {
2472 packed_lock.remove();
2473 }
2474
2475 if loose_ref.is_some()
2476 && let Err(err) = fs::remove_file(&path)
2477 {
2478 if packed_changed && let Some(bytes) = packed_original.as_ref() {
2479 let _ = restore_file_atomically(&packed_path, bytes);
2480 }
2481 return Err(RefDeleteError::Io(err));
2482 }
2483 loose_lock.remove();
2484
2485 self.remove_reflog_file(&name);
2488 Ok(RefDelete { name, oid })
2489 }
2490
2491 fn read_loose_ref(&self, name: &str) -> Result<Option<Ref>> {
2492 let path = self.ref_path(name);
2493 let metadata = match fs::symlink_metadata(&path) {
2494 Ok(metadata) => metadata,
2495 Err(err)
2496 if err.kind() == std::io::ErrorKind::NotFound || err.raw_os_error() == Some(20) =>
2497 {
2498 return Ok(None);
2499 }
2500 Err(err) => return Err(err.into()),
2501 };
2502 if metadata.is_dir() {
2503 return Ok(None);
2504 }
2505 if metadata.file_type().is_symlink() {
2506 if let Ok(target) = fs::read_link(&path) {
2507 let target = target.to_string_lossy();
2508 if target.starts_with("refs/") {
2509 return Ok(Some(Ref {
2510 name: name.to_string(),
2511 target: RefTarget::Symbolic(target.into_owned()),
2512 }));
2513 }
2514 }
2515 if fs::metadata(&path).is_err_and(|err| {
2520 err.kind() == std::io::ErrorKind::NotFound
2521 || err.kind() == std::io::ErrorKind::NotADirectory
2522 }) {
2523 return Ok(None);
2524 }
2525 }
2526 let bytes = fs::read(path)?;
2527 if loose_ref_bytes_are_reftable_sentinel(name, &bytes) {
2528 return Ok(None);
2529 }
2530 Ok(Some(parse_loose_ref(self.format, name, &bytes)?))
2531 }
2532
2533 fn read_packed_ref(&self, name: &str) -> Result<Option<PackedRef>> {
2534 let path = self.storage_dir.join("packed-refs");
2535 if !path.exists() {
2536 return Ok(None);
2537 }
2538 Ok(parse_packed_refs(self.format, &fs::read(path)?)?
2539 .into_iter()
2540 .find(|reference| reference.reference.name == name))
2541 }
2542
2543 fn read_reftable_ref(&self, name: &str) -> Result<Option<RefTarget>> {
2544 for table in self.reftables()?.into_iter().rev() {
2545 if let Some(record) = table.refs.into_iter().find(|record| record.name == name) {
2546 return reftable_ref_target(record.value);
2547 }
2548 }
2549 Ok(None)
2550 }
2551
2552 fn list_reftable_refs(&self) -> Result<Vec<Ref>> {
2553 self.list_reftable_refs_with_prefix("refs/")
2554 }
2555
2556 fn list_reftable_refs_with_prefix(&self, prefix: &str) -> Result<Vec<Ref>> {
2557 let mut refs = BTreeMap::<String, Ref>::new();
2558 for table in self.reftables()? {
2559 for record in table.refs {
2560 if !record.name.starts_with("refs/") || !record.name.starts_with(prefix) {
2561 continue;
2562 }
2563 match reftable_ref_target(record.value)? {
2564 Some(target) => {
2565 refs.insert(
2566 record.name.clone(),
2567 Ref {
2568 name: record.name,
2569 target,
2570 },
2571 );
2572 }
2573 None => {
2574 refs.remove(&record.name);
2575 }
2576 }
2577 }
2578 }
2579 Ok(refs.into_values().collect())
2580 }
2581
2582 pub fn list_root_refs(&self) -> Result<Vec<Ref>> {
2590 if self.uses_reftable()? {
2591 let mut refs = BTreeMap::<String, Ref>::new();
2592 for table in self.reftables()? {
2593 for record in table.refs {
2594 if record.name.starts_with("refs/")
2595 || !is_root_ref_syntax(&record.name)
2596 || matches!(record.name.as_str(), "FETCH_HEAD" | "MERGE_HEAD")
2597 {
2598 continue;
2599 }
2600 match reftable_ref_target(record.value)? {
2601 Some(target) => {
2602 refs.insert(
2603 record.name.clone(),
2604 Ref {
2605 name: record.name,
2606 target,
2607 },
2608 );
2609 }
2610 None => {
2611 refs.remove(&record.name);
2612 }
2613 }
2614 }
2615 }
2616 return Ok(refs.into_values().collect());
2617 }
2618
2619 let mut refs = Vec::new();
2620 for entry in fs::read_dir(&self.git_dir)? {
2621 let entry = entry?;
2622 if !entry.file_type()?.is_file() {
2623 continue;
2624 }
2625 let name = entry.file_name().to_string_lossy().into_owned();
2626 if !is_root_ref_syntax(&name) || matches!(name.as_str(), "FETCH_HEAD" | "MERGE_HEAD") {
2627 continue;
2628 }
2629 if let Ok(reference) = parse_loose_ref(self.format, name, &fs::read(entry.path())?) {
2630 refs.push(reference);
2631 }
2632 }
2633 refs.sort_by(|left, right| left.name.cmp(&right.name));
2634 Ok(refs)
2635 }
2636
2637 fn reftables(&self) -> Result<Vec<Reftable>> {
2638 let reftable_dir = self.storage_dir.join("reftable");
2639 let tables_list = reftable_dir.join("tables.list");
2640 for _ in 0..10 {
2641 let text = match fs::read_to_string(&tables_list) {
2642 Ok(text) => text,
2643 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
2644 if !tables_list.exists() {
2645 return Ok(Vec::new());
2646 }
2647 thread::sleep(Duration::from_millis(10));
2648 continue;
2649 }
2650 Err(err) => return Err(err.into()),
2651 };
2652 let mut tables = Vec::new();
2653 let mut reload = false;
2654 for raw_line in text.lines() {
2655 let line = raw_line.trim();
2656 if line.is_empty() {
2657 continue;
2658 }
2659 if line.contains('/')
2660 || line.contains('\\')
2661 || Path::new(line).components().count() != 1
2662 {
2663 return Err(GitError::InvalidPath(format!(
2664 "invalid reftable table name {line}"
2665 )));
2666 }
2667 let bytes = match fs::read(reftable_dir.join(line)) {
2668 Ok(bytes) => bytes,
2669 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
2670 reload = true;
2671 break;
2672 }
2673 Err(err) => return Err(err.into()),
2674 };
2675 let table = Reftable::parse(&bytes)?;
2676 if table.header.object_format != self.format {
2677 return Err(GitError::InvalidFormat(format!(
2678 "reftable {line} has {} object ids in {} repository",
2679 table.header.object_format.name(),
2680 self.format.name()
2681 )));
2682 }
2683 tables.push(table);
2684 }
2685 if reload {
2686 thread::sleep(Duration::from_millis(10));
2687 continue;
2688 }
2689 return Ok(tables);
2690 }
2691 Err(GitError::Io(format!(
2692 "cannot read stable reftable stack {}",
2693 tables_list.display()
2694 )))
2695 }
2696
2697 fn reftable_store_with_storage(&self, storage_dir: PathBuf) -> FileRefStore {
2698 FileRefStore {
2699 git_dir: self.git_dir.clone(),
2700 common_dir: self.common_dir.clone(),
2701 storage_dir,
2702 format: self.format,
2703 ref_storage: self.ref_storage.clone(),
2704 packed_refs_lock_timeout_millis: self.packed_refs_lock_timeout_millis,
2705 reftable_lock_timeout_millis: self.reftable_lock_timeout_millis,
2706 reftable_write_options: self.reftable_write_options,
2707 combine_reftable_logs: self.combine_reftable_logs,
2708 prefer_symlink_refs: self.prefer_symlink_refs,
2709 reference_fsync: self.reference_fsync,
2710 shared_repository: self.shared_repository.clone(),
2711 }
2712 }
2713
2714 fn shared_reftable_storage_dir(&self) -> PathBuf {
2725 match self.ref_storage.backend.as_ref() {
2726 Some((_, Some(path))) => path.clone(),
2727 _ => self.common_dir.clone(),
2728 }
2729 }
2730
2731 fn current_worktree_storage_dir(&self) -> PathBuf {
2732 if self.storage_dir == self.git_dir {
2736 return self.git_dir.clone();
2737 }
2738 if self.storage_dir != self.common_dir {
2739 if self.git_dir != self.common_dir
2740 && let Some(worktree) = self.git_dir.file_name()
2741 {
2742 return self.storage_dir.join("worktrees").join(worktree);
2743 }
2744 return self.storage_dir.clone();
2745 }
2746 self.git_dir.clone()
2747 }
2748
2749 fn reftable_store_for_ref(&self, name: &str) -> Result<(FileRefStore, String)> {
2750 if let Some((worktree, rewritten)) = reftable_other_worktree_ref(name) {
2751 let storage_dir = self
2752 .shared_reftable_storage_dir()
2753 .join("worktrees")
2754 .join(worktree);
2755 return Ok((
2756 self.reftable_store_with_storage(storage_dir),
2757 rewritten.to_string(),
2758 ));
2759 }
2760 if reftable_current_worktree_ref(name) {
2761 return Ok((
2762 self.reftable_store_with_storage(self.current_worktree_storage_dir()),
2763 name.to_string(),
2764 ));
2765 }
2766 Ok((
2767 self.reftable_store_with_storage(self.shared_reftable_storage_dir()),
2768 name.to_string(),
2769 ))
2770 }
2771
2772 pub fn uses_reftable(&self) -> Result<bool> {
2773 if let Some(value) = self.ref_storage.invalid_value.as_deref() {
2774 return parse_ref_storage_backend_value(value)
2775 .map(|(kind, _)| kind == RefBackendKind::Reftable);
2776 }
2777 Ok(matches!(
2778 self.ref_storage.backend,
2779 Some((RefBackendKind::Reftable, _))
2780 ))
2781 }
2782
2783 pub fn reftable_table_count(&self) -> Result<usize> {
2784 Ok(self.reftable_table_names()?.len())
2785 }
2786
2787 fn append_reftable_records(&self, records: Vec<ReftableRefRecord>) -> Result<()> {
2788 if records.is_empty() {
2789 return Ok(());
2790 }
2791 self.append_reftable_table(records, Vec::new())?;
2792 Ok(())
2793 }
2794
2795 fn next_reftable_update_index(&self, table_names: &[String]) -> Result<u64> {
2796 let reftable_dir = self.storage_dir.join("reftable");
2797 let mut max_update_index = 0;
2798 for name in table_names {
2799 let table = Reftable::parse(&fs::read(reftable_dir.join(name))?)?;
2800 max_update_index = max_update_index.max(table.header.max_update_index);
2801 }
2802 max_update_index
2803 .checked_add(1)
2804 .ok_or_else(|| GitError::InvalidFormat("reftable update index overflow".into()))
2805 }
2806
2807 fn reftable_table_names(&self) -> Result<Vec<String>> {
2809 self.reftable_table_names_from(&self.storage_dir.join("reftable").join("tables.list"))
2810 }
2811
2812 fn reftable_table_names_from(&self, tables_list: &Path) -> Result<Vec<String>> {
2813 if !tables_list.exists() {
2814 return Ok(Vec::new());
2815 }
2816 Ok(fs::read_to_string(tables_list)?
2817 .lines()
2818 .map(str::trim)
2819 .filter(|line| !line.is_empty())
2820 .map(str::to_string)
2821 .collect())
2822 }
2823
2824 fn reftable_lock_timeout_millis(&self) -> Result<u64> {
2825 if let Some(timeout_millis) = self.reftable_lock_timeout_millis {
2826 return Ok(timeout_millis);
2827 }
2828 let config_path = self.common_dir.join("config");
2829 let Ok(config) = GitConfig::read(config_path) else {
2830 return Ok(0);
2831 };
2832 Ok(config
2833 .get("reftable", None, "lockTimeout")
2834 .and_then(|value| value.parse::<u64>().ok())
2835 .unwrap_or(0))
2836 }
2837
2838 fn acquire_reftable_list_lock(&self, list_path: PathBuf) -> Result<ReftableListLock> {
2839 let lock_path = lock_path_for(&list_path)?;
2840 let timeout_millis = self.reftable_lock_timeout_millis()?;
2841 ReftableListLock::acquire(list_path, lock_path, timeout_millis)
2842 }
2843
2844 fn append_reftable_table(
2849 &self,
2850 refs: Vec<ReftableRefRecord>,
2851 logs: Vec<ReftableLogRecord>,
2852 ) -> Result<u64> {
2853 self.append_reftable_table_with_compaction(refs, logs, true)
2854 }
2855
2856 fn append_reftable_table_with_compaction(
2857 &self,
2858 mut refs: Vec<ReftableRefRecord>,
2859 mut logs: Vec<ReftableLogRecord>,
2860 auto_compact: bool,
2861 ) -> Result<u64> {
2862 let reftable_dir = self.storage_dir.join("reftable");
2863 fs::create_dir_all(&reftable_dir)?;
2864 let list_path = reftable_dir.join("tables.list");
2865 let list_lock = self.acquire_reftable_list_lock(list_path.clone())?;
2866 let mut table_names = self.reftable_table_names_from(&list_path)?;
2867 let update_index = self.next_reftable_update_index(&table_names)?;
2868 for record in &mut refs {
2869 record.update_index = update_index;
2870 }
2871 for record in &mut logs {
2872 record.update_index = update_index;
2873 }
2874 let table_name = reftable_table_name(update_index, update_index);
2875 let bytes = Reftable::write_with_options(
2876 self.format,
2877 update_index,
2878 update_index,
2879 &refs,
2880 &logs,
2881 self.reftable_write_options,
2882 )?;
2883 let table_path = reftable_dir.join(&table_name);
2884 write_locked(
2885 &table_path,
2886 &bytes,
2887 self.reference_fsync.method_if_enabled(),
2888 )?;
2889 self.apply_reftable_shared_file_mode(&table_path)?;
2890 table_names.push(table_name);
2891 let mut list = Vec::new();
2892 for name in &table_names {
2893 list.extend_from_slice(name.as_bytes());
2894 list.push(b'\n');
2895 }
2896 list_lock.commit(&list, self.reference_fsync.method_if_enabled())?;
2897 self.apply_reftable_shared_file_mode(&list_path)?;
2898 if auto_compact && logs.is_empty() && table_names.len() > 6 {
2899 self.auto_compact_reftable_stack()?;
2900 }
2901 Ok(update_index)
2902 }
2903
2904 pub fn compact_reftable_stack(&self) -> Result<()> {
2905 let old_names = self.reftable_table_names()?;
2906 self.compact_reftable_stack_range(0, old_names.len(), true)
2907 }
2908
2909 fn compact_reftable_stack_range(
2910 &self,
2911 start: usize,
2912 end: usize,
2913 fail_on_locked_table: bool,
2914 ) -> Result<()> {
2915 let reftable_dir = self.storage_dir.join("reftable");
2916 let list_path = reftable_dir.join("tables.list");
2917 let list_lock = self.acquire_reftable_list_lock(list_path.clone())?;
2918 let old_names = self.reftable_table_names_from(&list_path)?;
2919 if start >= end || end > old_names.len() {
2920 return Ok(());
2921 }
2922 let compact_names = old_names[start..end].to_vec();
2923 if compact_names.is_empty() {
2924 return Ok(());
2925 }
2926 if fail_on_locked_table {
2927 for name in &compact_names {
2928 if reftable_dir.join(format!("{name}.lock")).exists() {
2929 return Err(GitError::Io(format!(
2930 "cannot lock references: {}: File exists",
2931 reftable_dir.join(format!("{name}.lock")).display()
2932 )));
2933 }
2934 }
2935 }
2936
2937 let mut refs: BTreeMap<String, ReftableRefRecord> = BTreeMap::new();
2938 let mut logs: BTreeMap<(String, u64), ReftableLogRecord> = BTreeMap::new();
2939 let mut min_index = u64::MAX;
2940 let mut max_index = 0u64;
2941 let drop_tombstones = start == 0;
2942 for name in &compact_names {
2943 let table = Reftable::parse(&fs::read(reftable_dir.join(name))?)?;
2944 for record in table.refs {
2945 match record.value {
2946 ReftableRefValue::Deletion if drop_tombstones => {
2947 refs.remove(&record.name);
2948 }
2949 _ => {
2950 min_index = min_index.min(record.update_index);
2951 max_index = max_index.max(record.update_index);
2952 refs.insert(record.name.clone(), record);
2953 }
2954 }
2955 }
2956 for record in table.logs {
2957 let key = (record.refname.clone(), record.update_index);
2958 match record.value {
2959 ReftableLogValue::Deletion if drop_tombstones => {
2960 logs.remove(&key);
2961 }
2962 _ => {
2963 min_index = min_index.min(record.update_index);
2964 max_index = max_index.max(record.update_index);
2965 logs.insert(key, record);
2966 }
2967 }
2968 }
2969 }
2970
2971 if refs.is_empty() && logs.is_empty() {
2972 min_index = compact_names
2973 .iter()
2974 .filter_map(|name| Reftable::parse(&fs::read(reftable_dir.join(name)).ok()?).ok())
2975 .map(|table| table.header.min_update_index)
2976 .min()
2977 .unwrap_or(1);
2978 max_index = min_index;
2979 }
2980
2981 let table_name = reftable_table_name(min_index, max_index);
2982 let refs = refs.into_values().collect::<Vec<_>>();
2983 let logs = logs.into_values().collect::<Vec<_>>();
2984 let bytes = Reftable::write_with_options(
2985 self.format,
2986 min_index,
2987 max_index,
2988 &refs,
2989 &logs,
2990 self.reftable_write_options,
2991 )?;
2992 let table_path = reftable_dir.join(&table_name);
2993 write_locked(
2994 &table_path,
2995 &bytes,
2996 self.reference_fsync.method_if_enabled(),
2997 )?;
2998 self.apply_reftable_shared_file_mode(&table_path)?;
2999 let mut list = Vec::new();
3000 for name in &old_names[..start] {
3001 list.extend_from_slice(name.as_bytes());
3002 list.push(b'\n');
3003 }
3004 list.extend_from_slice(table_name.as_bytes());
3005 list.push(b'\n');
3006 for name in &old_names[end..] {
3007 list.extend_from_slice(name.as_bytes());
3008 list.push(b'\n');
3009 }
3010 list_lock.commit(&list, self.reference_fsync.method_if_enabled())?;
3011 self.apply_reftable_shared_file_mode(&list_path)?;
3012 for name in compact_names {
3013 if name != table_name {
3014 let _ = fs::remove_file(reftable_dir.join(name));
3015 }
3016 }
3017 Ok(())
3018 }
3019
3020 fn apply_reftable_shared_file_mode(&self, path: &Path) -> Result<()> {
3021 #[cfg(unix)]
3022 {
3023 use std::os::unix::fs::PermissionsExt;
3024
3025 let config_path = self.common_dir.join("config");
3026 let Ok(config) = GitConfig::read(config_path) else {
3027 return Ok(());
3028 };
3029 let Some(value) = config.get("core", None, "sharedRepository") else {
3030 return Ok(());
3031 };
3032 let mode_or = match value {
3033 "1" | "group" | "true" => 0o660,
3034 "2" | "all" | "world" | "everybody" => 0o664,
3035 _ => return Ok(()),
3036 };
3037 let metadata = fs::metadata(path)?;
3038 let old_mode = metadata.permissions().mode();
3039 let mut permissions = metadata.permissions();
3040 permissions.set_mode((old_mode | mode_or) & 0o7777);
3041 fs::set_permissions(path, permissions)?;
3042 }
3043 #[cfg(not(unix))]
3044 {
3045 let _ = path;
3046 }
3047 Ok(())
3048 }
3049
3050 fn auto_compact_reftable_stack(&self) -> Result<()> {
3051 if reftable_autocompaction_disabled() {
3052 return Ok(());
3053 }
3054 let names = self.reftable_table_names()?;
3055 if names.len() <= 1 {
3056 return Ok(());
3057 }
3058 let reftable_dir = self.storage_dir.join("reftable");
3059 let header_overhead = match self.format {
3060 ObjectFormat::Sha1 => 23,
3061 ObjectFormat::Sha256 => 27,
3062 };
3063 let sizes = names
3064 .iter()
3065 .map(|name| {
3066 Ok(fs::metadata(reftable_dir.join(name))?
3067 .len()
3068 .saturating_sub(header_overhead))
3069 })
3070 .collect::<Result<Vec<_>>>()?;
3071 let factor = self.reftable_geometric_factor()?;
3072 let Some((start, end)) = reftable_compaction_segment(&sizes, factor) else {
3073 return Ok(());
3074 };
3075
3076 let start = names[start..end]
3082 .iter()
3083 .rposition(|name| reftable_dir.join(format!("{name}.lock")).exists())
3084 .map_or(start, |locked| start + locked + 1);
3085 if end.saturating_sub(start) < 2 {
3086 return Ok(());
3087 }
3088 self.compact_reftable_stack_range(start, end, false)?;
3089 Ok(())
3090 }
3091
3092 fn reftable_geometric_factor(&self) -> Result<u64> {
3093 let config_path = self.common_dir.join("config");
3094 let Ok(config) = GitConfig::read(config_path) else {
3095 return Ok(2);
3096 };
3097 Ok(config
3098 .get("reftable", None, "geometricFactor")
3099 .and_then(|value| value.parse::<u8>().ok())
3100 .filter(|factor| *factor != 0)
3101 .map(u64::from)
3102 .unwrap_or(2))
3103 }
3104
3105 fn read_reftable_logs(&self, name: &str) -> Result<Vec<ReflogEntry>> {
3112 let mut by_index: BTreeMap<u64, Option<ReftableLogUpdate>> = BTreeMap::new();
3115 for table in self.reftables()? {
3116 for record in table.logs {
3117 if record.refname != name {
3118 continue;
3119 }
3120 match record.value {
3121 ReftableLogValue::Deletion => {
3122 by_index.insert(record.update_index, None);
3123 }
3124 ReftableLogValue::Update(update) => {
3125 by_index.insert(record.update_index, Some(update));
3126 }
3127 }
3128 }
3129 }
3130 let null = ObjectId::null(self.format);
3131 let mut entries = Vec::new();
3132 for update in by_index.into_values().flatten() {
3133 if reftable_log_update_is_empty_marker(&update, null) {
3135 continue;
3136 }
3137 entries.push(reflog_entry_from_reftable(update));
3138 }
3139 Ok(entries)
3140 }
3141
3142 fn reftable_log_exists(&self, name: &str) -> Result<bool> {
3143 let mut by_index: BTreeMap<u64, bool> = BTreeMap::new();
3144 for table in self.reftables()? {
3145 for record in table.logs {
3146 if record.refname != name {
3147 continue;
3148 }
3149 match record.value {
3150 ReftableLogValue::Deletion => {
3151 by_index.insert(record.update_index, false);
3152 }
3153 ReftableLogValue::Update(_) => {
3154 by_index.insert(record.update_index, true);
3155 }
3156 }
3157 }
3158 }
3159 Ok(by_index.into_values().any(|exists| exists))
3160 }
3161
3162 fn collect_loose_refs(
3163 &self,
3164 dir: &Path,
3165 prefix: &str,
3166 refs: &mut BTreeMap<String, Ref>,
3167 ) -> Result<()> {
3168 for entry in fs::read_dir(dir)? {
3169 let entry = entry?;
3170 let path = entry.path();
3171 let name = format!("{prefix}/{}", entry.file_name().to_string_lossy());
3172 if path.is_dir() {
3173 self.collect_loose_refs(&path, &name, refs)?;
3174 } else if !name.ends_with(".lock") {
3175 let bytes = match fs::read(&path) {
3176 Ok(bytes) => bytes,
3177 Err(err)
3178 if entry.file_type()?.is_symlink()
3179 && (err.kind() == std::io::ErrorKind::NotFound
3180 || err.kind() == std::io::ErrorKind::NotADirectory) =>
3181 {
3182 continue;
3183 }
3184 Err(err) => return Err(err.into()),
3185 };
3186 if loose_ref_bytes_are_reftable_sentinel(&name, &bytes) {
3187 continue;
3188 }
3189 match parse_loose_ref(self.format, name.clone(), &bytes) {
3193 Ok(reference) if ref_target_is_broken(&reference.target) => {
3194 warn_broken_ref(&name);
3195 }
3196 Ok(reference) => {
3197 refs.insert(name, reference);
3198 }
3199 Err(_) => warn_broken_ref(&name),
3200 }
3201 }
3202 }
3203 Ok(())
3204 }
3205
3206 fn collect_loose_ref_snapshot(
3207 &self,
3208 dir: &Path,
3209 prefix: &str,
3210 entries: &mut BTreeMap<String, RefReadSnapshotValue>,
3211 ) -> Result<()> {
3212 let read_dir = match fs::read_dir(dir) {
3213 Ok(read_dir) => read_dir,
3214 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
3215 Err(err) => return Err(err.into()),
3216 };
3217 for entry in read_dir {
3218 let entry = entry?;
3219 let path = entry.path();
3220 let name = format!("{prefix}/{}", entry.file_name().to_string_lossy());
3221 if path.is_dir() {
3222 self.collect_loose_ref_snapshot(&path, &name, entries)?;
3223 continue;
3224 }
3225 if name.ends_with(".lock") {
3226 continue;
3227 }
3228 if entry.file_type()?.is_symlink()
3229 && let Ok(target) = fs::read_link(&path)
3230 && target.to_string_lossy().starts_with("refs/")
3231 {
3232 entries.insert(
3233 name,
3234 RefReadSnapshotValue::Target(RefTarget::Symbolic(
3235 target.to_string_lossy().into_owned(),
3236 )),
3237 );
3238 continue;
3239 }
3240 let bytes = match fs::read(&path) {
3241 Ok(bytes) => bytes,
3242 Err(err)
3243 if entry.file_type()?.is_symlink()
3244 && (err.kind() == std::io::ErrorKind::NotFound
3245 || err.kind() == std::io::ErrorKind::NotADirectory) =>
3246 {
3247 continue;
3248 }
3249 Err(err) => return Err(err.into()),
3250 };
3251 if loose_ref_bytes_are_reftable_sentinel(&name, &bytes) {
3252 continue;
3253 }
3254 let value = match parse_loose_ref(self.format, name.clone(), &bytes) {
3255 Ok(reference)
3256 if validate_ref_name(&name).is_ok()
3257 && !ref_target_is_broken(&reference.target) =>
3258 {
3259 RefReadSnapshotValue::Target(reference.target)
3260 }
3261 Ok(_) | Err(_) => RefReadSnapshotValue::Broken,
3262 };
3263 entries.insert(name, value);
3264 }
3265 Ok(())
3266 }
3267
3268 fn collect_loose_refs_with_prefix(
3269 &self,
3270 prefix: &str,
3271 refs: &mut BTreeMap<String, Ref>,
3272 ) -> Result<()> {
3273 self.collect_loose_refs_with_prefix_from_base(&self.storage_dir, prefix, refs)
3274 }
3275
3276 fn collect_current_worktree_loose_refs_with_prefix(
3277 &self,
3278 prefix: &str,
3279 refs: &mut BTreeMap<String, Ref>,
3280 ) -> Result<()> {
3281 let base = self.current_worktree_storage_dir();
3282 if base == self.storage_dir {
3283 return Ok(());
3284 }
3285 for namespace in CURRENT_WORKTREE_REF_PREFIXES {
3286 if ref_prefixes_overlap(prefix, namespace) {
3287 let scan_prefix = if prefix.starts_with(namespace) {
3288 prefix
3289 } else {
3290 namespace
3291 };
3292 self.collect_loose_refs_with_prefix_from_base(&base, scan_prefix, refs)?;
3293 }
3294 }
3295 Ok(())
3296 }
3297
3298 fn collect_loose_refs_with_prefix_from_base(
3299 &self,
3300 base: &Path,
3301 prefix: &str,
3302 refs: &mut BTreeMap<String, Ref>,
3303 ) -> Result<()> {
3304 if !safe_ref_prefix_for_directory_scan(prefix) {
3305 return self.collect_all_loose_refs_from_base(base, refs);
3306 }
3307
3308 let trimmed = prefix.trim_end_matches('/');
3309 if prefix.ends_with('/') {
3310 let candidate = base.join(trimmed);
3311 match fs::metadata(&candidate) {
3312 Ok(meta) if meta.is_dir() => {
3313 self.collect_loose_refs(&candidate, trimmed, refs)?;
3314 return Ok(());
3315 }
3316 Ok(_) => return Ok(()),
3317 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
3318 Err(err) => return Err(err.into()),
3319 }
3320 }
3321
3322 let Some((parent_prefix, _)) = trimmed.rsplit_once('/') else {
3323 return self.collect_all_loose_refs_from_base(base, refs);
3324 };
3325 let parent = base.join(parent_prefix);
3326 match fs::metadata(&parent) {
3327 Ok(meta) if meta.is_dir() => self.collect_loose_refs(&parent, parent_prefix, refs),
3328 Ok(_) => Ok(()),
3329 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
3330 Err(err) => Err(err.into()),
3331 }
3332 }
3333
3334 fn collect_loose_ref_names(
3335 &self,
3336 dir: &Path,
3337 prefix: &str,
3338 names: &mut BTreeSet<String>,
3339 ) -> Result<()> {
3340 for entry in fs::read_dir(dir)? {
3341 let entry = entry?;
3342 let path = entry.path();
3343 let name = format!("{prefix}/{}", entry.file_name().to_string_lossy());
3344 if path.is_dir() {
3345 self.collect_loose_ref_names(&path, &name, names)?;
3346 } else if !name.ends_with(".lock") {
3347 names.insert(name);
3348 }
3349 }
3350 Ok(())
3351 }
3352
3353 fn collect_loose_ref_names_with_prefix(
3354 &self,
3355 prefix: &str,
3356 names: &mut BTreeSet<String>,
3357 ) -> Result<()> {
3358 self.collect_loose_ref_names_with_prefix_from_base(&self.storage_dir, prefix, names)
3359 }
3360
3361 fn collect_current_worktree_loose_ref_names_with_prefix(
3362 &self,
3363 prefix: &str,
3364 names: &mut BTreeSet<String>,
3365 ) -> Result<()> {
3366 let base = self.current_worktree_storage_dir();
3367 if base == self.storage_dir {
3368 return Ok(());
3369 }
3370 for namespace in CURRENT_WORKTREE_REF_PREFIXES {
3371 if ref_prefixes_overlap(prefix, namespace) {
3372 let scan_prefix = if prefix.starts_with(namespace) {
3373 prefix
3374 } else {
3375 namespace
3376 };
3377 self.collect_loose_ref_names_with_prefix_from_base(&base, scan_prefix, names)?;
3378 }
3379 }
3380 Ok(())
3381 }
3382
3383 fn collect_loose_ref_names_with_prefix_from_base(
3384 &self,
3385 base: &Path,
3386 prefix: &str,
3387 names: &mut BTreeSet<String>,
3388 ) -> Result<()> {
3389 if !safe_ref_prefix_for_directory_scan(prefix) {
3390 return self.collect_all_loose_ref_names_from_base(base, names);
3391 }
3392
3393 let trimmed = prefix.trim_end_matches('/');
3394 if prefix.ends_with('/') {
3395 let candidate = base.join(trimmed);
3396 match fs::metadata(&candidate) {
3397 Ok(meta) if meta.is_dir() => {
3398 self.collect_loose_ref_names(&candidate, trimmed, names)?;
3399 return Ok(());
3400 }
3401 Ok(_) => return Ok(()),
3402 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
3403 Err(err) => return Err(err.into()),
3404 }
3405 }
3406
3407 let Some((parent_prefix, _)) = trimmed.rsplit_once('/') else {
3408 return self.collect_all_loose_ref_names_from_base(base, names);
3409 };
3410 let parent = base.join(parent_prefix);
3411 match fs::metadata(&parent) {
3412 Ok(meta) if meta.is_dir() => {
3413 self.collect_loose_ref_names(&parent, parent_prefix, names)
3414 }
3415 Ok(_) => Ok(()),
3416 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
3417 Err(err) => Err(err.into()),
3418 }
3419 }
3420
3421 fn collect_all_loose_ref_names_from_base(
3422 &self,
3423 base: &Path,
3424 names: &mut BTreeSet<String>,
3425 ) -> Result<()> {
3426 let refs_dir = base.join("refs");
3427 if refs_dir.exists() {
3428 self.collect_loose_ref_names(&refs_dir, "refs", names)?;
3429 }
3430 Ok(())
3431 }
3432
3433 fn collect_all_loose_refs_from_base(
3434 &self,
3435 base: &Path,
3436 refs: &mut BTreeMap<String, Ref>,
3437 ) -> Result<()> {
3438 let refs_dir = base.join("refs");
3439 if refs_dir.exists() {
3440 self.collect_loose_refs(&refs_dir, "refs", refs)?;
3441 }
3442 Ok(())
3443 }
3444
3445 fn collect_reflog_names(
3446 &self,
3447 dir: &Path,
3448 prefix: &str,
3449 names: &mut BTreeSet<String>,
3450 ) -> Result<()> {
3451 let Ok(entries) = fs::read_dir(dir) else {
3452 return Ok(());
3453 };
3454 for entry in entries {
3455 let entry = entry?;
3456 let path = entry.path();
3457 let name = format!("{prefix}/{}", entry.file_name().to_string_lossy());
3458 if path.is_dir() {
3459 self.collect_reflog_names(&path, &name, names)?;
3460 } else if let Some(name) = name.strip_prefix("logs/") {
3461 names.insert(name.to_string());
3462 }
3463 }
3464 Ok(())
3465 }
3466
3467 fn loose_refs_have_prefix(&self, prefix: &str) -> Result<bool> {
3468 if !prefix.starts_with("refs/") || !prefix.ends_with('/') {
3469 return Ok(self
3470 .list_refs()?
3471 .iter()
3472 .any(|reference| reference.name.starts_with(prefix)));
3473 }
3474 let loose_prefix = prefix.trim_end_matches('/');
3475 let dir = self.common_dir.join(loose_prefix);
3476 match fs::metadata(&dir) {
3477 Ok(meta) if meta.is_dir() => {
3478 let mut refs = BTreeMap::new();
3479 self.collect_loose_refs(&dir, loose_prefix, &mut refs)?;
3480 Ok(!refs.is_empty())
3481 }
3482 Ok(_) => Ok(false),
3483 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
3484 Err(err) => Err(err.into()),
3485 }
3486 }
3487
3488 fn write_loose_ref(&self, reference: &Ref) -> Result<()> {
3489 if self.uses_reftable()? {
3490 let (store, name) = self.reftable_store_for_ref(&reference.name)?;
3491 store.append_reftable_records(vec![ReftableRefRecord {
3492 name,
3493 update_index: 0,
3494 value: reftable_value_from_ref_target(&reference.target),
3495 }])?;
3496 return Ok(());
3497 }
3498 let path = self.ref_path(&reference.name);
3499 let parent = path
3500 .parent()
3501 .ok_or_else(|| GitError::InvalidPath("ref path has no parent".into()))?;
3502 self.shared_repository.create_dir_all(parent)?;
3503 write_locked(
3504 &path,
3505 &write_loose_ref(reference),
3506 self.reference_fsync.method_if_enabled(),
3507 )?;
3508 self.shared_repository.adjust_file(&path)
3509 }
3510
3511 fn delete_loose_ref(&self, name: &str) -> Result<()> {
3512 let path = self.ref_path(name);
3513 let lock_path = lock_path_for(&path)?;
3514 {
3515 let mut file = fs::OpenOptions::new()
3516 .write(true)
3517 .create_new(true)
3518 .open(&lock_path)?;
3519 file.write_all(b"delete\n")?;
3520 if let Some(method) = self.reference_fsync.method_if_enabled() {
3521 sync_reference_file(&file, method)?;
3522 }
3523 }
3524 match fs::remove_file(&path) {
3525 Ok(()) => {
3526 fs::remove_file(lock_path)?;
3527 self.prune_empty_ref_dirs(name);
3528 Ok(())
3529 }
3530 Err(err) => {
3531 let _ = fs::remove_file(lock_path);
3532 Err(GitError::Io(err.to_string()))
3533 }
3534 }
3535 }
3536
3537 fn prune_loose_ref_after_pack(&self, name: &str, packed_oid: &ObjectId) -> Result<bool> {
3546 let path = self.ref_path(name);
3547 let lock_path = lock_path_for(&path)?;
3548 let lock = fs::OpenOptions::new()
3549 .write(true)
3550 .create_new(true)
3551 .open(&lock_path)?;
3552 let outcome = (|| -> Result<bool> {
3553 let bytes = match fs::read(&path) {
3554 Ok(bytes) => bytes,
3555 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false),
3556 Err(err) => return Err(err.into()),
3557 };
3558 let reference = match parse_loose_ref(self.format, name.to_string(), &bytes) {
3559 Ok(reference) => reference,
3560 Err(_) => return Ok(false),
3561 };
3562 if reference.target != RefTarget::Direct(*packed_oid) {
3563 return Ok(false);
3564 }
3565 fs::remove_file(&path)?;
3566 Ok(true)
3567 })();
3568 drop(lock);
3569 let cleanup = fs::remove_file(&lock_path);
3570 let deleted = outcome?;
3571 cleanup?;
3572 if deleted {
3573 self.prune_empty_ref_dirs(name);
3574 }
3575 Ok(deleted)
3576 }
3577
3578 fn prune_empty_ref_dirs(&self, name: &str) {
3584 let boundary = self.ref_prune_boundary(name);
3585 if let Some(parent) = self.ref_path(name).parent() {
3586 prune_empty_dirs_up_to(parent, &boundary);
3587 }
3588 }
3589
3590 fn ref_prune_boundary(&self, name: &str) -> PathBuf {
3591 let base = self.ref_base_dir(name).to_path_buf();
3592 let mut components = name.split('/');
3593 if components.next() == Some("refs")
3594 && let Some(namespace) = components.next()
3595 {
3596 return base.join("refs").join(namespace);
3597 }
3598 base
3599 }
3600
3601 fn remove_reflog_file(&self, name: &str) {
3607 if matches!(self.uses_reftable(), Ok(true)) {
3611 let _ = self.tombstone_reftable_logs(name);
3612 return;
3613 }
3614 let path = self.reflog_path(name);
3615 let _ = fs::remove_file(&path);
3616 let base = self.ref_base_dir(name).to_path_buf();
3617 let logs_refs_root = base.join("logs").join("refs");
3618 if let Some(parent) = path.parent() {
3619 prune_empty_dirs_up_to(parent, &logs_refs_root);
3620 }
3621 }
3622
3623 fn tombstone_reftable_logs(&self, name: &str) -> Result<()> {
3627 self.rewrite_reftable_logs(name, &[], false)
3628 }
3629
3630 fn head_reflog_mirror(
3642 head_branch: Option<&str>,
3643 reflogs: &[(String, ReflogEntry)],
3644 ) -> Vec<(String, ReflogEntry)> {
3645 let Some(head_branch) = head_branch else {
3646 return Vec::new();
3647 };
3648 if reflogs.iter().any(|(name, _)| name == "HEAD") {
3651 return Vec::new();
3652 }
3653 reflogs
3654 .iter()
3655 .filter(|(name, _)| name == head_branch)
3656 .map(|(_, entry)| ("HEAD".to_string(), entry.clone()))
3657 .collect()
3658 }
3659
3660 fn head_symref_target(&self) -> Option<String> {
3664 match self.read_ref("HEAD") {
3665 Ok(Some(RefTarget::Symbolic(branch))) => Some(branch),
3666 _ => None,
3667 }
3668 }
3669
3670 fn reftable_update_from_reflog(&self, entry: &ReflogEntry) -> Result<ReftableLogUpdate> {
3676 let mut update = reftable_update_from_reflog(entry)?;
3677 let block_size = if self.reftable_write_options.block_size == 0 {
3678 ReftableWriteOptions::default().block_size
3679 } else {
3680 self.reftable_write_options.block_size
3681 };
3682 let limit = (block_size / 2) as usize;
3683 if update.message.len() > limit {
3684 let mut end = limit;
3685 while !update.message.is_char_boundary(end) {
3686 end -= 1;
3687 }
3688 update.message.truncate(end);
3689 }
3690 Ok(update)
3691 }
3692
3693 pub fn append_reflog(&self, name: &str, entry: &ReflogEntry) -> Result<()> {
3694 validate_ref_name_for_read(name)?;
3695 if self.uses_reftable()? {
3696 let update = self.reftable_update_from_reflog(entry)?;
3697 self.append_reftable_table(
3698 Vec::new(),
3699 vec![ReftableLogRecord {
3700 refname: name.to_string(),
3701 update_index: 0,
3702 value: ReftableLogValue::Update(update),
3703 }],
3704 )?;
3705 self.auto_compact_reftable_stack()?;
3706 return Ok(());
3707 }
3708 let path = self.reflog_path(name);
3709 let parent = path
3710 .parent()
3711 .ok_or_else(|| GitError::InvalidPath("reflog path has no parent".into()))?;
3712 self.shared_repository.create_dir_all(parent)?;
3713 let mut file = fs::OpenOptions::new()
3714 .create(true)
3715 .append(true)
3716 .open(&path)?;
3717 file.write_all(&entry.to_line())?;
3718 drop(file);
3719 self.shared_repository.adjust_file(&path)
3720 }
3721
3722 fn append_loose_reflogs(&self, reflogs: Vec<(String, ReflogEntry)>) -> Result<()> {
3728 let mut prepared_parents = BTreeSet::new();
3729 for (name, entry) in reflogs {
3730 let path = self.reflog_path(&name);
3731 let parent = path
3732 .parent()
3733 .ok_or_else(|| GitError::InvalidPath("reflog path has no parent".into()))?;
3734 if prepared_parents.insert(parent.to_path_buf()) {
3735 self.shared_repository.create_dir_all(parent)?;
3736 }
3737 let mut file = fs::OpenOptions::new()
3738 .create(true)
3739 .append(true)
3740 .open(&path)?;
3741 file.write_all(&entry.to_line())?;
3742 drop(file);
3743 self.shared_repository.adjust_file(&path)?;
3744 }
3745 Ok(())
3746 }
3747
3748 fn rewrite_reftable_logs(
3756 &self,
3757 name: &str,
3758 entries: &[ReflogEntry],
3759 preserve_empty: bool,
3760 ) -> Result<()> {
3761 let mut live_indexes: BTreeSet<u64> = BTreeSet::new();
3764 let mut deleted_indexes: BTreeSet<u64> = BTreeSet::new();
3765 for table in self.reftables()? {
3766 for record in table.logs {
3767 if record.refname != name {
3768 continue;
3769 }
3770 match record.value {
3771 ReftableLogValue::Deletion => {
3772 deleted_indexes.insert(record.update_index);
3773 live_indexes.remove(&record.update_index);
3774 }
3775 ReftableLogValue::Update(_) => {
3776 live_indexes.insert(record.update_index);
3777 deleted_indexes.remove(&record.update_index);
3778 }
3779 }
3780 }
3781 }
3782
3783 let table_names = self.reftable_table_names()?;
3784 let base = self.next_reftable_update_index(&table_names)?;
3785 let mut logs: Vec<ReftableLogRecord> = Vec::new();
3786 for index in &live_indexes {
3788 logs.push(ReftableLogRecord {
3789 refname: name.to_string(),
3790 update_index: *index,
3791 value: ReftableLogValue::Deletion,
3792 });
3793 }
3794 for (offset, entry) in entries.iter().enumerate() {
3797 let update_index = base
3798 .checked_add(offset as u64)
3799 .ok_or_else(|| GitError::InvalidFormat("reftable update index overflow".into()))?;
3800 logs.push(ReftableLogRecord {
3801 refname: name.to_string(),
3802 update_index,
3803 value: ReftableLogValue::Update(self.reftable_update_from_reflog(entry)?),
3804 });
3805 }
3806 if entries.is_empty() && preserve_empty {
3807 let null = ObjectId::null(self.format);
3808 logs.push(ReftableLogRecord {
3809 refname: name.to_string(),
3810 update_index: base,
3811 value: ReftableLogValue::Update(ReftableLogUpdate {
3812 old_oid: null,
3813 new_oid: null,
3814 name: String::new(),
3815 email: String::new(),
3816 time: 0,
3817 tz_offset: 0,
3818 message: String::new(),
3819 }),
3820 });
3821 }
3822 if logs.is_empty() {
3823 return Ok(());
3824 }
3825 let leave_empty_rewrite_tombstones_separate = entries.is_empty();
3826 if leave_empty_rewrite_tombstones_separate
3827 && !reftable_autocompaction_disabled()
3828 && self.reftable_table_names()?.len() > 1
3829 {
3830 self.compact_reftable_stack()?;
3831 }
3832 self.append_reftable_table_spanning(Vec::new(), logs)?;
3833 if !leave_empty_rewrite_tombstones_separate {
3834 self.auto_compact_reftable_stack()?;
3835 }
3836 Ok(())
3837 }
3838
3839 fn append_reftable_table_spanning(
3844 &self,
3845 mut refs: Vec<ReftableRefRecord>,
3846 logs: Vec<ReftableLogRecord>,
3847 ) -> Result<u64> {
3848 let reftable_dir = self.storage_dir.join("reftable");
3849 fs::create_dir_all(&reftable_dir)?;
3850 let list_path = reftable_dir.join("tables.list");
3851 let list_lock = self.acquire_reftable_list_lock(list_path.clone())?;
3852 let mut table_names = self.reftable_table_names_from(&list_path)?;
3853 let alloc_index = self.next_reftable_update_index(&table_names)?;
3854 for record in &mut refs {
3855 record.update_index = alloc_index;
3856 }
3857 let min_index = alloc_index;
3863 let mut max_index = alloc_index;
3864 for record in &logs {
3865 max_index = max_index.max(record.update_index);
3866 }
3867 for record in &refs {
3868 max_index = max_index.max(record.update_index);
3869 }
3870 let table_name = reftable_table_name(min_index, max_index);
3871 let bytes = Reftable::write_with_options(
3872 self.format,
3873 min_index,
3874 max_index,
3875 &refs,
3876 &logs,
3877 self.reftable_write_options,
3878 )?;
3879 let table_path = reftable_dir.join(&table_name);
3880 write_locked(
3881 &table_path,
3882 &bytes,
3883 self.reference_fsync.method_if_enabled(),
3884 )?;
3885 self.apply_reftable_shared_file_mode(&table_path)?;
3886 table_names.push(table_name);
3887 let mut list = Vec::new();
3888 for name in &table_names {
3889 list.extend_from_slice(name.as_bytes());
3890 list.push(b'\n');
3891 }
3892 list_lock.commit(&list, self.reference_fsync.method_if_enabled())?;
3893 self.apply_reftable_shared_file_mode(&list_path)?;
3894 Ok(max_index)
3895 }
3896
3897 fn ref_path(&self, name: &str) -> PathBuf {
3898 self.ref_base_dir(name).join(name)
3899 }
3900
3901 fn display_ref_path(&self, name: &str) -> String {
3902 self.display_storage_path(&self.ref_path(name))
3903 }
3904
3905 fn display_storage_path(&self, path: &Path) -> String {
3906 if let Ok(relative) = path.strip_prefix(&self.git_dir) {
3907 let prefix = self
3908 .git_dir
3909 .file_name()
3910 .and_then(|name| name.to_str())
3911 .unwrap_or(".git");
3912 return format!("{prefix}/{}", relative.to_string_lossy().replace('\\', "/"));
3913 }
3914 path.to_string_lossy().into_owned()
3915 }
3916
3917 fn reflog_path(&self, name: &str) -> PathBuf {
3918 self.ref_base_dir(name).join("logs").join(name)
3919 }
3920
3921 fn ref_base_dir(&self, name: &str) -> PathBuf {
3922 if current_worktree_ref(name) {
3923 self.current_worktree_storage_dir()
3924 } else {
3925 self.storage_dir.clone()
3926 }
3927 }
3928
3929 fn check_ref_directory_conflict_targeted(&self, name: &str) -> Result<()> {
3930 match self.refname_directory_conflict(name)? {
3931 Some(conflict) => Err(ref_directory_conflict_error(name, &conflict)),
3932 None => Ok(()),
3933 }
3934 }
3935
3936 pub fn refname_directory_conflict(&self, name: &str) -> Result<Option<String>> {
3942 let components = name.split('/').collect::<Vec<_>>();
3943 let mut ancestors = Vec::new();
3944 for index in 1..components.len() {
3945 let ancestor = components[..index].join("/");
3946 if self.loose_ref_file_exists_for_conflict(&ancestor)? {
3947 return Ok(Some(ancestor));
3948 }
3949 ancestors.push(ancestor);
3950 }
3951 let child_prefix = format!("{name}/");
3952 if let Some(existing) = self.first_loose_ref_name_with_prefix(&child_prefix)? {
3953 return Ok(Some(existing));
3954 }
3955 if let Some(existing) =
3956 self.first_packed_ref_directory_conflict(&ancestors, &child_prefix)?
3957 {
3958 return Ok(Some(existing));
3959 }
3960 Ok(None)
3961 }
3962
3963 fn loose_ref_file_exists_for_conflict(&self, name: &str) -> Result<bool> {
3964 let path = self.ref_path(name);
3965 match fs::symlink_metadata(path) {
3966 Ok(meta) if meta.is_dir() => Ok(false),
3967 Ok(meta)
3968 if meta.file_type().is_symlink()
3969 && fs::metadata(self.ref_path(name)).is_ok_and(|target| target.is_dir()) =>
3970 {
3971 Ok(false)
3972 }
3973 Ok(_) => {
3974 let path = self.ref_path(name);
3975 if let Ok(bytes) = fs::read(path)
3976 && loose_ref_bytes_are_reftable_sentinel(name, &bytes)
3977 {
3978 return Ok(false);
3979 }
3980 Ok(true)
3981 }
3982 Err(err)
3983 if err.kind() == std::io::ErrorKind::NotFound
3984 || err.kind() == std::io::ErrorKind::NotADirectory =>
3985 {
3986 Ok(false)
3987 }
3988 Err(err) => Err(err.into()),
3989 }
3990 }
3991
3992 fn first_packed_ref_directory_conflict(
3993 &self,
3994 ancestors: &[String],
3995 child_prefix: &str,
3996 ) -> Result<Option<String>> {
3997 let packed_path = self.storage_dir.join("packed-refs");
3998 let text = match fs::read_to_string(packed_path) {
3999 Ok(text) => text,
4000 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
4001 Err(err) => return Err(err.into()),
4002 };
4003 for raw_line in text.lines() {
4004 let line = raw_line.trim_end();
4005 if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
4006 continue;
4007 }
4008 let (_, name) = line
4009 .split_once(' ')
4010 .ok_or_else(|| packed_refs_unexpected_line(line))?;
4011 if ancestors.iter().any(|ancestor| ancestor == name) || name.starts_with(child_prefix) {
4012 return Ok(Some(name.to_owned()));
4013 }
4014 }
4015 Ok(None)
4016 }
4017
4018 fn first_loose_ref_name_with_prefix(&self, prefix: &str) -> Result<Option<String>> {
4019 if !prefix.starts_with("refs/") || !prefix.ends_with('/') {
4020 return Ok(None);
4021 }
4022 let trimmed = prefix.trim_end_matches('/');
4023 let dir = self.ref_base_dir(trimmed).join(trimmed);
4024 self.first_loose_ref_name_in_dir(&dir, trimmed)
4025 }
4026
4027 fn first_loose_ref_name_in_dir(&self, dir: &Path, prefix: &str) -> Result<Option<String>> {
4028 let entries = match fs::read_dir(dir) {
4029 Ok(entries) => entries,
4030 Err(err)
4031 if err.kind() == std::io::ErrorKind::NotFound
4032 || err.kind() == std::io::ErrorKind::NotADirectory =>
4033 {
4034 return Ok(None);
4035 }
4036 Err(err) => return Err(err.into()),
4037 };
4038 for entry in entries {
4039 let entry = entry?;
4040 let path = entry.path();
4041 let name = format!("{prefix}/{}", entry.file_name().to_string_lossy());
4042 if path.is_dir() {
4043 if let Some(found) = self.first_loose_ref_name_in_dir(&path, &name)? {
4044 return Ok(Some(found));
4045 }
4046 } else if !name.ends_with(".lock") {
4047 return Ok(Some(name));
4048 }
4049 }
4050 Ok(None)
4051 }
4052}
4053
4054fn reftable_ref_target(value: ReftableRefValue) -> Result<Option<RefTarget>> {
4055 match value {
4056 ReftableRefValue::Deletion => Ok(None),
4057 ReftableRefValue::Direct(oid) | ReftableRefValue::Peeled { target: oid, .. } => {
4058 Ok(Some(RefTarget::Direct(oid)))
4059 }
4060 ReftableRefValue::Symbolic(target) => Ok(Some(RefTarget::Symbolic(target))),
4061 }
4062}
4063
4064fn reftable_value_from_ref_target(target: &RefTarget) -> ReftableRefValue {
4065 match target {
4066 RefTarget::Direct(oid) => ReftableRefValue::Direct(*oid),
4067 RefTarget::Symbolic(target) => ReftableRefValue::Symbolic(target.clone()),
4068 }
4069}
4070
4071fn reflog_entry_from_reftable(update: ReftableLogUpdate) -> ReflogEntry {
4078 let committer = format!(
4079 "{} <{}> {} {}",
4080 update.name,
4081 update.email,
4082 update.time,
4083 format_reflog_tz(update.tz_offset),
4084 );
4085 let mut message = update.message.into_bytes();
4090 if message.last() == Some(&b'\n') {
4091 message.pop();
4092 }
4093 ReflogEntry {
4094 old_oid: update.old_oid,
4095 new_oid: update.new_oid,
4096 committer: committer.into_bytes(),
4097 message,
4098 }
4099}
4100
4101fn reftable_log_update_is_empty_marker(update: &ReftableLogUpdate, null: ObjectId) -> bool {
4102 update.old_oid == null && update.new_oid == null
4103}
4104
4105fn reftable_update_from_reflog(entry: &ReflogEntry) -> Result<ReftableLogUpdate> {
4108 let committer = std::str::from_utf8(&entry.committer)
4109 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
4110 let (name, email, time, tz_offset) = split_committer_ident(committer)?;
4111 let mut message = std::str::from_utf8(&entry.message)
4112 .map_err(|err| GitError::InvalidFormat(err.to_string()))?
4113 .to_string();
4114 if !message.ends_with('\n') {
4118 message.push('\n');
4119 }
4120 Ok(ReftableLogUpdate {
4121 old_oid: entry.old_oid,
4122 new_oid: entry.new_oid,
4123 name,
4124 email,
4125 time,
4126 tz_offset,
4127 message,
4128 })
4129}
4130
4131fn split_committer_ident(committer: &str) -> Result<(String, String, u64, i16)> {
4136 let open = committer.find(" <").ok_or_else(|| {
4137 GitError::InvalidFormat("reflog committer is missing email opener".into())
4138 })?;
4139 let name = committer[..open].to_string();
4140 let after_open = open + 2;
4141 let close = committer[after_open..].find('>').ok_or_else(|| {
4142 GitError::InvalidFormat("reflog committer is missing email closer".into())
4143 })?;
4144 let email = committer[after_open..after_open + close].to_string();
4145 let rest = committer[after_open + close + 1..].trim();
4146 let (time_str, tz_str) = rest.split_once(' ').ok_or_else(|| {
4147 GitError::InvalidFormat("reflog committer is missing timestamp/timezone".into())
4148 })?;
4149 let time = time_str
4150 .trim()
4151 .parse::<u64>()
4152 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
4153 let tz_offset = parse_reflog_tz(tz_str.trim())?;
4154 Ok((name, email, time, tz_offset))
4155}
4156
4157fn format_reflog_tz(tz_offset: i16) -> String {
4160 let sign = if tz_offset < 0 { '-' } else { '+' };
4161 let magnitude = tz_offset.unsigned_abs();
4162 format!("{sign}{magnitude:04}")
4163}
4164
4165fn parse_reflog_tz(tz: &str) -> Result<i16> {
4168 let (sign, digits) = match tz.strip_prefix('-') {
4169 Some(rest) => (-1i16, rest),
4170 None => (1i16, tz.strip_prefix('+').unwrap_or(tz)),
4171 };
4172 let magnitude = digits
4173 .parse::<i16>()
4174 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
4175 Ok(sign * magnitude)
4176}
4177
4178fn reftable_table_name(min_update_index: u64, max_update_index: u64) -> String {
4184 let nanos = SystemTime::now()
4185 .duration_since(UNIX_EPOCH)
4186 .map(|duration| duration.as_nanos())
4187 .unwrap_or(0);
4188 let salt = (nanos as u64) ^ (u64::from(std::process::id()) << 16);
4191 format!(
4192 "0x{min_update_index:012x}-0x{max_update_index:012x}-{:08x}.ref",
4193 salt as u32
4194 )
4195}
4196
4197fn reftable_autocompaction_disabled() -> bool {
4198 std::env::var("GIT_TEST_REFTABLE_AUTOCOMPACTION")
4199 .is_ok_and(|value| value.eq_ignore_ascii_case("false"))
4200}
4201
4202fn reftable_compaction_segment(sizes: &[u64], factor: u64) -> Option<(usize, usize)> {
4205 if sizes.len() <= 1 {
4206 return None;
4207 }
4208 let factor = if factor == 0 { 2 } else { factor };
4209
4210 let mut pivot = None;
4211 let mut end = 0;
4212 for index in (1..sizes.len()).rev() {
4213 if sizes[index - 1] < sizes[index].saturating_mul(factor) {
4214 pivot = Some(index);
4215 end = index + 1;
4216 break;
4217 }
4218 }
4219 let mut index = pivot?;
4220 let mut bytes = sizes[index];
4221 let mut start = index;
4222 while index > 0 {
4223 let current = bytes;
4224 bytes = bytes.saturating_add(sizes[index - 1]);
4225 if sizes[index - 1] < current.saturating_mul(factor) {
4226 start = index - 1;
4227 }
4228 index -= 1;
4229 }
4230 (start < end).then_some((start, end))
4231}
4232
4233#[cfg(test)]
4238fn reftable_table_name_is_valid(name: &str) -> bool {
4239 fn hex_prefix(s: &str) -> Option<&str> {
4240 let body = s
4242 .strip_prefix("0x")
4243 .or_else(|| s.strip_prefix("0X"))
4244 .unwrap_or(s);
4245 let consumed = body
4246 .find(|c: char| !c.is_ascii_hexdigit())
4247 .unwrap_or(body.len());
4248 if consumed == 0 {
4249 return None;
4250 }
4251 Some(&body[consumed..])
4252 }
4253 let Some(rest) = hex_prefix(name) else {
4254 return false;
4255 };
4256 let Some(rest) = rest.strip_prefix('-') else {
4257 return false;
4258 };
4259 let Some(rest) = hex_prefix(rest) else {
4260 return false;
4261 };
4262 let Some(rest) = rest.strip_prefix('-') else {
4263 return false;
4264 };
4265 let Some(rest) = hex_prefix(rest) else {
4266 return false;
4267 };
4268 rest == ".ref" || rest == ".log"
4269}
4270
4271fn repository_common_dir(git_dir: &Path) -> PathBuf {
4272 if let Some(common_dir) = std::env::var_os("GIT_COMMON_DIR") {
4273 return PathBuf::from(common_dir);
4274 }
4275 let commondir = git_dir.join("commondir");
4276 if let Ok(value) = fs::read_to_string(&commondir) {
4277 let path = PathBuf::from(value.trim());
4278 let common = if path.is_absolute() {
4279 path
4280 } else {
4281 git_dir.join(path)
4282 };
4283 return fs::canonicalize(&common).unwrap_or(common);
4284 }
4285 git_dir.to_path_buf()
4286}
4287
4288fn log_all_ref_updates_matches(name: &str, value: &str) -> bool {
4289 if value.eq_ignore_ascii_case("always") {
4290 return true;
4291 }
4292 if !sley_config::parse_config_bool(value).unwrap_or(false) {
4293 return false;
4294 }
4295 name == "HEAD"
4296 || name.starts_with("refs/heads/")
4297 || name.starts_with("refs/remotes/")
4298 || name.starts_with("refs/notes/")
4299}
4300
4301#[derive(Clone, Copy, PartialEq, Eq, Debug)]
4305pub enum RefTransactionPhase {
4306 Preparing,
4309 Prepared,
4312 Committed,
4314 Aborted,
4316}
4317
4318impl RefTransactionPhase {
4319 pub fn as_str(self) -> &'static str {
4321 match self {
4322 RefTransactionPhase::Preparing => "preparing",
4323 RefTransactionPhase::Prepared => "prepared",
4324 RefTransactionPhase::Committed => "committed",
4325 RefTransactionPhase::Aborted => "aborted",
4326 }
4327 }
4328}
4329
4330#[derive(Clone, Debug)]
4336pub struct RefTransactionHookUpdate {
4337 pub old_value: String,
4338 pub new_value: String,
4339 pub refname: String,
4340}
4341
4342pub trait ReferenceTransactionHook {
4354 fn run(&self, phase: RefTransactionPhase, updates: &[RefTransactionHookUpdate])
4355 -> Result<bool>;
4356}
4357
4358pub struct FileRefTransaction<'a> {
4359 store: &'a FileRefStore,
4360 changes: Vec<QueuedRefChange>,
4361 hook: Option<&'a dyn ReferenceTransactionHook>,
4362}
4363
4364struct QueuedUpdate {
4367 name: String,
4368 precondition: RefPrecondition,
4369 new: RefTarget,
4370 reflog: Option<ReflogEntry>,
4371}
4372
4373struct QueuedDelete {
4374 name: String,
4375 precondition: RefDeletePrecondition,
4376}
4377
4378enum QueuedRefChange {
4379 Update(QueuedUpdate),
4380 Delete(QueuedDelete),
4381}
4382
4383#[derive(Debug, Clone, PartialEq, Eq)]
4385pub enum RefDeletePrecondition {
4386 Any,
4388 Immediate(RefTarget),
4390 Direct(Option<ObjectId>),
4392 Peeled(ObjectId),
4394}
4395
4396impl<'a> FileRefTransaction<'a> {
4397 pub fn with_hook(mut self, hook: &'a dyn ReferenceTransactionHook) -> Self {
4403 self.hook = Some(hook);
4404 self
4405 }
4406
4407 pub fn update(&mut self, update: RefUpdate) {
4412 self.changes.push(QueuedRefChange::Update(QueuedUpdate {
4413 name: update.name,
4414 precondition: RefPrecondition::from_expected(update.expected),
4415 new: update.new,
4416 reflog: update.reflog,
4417 }));
4418 }
4419
4420 pub fn update_to(
4426 &mut self,
4427 name: impl Into<String>,
4428 new: RefTarget,
4429 precondition: RefPrecondition,
4430 reflog: Option<ReflogEntry>,
4431 ) {
4432 self.changes.push(QueuedRefChange::Update(QueuedUpdate {
4433 name: name.into(),
4434 precondition,
4435 new,
4436 reflog,
4437 }));
4438 }
4439
4440 pub fn delete(&mut self, delete: DeleteRef) {
4445 self.delete_with_precondition(
4446 delete.name,
4447 RefDeletePrecondition::Direct(delete.expected_old),
4448 delete.reflog,
4449 );
4450 }
4451
4452 pub fn delete_with_precondition(
4458 &mut self,
4459 name: impl Into<String>,
4460 precondition: RefDeletePrecondition,
4461 _reflog: Option<DeleteRefReflog>,
4462 ) {
4463 self.changes.push(QueuedRefChange::Delete(QueuedDelete {
4464 name: name.into(),
4465 precondition,
4466 }));
4467 }
4468
4469 pub fn commit(self) -> Result<()> {
4490 if env::var_os("GIT_QUARANTINE_PATH").is_some() {
4491 return Err(GitError::Transaction(
4492 "ref updates forbidden inside quarantine environment".into(),
4493 ));
4494 }
4495 let FileRefTransaction {
4496 store,
4497 changes,
4498 hook,
4499 } = self;
4500 let changes = coalesce_ref_changes(changes)?;
4501 let hook_updates = hook.map(|_| hook_updates_for_changes(store.format, &changes));
4506 if let (Some(hook), Some(updates)) = (hook, hook_updates.as_ref())
4507 && hook.run(RefTransactionPhase::Preparing, updates)?
4508 {
4509 return Err(ref_transaction_hook_abort(RefTransactionPhase::Preparing));
4510 }
4511 if store.uses_reftable()? {
4512 store.commit_reftable(changes)
4513 } else {
4514 store.commit_loose_hooked(changes, hook, hook_updates.as_deref())
4515 }
4516 }
4517}
4518
4519fn ref_transaction_hook_abort(phase: RefTransactionPhase) -> GitError {
4522 GitError::Transaction(format!(
4523 "in '{}' phase, update aborted by the reference-transaction hook",
4524 phase.as_str()
4525 ))
4526}
4527
4528fn hook_updates_for_changes(
4534 format: ObjectFormat,
4535 changes: &[CoalescedRefChange],
4536) -> Vec<RefTransactionHookUpdate> {
4537 let zero = ObjectId::null(format).to_string();
4538 changes
4539 .iter()
4540 .map(|change| match change {
4541 CoalescedRefChange::Update(update) => RefTransactionHookUpdate {
4542 old_value: hook_old_value(&zero, &update.precondition),
4543 new_value: hook_target_value(&zero, Some(&update.new)),
4544 refname: update.name.clone(),
4545 },
4546 CoalescedRefChange::Delete(delete) => RefTransactionHookUpdate {
4547 old_value: hook_delete_old_value(&zero, &delete.precondition),
4548 new_value: zero.clone(),
4549 refname: delete.name.clone(),
4550 },
4551 })
4552 .collect()
4553}
4554
4555fn hook_old_value(zero: &str, precondition: &RefPrecondition) -> String {
4559 match precondition {
4560 RefPrecondition::Any | RefPrecondition::MustExist => zero.to_string(),
4561 RefPrecondition::MustNotExist => zero.to_string(),
4562 RefPrecondition::MustExistAndMatch(target) | RefPrecondition::ExistingMustMatch(target) => {
4563 hook_target_value(zero, Some(target))
4564 }
4565 }
4566}
4567
4568fn hook_delete_old_value(zero: &str, precondition: &RefDeletePrecondition) -> String {
4571 match precondition {
4572 RefDeletePrecondition::Any => zero.to_string(),
4573 RefDeletePrecondition::Immediate(target) => hook_target_value(zero, Some(target)),
4574 RefDeletePrecondition::Direct(Some(oid)) | RefDeletePrecondition::Peeled(oid) => {
4575 oid.to_string()
4576 }
4577 RefDeletePrecondition::Direct(None) => zero.to_string(),
4578 }
4579}
4580
4581fn hook_target_value(zero: &str, target: Option<&RefTarget>) -> String {
4584 match target {
4585 None => zero.to_string(),
4586 Some(RefTarget::Direct(oid)) => oid.to_string(),
4587 Some(RefTarget::Symbolic(name)) => format!("ref:{name}"),
4588 }
4589}
4590
4591impl FileRefStore {
4592 fn commit_reftable(&self, changes: Vec<CoalescedRefChange>) -> Result<()> {
4593 let routed = self.route_reftable_changes(changes)?;
4594 if routed.len() != 1 || routed[0].0.storage_dir != self.storage_dir {
4595 for (store, changes) in routed {
4596 store.commit_reftable_local(changes)?;
4597 }
4598 return Ok(());
4599 }
4600 let mut routed = routed;
4601 if let Some((_, changes)) = routed.pop() {
4602 self.commit_reftable_local(changes)
4603 } else {
4604 Ok(())
4605 }
4606 }
4607
4608 fn commit_reftable_local(&self, changes: Vec<CoalescedRefChange>) -> Result<()> {
4609 let has_reflogs = changes.iter().any(|change| {
4611 matches!(change, CoalescedRefChange::Update(update) if !update.reflog.is_empty())
4612 });
4613 let head_branch = if has_reflogs {
4614 self.head_symref_target()
4615 } else {
4616 None
4617 };
4618 if changes
4619 .iter()
4620 .any(|change| matches!(change, CoalescedRefChange::Update(_)))
4621 {
4622 let mut names = self
4623 .list_refs()?
4624 .into_iter()
4625 .map(|reference| reference.name)
4626 .collect::<BTreeSet<_>>();
4627 for change in &changes {
4628 if let CoalescedRefChange::Update(update) = change {
4629 names.insert(update.name.clone());
4630 }
4631 }
4632 for change in &changes {
4633 if let CoalescedRefChange::Update(update) = change {
4634 check_ref_directory_conflict_in_names(&update.name, &names)?;
4635 }
4636 }
4637 }
4638 let mut records = Vec::with_capacity(changes.len());
4639 let mut reflogs = Vec::new();
4640 let mut delete_names = Vec::new();
4641 for change in changes {
4642 match change {
4643 CoalescedRefChange::Update(update) => {
4644 if !matches!(update.precondition, RefPrecondition::Any) {
4645 let current = self.read_ref(&update.name)?;
4646 if !update.precondition.is_satisfied_by(current.as_ref()) {
4647 return Err(GitError::Transaction(
4648 update.precondition.describe(&update.name),
4649 ));
4650 }
4651 }
4652 records.push(ReftableRefRecord {
4653 name: update.name.clone(),
4654 update_index: 0,
4655 value: reftable_value_from_ref_target(&update.new),
4656 });
4657 for entry in update.reflog {
4658 reflogs.push((update.name.clone(), entry));
4659 }
4660 }
4661 CoalescedRefChange::Delete(delete) => {
4662 let current = self.read_ref(&delete.name)?;
4663 verify_delete_precondition(
4667 self,
4668 &delete.name,
4669 current.as_ref(),
4670 &delete.precondition,
4671 )?;
4672 records.push(ReftableRefRecord {
4673 name: delete.name.clone(),
4674 update_index: 0,
4675 value: ReftableRefValue::Deletion,
4676 });
4677 delete_names.push(delete.name.clone());
4678 }
4679 }
4680 }
4681 if self.combine_reftable_logs || self.git_dir != self.common_dir {
4682 let head_mirror = Self::head_reflog_mirror(head_branch.as_deref(), &reflogs);
4683 reflogs.extend(head_mirror);
4684 let log_records = reflogs
4685 .into_iter()
4686 .map(|(name, entry)| {
4687 Ok(ReftableLogRecord {
4688 refname: name,
4689 update_index: 0,
4690 value: ReftableLogValue::Update(self.reftable_update_from_reflog(&entry)?),
4691 })
4692 })
4693 .collect::<Result<Vec<_>>>()?;
4694 self.append_reftable_table_with_compaction(records, log_records, false)?;
4695 for name in &delete_names {
4696 self.remove_reflog_file(name);
4697 }
4698 if self.storage_dir == self.shared_reftable_storage_dir() {
4704 self.auto_compact_reftable_stack()?;
4705 }
4706 return Ok(());
4707 }
4708 self.append_reftable_records(records)?;
4709 for name in &delete_names {
4713 self.remove_reflog_file(name);
4714 }
4715 let head_mirror = Self::head_reflog_mirror(head_branch.as_deref(), &reflogs);
4716 reflogs.extend(head_mirror);
4717 for (name, entry) in reflogs {
4718 self.append_reflog(&name, &entry)?;
4719 }
4720 Ok(())
4721 }
4722
4723 fn route_reftable_changes(
4724 &self,
4725 changes: Vec<CoalescedRefChange>,
4726 ) -> Result<Vec<(FileRefStore, Vec<CoalescedRefChange>)>> {
4727 let mut grouped = BTreeMap::<PathBuf, (FileRefStore, Vec<CoalescedRefChange>)>::new();
4728 for change in changes {
4729 let name = match &change {
4730 CoalescedRefChange::Update(update) => update.name.as_str(),
4731 CoalescedRefChange::Delete(delete) => delete.name.as_str(),
4732 };
4733 let (store, rewritten) = self.reftable_store_for_ref(name)?;
4734 let rewritten_change = match change {
4735 CoalescedRefChange::Update(mut update) => {
4736 update.name = rewritten;
4737 CoalescedRefChange::Update(update)
4738 }
4739 CoalescedRefChange::Delete(mut delete) => {
4740 delete.name = rewritten;
4741 CoalescedRefChange::Delete(delete)
4742 }
4743 };
4744 grouped
4745 .entry(store.storage_dir.clone())
4746 .or_insert_with(|| (store, Vec::new()))
4747 .1
4748 .push(rewritten_change);
4749 }
4750 Ok(grouped.into_values().collect())
4751 }
4752
4753 #[allow(dead_code)]
4756 fn commit_loose(&self, changes: Vec<CoalescedRefChange>) -> Result<()> {
4757 self.commit_loose_hooked(changes, None, None)
4758 }
4759
4760 fn commit_loose_hooked(
4766 &self,
4767 changes: Vec<CoalescedRefChange>,
4768 hook: Option<&dyn ReferenceTransactionHook>,
4769 hook_updates: Option<&[RefTransactionHookUpdate]>,
4770 ) -> Result<()> {
4771 let has_reflogs = changes.iter().any(|change| {
4773 matches!(change, CoalescedRefChange::Update(update) if !update.reflog.is_empty())
4774 });
4775 let head_branch = if has_reflogs {
4776 self.head_symref_target()
4777 } else {
4778 None
4779 };
4780 let has_delete = changes
4781 .iter()
4782 .any(|change| matches!(change, CoalescedRefChange::Delete(_)));
4783 let update_count = changes
4784 .iter()
4785 .filter(|change| matches!(change, CoalescedRefChange::Update(_)))
4786 .count();
4787 let targeted_conflict_check = update_count == 1 && !has_delete;
4788 let conflict_names = if update_count > 0 && !targeted_conflict_check {
4789 let mut names = self
4790 .list_refs()?
4791 .into_iter()
4792 .map(|reference| reference.name)
4793 .collect::<BTreeSet<_>>();
4794 for change in &changes {
4795 if let CoalescedRefChange::Update(update) = change {
4796 names.insert(update.name.clone());
4797 }
4798 }
4799 Some(names)
4800 } else {
4801 None
4802 };
4803 let mut pending = Vec::with_capacity(changes.len() + usize::from(has_delete));
4804 let mut prepared_parents = BTreeSet::new();
4805 for change in &changes {
4807 let name = change.name();
4808 if matches!(change, CoalescedRefChange::Update(_)) {
4809 let conflict_result = if targeted_conflict_check {
4810 self.check_ref_directory_conflict_targeted(name)
4811 } else if let Some(conflict_names) = conflict_names.as_ref() {
4812 check_ref_directory_conflict_in_names(name, conflict_names)
4813 } else {
4814 Ok(())
4815 };
4816 if let Err(err) = conflict_result {
4817 release_pending_locks(&pending);
4818 return Err(err);
4819 }
4820 }
4821 let path = self.ref_path(name);
4822 let parent = path
4823 .parent()
4824 .ok_or_else(|| GitError::InvalidPath("ref path has no parent".into()))?;
4825 if prepared_parents.insert(parent.to_path_buf())
4826 && let Err(err) = fs::create_dir_all(parent)
4827 {
4828 release_pending_locks(&pending);
4829 if err.kind() == std::io::ErrorKind::NotADirectory {
4830 return Err(ref_directory_conflict_error(
4831 name,
4832 &parent_to_ref_name(&self.ref_base_dir(name), parent),
4833 ));
4834 }
4835 return Err(GitError::Io(err.to_string()));
4836 }
4837 let lock_path = match lock_path_for(&path) {
4838 Ok(lock_path) => lock_path,
4839 Err(err) => {
4840 release_pending_locks(&pending);
4841 return Err(err);
4842 }
4843 };
4844 let action = match change {
4845 CoalescedRefChange::Update(update)
4846 if self.prefer_symlink_refs && matches!(update.new, RefTarget::Symbolic(_)) =>
4847 {
4848 let RefTarget::Symbolic(target) = &update.new else {
4849 unreachable!("guard requires a symbolic target")
4850 };
4851 PendingPathAction::WriteSymbolic {
4852 target: target.clone(),
4853 fallback_contents: write_loose_ref(&Ref {
4854 name: update.name.clone(),
4855 target: update.new.clone(),
4856 }),
4857 }
4858 }
4859 CoalescedRefChange::Update(update) => PendingPathAction::Write {
4860 contents: write_loose_ref(&Ref {
4861 name: update.name.clone(),
4862 target: update.new.clone(),
4863 }),
4864 },
4865 CoalescedRefChange::Delete(_) => PendingPathAction::Delete,
4866 };
4867 let mut lock_file = match fs::OpenOptions::new()
4868 .write(true)
4869 .create_new(true)
4870 .open(&lock_path)
4871 {
4872 Ok(file) => file,
4873 Err(err) => {
4874 release_pending_locks(&pending);
4875 return Err(GitError::Io(format!("could not lock ref {name}: {err}")));
4876 }
4877 };
4878 let stage_result = match &action {
4883 PendingPathAction::Write { contents } => lock_file.write_all(contents),
4884 PendingPathAction::WriteSymbolic { .. } => Ok(()),
4885 PendingPathAction::Delete => lock_file.write_all(b"delete\n"),
4886 PendingPathAction::ReleaseLock => Ok(()),
4887 };
4888 drop(lock_file);
4889 if let Err(err) = stage_result {
4890 let _ = fs::remove_file(&lock_path);
4891 release_pending_locks(&pending);
4892 return Err(err.into());
4893 }
4894 let staged = !matches!(action, PendingPathAction::WriteSymbolic { .. });
4895 pending.push(PendingPathChange {
4896 name: name.to_string(),
4897 display_path: self.display_ref_path(name),
4898 path,
4899 lock_path,
4900 original: OriginalPathState::Missing,
4901 action,
4902 staged,
4903 });
4904 }
4905
4906 let packed_path = self.storage_dir.join("packed-refs");
4907 let mut packed_refs = Vec::new();
4908 let mut use_packed_snapshot = false;
4909 if has_delete {
4910 let packed_lock_path = match lock_path_for(&packed_path) {
4911 Ok(lock_path) => lock_path,
4912 Err(err) => {
4913 release_pending_locks(&pending);
4914 return Err(err);
4915 }
4916 };
4917 if let Err(err) = acquire_path_lock_with_timeout(
4918 &packed_lock_path,
4919 self.packed_refs_lock_timeout_millis,
4920 ) {
4921 release_pending_locks(&pending);
4922 return Err(err);
4923 }
4924 let packed_original = match fs::read(&packed_path) {
4925 Ok(bytes) => Some(bytes),
4926 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
4927 Err(err) => {
4928 release_pending_locks(&pending);
4929 let _ = fs::remove_file(&packed_lock_path);
4930 return Err(GitError::Io(err.to_string()));
4931 }
4932 };
4933 packed_refs = match &packed_original {
4934 Some(bytes) => match parse_packed_refs(self.format, bytes) {
4935 Ok(refs) => refs,
4936 Err(err) => {
4937 release_pending_locks(&pending);
4938 let _ = fs::remove_file(&packed_lock_path);
4939 return Err(err);
4940 }
4941 },
4942 None => Vec::new(),
4943 };
4944 use_packed_snapshot = true;
4945 pending.push(PendingPathChange {
4946 name: "packed-refs".into(),
4947 display_path: self.display_storage_path(&packed_path),
4948 path: packed_path.clone(),
4949 lock_path: packed_lock_path,
4950 original: packed_original
4951 .as_ref()
4952 .map_or(OriginalPathState::Missing, |bytes| {
4953 OriginalPathState::File(bytes.clone())
4954 }),
4955 action: PendingPathAction::ReleaseLock,
4956 staged: false,
4957 });
4958 } else if packed_path.exists() {
4959 packed_refs = parse_packed_refs(self.format, &fs::read(&packed_path)?)?;
4960 use_packed_snapshot = true;
4961 }
4962 let packed_ref_targets = packed_refs
4963 .iter()
4964 .map(|reference| {
4965 (
4966 reference.reference.name.clone(),
4967 reference.reference.target.clone(),
4968 )
4969 })
4970 .collect::<HashMap<_, _>>();
4971
4972 let mut reflogs = Vec::new();
4976 let mut delete_names = BTreeSet::new();
4977 for index in 0..changes.len() {
4978 match &changes[index] {
4979 CoalescedRefChange::Update(update) => {
4980 let (current, has_loose) = if use_packed_snapshot {
4981 match self.read_locked_ref_state(&update.name, &packed_ref_targets) {
4982 Ok(state) => (state.current, state.has_loose),
4983 Err(err) => {
4984 release_pending_locks(&pending);
4985 return Err(err);
4986 }
4987 }
4988 } else {
4989 match self.read_loose_ref(&update.name) {
4990 Ok(loose) => {
4991 let has_loose = loose.is_some();
4992 (loose.map(|reference| reference.target), has_loose)
4993 }
4994 Err(err) => {
4995 release_pending_locks(&pending);
4996 return Err(err);
4997 }
4998 }
4999 };
5000 if !matches!(update.precondition, RefPrecondition::Any)
5001 && !update.precondition.is_satisfied_by(current.as_ref())
5002 {
5003 release_pending_locks(&pending);
5004 return Err(GitError::Transaction(
5005 update.precondition.describe(&update.name),
5006 ));
5007 }
5008 pending[index].original = if has_loose {
5009 match read_original_path_state(&pending[index].path) {
5010 Ok(original) => original,
5011 Err(err) => {
5012 release_pending_locks(&pending);
5013 return Err(err);
5014 }
5015 }
5016 } else {
5017 OriginalPathState::Missing
5018 };
5019 if matches!(pending[index].original, OriginalPathState::Missing)
5020 && current.as_ref() == Some(&update.new)
5021 {
5022 pending[index].action = PendingPathAction::ReleaseLock;
5023 }
5024 for entry in &update.reflog {
5025 reflogs.push((update.name.clone(), entry.clone()));
5026 }
5027 }
5028 CoalescedRefChange::Delete(delete) => {
5029 let state = match self.read_locked_ref_state(&delete.name, &packed_ref_targets)
5030 {
5031 Ok(state) => state,
5032 Err(err) => {
5033 release_pending_locks(&pending);
5034 return Err(err);
5035 }
5036 };
5037 if let Err(err) = verify_delete_precondition(
5041 self,
5042 &delete.name,
5043 state.current.as_ref(),
5044 &delete.precondition,
5045 ) {
5046 release_pending_locks(&pending);
5047 return Err(err);
5048 }
5049 pending[index].original = if state.has_loose {
5050 match read_original_path_state(&pending[index].path) {
5051 Ok(original) => original,
5052 Err(err) => {
5053 release_pending_locks(&pending);
5054 return Err(err);
5055 }
5056 }
5057 } else {
5058 OriginalPathState::Missing
5059 };
5060 delete_names.insert(delete.name.clone());
5061 }
5062 }
5063 }
5064
5065 if has_delete {
5066 let old_len = packed_refs.len();
5067 packed_refs.retain(|reference| !delete_names.contains(&reference.reference.name));
5068 if packed_refs.len() != old_len {
5069 let packed_new_path = self.storage_dir.join("packed-refs.new");
5074 match fs::OpenOptions::new()
5075 .write(true)
5076 .create_new(true)
5077 .open(&packed_new_path)
5078 {
5079 Ok(file) => {
5080 drop(file);
5081 let _ = fs::remove_file(&packed_new_path);
5082 }
5083 Err(err) => {
5084 release_pending_locks(&pending);
5085 return Err(GitError::Io(format!(
5086 "Unable to create '{}': {err}",
5087 packed_new_path.display()
5088 )));
5089 }
5090 }
5091 let packed_bytes = match write_packed_refs(&packed_refs) {
5092 Ok(bytes) => bytes,
5093 Err(err) => {
5094 release_pending_locks(&pending);
5095 return Err(err);
5096 }
5097 };
5098 if let Some(packed) = pending.last_mut() {
5099 packed.action = PendingPathAction::Write {
5100 contents: packed_bytes,
5101 };
5102 }
5103 }
5104 }
5105
5106 for change in &mut pending {
5109 if let Err(err) = stage_pending_change(change) {
5110 release_pending_locks(&pending);
5111 return Err(err);
5112 }
5113 }
5114
5115 if let (Some(hook), Some(updates)) = (hook, hook_updates)
5119 && hook.run(RefTransactionPhase::Prepared, updates)?
5120 {
5121 release_pending_locks(&pending);
5122 return Err(ref_transaction_hook_abort(RefTransactionPhase::Prepared));
5123 }
5124
5125 for index in 0..pending.len() {
5128 if let Err(err) = maybe_fail_loose_commit_action(index) {
5129 rollback_after_apply(&pending, index);
5130 return Err(err);
5131 }
5132 if let Err(err) = apply_pending_change(&pending[index]) {
5133 rollback_after_apply(&pending, index + 1);
5134 return Err(err);
5135 }
5136 if matches!(
5137 pending[index].action,
5138 PendingPathAction::Write { .. } | PendingPathAction::WriteSymbolic { .. }
5139 ) && let Err(err) = self.shared_repository.adjust_file(&pending[index].path)
5140 {
5141 rollback_after_apply(&pending, index + 1);
5142 return Err(err);
5143 }
5144 if matches!(pending[index].action, PendingPathAction::Delete) {
5145 self.prune_empty_ref_dirs(&pending[index].name);
5146 }
5147 }
5148
5149 for name in &delete_names {
5153 self.remove_reflog_file(name);
5154 }
5155 let head_mirror = Self::head_reflog_mirror(head_branch.as_deref(), &reflogs);
5158 reflogs.extend(head_mirror);
5159 self.append_loose_reflogs(reflogs)?;
5161 if let (Some(hook), Some(updates)) = (hook, hook_updates) {
5164 hook.run(RefTransactionPhase::Committed, updates)?;
5165 }
5166 Ok(())
5167 }
5168
5169 fn read_locked_ref_state(
5170 &self,
5171 name: &str,
5172 packed_refs: &HashMap<String, RefTarget>,
5173 ) -> Result<LockedRefState> {
5174 let loose = self.read_loose_ref(name)?;
5175 let current = if let Some(reference) = loose.as_ref() {
5176 Some(reference.target.clone())
5177 } else {
5178 packed_refs.get(name).cloned()
5179 };
5180 Ok(LockedRefState {
5181 current,
5182 has_loose: loose.is_some(),
5183 })
5184 }
5185}
5186
5187struct LockedRefState {
5188 current: Option<RefTarget>,
5189 has_loose: bool,
5190}
5191
5192enum CoalescedRefChange {
5193 Update(CoalescedRefUpdate),
5194 Delete(CoalescedRefDelete),
5195}
5196
5197impl CoalescedRefChange {
5198 fn name(&self) -> &str {
5199 match self {
5200 Self::Update(update) => &update.name,
5201 Self::Delete(delete) => &delete.name,
5202 }
5203 }
5204}
5205
5206struct CoalescedRefUpdate {
5208 name: String,
5209 precondition: RefPrecondition,
5210 new: RefTarget,
5211 reflog: Vec<ReflogEntry>,
5212}
5213
5214struct CoalescedRefDelete {
5215 name: String,
5216 precondition: RefDeletePrecondition,
5217}
5218
5219fn coalesce_ref_changes(changes: Vec<QueuedRefChange>) -> Result<Vec<CoalescedRefChange>> {
5220 let has_delete = changes
5221 .iter()
5222 .any(|change| matches!(change, QueuedRefChange::Delete(_)));
5223 if !has_delete {
5224 let updates = changes
5225 .into_iter()
5226 .map(|change| match change {
5227 QueuedRefChange::Update(update) => update,
5228 QueuedRefChange::Delete(_) => unreachable!("has_delete was false"),
5229 })
5230 .collect::<Vec<_>>();
5231 return coalesce_ref_updates(updates).map(|updates| {
5232 updates
5233 .into_iter()
5234 .map(CoalescedRefChange::Update)
5235 .collect()
5236 });
5237 }
5238
5239 let mut seen = BTreeSet::new();
5240 let mut coalesced = Vec::with_capacity(changes.len());
5241 for change in changes {
5242 let name = match &change {
5243 QueuedRefChange::Update(update) => &update.name,
5244 QueuedRefChange::Delete(delete) => &delete.name,
5245 };
5246 match &change {
5247 QueuedRefChange::Update(update) => validate_ref_name_for_update(&update.name)?,
5248 QueuedRefChange::Delete(delete) => validate_ref_name_for_read(&delete.name)?,
5249 }
5250 if !seen.insert(name.clone()) {
5251 return Err(GitError::Transaction(format!(
5252 "ref {name} appears more than once in transaction"
5253 )));
5254 }
5255 coalesced.push(match change {
5256 QueuedRefChange::Update(update) => CoalescedRefChange::Update(CoalescedRefUpdate {
5257 name: update.name,
5258 precondition: update.precondition,
5259 new: update.new,
5260 reflog: update.reflog.into_iter().collect(),
5261 }),
5262 QueuedRefChange::Delete(delete) => CoalescedRefChange::Delete(CoalescedRefDelete {
5263 name: delete.name,
5264 precondition: delete.precondition,
5265 }),
5266 });
5267 }
5268 Ok(coalesced)
5269}
5270
5271fn coalesce_ref_updates(updates: Vec<QueuedUpdate>) -> Result<Vec<CoalescedRefUpdate>> {
5276 let mut order: Vec<String> = Vec::new();
5277 let mut by_name: HashMap<String, CoalescedRefUpdate> = HashMap::new();
5278 for update in updates {
5279 validate_ref_name_for_update(&update.name)?;
5280 match by_name.get_mut(&update.name) {
5281 Some(existing) => {
5282 existing.new = update.new;
5283 if let Some(entry) = update.reflog {
5284 existing.reflog.push(entry);
5285 }
5286 }
5287 None => {
5288 order.push(update.name.clone());
5289 by_name.insert(
5290 update.name.clone(),
5291 CoalescedRefUpdate {
5292 name: update.name,
5293 precondition: update.precondition,
5294 new: update.new,
5295 reflog: update.reflog.into_iter().collect(),
5296 },
5297 );
5298 }
5299 }
5300 }
5301 let mut coalesced = Vec::with_capacity(order.len());
5302 for name in order {
5303 if let Some(update) = by_name.remove(&name) {
5304 coalesced.push(update);
5305 }
5306 }
5307 Ok(coalesced)
5308}
5309
5310struct PendingPathChange {
5313 name: String,
5314 display_path: String,
5315 path: PathBuf,
5316 lock_path: PathBuf,
5317 original: OriginalPathState,
5318 action: PendingPathAction,
5319 staged: bool,
5320}
5321
5322enum OriginalPathState {
5323 Missing,
5324 File(Vec<u8>),
5325 Symlink(PathBuf),
5326}
5327
5328enum PendingPathAction {
5329 Write {
5330 contents: Vec<u8>,
5331 },
5332 WriteSymbolic {
5333 target: String,
5334 fallback_contents: Vec<u8>,
5335 },
5336 Delete,
5337 ReleaseLock,
5338}
5339
5340struct RefDirPruneGuard<'a> {
5341 store: &'a FileRefStore,
5342 name: String,
5343}
5344
5345impl Drop for RefDirPruneGuard<'_> {
5346 fn drop(&mut self) {
5347 self.store.prune_empty_ref_dirs(&self.name);
5348 }
5349}
5350
5351struct DeleteLock {
5352 path: PathBuf,
5353 file: Option<fs::File>,
5354 active: bool,
5355}
5356
5357impl DeleteLock {
5358 fn acquire(path: PathBuf) -> std::result::Result<Self, RefDeleteError> {
5359 match fs::OpenOptions::new()
5360 .write(true)
5361 .create_new(true)
5362 .open(&path)
5363 {
5364 Ok(file) => Ok(Self {
5365 path,
5366 file: Some(file),
5367 active: true,
5368 }),
5369 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
5370 Err(RefDeleteError::Locked)
5371 }
5372 Err(err) => Err(RefDeleteError::Io(err)),
5373 }
5374 }
5375
5376 fn write_all(
5377 &mut self,
5378 bytes: &[u8],
5379 fsync_method: Option<ReferenceFsyncMethod>,
5380 ) -> std::result::Result<(), RefDeleteError> {
5381 let Some(file) = self.file.as_mut() else {
5382 return Err(RefDeleteError::Io(std::io::Error::other(
5383 "lock file is already closed",
5384 )));
5385 };
5386 file.set_len(0)?;
5387 file.write_all(bytes)?;
5388 if let Some(method) = fsync_method {
5389 sync_reference_file(file, method)?;
5390 }
5391 Ok(())
5392 }
5393
5394 fn close(mut self) -> PathBuf {
5395 self.active = false;
5396 let _ = self.file.take();
5397 self.path.clone()
5398 }
5399
5400 fn remove(mut self) {
5401 self.active = false;
5402 let _ = self.file.take();
5403 let _ = fs::remove_file(&self.path);
5404 }
5405}
5406
5407impl Drop for DeleteLock {
5408 fn drop(&mut self) {
5409 if self.active {
5410 let _ = self.file.take();
5411 let _ = fs::remove_file(&self.path);
5412 }
5413 }
5414}
5415
5416struct ReftableListLock {
5417 list_path: PathBuf,
5418 lock_path: PathBuf,
5419 file: Option<fs::File>,
5420 active: bool,
5421}
5422
5423impl ReftableListLock {
5424 fn acquire(list_path: PathBuf, lock_path: PathBuf, timeout_millis: u64) -> Result<Self> {
5425 let start = SystemTime::now();
5426 loop {
5427 match fs::OpenOptions::new()
5428 .write(true)
5429 .create_new(true)
5430 .open(&lock_path)
5431 {
5432 Ok(file) => {
5433 return Ok(Self {
5434 list_path,
5435 lock_path,
5436 file: Some(file),
5437 active: true,
5438 });
5439 }
5440 Err(err)
5441 if err.kind() == std::io::ErrorKind::AlreadyExists && timeout_millis > 0 =>
5442 {
5443 let elapsed = start
5444 .elapsed()
5445 .unwrap_or_else(|_| Duration::from_millis(timeout_millis + 1));
5446 if elapsed.as_millis() as u64 >= timeout_millis {
5447 return Err(GitError::Io(format!(
5448 "cannot lock references: {}: File exists",
5449 lock_path.display()
5450 )));
5451 }
5452 thread::sleep(Duration::from_millis(50));
5453 }
5454 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
5455 return Err(GitError::Io(format!(
5456 "cannot lock references: {}: File exists",
5457 lock_path.display()
5458 )));
5459 }
5460 Err(err) => return Err(err.into()),
5461 }
5462 }
5463 }
5464
5465 fn commit(mut self, bytes: &[u8], fsync_method: Option<ReferenceFsyncMethod>) -> Result<()> {
5466 let Some(mut file) = self.file.take() else {
5467 return Err(GitError::Io("reftable list lock is already closed".into()));
5468 };
5469 file.set_len(0)?;
5470 file.write_all(bytes)?;
5471 if let Some(method) = fsync_method {
5472 sync_reference_file(&file, method)?;
5473 }
5474 drop(file);
5475 fs::rename(&self.lock_path, &self.list_path)
5476 .map_err(|err| GitError::Io(err.to_string()))?;
5477 self.active = false;
5478 Ok(())
5479 }
5480}
5481
5482impl Drop for ReftableListLock {
5483 fn drop(&mut self) {
5484 if self.active {
5485 let _ = self.file.take();
5486 let _ = fs::remove_file(&self.lock_path);
5487 }
5488 }
5489}
5490
5491fn checked_delete_oid(
5492 expected: Option<ObjectId>,
5493 current: Option<RefTarget>,
5494) -> std::result::Result<ObjectId, RefDeleteError> {
5495 let Some(current) = current else {
5496 return Err(RefDeleteError::NotFound);
5497 };
5498 let RefTarget::Direct(actual) = current else {
5499 return Err(RefDeleteError::ExpectedMismatch {
5500 expected,
5501 actual: None,
5502 });
5503 };
5504 if let Some(expected_oid) = expected
5505 && expected_oid != actual
5506 {
5507 return Err(RefDeleteError::ExpectedMismatch {
5508 expected: Some(expected_oid),
5509 actual: Some(actual),
5510 });
5511 }
5512 Ok(actual)
5513}
5514
5515fn verify_delete_precondition(
5521 store: &FileRefStore,
5522 name: &str,
5523 current: Option<&RefTarget>,
5524 precondition: &RefDeletePrecondition,
5525) -> Result<()> {
5526 let Some(current) = current else {
5527 return Err(GitError::Transaction(format!("ref {name} not found")));
5528 };
5529 match precondition {
5530 RefDeletePrecondition::Any => {
5531 peeled_oid_for_delete(store, current)?;
5532 Ok(())
5533 }
5534 RefDeletePrecondition::Immediate(expected) if current == expected => {
5535 peeled_oid_for_delete(store, current)?;
5536 Ok(())
5537 }
5538 RefDeletePrecondition::Immediate(_) => Err(delete_precondition_mismatch(name)),
5539 RefDeletePrecondition::Direct(expected) => {
5540 let RefTarget::Direct(actual) = current else {
5541 return Err(delete_precondition_mismatch(name));
5542 };
5543 if let Some(expected) = expected
5544 && expected != actual
5545 {
5546 return Err(delete_precondition_mismatch(name));
5547 }
5548 Ok(())
5549 }
5550 RefDeletePrecondition::Peeled(expected) => {
5551 let actual = peeled_oid_for_delete(store, current)?;
5552 if actual == Some(*expected) {
5553 Ok(())
5554 } else {
5555 Err(delete_precondition_mismatch(name))
5556 }
5557 }
5558 }
5559}
5560
5561fn peeled_oid_for_delete(store: &FileRefStore, target: &RefTarget) -> Result<Option<ObjectId>> {
5562 match target {
5563 RefTarget::Direct(oid) => Ok(Some(*oid)),
5564 RefTarget::Symbolic(name) => resolve_ref_peeled(store, name),
5565 }
5566}
5567
5568fn delete_precondition_mismatch(name: &str) -> GitError {
5569 GitError::Transaction(format!("expected ref {name} to match"))
5570}
5571
5572fn ref_delete_error_from_git(err: GitError) -> RefDeleteError {
5573 match err {
5574 GitError::InvalidPath(_) => RefDeleteError::InvalidName,
5575 GitError::NotFound(_) => RefDeleteError::NotFound,
5576 GitError::Io(message) if message.contains("File exists") => RefDeleteError::Locked,
5577 GitError::Io(message) if message.contains("could not lock") => RefDeleteError::Locked,
5578 GitError::Transaction(message) if message.contains("could not lock") => {
5579 RefDeleteError::Locked
5580 }
5581 other => RefDeleteError::Io(std::io::Error::other(other.to_string())),
5582 }
5583}
5584
5585fn read_original_path_state(path: &Path) -> Result<OriginalPathState> {
5586 let metadata = match fs::symlink_metadata(path) {
5587 Ok(metadata) => metadata,
5588 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
5589 return Ok(OriginalPathState::Missing);
5590 }
5591 Err(err) => return Err(GitError::Io(err.to_string())),
5592 };
5593 if metadata.file_type().is_symlink() {
5594 return fs::read_link(path)
5595 .map(OriginalPathState::Symlink)
5596 .map_err(|err| GitError::Io(err.to_string()));
5597 }
5598 if metadata.is_dir() {
5599 return Ok(OriginalPathState::Missing);
5600 }
5601 match fs::read(path) {
5602 Ok(bytes) => Ok(OriginalPathState::File(bytes)),
5603 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(OriginalPathState::Missing),
5604 Err(err) => Err(GitError::Io(err.to_string())),
5605 }
5606}
5607
5608fn remove_empty_dir_tree(path: &Path) -> std::io::Result<()> {
5612 let meta = match fs::symlink_metadata(path) {
5613 Ok(meta) => meta,
5614 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
5615 Err(err) => return Err(err),
5616 };
5617 if !meta.is_dir() {
5618 return Ok(());
5619 }
5620 for entry in fs::read_dir(path)? {
5621 let entry = entry?;
5622 if entry.file_type()?.is_dir() {
5623 remove_empty_dir_tree(&entry.path())?;
5624 } else {
5625 return Err(std::io::Error::other("directory not empty"));
5626 }
5627 }
5628 fs::remove_dir(path)
5629}
5630
5631fn stage_lock_file(lock_path: &Path, contents: &[u8]) -> Result<()> {
5632 let mut file = fs::OpenOptions::new()
5633 .write(true)
5634 .truncate(true)
5635 .open(lock_path)?;
5636 file.write_all(contents)?;
5637 Ok(())
5638}
5639
5640fn stage_pending_change(change: &mut PendingPathChange) -> Result<()> {
5641 if change.staged {
5642 return Ok(());
5643 }
5644 match &change.action {
5645 PendingPathAction::Write { contents } => stage_lock_file(&change.lock_path, contents)?,
5646 PendingPathAction::WriteSymbolic {
5647 target,
5648 fallback_contents,
5649 } => stage_symbolic_lock_file(&change.lock_path, target, fallback_contents)?,
5650 PendingPathAction::Delete => stage_lock_file(&change.lock_path, b"delete\n")?,
5651 PendingPathAction::ReleaseLock => {}
5652 }
5653 change.staged = true;
5654 Ok(())
5655}
5656
5657fn apply_pending_change(change: &PendingPathChange) -> Result<()> {
5658 match &change.action {
5659 PendingPathAction::Write { .. } | PendingPathAction::WriteSymbolic { .. } => {
5660 match fs::rename(&change.lock_path, &change.path) {
5661 Ok(()) => Ok(()),
5662 Err(first_err) => {
5663 match fs::symlink_metadata(&change.path) {
5668 Ok(meta) if meta.is_dir() => {
5669 remove_empty_dir_tree(&change.path).map_err(|err| {
5670 if err.kind() == std::io::ErrorKind::Other
5671 && err.to_string() == "directory not empty"
5672 {
5673 GitError::Transaction(format!(
5674 "cannot lock ref '{}': there is a non-empty directory '{}' blocking reference '{}'",
5675 change.name, change.display_path, change.name
5676 ))
5677 } else {
5678 GitError::Io(err.to_string())
5679 }
5680 })?;
5681 fs::rename(&change.lock_path, &change.path)
5682 .map_err(|err| GitError::Io(err.to_string()))
5683 }
5684 _ => Err(GitError::Io(first_err.to_string())),
5685 }
5686 }
5687 }
5688 }
5689 PendingPathAction::Delete => {
5690 if !matches!(change.original, OriginalPathState::Missing) {
5691 fs::remove_file(&change.path).map_err(|err| GitError::Io(err.to_string()))?;
5692 }
5693 fs::remove_file(&change.lock_path).map_err(|err| GitError::Io(err.to_string()))
5694 }
5695 PendingPathAction::ReleaseLock => {
5696 fs::remove_file(&change.lock_path).map_err(|err| GitError::Io(err.to_string()))
5697 }
5698 }
5699}
5700
5701fn stage_symbolic_lock_file(
5702 lock_path: &Path,
5703 target: &str,
5704 fallback_contents: &[u8],
5705) -> Result<()> {
5706 #[cfg(unix)]
5707 {
5708 let _ = fallback_contents;
5709 fs::remove_file(lock_path)?;
5710 std::os::unix::fs::symlink(target, lock_path)?;
5711 Ok(())
5712 }
5713 #[cfg(not(unix))]
5714 {
5715 let _ = target;
5716 stage_lock_file(lock_path, fallback_contents)
5717 }
5718}
5719
5720fn release_pending_locks(pending: &[PendingPathChange]) {
5723 for change in pending {
5724 let _ = fs::remove_file(&change.lock_path);
5725 }
5726}
5727
5728fn rollback_after_apply(pending: &[PendingPathChange], applied: usize) {
5732 for change in pending.iter().take(applied) {
5733 if matches!(change.action, PendingPathAction::ReleaseLock) {
5734 let _ = fs::remove_file(&change.lock_path);
5735 continue;
5736 }
5737 match &change.original {
5738 OriginalPathState::File(bytes) => {
5739 let _ = restore_file_atomically(&change.path, bytes);
5740 }
5741 OriginalPathState::Symlink(target) => {
5742 let _ = restore_symlink_atomically(&change.path, &change.lock_path, target);
5743 }
5744 OriginalPathState::Missing => {
5745 let _ = fs::remove_file(&change.path);
5746 }
5747 }
5748 let _ = fs::remove_file(&change.lock_path);
5749 }
5750 for change in pending.iter().skip(applied) {
5751 let _ = fs::remove_file(&change.lock_path);
5752 }
5753}
5754
5755#[cfg(unix)]
5756fn restore_symlink_atomically(path: &Path, lock_path: &Path, target: &Path) -> std::io::Result<()> {
5757 let _ = fs::remove_file(lock_path);
5758 std::os::unix::fs::symlink(target, lock_path)?;
5759 fs::rename(lock_path, path)
5760}
5761
5762#[cfg(windows)]
5763fn restore_symlink_atomically(path: &Path, lock_path: &Path, target: &Path) -> std::io::Result<()> {
5764 let _ = fs::remove_file(lock_path);
5765 std::os::windows::fs::symlink_file(target, lock_path)?;
5766 fs::rename(lock_path, path)
5767}
5768
5769#[cfg(not(any(unix, windows)))]
5770fn restore_symlink_atomically(
5771 _path: &Path,
5772 _lock_path: &Path,
5773 _target: &Path,
5774) -> std::io::Result<()> {
5775 Err(std::io::Error::new(
5776 std::io::ErrorKind::Unsupported,
5777 "symbolic reference rollback is unsupported on this platform",
5778 ))
5779}
5780
5781#[cfg(test)]
5782thread_local! {
5783 static FAIL_LOOSE_COMMIT_ACTION: std::cell::Cell<Option<usize>> =
5784 const { std::cell::Cell::new(None) };
5785 static REFERENCE_FSYNC_CALLS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
5786 static FAIL_REFERENCE_FSYNC: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
5787}
5788
5789#[cfg(test)]
5790fn set_fail_loose_commit_action_for_test(index: Option<usize>) {
5791 FAIL_LOOSE_COMMIT_ACTION.with(|cell| cell.set(index));
5792}
5793
5794#[cfg(test)]
5795fn maybe_fail_loose_commit_action(index: usize) -> Result<()> {
5796 let should_fail = FAIL_LOOSE_COMMIT_ACTION.with(|cell| cell.get() == Some(index));
5797 if should_fail {
5798 FAIL_LOOSE_COMMIT_ACTION.with(|cell| cell.set(None));
5799 return Err(GitError::Io(format!(
5800 "injected loose ref transaction failure at action {index}"
5801 )));
5802 }
5803 Ok(())
5804}
5805
5806#[cfg(not(test))]
5807fn maybe_fail_loose_commit_action(_index: usize) -> Result<()> {
5808 Ok(())
5809}
5810
5811fn restore_file_atomically(path: &Path, bytes: &[u8]) -> Result<()> {
5814 if let Some(parent) = path.parent() {
5815 fs::create_dir_all(parent)?;
5816 }
5817 write_locked(path, bytes, None)
5818}
5819
5820#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
5821pub struct FullRefName<'a> {
5822 name: &'a str,
5823}
5824
5825impl<'a> FullRefName<'a> {
5826 pub fn new(name: &'a str) -> Result<Self> {
5827 validate_ref_name(name)?;
5828 Ok(Self { name })
5829 }
5830
5831 pub fn as_str(&self) -> &str {
5832 self.name
5833 }
5834
5835 pub fn into_str(self) -> &'a str {
5836 self.name
5837 }
5838
5839 pub fn to_owned(&self) -> FullRefNameBuf {
5840 FullRefNameBuf {
5841 name: self.name.to_string(),
5842 }
5843 }
5844
5845 pub fn as_branch(&self) -> Result<BranchRefName<'a>> {
5846 BranchRefName::from_full_ref(*self)
5847 }
5848
5849 pub fn as_tag(&self) -> Result<TagRefName<'a>> {
5850 TagRefName::from_full_ref(*self)
5851 }
5852
5853 pub fn as_remote(&self) -> Result<RemoteRefName<'a>> {
5854 RemoteRefName::from_full_ref(*self)
5855 }
5856}
5857
5858impl AsRef<str> for FullRefName<'_> {
5859 fn as_ref(&self) -> &str {
5860 self.as_str()
5861 }
5862}
5863
5864impl fmt::Display for FullRefName<'_> {
5865 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5866 f.write_str(self.as_str())
5867 }
5868}
5869
5870#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
5871pub struct FullRefNameBuf {
5872 name: String,
5873}
5874
5875impl FullRefNameBuf {
5876 pub fn new(name: impl Into<String>) -> Result<Self> {
5877 let name = name.into();
5878 validate_ref_name(&name)?;
5879 Ok(Self { name })
5880 }
5881
5882 pub fn as_ref_name(&self) -> FullRefName<'_> {
5883 FullRefName { name: &self.name }
5884 }
5885
5886 pub fn as_str(&self) -> &str {
5887 &self.name
5888 }
5889
5890 pub fn into_string(self) -> String {
5891 self.name
5892 }
5893
5894 pub fn as_branch(&self) -> Result<BranchRefName<'_>> {
5895 self.as_ref_name().as_branch()
5896 }
5897
5898 pub fn as_tag(&self) -> Result<TagRefName<'_>> {
5899 self.as_ref_name().as_tag()
5900 }
5901
5902 pub fn as_remote(&self) -> Result<RemoteRefName<'_>> {
5903 self.as_ref_name().as_remote()
5904 }
5905}
5906
5907impl AsRef<str> for FullRefNameBuf {
5908 fn as_ref(&self) -> &str {
5909 self.as_str()
5910 }
5911}
5912
5913impl Borrow<str> for FullRefNameBuf {
5914 fn borrow(&self) -> &str {
5915 self.as_str()
5916 }
5917}
5918
5919impl Deref for FullRefNameBuf {
5920 type Target = str;
5921
5922 fn deref(&self) -> &Self::Target {
5923 self.as_str()
5924 }
5925}
5926
5927impl fmt::Display for FullRefNameBuf {
5928 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5929 f.write_str(self.as_str())
5930 }
5931}
5932
5933#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
5934pub struct BranchRefName<'a> {
5935 name: &'a str,
5936}
5937
5938impl<'a> BranchRefName<'a> {
5939 pub const PREFIX: &'static str = "refs/heads/";
5940
5941 pub fn from_full(name: &'a str) -> Result<Self> {
5942 let full = FullRefName::new(name)?;
5943 Self::from_full_ref(full)
5944 }
5945
5946 pub fn from_full_ref(name: FullRefName<'a>) -> Result<Self> {
5947 validate_namespaced_ref(name.as_str(), Self::PREFIX, "branch")?;
5948 Ok(Self {
5949 name: name.into_str(),
5950 })
5951 }
5952
5953 pub fn as_full_ref_name(&self) -> FullRefName<'a> {
5954 FullRefName { name: self.name }
5955 }
5956
5957 pub fn as_str(&self) -> &str {
5958 self.name
5959 }
5960
5961 pub fn branch_name(&self) -> &str {
5962 self.short_name()
5963 }
5964
5965 pub fn short_name(&self) -> &str {
5966 &self.name[Self::PREFIX.len()..]
5967 }
5968
5969 pub fn into_str(self) -> &'a str {
5970 self.name
5971 }
5972
5973 pub fn to_owned(&self) -> BranchRefNameBuf {
5974 BranchRefNameBuf {
5975 name: self.name.to_string(),
5976 }
5977 }
5978}
5979
5980impl AsRef<str> for BranchRefName<'_> {
5981 fn as_ref(&self) -> &str {
5982 self.as_str()
5983 }
5984}
5985
5986impl fmt::Display for BranchRefName<'_> {
5987 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5988 f.write_str(self.as_str())
5989 }
5990}
5991
5992impl<'a> From<BranchRefName<'a>> for FullRefName<'a> {
5993 fn from(name: BranchRefName<'a>) -> Self {
5994 name.as_full_ref_name()
5995 }
5996}
5997
5998#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
5999pub struct BranchRefNameBuf {
6000 name: String,
6001}
6002
6003impl BranchRefNameBuf {
6004 pub fn from_branch_name(branch: &str) -> Result<Self> {
6005 validate_short_ref_name("branch", branch)?;
6006 let name = format!("{}{}", BranchRefName::PREFIX, branch);
6007 Self::from_full(name)
6008 }
6009
6010 pub fn from_full(name: impl Into<String>) -> Result<Self> {
6011 let name = name.into();
6012 BranchRefName::from_full(&name)?;
6013 Ok(Self { name })
6014 }
6015
6016 pub fn as_ref_name(&self) -> BranchRefName<'_> {
6017 BranchRefName { name: &self.name }
6018 }
6019
6020 pub fn as_full_ref_name(&self) -> FullRefName<'_> {
6021 FullRefName { name: &self.name }
6022 }
6023
6024 pub fn as_str(&self) -> &str {
6025 &self.name
6026 }
6027
6028 pub fn branch_name(&self) -> &str {
6029 self.short_name()
6030 }
6031
6032 pub fn short_name(&self) -> &str {
6033 &self.name[BranchRefName::PREFIX.len()..]
6034 }
6035
6036 pub fn into_string(self) -> String {
6037 self.name
6038 }
6039}
6040
6041impl AsRef<str> for BranchRefNameBuf {
6042 fn as_ref(&self) -> &str {
6043 self.as_str()
6044 }
6045}
6046
6047impl Borrow<str> for BranchRefNameBuf {
6048 fn borrow(&self) -> &str {
6049 self.as_str()
6050 }
6051}
6052
6053impl Deref for BranchRefNameBuf {
6054 type Target = str;
6055
6056 fn deref(&self) -> &Self::Target {
6057 self.as_str()
6058 }
6059}
6060
6061impl fmt::Display for BranchRefNameBuf {
6062 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6063 f.write_str(self.as_str())
6064 }
6065}
6066
6067impl From<BranchRefNameBuf> for FullRefNameBuf {
6068 fn from(name: BranchRefNameBuf) -> Self {
6069 Self { name: name.name }
6070 }
6071}
6072
6073#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
6074pub struct TagRefName<'a> {
6075 name: &'a str,
6076}
6077
6078impl<'a> TagRefName<'a> {
6079 pub const PREFIX: &'static str = "refs/tags/";
6080
6081 pub fn from_full(name: &'a str) -> Result<Self> {
6082 let full = FullRefName::new(name)?;
6083 Self::from_full_ref(full)
6084 }
6085
6086 pub fn from_full_ref(name: FullRefName<'a>) -> Result<Self> {
6087 validate_namespaced_ref(name.as_str(), Self::PREFIX, "tag")?;
6088 Ok(Self {
6089 name: name.into_str(),
6090 })
6091 }
6092
6093 pub fn as_full_ref_name(&self) -> FullRefName<'a> {
6094 FullRefName { name: self.name }
6095 }
6096
6097 pub fn as_str(&self) -> &str {
6098 self.name
6099 }
6100
6101 pub fn tag_name(&self) -> &str {
6102 self.short_name()
6103 }
6104
6105 pub fn short_name(&self) -> &str {
6106 &self.name[Self::PREFIX.len()..]
6107 }
6108
6109 pub fn into_str(self) -> &'a str {
6110 self.name
6111 }
6112
6113 pub fn to_owned(&self) -> TagRefNameBuf {
6114 TagRefNameBuf {
6115 name: self.name.to_string(),
6116 }
6117 }
6118}
6119
6120impl AsRef<str> for TagRefName<'_> {
6121 fn as_ref(&self) -> &str {
6122 self.as_str()
6123 }
6124}
6125
6126impl fmt::Display for TagRefName<'_> {
6127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6128 f.write_str(self.as_str())
6129 }
6130}
6131
6132impl<'a> From<TagRefName<'a>> for FullRefName<'a> {
6133 fn from(name: TagRefName<'a>) -> Self {
6134 name.as_full_ref_name()
6135 }
6136}
6137
6138#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
6139pub struct TagRefNameBuf {
6140 name: String,
6141}
6142
6143impl TagRefNameBuf {
6144 pub fn from_tag_name(tag: &str) -> Result<Self> {
6145 if tag.starts_with('-') || tag == "HEAD" {
6148 return Err(GitError::InvalidPath(format!("invalid tag name {tag}")));
6149 }
6150 Self::from_tag_name_unrestricted(tag)
6151 }
6152
6153 pub fn from_tag_name_unrestricted(tag: &str) -> Result<Self> {
6158 let name = format!("{}{}", TagRefName::PREFIX, tag);
6159 check_refname_format(&name, false)?;
6160 Ok(Self { name })
6161 }
6162
6163 pub fn from_full(name: impl Into<String>) -> Result<Self> {
6164 let name = name.into();
6165 TagRefName::from_full(&name)?;
6166 Ok(Self { name })
6167 }
6168
6169 pub fn as_ref_name(&self) -> TagRefName<'_> {
6170 TagRefName { name: &self.name }
6171 }
6172
6173 pub fn as_full_ref_name(&self) -> FullRefName<'_> {
6174 FullRefName { name: &self.name }
6175 }
6176
6177 pub fn as_str(&self) -> &str {
6178 &self.name
6179 }
6180
6181 pub fn tag_name(&self) -> &str {
6182 self.short_name()
6183 }
6184
6185 pub fn short_name(&self) -> &str {
6186 &self.name[TagRefName::PREFIX.len()..]
6187 }
6188
6189 pub fn into_string(self) -> String {
6190 self.name
6191 }
6192}
6193
6194impl AsRef<str> for TagRefNameBuf {
6195 fn as_ref(&self) -> &str {
6196 self.as_str()
6197 }
6198}
6199
6200impl Borrow<str> for TagRefNameBuf {
6201 fn borrow(&self) -> &str {
6202 self.as_str()
6203 }
6204}
6205
6206impl Deref for TagRefNameBuf {
6207 type Target = str;
6208
6209 fn deref(&self) -> &Self::Target {
6210 self.as_str()
6211 }
6212}
6213
6214impl fmt::Display for TagRefNameBuf {
6215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6216 f.write_str(self.as_str())
6217 }
6218}
6219
6220impl From<TagRefNameBuf> for FullRefNameBuf {
6221 fn from(name: TagRefNameBuf) -> Self {
6222 Self { name: name.name }
6223 }
6224}
6225
6226#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
6227pub struct RemoteRefName<'a> {
6228 name: &'a str,
6229}
6230
6231impl<'a> RemoteRefName<'a> {
6232 pub const PREFIX: &'static str = "refs/remotes/";
6233
6234 pub fn from_full(name: &'a str) -> Result<Self> {
6235 let full = FullRefName::new(name)?;
6236 Self::from_full_ref(full)
6237 }
6238
6239 pub fn from_full_ref(name: FullRefName<'a>) -> Result<Self> {
6240 validate_namespaced_ref(name.as_str(), Self::PREFIX, "remote")?;
6241 Ok(Self {
6242 name: name.into_str(),
6243 })
6244 }
6245
6246 pub fn as_full_ref_name(&self) -> FullRefName<'a> {
6247 FullRefName { name: self.name }
6248 }
6249
6250 pub fn as_str(&self) -> &str {
6251 self.name
6252 }
6253
6254 pub fn short_name(&self) -> &str {
6255 &self.name[Self::PREFIX.len()..]
6256 }
6257
6258 pub fn remote_name(&self) -> &str {
6259 match self.short_name().split_once('/') {
6260 Some((remote, _branch)) => remote,
6261 None => self.short_name(),
6262 }
6263 }
6264
6265 pub fn remote_branch(&self) -> Option<&str> {
6266 self.short_name()
6267 .split_once('/')
6268 .map(|(_remote, branch)| branch)
6269 }
6270
6271 pub fn into_str(self) -> &'a str {
6272 self.name
6273 }
6274
6275 pub fn to_owned(&self) -> RemoteRefNameBuf {
6276 RemoteRefNameBuf {
6277 name: self.name.to_string(),
6278 }
6279 }
6280}
6281
6282impl AsRef<str> for RemoteRefName<'_> {
6283 fn as_ref(&self) -> &str {
6284 self.as_str()
6285 }
6286}
6287
6288impl fmt::Display for RemoteRefName<'_> {
6289 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6290 f.write_str(self.as_str())
6291 }
6292}
6293
6294impl<'a> From<RemoteRefName<'a>> for FullRefName<'a> {
6295 fn from(name: RemoteRefName<'a>) -> Self {
6296 name.as_full_ref_name()
6297 }
6298}
6299
6300#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
6301pub struct RemoteRefNameBuf {
6302 name: String,
6303}
6304
6305impl RemoteRefNameBuf {
6306 pub fn from_short_name(name: &str) -> Result<Self> {
6307 validate_short_ref_name("remote ref", name)?;
6308 let name = format!("{}{}", RemoteRefName::PREFIX, name);
6309 Self::from_full(name)
6310 }
6311
6312 pub fn from_remote_branch(remote: &str, branch: &str) -> Result<Self> {
6313 validate_remote_name(remote)?;
6314 validate_short_ref_name("remote branch", branch)?;
6315 let name = format!("{}{}/{}", RemoteRefName::PREFIX, remote, branch);
6316 Self::from_full(name)
6317 }
6318
6319 pub fn from_full(name: impl Into<String>) -> Result<Self> {
6320 let name = name.into();
6321 RemoteRefName::from_full(&name)?;
6322 Ok(Self { name })
6323 }
6324
6325 pub fn as_ref_name(&self) -> RemoteRefName<'_> {
6326 RemoteRefName { name: &self.name }
6327 }
6328
6329 pub fn as_full_ref_name(&self) -> FullRefName<'_> {
6330 FullRefName { name: &self.name }
6331 }
6332
6333 pub fn as_str(&self) -> &str {
6334 &self.name
6335 }
6336
6337 pub fn short_name(&self) -> &str {
6338 &self.name[RemoteRefName::PREFIX.len()..]
6339 }
6340
6341 pub fn remote_name(&self) -> &str {
6342 match self.short_name().split_once('/') {
6343 Some((remote, _branch)) => remote,
6344 None => self.short_name(),
6345 }
6346 }
6347
6348 pub fn remote_branch(&self) -> Option<&str> {
6349 self.short_name()
6350 .split_once('/')
6351 .map(|(_remote, branch)| branch)
6352 }
6353
6354 pub fn into_string(self) -> String {
6355 self.name
6356 }
6357}
6358
6359impl AsRef<str> for RemoteRefNameBuf {
6360 fn as_ref(&self) -> &str {
6361 self.as_str()
6362 }
6363}
6364
6365impl Borrow<str> for RemoteRefNameBuf {
6366 fn borrow(&self) -> &str {
6367 self.as_str()
6368 }
6369}
6370
6371impl Deref for RemoteRefNameBuf {
6372 type Target = str;
6373
6374 fn deref(&self) -> &Self::Target {
6375 self.as_str()
6376 }
6377}
6378
6379impl fmt::Display for RemoteRefNameBuf {
6380 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6381 f.write_str(self.as_str())
6382 }
6383}
6384
6385impl From<RemoteRefNameBuf> for FullRefNameBuf {
6386 fn from(name: RemoteRefNameBuf) -> Self {
6387 Self { name: name.name }
6388 }
6389}
6390
6391pub fn branch_ref_name(branch: &str) -> Result<String> {
6392 BranchRefNameBuf::from_branch_name(branch).map(BranchRefNameBuf::into_string)
6393}
6394
6395pub fn branch_ref_name_for_read(branch: &str) -> Result<String> {
6396 let name = format!("{}{}", BranchRefName::PREFIX, branch);
6397 if validate_ref_name(&name).is_err() {
6398 if name.contains("..") {
6399 validate_ref_path_safe_for_read(&name)?;
6400 } else {
6401 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
6402 }
6403 }
6404 Ok(name)
6405}
6406
6407pub fn branch_ref_name_for_source(branch: &str) -> Result<String> {
6408 if branch.starts_with("--") {
6409 return Err(GitError::InvalidPath(format!(
6410 "invalid branch name {branch}"
6411 )));
6412 }
6413 let name = format!("{}{}", BranchRefName::PREFIX, branch);
6414 check_refname_format(&name, false)?;
6415 Ok(name)
6416}
6417
6418pub fn tag_ref_name(tag: &str) -> Result<String> {
6419 TagRefNameBuf::from_tag_name(tag).map(TagRefNameBuf::into_string)
6420}
6421
6422fn sync_reference_file(file: &fs::File, method: ReferenceFsyncMethod) -> std::io::Result<()> {
6423 #[cfg(test)]
6424 {
6425 REFERENCE_FSYNC_CALLS.with(|calls| calls.set(calls.get() + 1));
6426 let fail = FAIL_REFERENCE_FSYNC.with(|fail| fail.replace(false));
6427 if fail {
6428 return Err(std::io::Error::other("injected reference fsync failure"));
6429 }
6430 }
6431 match method {
6432 ReferenceFsyncMethod::WriteoutOnly => file.sync_data(),
6433 ReferenceFsyncMethod::Fsync | ReferenceFsyncMethod::Batch => file.sync_all(),
6434 }
6435}
6436
6437fn write_locked(
6438 path: &Path,
6439 bytes: &[u8],
6440 fsync_method: Option<ReferenceFsyncMethod>,
6441) -> Result<()> {
6442 write_locked_with_timeout(path, bytes, 0, fsync_method)
6443}
6444
6445fn write_locked_with_timeout(
6446 path: &Path,
6447 bytes: &[u8],
6448 timeout_millis: u64,
6449 fsync_method: Option<ReferenceFsyncMethod>,
6450) -> Result<()> {
6451 let lock_path = lock_path_for(path)?;
6452 let start = SystemTime::now();
6453 loop {
6454 match fs::OpenOptions::new()
6455 .write(true)
6456 .create_new(true)
6457 .open(&lock_path)
6458 {
6459 Ok(mut file) => {
6460 let write_result = file.write_all(bytes).and_then(|()| match fsync_method {
6461 Some(method) => sync_reference_file(&file, method),
6462 None => Ok(()),
6463 });
6464 if let Err(err) = write_result {
6465 drop(file);
6466 let _ = fs::remove_file(&lock_path);
6467 return Err(err.into());
6468 }
6469 break;
6470 }
6471 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists && timeout_millis > 0 => {
6472 let elapsed = start
6473 .elapsed()
6474 .unwrap_or_else(|_| Duration::from_millis(timeout_millis + 1));
6475 if elapsed.as_millis() as u64 >= timeout_millis {
6476 return Err(GitError::Io(format!(
6477 "could not lock {}: File exists",
6478 path.display()
6479 )));
6480 }
6481 thread::sleep(Duration::from_millis(50));
6482 }
6483 Err(err) => return Err(err.into()),
6484 }
6485 }
6486 match fs::rename(&lock_path, path) {
6487 Ok(()) => Ok(()),
6488 Err(err) => {
6489 let _ = fs::remove_file(lock_path);
6490 Err(GitError::Io(err.to_string()))
6491 }
6492 }
6493}
6494
6495fn acquire_path_lock_with_timeout(path: &Path, timeout_millis: u64) -> Result<()> {
6496 let start = SystemTime::now();
6497 loop {
6498 match fs::OpenOptions::new()
6499 .write(true)
6500 .create_new(true)
6501 .open(path)
6502 {
6503 Ok(file) => {
6504 drop(file);
6505 return Ok(());
6506 }
6507 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists && timeout_millis > 0 => {
6508 let elapsed = start
6509 .elapsed()
6510 .unwrap_or_else(|_| Duration::from_millis(timeout_millis + 1));
6511 if elapsed.as_millis() as u64 >= timeout_millis {
6512 return Err(GitError::Io(format!(
6513 "Unable to create '{}': File exists",
6514 path.display()
6515 )));
6516 }
6517 thread::sleep(Duration::from_millis(50));
6518 }
6519 Err(err) => {
6520 return Err(GitError::Io(format!(
6521 "Unable to create '{}': {err}",
6522 path.display()
6523 )));
6524 }
6525 }
6526 }
6527}
6528
6529fn lock_path_for(path: &Path) -> Result<PathBuf> {
6530 let file_name = path
6531 .file_name()
6532 .ok_or_else(|| GitError::InvalidPath("ref path has no filename".into()))?;
6533 let mut lock_name = file_name.to_os_string();
6534 lock_name.push(".lock");
6535 Ok(path.with_file_name(lock_name))
6536}
6537
6538pub fn check_refname_format(name: &str, allow_onelevel: bool) -> Result<()> {
6540 if name.is_empty()
6541 || name == "@"
6542 || name.starts_with('/')
6543 || name.ends_with('/')
6544 || name.ends_with('.')
6545 || name.contains("..")
6546 || name.contains("//")
6547 || name.contains("@{")
6548 || (!allow_onelevel && !name.contains('/'))
6549 {
6550 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
6551 }
6552 for component in name.split('/') {
6553 if component.is_empty() || component.starts_with('.') || component.ends_with(".lock") {
6554 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
6555 }
6556 for (idx, byte) in component.bytes().enumerate() {
6557 if byte <= b' '
6558 || byte == 0x7f
6559 || matches!(byte, b'~' | b'^' | b':' | b'?' | b'*' | b'[' | b'\\')
6560 {
6561 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
6562 }
6563 if byte == b'.' && component.as_bytes().get(idx + 1) == Some(&b'.') {
6564 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
6565 }
6566 if byte == b'@' && component.as_bytes().get(idx + 1) == Some(&b'{') {
6567 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
6568 }
6569 }
6570 }
6571 Ok(())
6572}
6573
6574pub fn validate_symref_name(name: &str) -> Result<()> {
6576 if name == "HEAD" {
6577 return Ok(());
6578 }
6579 check_refname_format(name, true)
6580}
6581
6582pub fn validate_symref_target(name: &str) -> Result<()> {
6584 check_refname_format(name, true)
6585}
6586
6587fn prune_empty_dirs_up_to(start: &Path, boundary: &Path) {
6592 let mut dir = start.to_path_buf();
6593 while dir.starts_with(boundary) && dir != *boundary {
6594 if fs::remove_dir(&dir).is_err() {
6595 break;
6596 }
6597 dir = match dir.parent() {
6598 Some(parent) => parent.to_path_buf(),
6599 None => break,
6600 };
6601 }
6602}
6603
6604fn packable_loose_ref_name(name: &str) -> bool {
6605 name.starts_with("refs/")
6606 && !name.starts_with("refs/bisect/")
6607 && !name.starts_with("refs/worktree/")
6608 && !name.starts_with("refs/rewritten/")
6609}
6610
6611fn pack_refs_auto_required_for(packed_path: &Path, loose_count: usize) -> Result<bool> {
6612 let packed_size = match fs::metadata(packed_path) {
6613 Ok(meta) => meta.len() as usize,
6614 Err(err) if err.kind() == std::io::ErrorKind::NotFound => 0,
6615 Err(err) => return Err(err.into()),
6616 };
6617 let estimated_packed_refs = packed_size / 100;
6618 let log2 = if estimated_packed_refs == 0 {
6619 0
6620 } else {
6621 usize::BITS as usize - estimated_packed_refs.leading_zeros() as usize - 1
6622 };
6623 let limit = (log2 * 5).max(16);
6624 Ok(loose_count >= limit)
6625}
6626
6627pub fn resolve_ref_peeled(store: &FileRefStore, name: &str) -> Result<Option<ObjectId>> {
6628 let mut current = name.to_string();
6629 for _ in 0..16 {
6630 match store.read_ref(¤t)? {
6631 Some(RefTarget::Direct(oid)) => return Ok(Some(oid)),
6632 Some(RefTarget::Symbolic(next)) => {
6633 if validate_ref_name_for_read(&next).is_err() {
6634 return Ok(None);
6635 }
6636 current = next;
6637 }
6638 None => return Ok(None),
6639 }
6640 }
6641 Ok(None)
6642}
6643
6644pub fn validate_ref_name_for_read(name: &str) -> Result<()> {
6645 if validate_ref_name(name).is_ok() {
6646 return Ok(());
6647 }
6648 if is_root_ref_syntax(name) {
6649 return Ok(());
6650 }
6651 if check_refname_format(name, true).is_ok() {
6652 return Ok(());
6653 }
6654 validate_ref_path_safe_for_read(name)
6655}
6656
6657pub fn validate_ref_name_for_update(name: &str) -> Result<()> {
6658 if validate_ref_name(name).is_ok() {
6659 return Ok(());
6660 }
6661 if is_root_ref_syntax(name) {
6662 return Ok(());
6663 }
6664 check_refname_format(name, true)
6665}
6666
6667pub fn refname_is_safe(refname: &str) -> bool {
6681 if let Some(rest) = refname.strip_prefix("refs/") {
6682 if rest.is_empty() || rest.starts_with('/') || rest.ends_with('/') || rest.contains('\\') {
6683 return false;
6684 }
6685 let path = Path::new(rest);
6686 !path.is_absolute()
6687 && !path.components().any(|component| {
6688 matches!(
6689 component,
6690 std::path::Component::ParentDir
6691 | std::path::Component::Prefix(_)
6692 | std::path::Component::RootDir
6693 )
6694 })
6695 } else {
6696 !refname.is_empty() && refname.bytes().all(|b| b.is_ascii_uppercase() || b == b'_')
6697 }
6698}
6699
6700fn is_root_ref_syntax(name: &str) -> bool {
6705 !name.is_empty()
6706 && name
6707 .bytes()
6708 .all(|b| b.is_ascii_uppercase() || b == b'-' || b == b'_')
6709}
6710
6711const CURRENT_WORKTREE_REF_PREFIXES: &[&str] =
6712 &["refs/bisect/", "refs/worktree/", "refs/rewritten/"];
6713
6714fn current_worktree_ref(name: &str) -> bool {
6715 is_root_ref_syntax(name)
6716 || CURRENT_WORKTREE_REF_PREFIXES
6717 .iter()
6718 .any(|prefix| name.starts_with(prefix))
6719}
6720
6721fn ref_prefixes_overlap(prefix: &str, namespace: &str) -> bool {
6722 let namespace = namespace.trim_end_matches('/');
6723 prefix == "refs/"
6724 || prefix == namespace
6725 || prefix.starts_with(&format!("{namespace}/"))
6726 || namespace.starts_with(prefix.trim_end_matches('/'))
6727}
6728
6729fn reftable_current_worktree_ref(name: &str) -> bool {
6730 current_worktree_ref(name)
6731}
6732
6733fn reftable_other_worktree_ref(name: &str) -> Option<(&str, &str)> {
6734 let rest = name.strip_prefix("worktrees/")?;
6735 let (worktree, rewritten) = rest.split_once('/')?;
6736 if worktree.is_empty() || rewritten.is_empty() {
6737 return None;
6738 }
6739 Some((worktree, rewritten))
6740}
6741
6742pub fn validate_ref_name(name: &str) -> Result<()> {
6743 if name == "HEAD" {
6744 return Ok(());
6745 }
6746 if !name.starts_with("refs/") || check_refname_format(name, false).is_err() {
6747 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
6748 }
6749 Ok(())
6750}
6751
6752fn validate_ref_path_safe_for_read(name: &str) -> Result<()> {
6753 let path = Path::new(name);
6754 if !name.starts_with("refs/")
6755 || name.starts_with('/')
6756 || name.contains('\\')
6757 || path.is_absolute()
6758 || path.components().any(|component| {
6759 matches!(
6760 component,
6761 std::path::Component::ParentDir | std::path::Component::Prefix(_)
6762 )
6763 })
6764 {
6765 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
6766 }
6767 Ok(())
6768}
6769
6770fn safe_ref_prefix_for_directory_scan(prefix: &str) -> bool {
6771 let path = Path::new(prefix);
6772 prefix.starts_with("refs/")
6773 && !prefix.starts_with('/')
6774 && !prefix.contains('\\')
6775 && !path.is_absolute()
6776 && !path.components().any(|component| {
6777 matches!(
6778 component,
6779 std::path::Component::ParentDir | std::path::Component::Prefix(_)
6780 )
6781 })
6782}
6783
6784fn warn_broken_ref_name(name: &str) {
6785 eprintln!("warning: ignoring ref with broken name {name}");
6786}
6787
6788fn warn_broken_ref(name: &str) {
6789 eprintln!("warning: ignoring broken ref {name}");
6790}
6791
6792fn ref_target_is_broken(target: &RefTarget) -> bool {
6794 matches!(target, RefTarget::Direct(oid) if oid.is_null())
6795}
6796
6797fn ref_directory_conflict_error(new_ref: &str, existing_ref: &str) -> GitError {
6798 GitError::Transaction(format!(
6799 "cannot lock ref '{new_ref}': '{existing_ref}' exists; cannot create '{new_ref}'"
6800 ))
6801}
6802
6803fn check_ref_directory_conflict_in_names(name: &str, names: &BTreeSet<String>) -> Result<()> {
6804 let components = name.split('/').collect::<Vec<_>>();
6805 for index in 1..components.len() {
6806 let ancestor = components[..index].join("/");
6807 if names.contains(&ancestor) {
6808 return Err(ref_directory_conflict_error(name, &ancestor));
6809 }
6810 }
6811 let child_prefix = format!("{name}/");
6812 if let Some(existing) = names.range(child_prefix.clone()..).next()
6813 && existing.starts_with(&child_prefix)
6814 {
6815 return Err(ref_directory_conflict_error(name, existing));
6816 }
6817 Ok(())
6818}
6819
6820fn parent_to_ref_name(base: &Path, parent: &Path) -> String {
6821 match parent.strip_prefix(base) {
6822 Ok(suffix) => suffix.to_string_lossy().replace('\\', "/"),
6823 Err(_) => parent.to_string_lossy().into_owned(),
6824 }
6825}
6826
6827fn validate_namespaced_ref(name: &str, prefix: &str, kind: &str) -> Result<()> {
6828 validate_ref_name(name)?;
6829 if name
6830 .strip_prefix(prefix)
6831 .is_none_or(|short_name| short_name.is_empty())
6832 {
6833 return Err(GitError::InvalidPath(format!(
6834 "invalid {kind} ref name {name}"
6835 )));
6836 }
6837 Ok(())
6838}
6839
6840fn validate_short_ref_name(kind: &str, name: &str) -> Result<()> {
6841 if name.is_empty()
6842 || name.starts_with('-')
6843 || name.starts_with('/')
6844 || name.ends_with('/')
6845 || name.contains(' ')
6846 || name.contains('\\')
6847 {
6848 return Err(GitError::InvalidPath(format!("invalid {kind} name {name}")));
6849 }
6850 Ok(())
6851}
6852
6853fn validate_remote_name(remote: &str) -> Result<()> {
6854 validate_short_ref_name("remote", remote)?;
6855 if remote.contains('/') {
6856 return Err(GitError::InvalidPath(format!(
6857 "invalid remote name {remote}"
6858 )));
6859 }
6860 Ok(())
6861}
6862
6863fn prepare_bundle_ref_updates<F>(
6864 refs: &[BundleRefUpdate],
6865 reflog: Option<&BundleRefUpdateReflog>,
6866 mut read_ref: F,
6867) -> Result<(Vec<RefUpdate>, Vec<AppliedBundleRefUpdate>)>
6868where
6869 F: FnMut(&str, &ObjectId) -> Result<Option<RefTarget>>,
6870{
6871 let mut seen = BTreeSet::new();
6872 let mut updates = Vec::with_capacity(refs.len());
6873 let mut applied = Vec::with_capacity(refs.len());
6874 for bundle_ref in refs {
6875 validate_ref_name(&bundle_ref.name)?;
6876 if !seen.insert(bundle_ref.name.clone()) {
6877 return Err(GitError::Transaction(format!(
6878 "duplicate bundle ref {}",
6879 bundle_ref.name
6880 )));
6881 }
6882 let old_oid = match read_ref(&bundle_ref.name, &bundle_ref.oid)? {
6883 Some(RefTarget::Direct(oid)) => Some(oid),
6884 Some(RefTarget::Symbolic(target)) => {
6885 return Err(GitError::Transaction(format!(
6886 "bundle ref {} would overwrite symbolic ref {target}",
6887 bundle_ref.name
6888 )));
6889 }
6890 None => None,
6891 };
6892 let reflog = match reflog {
6893 Some(reflog) => Some(ReflogEntry {
6894 old_oid: match &old_oid {
6895 Some(oid) => *oid,
6896 None => null_oid(bundle_ref.oid.format())?,
6897 },
6898 new_oid: bundle_ref.oid,
6899 committer: reflog.committer.clone(),
6900 message: reflog.message.clone(),
6901 }),
6902 None => None,
6903 };
6904 updates.push(RefUpdate {
6905 name: bundle_ref.name.clone(),
6906 expected: old_oid.map(RefTarget::Direct),
6907 new: RefTarget::Direct(bundle_ref.oid),
6908 reflog,
6909 });
6910 applied.push(AppliedBundleRefUpdate {
6911 name: bundle_ref.name.clone(),
6912 old_oid,
6913 new_oid: bundle_ref.oid,
6914 });
6915 }
6916 Ok((updates, applied))
6917}
6918
6919fn null_oid(format: ObjectFormat) -> Result<ObjectId> {
6920 Ok(ObjectId::null(format))
6921}
6922
6923#[cfg(test)]
6924mod tests {
6925 use super::*;
6926 use std::sync::atomic::{AtomicU64, Ordering};
6927
6928 static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
6929
6930 #[test]
6931 fn refname_patterns_match_git_wildcards_and_case_policy() {
6932 assert!(refname_pattern_matches_case(
6933 "release/*",
6934 "release/1",
6935 false
6936 ));
6937 assert!(refname_pattern_matches_case("V[1-3].?", "v2.0", true));
6938 assert!(!refname_pattern_matches_case("V[1-3].?", "v2.0", false));
6939 assert!(refname_pattern_matches_case(
6940 r"release\*",
6941 "release*",
6942 false
6943 ));
6944 assert!(refname_pattern_matches_case("release[", "release[", false));
6945 }
6946
6947 #[test]
6948 fn core_fsync_reference_component_matches_git_groups_and_negation() {
6949 assert!(!core_fsync_includes_reference("none"));
6950 assert!(!core_fsync_includes_reference("objects,index"));
6951 assert!(!core_fsync_includes_reference("-reference"));
6952 assert!(core_fsync_includes_reference("reference"));
6953 assert!(core_fsync_includes_reference("ref"));
6954 assert!(core_fsync_includes_reference("committed"));
6955 assert!(core_fsync_includes_reference("added"));
6956 assert!(core_fsync_includes_reference("all"));
6957 assert!(core_fsync_includes_reference("reference,-reference"));
6960 assert!(core_fsync_includes_reference("-reference,reference"));
6961 }
6962
6963 #[test]
6964 fn reference_fsync_policy_controls_reference_barriers() {
6965 let disabled_dir = temp_git_dir();
6966 REFERENCE_FSYNC_CALLS.with(|calls| calls.set(0));
6967 FileRefStore::new(&disabled_dir, ObjectFormat::Sha1)
6968 .with_reference_fsync_config(Some("none"), Some("fsync"))
6969 .write_packed_refs(&[])
6970 .expect("disabled reference write");
6971 assert_eq!(REFERENCE_FSYNC_CALLS.with(std::cell::Cell::get), 0);
6972 fs::remove_dir_all(disabled_dir).expect("remove disabled repository");
6973
6974 let enabled_dir = temp_git_dir();
6975 REFERENCE_FSYNC_CALLS.with(|calls| calls.set(0));
6976 FileRefStore::new(&enabled_dir, ObjectFormat::Sha1)
6977 .with_reference_fsync_config(Some("reference"), Some("fsync"))
6978 .write_packed_refs(&[])
6979 .expect("enabled reference write");
6980 assert_eq!(REFERENCE_FSYNC_CALLS.with(std::cell::Cell::get), 1);
6981 fs::remove_dir_all(enabled_dir).expect("remove enabled repository");
6982 }
6983
6984 #[test]
6985 fn reference_fsync_failure_aborts_locked_write_and_cleans_lock() {
6986 let git_dir = temp_git_dir();
6987 REFERENCE_FSYNC_CALLS.with(|calls| calls.set(0));
6988 FAIL_REFERENCE_FSYNC.with(|fail| fail.set(true));
6989 let err = FileRefStore::new(&git_dir, ObjectFormat::Sha1)
6990 .with_reference_fsync_config(Some("reference"), Some("fsync"))
6991 .write_packed_refs(&[])
6992 .expect_err("injected fsync failure must abort");
6993 assert!(err.to_string().contains("injected reference fsync failure"));
6994 assert!(!git_dir.join("packed-refs").exists());
6995 assert!(!git_dir.join("packed-refs.lock").exists());
6996 assert_eq!(REFERENCE_FSYNC_CALLS.with(std::cell::Cell::get), 1);
6997 fs::remove_dir_all(git_dir).expect("remove failed repository");
6998 }
6999
7000 #[test]
7001 fn loose_ref_round_trips_direct() {
7002 let oid = "ce013625030ba8dba906f756967f9e9ca394464a";
7003 let reference = parse_loose_ref(ObjectFormat::Sha1, "refs/heads/main", oid.as_bytes())
7004 .expect("test operation should succeed");
7005 assert_eq!(write_loose_ref(&reference), format!("{oid}\n").into_bytes());
7006 }
7007
7008 #[test]
7009 fn loose_fetch_head_reads_first_object_id() {
7010 let oid = ObjectId::from_hex(
7011 ObjectFormat::Sha1,
7012 "ce013625030ba8dba906f756967f9e9ca394464a",
7013 )
7014 .expect("test operation should succeed");
7015 let bytes = b"ce013625030ba8dba906f756967f9e9ca394464a\t\tbranch 'main' of ../sub\n";
7016 let reference = parse_loose_ref(ObjectFormat::Sha1, "FETCH_HEAD", bytes)
7017 .expect("test operation should succeed");
7018 assert_eq!(reference.target, RefTarget::Direct(oid));
7019 }
7020
7021 #[test]
7022 fn symref_names_allow_onelevel_pseudo_refs() {
7023 for name in ["NOTHEAD", "FOO", "ORIG_HEAD", "TEST_SYMREF"] {
7024 validate_symref_name(name).expect("symref name should be valid");
7025 }
7026 assert!(validate_ref_name("NOTHEAD").is_err());
7027 assert!(validate_symref_target("refs/heads/foo").is_ok());
7028 assert!(validate_symref_target("ORIG_HEAD").is_ok());
7029 assert!(validate_symref_target("foo..bar").is_err());
7030 }
7031
7032 #[test]
7033 fn resolve_ref_peeled_follows_symref_chains() {
7034 let git_dir = temp_git_dir();
7035 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7036 let oid = ObjectId::from_hex(
7037 ObjectFormat::Sha1,
7038 "ce013625030ba8dba906f756967f9e9ca394464a",
7039 )
7040 .expect("test operation should succeed");
7041 let mut tx = store.transaction();
7042 tx.update(RefUpdate {
7043 name: "refs/heads/target".into(),
7044 expected: None,
7045 new: RefTarget::Direct(oid),
7046 reflog: None,
7047 });
7048 tx.commit().expect("seed target ref");
7049 let mut tx = store.transaction();
7050 tx.update(RefUpdate {
7051 name: "refs/heads/alias".into(),
7052 expected: None,
7053 new: RefTarget::Symbolic("refs/heads/target".into()),
7054 reflog: None,
7055 });
7056 tx.commit().expect("seed alias ref");
7057 let mut tx = store.transaction();
7058 tx.update(RefUpdate {
7059 name: "ORIG_HEAD".into(),
7060 expected: None,
7061 new: RefTarget::Symbolic("refs/heads/alias".into()),
7062 reflog: None,
7063 });
7064 tx.commit().expect("seed ORIG_HEAD symref");
7065 assert_eq!(
7066 resolve_ref_peeled(&store, "ORIG_HEAD").expect("resolve ORIG_HEAD"),
7067 Some(oid)
7068 );
7069 let _ = fs::remove_dir_all(git_dir);
7070 }
7071
7072 #[test]
7073 fn symref_directory_conflict_is_reported_gracefully() {
7074 let git_dir = temp_git_dir();
7075 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7076 let oid = ObjectId::from_hex(
7077 ObjectFormat::Sha1,
7078 "ce013625030ba8dba906f756967f9e9ca394464a",
7079 )
7080 .expect("test operation should succeed");
7081 let mut tx = store.transaction();
7082 tx.update(RefUpdate {
7083 name: "refs/heads/df".into(),
7084 expected: None,
7085 new: RefTarget::Direct(oid),
7086 reflog: None,
7087 });
7088 tx.commit().expect("seed branch ref");
7089
7090 let mut tx = store.transaction();
7091 tx.update(RefUpdate {
7092 name: "refs/heads/df/conflict".into(),
7093 expected: None,
7094 new: RefTarget::Symbolic("refs/heads/df".into()),
7095 reflog: None,
7096 });
7097 let err = tx.commit().expect_err("child ref should conflict");
7098 assert!(
7099 matches!(err, GitError::Transaction(message) if message.contains(
7100 "cannot lock ref 'refs/heads/df/conflict'"
7101 ) && message.contains("refs/heads/df"))
7102 );
7103 let _ = fs::remove_dir_all(git_dir);
7104 }
7105
7106 #[test]
7107 fn transaction_checks_expected_value() {
7108 let oid = ObjectId::from_hex(
7109 ObjectFormat::Sha1,
7110 "ce013625030ba8dba906f756967f9e9ca394464a",
7111 )
7112 .expect("test operation should succeed");
7113 let mut store = RefStore::new();
7114 let mut tx = store.transaction();
7115 tx.update(RefUpdate {
7116 name: "refs/heads/main".into(),
7117 expected: None,
7118 new: RefTarget::Direct(oid),
7119 reflog: None,
7120 });
7121 tx.commit().expect("test operation should succeed");
7122 assert_eq!(store.get("refs/heads/main"), Some(&RefTarget::Direct(oid)));
7123 }
7124
7125 #[test]
7126 fn packed_refs_parse_peeled_refs() {
7127 let packed = b"# pack-refs with: peeled fully-peeled sorted \n\
7128ce013625030ba8dba906f756967f9e9ca394464a refs/tags/v1\n\
7129^e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\n";
7130 let refs =
7131 parse_packed_refs(ObjectFormat::Sha1, packed).expect("test operation should succeed");
7132 assert_eq!(refs.len(), 1);
7133 assert_eq!(refs[0].reference.name, "refs/tags/v1");
7134 assert_eq!(
7135 refs[0]
7136 .peeled
7137 .as_ref()
7138 .expect("test operation should succeed")
7139 .to_hex(),
7140 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"
7141 );
7142 }
7143
7144 #[test]
7145 fn packed_refs_write_sorted_with_peeled_refs() {
7146 let head_oid = ObjectId::from_hex(
7147 ObjectFormat::Sha1,
7148 "ce013625030ba8dba906f756967f9e9ca394464a",
7149 )
7150 .expect("test operation should succeed");
7151 let tag_oid = ObjectId::from_hex(
7152 ObjectFormat::Sha1,
7153 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
7154 )
7155 .expect("test operation should succeed");
7156 let peeled_oid = ObjectId::from_hex(
7157 ObjectFormat::Sha1,
7158 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7159 )
7160 .expect("test operation should succeed");
7161 let refs = vec![
7162 PackedRef {
7163 reference: Ref {
7164 name: "refs/tags/v1".into(),
7165 target: RefTarget::Direct(tag_oid),
7166 },
7167 peeled: Some(peeled_oid),
7168 },
7169 PackedRef {
7170 reference: Ref {
7171 name: "refs/heads/main".into(),
7172 target: RefTarget::Direct(head_oid),
7173 },
7174 peeled: None,
7175 },
7176 ];
7177 let bytes = write_packed_refs(&refs).expect("test operation should succeed");
7178 let expected = format!(
7179 "# pack-refs with: peeled fully-peeled sorted \n\
7180{head_oid} refs/heads/main\n\
7181{tag_oid} refs/tags/v1\n\
7182^{peeled_oid}\n"
7183 );
7184 assert_eq!(
7185 String::from_utf8(bytes.clone()).expect("test operation should succeed"),
7186 expected
7187 );
7188 let parsed =
7189 parse_packed_refs(ObjectFormat::Sha1, &bytes).expect("test operation should succeed");
7190 assert_eq!(parsed[0], refs[1]);
7191 assert_eq!(parsed[1], refs[0]);
7192 }
7193
7194 #[test]
7195 fn full_ref_name_validates_and_round_trips_owned() {
7196 let full = FullRefName::new("refs/heads/main").expect("valid full branch ref");
7197 assert_eq!(full.as_str(), "refs/heads/main");
7198 assert_eq!(full.to_string(), "refs/heads/main");
7199 assert_eq!(full.to_owned().into_string(), "refs/heads/main");
7200
7201 let head = FullRefNameBuf::new("HEAD").expect("valid HEAD ref");
7202 assert_eq!(head.as_ref_name().into_str(), "HEAD");
7203
7204 assert!(FullRefName::new("main").is_err());
7205 assert!(FullRefNameBuf::new("refs/heads/bad.lock").is_err());
7206 }
7207
7208 #[test]
7209 fn branch_ref_name_helpers_validate_short_and_full_names() {
7210 let branch =
7211 BranchRefNameBuf::from_branch_name("feature/topic").expect("valid branch short name");
7212 assert_eq!(branch.as_str(), "refs/heads/feature/topic");
7213 assert_eq!(branch.branch_name(), "feature/topic");
7214 assert_eq!(
7215 branch.as_full_ref_name().as_str(),
7216 "refs/heads/feature/topic"
7217 );
7218 assert_eq!(
7219 branch_ref_name("feature/topic").expect("valid branch short name"),
7220 branch.as_str()
7221 );
7222
7223 let borrowed = BranchRefName::from_full("refs/heads/main").expect("valid full branch ref");
7224 assert_eq!(borrowed.branch_name(), "main");
7225 assert_eq!(borrowed.to_owned().into_string(), "refs/heads/main");
7226 assert_eq!(
7227 FullRefName::new("refs/heads/main")
7228 .expect("valid full branch ref")
7229 .as_branch()
7230 .expect("full ref is a branch")
7231 .branch_name(),
7232 "main"
7233 );
7234
7235 assert!(BranchRefName::from_full("refs/tags/main").is_err());
7236 assert!(BranchRefName::from_full("refs/heads").is_err());
7237 assert!(BranchRefNameBuf::from_branch_name("-bad").is_err());
7238 }
7239
7240 #[test]
7241 fn tag_ref_name_helpers_validate_short_and_full_names() {
7242 let tag = TagRefNameBuf::from_tag_name("v1.0").expect("valid tag short name");
7243 assert_eq!(tag.as_str(), "refs/tags/v1.0");
7244 assert_eq!(tag.tag_name(), "v1.0");
7245 assert_eq!(tag.as_full_ref_name().as_str(), "refs/tags/v1.0");
7246 assert_eq!(
7247 tag_ref_name("v1.0").expect("valid tag short name"),
7248 tag.as_str()
7249 );
7250
7251 let borrowed = TagRefName::from_full("refs/tags/release/1").expect("valid full tag ref");
7252 assert_eq!(borrowed.tag_name(), "release/1");
7253 assert_eq!(borrowed.to_owned().into_string(), "refs/tags/release/1");
7254 assert_eq!(
7255 FullRefName::new("refs/tags/release/1")
7256 .expect("valid full tag ref")
7257 .as_tag()
7258 .expect("full ref is a tag")
7259 .tag_name(),
7260 "release/1"
7261 );
7262
7263 assert!(TagRefName::from_full("refs/heads/v1.0").is_err());
7264 assert!(TagRefName::from_full("refs/tags").is_err());
7265 assert!(TagRefNameBuf::from_tag_name("bad tag").is_err());
7266 }
7267
7268 #[test]
7269 fn remote_ref_name_helpers_validate_namespace_and_components() {
7270 let remote = RemoteRefNameBuf::from_remote_branch("origin", "feature/topic")
7271 .expect("valid remote branch ref");
7272 assert_eq!(remote.as_str(), "refs/remotes/origin/feature/topic");
7273 assert_eq!(remote.short_name(), "origin/feature/topic");
7274 assert_eq!(remote.remote_name(), "origin");
7275 assert_eq!(remote.remote_branch(), Some("feature/topic"));
7276 assert_eq!(
7277 remote.as_full_ref_name().as_str(),
7278 "refs/remotes/origin/feature/topic"
7279 );
7280
7281 let head =
7282 RemoteRefName::from_full("refs/remotes/origin/HEAD").expect("valid remote HEAD ref");
7283 assert_eq!(head.remote_name(), "origin");
7284 assert_eq!(head.remote_branch(), Some("HEAD"));
7285 assert_eq!(
7286 FullRefName::new("refs/remotes/upstream/main")
7287 .expect("valid full remote ref")
7288 .as_remote()
7289 .expect("full ref is remote-tracking")
7290 .remote_name(),
7291 "upstream"
7292 );
7293
7294 let short =
7295 RemoteRefNameBuf::from_short_name("origin/main").expect("valid remote short ref");
7296 assert_eq!(short.as_str(), "refs/remotes/origin/main");
7297
7298 assert!(RemoteRefName::from_full("refs/heads/origin/main").is_err());
7299 assert!(RemoteRefName::from_full("refs/remotes/").is_err());
7300 assert!(RemoteRefNameBuf::from_remote_branch("origin/fork", "main").is_err());
7301 }
7302
7303 #[test]
7304 fn file_ref_store_writes_ref_and_reflog() {
7305 let git_dir = temp_git_dir();
7306 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7307 let oid = ObjectId::from_hex(
7308 ObjectFormat::Sha1,
7309 "ce013625030ba8dba906f756967f9e9ca394464a",
7310 )
7311 .expect("test operation should succeed");
7312 let mut tx = store.transaction();
7313 tx.update(RefUpdate {
7314 name: "refs/heads/main".into(),
7315 expected: None,
7316 new: RefTarget::Direct(oid),
7317 reflog: Some(ReflogEntry {
7318 old_oid: zero_oid(ObjectFormat::Sha1).expect("test operation should succeed"),
7319 new_oid: oid,
7320 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
7321 message: b"update by test".to_vec(),
7322 }),
7323 });
7324 tx.commit().expect("test operation should succeed");
7325 assert_eq!(
7326 store
7327 .read_ref("refs/heads/main")
7328 .expect("test operation should succeed"),
7329 Some(RefTarget::Direct(oid))
7330 );
7331 let log = store
7332 .read_reflog("refs/heads/main")
7333 .expect("test operation should succeed");
7334 assert_eq!(log.len(), 1);
7335 assert_eq!(log[0].message, b"update by test");
7336 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7337 }
7338
7339 #[test]
7340 fn file_ref_store_applies_bundle_refs_with_reflog() {
7341 let git_dir = temp_git_dir();
7342 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7343 let old_main = ObjectId::from_hex(
7344 ObjectFormat::Sha1,
7345 "ce013625030ba8dba906f756967f9e9ca394464a",
7346 )
7347 .expect("test operation should succeed");
7348 let new_main = ObjectId::from_hex(
7349 ObjectFormat::Sha1,
7350 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7351 )
7352 .expect("test operation should succeed");
7353 let tag_oid = ObjectId::from_hex(
7354 ObjectFormat::Sha1,
7355 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
7356 )
7357 .expect("test operation should succeed");
7358 let mut tx = store.transaction();
7359 tx.update(RefUpdate {
7360 name: "refs/heads/main".into(),
7361 expected: None,
7362 new: RefTarget::Direct(old_main.clone()),
7363 reflog: None,
7364 });
7365 tx.commit().expect("test operation should succeed");
7366
7367 let applied = store
7368 .apply_bundle_ref_updates(
7369 &[
7370 BundleRefUpdate {
7371 name: "refs/heads/main".into(),
7372 oid: new_main.clone(),
7373 },
7374 BundleRefUpdate {
7375 name: "refs/tags/v1.0".into(),
7376 oid: tag_oid,
7377 },
7378 ],
7379 Some(BundleRefUpdateReflog {
7380 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
7381 message: b"bundle: import refs".to_vec(),
7382 }),
7383 )
7384 .expect("test operation should succeed");
7385
7386 assert_eq!(
7387 applied,
7388 vec![
7389 AppliedBundleRefUpdate {
7390 name: "refs/heads/main".into(),
7391 old_oid: Some(old_main.clone()),
7392 new_oid: new_main.clone(),
7393 },
7394 AppliedBundleRefUpdate {
7395 name: "refs/tags/v1.0".into(),
7396 old_oid: None,
7397 new_oid: tag_oid,
7398 }
7399 ]
7400 );
7401 assert_eq!(
7402 store
7403 .read_ref("refs/heads/main")
7404 .expect("test operation should succeed"),
7405 Some(RefTarget::Direct(new_main.clone()))
7406 );
7407 assert_eq!(
7408 store
7409 .read_ref("refs/tags/v1.0")
7410 .expect("test operation should succeed"),
7411 Some(RefTarget::Direct(tag_oid))
7412 );
7413 let main_log = store
7414 .read_reflog("refs/heads/main")
7415 .expect("test operation should succeed");
7416 assert_eq!(main_log.len(), 1);
7417 assert_eq!(main_log[0].old_oid, old_main);
7418 assert_eq!(main_log[0].new_oid, new_main);
7419 assert_eq!(main_log[0].message, b"bundle: import refs");
7420 let tag_log = store
7421 .read_reflog("refs/tags/v1.0")
7422 .expect("test operation should succeed");
7423 assert_eq!(tag_log.len(), 1);
7424 assert_eq!(
7425 tag_log[0].old_oid,
7426 zero_oid(ObjectFormat::Sha1).expect("test operation should succeed")
7427 );
7428 assert_eq!(tag_log[0].new_oid, tag_oid);
7429 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7430 }
7431
7432 #[test]
7433 fn file_ref_store_rejects_bad_bundle_ref_before_writing() {
7434 let git_dir = temp_git_dir();
7435 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7436 let oid = ObjectId::from_hex(
7437 ObjectFormat::Sha1,
7438 "ce013625030ba8dba906f756967f9e9ca394464a",
7439 )
7440 .expect("test operation should succeed");
7441
7442 let result = store.apply_bundle_ref_updates(
7443 &[
7444 BundleRefUpdate {
7445 name: "refs/heads/main".into(),
7446 oid,
7447 },
7448 BundleRefUpdate {
7449 name: "refs/heads/bad.lock".into(),
7450 oid,
7451 },
7452 ],
7453 None,
7454 );
7455
7456 assert!(result.is_err());
7457 assert_eq!(
7458 store
7459 .read_ref("refs/heads/main")
7460 .expect("test operation should succeed"),
7461 None
7462 );
7463 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7464 }
7465
7466 #[test]
7467 fn file_ref_store_rejects_bundle_ref_over_symbolic_ref() {
7468 let git_dir = temp_git_dir();
7469 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7470 let oid = ObjectId::from_hex(
7471 ObjectFormat::Sha1,
7472 "ce013625030ba8dba906f756967f9e9ca394464a",
7473 )
7474 .expect("test operation should succeed");
7475 let mut tx = store.transaction();
7476 tx.update(RefUpdate {
7477 name: "refs/heads/main".into(),
7478 expected: None,
7479 new: RefTarget::Symbolic("refs/heads/base".into()),
7480 reflog: None,
7481 });
7482 tx.commit().expect("test operation should succeed");
7483
7484 let result = store.apply_bundle_ref_updates(
7485 &[BundleRefUpdate {
7486 name: "refs/heads/main".into(),
7487 oid,
7488 }],
7489 None,
7490 );
7491
7492 assert!(result.is_err());
7493 assert_eq!(
7494 store
7495 .read_ref("refs/heads/main")
7496 .expect("test operation should succeed"),
7497 Some(RefTarget::Symbolic("refs/heads/base".into()))
7498 );
7499 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7500 }
7501
7502 #[test]
7503 fn file_ref_store_expires_reflog_entries_by_timestamp() {
7504 let git_dir = temp_git_dir();
7505 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7506 let first = ObjectId::from_hex(
7507 ObjectFormat::Sha1,
7508 "ce013625030ba8dba906f756967f9e9ca394464a",
7509 )
7510 .expect("test operation should succeed");
7511 let second = ObjectId::from_hex(
7512 ObjectFormat::Sha1,
7513 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7514 )
7515 .expect("test operation should succeed");
7516 let mut tx = store.transaction();
7517 tx.update(RefUpdate {
7518 name: "refs/heads/main".into(),
7519 expected: None,
7520 new: RefTarget::Direct(first.clone()),
7521 reflog: Some(ReflogEntry {
7522 old_oid: zero_oid(ObjectFormat::Sha1).expect("test operation should succeed"),
7523 new_oid: first.clone(),
7524 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
7525 message: b"old".to_vec(),
7526 }),
7527 });
7528 tx.update(RefUpdate {
7529 name: "refs/heads/main".into(),
7530 expected: None,
7531 new: RefTarget::Direct(second.clone()),
7532 reflog: Some(ReflogEntry {
7533 old_oid: first,
7534 new_oid: second.clone(),
7535 committer: b"Git Rs <sley@example.invalid> 100 +0000".to_vec(),
7536 message: b"new".to_vec(),
7537 }),
7538 });
7539 tx.commit().expect("test operation should succeed");
7540
7541 let removed = store
7542 .expire_reflog_older_than("refs/heads/main", 50)
7543 .expect("test operation should succeed");
7544 assert_eq!(removed, 1);
7545 let log = store
7546 .read_reflog("refs/heads/main")
7547 .expect("test operation should succeed");
7548 assert_eq!(log.len(), 1);
7549 assert_eq!(log[0].new_oid, second);
7550 assert_eq!(log[0].message, b"new");
7551 assert!(
7552 !git_dir
7553 .join("logs")
7554 .join("refs")
7555 .join("heads")
7556 .join("main.lock")
7557 .exists()
7558 );
7559 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7560 }
7561
7562 #[test]
7563 fn file_ref_store_creates_branch() {
7564 let git_dir = temp_git_dir();
7565 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7566 let oid = ObjectId::from_hex(
7567 ObjectFormat::Sha1,
7568 "ce013625030ba8dba906f756967f9e9ca394464a",
7569 )
7570 .expect("test operation should succeed");
7571 let branch = store
7572 .create_branch(
7573 "feature",
7574 oid,
7575 b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
7576 b"branch: Created from main".to_vec(),
7577 )
7578 .expect("test operation should succeed");
7579 assert_eq!(branch.name, "refs/heads/feature");
7580 assert_eq!(
7581 store
7582 .read_ref("refs/heads/feature")
7583 .expect("test operation should succeed"),
7584 Some(RefTarget::Direct(oid))
7585 );
7586 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7587 }
7588
7589 #[test]
7590 fn file_ref_store_deletes_loose_branch() {
7591 let git_dir = temp_git_dir();
7592 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7593 let oid = ObjectId::from_hex(
7594 ObjectFormat::Sha1,
7595 "ce013625030ba8dba906f756967f9e9ca394464a",
7596 )
7597 .expect("test operation should succeed");
7598 store
7599 .create_branch(
7600 "feature",
7601 oid,
7602 b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
7603 b"branch: Created from main".to_vec(),
7604 )
7605 .expect("test operation should succeed");
7606 let deleted = store
7607 .delete_branch("feature")
7608 .expect("test operation should succeed");
7609 assert_eq!(deleted.name, "refs/heads/feature");
7610 assert_eq!(deleted.oid, oid);
7611 assert_eq!(
7612 store
7613 .read_ref("refs/heads/feature")
7614 .expect("test operation should succeed"),
7615 None
7616 );
7617 assert!(!git_dir.join("refs").join("heads").join("feature").exists());
7618 assert!(
7619 !git_dir
7620 .join("logs")
7621 .join("refs")
7622 .join("heads")
7623 .join("feature")
7624 .exists()
7625 );
7626 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7627 }
7628
7629 #[test]
7630 fn file_ref_store_deletes_generic_loose_ref() {
7631 let git_dir = temp_git_dir();
7632 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7633 let oid = ObjectId::from_hex(
7634 ObjectFormat::Sha1,
7635 "ce013625030ba8dba906f756967f9e9ca394464a",
7636 )
7637 .expect("test operation should succeed");
7638 let mut tx = store.transaction();
7639 tx.update(RefUpdate {
7640 name: "refs/heads/topic".into(),
7641 expected: None,
7642 new: RefTarget::Direct(oid),
7643 reflog: Some(ReflogEntry {
7644 old_oid: zero_oid(ObjectFormat::Sha1).expect("test operation should succeed"),
7645 new_oid: oid,
7646 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
7647 message: b"update by test".to_vec(),
7648 }),
7649 });
7650 tx.commit().expect("test operation should succeed");
7651 let deleted = store
7652 .delete_ref("refs/heads/topic")
7653 .expect("test operation should succeed");
7654 assert_eq!(deleted.name, "refs/heads/topic");
7655 assert_eq!(deleted.oid, oid);
7656 assert_eq!(
7657 store
7658 .read_ref("refs/heads/topic")
7659 .expect("test operation should succeed"),
7660 None
7661 );
7662 assert!(!git_dir.join("refs").join("heads").join("topic").exists());
7663 assert!(
7664 !git_dir
7665 .join("logs")
7666 .join("refs")
7667 .join("heads")
7668 .join("topic")
7669 .exists()
7670 );
7671 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7672 }
7673
7674 #[test]
7675 fn file_ref_store_delete_prunes_below_namespace_only() {
7676 let git_dir = temp_git_dir();
7677 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7678 let oid = ObjectId::from_hex(
7679 ObjectFormat::Sha1,
7680 "ce013625030ba8dba906f756967f9e9ca394464a",
7681 )
7682 .expect("test operation should succeed");
7683 for name in ["refs/heads/l/m", "refs/foo/bar"] {
7684 let mut tx = store.transaction();
7685 tx.update(RefUpdate {
7686 name: name.into(),
7687 expected: None,
7688 new: RefTarget::Direct(oid),
7689 reflog: None,
7690 });
7691 tx.commit().expect("test operation should succeed");
7692 store
7693 .delete_ref(name)
7694 .expect("test operation should succeed");
7695 }
7696 assert!(!git_dir.join("refs").join("heads").join("l").exists());
7697 assert!(git_dir.join("refs").join("heads").exists());
7698 assert!(!git_dir.join("refs").join("foo").join("bar").exists());
7699 assert!(git_dir.join("refs").join("foo").exists());
7700 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7701 }
7702
7703 #[test]
7704 fn file_ref_store_delete_ref_checked_removes_reflog() {
7705 let git_dir = temp_git_dir();
7706 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7707 let oid = ObjectId::from_hex(
7708 ObjectFormat::Sha1,
7709 "ce013625030ba8dba906f756967f9e9ca394464a",
7710 )
7711 .expect("test operation should succeed");
7712 let mut tx = store.transaction();
7716 tx.update(RefUpdate {
7717 name: "refs/heads/main".into(),
7718 expected: None,
7719 new: RefTarget::Direct(oid),
7720 reflog: Some(ReflogEntry {
7721 old_oid: zero_oid(ObjectFormat::Sha1).expect("test operation should succeed"),
7722 new_oid: oid,
7723 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
7724 message: b"create main".to_vec(),
7725 }),
7726 });
7727 tx.commit().expect("test operation should succeed");
7728 assert!(
7729 git_dir
7730 .join("logs")
7731 .join("refs")
7732 .join("heads")
7733 .join("main")
7734 .exists(),
7735 "reflog file should exist before the checked delete"
7736 );
7737
7738 let deleted = store
7739 .delete_ref_checked(DeleteRef {
7740 name: "refs/heads/main".into(),
7741 expected_old: Some(oid),
7742 reflog: Some(DeleteRefReflog {
7743 committer: b"Git Rs <sley@example.invalid> 123 +0000".to_vec(),
7744 message: b"delete main".to_vec(),
7745 }),
7746 })
7747 .expect("test operation should succeed");
7748
7749 assert_eq!(deleted.name, "refs/heads/main");
7750 assert_eq!(deleted.oid, oid);
7751 assert_eq!(
7752 store
7753 .read_ref("refs/heads/main")
7754 .expect("test operation should succeed"),
7755 None
7756 );
7757 assert!(
7760 !git_dir
7761 .join("logs")
7762 .join("refs")
7763 .join("heads")
7764 .join("main")
7765 .exists(),
7766 "reflog file should be removed by the checked delete"
7767 );
7768 assert!(
7769 store
7770 .read_reflog("refs/heads/main")
7771 .expect("test operation should succeed")
7772 .is_empty()
7773 );
7774 assert!(
7775 !git_dir
7776 .join("refs")
7777 .join("heads")
7778 .join("main.lock")
7779 .exists()
7780 );
7781 assert!(!git_dir.join("packed-refs.lock").exists());
7782 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7783 }
7784
7785 #[test]
7786 fn file_ref_store_delete_ref_checked_stale_expected_leaves_ref_untouched() {
7787 let git_dir = temp_git_dir();
7788 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7789 let actual = ObjectId::from_hex(
7790 ObjectFormat::Sha1,
7791 "ce013625030ba8dba906f756967f9e9ca394464a",
7792 )
7793 .expect("test operation should succeed");
7794 let expected = ObjectId::from_hex(
7795 ObjectFormat::Sha1,
7796 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7797 )
7798 .expect("test operation should succeed");
7799 let mut tx = store.transaction();
7800 tx.update(RefUpdate {
7801 name: "refs/heads/main".into(),
7802 expected: None,
7803 new: RefTarget::Direct(actual),
7804 reflog: None,
7805 });
7806 tx.commit().expect("test operation should succeed");
7807
7808 let err = store
7809 .delete_ref_checked(DeleteRef {
7810 name: "refs/heads/main".into(),
7811 expected_old: Some(expected),
7812 reflog: None,
7813 })
7814 .expect_err("stale expected must fail");
7815
7816 assert!(matches!(
7817 err,
7818 RefDeleteError::ExpectedMismatch {
7819 expected: Some(got_expected),
7820 actual: Some(got_actual),
7821 } if got_expected == expected && got_actual == actual
7822 ));
7823 assert_eq!(
7824 store
7825 .read_ref("refs/heads/main")
7826 .expect("test operation should succeed"),
7827 Some(RefTarget::Direct(actual))
7828 );
7829 assert!(
7830 !git_dir
7831 .join("refs")
7832 .join("heads")
7833 .join("main.lock")
7834 .exists()
7835 );
7836 assert!(!git_dir.join("packed-refs.lock").exists());
7837 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7838 }
7839
7840 #[test]
7841 fn file_ref_store_delete_ref_checked_missing_returns_not_found() {
7842 let git_dir = temp_git_dir();
7843 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7844
7845 let err = store
7846 .delete_ref_checked(DeleteRef {
7847 name: "refs/heads/missing".into(),
7848 expected_old: None,
7849 reflog: None,
7850 })
7851 .expect_err("missing ref must fail");
7852
7853 assert!(matches!(err, RefDeleteError::NotFound));
7854 assert!(
7855 !git_dir
7856 .join("refs")
7857 .join("heads")
7858 .join("missing.lock")
7859 .exists()
7860 );
7861 assert!(!git_dir.join("packed-refs.lock").exists());
7862 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7863 }
7864
7865 #[test]
7866 fn file_ref_store_delete_ref_checked_removes_packed_ref() {
7867 let git_dir = temp_git_dir();
7868 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7869 let oid = ObjectId::from_hex(
7870 ObjectFormat::Sha1,
7871 "ce013625030ba8dba906f756967f9e9ca394464a",
7872 )
7873 .expect("test operation should succeed");
7874 let other = ObjectId::from_hex(
7875 ObjectFormat::Sha1,
7876 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7877 )
7878 .expect("test operation should succeed");
7879 store
7880 .write_packed_refs(&[
7881 PackedRef {
7882 reference: Ref {
7883 name: "refs/heads/main".into(),
7884 target: RefTarget::Direct(oid),
7885 },
7886 peeled: None,
7887 },
7888 PackedRef {
7889 reference: Ref {
7890 name: "refs/heads/other".into(),
7891 target: RefTarget::Direct(other),
7892 },
7893 peeled: None,
7894 },
7895 ])
7896 .expect("test operation should succeed");
7897
7898 store
7899 .delete_ref_checked(DeleteRef {
7900 name: "refs/heads/main".into(),
7901 expected_old: Some(oid),
7902 reflog: None,
7903 })
7904 .expect("test operation should succeed");
7905
7906 assert_eq!(
7907 store
7908 .read_ref("refs/heads/main")
7909 .expect("test operation should succeed"),
7910 None
7911 );
7912 assert_eq!(
7913 store
7914 .read_ref("refs/heads/other")
7915 .expect("test operation should succeed"),
7916 Some(RefTarget::Direct(other))
7917 );
7918 let packed =
7919 fs::read_to_string(git_dir.join("packed-refs")).expect("test operation should succeed");
7920 assert!(!packed.contains("refs/heads/main"));
7921 assert!(packed.contains("refs/heads/other"));
7922 assert!(
7923 !git_dir
7924 .join("refs")
7925 .join("heads")
7926 .join("main.lock")
7927 .exists()
7928 );
7929 assert!(!git_dir.join("packed-refs.lock").exists());
7930 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7931 }
7932
7933 #[test]
7934 fn file_ref_store_delete_ref_checked_lock_conflict_returns_locked() {
7935 let git_dir = temp_git_dir();
7936 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7937 let oid = ObjectId::from_hex(
7938 ObjectFormat::Sha1,
7939 "ce013625030ba8dba906f756967f9e9ca394464a",
7940 )
7941 .expect("test operation should succeed");
7942 let mut tx = store.transaction();
7943 tx.update(RefUpdate {
7944 name: "refs/heads/main".into(),
7945 expected: None,
7946 new: RefTarget::Direct(oid),
7947 reflog: None,
7948 });
7949 tx.commit().expect("test operation should succeed");
7950 fs::write(
7951 git_dir.join("refs").join("heads").join("main.lock"),
7952 b"held\n",
7953 )
7954 .expect("test operation should succeed");
7955
7956 let err = store
7957 .delete_ref_checked(DeleteRef {
7958 name: "refs/heads/main".into(),
7959 expected_old: Some(oid),
7960 reflog: None,
7961 })
7962 .expect_err("held lock must fail");
7963
7964 assert!(matches!(err, RefDeleteError::Locked));
7965 assert_eq!(
7966 store
7967 .read_ref("refs/heads/main")
7968 .expect("test operation should succeed"),
7969 Some(RefTarget::Direct(oid))
7970 );
7971 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7972 }
7973
7974 #[test]
7975 fn file_ref_store_reports_current_branch() {
7976 let git_dir = temp_git_dir();
7977 fs::write(git_dir.join("HEAD"), b"ref: refs/heads/main\n")
7978 .expect("test operation should succeed");
7979 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7980 assert_eq!(
7981 store
7982 .current_branch_ref()
7983 .expect("test operation should succeed"),
7984 Some("refs/heads/main".into())
7985 );
7986 assert_eq!(
7987 store
7988 .current_branch()
7989 .expect("test operation should succeed"),
7990 Some("main".into())
7991 );
7992 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7993 }
7994
7995 #[test]
7996 fn file_ref_store_resolves_linked_worktree_head_through_common_refs() {
7997 let common = temp_git_dir();
7998 let admin = common.join("worktrees").join("linked");
7999 fs::create_dir_all(&admin).expect("test operation should succeed");
8000 fs::write(admin.join("commondir"), "../..\n").expect("test operation should succeed");
8001 fs::write(admin.join("HEAD"), b"ref: refs/heads/topic\n")
8002 .expect("test operation should succeed");
8003 let oid = ObjectId::from_hex(
8004 ObjectFormat::Sha256,
8005 "08ffba112b648c22b5425f01bec2c37ffc524c4d48ef04337779df3973733050",
8006 )
8007 .expect("test operation should succeed");
8008 fs::create_dir_all(common.join("refs").join("heads"))
8009 .expect("test operation should succeed");
8010 fs::write(
8011 common.join("refs").join("heads").join("topic"),
8012 format!("{oid}\n"),
8013 )
8014 .expect("test operation should succeed");
8015
8016 let store = FileRefStore::new(&admin, ObjectFormat::Sha256);
8017 assert_eq!(
8018 store
8019 .read_ref("HEAD")
8020 .expect("test operation should succeed"),
8021 Some(RefTarget::Symbolic("refs/heads/topic".into()))
8022 );
8023 assert_eq!(
8024 store
8025 .read_ref("refs/heads/topic")
8026 .expect("test operation should succeed"),
8027 Some(RefTarget::Direct(oid))
8028 );
8029
8030 fs::remove_dir_all(common).expect("test operation should succeed");
8031 }
8032
8033 #[test]
8034 fn file_ref_store_creates_tag() {
8035 let git_dir = temp_git_dir();
8036 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8037 let oid = ObjectId::from_hex(
8038 ObjectFormat::Sha1,
8039 "ce013625030ba8dba906f756967f9e9ca394464a",
8040 )
8041 .expect("test operation should succeed");
8042 let tag = store
8043 .create_tag("v1.0", oid)
8044 .expect("test operation should succeed");
8045 assert_eq!(tag.name, "refs/tags/v1.0");
8046 assert_eq!(
8047 store
8048 .read_ref("refs/tags/v1.0")
8049 .expect("test operation should succeed"),
8050 Some(RefTarget::Direct(oid))
8051 );
8052 assert!(
8053 store
8054 .read_reflog("refs/tags/v1.0")
8055 .expect("test operation should succeed")
8056 .is_empty()
8057 );
8058 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8059 }
8060
8061 #[test]
8062 fn file_ref_store_deletes_loose_tag() {
8063 let git_dir = temp_git_dir();
8064 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8065 let oid = ObjectId::from_hex(
8066 ObjectFormat::Sha1,
8067 "ce013625030ba8dba906f756967f9e9ca394464a",
8068 )
8069 .expect("test operation should succeed");
8070 store
8071 .create_tag("v1.0", oid)
8072 .expect("test operation should succeed");
8073 let deleted = store
8074 .delete_tag("v1.0")
8075 .expect("test operation should succeed");
8076 assert_eq!(deleted.name, "refs/tags/v1.0");
8077 assert_eq!(deleted.oid, oid);
8078 assert_eq!(
8079 store
8080 .read_ref("refs/tags/v1.0")
8081 .expect("test operation should succeed"),
8082 None
8083 );
8084 assert!(!git_dir.join("refs").join("tags").join("v1.0").exists());
8085 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8086 }
8087
8088 #[test]
8089 fn file_ref_store_reads_packed_ref() {
8090 let git_dir = temp_git_dir();
8091 fs::write(
8092 git_dir.join("packed-refs"),
8093 b"ce013625030ba8dba906f756967f9e9ca394464a refs/heads/main\n",
8094 )
8095 .expect("test operation should succeed");
8096 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8097 assert!(matches!(
8098 store
8099 .read_ref("refs/heads/main")
8100 .expect("test operation should succeed"),
8101 Some(RefTarget::Direct(_))
8102 ));
8103 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8104 }
8105
8106 #[test]
8107 fn file_ref_store_lists_loose_refs_over_packed_refs() {
8108 let git_dir = temp_git_dir();
8109 fs::write(
8110 git_dir.join("packed-refs"),
8111 b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 refs/heads/main\n",
8112 )
8113 .expect("test operation should succeed");
8114 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8115 let oid = ObjectId::from_hex(
8116 ObjectFormat::Sha1,
8117 "ce013625030ba8dba906f756967f9e9ca394464a",
8118 )
8119 .expect("test operation should succeed");
8120 let mut tx = store.transaction();
8121 tx.update(RefUpdate {
8122 name: "refs/heads/main".into(),
8123 expected: None,
8124 new: RefTarget::Direct(oid),
8125 reflog: None,
8126 });
8127 tx.commit().expect("test operation should succeed");
8128 let refs = store.list_refs().expect("test operation should succeed");
8129 assert_eq!(refs.len(), 1);
8130 assert_eq!(refs[0].target, RefTarget::Direct(oid));
8131 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8132 }
8133
8134 #[test]
8135 fn file_ref_store_lists_refs_with_prefix_and_preserves_loose_shadowing() {
8136 let git_dir = temp_git_dir();
8137 fs::write(
8138 git_dir.join("packed-refs"),
8139 b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 refs/heads/main\n\
8140 18f002b4484b838b205a48b1e9e6763ba5e3a607 refs/heads/topic\n\
8141 ce013625030ba8dba906f756967f9e9ca394464a refs/tags/v1.0\n",
8142 )
8143 .expect("test operation should succeed");
8144 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8145 let loose_main = ObjectId::from_hex(
8146 ObjectFormat::Sha1,
8147 "ce013625030ba8dba906f756967f9e9ca394464a",
8148 )
8149 .expect("test operation should succeed");
8150 let packed_topic = ObjectId::from_hex(
8151 ObjectFormat::Sha1,
8152 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
8153 )
8154 .expect("test operation should succeed");
8155 let mut tx = store.transaction();
8156 tx.update(RefUpdate {
8157 name: "refs/heads/main".into(),
8158 expected: None,
8159 new: RefTarget::Direct(loose_main),
8160 reflog: None,
8161 });
8162 tx.commit().expect("test operation should succeed");
8163
8164 assert_eq!(
8165 store
8166 .list_refs_with_prefix("refs/heads/")
8167 .expect("test operation should succeed"),
8168 vec![
8169 Ref {
8170 name: "refs/heads/main".into(),
8171 target: RefTarget::Direct(loose_main),
8172 },
8173 Ref {
8174 name: "refs/heads/topic".into(),
8175 target: RefTarget::Direct(packed_topic),
8176 },
8177 ]
8178 );
8179 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8180 }
8181
8182 #[test]
8183 fn file_ref_store_writes_packed_refs() {
8184 let git_dir = temp_git_dir();
8185 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8186 let oid = ObjectId::from_hex(
8187 ObjectFormat::Sha1,
8188 "ce013625030ba8dba906f756967f9e9ca394464a",
8189 )
8190 .expect("test operation should succeed");
8191 store
8192 .write_packed_refs(&[PackedRef {
8193 reference: Ref {
8194 name: "refs/heads/main".into(),
8195 target: RefTarget::Direct(oid),
8196 },
8197 peeled: None,
8198 }])
8199 .expect("test operation should succeed");
8200 assert_eq!(
8201 store
8202 .read_ref("refs/heads/main")
8203 .expect("test operation should succeed"),
8204 Some(RefTarget::Direct(oid))
8205 );
8206 let refs = store.list_refs().expect("test operation should succeed");
8207 assert_eq!(refs.len(), 1);
8208 assert_eq!(refs[0].target, RefTarget::Direct(oid));
8209 assert!(git_dir.join("packed-refs").exists());
8210 assert!(!git_dir.join("packed-refs.lock").exists());
8211 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8212 }
8213
8214 #[test]
8215 fn file_ref_store_checks_ref_prefix_in_packed_refs() {
8216 let git_dir = temp_git_dir();
8217 fs::write(
8218 git_dir.join("packed-refs"),
8219 b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 refs/heads/main\n\
8220 ce013625030ba8dba906f756967f9e9ca394464a refs/replace/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\n",
8221 )
8222 .expect("test operation should succeed");
8223 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8224 assert!(
8225 store
8226 .has_refs_with_prefix("refs/replace/")
8227 .expect("test operation should succeed")
8228 );
8229 assert!(
8230 !store
8231 .has_refs_with_prefix("refs/notes/")
8232 .expect("test operation should succeed")
8233 );
8234 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8235 }
8236
8237 #[test]
8238 fn file_ref_store_checks_ref_prefix_in_loose_refs() {
8239 let git_dir = temp_git_dir();
8240 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8241 let oid = ObjectId::from_hex(
8242 ObjectFormat::Sha1,
8243 "ce013625030ba8dba906f756967f9e9ca394464a",
8244 )
8245 .expect("test operation should succeed");
8246 let mut tx = store.transaction();
8247 tx.update(RefUpdate {
8248 name: "refs/replace/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391".into(),
8249 expected: None,
8250 new: RefTarget::Direct(oid),
8251 reflog: None,
8252 });
8253 tx.commit().expect("test operation should succeed");
8254 assert!(
8255 store
8256 .has_refs_with_prefix("refs/replace/")
8257 .expect("test operation should succeed")
8258 );
8259 assert!(
8260 !store
8261 .has_refs_with_prefix("refs/notes/")
8262 .expect("test operation should succeed")
8263 );
8264 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8265 }
8266
8267 #[test]
8268 fn file_ref_store_reads_reftable_stack_and_ignores_dummy_head() {
8269 let git_dir = temp_git_dir();
8270 write_reftable_config(&git_dir);
8271 fs::write(git_dir.join("HEAD"), b"ref: refs/heads/.invalid\n")
8272 .expect("test operation should succeed");
8273 let head_oid = ObjectId::from_hex(
8274 ObjectFormat::Sha1,
8275 "ce013625030ba8dba906f756967f9e9ca394464a",
8276 )
8277 .expect("test operation should succeed");
8278 let tag_oid = ObjectId::from_hex(
8279 ObjectFormat::Sha1,
8280 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
8281 )
8282 .expect("test operation should succeed");
8283 let peeled_oid = ObjectId::from_hex(
8284 ObjectFormat::Sha1,
8285 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
8286 )
8287 .expect("test operation should succeed");
8288 write_reftable_stack(
8289 &git_dir,
8290 &[(
8291 "0x000000000001-0x000000000001-00000000.ref",
8292 vec![
8293 sley_formats::ReftableRefRecord {
8294 name: "HEAD".into(),
8295 update_index: 1,
8296 value: ReftableRefValue::Symbolic("refs/heads/main".into()),
8297 },
8298 sley_formats::ReftableRefRecord {
8299 name: "refs/heads/main".into(),
8300 update_index: 1,
8301 value: ReftableRefValue::Direct(head_oid),
8302 },
8303 sley_formats::ReftableRefRecord {
8304 name: "refs/tags/v1.0".into(),
8305 update_index: 1,
8306 value: ReftableRefValue::Peeled {
8307 target: tag_oid,
8308 peeled: peeled_oid,
8309 },
8310 },
8311 ],
8312 )],
8313 );
8314
8315 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8316 assert_eq!(
8317 store
8318 .read_ref("HEAD")
8319 .expect("test operation should succeed"),
8320 Some(RefTarget::Symbolic("refs/heads/main".into()))
8321 );
8322 assert_eq!(
8323 store
8324 .read_ref("refs/heads/main")
8325 .expect("test operation should succeed"),
8326 Some(RefTarget::Direct(head_oid))
8327 );
8328 assert_eq!(
8329 store
8330 .read_ref("refs/tags/v1.0")
8331 .expect("test operation should succeed"),
8332 Some(RefTarget::Direct(tag_oid))
8333 );
8334 let refs = store.list_refs().expect("test operation should succeed");
8335 assert_eq!(
8336 refs,
8337 vec![
8338 Ref {
8339 name: "refs/heads/main".into(),
8340 target: RefTarget::Direct(head_oid),
8341 },
8342 Ref {
8343 name: "refs/tags/v1.0".into(),
8344 target: RefTarget::Direct(tag_oid),
8345 },
8346 ]
8347 );
8348 assert_eq!(
8349 store
8350 .list_refs_with_prefix("refs/tags/")
8351 .expect("test operation should succeed"),
8352 vec![Ref {
8353 name: "refs/tags/v1.0".into(),
8354 target: RefTarget::Direct(tag_oid),
8355 }]
8356 );
8357
8358 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8359 }
8360
8361 #[test]
8362 fn root_ref_listing_is_backend_independent() {
8363 let oid = ObjectId::from_hex(
8364 ObjectFormat::Sha1,
8365 "ce013625030ba8dba906f756967f9e9ca394464a",
8366 )
8367 .expect("test operation should succeed");
8368 let expected = vec![
8369 Ref {
8370 name: "AUTO_MERGE".into(),
8371 target: RefTarget::Direct(oid),
8372 },
8373 Ref {
8374 name: "FOO_HEAD".into(),
8375 target: RefTarget::Direct(oid),
8376 },
8377 Ref {
8378 name: "HEAD".into(),
8379 target: RefTarget::Symbolic("refs/heads/main".into()),
8380 },
8381 ];
8382
8383 let files_dir = temp_git_dir();
8384 fs::write(files_dir.join("AUTO_MERGE"), format!("{oid}\n")).expect("write files root ref");
8385 fs::write(files_dir.join("FOO_HEAD"), format!("{oid}\n")).expect("write files root ref");
8386 fs::write(files_dir.join("HEAD"), b"ref: refs/heads/main\n").expect("write files root ref");
8387 fs::write(files_dir.join("FETCH_HEAD"), format!("{oid}\n"))
8388 .expect("write working-state file");
8389 fs::write(files_dir.join("MERGE_HEAD"), format!("{oid}\n"))
8390 .expect("write working-state file");
8391 let files = FileRefStore::new(&files_dir, ObjectFormat::Sha1)
8392 .list_root_refs()
8393 .expect("list files root refs");
8394
8395 let reftable_dir = temp_git_dir();
8396 write_reftable_config(&reftable_dir);
8397 write_reftable_stack(
8398 &reftable_dir,
8399 &[(
8400 "0x000000000001-0x000000000001-00000000.ref",
8401 vec![
8402 ReftableRefRecord {
8403 name: "AUTO_MERGE".into(),
8404 update_index: 1,
8405 value: ReftableRefValue::Direct(oid),
8406 },
8407 ReftableRefRecord {
8408 name: "FETCH_HEAD".into(),
8409 update_index: 1,
8410 value: ReftableRefValue::Direct(oid),
8411 },
8412 ReftableRefRecord {
8413 name: "FOO_HEAD".into(),
8414 update_index: 1,
8415 value: ReftableRefValue::Direct(oid),
8416 },
8417 ReftableRefRecord {
8418 name: "HEAD".into(),
8419 update_index: 1,
8420 value: ReftableRefValue::Symbolic("refs/heads/main".into()),
8421 },
8422 ReftableRefRecord {
8423 name: "MERGE_HEAD".into(),
8424 update_index: 1,
8425 value: ReftableRefValue::Direct(oid),
8426 },
8427 ReftableRefRecord {
8428 name: "refs/heads/main".into(),
8429 update_index: 1,
8430 value: ReftableRefValue::Direct(oid),
8431 },
8432 ],
8433 )],
8434 );
8435 let reftable = FileRefStore::new(&reftable_dir, ObjectFormat::Sha1)
8436 .list_root_refs()
8437 .expect("list reftable root refs");
8438
8439 assert_eq!(files, expected);
8440 assert_eq!(reftable, expected);
8441 fs::remove_dir_all(files_dir).expect("remove test files repo");
8442 fs::remove_dir_all(reftable_dir).expect("remove test reftable repo");
8443 }
8444
8445 #[test]
8446 fn file_ref_store_reads_loose_fetch_head_in_reftable_repo() {
8447 let git_dir = temp_git_dir();
8448 write_reftable_config(&git_dir);
8449 fs::write(git_dir.join("HEAD"), b"ref: refs/heads/.invalid\n")
8450 .expect("test operation should succeed");
8451 fs::create_dir_all(git_dir.join("reftable")).expect("test operation should succeed");
8452 fs::write(git_dir.join("reftable").join("tables.list"), b"")
8453 .expect("test operation should succeed");
8454 let oid = ObjectId::from_hex(
8455 ObjectFormat::Sha1,
8456 "ce013625030ba8dba906f756967f9e9ca394464a",
8457 )
8458 .expect("test operation should succeed");
8459 fs::write(
8460 git_dir.join("FETCH_HEAD"),
8461 b"ce013625030ba8dba906f756967f9e9ca394464a\t\tbranch 'main' of ../sub\n",
8462 )
8463 .expect("test operation should succeed");
8464
8465 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8466 assert_eq!(
8467 store
8468 .read_ref("FETCH_HEAD")
8469 .expect("test operation should succeed"),
8470 Some(RefTarget::Direct(oid))
8471 );
8472 assert!(
8473 store
8474 .raw_ref_exists("FETCH_HEAD")
8475 .expect("test operation should succeed")
8476 );
8477
8478 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8479 }
8480
8481 #[test]
8482 fn file_ref_store_empty_reftable_reflog_rewrite_keeps_marker() {
8483 let git_dir = temp_git_dir();
8484 write_reftable_config(&git_dir);
8485 fs::create_dir_all(git_dir.join("reftable")).expect("test operation should succeed");
8486 fs::write(git_dir.join("reftable").join("tables.list"), b"")
8487 .expect("test operation should succeed");
8488 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8489 let null = ObjectId::null(ObjectFormat::Sha1);
8490 let oid = ObjectId::from_hex(
8491 ObjectFormat::Sha1,
8492 "ce013625030ba8dba906f756967f9e9ca394464a",
8493 )
8494 .expect("test operation should succeed");
8495
8496 store
8497 .write_reflog(
8498 "refs/heads/main",
8499 &[ReflogEntry {
8500 old_oid: null,
8501 new_oid: oid,
8502 committer: b"A U Thor <author@example.com> 1 +0000".to_vec(),
8503 message: b"commit: initial".to_vec(),
8504 }],
8505 )
8506 .expect("test operation should succeed");
8507
8508 store
8509 .write_reflog("refs/heads/main", &[])
8510 .expect("test operation should succeed");
8511
8512 assert!(
8513 store
8514 .read_reflog("refs/heads/main")
8515 .expect("test operation should succeed")
8516 .is_empty()
8517 );
8518 assert!(
8519 store
8520 .reflog_exists("refs/heads/main")
8521 .expect("test operation should succeed")
8522 );
8523 let tables = store.reftables().expect("test operation should succeed");
8524 assert!(
8525 tables
8526 .windows(2)
8527 .all(|pair| pair[0].header.max_update_index < pair[1].header.min_update_index)
8528 );
8529 let marker = tables
8530 .iter()
8531 .flat_map(|table| table.logs.iter())
8532 .find(|record| {
8533 record.refname == "refs/heads/main"
8534 && matches!(
8535 &record.value,
8536 ReftableLogValue::Update(update)
8537 if update.old_oid == null && update.new_oid == null
8538 )
8539 })
8540 .expect("empty reflog marker should exist");
8541 let ReftableLogValue::Update(update) = &marker.value else {
8542 panic!("empty reflog marker should be an update");
8543 };
8544 assert_eq!(update.old_oid, null);
8545 assert_eq!(update.new_oid, null);
8546
8547 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8548 }
8549
8550 #[test]
8551 fn file_ref_store_limits_reftable_reflog_messages_to_half_a_block() {
8552 let git_dir = temp_git_dir();
8553 write_reftable_config(&git_dir);
8554 fs::create_dir_all(git_dir.join("reftable")).expect("test operation should succeed");
8555 fs::write(git_dir.join("reftable").join("tables.list"), b"")
8556 .expect("test operation should succeed");
8557 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1).with_reftable_write_options(
8558 ReftableWriteOptions {
8559 block_size: 0,
8560 ..ReftableWriteOptions::default()
8561 },
8562 );
8563 let oid = ObjectId::from_hex(
8564 ObjectFormat::Sha1,
8565 "ce013625030ba8dba906f756967f9e9ca394464a",
8566 )
8567 .expect("test operation should succeed");
8568 let mut tx = store.transaction();
8569 tx.update(RefUpdate {
8570 name: "refs/heads/main".into(),
8571 expected: None,
8572 new: RefTarget::Direct(oid),
8573 reflog: Some(ReflogEntry {
8574 old_oid: ObjectId::null(ObjectFormat::Sha1),
8575 new_oid: oid,
8576 committer: b"A U Thor <author@example.com> 1 +0000".to_vec(),
8577 message: vec![b'x'; 50_000],
8578 }),
8579 });
8580 tx.commit()
8581 .expect("long reflog message should be truncated");
8582
8583 let message = store
8584 .reftables()
8585 .expect("test operation should succeed")
8586 .into_iter()
8587 .flat_map(|table| table.logs)
8588 .find_map(|record| match record.value {
8589 ReftableLogValue::Update(update) => Some(update.message),
8590 ReftableLogValue::Deletion => None,
8591 })
8592 .expect("reflog update should exist");
8593 assert_eq!(message.len(), 2048);
8594
8595 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8596 }
8597
8598 #[test]
8599 fn file_ref_store_lists_only_visible_reftable_reflog_names() {
8600 let git_dir = temp_git_dir();
8601 write_reftable_config(&git_dir);
8602 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8603 let oid = ObjectId::from_hex(
8604 ObjectFormat::Sha1,
8605 "ce013625030ba8dba906f756967f9e9ca394464a",
8606 )
8607 .expect("test operation should succeed");
8608
8609 store
8610 .write_reflog("refs/heads/empty", &[])
8611 .expect("test operation should succeed");
8612 store
8613 .append_reflog("refs/heads/main", &reflog_entry(&oid, 1, "create main"))
8614 .expect("test operation should succeed");
8615
8616 assert_eq!(
8617 store
8618 .list_reflog_names()
8619 .expect("test operation should succeed"),
8620 vec!["refs/heads/main".to_string()]
8621 );
8622
8623 store
8624 .write_reflog("refs/heads/main", &[])
8625 .expect("test operation should succeed");
8626 assert!(
8627 store
8628 .list_reflog_names()
8629 .expect("test operation should succeed")
8630 .is_empty()
8631 );
8632
8633 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8634 }
8635
8636 #[test]
8637 fn file_ref_store_applies_reftable_stack_overrides_and_deletions() {
8638 let git_dir = temp_git_dir();
8639 write_reftable_config(&git_dir);
8640 let first = ObjectId::from_hex(
8641 ObjectFormat::Sha1,
8642 "ce013625030ba8dba906f756967f9e9ca394464a",
8643 )
8644 .expect("test operation should succeed");
8645 let second = ObjectId::from_hex(
8646 ObjectFormat::Sha1,
8647 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
8648 )
8649 .expect("test operation should succeed");
8650 write_reftable_stack(
8651 &git_dir,
8652 &[
8653 (
8654 "0x000000000001-0x000000000001-00000000.ref",
8655 vec![
8656 sley_formats::ReftableRefRecord {
8657 name: "refs/heads/main".into(),
8658 update_index: 1,
8659 value: ReftableRefValue::Direct(first),
8660 },
8661 sley_formats::ReftableRefRecord {
8662 name: "refs/heads/topic".into(),
8663 update_index: 1,
8664 value: ReftableRefValue::Direct(second.clone()),
8665 },
8666 ],
8667 ),
8668 (
8669 "000000000002-000000000002-tip.ref",
8670 vec![
8671 sley_formats::ReftableRefRecord {
8672 name: "refs/heads/main".into(),
8673 update_index: 2,
8674 value: ReftableRefValue::Direct(second.clone()),
8675 },
8676 sley_formats::ReftableRefRecord {
8677 name: "refs/heads/topic".into(),
8678 update_index: 2,
8679 value: ReftableRefValue::Deletion,
8680 },
8681 ],
8682 ),
8683 ],
8684 );
8685
8686 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8687 assert_eq!(
8688 store
8689 .read_ref("refs/heads/main")
8690 .expect("test operation should succeed"),
8691 Some(RefTarget::Direct(second.clone()))
8692 );
8693 assert_eq!(
8694 store
8695 .read_ref("refs/heads/topic")
8696 .expect("test operation should succeed"),
8697 None
8698 );
8699 assert_eq!(
8700 store.list_refs().expect("test operation should succeed"),
8701 vec![Ref {
8702 name: "refs/heads/main".into(),
8703 target: RefTarget::Direct(second),
8704 }]
8705 );
8706
8707 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8708 }
8709
8710 #[test]
8711 fn file_ref_store_writes_reftable_transaction_table() {
8712 let git_dir = temp_git_dir();
8713 write_reftable_config(&git_dir);
8714 let first = ObjectId::from_hex(
8715 ObjectFormat::Sha1,
8716 "ce013625030ba8dba906f756967f9e9ca394464a",
8717 )
8718 .expect("test operation should succeed");
8719 let second = ObjectId::from_hex(
8720 ObjectFormat::Sha1,
8721 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
8722 )
8723 .expect("test operation should succeed");
8724 write_reftable_stack(
8725 &git_dir,
8726 &[(
8727 "0x000000000001-0x000000000001-00000000.ref",
8728 vec![sley_formats::ReftableRefRecord {
8729 name: "refs/heads/main".into(),
8730 update_index: 1,
8731 value: ReftableRefValue::Direct(first),
8732 }],
8733 )],
8734 );
8735
8736 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8737 let mut tx = store.transaction();
8738 tx.update(RefUpdate {
8739 name: "HEAD".into(),
8740 expected: None,
8741 new: RefTarget::Symbolic("refs/heads/main".into()),
8742 reflog: None,
8743 });
8744 tx.update(RefUpdate {
8745 name: "refs/heads/main".into(),
8746 expected: None,
8747 new: RefTarget::Direct(second.clone()),
8748 reflog: None,
8749 });
8750 tx.commit().expect("test operation should succeed");
8751
8752 assert_eq!(
8753 store
8754 .read_ref("HEAD")
8755 .expect("test operation should succeed"),
8756 Some(RefTarget::Symbolic("refs/heads/main".into()))
8757 );
8758 assert_eq!(
8759 store
8760 .read_ref("refs/heads/main")
8761 .expect("test operation should succeed"),
8762 Some(RefTarget::Direct(second.clone()))
8763 );
8764 assert_eq!(
8765 store
8766 .list_refs()
8767 .expect("test operation should succeed")
8768 .len(),
8769 1
8770 );
8771 assert!(!git_dir.join("HEAD").exists());
8772 let tables = fs::read_to_string(git_dir.join("reftable").join("tables.list"))
8773 .expect("test operation should succeed");
8774 assert_eq!(tables.lines().count(), 2);
8775 let last = tables
8776 .lines()
8777 .last()
8778 .expect("test operation should succeed");
8779 assert!(
8783 last.starts_with("0x") && last.ends_with(".ref"),
8784 "expected git-format reftable name in tables.list, got {tables}"
8785 );
8786 assert!(
8787 reftable_table_name_is_valid(last),
8788 "rust-written reftable name must parse as git's hex format, got {last}"
8789 );
8790
8791 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8792 }
8793
8794 #[test]
8795 fn reftable_auto_compaction_uses_git_geometric_segments() {
8796 assert_eq!(
8797 reftable_compaction_segment(&[512, 64, 17, 16, 9, 9, 9, 16, 2, 16], 2),
8798 Some((1, 10))
8799 );
8800 assert_eq!(reftable_compaction_segment(&[64, 32, 16, 8, 4, 2], 2), None);
8801 }
8802
8803 #[test]
8804 fn file_ref_store_geometric_compaction_keeps_a_stack() {
8805 let git_dir = temp_git_dir();
8806 write_reftable_config(&git_dir);
8807 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8808 let oid = ObjectId::from_hex(
8809 ObjectFormat::Sha1,
8810 "ce013625030ba8dba906f756967f9e9ca394464a",
8811 )
8812 .expect("test operation should succeed");
8813
8814 for index in 1..=20 {
8815 let mut tx = store.transaction();
8816 tx.update(RefUpdate {
8817 name: format!("refs/heads/branch-{index}"),
8818 expected: None,
8819 new: RefTarget::Direct(oid),
8820 reflog: None,
8821 });
8822 tx.commit().expect("test operation should succeed");
8823 }
8824
8825 let table_count = store
8826 .reftable_table_count()
8827 .expect("test operation should succeed");
8828 assert!(table_count > 1, "geometric stack collapsed to one table");
8829 assert!(table_count < 20, "geometric stack was never compacted");
8830
8831 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8832 }
8833
8834 #[test]
8835 fn combined_reftable_transactions_auto_compact_geometrically() {
8836 let git_dir = temp_git_dir();
8837 write_reftable_config(&git_dir);
8838 let store =
8839 FileRefStore::new(&git_dir, ObjectFormat::Sha1).with_reftable_combined_logs(true);
8840 let oid = ObjectId::from_hex(
8841 ObjectFormat::Sha1,
8842 "ce013625030ba8dba906f756967f9e9ca394464a",
8843 )
8844 .expect("test operation should succeed");
8845
8846 for index in 1..=20 {
8847 let mut tx = store.transaction();
8848 tx.update(RefUpdate {
8849 name: format!("refs/heads/branch-{index}"),
8850 expected: None,
8851 new: RefTarget::Direct(oid),
8852 reflog: Some(ReflogEntry {
8853 old_oid: ObjectId::null(ObjectFormat::Sha1),
8854 new_oid: oid,
8855 committer: format!("A <a@example.invalid> {index} +0000").into_bytes(),
8856 message: Vec::new(),
8857 }),
8858 });
8859 tx.commit().expect("test operation should succeed");
8860 }
8861
8862 let table_count = store
8863 .reftable_table_count()
8864 .expect("test operation should succeed");
8865 assert!(table_count > 0, "combined transaction stack disappeared");
8866 assert!(table_count < 20, "combined transactions never compacted");
8867
8868 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8869 }
8870
8871 #[test]
8872 fn file_ref_store_deletes_reftable_refs_with_tombstones() {
8873 let git_dir = temp_git_dir();
8874 write_reftable_config(&git_dir);
8875 let oid = ObjectId::from_hex(
8876 ObjectFormat::Sha1,
8877 "ce013625030ba8dba906f756967f9e9ca394464a",
8878 )
8879 .expect("test operation should succeed");
8880 write_reftable_stack(
8881 &git_dir,
8882 &[(
8883 "0x000000000001-0x000000000001-00000000.ref",
8884 vec![
8885 sley_formats::ReftableRefRecord {
8886 name: "refs/heads/main".into(),
8887 update_index: 1,
8888 value: ReftableRefValue::Direct(oid),
8889 },
8890 sley_formats::ReftableRefRecord {
8891 name: "refs/alias/main".into(),
8892 update_index: 1,
8893 value: ReftableRefValue::Symbolic("refs/heads/main".into()),
8894 },
8895 ],
8896 )],
8897 );
8898
8899 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8900 assert!(
8901 store
8902 .delete_symbolic_ref("refs/alias/main")
8903 .expect("test operation should succeed")
8904 );
8905 assert_eq!(
8906 store
8907 .read_ref("refs/alias/main")
8908 .expect("test operation should succeed"),
8909 None
8910 );
8911 let deleted = store
8912 .delete_ref("refs/heads/main")
8913 .expect("test operation should succeed");
8914 assert_eq!(deleted.oid, oid);
8915 assert_eq!(
8916 store
8917 .read_ref("refs/heads/main")
8918 .expect("test operation should succeed"),
8919 None
8920 );
8921 assert!(
8922 store
8923 .list_refs()
8924 .expect("test operation should succeed")
8925 .is_empty()
8926 );
8927 let tables = fs::read_to_string(git_dir.join("reftable").join("tables.list"))
8928 .expect("test operation should succeed");
8929 assert_eq!(tables.lines().count(), 3);
8930
8931 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8932 }
8933
8934 #[test]
8935 fn file_ref_store_deletes_packed_branch() {
8936 let git_dir = temp_git_dir();
8937 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8938 let branch_oid = ObjectId::from_hex(
8939 ObjectFormat::Sha1,
8940 "ce013625030ba8dba906f756967f9e9ca394464a",
8941 )
8942 .expect("test operation should succeed");
8943 let tag_oid = ObjectId::from_hex(
8944 ObjectFormat::Sha1,
8945 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
8946 )
8947 .expect("test operation should succeed");
8948 store
8949 .write_packed_refs(&[
8950 PackedRef {
8951 reference: Ref {
8952 name: "refs/heads/feature".into(),
8953 target: RefTarget::Direct(branch_oid),
8954 },
8955 peeled: None,
8956 },
8957 PackedRef {
8958 reference: Ref {
8959 name: "refs/tags/v1.0".into(),
8960 target: RefTarget::Direct(tag_oid),
8961 },
8962 peeled: None,
8963 },
8964 ])
8965 .expect("test operation should succeed");
8966 let deleted = store
8967 .delete_branch("feature")
8968 .expect("test operation should succeed");
8969 assert_eq!(deleted.name, "refs/heads/feature");
8970 assert_eq!(deleted.oid, branch_oid);
8971 assert_eq!(
8972 store
8973 .read_ref("refs/heads/feature")
8974 .expect("test operation should succeed"),
8975 None
8976 );
8977 assert_eq!(
8978 store
8979 .read_ref("refs/tags/v1.0")
8980 .expect("test operation should succeed"),
8981 Some(RefTarget::Direct(tag_oid))
8982 );
8983 assert!(!git_dir.join("packed-refs.lock").exists());
8984 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8985 }
8986
8987 #[test]
8988 fn file_ref_store_deletes_packed_tag() {
8989 let git_dir = temp_git_dir();
8990 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
8991 let oid = ObjectId::from_hex(
8992 ObjectFormat::Sha1,
8993 "ce013625030ba8dba906f756967f9e9ca394464a",
8994 )
8995 .expect("test operation should succeed");
8996 store
8997 .write_packed_refs(&[PackedRef {
8998 reference: Ref {
8999 name: "refs/tags/v1.0".into(),
9000 target: RefTarget::Direct(oid),
9001 },
9002 peeled: None,
9003 }])
9004 .expect("test operation should succeed");
9005 let deleted = store
9006 .delete_tag("v1.0")
9007 .expect("test operation should succeed");
9008 assert_eq!(deleted.name, "refs/tags/v1.0");
9009 assert_eq!(deleted.oid, oid);
9010 assert_eq!(
9011 store
9012 .read_ref("refs/tags/v1.0")
9013 .expect("test operation should succeed"),
9014 None
9015 );
9016 assert!(!git_dir.join("packed-refs.lock").exists());
9017 fs::remove_dir_all(git_dir).expect("test operation should succeed");
9018 }
9019
9020 #[test]
9021 fn file_ref_store_packs_loose_refs_and_prunes() {
9022 let git_dir = temp_git_dir();
9023 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
9024 let main_oid = ObjectId::from_hex(
9025 ObjectFormat::Sha1,
9026 "ce013625030ba8dba906f756967f9e9ca394464a",
9027 )
9028 .expect("test operation should succeed");
9029 let tag_oid = ObjectId::from_hex(
9030 ObjectFormat::Sha1,
9031 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
9032 )
9033 .expect("test operation should succeed");
9034 let mut tx = store.transaction();
9035 tx.update(RefUpdate {
9036 name: "refs/heads/main".into(),
9037 expected: None,
9038 new: RefTarget::Direct(main_oid),
9039 reflog: None,
9040 });
9041 tx.update(RefUpdate {
9042 name: "refs/tags/v1.0".into(),
9043 expected: None,
9044 new: RefTarget::Direct(tag_oid),
9045 reflog: None,
9046 });
9047 tx.commit().expect("test operation should succeed");
9048
9049 let packed = store
9050 .pack_refs(true)
9051 .expect("test operation should succeed");
9052 assert_eq!(packed.len(), 2);
9053 assert_eq!(
9054 store
9055 .read_ref("refs/heads/main")
9056 .expect("test operation should succeed"),
9057 Some(RefTarget::Direct(main_oid))
9058 );
9059 assert_eq!(
9060 store
9061 .read_ref("refs/tags/v1.0")
9062 .expect("test operation should succeed"),
9063 Some(RefTarget::Direct(tag_oid))
9064 );
9065 assert!(!git_dir.join("refs").join("heads").join("main").exists());
9066 assert!(!git_dir.join("refs").join("tags").join("v1.0").exists());
9067 assert!(git_dir.join("packed-refs").exists());
9068 assert!(!git_dir.join("packed-refs.lock").exists());
9069 fs::remove_dir_all(git_dir).expect("test operation should succeed");
9070 }
9071
9072 #[test]
9073 fn post_pack_prune_preserves_a_racing_loose_update() {
9074 let git_dir = temp_git_dir();
9075 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
9076 let packed_oid = ObjectId::from_hex(
9077 ObjectFormat::Sha1,
9078 "ce013625030ba8dba906f756967f9e9ca394464a",
9079 )
9080 .expect("test operation should succeed");
9081 let newer_oid = ObjectId::from_hex(
9082 ObjectFormat::Sha1,
9083 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
9084 )
9085 .expect("test operation should succeed");
9086 let mut tx = store.transaction();
9087 tx.update(RefUpdate {
9088 name: "refs/heads/main".into(),
9089 expected: None,
9090 new: RefTarget::Direct(newer_oid),
9091 reflog: None,
9092 });
9093 tx.commit().expect("seed racing loose update");
9094
9095 assert!(
9096 !store
9097 .prune_loose_ref_after_pack("refs/heads/main", &packed_oid)
9098 .expect("mismatched loose value is preserved")
9099 );
9100 assert_eq!(
9101 store
9102 .read_ref("refs/heads/main")
9103 .expect("read preserved loose ref"),
9104 Some(RefTarget::Direct(newer_oid))
9105 );
9106 assert!(!git_dir.join("refs/heads/main.lock").exists());
9107
9108 assert!(
9109 store
9110 .prune_loose_ref_after_pack("refs/heads/main", &newer_oid)
9111 .expect("matching loose value is pruned")
9112 );
9113 assert!(!git_dir.join("refs/heads/main").exists());
9114 assert!(!git_dir.join("refs/heads/main.lock").exists());
9115 fs::remove_dir_all(git_dir).expect("test operation should succeed");
9116 }
9117
9118 #[test]
9119 fn file_ref_store_imports_files_snapshot_in_final_packed_form() {
9120 let git_dir = temp_git_dir();
9121 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
9122 let oid = ObjectId::from_hex(
9123 ObjectFormat::Sha1,
9124 "ce013625030ba8dba906f756967f9e9ca394464a",
9125 )
9126 .expect("test operation should succeed");
9127 let refs = vec![
9128 Ref {
9129 name: "HEAD".into(),
9130 target: RefTarget::Symbolic("refs/heads/main".into()),
9131 },
9132 Ref {
9133 name: "ORIG_HEAD".into(),
9134 target: RefTarget::Direct(oid),
9135 },
9136 Ref {
9137 name: "refs/heads/main".into(),
9138 target: RefTarget::Direct(oid),
9139 },
9140 ];
9141 let reflogs = vec![(
9142 "refs/heads/main".into(),
9143 vec![reflog_entry(&oid, 1, "import")],
9144 )];
9145
9146 store
9147 .import_snapshot(&refs, &reflogs, true)
9148 .expect("import files snapshot");
9149
9150 assert!(git_dir.join("packed-refs").exists());
9151 assert!(!git_dir.join("refs/heads/main").exists());
9152 assert!(git_dir.join("HEAD").exists());
9153 assert!(git_dir.join("ORIG_HEAD").exists());
9154 assert_eq!(
9155 store.read_ref("refs/heads/main").expect("read packed ref"),
9156 Some(RefTarget::Direct(oid))
9157 );
9158 assert_eq!(
9159 store
9160 .read_reflog("refs/heads/main")
9161 .expect("read reflog")
9162 .len(),
9163 1
9164 );
9165 fs::remove_dir_all(git_dir).expect("test operation should succeed");
9166 }
9167
9168 #[test]
9169 fn files_snapshot_exports_materialized_logs_without_probing_every_ref() {
9170 let git_dir = temp_git_dir();
9171 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
9172 let oid = ObjectId::from_hex(
9173 ObjectFormat::Sha1,
9174 "ce013625030ba8dba906f756967f9e9ca394464a",
9175 )
9176 .expect("valid oid");
9177 let refs = vec![Ref {
9178 name: "refs/heads/no-log".into(),
9179 target: RefTarget::Direct(oid),
9180 }];
9181 store
9182 .import_snapshot(&refs, &[], false)
9183 .expect("import ref without reflog");
9184 let deleted_log = vec![reflog_entry(&oid, 1, "deleted ref")];
9185 store
9186 .write_reflog("refs/heads/deleted", &deleted_log)
9187 .expect("write materialized reflog without live ref");
9188
9189 assert_eq!(
9190 store.export_snapshot(true).expect("export files snapshot"),
9191 RefSnapshot {
9192 refs,
9193 reflogs: vec![("refs/heads/deleted".into(), deleted_log)],
9194 }
9195 );
9196 fs::remove_dir_all(git_dir).expect("remove test repository");
9197 }
9198
9199 #[test]
9200 fn reftable_snapshot_export_batches_refs_and_reflogs() {
9201 let git_dir = temp_git_dir();
9202 write_reftable_config(&git_dir);
9203 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
9204 let oid = ObjectId::from_hex(
9205 ObjectFormat::Sha1,
9206 "ce013625030ba8dba906f756967f9e9ca394464a",
9207 )
9208 .expect("test operation should succeed");
9209 let refs = vec![
9210 Ref {
9211 name: "HEAD".into(),
9212 target: RefTarget::Symbolic("refs/heads/main".into()),
9213 },
9214 Ref {
9215 name: "ORIG_HEAD".into(),
9216 target: RefTarget::Direct(oid),
9217 },
9218 Ref {
9219 name: "refs/heads/main".into(),
9220 target: RefTarget::Direct(oid),
9221 },
9222 ];
9223 let reflogs = vec![
9224 ("HEAD".into(), vec![reflog_entry(&oid, 1, "head")]),
9225 (
9226 "refs/heads/main".into(),
9227 vec![
9228 reflog_entry(&oid, 2, "first"),
9229 reflog_entry(&oid, 3, "second"),
9230 ],
9231 ),
9232 ];
9233 store
9234 .import_snapshot(&refs, &reflogs, false)
9235 .expect("import reftable snapshot");
9236
9237 assert_eq!(
9238 store.export_snapshot(true).expect("export with reflogs"),
9239 RefSnapshot {
9240 refs: refs.clone(),
9241 reflogs,
9242 }
9243 );
9244 assert_eq!(
9245 store
9246 .export_snapshot(false)
9247 .expect("export without reflogs"),
9248 RefSnapshot {
9249 refs,
9250 reflogs: Vec::new(),
9251 }
9252 );
9253 fs::remove_dir_all(git_dir).expect("remove test reftable repo");
9254 }
9255
9256 #[test]
9257 fn file_ref_store_packs_loose_refs_without_pruning() {
9258 let git_dir = temp_git_dir();
9259 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
9260 let oid = ObjectId::from_hex(
9261 ObjectFormat::Sha1,
9262 "ce013625030ba8dba906f756967f9e9ca394464a",
9263 )
9264 .expect("test operation should succeed");
9265 let mut tx = store.transaction();
9266 tx.update(RefUpdate {
9267 name: "refs/heads/main".into(),
9268 expected: None,
9269 new: RefTarget::Direct(oid),
9270 reflog: None,
9271 });
9272 tx.commit().expect("test operation should succeed");
9273
9274 let packed = store
9275 .pack_refs(false)
9276 .expect("test operation should succeed");
9277 assert_eq!(packed.len(), 1);
9278 assert!(git_dir.join("refs").join("heads").join("main").exists());
9279 assert_eq!(
9280 store
9281 .read_ref("refs/heads/main")
9282 .expect("test operation should succeed"),
9283 Some(RefTarget::Direct(oid))
9284 );
9285 fs::remove_dir_all(git_dir).expect("test operation should succeed");
9286 }
9287
9288 #[test]
9289 fn file_ref_store_packs_loose_refs_with_peeled_ids() {
9290 let git_dir = temp_git_dir();
9291 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
9292 let tag_oid = ObjectId::from_hex(
9293 ObjectFormat::Sha1,
9294 "ce013625030ba8dba906f756967f9e9ca394464a",
9295 )
9296 .expect("test operation should succeed");
9297 let peeled_oid = ObjectId::from_hex(
9298 ObjectFormat::Sha1,
9299 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
9300 )
9301 .expect("test operation should succeed");
9302 let mut tx = store.transaction();
9303 tx.update(RefUpdate {
9304 name: "refs/tags/v1.0".into(),
9305 expected: None,
9306 new: RefTarget::Direct(tag_oid),
9307 reflog: None,
9308 });
9309 tx.commit().expect("test operation should succeed");
9310
9311 let packed = store
9312 .pack_refs_with_peeler(true, |name, oid| {
9313 if name == "refs/tags/v1.0" && oid == &tag_oid {
9314 Ok(Some(peeled_oid))
9315 } else {
9316 Ok(None)
9317 }
9318 })
9319 .expect("test operation should succeed");
9320 assert_eq!(packed.len(), 1);
9321 assert_eq!(packed[0].peeled, Some(peeled_oid));
9322 let bytes =
9323 fs::read_to_string(git_dir.join("packed-refs")).expect("test operation should succeed");
9324 assert!(bytes.contains(&format!("^{peeled_oid}\n")));
9325 assert!(!git_dir.join("refs").join("tags").join("v1.0").exists());
9326 fs::remove_dir_all(git_dir).expect("test operation should succeed");
9327 }
9328
9329 fn reflog_entry(new_oid: &ObjectId, timestamp: i64, message: &str) -> ReflogEntry {
9330 ReflogEntry {
9331 old_oid: zero_oid(new_oid.format()).expect("test operation should succeed"),
9332 new_oid: *new_oid,
9333 committer: format!("Git Rs <sley@example.invalid> {timestamp} +0000").into_bytes(),
9334 message: message.as_bytes().to_vec(),
9335 }
9336 }
9337
9338 #[test]
9339 fn expire_reflog_drops_old_entries_and_keeps_latest() {
9340 let oid_a = ObjectId::from_hex(
9341 ObjectFormat::Sha1,
9342 "ce013625030ba8dba906f756967f9e9ca394464a",
9343 )
9344 .expect("test operation should succeed");
9345 let oid_b = ObjectId::from_hex(
9346 ObjectFormat::Sha1,
9347 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
9348 )
9349 .expect("test operation should succeed");
9350 let oid_c = ObjectId::from_hex(
9351 ObjectFormat::Sha1,
9352 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
9353 )
9354 .expect("test operation should succeed");
9355 let entries = vec![
9356 reflog_entry(&oid_a, 10, "oldest"),
9357 reflog_entry(&oid_b, 100, "middle"),
9358 reflog_entry(&oid_c, 20, "latest"),
9359 ];
9360
9361 let retained =
9364 expire_reflog(&entries, 50, None, |_| true).expect("test operation should succeed");
9365 assert_eq!(retained.len(), 2);
9366 assert_eq!(retained[0].message, b"middle");
9367 assert_eq!(retained[1].message, b"latest");
9368 }
9369
9370 #[test]
9371 fn expire_reflog_applies_stricter_unreachable_cutoff() {
9372 let reachable = ObjectId::from_hex(
9373 ObjectFormat::Sha1,
9374 "ce013625030ba8dba906f756967f9e9ca394464a",
9375 )
9376 .expect("test operation should succeed");
9377 let unreachable = ObjectId::from_hex(
9378 ObjectFormat::Sha1,
9379 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
9380 )
9381 .expect("test operation should succeed");
9382 let tip = ObjectId::from_hex(
9383 ObjectFormat::Sha1,
9384 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
9385 )
9386 .expect("test operation should succeed");
9387 let entries = vec![
9390 reflog_entry(&reachable, 100, "reachable"),
9391 reflog_entry(&unreachable, 100, "unreachable"),
9392 reflog_entry(&tip, 200, "tip"),
9393 ];
9394 let retained = expire_reflog(&entries, 50, Some(150), |oid| {
9395 oid == &reachable || oid == &tip
9396 })
9397 .expect("test operation should succeed");
9398 assert_eq!(retained.len(), 2);
9399 assert_eq!(retained[0].message, b"reachable");
9400 assert_eq!(retained[1].message, b"tip");
9401 }
9402
9403 #[test]
9404 fn expire_reflog_keeps_single_entry_below_cutoff() {
9405 let oid = ObjectId::from_hex(
9406 ObjectFormat::Sha1,
9407 "ce013625030ba8dba906f756967f9e9ca394464a",
9408 )
9409 .expect("test operation should succeed");
9410 let entries = vec![reflog_entry(&oid, 1, "only")];
9411 let retained = expire_reflog(&entries, i64::MAX, Some(i64::MAX), |_| false)
9412 .expect("test operation should succeed");
9413 assert_eq!(retained.len(), 1);
9414 assert_eq!(retained[0].message, b"only");
9415 }
9416
9417 #[test]
9418 fn file_ref_store_expire_reflog_file_rewrites_and_dry_runs() {
9419 let git_dir = temp_git_dir();
9420 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
9421 let first = ObjectId::from_hex(
9422 ObjectFormat::Sha1,
9423 "ce013625030ba8dba906f756967f9e9ca394464a",
9424 )
9425 .expect("test operation should succeed");
9426 let second = ObjectId::from_hex(
9427 ObjectFormat::Sha1,
9428 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
9429 )
9430 .expect("test operation should succeed");
9431 store
9432 .write_reflog(
9433 "refs/heads/main",
9434 &[
9435 reflog_entry(&first, 10, "old"),
9436 reflog_entry(&second, 100, "new"),
9437 ],
9438 )
9439 .expect("test operation should succeed");
9440
9441 let would_remove = store
9443 .expire_reflog_file("refs/heads/main", 50, None, false, |_| true)
9444 .expect("test operation should succeed");
9445 assert_eq!(would_remove, 1);
9446 assert_eq!(
9447 store
9448 .read_reflog("refs/heads/main")
9449 .expect("test operation should succeed")
9450 .len(),
9451 2
9452 );
9453
9454 let removed = store
9456 .expire_reflog_file("refs/heads/main", 50, None, true, |_| true)
9457 .expect("test operation should succeed");
9458 assert_eq!(removed, 1);
9459 let log = store
9460 .read_reflog("refs/heads/main")
9461 .expect("test operation should succeed");
9462 assert_eq!(log.len(), 1);
9463 assert_eq!(log[0].new_oid, second);
9464 assert!(
9465 !git_dir
9466 .join("logs")
9467 .join("refs")
9468 .join("heads")
9469 .join("main.lock")
9470 .exists()
9471 );
9472 fs::remove_dir_all(git_dir).expect("test operation should succeed");
9473 }
9474
9475 #[test]
9476 fn file_ref_transaction_commits_all_refs_atomically() {
9477 let git_dir = temp_git_dir();
9478 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
9479 let main_oid = ObjectId::from_hex(
9480 ObjectFormat::Sha1,
9481 "ce013625030ba8dba906f756967f9e9ca394464a",
9482 )
9483 .expect("test operation should succeed");
9484 let topic_oid = ObjectId::from_hex(
9485 ObjectFormat::Sha1,
9486 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
9487 )
9488 .expect("test operation should succeed");
9489 let tag_oid = ObjectId::from_hex(
9490 ObjectFormat::Sha1,
9491 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
9492 )
9493 .expect("test operation should succeed");
9494 let mut tx = store.transaction();
9495 tx.update(RefUpdate {
9496 name: "refs/heads/main".into(),
9497 expected: None,
9498 new: RefTarget::Direct(main_oid),
9499 reflog: Some(reflog_entry(&main_oid, 0, "create main")),
9500 });
9501 tx.update(RefUpdate {
9502 name: "refs/heads/topic".into(),
9503 expected: None,
9504 new: RefTarget::Direct(topic_oid),
9505 reflog: None,
9506 });
9507 tx.update(RefUpdate {
9508 name: "refs/tags/v1.0".into(),
9509 expected: None,
9510 new: RefTarget::Direct(tag_oid),
9511 reflog: None,
9512 });
9513 tx.commit().expect("test operation should succeed");
9514
9515 assert_eq!(
9516 store
9517 .read_ref("refs/heads/main")
9518 .expect("test operation should succeed"),
9519 Some(RefTarget::Direct(main_oid))
9520 );
9521 assert_eq!(
9522 store
9523 .read_ref("refs/heads/topic")
9524 .expect("test operation should succeed"),
9525 Some(RefTarget::Direct(topic_oid))
9526 );
9527 assert_eq!(
9528 store
9529 .read_ref("refs/tags/v1.0")
9530 .expect("test operation should succeed"),
9531 Some(RefTarget::Direct(tag_oid))
9532 );
9533 let main_log = store
9534 .read_reflog("refs/heads/main")
9535 .expect("test operation should succeed");
9536 assert_eq!(main_log.len(), 1);
9537 assert_eq!(main_log[0].new_oid, main_oid);
9538 assert!(
9540 !git_dir
9541 .join("refs")
9542 .join("heads")
9543 .join("main.lock")
9544 .exists()
9545 );
9546 assert!(
9547 !git_dir
9548 .join("refs")
9549 .join("heads")
9550 .join("topic.lock")
9551 .exists()
9552 );
9553 assert!(!git_dir.join("refs").join("tags").join("v1.0.lock").exists());
9554 fs::remove_dir_all(git_dir).expect("test operation should succeed");
9555 }
9556
9557 #[test]
9558 fn file_ref_transaction_preserves_batched_reflog_order() {
9559 let git_dir = temp_git_dir();
9560 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
9561 let first = ObjectId::from_hex(
9562 ObjectFormat::Sha1,
9563 "ce013625030ba8dba906f756967f9e9ca394464a",
9564 )
9565 .expect("test operation should succeed");
9566 let second = ObjectId::from_hex(
9567 ObjectFormat::Sha1,
9568 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
9569 )
9570 .expect("test operation should succeed");
9571 let mut tx = store.transaction();
9572 tx.update(RefUpdate {
9573 name: "refs/heads/main".into(),
9574 expected: None,
9575 new: RefTarget::Direct(first),
9576 reflog: Some(reflog_entry(&first, 1, "first")),
9577 });
9578 tx.update(RefUpdate {
9579 name: "refs/heads/topic".into(),
9580 expected: None,
9581 new: RefTarget::Direct(first),
9582 reflog: Some(reflog_entry(&first, 2, "topic")),
9583 });
9584 tx.update(RefUpdate {
9585 name: "refs/heads/main".into(),
9586 expected: None,
9587 new: RefTarget::Direct(second),
9588 reflog: Some(reflog_entry(&second, 3, "second")),
9589 });
9590 tx.commit().expect("test operation should succeed");
9591
9592 let main_log = store
9593 .read_reflog("refs/heads/main")
9594 .expect("test operation should succeed");
9595 assert_eq!(
9596 main_log
9597 .iter()
9598 .map(|entry| entry.message.as_slice())
9599 .collect::<Vec<_>>(),
9600 vec![b"first".as_slice(), b"second".as_slice()]
9601 );
9602 assert_eq!(
9603 store
9604 .read_reflog("refs/heads/topic")
9605 .expect("test operation should succeed")[0]
9606 .message,
9607 b"topic"
9608 );
9609 assert_eq!(
9610 store
9611 .read_ref("refs/heads/main")
9612 .expect("test operation should succeed"),
9613 Some(RefTarget::Direct(second))
9614 );
9615 fs::remove_dir_all(git_dir).expect("test operation should succeed");
9616 }
9617
9618 #[test]
9619 fn file_ref_transaction_replaces_empty_directory_at_ref_path() {
9620 let git_dir = temp_git_dir();
9621 let blocked_ref = git_dir.join("refs").join("heads").join("topic");
9622 fs::create_dir_all(blocked_ref.join("empty").join("nested"))
9623 .expect("test operation should succeed");
9624 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
9625 let oid = ObjectId::from_hex(
9626 ObjectFormat::Sha1,
9627 "ce013625030ba8dba906f756967f9e9ca394464a",
9628 )
9629 .expect("test operation should succeed");
9630 let mut tx = store.transaction();
9631 tx.update(RefUpdate {
9632 name: "refs/heads/topic".into(),
9633 expected: None,
9634 new: RefTarget::Direct(oid),
9635 reflog: None,
9636 });
9637 tx.commit().expect("test operation should succeed");
9638
9639 assert!(blocked_ref.is_file());
9640 assert_eq!(
9641 store
9642 .read_ref("refs/heads/topic")
9643 .expect("test operation should succeed"),
9644 Some(RefTarget::Direct(oid))
9645 );
9646 fs::remove_dir_all(git_dir).expect("test operation should succeed");
9647 }
9648
9649 #[test]
9650 fn file_ref_transaction_reports_non_empty_directory_without_mutation() {
9651 let git_dir = temp_git_dir();
9652 let blocked_ref = git_dir.join("refs/heads/topic");
9653 fs::create_dir_all(blocked_ref.join("nested")).expect("test operation should succeed");
9654 fs::write(blocked_ref.join("nested/blocker.lock"), b"")
9655 .expect("test operation should succeed");
9656 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
9657 let oid = ObjectId::from_hex(
9658 ObjectFormat::Sha1,
9659 "ce013625030ba8dba906f756967f9e9ca394464a",
9660 )
9661 .expect("test operation should succeed");
9662 let mut tx = store.transaction();
9663 tx.update(RefUpdate {
9664 name: "refs/heads/topic".into(),
9665 expected: None,
9666 new: RefTarget::Direct(oid),
9667 reflog: None,
9668 });
9669
9670 let error = tx.commit().expect_err("non-empty directory must block ref");
9671 assert!(error.to_string().contains("there is a non-empty directory"));
9672 assert!(blocked_ref.join("nested/blocker.lock").is_file());
9673 assert!(!git_dir.join("refs/heads/topic.lock").exists());
9674 fs::remove_dir_all(git_dir).expect("test operation should succeed");
9675 }
9676
9677 #[cfg(unix)]
9678 #[test]
9679 fn file_ref_store_ignores_broken_filesystem_symlink_during_iteration() {
9680 let git_dir = temp_git_dir();
9681 let heads = git_dir.join("refs/heads");
9682 fs::create_dir_all(&heads).expect("test operation should succeed");
9683 std::os::unix::fs::symlink("does-not-exist", heads.join("broken"))
9684 .expect("test operation should succeed");
9685 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
9686
9687 assert!(
9688 store
9689 .list_refs()
9690 .expect("test operation should succeed")
9691 .is_empty()
9692 );
9693 fs::remove_dir_all(git_dir).expect("test operation should succeed");
9694 }
9695
9696 #[test]
9697 fn update_snapshot_keeps_broken_loose_ref_over_packed_value() {
9698 let git_dir = temp_git_dir();
9699 fs::create_dir_all(git_dir.join("refs/heads")).expect("test operation should succeed");
9700 let oid = ObjectId::from_hex(
9701 ObjectFormat::Sha1,
9702 "ce013625030ba8dba906f756967f9e9ca394464a",
9703 )
9704 .expect("test operation should succeed");
9705 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
9706 store
9707 .write_packed_refs(&[PackedRef {
9708 reference: Ref {
9709 name: "refs/heads/topic".into(),
9710 target: RefTarget::Direct(oid),
9711 },
9712 peeled: None,
9713 }])
9714 .expect("test operation should succeed");
9715 fs::write(git_dir.join("refs/heads/topic"), b"gobbledigook\n")
9716 .expect("test operation should succeed");
9717
9718 let snapshot = store
9719 .read_snapshot_for_update()
9720 .expect("test operation should succeed");
9721 assert_eq!(
9722 snapshot.get("refs/heads/topic"),
9723 Some(&RefReadSnapshotValue::Broken)
9724 );
9725 fs::remove_dir_all(git_dir).expect("test operation should succeed");
9726 }
9727
9728 #[cfg(unix)]
9729 #[test]
9730 fn file_ref_transaction_writes_symbolic_ref_as_symlink_when_preferred() {
9731 let git_dir = temp_git_dir();
9732 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1).with_prefer_symlink_refs(true);
9733 let mut tx = store.transaction();
9734 tx.update(RefUpdate {
9735 name: "TEST_SYMREF_HEAD".into(),
9736 expected: None,
9737 new: RefTarget::Symbolic("refs/heads/main".into()),
9738 reflog: None,
9739 });
9740 tx.commit().expect("test operation should succeed");
9741
9742 let path = git_dir.join("TEST_SYMREF_HEAD");
9743 assert!(
9744 fs::symlink_metadata(&path)
9745 .expect("test operation should succeed")
9746 .file_type()
9747 .is_symlink()
9748 );
9749 assert_eq!(
9750 fs::read_link(path).expect("test operation should succeed"),
9751 PathBuf::from("refs/heads/main")
9752 );
9753 fs::remove_dir_all(git_dir).expect("test operation should succeed");
9754 }
9755
9756 #[cfg(unix)]
9757 #[test]
9758 fn file_ref_transaction_rollback_restores_original_symbolic_ref_symlink() {
9759 let git_dir = temp_git_dir();
9760 let heads = git_dir.join("refs/heads");
9761 fs::create_dir_all(&heads).expect("test operation should succeed");
9762 let oid = ObjectId::from_hex(
9763 ObjectFormat::Sha1,
9764 "ce013625030ba8dba906f756967f9e9ca394464a",
9765 )
9766 .expect("test operation should succeed");
9767 fs::write(heads.join("main"), format!("{oid}\n")).expect("test operation should succeed");
9768 let symref_path = git_dir.join("TEST_SYMREF_HEAD");
9769 std::os::unix::fs::symlink("refs/heads/main", &symref_path)
9770 .expect("test operation should succeed");
9771 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1).with_prefer_symlink_refs(true);
9772 let mut tx = store.transaction();
9773 tx.update(RefUpdate {
9774 name: "TEST_SYMREF_HEAD".into(),
9775 expected: None,
9776 new: RefTarget::Symbolic("refs/heads/topic".into()),
9777 reflog: None,
9778 });
9779 tx.update(RefUpdate {
9780 name: "refs/heads/other".into(),
9781 expected: None,
9782 new: RefTarget::Direct(oid),
9783 reflog: None,
9784 });
9785
9786 set_fail_loose_commit_action_for_test(Some(1));
9787 tx.commit()
9788 .expect_err("injected second apply failure must roll back the symref");
9789
9790 assert!(
9791 fs::symlink_metadata(&symref_path)
9792 .expect("test operation should succeed")
9793 .file_type()
9794 .is_symlink()
9795 );
9796 assert_eq!(
9797 fs::read_link(&symref_path).expect("test operation should succeed"),
9798 PathBuf::from("refs/heads/main")
9799 );
9800 assert!(!heads.join("other").exists());
9801 fs::remove_dir_all(git_dir).expect("test operation should succeed");
9802 }
9803
9804 #[test]
9805 fn file_ref_transaction_rolls_back_all_refs_on_expected_mismatch() {
9806 let git_dir = temp_git_dir();
9807 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
9808 let old_topic = ObjectId::from_hex(
9809 ObjectFormat::Sha1,
9810 "ce013625030ba8dba906f756967f9e9ca394464a",
9811 )
9812 .expect("test operation should succeed");
9813 let new_main = ObjectId::from_hex(
9814 ObjectFormat::Sha1,
9815 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
9816 )
9817 .expect("test operation should succeed");
9818 let new_tag = ObjectId::from_hex(
9819 ObjectFormat::Sha1,
9820 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
9821 )
9822 .expect("test operation should succeed");
9823 let wrong_expected = ObjectId::from_hex(
9824 ObjectFormat::Sha1,
9825 "0000000000000000000000000000000000000001",
9826 )
9827 .expect("test operation should succeed");
9828
9829 let mut seed = store.transaction();
9832 seed.update(RefUpdate {
9833 name: "refs/heads/topic".into(),
9834 expected: None,
9835 new: RefTarget::Direct(old_topic.clone()),
9836 reflog: None,
9837 });
9838 seed.commit().expect("test operation should succeed");
9839
9840 let mut tx = store.transaction();
9841 tx.update(RefUpdate {
9843 name: "refs/heads/main".into(),
9844 expected: None,
9845 new: RefTarget::Direct(new_main.clone()),
9846 reflog: Some(reflog_entry(&new_main, 0, "create main")),
9847 });
9848 tx.update(RefUpdate {
9850 name: "refs/heads/topic".into(),
9851 expected: Some(RefTarget::Direct(wrong_expected)),
9852 new: RefTarget::Direct(new_main.clone()),
9853 reflog: None,
9854 });
9855 tx.update(RefUpdate {
9857 name: "refs/tags/v1.0".into(),
9858 expected: None,
9859 new: RefTarget::Direct(new_tag),
9860 reflog: None,
9861 });
9862 let result = tx.commit();
9863 assert!(result.is_err());
9864
9865 assert_eq!(
9868 store
9869 .read_ref("refs/heads/main")
9870 .expect("test operation should succeed"),
9871 None
9872 );
9873 assert_eq!(
9874 store
9875 .read_ref("refs/heads/topic")
9876 .expect("test operation should succeed"),
9877 Some(RefTarget::Direct(old_topic))
9878 );
9879 assert_eq!(
9880 store
9881 .read_ref("refs/tags/v1.0")
9882 .expect("test operation should succeed"),
9883 None
9884 );
9885 assert!(
9886 store
9887 .read_reflog("refs/heads/main")
9888 .expect("test operation should succeed")
9889 .is_empty()
9890 );
9891
9892 assert!(
9894 !git_dir
9895 .join("refs")
9896 .join("heads")
9897 .join("main.lock")
9898 .exists()
9899 );
9900 assert!(
9901 !git_dir
9902 .join("refs")
9903 .join("heads")
9904 .join("topic.lock")
9905 .exists()
9906 );
9907 assert!(!git_dir.join("refs").join("tags").join("v1.0.lock").exists());
9908 fs::remove_dir_all(git_dir).expect("test operation should succeed");
9909 }
9910
9911 #[test]
9912 fn file_ref_transaction_mixes_update_and_delete() {
9913 let git_dir = temp_git_dir();
9914 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
9915 let old_main = ObjectId::from_hex(
9916 ObjectFormat::Sha1,
9917 "ce013625030ba8dba906f756967f9e9ca394464a",
9918 )
9919 .expect("test operation should succeed");
9920 let new_topic = ObjectId::from_hex(
9921 ObjectFormat::Sha1,
9922 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
9923 )
9924 .expect("test operation should succeed");
9925 let mut seed = store.transaction();
9926 seed.update(RefUpdate {
9927 name: "refs/heads/main".into(),
9928 expected: None,
9929 new: RefTarget::Direct(old_main),
9930 reflog: None,
9931 });
9932 seed.commit().expect("test operation should succeed");
9933
9934 let mut tx = store.transaction();
9935 tx.update(RefUpdate {
9936 name: "refs/heads/topic".into(),
9937 expected: None,
9938 new: RefTarget::Direct(new_topic),
9939 reflog: None,
9940 });
9941 tx.delete_with_precondition(
9942 "refs/heads/main",
9943 RefDeletePrecondition::Direct(Some(old_main)),
9944 None,
9945 );
9946 tx.commit().expect("test operation should succeed");
9947
9948 assert_eq!(
9949 store
9950 .read_ref("refs/heads/main")
9951 .expect("test operation should succeed"),
9952 None
9953 );
9954 assert_eq!(
9955 store
9956 .read_ref("refs/heads/topic")
9957 .expect("test operation should succeed"),
9958 Some(RefTarget::Direct(new_topic))
9959 );
9960 fs::remove_dir_all(git_dir).expect("test operation should succeed");
9961 }
9962
9963 #[test]
9964 fn file_ref_transaction_rejects_deleted_descendant_parent_create() {
9965 let git_dir = temp_git_dir();
9966 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
9967 let old_conflict = ObjectId::from_hex(
9968 ObjectFormat::Sha1,
9969 "ce013625030ba8dba906f756967f9e9ca394464a",
9970 )
9971 .expect("test operation should succeed");
9972 let new_parent = ObjectId::from_hex(
9973 ObjectFormat::Sha1,
9974 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
9975 )
9976 .expect("test operation should succeed");
9977 let mut seed = store.transaction();
9978 seed.update(RefUpdate {
9979 name: "refs/heads/branch/conflict".into(),
9980 expected: None,
9981 new: RefTarget::Direct(old_conflict),
9982 reflog: None,
9983 });
9984 seed.commit().expect("test operation should succeed");
9985
9986 let mut tx = store.transaction();
9987 tx.delete_with_precondition(
9988 "refs/heads/branch/conflict",
9989 RefDeletePrecondition::Direct(Some(old_conflict)),
9990 None,
9991 );
9992 tx.update(RefUpdate {
9993 name: "refs/heads/branch".into(),
9994 expected: None,
9995 new: RefTarget::Direct(new_parent),
9996 reflog: None,
9997 });
9998 let err = tx
9999 .commit()
10000 .expect_err("D/F-conflicting delete plus create must fail");
10001 assert_eq!(
10002 err.to_string(),
10003 "transaction failed: cannot lock ref 'refs/heads/branch': 'refs/heads/branch/conflict' exists; cannot create 'refs/heads/branch'"
10004 );
10005
10006 assert_eq!(
10007 store
10008 .read_ref("refs/heads/branch/conflict")
10009 .expect("test operation should succeed"),
10010 Some(RefTarget::Direct(old_conflict))
10011 );
10012 assert_eq!(
10013 store
10014 .read_ref("refs/heads/branch")
10015 .expect("test operation should succeed"),
10016 None
10017 );
10018 fs::remove_dir_all(git_dir).expect("test operation should succeed");
10019 }
10020
10021 #[test]
10022 fn file_ref_transaction_stale_delete_rolls_back_update() {
10023 let git_dir = temp_git_dir();
10024 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
10025 let old_oid = ObjectId::from_hex(
10026 ObjectFormat::Sha1,
10027 "ce013625030ba8dba906f756967f9e9ca394464a",
10028 )
10029 .expect("test operation should succeed");
10030 let new_oid = ObjectId::from_hex(
10031 ObjectFormat::Sha1,
10032 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
10033 )
10034 .expect("test operation should succeed");
10035 let mut seed = store.transaction();
10036 for name in ["refs/heads/main", "refs/heads/topic"] {
10037 seed.update(RefUpdate {
10038 name: name.into(),
10039 expected: None,
10040 new: RefTarget::Direct(old_oid),
10041 reflog: None,
10042 });
10043 }
10044 seed.commit().expect("test operation should succeed");
10045
10046 let mut tx = store.transaction();
10047 tx.update(RefUpdate {
10048 name: "refs/heads/topic".into(),
10049 expected: None,
10050 new: RefTarget::Direct(new_oid),
10051 reflog: None,
10052 });
10053 tx.delete_with_precondition(
10054 "refs/heads/main",
10055 RefDeletePrecondition::Direct(Some(new_oid)),
10056 None,
10057 );
10058 let err = tx.commit().expect_err("stale delete must abort");
10059 assert!(err.to_string().contains("expected ref refs/heads/main"));
10060
10061 assert_eq!(
10062 store
10063 .read_ref("refs/heads/main")
10064 .expect("test operation should succeed"),
10065 Some(RefTarget::Direct(old_oid))
10066 );
10067 assert_eq!(
10068 store
10069 .read_ref("refs/heads/topic")
10070 .expect("test operation should succeed"),
10071 Some(RefTarget::Direct(old_oid))
10072 );
10073 fs::remove_dir_all(git_dir).expect("test operation should succeed");
10074 }
10075
10076 #[test]
10077 fn file_ref_transaction_rejects_duplicate_mixed_ref() {
10078 let git_dir = temp_git_dir();
10079 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
10080 let oid = ObjectId::from_hex(
10081 ObjectFormat::Sha1,
10082 "ce013625030ba8dba906f756967f9e9ca394464a",
10083 )
10084 .expect("test operation should succeed");
10085 let mut tx = store.transaction();
10086 tx.update(RefUpdate {
10087 name: "refs/heads/main".into(),
10088 expected: None,
10089 new: RefTarget::Direct(oid),
10090 reflog: None,
10091 });
10092 tx.delete_with_precondition("refs/heads/main", RefDeletePrecondition::Any, None);
10093
10094 let err = tx.commit().expect_err("duplicate ref must fail");
10095 assert!(err.to_string().contains("refs/heads/main"));
10096 assert_eq!(
10097 store
10098 .read_ref("refs/heads/main")
10099 .expect("test operation should succeed"),
10100 None
10101 );
10102 fs::remove_dir_all(git_dir).expect("test operation should succeed");
10103 }
10104
10105 #[test]
10106 fn file_ref_transaction_deletes_symbolic_ref_with_immediate_expectation() {
10107 let git_dir = temp_git_dir();
10108 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
10109 let oid = ObjectId::from_hex(
10110 ObjectFormat::Sha1,
10111 "ce013625030ba8dba906f756967f9e9ca394464a",
10112 )
10113 .expect("test operation should succeed");
10114 let mut seed = store.transaction();
10115 seed.update(RefUpdate {
10116 name: "refs/heads/main".into(),
10117 expected: None,
10118 new: RefTarget::Direct(oid),
10119 reflog: None,
10120 });
10121 seed.update(RefUpdate {
10122 name: "refs/aliases/main".into(),
10123 expected: None,
10124 new: RefTarget::Symbolic("refs/heads/main".into()),
10125 reflog: None,
10126 });
10127 seed.commit().expect("test operation should succeed");
10128
10129 let mut tx = store.transaction();
10130 tx.delete_with_precondition(
10131 "refs/aliases/main",
10132 RefDeletePrecondition::Immediate(RefTarget::Symbolic("refs/heads/main".into())),
10133 None,
10134 );
10135 tx.commit().expect("test operation should succeed");
10136
10137 assert_eq!(
10138 store
10139 .read_ref("refs/aliases/main")
10140 .expect("test operation should succeed"),
10141 None
10142 );
10143 assert_eq!(
10144 store
10145 .read_ref("refs/heads/main")
10146 .expect("test operation should succeed"),
10147 Some(RefTarget::Direct(oid))
10148 );
10149 fs::remove_dir_all(git_dir).expect("test operation should succeed");
10150 }
10151
10152 #[test]
10153 fn file_ref_transaction_rolls_back_delete_after_late_write_failure() {
10154 let git_dir = temp_git_dir();
10155 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
10156 let old_oid = ObjectId::from_hex(
10157 ObjectFormat::Sha1,
10158 "ce013625030ba8dba906f756967f9e9ca394464a",
10159 )
10160 .expect("test operation should succeed");
10161 let new_oid = ObjectId::from_hex(
10162 ObjectFormat::Sha1,
10163 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
10164 )
10165 .expect("test operation should succeed");
10166 let mut seed = store.transaction();
10167 for name in ["refs/heads/main", "refs/heads/topic"] {
10168 seed.update(RefUpdate {
10169 name: name.into(),
10170 expected: None,
10171 new: RefTarget::Direct(old_oid),
10172 reflog: None,
10173 });
10174 }
10175 seed.commit().expect("test operation should succeed");
10176
10177 set_fail_loose_commit_action_for_test(Some(1));
10178 let mut tx = store.transaction();
10179 tx.delete_with_precondition(
10180 "refs/heads/main",
10181 RefDeletePrecondition::Direct(Some(old_oid)),
10182 None,
10183 );
10184 tx.update(RefUpdate {
10185 name: "refs/heads/topic".into(),
10186 expected: None,
10187 new: RefTarget::Direct(new_oid),
10188 reflog: None,
10189 });
10190 let err = tx.commit().expect_err("injected failure must abort");
10191 assert!(
10192 err.to_string()
10193 .contains("injected loose ref transaction failure")
10194 );
10195
10196 assert_eq!(
10197 store
10198 .read_ref("refs/heads/main")
10199 .expect("test operation should succeed"),
10200 Some(RefTarget::Direct(old_oid))
10201 );
10202 assert_eq!(
10203 store
10204 .read_ref("refs/heads/topic")
10205 .expect("test operation should succeed"),
10206 Some(RefTarget::Direct(old_oid))
10207 );
10208 assert!(
10209 !git_dir
10210 .join("refs")
10211 .join("heads")
10212 .join("main.lock")
10213 .exists()
10214 );
10215 assert!(
10216 !git_dir
10217 .join("refs")
10218 .join("heads")
10219 .join("topic.lock")
10220 .exists()
10221 );
10222 fs::remove_dir_all(git_dir).expect("test operation should succeed");
10223 }
10224
10225 fn temp_git_dir() -> PathBuf {
10226 let path = std::env::temp_dir().join(format!(
10227 "sley-refs-{}-{}",
10228 std::process::id(),
10229 TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
10230 ));
10231 fs::create_dir_all(&path).expect("test operation should succeed");
10232 path
10233 }
10234
10235 fn zero_oid(format: ObjectFormat) -> Result<ObjectId> {
10236 Ok(ObjectId::null(format))
10237 }
10238
10239 fn write_reftable_config(git_dir: &Path) {
10240 fs::write(
10241 git_dir.join("config"),
10242 b"[core]\n\trepositoryformatversion = 1\n[extensions]\n\trefStorage = reftable\n",
10243 )
10244 .expect("test operation should succeed");
10245 }
10246
10247 fn write_reftable_stack(
10248 git_dir: &Path,
10249 tables: &[(&str, Vec<sley_formats::ReftableRefRecord>)],
10250 ) {
10251 let reftable_dir = git_dir.join("reftable");
10252 fs::create_dir_all(&reftable_dir).expect("test operation should succeed");
10253 let mut list = String::new();
10254 for (idx, (name, refs)) in tables.iter().enumerate() {
10255 let update_index = (idx + 1) as u64;
10256 let bytes = sley_formats::Reftable::write_ref_only(
10257 ObjectFormat::Sha1,
10258 update_index,
10259 update_index,
10260 refs,
10261 )
10262 .expect("test operation should succeed");
10263 fs::write(reftable_dir.join(name), bytes).expect("test operation should succeed");
10264 list.push_str(name);
10265 list.push('\n');
10266 }
10267 fs::write(reftable_dir.join("tables.list"), list).expect("test operation should succeed");
10268 }
10269}