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::{Reftable, ReftableRefRecord, ReftableRefValue};
8use std::borrow::Borrow;
9use std::collections::{BTreeMap, BTreeSet, HashMap};
10use std::fmt;
11use std::fs;
12use std::io::Write;
13use std::ops::Deref;
14use std::path::{Path, PathBuf};
15use std::time::{SystemTime, UNIX_EPOCH};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum RefTarget {
19 Direct(ObjectId),
20 Symbolic(String),
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Ref {
25 pub name: String,
26 pub target: RefTarget,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct RefDelete {
31 pub name: String,
32 pub oid: ObjectId,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct DeleteRef {
37 pub name: String,
38 pub expected_old: Option<ObjectId>,
39 pub reflog: Option<DeleteRefReflog>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct DeleteRefReflog {
44 pub committer: Vec<u8>,
45 pub message: Vec<u8>,
46}
47
48#[derive(Debug)]
49pub enum RefDeleteError {
50 NotFound,
51 ExpectedMismatch {
52 expected: Option<ObjectId>,
53 actual: Option<ObjectId>,
54 },
55 Locked,
56 InvalidName,
57 Io(std::io::Error),
58}
59
60impl fmt::Display for RefDeleteError {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 match self {
63 Self::NotFound => f.write_str("ref not found"),
64 Self::ExpectedMismatch { expected, actual } => {
65 write!(
66 f,
67 "ref expected old oid mismatch: expected {:?}, actual {:?}",
68 expected, actual
69 )
70 }
71 Self::Locked => f.write_str("ref is locked"),
72 Self::InvalidName => f.write_str("invalid ref name"),
73 Self::Io(err) => write!(f, "io error: {err}"),
74 }
75 }
76}
77
78impl std::error::Error for RefDeleteError {}
79
80impl From<std::io::Error> for RefDeleteError {
81 fn from(value: std::io::Error) -> Self {
82 Self::Io(value)
83 }
84}
85
86pub fn parse_loose_ref(format: ObjectFormat, name: impl Into<String>, bytes: &[u8]) -> Result<Ref> {
87 let name = name.into();
88 let value = std::str::from_utf8(bytes)
89 .map_err(|err| GitError::InvalidFormat(err.to_string()))?
90 .trim_end_matches('\n');
91 let target = if let Some(symbolic) = value.strip_prefix("ref: ") {
92 RefTarget::Symbolic(symbolic.to_string())
93 } else {
94 RefTarget::Direct(ObjectId::from_hex(format, value)?)
95 };
96 Ok(Ref { name, target })
97}
98
99pub fn write_loose_ref(reference: &Ref) -> Vec<u8> {
100 match &reference.target {
101 RefTarget::Direct(oid) => format!("{oid}\n").into_bytes(),
102 RefTarget::Symbolic(target) => format!("ref: {target}\n").into_bytes(),
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct PackedRef {
108 pub reference: Ref,
109 pub peeled: Option<ObjectId>,
110}
111
112pub fn parse_packed_refs(format: ObjectFormat, bytes: &[u8]) -> Result<Vec<PackedRef>> {
113 let text =
114 std::str::from_utf8(bytes).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
115 let mut refs: Vec<PackedRef> = Vec::new();
116 for raw_line in text.lines() {
117 let line = raw_line.trim_end();
118 if line.is_empty() || line.starts_with('#') {
119 continue;
120 }
121 if let Some(peeled) = line.strip_prefix('^') {
122 let oid = ObjectId::from_hex(format, peeled)?;
123 let Some(last) = refs.last_mut() else {
124 return Err(GitError::InvalidFormat(
125 "peeled packed ref without preceding ref".into(),
126 ));
127 };
128 last.peeled = Some(oid);
129 continue;
130 }
131 let (oid, name) = line
132 .split_once(' ')
133 .ok_or_else(|| GitError::InvalidFormat("invalid packed ref line".into()))?;
134 validate_ref_name(name)?;
135 refs.push(PackedRef {
136 reference: Ref {
137 name: name.into(),
138 target: RefTarget::Direct(ObjectId::from_hex(format, oid)?),
139 },
140 peeled: None,
141 });
142 }
143 Ok(refs)
144}
145
146fn packed_refs_have_prefix(format: ObjectFormat, bytes: &[u8], prefix: &str) -> Result<bool> {
147 let text =
148 std::str::from_utf8(bytes).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
149 let mut found = false;
150 let mut saw_ref = false;
151 for raw_line in text.lines() {
152 let line = raw_line.trim_end();
153 if line.is_empty() || line.starts_with('#') {
154 continue;
155 }
156 if let Some(peeled) = line.strip_prefix('^') {
157 ObjectId::from_hex(format, peeled)?;
158 if !saw_ref {
159 return Err(GitError::InvalidFormat(
160 "peeled packed ref without preceding ref".into(),
161 ));
162 }
163 continue;
164 }
165 let (oid, name) = line
166 .split_once(' ')
167 .ok_or_else(|| GitError::InvalidFormat("invalid packed ref line".into()))?;
168 validate_ref_name(name)?;
169 ObjectId::from_hex(format, oid)?;
170 saw_ref = true;
171 found |= name.starts_with(prefix);
172 }
173 Ok(found)
174}
175
176pub fn write_packed_refs(refs: &[PackedRef]) -> Result<Vec<u8>> {
177 let mut refs = refs.to_vec();
178 refs.sort_by(|left, right| left.reference.name.cmp(&right.reference.name));
179 let mut out = b"# pack-refs with: peeled fully-peeled sorted \n".to_vec();
180 for packed in refs {
181 validate_ref_name(&packed.reference.name)?;
182 let RefTarget::Direct(oid) = &packed.reference.target else {
183 return Err(GitError::InvalidFormat(format!(
184 "packed ref {} is symbolic",
185 packed.reference.name
186 )));
187 };
188 out.extend_from_slice(oid.to_hex().as_bytes());
189 out.push(b' ');
190 out.extend_from_slice(packed.reference.name.as_bytes());
191 out.push(b'\n');
192 if let Some(peeled) = packed.peeled {
193 out.push(b'^');
194 out.extend_from_slice(peeled.to_hex().as_bytes());
195 out.push(b'\n');
196 }
197 }
198 Ok(out)
199}
200
201#[derive(Debug, Clone, PartialEq, Eq)]
202pub struct ReflogEntry {
203 pub old_oid: ObjectId,
204 pub new_oid: ObjectId,
205 pub committer: Vec<u8>,
206 pub message: Vec<u8>,
207}
208
209impl ReflogEntry {
210 pub fn to_line(&self) -> Vec<u8> {
211 let mut out = Vec::new();
212 out.extend_from_slice(self.old_oid.to_hex().as_bytes());
213 out.push(b' ');
214 out.extend_from_slice(self.new_oid.to_hex().as_bytes());
215 out.push(b' ');
216 out.extend_from_slice(&self.committer);
217 if !self.message.is_empty() {
218 out.push(b'\t');
219 out.extend_from_slice(&self.message);
220 }
221 out.push(b'\n');
222 out
223 }
224
225 pub fn timestamp_seconds(&self) -> Result<i64> {
226 let committer = std::str::from_utf8(&self.committer)
227 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
228 let Some((before_tz, _tz)) = committer.rsplit_once(' ') else {
229 return Err(GitError::InvalidFormat(
230 "reflog committer is missing timezone".into(),
231 ));
232 };
233 let Some((_identity, timestamp)) = before_tz.rsplit_once(' ') else {
234 return Err(GitError::InvalidFormat(
235 "reflog committer is missing timestamp".into(),
236 ));
237 };
238 timestamp
239 .parse::<i64>()
240 .map_err(|err| GitError::InvalidFormat(err.to_string()))
241 }
242}
243
244pub fn parse_reflog(format: ObjectFormat, bytes: &[u8]) -> Result<Vec<ReflogEntry>> {
245 let text =
246 std::str::from_utf8(bytes).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
247 let mut entries = Vec::new();
248 for line in text.lines() {
249 let mut parts = line.splitn(3, ' ');
250 let old = parts
251 .next()
252 .ok_or_else(|| GitError::InvalidFormat("missing reflog old oid".into()))?;
253 let new = parts
254 .next()
255 .ok_or_else(|| GitError::InvalidFormat("missing reflog new oid".into()))?;
256 let rest = parts
257 .next()
258 .ok_or_else(|| GitError::InvalidFormat("missing reflog committer".into()))?;
259 let (committer, message) = rest.split_once('\t').unwrap_or((rest, ""));
260 entries.push(ReflogEntry {
261 old_oid: ObjectId::from_hex(format, old)?,
262 new_oid: ObjectId::from_hex(format, new)?,
263 committer: committer.as_bytes().to_vec(),
264 message: message.as_bytes().to_vec(),
265 });
266 }
267 Ok(entries)
268}
269
270pub fn expire_reflog(
288 entries: &[ReflogEntry],
289 cutoff_unix: i64,
290 expire_unreachable_cutoff: Option<i64>,
291 is_reachable: impl Fn(&ObjectId) -> bool,
292) -> Result<Vec<ReflogEntry>> {
293 let last_index = entries.len().checked_sub(1);
294 let mut retained = Vec::with_capacity(entries.len());
295 for (index, entry) in entries.iter().enumerate() {
296 if Some(index) == last_index {
299 retained.push(entry.clone());
300 continue;
301 }
302 let timestamp = entry.timestamp_seconds()?;
303 let mut expired = timestamp < cutoff_unix;
304 if let Some(unreachable_cutoff) = expire_unreachable_cutoff
305 && !is_reachable(&entry.new_oid)
306 {
307 expired = expired || timestamp < unreachable_cutoff;
308 }
309 if !expired {
310 retained.push(entry.clone());
311 }
312 }
313 Ok(retained)
314}
315
316#[derive(Debug, Default, Clone)]
317pub struct RefStore {
318 refs: HashMap<String, RefTarget>,
319 reflogs: BTreeMap<String, Vec<ReflogEntry>>,
320}
321
322impl RefStore {
323 pub fn new() -> Self {
324 Self::default()
325 }
326
327 pub fn get(&self, name: &str) -> Option<&RefTarget> {
328 self.refs.get(name)
329 }
330
331 pub fn transaction(&mut self) -> RefTransaction<'_> {
332 RefTransaction {
333 store: self,
334 updates: Vec::new(),
335 }
336 }
337
338 pub fn reflog(&self, name: &str) -> &[ReflogEntry] {
339 self.reflogs
340 .get(name)
341 .map(Vec::as_slice)
342 .unwrap_or_default()
343 }
344}
345
346#[derive(Debug)]
347pub struct RefUpdate {
348 pub name: String,
349 pub expected: Option<RefTarget>,
350 pub new: RefTarget,
351 pub reflog: Option<ReflogEntry>,
352}
353
354#[derive(Debug, Clone, PartialEq, Eq)]
362pub enum RefPrecondition {
363 Any,
365 MustExist,
367 MustNotExist,
369 MustExistAndMatch(RefTarget),
371 ExistingMustMatch(RefTarget),
374}
375
376impl RefPrecondition {
377 fn from_expected(expected: Option<RefTarget>) -> Self {
379 match expected {
380 None => Self::Any,
381 Some(target) => Self::MustExistAndMatch(target),
382 }
383 }
384
385 fn is_satisfied_by(&self, current: Option<&RefTarget>) -> bool {
388 match self {
389 Self::Any => true,
390 Self::MustExist => current.is_some(),
391 Self::MustNotExist => current.is_none(),
392 Self::MustExistAndMatch(target) => current == Some(target),
393 Self::ExistingMustMatch(target) => match current {
394 None => true,
395 Some(current) => current == target,
396 },
397 }
398 }
399
400 fn describe(&self, name: &str) -> String {
402 match self {
403 Self::Any => format!("ref {name} precondition not met"),
404 Self::MustExist => format!("expected ref {name} to exist"),
405 Self::MustNotExist => format!("expected ref {name} to not already exist"),
406 Self::MustExistAndMatch(_) => format!("expected ref {name} to match"),
407 Self::ExistingMustMatch(_) => {
408 format!("expected ref {name} to match its current value")
409 }
410 }
411 }
412}
413
414pub struct RefTransaction<'a> {
415 store: &'a mut RefStore,
416 updates: Vec<RefUpdate>,
417}
418
419impl<'a> RefTransaction<'a> {
420 pub fn update(&mut self, update: RefUpdate) {
421 self.updates.push(update);
422 }
423
424 pub fn commit(self) -> Result<()> {
425 for update in &self.updates {
426 if let Some(expected) = &update.expected
427 && self.store.refs.get(&update.name) != Some(expected)
428 {
429 return Err(GitError::Transaction(format!(
430 "expected ref {} to match",
431 update.name
432 )));
433 }
434 }
435 for update in self.updates {
436 self.store.refs.insert(update.name.clone(), update.new);
437 if let Some(entry) = update.reflog {
438 self.store
439 .reflogs
440 .entry(update.name)
441 .or_default()
442 .push(entry);
443 }
444 }
445 Ok(())
446 }
447}
448
449#[derive(Debug, Clone)]
450pub struct FileRefStore {
451 git_dir: PathBuf,
452 common_dir: PathBuf,
453 format: ObjectFormat,
454}
455
456#[derive(Debug, Clone, PartialEq, Eq)]
457pub struct BranchCreate {
458 pub name: String,
459 pub oid: ObjectId,
460}
461
462#[derive(Debug, Clone, PartialEq, Eq)]
463pub struct BranchDelete {
464 pub name: String,
465 pub oid: ObjectId,
466}
467
468#[derive(Debug, Clone, PartialEq, Eq)]
469pub struct TagCreate {
470 pub name: String,
471 pub oid: ObjectId,
472}
473
474#[derive(Debug, Clone, PartialEq, Eq)]
475pub struct TagDelete {
476 pub name: String,
477 pub oid: ObjectId,
478}
479
480#[derive(Debug, Clone, PartialEq, Eq)]
481pub struct BundleRefUpdate {
482 pub name: String,
483 pub oid: ObjectId,
484}
485
486#[derive(Debug, Clone, PartialEq, Eq)]
487pub struct BundleRefUpdateReflog {
488 pub committer: Vec<u8>,
489 pub message: Vec<u8>,
490}
491
492#[derive(Debug, Clone, PartialEq, Eq)]
493pub struct AppliedBundleRefUpdate {
494 pub name: String,
495 pub old_oid: Option<ObjectId>,
496 pub new_oid: ObjectId,
497}
498
499impl FileRefStore {
500 pub fn new(git_dir: impl Into<PathBuf>, format: ObjectFormat) -> Self {
501 let git_dir = git_dir.into();
502 let common_dir = repository_common_dir(&git_dir);
503 Self {
504 git_dir,
505 common_dir,
506 format,
507 }
508 }
509
510 pub fn read_ref(&self, name: &str) -> Result<Option<RefTarget>> {
511 validate_ref_name_for_read(name)?;
512 self.read_ref_unchecked(name)
513 }
514
515 fn read_ref_unchecked(&self, name: &str) -> Result<Option<RefTarget>> {
516 if self.uses_reftable()? {
517 return self.read_reftable_ref(name);
518 }
519 if let Some(reference) = self.read_loose_ref(name)? {
520 return Ok(Some(reference.target));
521 }
522 if let Some(reference) = self.read_packed_ref(name)? {
523 return Ok(Some(reference.reference.target));
524 }
525 Ok(None)
526 }
527
528 pub fn raw_ref_exists(&self, name: &str) -> Result<bool> {
538 if self.uses_reftable()? {
539 return Ok(self.read_reftable_ref(name)?.is_some());
540 }
541 let base = if is_root_ref_syntax(name) {
545 &self.git_dir
546 } else {
547 &self.common_dir
548 };
549 let path = base.join(name);
550 match fs::symlink_metadata(&path) {
551 Ok(meta) if meta.is_dir() => {
552 Ok(self.read_packed_ref(name)?.is_some())
555 }
556 Ok(_) => Ok(true),
557 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
558 Ok(self.read_packed_ref(name)?.is_some())
559 }
560 Err(err) => Err(err.into()),
561 }
562 }
563
564 pub fn read_reflog(&self, name: &str) -> Result<Vec<ReflogEntry>> {
565 validate_ref_name_for_read(name)?;
566 let path = self.reflog_path(name);
567 if !path.exists() {
568 return Ok(Vec::new());
569 }
570 parse_reflog(self.format, &fs::read(path)?)
571 }
572
573 pub fn write_reflog(&self, name: &str, entries: &[ReflogEntry]) -> Result<()> {
574 validate_ref_name_for_read(name)?;
575 let path = self.reflog_path(name);
576 let parent = path
577 .parent()
578 .ok_or_else(|| GitError::InvalidPath("reflog path has no parent".into()))?;
579 fs::create_dir_all(parent)?;
580 let mut bytes = Vec::new();
581 for entry in entries {
582 bytes.extend_from_slice(&entry.to_line());
583 }
584 write_locked(&path, &bytes)
585 }
586
587 pub fn expire_reflog_older_than(&self, name: &str, cutoff_seconds: i64) -> Result<usize> {
588 validate_ref_name_for_read(name)?;
589 let path = self.reflog_path(name);
590 if !path.exists() {
591 return Ok(0);
592 }
593 let entries = parse_reflog(self.format, &fs::read(&path)?)?;
594 let original_len = entries.len();
595 let mut retained = Vec::new();
596 for entry in entries {
597 if entry.timestamp_seconds()? >= cutoff_seconds {
598 retained.push(entry);
599 }
600 }
601 let mut bytes = Vec::new();
602 for entry in &retained {
603 bytes.extend_from_slice(&entry.to_line());
604 }
605 write_locked(&path, &bytes)?;
606 Ok(original_len - retained.len())
607 }
608
609 pub fn expire_reflog_file(
620 &self,
621 name: &str,
622 cutoff_unix: i64,
623 expire_unreachable_cutoff: Option<i64>,
624 write: bool,
625 is_reachable: impl Fn(&ObjectId) -> bool,
626 ) -> Result<usize> {
627 validate_ref_name(name)?;
628 let path = self.reflog_path(name);
629 if !path.exists() {
630 return Ok(0);
631 }
632 let entries = parse_reflog(self.format, &fs::read(&path)?)?;
633 let original_len = entries.len();
634 let retained = expire_reflog(
635 &entries,
636 cutoff_unix,
637 expire_unreachable_cutoff,
638 is_reachable,
639 )?;
640 let removed = original_len - retained.len();
641 if write && removed > 0 {
642 let mut bytes = Vec::new();
643 for entry in &retained {
644 bytes.extend_from_slice(&entry.to_line());
645 }
646 write_locked(&path, &bytes)?;
647 }
648 Ok(removed)
649 }
650
651 pub fn list_refs(&self) -> Result<Vec<Ref>> {
652 if self.uses_reftable()? {
653 return self.list_reftable_refs();
654 }
655 let mut refs = Vec::new();
656 let packed_path = self.common_dir.join("packed-refs");
657 if packed_path.exists() {
658 for packed in parse_packed_refs(self.format, &fs::read(packed_path)?)? {
659 refs.push(packed.reference);
660 }
661 }
662 let refs_dir = self.common_dir.join("refs");
663 let mut loose_refs = BTreeMap::new();
664 if refs_dir.exists() {
665 self.collect_loose_refs(&refs_dir, "refs", &mut loose_refs)?;
666 }
667 if !loose_refs.is_empty() {
668 refs.retain(|reference| !loose_refs.contains_key(&reference.name));
669 refs.extend(loose_refs.into_values());
670 }
671 refs.sort_by(|left, right| left.name.cmp(&right.name));
672 Ok(refs)
673 }
674
675 pub fn has_refs_with_prefix(&self, prefix: &str) -> Result<bool> {
676 if self.uses_reftable()? {
677 return Ok(self
678 .list_reftable_refs()?
679 .iter()
680 .any(|reference| reference.name.starts_with(prefix)));
681 }
682 let packed_path = self.common_dir.join("packed-refs");
683 if packed_path.exists()
684 && packed_refs_have_prefix(self.format, &fs::read(&packed_path)?, prefix)?
685 {
686 return Ok(true);
687 }
688 self.loose_refs_have_prefix(prefix)
689 }
690
691 pub fn write_packed_refs(&self, refs: &[PackedRef]) -> Result<()> {
692 write_locked(
693 &self.common_dir.join("packed-refs"),
694 &write_packed_refs(refs)?,
695 )
696 }
697
698 pub fn pack_refs(&self, prune_loose: bool) -> Result<Vec<PackedRef>> {
699 self.pack_refs_with_peeler(prune_loose, |_, _| Ok(None))
700 }
701
702 pub fn pack_refs_with_peeler<F>(&self, prune_loose: bool, mut peel: F) -> Result<Vec<PackedRef>>
703 where
704 F: FnMut(&str, &ObjectId) -> Result<Option<ObjectId>>,
705 {
706 let mut packed_refs = BTreeMap::new();
707 let packed_path = self.common_dir.join("packed-refs");
708 if packed_path.exists() {
709 for packed in parse_packed_refs(self.format, &fs::read(&packed_path)?)? {
710 packed_refs.insert(packed.reference.name.clone(), packed);
711 }
712 }
713
714 let mut loose_refs = BTreeMap::new();
715 let refs_dir = self.common_dir.join("refs");
716 if refs_dir.exists() {
717 self.collect_loose_refs(&refs_dir, "refs", &mut loose_refs)?;
718 }
719 let mut packed_loose_names = Vec::new();
720 for reference in loose_refs.into_values() {
721 let RefTarget::Direct(oid) = reference.target else {
722 continue;
723 };
724 let peeled = peel(&reference.name, &oid)?;
725 packed_loose_names.push(reference.name.clone());
726 packed_refs.insert(
727 reference.name.clone(),
728 PackedRef {
729 reference: Ref {
730 name: reference.name,
731 target: RefTarget::Direct(oid),
732 },
733 peeled,
734 },
735 );
736 }
737
738 let refs = packed_refs.into_values().collect::<Vec<_>>();
739 self.write_packed_refs(&refs)?;
740 if prune_loose {
741 for name in packed_loose_names {
742 self.delete_loose_ref(&name)?;
743 }
744 }
745 Ok(refs)
746 }
747
748 pub fn current_branch_ref(&self) -> Result<Option<String>> {
749 match self.read_ref("HEAD")? {
750 Some(RefTarget::Symbolic(name)) if name.starts_with("refs/heads/") => Ok(Some(name)),
751 _ => Ok(None),
752 }
753 }
754
755 pub fn current_branch(&self) -> Result<Option<String>> {
756 Ok(self
757 .current_branch_ref()?
758 .and_then(|name| name.strip_prefix("refs/heads/").map(str::to_string)))
759 }
760
761 pub fn transaction(&self) -> FileRefTransaction<'_> {
762 FileRefTransaction {
763 store: self,
764 changes: Vec::new(),
765 }
766 }
767
768 pub fn create_branch(
769 &self,
770 branch: &str,
771 start: ObjectId,
772 committer: Vec<u8>,
773 message: Vec<u8>,
774 ) -> Result<BranchCreate> {
775 let name = branch_ref_name(branch)?;
776 if self.read_ref(&name)?.is_some() {
777 return Err(GitError::Transaction(format!(
778 "branch {branch} already exists"
779 )));
780 }
781 let zero = ObjectId::null(self.format);
782 let mut tx = self.transaction();
783 tx.update(RefUpdate {
784 name: name.clone(),
785 expected: None,
786 new: RefTarget::Direct(start),
787 reflog: Some(ReflogEntry {
788 old_oid: zero,
789 new_oid: start,
790 committer,
791 message,
792 }),
793 });
794 tx.commit()?;
795 Ok(BranchCreate { name, oid: start })
796 }
797
798 pub fn delete_branch(&self, branch: &str) -> Result<BranchDelete> {
799 let name = branch_ref_name(branch)?;
800 if matches!(self.read_ref("HEAD")?, Some(RefTarget::Symbolic(head)) if head == name) {
801 return Err(GitError::Transaction(format!(
802 "cannot delete branch {branch} checked out at HEAD"
803 )));
804 }
805 let oid = self.delete_direct_ref(&name, "branch", branch)?;
806 self.remove_reflog_file(&name);
807 Ok(BranchDelete { name, oid })
808 }
809
810 pub fn move_branch(
811 &self,
812 old_branch: &str,
813 new_branch: &str,
814 force: bool,
815 committer: Vec<u8>,
816 ) -> Result<()> {
817 self.copy_or_move_branch(old_branch, new_branch, force, false, committer)
818 }
819
820 pub fn copy_branch(
821 &self,
822 old_branch: &str,
823 new_branch: &str,
824 force: bool,
825 committer: Vec<u8>,
826 ) -> Result<()> {
827 self.copy_or_move_branch(old_branch, new_branch, force, true, committer)
828 }
829
830 fn conflicting_ref_for_path(&self, new_name: &str, exclude: &str) -> Result<Option<String>> {
837 for reference in self.list_refs()? {
838 let name = &reference.name;
839 if name == new_name || name == exclude {
840 continue;
841 }
842 if new_name.starts_with(&format!("{name}/")) {
844 return Ok(Some(name.clone()));
845 }
846 if name.starts_with(&format!("{new_name}/")) {
848 return Ok(Some(name.clone()));
849 }
850 }
851 Ok(None)
852 }
853
854 fn copy_or_move_branch(
855 &self,
856 old_branch: &str,
857 new_branch: &str,
858 force: bool,
859 copy: bool,
860 committer: Vec<u8>,
861 ) -> Result<()> {
862 let old_name = branch_ref_name(old_branch)?;
863 let new_name = branch_ref_name(new_branch)?;
864 if old_name == new_name {
865 return Ok(());
866 }
867 let Some(target) = self.read_ref(&old_name)? else {
868 return Err(GitError::reference_not_found(format!(
869 "branch {old_branch}"
870 )));
871 };
872 let RefTarget::Direct(oid) = target else {
873 return Err(GitError::InvalidFormat(format!(
874 "branch {old_branch} is symbolic"
875 )));
876 };
877 if let Some(conflict) = self.conflicting_ref_for_path(&new_name, &old_name)? {
883 return Err(GitError::Transaction(format!(
884 "'{conflict}' exists; cannot create '{new_name}'"
885 )));
886 }
887 let dest_entry = self.read_ref(&new_name)?;
891 let dest_resolves = resolve_ref_peeled(self, &new_name)?.is_some();
892 if dest_resolves && !force {
893 return Err(GitError::Transaction(format!(
894 "branch {new_branch} already exists"
895 )));
896 }
897 match dest_entry {
901 Some(RefTarget::Symbolic(_)) => {
902 self.delete_symbolic_ref(&new_name)?;
903 self.remove_reflog_file(&new_name);
904 }
905 Some(RefTarget::Direct(_)) => {
906 let _ = self.delete_direct_ref(&new_name, "branch", new_branch)?;
907 self.remove_reflog_file(&new_name);
908 }
909 None => {}
910 }
911
912 let mut reflog = self.read_reflog(&old_name)?;
915 reflog.push(ReflogEntry {
916 old_oid: oid,
917 new_oid: oid,
918 committer,
919 message: if copy {
920 format!("Branch: copied {old_name} to {new_name}").into_bytes()
921 } else {
922 format!("Branch: renamed {old_name} to {new_name}").into_bytes()
923 },
924 });
925
926 if !copy {
931 let _ = self.delete_direct_ref(&old_name, "branch", old_branch)?;
932 self.remove_reflog_file(&old_name);
933 }
934
935 self.write_loose_ref(&Ref {
936 name: new_name.clone(),
937 target: RefTarget::Direct(oid),
938 })?;
939 self.write_reflog(&new_name, &reflog)?;
940
941 if !copy
942 && matches!(self.read_ref("HEAD")?, Some(RefTarget::Symbolic(head)) if head == old_name)
943 {
944 self.write_loose_ref(&Ref {
945 name: "HEAD".into(),
946 target: RefTarget::Symbolic(new_name),
947 })?;
948 }
949 Ok(())
950 }
951
952 pub fn create_tag(&self, tag: &str, target: ObjectId) -> Result<TagCreate> {
953 let name = tag_ref_name(tag)?;
954 if self.read_ref(&name)?.is_some() {
955 return Err(GitError::Transaction(format!("tag {tag} already exists")));
956 }
957 let mut tx = self.transaction();
958 tx.update(RefUpdate {
959 name: name.clone(),
960 expected: None,
961 new: RefTarget::Direct(target),
962 reflog: None,
963 });
964 tx.commit()?;
965 Ok(TagCreate { name, oid: target })
966 }
967
968 pub fn apply_bundle_ref_updates(
969 &self,
970 refs: &[BundleRefUpdate],
971 reflog: Option<BundleRefUpdateReflog>,
972 ) -> Result<Vec<AppliedBundleRefUpdate>> {
973 let (updates, applied) = prepare_bundle_ref_updates(refs, reflog.as_ref(), |name, oid| {
974 if oid.format() != self.format {
975 return Err(GitError::InvalidObjectId(format!(
976 "bundle ref {name} has {} object id for {} repository",
977 oid.format().name(),
978 self.format.name()
979 )));
980 }
981 self.read_ref(name)
982 })?;
983 let mut tx = self.transaction();
984 for update in updates {
985 tx.update(update);
986 }
987 tx.commit()?;
988 Ok(applied)
989 }
990
991 pub fn delete_tag(&self, tag: &str) -> Result<TagDelete> {
992 let name = TagRefNameBuf::from_tag_name_unrestricted(tag)?.into_string();
993 let oid = self.delete_direct_ref(&name, "tag", tag)?;
994 Ok(TagDelete { name, oid })
995 }
996
997 pub fn delete_ref(&self, name: &str) -> Result<RefDelete> {
998 validate_ref_name(name)?;
999 let oid = self.delete_direct_ref(name, "ref", name)?;
1000 self.remove_reflog_file(name);
1001 Ok(RefDelete {
1002 name: name.into(),
1003 oid,
1004 })
1005 }
1006
1007 pub fn delete_ref_checked(
1008 &self,
1009 delete: DeleteRef,
1010 ) -> std::result::Result<RefDelete, RefDeleteError> {
1011 validate_ref_name(&delete.name).map_err(|_| RefDeleteError::InvalidName)?;
1012 if self.uses_reftable().map_err(ref_delete_error_from_git)? {
1013 return self.delete_reftable_ref_checked(delete);
1014 }
1015 self.delete_files_ref_checked(delete)
1016 }
1017
1018 pub fn delete_symbolic_ref(&self, name: &str) -> Result<bool> {
1019 validate_ref_name_for_read(name)?;
1020 if self.uses_reftable()? {
1021 let Some(target) = self.read_ref(name)? else {
1022 return Ok(false);
1023 };
1024 if !matches!(target, RefTarget::Symbolic(_)) {
1025 return Ok(false);
1026 }
1027 self.append_reftable_records(vec![ReftableRefRecord {
1028 name: name.to_string(),
1029 update_index: 0,
1030 value: ReftableRefValue::Deletion,
1031 }])?;
1032 self.remove_reflog_file(name);
1033 return Ok(true);
1034 }
1035 let Some(reference) = self.read_loose_ref(name)? else {
1036 return Ok(false);
1037 };
1038 if !matches!(reference.target, RefTarget::Symbolic(_)) {
1039 return Ok(false);
1040 }
1041 self.delete_loose_ref(name)?;
1042 self.remove_reflog_file(name);
1043 Ok(true)
1044 }
1045
1046 fn delete_direct_ref(&self, name: &str, kind: &str, short_name: &str) -> Result<ObjectId> {
1047 if self.uses_reftable()? {
1048 let Some(target) = self.read_ref(name)? else {
1049 return Err(GitError::reference_not_found(format!(
1050 "{kind} {short_name}"
1051 )));
1052 };
1053 let RefTarget::Direct(oid) = target else {
1054 return Err(GitError::InvalidFormat(format!(
1055 "{kind} {short_name} is symbolic"
1056 )));
1057 };
1058 self.append_reftable_records(vec![ReftableRefRecord {
1059 name: name.to_string(),
1060 update_index: 0,
1061 value: ReftableRefValue::Deletion,
1062 }])?;
1063 return Ok(oid);
1064 }
1065 let Some(reference) = self.read_loose_ref(name)? else {
1066 return self.delete_packed_ref(name, kind, short_name);
1067 };
1068 let oid = match reference.target {
1069 RefTarget::Direct(oid) => oid,
1070 RefTarget::Symbolic(target) => {
1071 return Err(GitError::InvalidFormat(format!(
1072 "{kind} {short_name} is symbolic to {target}"
1073 )));
1074 }
1075 };
1076 self.delete_loose_ref(name)?;
1077 Ok(oid)
1078 }
1079
1080 fn delete_packed_ref(&self, name: &str, kind: &str, short_name: &str) -> Result<ObjectId> {
1081 let path = self.common_dir.join("packed-refs");
1082 if !path.exists() {
1083 return Err(GitError::reference_not_found(format!(
1084 "{kind} {short_name}"
1085 )));
1086 }
1087 let mut refs = parse_packed_refs(self.format, &fs::read(&path)?)?;
1088 let Some(index) = refs
1089 .iter()
1090 .position(|reference| reference.reference.name == name)
1091 else {
1092 return Err(GitError::reference_not_found(format!(
1093 "{kind} {short_name}"
1094 )));
1095 };
1096 let removed = refs.remove(index);
1097 let RefTarget::Direct(oid) = removed.reference.target else {
1098 return Err(GitError::InvalidFormat(format!(
1099 "{kind} {short_name} is symbolic"
1100 )));
1101 };
1102 self.write_packed_refs(&refs)?;
1103 Ok(oid)
1104 }
1105
1106 fn delete_reftable_ref_checked(
1107 &self,
1108 delete: DeleteRef,
1109 ) -> std::result::Result<RefDelete, RefDeleteError> {
1110 let target = self
1111 .read_ref(&delete.name)
1112 .map_err(ref_delete_error_from_git)?;
1113 let oid = checked_delete_oid(delete.expected_old, target)?;
1114 self.append_reftable_records(vec![ReftableRefRecord {
1115 name: delete.name.clone(),
1116 update_index: 0,
1117 value: ReftableRefValue::Deletion,
1118 }])
1119 .map_err(ref_delete_error_from_git)?;
1120 self.remove_reflog_file(&delete.name);
1123 Ok(RefDelete {
1124 name: delete.name,
1125 oid,
1126 })
1127 }
1128
1129 fn delete_files_ref_checked(
1130 &self,
1131 delete: DeleteRef,
1132 ) -> std::result::Result<RefDelete, RefDeleteError> {
1133 let name = delete.name;
1134 let path = self.ref_path(&name);
1135 let parent = path.parent().ok_or(RefDeleteError::InvalidName)?;
1136 fs::create_dir_all(parent).map_err(RefDeleteError::from)?;
1137
1138 let loose_lock_path = lock_path_for(&path).map_err(|_| RefDeleteError::InvalidName)?;
1139 let _prune_guard = RefDirPruneGuard {
1140 store: self,
1141 name: name.clone(),
1142 };
1143 let loose_lock = DeleteLock::acquire(loose_lock_path)?;
1144
1145 let packed_path = self.common_dir.join("packed-refs");
1146 let packed_lock_path =
1147 lock_path_for(&packed_path).map_err(|_| RefDeleteError::InvalidName)?;
1148 let mut packed_lock = DeleteLock::acquire(packed_lock_path)?;
1149
1150 let loose_ref = self
1151 .read_loose_ref(&name)
1152 .map_err(ref_delete_error_from_git)?;
1153 let packed_original = match fs::read(&packed_path) {
1154 Ok(bytes) => Some(bytes),
1155 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
1156 Err(err) => return Err(RefDeleteError::Io(err)),
1157 };
1158 let mut packed_refs = match &packed_original {
1159 Some(bytes) => {
1160 parse_packed_refs(self.format, bytes).map_err(ref_delete_error_from_git)?
1161 }
1162 None => Vec::new(),
1163 };
1164 let packed_index = packed_refs
1165 .iter()
1166 .position(|reference| reference.reference.name == name);
1167
1168 let current = if let Some(reference) = loose_ref.as_ref() {
1169 Some(reference.target.clone())
1170 } else {
1171 packed_index.map(|index| packed_refs[index].reference.target.clone())
1172 };
1173 let oid = checked_delete_oid(delete.expected_old, current)?;
1174
1175 let packed_changed = if let Some(index) = packed_index {
1176 packed_refs.remove(index);
1177 true
1178 } else {
1179 false
1180 };
1181
1182 if packed_changed {
1183 let packed_bytes =
1184 write_packed_refs(&packed_refs).map_err(ref_delete_error_from_git)?;
1185 packed_lock.write_all(&packed_bytes)?;
1186 let lock_path = packed_lock.close();
1187 if let Err(err) = fs::rename(&lock_path, &packed_path) {
1188 let _ = fs::remove_file(&lock_path);
1189 return Err(RefDeleteError::Io(err));
1190 }
1191 } else {
1192 packed_lock.remove();
1193 }
1194
1195 if loose_ref.is_some()
1196 && let Err(err) = fs::remove_file(&path)
1197 {
1198 if packed_changed && let Some(bytes) = packed_original.as_ref() {
1199 let _ = restore_file_atomically(&packed_path, bytes);
1200 }
1201 return Err(RefDeleteError::Io(err));
1202 }
1203 loose_lock.remove();
1204
1205 self.remove_reflog_file(&name);
1208 Ok(RefDelete { name, oid })
1209 }
1210
1211 fn read_loose_ref(&self, name: &str) -> Result<Option<Ref>> {
1212 let path = self.ref_path(name);
1213 if !path.exists() {
1214 return Ok(None);
1215 }
1216 if path.is_dir() {
1217 return Ok(None);
1218 }
1219 Ok(Some(parse_loose_ref(self.format, name, &fs::read(path)?)?))
1220 }
1221
1222 fn read_packed_ref(&self, name: &str) -> Result<Option<PackedRef>> {
1223 let path = self.common_dir.join("packed-refs");
1224 if !path.exists() {
1225 return Ok(None);
1226 }
1227 Ok(parse_packed_refs(self.format, &fs::read(path)?)?
1228 .into_iter()
1229 .find(|reference| reference.reference.name == name))
1230 }
1231
1232 fn read_reftable_ref(&self, name: &str) -> Result<Option<RefTarget>> {
1233 for table in self.reftables()?.into_iter().rev() {
1234 if let Some(record) = table.refs.into_iter().find(|record| record.name == name) {
1235 return reftable_ref_target(record.value);
1236 }
1237 }
1238 Ok(None)
1239 }
1240
1241 fn list_reftable_refs(&self) -> Result<Vec<Ref>> {
1242 let mut refs = BTreeMap::<String, Ref>::new();
1243 for table in self.reftables()? {
1244 for record in table.refs {
1245 if !record.name.starts_with("refs/") {
1246 continue;
1247 }
1248 match reftable_ref_target(record.value)? {
1249 Some(target) => {
1250 refs.insert(
1251 record.name.clone(),
1252 Ref {
1253 name: record.name,
1254 target,
1255 },
1256 );
1257 }
1258 None => {
1259 refs.remove(&record.name);
1260 }
1261 }
1262 }
1263 }
1264 Ok(refs.into_values().collect())
1265 }
1266
1267 fn reftables(&self) -> Result<Vec<Reftable>> {
1268 let reftable_dir = self.common_dir.join("reftable");
1269 let tables_list = reftable_dir.join("tables.list");
1270 if !tables_list.exists() {
1271 return Ok(Vec::new());
1272 }
1273 let text = fs::read_to_string(&tables_list)?;
1274 let mut tables = Vec::new();
1275 for raw_line in text.lines() {
1276 let line = raw_line.trim();
1277 if line.is_empty() {
1278 continue;
1279 }
1280 if line.contains('/')
1281 || line.contains('\\')
1282 || Path::new(line).components().count() != 1
1283 {
1284 return Err(GitError::InvalidPath(format!(
1285 "invalid reftable table name {line}"
1286 )));
1287 }
1288 let table = Reftable::parse(&fs::read(reftable_dir.join(line))?)?;
1289 if table.header.object_format != self.format {
1290 return Err(GitError::InvalidFormat(format!(
1291 "reftable {line} has {} object ids in {} repository",
1292 table.header.object_format.name(),
1293 self.format.name()
1294 )));
1295 }
1296 tables.push(table);
1297 }
1298 Ok(tables)
1299 }
1300
1301 fn uses_reftable(&self) -> Result<bool> {
1302 let config_path = self.common_dir.join("config");
1303 if !config_path.exists() {
1304 return Ok(false);
1305 }
1306 let config = GitConfig::parse(&fs::read(config_path)?)?;
1307 Ok(matches!(
1308 config.get("extensions", None, "refStorage"),
1309 Some(value) if value.eq_ignore_ascii_case("reftable")
1310 ))
1311 }
1312
1313 fn append_reftable_records(&self, mut records: Vec<ReftableRefRecord>) -> Result<()> {
1314 if records.is_empty() {
1315 return Ok(());
1316 }
1317 let reftable_dir = self.common_dir.join("reftable");
1318 fs::create_dir_all(&reftable_dir)?;
1319 let tables_list = reftable_dir.join("tables.list");
1320 let mut table_names = if tables_list.exists() {
1321 fs::read_to_string(&tables_list)?
1322 .lines()
1323 .map(str::trim)
1324 .filter(|line| !line.is_empty())
1325 .map(str::to_string)
1326 .collect::<Vec<_>>()
1327 } else {
1328 Vec::new()
1329 };
1330 let update_index = self.next_reftable_update_index(&table_names)?;
1331 for record in &mut records {
1332 record.update_index = update_index;
1333 }
1334 let table_name = reftable_table_name(update_index);
1335 let bytes = Reftable::write_ref_only(self.format, update_index, update_index, &records)?;
1336 write_locked(&reftable_dir.join(&table_name), &bytes)?;
1337 table_names.push(table_name);
1338 let mut list = Vec::new();
1339 for name in &table_names {
1340 list.extend_from_slice(name.as_bytes());
1341 list.push(b'\n');
1342 }
1343 write_locked(&tables_list, &list)
1344 }
1345
1346 fn next_reftable_update_index(&self, table_names: &[String]) -> Result<u64> {
1347 let reftable_dir = self.common_dir.join("reftable");
1348 let mut max_update_index = 0;
1349 for name in table_names {
1350 let table = Reftable::parse(&fs::read(reftable_dir.join(name))?)?;
1351 max_update_index = max_update_index.max(table.header.max_update_index);
1352 }
1353 max_update_index
1354 .checked_add(1)
1355 .ok_or_else(|| GitError::InvalidFormat("reftable update index overflow".into()))
1356 }
1357
1358 fn collect_loose_refs(
1359 &self,
1360 dir: &Path,
1361 prefix: &str,
1362 refs: &mut BTreeMap<String, Ref>,
1363 ) -> Result<()> {
1364 for entry in fs::read_dir(dir)? {
1365 let entry = entry?;
1366 let path = entry.path();
1367 let name = format!("{prefix}/{}", entry.file_name().to_string_lossy());
1368 if path.is_dir() {
1369 self.collect_loose_refs(&path, &name, refs)?;
1370 } else if !name.ends_with(".lock") {
1371 let reference = parse_loose_ref(self.format, name.clone(), &fs::read(path)?)?;
1372 refs.insert(name, reference);
1373 }
1374 }
1375 Ok(())
1376 }
1377
1378 fn loose_refs_have_prefix(&self, prefix: &str) -> Result<bool> {
1379 if !prefix.starts_with("refs/") || !prefix.ends_with('/') {
1380 return Ok(self
1381 .list_refs()?
1382 .iter()
1383 .any(|reference| reference.name.starts_with(prefix)));
1384 }
1385 let loose_prefix = prefix.trim_end_matches('/');
1386 let dir = self.common_dir.join(loose_prefix);
1387 match fs::metadata(&dir) {
1388 Ok(meta) if meta.is_dir() => {
1389 let mut refs = BTreeMap::new();
1390 self.collect_loose_refs(&dir, loose_prefix, &mut refs)?;
1391 Ok(!refs.is_empty())
1392 }
1393 Ok(_) => Ok(false),
1394 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
1395 Err(err) => Err(err.into()),
1396 }
1397 }
1398
1399 fn write_loose_ref(&self, reference: &Ref) -> Result<()> {
1400 if self.uses_reftable()? {
1401 self.append_reftable_records(vec![ReftableRefRecord {
1402 name: reference.name.clone(),
1403 update_index: 0,
1404 value: reftable_value_from_ref_target(&reference.target),
1405 }])?;
1406 return Ok(());
1407 }
1408 let path = self.ref_path(&reference.name);
1409 let parent = path
1410 .parent()
1411 .ok_or_else(|| GitError::InvalidPath("ref path has no parent".into()))?;
1412 fs::create_dir_all(parent)?;
1413 write_locked(&path, &write_loose_ref(reference))
1414 }
1415
1416 fn delete_loose_ref(&self, name: &str) -> Result<()> {
1417 let path = self.ref_path(name);
1418 let lock_path = lock_path_for(&path)?;
1419 {
1420 let mut file = fs::OpenOptions::new()
1421 .write(true)
1422 .create_new(true)
1423 .open(&lock_path)?;
1424 file.write_all(b"delete\n")?;
1425 file.sync_all()?;
1426 }
1427 match fs::remove_file(&path) {
1428 Ok(()) => {
1429 fs::remove_file(lock_path)?;
1430 self.prune_empty_ref_dirs(name);
1431 Ok(())
1432 }
1433 Err(err) => {
1434 let _ = fs::remove_file(lock_path);
1435 Err(GitError::Io(err.to_string()))
1436 }
1437 }
1438 }
1439
1440 fn prune_empty_ref_dirs(&self, name: &str) {
1446 let base = self.ref_base_dir(name).to_path_buf();
1447 let refs_root = base.join("refs");
1448 if let Some(parent) = self.ref_path(name).parent() {
1449 prune_empty_dirs_up_to(parent, &refs_root);
1450 }
1451 }
1452
1453 fn remove_reflog_file(&self, name: &str) {
1459 let path = self.reflog_path(name);
1460 let _ = fs::remove_file(&path);
1461 let base = self.ref_base_dir(name).to_path_buf();
1462 let logs_refs_root = base.join("logs").join("refs");
1463 if let Some(parent) = path.parent() {
1464 prune_empty_dirs_up_to(parent, &logs_refs_root);
1465 }
1466 }
1467
1468 fn head_reflog_mirror(
1480 head_branch: Option<&str>,
1481 reflogs: &[(String, ReflogEntry)],
1482 ) -> Vec<(String, ReflogEntry)> {
1483 let Some(head_branch) = head_branch else {
1484 return Vec::new();
1485 };
1486 if reflogs.iter().any(|(name, _)| name == "HEAD") {
1489 return Vec::new();
1490 }
1491 reflogs
1492 .iter()
1493 .filter(|(name, _)| name == head_branch)
1494 .map(|(_, entry)| ("HEAD".to_string(), entry.clone()))
1495 .collect()
1496 }
1497
1498 fn head_symref_target(&self) -> Option<String> {
1502 match self.read_ref("HEAD") {
1503 Ok(Some(RefTarget::Symbolic(branch))) => Some(branch),
1504 _ => None,
1505 }
1506 }
1507
1508 pub fn append_reflog(&self, name: &str, entry: &ReflogEntry) -> Result<()> {
1509 validate_ref_name_for_read(name)?;
1510 let path = self.reflog_path(name);
1511 let parent = path
1512 .parent()
1513 .ok_or_else(|| GitError::InvalidPath("reflog path has no parent".into()))?;
1514 fs::create_dir_all(parent)?;
1515 let mut file = fs::OpenOptions::new()
1516 .create(true)
1517 .append(true)
1518 .open(path)?;
1519 file.write_all(&entry.to_line())?;
1520 file.sync_all()?;
1521 Ok(())
1522 }
1523
1524 fn ref_path(&self, name: &str) -> PathBuf {
1525 self.ref_base_dir(name).join(name)
1526 }
1527
1528 fn reflog_path(&self, name: &str) -> PathBuf {
1529 self.ref_base_dir(name).join("logs").join(name)
1530 }
1531
1532 fn ref_base_dir(&self, name: &str) -> &Path {
1533 if name == "HEAD" {
1534 &self.git_dir
1535 } else {
1536 &self.common_dir
1537 }
1538 }
1539
1540 fn check_ref_directory_conflict(&self, name: &str) -> Result<()> {
1541 let components = name.split('/').collect::<Vec<_>>();
1542 for index in 1..components.len() {
1543 let ancestor = components[..index].join("/");
1544 if self.read_ref_unchecked(&ancestor)?.is_some() {
1545 return Err(ref_directory_conflict_error(name, &ancestor));
1546 }
1547 }
1548 let child_prefix = format!("{name}/");
1549 for reference in self.list_refs()? {
1550 if reference.name.starts_with(&child_prefix) {
1551 return Err(ref_directory_conflict_error(name, &reference.name));
1552 }
1553 }
1554 Ok(())
1555 }
1556}
1557
1558fn reftable_ref_target(value: ReftableRefValue) -> Result<Option<RefTarget>> {
1559 match value {
1560 ReftableRefValue::Deletion => Ok(None),
1561 ReftableRefValue::Direct(oid) | ReftableRefValue::Peeled { target: oid, .. } => {
1562 Ok(Some(RefTarget::Direct(oid)))
1563 }
1564 ReftableRefValue::Symbolic(target) => Ok(Some(RefTarget::Symbolic(target))),
1565 }
1566}
1567
1568fn reftable_value_from_ref_target(target: &RefTarget) -> ReftableRefValue {
1569 match target {
1570 RefTarget::Direct(oid) => ReftableRefValue::Direct(*oid),
1571 RefTarget::Symbolic(target) => ReftableRefValue::Symbolic(target.clone()),
1572 }
1573}
1574
1575fn reftable_table_name(update_index: u64) -> String {
1576 let nanos = SystemTime::now()
1577 .duration_since(UNIX_EPOCH)
1578 .map(|duration| duration.as_nanos())
1579 .unwrap_or(0);
1580 format!("0x{update_index:012x}-0x{update_index:012x}-sley-{nanos:x}.ref")
1581}
1582
1583fn repository_common_dir(git_dir: &Path) -> PathBuf {
1584 if let Some(common_dir) = std::env::var_os("GIT_COMMON_DIR") {
1585 return PathBuf::from(common_dir);
1586 }
1587 let commondir = git_dir.join("commondir");
1588 if let Ok(value) = fs::read_to_string(&commondir) {
1589 let path = PathBuf::from(value.trim());
1590 let common = if path.is_absolute() {
1591 path
1592 } else {
1593 git_dir.join(path)
1594 };
1595 return fs::canonicalize(&common).unwrap_or(common);
1596 }
1597 git_dir.to_path_buf()
1598}
1599
1600pub struct FileRefTransaction<'a> {
1601 store: &'a FileRefStore,
1602 changes: Vec<QueuedRefChange>,
1603}
1604
1605struct QueuedUpdate {
1608 name: String,
1609 precondition: RefPrecondition,
1610 new: RefTarget,
1611 reflog: Option<ReflogEntry>,
1612}
1613
1614struct QueuedDelete {
1615 name: String,
1616 precondition: RefDeletePrecondition,
1617}
1618
1619enum QueuedRefChange {
1620 Update(QueuedUpdate),
1621 Delete(QueuedDelete),
1622}
1623
1624#[derive(Debug, Clone, PartialEq, Eq)]
1626pub enum RefDeletePrecondition {
1627 Any,
1629 Immediate(RefTarget),
1631 Direct(Option<ObjectId>),
1633 Peeled(ObjectId),
1635}
1636
1637impl<'a> FileRefTransaction<'a> {
1638 pub fn update(&mut self, update: RefUpdate) {
1643 self.changes.push(QueuedRefChange::Update(QueuedUpdate {
1644 name: update.name,
1645 precondition: RefPrecondition::from_expected(update.expected),
1646 new: update.new,
1647 reflog: update.reflog,
1648 }));
1649 }
1650
1651 pub fn update_to(
1657 &mut self,
1658 name: impl Into<String>,
1659 new: RefTarget,
1660 precondition: RefPrecondition,
1661 reflog: Option<ReflogEntry>,
1662 ) {
1663 self.changes.push(QueuedRefChange::Update(QueuedUpdate {
1664 name: name.into(),
1665 precondition,
1666 new,
1667 reflog,
1668 }));
1669 }
1670
1671 pub fn delete(&mut self, delete: DeleteRef) {
1676 self.delete_with_precondition(
1677 delete.name,
1678 RefDeletePrecondition::Direct(delete.expected_old),
1679 delete.reflog,
1680 );
1681 }
1682
1683 pub fn delete_with_precondition(
1689 &mut self,
1690 name: impl Into<String>,
1691 precondition: RefDeletePrecondition,
1692 _reflog: Option<DeleteRefReflog>,
1693 ) {
1694 self.changes.push(QueuedRefChange::Delete(QueuedDelete {
1695 name: name.into(),
1696 precondition,
1697 }));
1698 }
1699
1700 pub fn commit(self) -> Result<()> {
1721 let FileRefTransaction { store, changes } = self;
1722 let changes = coalesce_ref_changes(changes)?;
1723 if store.uses_reftable()? {
1724 return store.commit_reftable(changes);
1725 }
1726 store.commit_loose(changes)
1727 }
1728}
1729
1730impl FileRefStore {
1731 fn commit_reftable(&self, changes: Vec<CoalescedRefChange>) -> Result<()> {
1732 let head_branch = self.head_symref_target();
1735 let mut records = Vec::with_capacity(changes.len());
1736 let mut reflogs = Vec::new();
1737 let mut delete_names = Vec::new();
1738 for change in changes {
1739 match change {
1740 CoalescedRefChange::Update(update) => {
1741 if !matches!(update.precondition, RefPrecondition::Any) {
1742 let current = self.read_ref(&update.name)?;
1743 if !update.precondition.is_satisfied_by(current.as_ref()) {
1744 return Err(GitError::Transaction(
1745 update.precondition.describe(&update.name),
1746 ));
1747 }
1748 }
1749 records.push(ReftableRefRecord {
1750 name: update.name.clone(),
1751 update_index: 0,
1752 value: reftable_value_from_ref_target(&update.new),
1753 });
1754 for entry in update.reflog {
1755 reflogs.push((update.name.clone(), entry));
1756 }
1757 }
1758 CoalescedRefChange::Delete(delete) => {
1759 let current = self.read_ref(&delete.name)?;
1760 verify_delete_precondition(
1764 self,
1765 &delete.name,
1766 current.as_ref(),
1767 &delete.precondition,
1768 )?;
1769 records.push(ReftableRefRecord {
1770 name: delete.name.clone(),
1771 update_index: 0,
1772 value: ReftableRefValue::Deletion,
1773 });
1774 delete_names.push(delete.name.clone());
1775 }
1776 }
1777 }
1778 self.append_reftable_records(records)?;
1779 for name in &delete_names {
1783 self.remove_reflog_file(name);
1784 }
1785 let head_mirror = Self::head_reflog_mirror(head_branch.as_deref(), &reflogs);
1786 reflogs.extend(head_mirror);
1787 for (name, entry) in reflogs {
1788 self.append_reflog(&name, &entry)?;
1789 }
1790 Ok(())
1791 }
1792
1793 fn commit_loose(&self, changes: Vec<CoalescedRefChange>) -> Result<()> {
1796 let head_branch = self.head_symref_target();
1799 let has_delete = changes
1800 .iter()
1801 .any(|change| matches!(change, CoalescedRefChange::Delete(_)));
1802 let mut pending = Vec::with_capacity(changes.len() + usize::from(has_delete));
1803 for change in &changes {
1805 let name = change.name();
1806 if matches!(change, CoalescedRefChange::Update(_))
1807 && let Err(err) = self.check_ref_directory_conflict(name)
1808 {
1809 release_pending_locks(&pending);
1810 return Err(err);
1811 }
1812 let path = self.ref_path(name);
1813 let parent = path
1814 .parent()
1815 .ok_or_else(|| GitError::InvalidPath("ref path has no parent".into()))?;
1816 if let Err(err) = fs::create_dir_all(parent) {
1817 release_pending_locks(&pending);
1818 if err.kind() == std::io::ErrorKind::NotADirectory {
1819 return Err(ref_directory_conflict_error(
1820 name,
1821 &parent_to_ref_name(self.ref_base_dir(name), parent),
1822 ));
1823 }
1824 return Err(GitError::Io(err.to_string()));
1825 }
1826 let lock_path = match lock_path_for(&path) {
1827 Ok(lock_path) => lock_path,
1828 Err(err) => {
1829 release_pending_locks(&pending);
1830 return Err(err);
1831 }
1832 };
1833 if let Err(err) = fs::OpenOptions::new()
1834 .write(true)
1835 .create_new(true)
1836 .open(&lock_path)
1837 {
1838 release_pending_locks(&pending);
1839 return Err(GitError::Io(format!("could not lock ref {name}: {err}")));
1840 }
1841 let action = match change {
1842 CoalescedRefChange::Update(update) => PendingPathAction::Write {
1843 contents: write_loose_ref(&Ref {
1844 name: update.name.clone(),
1845 target: update.new.clone(),
1846 }),
1847 },
1848 CoalescedRefChange::Delete(_) => PendingPathAction::Delete,
1849 };
1850 pending.push(PendingPathChange {
1851 name: name.to_string(),
1852 path,
1853 lock_path,
1854 original: None,
1855 action,
1856 });
1857 }
1858
1859 let packed_path = self.common_dir.join("packed-refs");
1860 let mut packed_refs = Vec::new();
1861 if has_delete {
1862 let packed_lock_path = match lock_path_for(&packed_path) {
1863 Ok(lock_path) => lock_path,
1864 Err(err) => {
1865 release_pending_locks(&pending);
1866 return Err(err);
1867 }
1868 };
1869 if let Err(err) = fs::OpenOptions::new()
1870 .write(true)
1871 .create_new(true)
1872 .open(&packed_lock_path)
1873 {
1874 release_pending_locks(&pending);
1875 return Err(GitError::Io(format!("could not lock packed-refs: {err}")));
1876 }
1877 let packed_original = match fs::read(&packed_path) {
1878 Ok(bytes) => Some(bytes),
1879 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
1880 Err(err) => {
1881 release_pending_locks(&pending);
1882 let _ = fs::remove_file(&packed_lock_path);
1883 return Err(GitError::Io(err.to_string()));
1884 }
1885 };
1886 packed_refs = match &packed_original {
1887 Some(bytes) => match parse_packed_refs(self.format, bytes) {
1888 Ok(refs) => refs,
1889 Err(err) => {
1890 release_pending_locks(&pending);
1891 let _ = fs::remove_file(&packed_lock_path);
1892 return Err(err);
1893 }
1894 },
1895 None => Vec::new(),
1896 };
1897 pending.push(PendingPathChange {
1898 name: "packed-refs".into(),
1899 path: packed_path.clone(),
1900 lock_path: packed_lock_path,
1901 original: packed_original,
1902 action: PendingPathAction::ReleaseLock,
1903 });
1904 }
1905
1906 let mut reflogs = Vec::new();
1910 let mut delete_names = BTreeSet::new();
1911 for index in 0..changes.len() {
1912 match &changes[index] {
1913 CoalescedRefChange::Update(update) => {
1914 if !matches!(update.precondition, RefPrecondition::Any) {
1915 let current = if has_delete {
1916 match self.read_ref_from_locked_packed(&update.name, &packed_refs) {
1917 Ok(current) => current,
1918 Err(err) => {
1919 release_pending_locks(&pending);
1920 return Err(err);
1921 }
1922 }
1923 } else {
1924 match self.read_ref(&update.name) {
1925 Ok(current) => current,
1926 Err(err) => {
1927 release_pending_locks(&pending);
1928 return Err(err);
1929 }
1930 }
1931 };
1932 if !update.precondition.is_satisfied_by(current.as_ref()) {
1933 release_pending_locks(&pending);
1934 return Err(GitError::Transaction(
1935 update.precondition.describe(&update.name),
1936 ));
1937 }
1938 }
1939 pending[index].original = match read_optional_file(&pending[index].path) {
1940 Ok(original) => original,
1941 Err(err) => {
1942 release_pending_locks(&pending);
1943 return Err(err);
1944 }
1945 };
1946 for entry in &update.reflog {
1947 reflogs.push((update.name.clone(), entry.clone()));
1948 }
1949 }
1950 CoalescedRefChange::Delete(delete) => {
1951 let state = match self.read_locked_ref_state(&delete.name, &packed_refs) {
1952 Ok(state) => state,
1953 Err(err) => {
1954 release_pending_locks(&pending);
1955 return Err(err);
1956 }
1957 };
1958 if let Err(err) = verify_delete_precondition(
1962 self,
1963 &delete.name,
1964 state.current.as_ref(),
1965 &delete.precondition,
1966 ) {
1967 release_pending_locks(&pending);
1968 return Err(err);
1969 }
1970 pending[index].original = if state.has_loose {
1971 match read_optional_file(&pending[index].path) {
1972 Ok(original) => original,
1973 Err(err) => {
1974 release_pending_locks(&pending);
1975 return Err(err);
1976 }
1977 }
1978 } else {
1979 None
1980 };
1981 delete_names.insert(delete.name.clone());
1982 }
1983 }
1984 }
1985
1986 if has_delete {
1987 let old_len = packed_refs.len();
1988 packed_refs.retain(|reference| !delete_names.contains(&reference.reference.name));
1989 if packed_refs.len() != old_len {
1990 let packed_bytes = match write_packed_refs(&packed_refs) {
1991 Ok(bytes) => bytes,
1992 Err(err) => {
1993 release_pending_locks(&pending);
1994 return Err(err);
1995 }
1996 };
1997 if let Some(packed) = pending.last_mut() {
1998 packed.action = PendingPathAction::Write {
1999 contents: packed_bytes,
2000 };
2001 }
2002 }
2003 }
2004
2005 for change in &pending {
2008 if let Err(err) = stage_pending_change(change) {
2009 release_pending_locks(&pending);
2010 return Err(err);
2011 }
2012 }
2013
2014 for index in 0..pending.len() {
2017 if let Err(err) = maybe_fail_loose_commit_action(index) {
2018 rollback_after_apply(&pending, index);
2019 return Err(err);
2020 }
2021 if let Err(err) = apply_pending_change(&pending[index]) {
2022 rollback_after_apply(&pending, index + 1);
2023 return Err(err);
2024 }
2025 }
2026
2027 for change in &pending {
2028 if matches!(change.action, PendingPathAction::Delete) && change.original.is_some() {
2029 self.prune_empty_ref_dirs(&change.name);
2030 }
2031 }
2032 for name in &delete_names {
2036 self.remove_reflog_file(name);
2037 }
2038 let head_mirror = Self::head_reflog_mirror(head_branch.as_deref(), &reflogs);
2041 reflogs.extend(head_mirror);
2042 for (name, entry) in reflogs {
2044 self.append_reflog(&name, &entry)?;
2045 }
2046 Ok(())
2047 }
2048
2049 fn read_ref_from_locked_packed(
2050 &self,
2051 name: &str,
2052 packed_refs: &[PackedRef],
2053 ) -> Result<Option<RefTarget>> {
2054 let state = self.read_locked_ref_state(name, packed_refs)?;
2055 Ok(state.current)
2056 }
2057
2058 fn read_locked_ref_state(
2059 &self,
2060 name: &str,
2061 packed_refs: &[PackedRef],
2062 ) -> Result<LockedRefState> {
2063 let loose = self.read_loose_ref(name)?;
2064 let packed_index = packed_refs
2065 .iter()
2066 .position(|reference| reference.reference.name == name);
2067 let current = if let Some(reference) = loose.as_ref() {
2068 Some(reference.target.clone())
2069 } else {
2070 packed_index.map(|index| packed_refs[index].reference.target.clone())
2071 };
2072 Ok(LockedRefState {
2073 current,
2074 has_loose: loose.is_some(),
2075 })
2076 }
2077}
2078
2079struct LockedRefState {
2080 current: Option<RefTarget>,
2081 has_loose: bool,
2082}
2083
2084enum CoalescedRefChange {
2085 Update(CoalescedRefUpdate),
2086 Delete(CoalescedRefDelete),
2087}
2088
2089impl CoalescedRefChange {
2090 fn name(&self) -> &str {
2091 match self {
2092 Self::Update(update) => &update.name,
2093 Self::Delete(delete) => &delete.name,
2094 }
2095 }
2096}
2097
2098struct CoalescedRefUpdate {
2100 name: String,
2101 precondition: RefPrecondition,
2102 new: RefTarget,
2103 reflog: Vec<ReflogEntry>,
2104}
2105
2106struct CoalescedRefDelete {
2107 name: String,
2108 precondition: RefDeletePrecondition,
2109}
2110
2111fn coalesce_ref_changes(changes: Vec<QueuedRefChange>) -> Result<Vec<CoalescedRefChange>> {
2112 let has_delete = changes
2113 .iter()
2114 .any(|change| matches!(change, QueuedRefChange::Delete(_)));
2115 if !has_delete {
2116 let updates = changes
2117 .into_iter()
2118 .map(|change| match change {
2119 QueuedRefChange::Update(update) => update,
2120 QueuedRefChange::Delete(_) => unreachable!("has_delete was false"),
2121 })
2122 .collect::<Vec<_>>();
2123 return coalesce_ref_updates(updates).map(|updates| {
2124 updates
2125 .into_iter()
2126 .map(CoalescedRefChange::Update)
2127 .collect()
2128 });
2129 }
2130
2131 let mut seen = BTreeSet::new();
2132 let mut coalesced = Vec::with_capacity(changes.len());
2133 for change in changes {
2134 let name = match &change {
2135 QueuedRefChange::Update(update) => &update.name,
2136 QueuedRefChange::Delete(delete) => &delete.name,
2137 };
2138 validate_ref_name_for_update(name)?;
2139 if !seen.insert(name.clone()) {
2140 return Err(GitError::Transaction(format!(
2141 "ref {name} appears more than once in transaction"
2142 )));
2143 }
2144 coalesced.push(match change {
2145 QueuedRefChange::Update(update) => CoalescedRefChange::Update(CoalescedRefUpdate {
2146 name: update.name,
2147 precondition: update.precondition,
2148 new: update.new,
2149 reflog: update.reflog.into_iter().collect(),
2150 }),
2151 QueuedRefChange::Delete(delete) => CoalescedRefChange::Delete(CoalescedRefDelete {
2152 name: delete.name,
2153 precondition: delete.precondition,
2154 }),
2155 });
2156 }
2157 Ok(coalesced)
2158}
2159
2160fn coalesce_ref_updates(updates: Vec<QueuedUpdate>) -> Result<Vec<CoalescedRefUpdate>> {
2165 let mut order: Vec<String> = Vec::new();
2166 let mut by_name: HashMap<String, CoalescedRefUpdate> = HashMap::new();
2167 for update in updates {
2168 validate_ref_name_for_update(&update.name)?;
2169 match by_name.get_mut(&update.name) {
2170 Some(existing) => {
2171 existing.new = update.new;
2172 if let Some(entry) = update.reflog {
2173 existing.reflog.push(entry);
2174 }
2175 }
2176 None => {
2177 order.push(update.name.clone());
2178 by_name.insert(
2179 update.name.clone(),
2180 CoalescedRefUpdate {
2181 name: update.name,
2182 precondition: update.precondition,
2183 new: update.new,
2184 reflog: update.reflog.into_iter().collect(),
2185 },
2186 );
2187 }
2188 }
2189 }
2190 let mut coalesced = Vec::with_capacity(order.len());
2191 for name in order {
2192 if let Some(update) = by_name.remove(&name) {
2193 coalesced.push(update);
2194 }
2195 }
2196 Ok(coalesced)
2197}
2198
2199struct PendingPathChange {
2202 name: String,
2203 path: PathBuf,
2204 lock_path: PathBuf,
2205 original: Option<Vec<u8>>,
2206 action: PendingPathAction,
2207}
2208
2209enum PendingPathAction {
2210 Write { contents: Vec<u8> },
2211 Delete,
2212 ReleaseLock,
2213}
2214
2215struct RefDirPruneGuard<'a> {
2216 store: &'a FileRefStore,
2217 name: String,
2218}
2219
2220impl Drop for RefDirPruneGuard<'_> {
2221 fn drop(&mut self) {
2222 self.store.prune_empty_ref_dirs(&self.name);
2223 }
2224}
2225
2226struct DeleteLock {
2227 path: PathBuf,
2228 file: Option<fs::File>,
2229 active: bool,
2230}
2231
2232impl DeleteLock {
2233 fn acquire(path: PathBuf) -> std::result::Result<Self, RefDeleteError> {
2234 match fs::OpenOptions::new()
2235 .write(true)
2236 .create_new(true)
2237 .open(&path)
2238 {
2239 Ok(file) => Ok(Self {
2240 path,
2241 file: Some(file),
2242 active: true,
2243 }),
2244 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
2245 Err(RefDeleteError::Locked)
2246 }
2247 Err(err) => Err(RefDeleteError::Io(err)),
2248 }
2249 }
2250
2251 fn write_all(&mut self, bytes: &[u8]) -> std::result::Result<(), RefDeleteError> {
2252 let Some(file) = self.file.as_mut() else {
2253 return Err(RefDeleteError::Io(std::io::Error::other(
2254 "lock file is already closed",
2255 )));
2256 };
2257 file.set_len(0)?;
2258 file.write_all(bytes)?;
2259 file.sync_all()?;
2260 Ok(())
2261 }
2262
2263 fn close(mut self) -> PathBuf {
2264 self.active = false;
2265 let _ = self.file.take();
2266 self.path.clone()
2267 }
2268
2269 fn remove(mut self) {
2270 self.active = false;
2271 let _ = self.file.take();
2272 let _ = fs::remove_file(&self.path);
2273 }
2274}
2275
2276impl Drop for DeleteLock {
2277 fn drop(&mut self) {
2278 if self.active {
2279 let _ = self.file.take();
2280 let _ = fs::remove_file(&self.path);
2281 }
2282 }
2283}
2284
2285fn checked_delete_oid(
2286 expected: Option<ObjectId>,
2287 current: Option<RefTarget>,
2288) -> std::result::Result<ObjectId, RefDeleteError> {
2289 let Some(current) = current else {
2290 return Err(RefDeleteError::NotFound);
2291 };
2292 let RefTarget::Direct(actual) = current else {
2293 return Err(RefDeleteError::ExpectedMismatch {
2294 expected,
2295 actual: None,
2296 });
2297 };
2298 if let Some(expected_oid) = expected
2299 && expected_oid != actual
2300 {
2301 return Err(RefDeleteError::ExpectedMismatch {
2302 expected: Some(expected_oid),
2303 actual: Some(actual),
2304 });
2305 }
2306 Ok(actual)
2307}
2308
2309fn verify_delete_precondition(
2315 store: &FileRefStore,
2316 name: &str,
2317 current: Option<&RefTarget>,
2318 precondition: &RefDeletePrecondition,
2319) -> Result<()> {
2320 let Some(current) = current else {
2321 return Err(GitError::Transaction(format!("ref {name} not found")));
2322 };
2323 match precondition {
2324 RefDeletePrecondition::Any => {
2325 peeled_oid_for_delete(store, current)?;
2326 Ok(())
2327 }
2328 RefDeletePrecondition::Immediate(expected) if current == expected => {
2329 peeled_oid_for_delete(store, current)?;
2330 Ok(())
2331 }
2332 RefDeletePrecondition::Immediate(_) => Err(delete_precondition_mismatch(name)),
2333 RefDeletePrecondition::Direct(expected) => {
2334 let RefTarget::Direct(actual) = current else {
2335 return Err(delete_precondition_mismatch(name));
2336 };
2337 if let Some(expected) = expected
2338 && expected != actual
2339 {
2340 return Err(delete_precondition_mismatch(name));
2341 }
2342 Ok(())
2343 }
2344 RefDeletePrecondition::Peeled(expected) => {
2345 let actual = peeled_oid_for_delete(store, current)?;
2346 if actual == Some(*expected) {
2347 Ok(())
2348 } else {
2349 Err(delete_precondition_mismatch(name))
2350 }
2351 }
2352 }
2353}
2354
2355fn peeled_oid_for_delete(store: &FileRefStore, target: &RefTarget) -> Result<Option<ObjectId>> {
2356 match target {
2357 RefTarget::Direct(oid) => Ok(Some(*oid)),
2358 RefTarget::Symbolic(name) => resolve_ref_peeled(store, name),
2359 }
2360}
2361
2362fn delete_precondition_mismatch(name: &str) -> GitError {
2363 GitError::Transaction(format!("expected ref {name} to match"))
2364}
2365
2366fn ref_delete_error_from_git(err: GitError) -> RefDeleteError {
2367 match err {
2368 GitError::InvalidPath(_) => RefDeleteError::InvalidName,
2369 GitError::NotFound(_) => RefDeleteError::NotFound,
2370 GitError::Io(message) if message.contains("File exists") => RefDeleteError::Locked,
2371 GitError::Io(message) if message.contains("could not lock") => RefDeleteError::Locked,
2372 GitError::Transaction(message) if message.contains("could not lock") => {
2373 RefDeleteError::Locked
2374 }
2375 other => RefDeleteError::Io(std::io::Error::other(other.to_string())),
2376 }
2377}
2378
2379fn read_optional_file(path: &Path) -> Result<Option<Vec<u8>>> {
2380 match fs::read(path) {
2381 Ok(bytes) => Ok(Some(bytes)),
2382 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
2383 Err(err) => Err(GitError::Io(err.to_string())),
2384 }
2385}
2386
2387fn stage_lock_file(lock_path: &Path, contents: &[u8]) -> Result<()> {
2388 let mut file = fs::OpenOptions::new()
2389 .write(true)
2390 .truncate(true)
2391 .open(lock_path)?;
2392 file.write_all(contents)?;
2393 file.sync_all()?;
2394 Ok(())
2395}
2396
2397fn stage_pending_change(change: &PendingPathChange) -> Result<()> {
2398 match &change.action {
2399 PendingPathAction::Write { contents } => stage_lock_file(&change.lock_path, contents),
2400 PendingPathAction::Delete => stage_lock_file(&change.lock_path, b"delete\n"),
2401 PendingPathAction::ReleaseLock => Ok(()),
2402 }
2403}
2404
2405fn apply_pending_change(change: &PendingPathChange) -> Result<()> {
2406 match &change.action {
2407 PendingPathAction::Write { .. } => {
2408 fs::rename(&change.lock_path, &change.path).map_err(|err| GitError::Io(err.to_string()))
2409 }
2410 PendingPathAction::Delete => {
2411 if change.original.is_some() {
2412 fs::remove_file(&change.path).map_err(|err| GitError::Io(err.to_string()))?;
2413 }
2414 fs::remove_file(&change.lock_path).map_err(|err| GitError::Io(err.to_string()))
2415 }
2416 PendingPathAction::ReleaseLock => {
2417 fs::remove_file(&change.lock_path).map_err(|err| GitError::Io(err.to_string()))
2418 }
2419 }
2420}
2421
2422fn release_pending_locks(pending: &[PendingPathChange]) {
2425 for change in pending {
2426 let _ = fs::remove_file(&change.lock_path);
2427 }
2428}
2429
2430fn rollback_after_apply(pending: &[PendingPathChange], applied: usize) {
2434 for change in pending.iter().take(applied) {
2435 if matches!(change.action, PendingPathAction::ReleaseLock) {
2436 let _ = fs::remove_file(&change.lock_path);
2437 continue;
2438 }
2439 match &change.original {
2440 Some(bytes) => {
2441 let _ = restore_file_atomically(&change.path, bytes);
2442 }
2443 None => {
2444 let _ = fs::remove_file(&change.path);
2445 }
2446 }
2447 let _ = fs::remove_file(&change.lock_path);
2448 }
2449 for change in pending.iter().skip(applied) {
2450 let _ = fs::remove_file(&change.lock_path);
2451 }
2452}
2453
2454#[cfg(test)]
2455thread_local! {
2456 static FAIL_LOOSE_COMMIT_ACTION: std::cell::Cell<Option<usize>> =
2457 const { std::cell::Cell::new(None) };
2458}
2459
2460#[cfg(test)]
2461fn set_fail_loose_commit_action_for_test(index: Option<usize>) {
2462 FAIL_LOOSE_COMMIT_ACTION.with(|cell| cell.set(index));
2463}
2464
2465#[cfg(test)]
2466fn maybe_fail_loose_commit_action(index: usize) -> Result<()> {
2467 let should_fail = FAIL_LOOSE_COMMIT_ACTION.with(|cell| cell.get() == Some(index));
2468 if should_fail {
2469 FAIL_LOOSE_COMMIT_ACTION.with(|cell| cell.set(None));
2470 return Err(GitError::Io(format!(
2471 "injected loose ref transaction failure at action {index}"
2472 )));
2473 }
2474 Ok(())
2475}
2476
2477#[cfg(not(test))]
2478fn maybe_fail_loose_commit_action(_index: usize) -> Result<()> {
2479 Ok(())
2480}
2481
2482fn restore_file_atomically(path: &Path, bytes: &[u8]) -> Result<()> {
2485 write_locked(path, bytes)
2486}
2487
2488#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2489pub struct FullRefName<'a> {
2490 name: &'a str,
2491}
2492
2493impl<'a> FullRefName<'a> {
2494 pub fn new(name: &'a str) -> Result<Self> {
2495 validate_ref_name(name)?;
2496 Ok(Self { name })
2497 }
2498
2499 pub fn as_str(&self) -> &str {
2500 self.name
2501 }
2502
2503 pub fn into_str(self) -> &'a str {
2504 self.name
2505 }
2506
2507 pub fn to_owned(&self) -> FullRefNameBuf {
2508 FullRefNameBuf {
2509 name: self.name.to_string(),
2510 }
2511 }
2512
2513 pub fn as_branch(&self) -> Result<BranchRefName<'a>> {
2514 BranchRefName::from_full_ref(*self)
2515 }
2516
2517 pub fn as_tag(&self) -> Result<TagRefName<'a>> {
2518 TagRefName::from_full_ref(*self)
2519 }
2520
2521 pub fn as_remote(&self) -> Result<RemoteRefName<'a>> {
2522 RemoteRefName::from_full_ref(*self)
2523 }
2524}
2525
2526impl AsRef<str> for FullRefName<'_> {
2527 fn as_ref(&self) -> &str {
2528 self.as_str()
2529 }
2530}
2531
2532impl fmt::Display for FullRefName<'_> {
2533 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2534 f.write_str(self.as_str())
2535 }
2536}
2537
2538#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2539pub struct FullRefNameBuf {
2540 name: String,
2541}
2542
2543impl FullRefNameBuf {
2544 pub fn new(name: impl Into<String>) -> Result<Self> {
2545 let name = name.into();
2546 validate_ref_name(&name)?;
2547 Ok(Self { name })
2548 }
2549
2550 pub fn as_ref_name(&self) -> FullRefName<'_> {
2551 FullRefName { name: &self.name }
2552 }
2553
2554 pub fn as_str(&self) -> &str {
2555 &self.name
2556 }
2557
2558 pub fn into_string(self) -> String {
2559 self.name
2560 }
2561
2562 pub fn as_branch(&self) -> Result<BranchRefName<'_>> {
2563 self.as_ref_name().as_branch()
2564 }
2565
2566 pub fn as_tag(&self) -> Result<TagRefName<'_>> {
2567 self.as_ref_name().as_tag()
2568 }
2569
2570 pub fn as_remote(&self) -> Result<RemoteRefName<'_>> {
2571 self.as_ref_name().as_remote()
2572 }
2573}
2574
2575impl AsRef<str> for FullRefNameBuf {
2576 fn as_ref(&self) -> &str {
2577 self.as_str()
2578 }
2579}
2580
2581impl Borrow<str> for FullRefNameBuf {
2582 fn borrow(&self) -> &str {
2583 self.as_str()
2584 }
2585}
2586
2587impl Deref for FullRefNameBuf {
2588 type Target = str;
2589
2590 fn deref(&self) -> &Self::Target {
2591 self.as_str()
2592 }
2593}
2594
2595impl fmt::Display for FullRefNameBuf {
2596 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2597 f.write_str(self.as_str())
2598 }
2599}
2600
2601#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2602pub struct BranchRefName<'a> {
2603 name: &'a str,
2604}
2605
2606impl<'a> BranchRefName<'a> {
2607 pub const PREFIX: &'static str = "refs/heads/";
2608
2609 pub fn from_full(name: &'a str) -> Result<Self> {
2610 let full = FullRefName::new(name)?;
2611 Self::from_full_ref(full)
2612 }
2613
2614 pub fn from_full_ref(name: FullRefName<'a>) -> Result<Self> {
2615 validate_namespaced_ref(name.as_str(), Self::PREFIX, "branch")?;
2616 Ok(Self {
2617 name: name.into_str(),
2618 })
2619 }
2620
2621 pub fn as_full_ref_name(&self) -> FullRefName<'a> {
2622 FullRefName { name: self.name }
2623 }
2624
2625 pub fn as_str(&self) -> &str {
2626 self.name
2627 }
2628
2629 pub fn branch_name(&self) -> &str {
2630 self.short_name()
2631 }
2632
2633 pub fn short_name(&self) -> &str {
2634 &self.name[Self::PREFIX.len()..]
2635 }
2636
2637 pub fn into_str(self) -> &'a str {
2638 self.name
2639 }
2640
2641 pub fn to_owned(&self) -> BranchRefNameBuf {
2642 BranchRefNameBuf {
2643 name: self.name.to_string(),
2644 }
2645 }
2646}
2647
2648impl AsRef<str> for BranchRefName<'_> {
2649 fn as_ref(&self) -> &str {
2650 self.as_str()
2651 }
2652}
2653
2654impl fmt::Display for BranchRefName<'_> {
2655 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2656 f.write_str(self.as_str())
2657 }
2658}
2659
2660impl<'a> From<BranchRefName<'a>> for FullRefName<'a> {
2661 fn from(name: BranchRefName<'a>) -> Self {
2662 name.as_full_ref_name()
2663 }
2664}
2665
2666#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2667pub struct BranchRefNameBuf {
2668 name: String,
2669}
2670
2671impl BranchRefNameBuf {
2672 pub fn from_branch_name(branch: &str) -> Result<Self> {
2673 validate_short_ref_name("branch", branch)?;
2674 let name = format!("{}{}", BranchRefName::PREFIX, branch);
2675 Self::from_full(name)
2676 }
2677
2678 pub fn from_full(name: impl Into<String>) -> Result<Self> {
2679 let name = name.into();
2680 BranchRefName::from_full(&name)?;
2681 Ok(Self { name })
2682 }
2683
2684 pub fn as_ref_name(&self) -> BranchRefName<'_> {
2685 BranchRefName { name: &self.name }
2686 }
2687
2688 pub fn as_full_ref_name(&self) -> FullRefName<'_> {
2689 FullRefName { name: &self.name }
2690 }
2691
2692 pub fn as_str(&self) -> &str {
2693 &self.name
2694 }
2695
2696 pub fn branch_name(&self) -> &str {
2697 self.short_name()
2698 }
2699
2700 pub fn short_name(&self) -> &str {
2701 &self.name[BranchRefName::PREFIX.len()..]
2702 }
2703
2704 pub fn into_string(self) -> String {
2705 self.name
2706 }
2707}
2708
2709impl AsRef<str> for BranchRefNameBuf {
2710 fn as_ref(&self) -> &str {
2711 self.as_str()
2712 }
2713}
2714
2715impl Borrow<str> for BranchRefNameBuf {
2716 fn borrow(&self) -> &str {
2717 self.as_str()
2718 }
2719}
2720
2721impl Deref for BranchRefNameBuf {
2722 type Target = str;
2723
2724 fn deref(&self) -> &Self::Target {
2725 self.as_str()
2726 }
2727}
2728
2729impl fmt::Display for BranchRefNameBuf {
2730 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2731 f.write_str(self.as_str())
2732 }
2733}
2734
2735impl From<BranchRefNameBuf> for FullRefNameBuf {
2736 fn from(name: BranchRefNameBuf) -> Self {
2737 Self { name: name.name }
2738 }
2739}
2740
2741#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2742pub struct TagRefName<'a> {
2743 name: &'a str,
2744}
2745
2746impl<'a> TagRefName<'a> {
2747 pub const PREFIX: &'static str = "refs/tags/";
2748
2749 pub fn from_full(name: &'a str) -> Result<Self> {
2750 let full = FullRefName::new(name)?;
2751 Self::from_full_ref(full)
2752 }
2753
2754 pub fn from_full_ref(name: FullRefName<'a>) -> Result<Self> {
2755 validate_namespaced_ref(name.as_str(), Self::PREFIX, "tag")?;
2756 Ok(Self {
2757 name: name.into_str(),
2758 })
2759 }
2760
2761 pub fn as_full_ref_name(&self) -> FullRefName<'a> {
2762 FullRefName { name: self.name }
2763 }
2764
2765 pub fn as_str(&self) -> &str {
2766 self.name
2767 }
2768
2769 pub fn tag_name(&self) -> &str {
2770 self.short_name()
2771 }
2772
2773 pub fn short_name(&self) -> &str {
2774 &self.name[Self::PREFIX.len()..]
2775 }
2776
2777 pub fn into_str(self) -> &'a str {
2778 self.name
2779 }
2780
2781 pub fn to_owned(&self) -> TagRefNameBuf {
2782 TagRefNameBuf {
2783 name: self.name.to_string(),
2784 }
2785 }
2786}
2787
2788impl AsRef<str> for TagRefName<'_> {
2789 fn as_ref(&self) -> &str {
2790 self.as_str()
2791 }
2792}
2793
2794impl fmt::Display for TagRefName<'_> {
2795 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2796 f.write_str(self.as_str())
2797 }
2798}
2799
2800impl<'a> From<TagRefName<'a>> for FullRefName<'a> {
2801 fn from(name: TagRefName<'a>) -> Self {
2802 name.as_full_ref_name()
2803 }
2804}
2805
2806#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2807pub struct TagRefNameBuf {
2808 name: String,
2809}
2810
2811impl TagRefNameBuf {
2812 pub fn from_tag_name(tag: &str) -> Result<Self> {
2813 if tag.starts_with('-') || tag == "HEAD" {
2816 return Err(GitError::InvalidPath(format!("invalid tag name {tag}")));
2817 }
2818 Self::from_tag_name_unrestricted(tag)
2819 }
2820
2821 pub fn from_tag_name_unrestricted(tag: &str) -> Result<Self> {
2826 let name = format!("{}{}", TagRefName::PREFIX, tag);
2827 check_refname_format(&name, false)?;
2828 Ok(Self { name })
2829 }
2830
2831 pub fn from_full(name: impl Into<String>) -> Result<Self> {
2832 let name = name.into();
2833 TagRefName::from_full(&name)?;
2834 Ok(Self { name })
2835 }
2836
2837 pub fn as_ref_name(&self) -> TagRefName<'_> {
2838 TagRefName { name: &self.name }
2839 }
2840
2841 pub fn as_full_ref_name(&self) -> FullRefName<'_> {
2842 FullRefName { name: &self.name }
2843 }
2844
2845 pub fn as_str(&self) -> &str {
2846 &self.name
2847 }
2848
2849 pub fn tag_name(&self) -> &str {
2850 self.short_name()
2851 }
2852
2853 pub fn short_name(&self) -> &str {
2854 &self.name[TagRefName::PREFIX.len()..]
2855 }
2856
2857 pub fn into_string(self) -> String {
2858 self.name
2859 }
2860}
2861
2862impl AsRef<str> for TagRefNameBuf {
2863 fn as_ref(&self) -> &str {
2864 self.as_str()
2865 }
2866}
2867
2868impl Borrow<str> for TagRefNameBuf {
2869 fn borrow(&self) -> &str {
2870 self.as_str()
2871 }
2872}
2873
2874impl Deref for TagRefNameBuf {
2875 type Target = str;
2876
2877 fn deref(&self) -> &Self::Target {
2878 self.as_str()
2879 }
2880}
2881
2882impl fmt::Display for TagRefNameBuf {
2883 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2884 f.write_str(self.as_str())
2885 }
2886}
2887
2888impl From<TagRefNameBuf> for FullRefNameBuf {
2889 fn from(name: TagRefNameBuf) -> Self {
2890 Self { name: name.name }
2891 }
2892}
2893
2894#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2895pub struct RemoteRefName<'a> {
2896 name: &'a str,
2897}
2898
2899impl<'a> RemoteRefName<'a> {
2900 pub const PREFIX: &'static str = "refs/remotes/";
2901
2902 pub fn from_full(name: &'a str) -> Result<Self> {
2903 let full = FullRefName::new(name)?;
2904 Self::from_full_ref(full)
2905 }
2906
2907 pub fn from_full_ref(name: FullRefName<'a>) -> Result<Self> {
2908 validate_namespaced_ref(name.as_str(), Self::PREFIX, "remote")?;
2909 Ok(Self {
2910 name: name.into_str(),
2911 })
2912 }
2913
2914 pub fn as_full_ref_name(&self) -> FullRefName<'a> {
2915 FullRefName { name: self.name }
2916 }
2917
2918 pub fn as_str(&self) -> &str {
2919 self.name
2920 }
2921
2922 pub fn short_name(&self) -> &str {
2923 &self.name[Self::PREFIX.len()..]
2924 }
2925
2926 pub fn remote_name(&self) -> &str {
2927 match self.short_name().split_once('/') {
2928 Some((remote, _branch)) => remote,
2929 None => self.short_name(),
2930 }
2931 }
2932
2933 pub fn remote_branch(&self) -> Option<&str> {
2934 self.short_name()
2935 .split_once('/')
2936 .map(|(_remote, branch)| branch)
2937 }
2938
2939 pub fn into_str(self) -> &'a str {
2940 self.name
2941 }
2942
2943 pub fn to_owned(&self) -> RemoteRefNameBuf {
2944 RemoteRefNameBuf {
2945 name: self.name.to_string(),
2946 }
2947 }
2948}
2949
2950impl AsRef<str> for RemoteRefName<'_> {
2951 fn as_ref(&self) -> &str {
2952 self.as_str()
2953 }
2954}
2955
2956impl fmt::Display for RemoteRefName<'_> {
2957 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2958 f.write_str(self.as_str())
2959 }
2960}
2961
2962impl<'a> From<RemoteRefName<'a>> for FullRefName<'a> {
2963 fn from(name: RemoteRefName<'a>) -> Self {
2964 name.as_full_ref_name()
2965 }
2966}
2967
2968#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2969pub struct RemoteRefNameBuf {
2970 name: String,
2971}
2972
2973impl RemoteRefNameBuf {
2974 pub fn from_short_name(name: &str) -> Result<Self> {
2975 validate_short_ref_name("remote ref", name)?;
2976 let name = format!("{}{}", RemoteRefName::PREFIX, name);
2977 Self::from_full(name)
2978 }
2979
2980 pub fn from_remote_branch(remote: &str, branch: &str) -> Result<Self> {
2981 validate_remote_name(remote)?;
2982 validate_short_ref_name("remote branch", branch)?;
2983 let name = format!("{}{}/{}", RemoteRefName::PREFIX, remote, branch);
2984 Self::from_full(name)
2985 }
2986
2987 pub fn from_full(name: impl Into<String>) -> Result<Self> {
2988 let name = name.into();
2989 RemoteRefName::from_full(&name)?;
2990 Ok(Self { name })
2991 }
2992
2993 pub fn as_ref_name(&self) -> RemoteRefName<'_> {
2994 RemoteRefName { name: &self.name }
2995 }
2996
2997 pub fn as_full_ref_name(&self) -> FullRefName<'_> {
2998 FullRefName { name: &self.name }
2999 }
3000
3001 pub fn as_str(&self) -> &str {
3002 &self.name
3003 }
3004
3005 pub fn short_name(&self) -> &str {
3006 &self.name[RemoteRefName::PREFIX.len()..]
3007 }
3008
3009 pub fn remote_name(&self) -> &str {
3010 match self.short_name().split_once('/') {
3011 Some((remote, _branch)) => remote,
3012 None => self.short_name(),
3013 }
3014 }
3015
3016 pub fn remote_branch(&self) -> Option<&str> {
3017 self.short_name()
3018 .split_once('/')
3019 .map(|(_remote, branch)| branch)
3020 }
3021
3022 pub fn into_string(self) -> String {
3023 self.name
3024 }
3025}
3026
3027impl AsRef<str> for RemoteRefNameBuf {
3028 fn as_ref(&self) -> &str {
3029 self.as_str()
3030 }
3031}
3032
3033impl Borrow<str> for RemoteRefNameBuf {
3034 fn borrow(&self) -> &str {
3035 self.as_str()
3036 }
3037}
3038
3039impl Deref for RemoteRefNameBuf {
3040 type Target = str;
3041
3042 fn deref(&self) -> &Self::Target {
3043 self.as_str()
3044 }
3045}
3046
3047impl fmt::Display for RemoteRefNameBuf {
3048 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3049 f.write_str(self.as_str())
3050 }
3051}
3052
3053impl From<RemoteRefNameBuf> for FullRefNameBuf {
3054 fn from(name: RemoteRefNameBuf) -> Self {
3055 Self { name: name.name }
3056 }
3057}
3058
3059pub fn branch_ref_name(branch: &str) -> Result<String> {
3060 BranchRefNameBuf::from_branch_name(branch).map(BranchRefNameBuf::into_string)
3061}
3062
3063pub fn tag_ref_name(tag: &str) -> Result<String> {
3064 TagRefNameBuf::from_tag_name(tag).map(TagRefNameBuf::into_string)
3065}
3066
3067fn write_locked(path: &Path, bytes: &[u8]) -> Result<()> {
3068 let lock_path = lock_path_for(path)?;
3069 {
3070 let mut file = fs::OpenOptions::new()
3071 .write(true)
3072 .create_new(true)
3073 .open(&lock_path)?;
3074 file.write_all(bytes)?;
3075 file.sync_all()?;
3076 }
3077 match fs::rename(&lock_path, path) {
3078 Ok(()) => Ok(()),
3079 Err(err) => {
3080 let _ = fs::remove_file(lock_path);
3081 Err(GitError::Io(err.to_string()))
3082 }
3083 }
3084}
3085
3086fn lock_path_for(path: &Path) -> Result<PathBuf> {
3087 let file_name = path
3088 .file_name()
3089 .ok_or_else(|| GitError::InvalidPath("ref path has no filename".into()))?;
3090 let mut lock_name = file_name.to_os_string();
3091 lock_name.push(".lock");
3092 Ok(path.with_file_name(lock_name))
3093}
3094
3095pub fn check_refname_format(name: &str, allow_onelevel: bool) -> Result<()> {
3097 if name.is_empty()
3098 || name == "@"
3099 || name.starts_with('/')
3100 || name.ends_with('/')
3101 || name.ends_with('.')
3102 || name.contains("..")
3103 || name.contains("//")
3104 || name.contains("@{")
3105 || (!allow_onelevel && !name.contains('/'))
3106 {
3107 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
3108 }
3109 for component in name.split('/') {
3110 if component.is_empty() || component.starts_with('.') || component.ends_with(".lock") {
3111 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
3112 }
3113 for (idx, byte) in component.bytes().enumerate() {
3114 if byte <= b' '
3115 || byte == 0x7f
3116 || matches!(byte, b'~' | b'^' | b':' | b'?' | b'*' | b'[' | b'\\')
3117 {
3118 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
3119 }
3120 if byte == b'.' && component.as_bytes().get(idx + 1) == Some(&b'.') {
3121 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
3122 }
3123 if byte == b'@' && component.as_bytes().get(idx + 1) == Some(&b'{') {
3124 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
3125 }
3126 }
3127 }
3128 Ok(())
3129}
3130
3131pub fn validate_symref_name(name: &str) -> Result<()> {
3133 if name == "HEAD" {
3134 return Ok(());
3135 }
3136 check_refname_format(name, true)
3137}
3138
3139pub fn validate_symref_target(name: &str) -> Result<()> {
3141 check_refname_format(name, true)
3142}
3143
3144fn prune_empty_dirs_up_to(start: &Path, boundary: &Path) {
3149 let mut dir = start.to_path_buf();
3150 while dir.starts_with(boundary) && dir != *boundary {
3151 if fs::remove_dir(&dir).is_err() {
3152 break;
3153 }
3154 dir = match dir.parent() {
3155 Some(parent) => parent.to_path_buf(),
3156 None => break,
3157 };
3158 }
3159}
3160
3161pub fn resolve_ref_peeled(store: &FileRefStore, name: &str) -> Result<Option<ObjectId>> {
3162 let mut current = name.to_string();
3163 for _ in 0..16 {
3164 match store.read_ref(¤t)? {
3165 Some(RefTarget::Direct(oid)) => return Ok(Some(oid)),
3166 Some(RefTarget::Symbolic(next)) => current = next,
3167 None => return Ok(None),
3168 }
3169 }
3170 Ok(None)
3171}
3172
3173fn validate_ref_name_for_read(name: &str) -> Result<()> {
3174 if validate_ref_name(name).is_ok() {
3175 return Ok(());
3176 }
3177 if is_root_ref_syntax(name) {
3178 return Ok(());
3179 }
3180 validate_symref_name(name)
3181}
3182
3183fn validate_ref_name_for_update(name: &str) -> Result<()> {
3184 if validate_ref_name(name).is_ok() {
3185 return Ok(());
3186 }
3187 if is_root_ref_syntax(name) {
3188 return Ok(());
3189 }
3190 validate_symref_name(name)
3191}
3192
3193fn is_root_ref_syntax(name: &str) -> bool {
3198 !name.is_empty()
3199 && name
3200 .bytes()
3201 .all(|b| b.is_ascii_uppercase() || b == b'-' || b == b'_')
3202}
3203
3204pub fn validate_ref_name(name: &str) -> Result<()> {
3205 if name == "HEAD" {
3206 return Ok(());
3207 }
3208 let path = Path::new(name);
3209 if !name.starts_with("refs/")
3210 || name.contains("..")
3211 || name.contains('\\')
3212 || name.ends_with('/')
3213 || name.ends_with(".lock")
3214 || path.is_absolute()
3215 || path.components().any(|component| {
3216 matches!(
3217 component,
3218 std::path::Component::ParentDir | std::path::Component::Prefix(_)
3219 )
3220 })
3221 {
3222 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
3223 }
3224 Ok(())
3225}
3226
3227fn ref_directory_conflict_error(new_ref: &str, existing_ref: &str) -> GitError {
3228 GitError::Transaction(format!(
3229 "cannot lock ref '{new_ref}': '{existing_ref}' exists; cannot create '{new_ref}'"
3230 ))
3231}
3232
3233fn parent_to_ref_name(base: &Path, parent: &Path) -> String {
3234 match parent.strip_prefix(base) {
3235 Ok(suffix) => suffix.to_string_lossy().replace('\\', "/"),
3236 Err(_) => parent.to_string_lossy().into_owned(),
3237 }
3238}
3239
3240fn validate_namespaced_ref(name: &str, prefix: &str, kind: &str) -> Result<()> {
3241 validate_ref_name(name)?;
3242 if name
3243 .strip_prefix(prefix)
3244 .is_none_or(|short_name| short_name.is_empty())
3245 {
3246 return Err(GitError::InvalidPath(format!(
3247 "invalid {kind} ref name {name}"
3248 )));
3249 }
3250 Ok(())
3251}
3252
3253fn validate_short_ref_name(kind: &str, name: &str) -> Result<()> {
3254 if name.is_empty()
3255 || name.starts_with('-')
3256 || name.starts_with('/')
3257 || name.ends_with('/')
3258 || name.contains(' ')
3259 || name.contains('\\')
3260 {
3261 return Err(GitError::InvalidPath(format!("invalid {kind} name {name}")));
3262 }
3263 Ok(())
3264}
3265
3266fn validate_remote_name(remote: &str) -> Result<()> {
3267 validate_short_ref_name("remote", remote)?;
3268 if remote.contains('/') {
3269 return Err(GitError::InvalidPath(format!(
3270 "invalid remote name {remote}"
3271 )));
3272 }
3273 Ok(())
3274}
3275
3276fn prepare_bundle_ref_updates<F>(
3277 refs: &[BundleRefUpdate],
3278 reflog: Option<&BundleRefUpdateReflog>,
3279 mut read_ref: F,
3280) -> Result<(Vec<RefUpdate>, Vec<AppliedBundleRefUpdate>)>
3281where
3282 F: FnMut(&str, &ObjectId) -> Result<Option<RefTarget>>,
3283{
3284 let mut seen = BTreeSet::new();
3285 let mut updates = Vec::with_capacity(refs.len());
3286 let mut applied = Vec::with_capacity(refs.len());
3287 for bundle_ref in refs {
3288 validate_ref_name(&bundle_ref.name)?;
3289 if !seen.insert(bundle_ref.name.clone()) {
3290 return Err(GitError::Transaction(format!(
3291 "duplicate bundle ref {}",
3292 bundle_ref.name
3293 )));
3294 }
3295 let old_oid = match read_ref(&bundle_ref.name, &bundle_ref.oid)? {
3296 Some(RefTarget::Direct(oid)) => Some(oid),
3297 Some(RefTarget::Symbolic(target)) => {
3298 return Err(GitError::Transaction(format!(
3299 "bundle ref {} would overwrite symbolic ref {target}",
3300 bundle_ref.name
3301 )));
3302 }
3303 None => None,
3304 };
3305 let reflog = match reflog {
3306 Some(reflog) => Some(ReflogEntry {
3307 old_oid: match &old_oid {
3308 Some(oid) => *oid,
3309 None => null_oid(bundle_ref.oid.format())?,
3310 },
3311 new_oid: bundle_ref.oid,
3312 committer: reflog.committer.clone(),
3313 message: reflog.message.clone(),
3314 }),
3315 None => None,
3316 };
3317 updates.push(RefUpdate {
3318 name: bundle_ref.name.clone(),
3319 expected: old_oid.map(RefTarget::Direct),
3320 new: RefTarget::Direct(bundle_ref.oid),
3321 reflog,
3322 });
3323 applied.push(AppliedBundleRefUpdate {
3324 name: bundle_ref.name.clone(),
3325 old_oid,
3326 new_oid: bundle_ref.oid,
3327 });
3328 }
3329 Ok((updates, applied))
3330}
3331
3332fn null_oid(format: ObjectFormat) -> Result<ObjectId> {
3333 Ok(ObjectId::null(format))
3334}
3335
3336#[cfg(test)]
3337mod tests {
3338 use super::*;
3339 use std::sync::atomic::{AtomicU64, Ordering};
3340
3341 static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
3342
3343 #[test]
3344 fn loose_ref_round_trips_direct() {
3345 let oid = "ce013625030ba8dba906f756967f9e9ca394464a";
3346 let reference = parse_loose_ref(ObjectFormat::Sha1, "refs/heads/main", oid.as_bytes())
3347 .expect("test operation should succeed");
3348 assert_eq!(write_loose_ref(&reference), format!("{oid}\n").into_bytes());
3349 }
3350
3351 #[test]
3352 fn symref_names_allow_onelevel_pseudo_refs() {
3353 for name in ["NOTHEAD", "FOO", "ORIG_HEAD", "TEST_SYMREF"] {
3354 validate_symref_name(name).expect("symref name should be valid");
3355 }
3356 assert!(validate_ref_name("NOTHEAD").is_err());
3357 assert!(validate_symref_target("refs/heads/foo").is_ok());
3358 assert!(validate_symref_target("ORIG_HEAD").is_ok());
3359 assert!(validate_symref_target("foo..bar").is_err());
3360 }
3361
3362 #[test]
3363 fn resolve_ref_peeled_follows_symref_chains() {
3364 let git_dir = temp_git_dir();
3365 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
3366 let oid = ObjectId::from_hex(
3367 ObjectFormat::Sha1,
3368 "ce013625030ba8dba906f756967f9e9ca394464a",
3369 )
3370 .expect("test operation should succeed");
3371 let mut tx = store.transaction();
3372 tx.update(RefUpdate {
3373 name: "refs/heads/target".into(),
3374 expected: None,
3375 new: RefTarget::Direct(oid),
3376 reflog: None,
3377 });
3378 tx.commit().expect("seed target ref");
3379 let mut tx = store.transaction();
3380 tx.update(RefUpdate {
3381 name: "refs/heads/alias".into(),
3382 expected: None,
3383 new: RefTarget::Symbolic("refs/heads/target".into()),
3384 reflog: None,
3385 });
3386 tx.commit().expect("seed alias ref");
3387 let mut tx = store.transaction();
3388 tx.update(RefUpdate {
3389 name: "ORIG_HEAD".into(),
3390 expected: None,
3391 new: RefTarget::Symbolic("refs/heads/alias".into()),
3392 reflog: None,
3393 });
3394 tx.commit().expect("seed ORIG_HEAD symref");
3395 assert_eq!(
3396 resolve_ref_peeled(&store, "ORIG_HEAD").expect("resolve ORIG_HEAD"),
3397 Some(oid)
3398 );
3399 let _ = fs::remove_dir_all(git_dir);
3400 }
3401
3402 #[test]
3403 fn symref_directory_conflict_is_reported_gracefully() {
3404 let git_dir = temp_git_dir();
3405 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
3406 let oid = ObjectId::from_hex(
3407 ObjectFormat::Sha1,
3408 "ce013625030ba8dba906f756967f9e9ca394464a",
3409 )
3410 .expect("test operation should succeed");
3411 let mut tx = store.transaction();
3412 tx.update(RefUpdate {
3413 name: "refs/heads/df".into(),
3414 expected: None,
3415 new: RefTarget::Direct(oid),
3416 reflog: None,
3417 });
3418 tx.commit().expect("seed branch ref");
3419
3420 let mut tx = store.transaction();
3421 tx.update(RefUpdate {
3422 name: "refs/heads/df/conflict".into(),
3423 expected: None,
3424 new: RefTarget::Symbolic("refs/heads/df".into()),
3425 reflog: None,
3426 });
3427 let err = tx.commit().expect_err("child ref should conflict");
3428 assert!(
3429 matches!(err, GitError::Transaction(message) if message.contains(
3430 "cannot lock ref 'refs/heads/df/conflict'"
3431 ) && message.contains("refs/heads/df"))
3432 );
3433 let _ = fs::remove_dir_all(git_dir);
3434 }
3435
3436 #[test]
3437 fn transaction_checks_expected_value() {
3438 let oid = ObjectId::from_hex(
3439 ObjectFormat::Sha1,
3440 "ce013625030ba8dba906f756967f9e9ca394464a",
3441 )
3442 .expect("test operation should succeed");
3443 let mut store = RefStore::new();
3444 let mut tx = store.transaction();
3445 tx.update(RefUpdate {
3446 name: "refs/heads/main".into(),
3447 expected: None,
3448 new: RefTarget::Direct(oid),
3449 reflog: None,
3450 });
3451 tx.commit().expect("test operation should succeed");
3452 assert_eq!(store.get("refs/heads/main"), Some(&RefTarget::Direct(oid)));
3453 }
3454
3455 #[test]
3456 fn packed_refs_parse_peeled_refs() {
3457 let packed = b"# pack-refs with: peeled fully-peeled sorted \n\
3458ce013625030ba8dba906f756967f9e9ca394464a refs/tags/v1\n\
3459^e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\n";
3460 let refs =
3461 parse_packed_refs(ObjectFormat::Sha1, packed).expect("test operation should succeed");
3462 assert_eq!(refs.len(), 1);
3463 assert_eq!(refs[0].reference.name, "refs/tags/v1");
3464 assert_eq!(
3465 refs[0]
3466 .peeled
3467 .as_ref()
3468 .expect("test operation should succeed")
3469 .to_hex(),
3470 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"
3471 );
3472 }
3473
3474 #[test]
3475 fn packed_refs_write_sorted_with_peeled_refs() {
3476 let head_oid = ObjectId::from_hex(
3477 ObjectFormat::Sha1,
3478 "ce013625030ba8dba906f756967f9e9ca394464a",
3479 )
3480 .expect("test operation should succeed");
3481 let tag_oid = ObjectId::from_hex(
3482 ObjectFormat::Sha1,
3483 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
3484 )
3485 .expect("test operation should succeed");
3486 let peeled_oid = ObjectId::from_hex(
3487 ObjectFormat::Sha1,
3488 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
3489 )
3490 .expect("test operation should succeed");
3491 let refs = vec![
3492 PackedRef {
3493 reference: Ref {
3494 name: "refs/tags/v1".into(),
3495 target: RefTarget::Direct(tag_oid),
3496 },
3497 peeled: Some(peeled_oid),
3498 },
3499 PackedRef {
3500 reference: Ref {
3501 name: "refs/heads/main".into(),
3502 target: RefTarget::Direct(head_oid),
3503 },
3504 peeled: None,
3505 },
3506 ];
3507 let bytes = write_packed_refs(&refs).expect("test operation should succeed");
3508 let expected = format!(
3509 "# pack-refs with: peeled fully-peeled sorted \n\
3510{head_oid} refs/heads/main\n\
3511{tag_oid} refs/tags/v1\n\
3512^{peeled_oid}\n"
3513 );
3514 assert_eq!(
3515 String::from_utf8(bytes.clone()).expect("test operation should succeed"),
3516 expected
3517 );
3518 let parsed =
3519 parse_packed_refs(ObjectFormat::Sha1, &bytes).expect("test operation should succeed");
3520 assert_eq!(parsed[0], refs[1]);
3521 assert_eq!(parsed[1], refs[0]);
3522 }
3523
3524 #[test]
3525 fn full_ref_name_validates_and_round_trips_owned() {
3526 let full = FullRefName::new("refs/heads/main").expect("valid full branch ref");
3527 assert_eq!(full.as_str(), "refs/heads/main");
3528 assert_eq!(full.to_string(), "refs/heads/main");
3529 assert_eq!(full.to_owned().into_string(), "refs/heads/main");
3530
3531 let head = FullRefNameBuf::new("HEAD").expect("valid HEAD ref");
3532 assert_eq!(head.as_ref_name().into_str(), "HEAD");
3533
3534 assert!(FullRefName::new("main").is_err());
3535 assert!(FullRefNameBuf::new("refs/heads/bad.lock").is_err());
3536 }
3537
3538 #[test]
3539 fn branch_ref_name_helpers_validate_short_and_full_names() {
3540 let branch =
3541 BranchRefNameBuf::from_branch_name("feature/topic").expect("valid branch short name");
3542 assert_eq!(branch.as_str(), "refs/heads/feature/topic");
3543 assert_eq!(branch.branch_name(), "feature/topic");
3544 assert_eq!(
3545 branch.as_full_ref_name().as_str(),
3546 "refs/heads/feature/topic"
3547 );
3548 assert_eq!(
3549 branch_ref_name("feature/topic").expect("valid branch short name"),
3550 branch.as_str()
3551 );
3552
3553 let borrowed = BranchRefName::from_full("refs/heads/main").expect("valid full branch ref");
3554 assert_eq!(borrowed.branch_name(), "main");
3555 assert_eq!(borrowed.to_owned().into_string(), "refs/heads/main");
3556 assert_eq!(
3557 FullRefName::new("refs/heads/main")
3558 .expect("valid full branch ref")
3559 .as_branch()
3560 .expect("full ref is a branch")
3561 .branch_name(),
3562 "main"
3563 );
3564
3565 assert!(BranchRefName::from_full("refs/tags/main").is_err());
3566 assert!(BranchRefName::from_full("refs/heads").is_err());
3567 assert!(BranchRefNameBuf::from_branch_name("-bad").is_err());
3568 }
3569
3570 #[test]
3571 fn tag_ref_name_helpers_validate_short_and_full_names() {
3572 let tag = TagRefNameBuf::from_tag_name("v1.0").expect("valid tag short name");
3573 assert_eq!(tag.as_str(), "refs/tags/v1.0");
3574 assert_eq!(tag.tag_name(), "v1.0");
3575 assert_eq!(tag.as_full_ref_name().as_str(), "refs/tags/v1.0");
3576 assert_eq!(
3577 tag_ref_name("v1.0").expect("valid tag short name"),
3578 tag.as_str()
3579 );
3580
3581 let borrowed = TagRefName::from_full("refs/tags/release/1").expect("valid full tag ref");
3582 assert_eq!(borrowed.tag_name(), "release/1");
3583 assert_eq!(borrowed.to_owned().into_string(), "refs/tags/release/1");
3584 assert_eq!(
3585 FullRefName::new("refs/tags/release/1")
3586 .expect("valid full tag ref")
3587 .as_tag()
3588 .expect("full ref is a tag")
3589 .tag_name(),
3590 "release/1"
3591 );
3592
3593 assert!(TagRefName::from_full("refs/heads/v1.0").is_err());
3594 assert!(TagRefName::from_full("refs/tags").is_err());
3595 assert!(TagRefNameBuf::from_tag_name("bad tag").is_err());
3596 }
3597
3598 #[test]
3599 fn remote_ref_name_helpers_validate_namespace_and_components() {
3600 let remote = RemoteRefNameBuf::from_remote_branch("origin", "feature/topic")
3601 .expect("valid remote branch ref");
3602 assert_eq!(remote.as_str(), "refs/remotes/origin/feature/topic");
3603 assert_eq!(remote.short_name(), "origin/feature/topic");
3604 assert_eq!(remote.remote_name(), "origin");
3605 assert_eq!(remote.remote_branch(), Some("feature/topic"));
3606 assert_eq!(
3607 remote.as_full_ref_name().as_str(),
3608 "refs/remotes/origin/feature/topic"
3609 );
3610
3611 let head =
3612 RemoteRefName::from_full("refs/remotes/origin/HEAD").expect("valid remote HEAD ref");
3613 assert_eq!(head.remote_name(), "origin");
3614 assert_eq!(head.remote_branch(), Some("HEAD"));
3615 assert_eq!(
3616 FullRefName::new("refs/remotes/upstream/main")
3617 .expect("valid full remote ref")
3618 .as_remote()
3619 .expect("full ref is remote-tracking")
3620 .remote_name(),
3621 "upstream"
3622 );
3623
3624 let short =
3625 RemoteRefNameBuf::from_short_name("origin/main").expect("valid remote short ref");
3626 assert_eq!(short.as_str(), "refs/remotes/origin/main");
3627
3628 assert!(RemoteRefName::from_full("refs/heads/origin/main").is_err());
3629 assert!(RemoteRefName::from_full("refs/remotes/").is_err());
3630 assert!(RemoteRefNameBuf::from_remote_branch("origin/fork", "main").is_err());
3631 }
3632
3633 #[test]
3634 fn file_ref_store_writes_ref_and_reflog() {
3635 let git_dir = temp_git_dir();
3636 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
3637 let oid = ObjectId::from_hex(
3638 ObjectFormat::Sha1,
3639 "ce013625030ba8dba906f756967f9e9ca394464a",
3640 )
3641 .expect("test operation should succeed");
3642 let mut tx = store.transaction();
3643 tx.update(RefUpdate {
3644 name: "refs/heads/main".into(),
3645 expected: None,
3646 new: RefTarget::Direct(oid),
3647 reflog: Some(ReflogEntry {
3648 old_oid: zero_oid(ObjectFormat::Sha1).expect("test operation should succeed"),
3649 new_oid: oid,
3650 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
3651 message: b"update by test".to_vec(),
3652 }),
3653 });
3654 tx.commit().expect("test operation should succeed");
3655 assert_eq!(
3656 store
3657 .read_ref("refs/heads/main")
3658 .expect("test operation should succeed"),
3659 Some(RefTarget::Direct(oid))
3660 );
3661 let log = store
3662 .read_reflog("refs/heads/main")
3663 .expect("test operation should succeed");
3664 assert_eq!(log.len(), 1);
3665 assert_eq!(log[0].message, b"update by test");
3666 fs::remove_dir_all(git_dir).expect("test operation should succeed");
3667 }
3668
3669 #[test]
3670 fn file_ref_store_applies_bundle_refs_with_reflog() {
3671 let git_dir = temp_git_dir();
3672 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
3673 let old_main = ObjectId::from_hex(
3674 ObjectFormat::Sha1,
3675 "ce013625030ba8dba906f756967f9e9ca394464a",
3676 )
3677 .expect("test operation should succeed");
3678 let new_main = ObjectId::from_hex(
3679 ObjectFormat::Sha1,
3680 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
3681 )
3682 .expect("test operation should succeed");
3683 let tag_oid = ObjectId::from_hex(
3684 ObjectFormat::Sha1,
3685 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
3686 )
3687 .expect("test operation should succeed");
3688 let mut tx = store.transaction();
3689 tx.update(RefUpdate {
3690 name: "refs/heads/main".into(),
3691 expected: None,
3692 new: RefTarget::Direct(old_main.clone()),
3693 reflog: None,
3694 });
3695 tx.commit().expect("test operation should succeed");
3696
3697 let applied = store
3698 .apply_bundle_ref_updates(
3699 &[
3700 BundleRefUpdate {
3701 name: "refs/heads/main".into(),
3702 oid: new_main.clone(),
3703 },
3704 BundleRefUpdate {
3705 name: "refs/tags/v1.0".into(),
3706 oid: tag_oid,
3707 },
3708 ],
3709 Some(BundleRefUpdateReflog {
3710 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
3711 message: b"bundle: import refs".to_vec(),
3712 }),
3713 )
3714 .expect("test operation should succeed");
3715
3716 assert_eq!(
3717 applied,
3718 vec![
3719 AppliedBundleRefUpdate {
3720 name: "refs/heads/main".into(),
3721 old_oid: Some(old_main.clone()),
3722 new_oid: new_main.clone(),
3723 },
3724 AppliedBundleRefUpdate {
3725 name: "refs/tags/v1.0".into(),
3726 old_oid: None,
3727 new_oid: tag_oid,
3728 }
3729 ]
3730 );
3731 assert_eq!(
3732 store
3733 .read_ref("refs/heads/main")
3734 .expect("test operation should succeed"),
3735 Some(RefTarget::Direct(new_main.clone()))
3736 );
3737 assert_eq!(
3738 store
3739 .read_ref("refs/tags/v1.0")
3740 .expect("test operation should succeed"),
3741 Some(RefTarget::Direct(tag_oid))
3742 );
3743 let main_log = store
3744 .read_reflog("refs/heads/main")
3745 .expect("test operation should succeed");
3746 assert_eq!(main_log.len(), 1);
3747 assert_eq!(main_log[0].old_oid, old_main);
3748 assert_eq!(main_log[0].new_oid, new_main);
3749 assert_eq!(main_log[0].message, b"bundle: import refs");
3750 let tag_log = store
3751 .read_reflog("refs/tags/v1.0")
3752 .expect("test operation should succeed");
3753 assert_eq!(tag_log.len(), 1);
3754 assert_eq!(
3755 tag_log[0].old_oid,
3756 zero_oid(ObjectFormat::Sha1).expect("test operation should succeed")
3757 );
3758 assert_eq!(tag_log[0].new_oid, tag_oid);
3759 fs::remove_dir_all(git_dir).expect("test operation should succeed");
3760 }
3761
3762 #[test]
3763 fn file_ref_store_rejects_bad_bundle_ref_before_writing() {
3764 let git_dir = temp_git_dir();
3765 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
3766 let oid = ObjectId::from_hex(
3767 ObjectFormat::Sha1,
3768 "ce013625030ba8dba906f756967f9e9ca394464a",
3769 )
3770 .expect("test operation should succeed");
3771
3772 let result = store.apply_bundle_ref_updates(
3773 &[
3774 BundleRefUpdate {
3775 name: "refs/heads/main".into(),
3776 oid,
3777 },
3778 BundleRefUpdate {
3779 name: "refs/heads/bad.lock".into(),
3780 oid,
3781 },
3782 ],
3783 None,
3784 );
3785
3786 assert!(result.is_err());
3787 assert_eq!(
3788 store
3789 .read_ref("refs/heads/main")
3790 .expect("test operation should succeed"),
3791 None
3792 );
3793 fs::remove_dir_all(git_dir).expect("test operation should succeed");
3794 }
3795
3796 #[test]
3797 fn file_ref_store_rejects_bundle_ref_over_symbolic_ref() {
3798 let git_dir = temp_git_dir();
3799 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
3800 let oid = ObjectId::from_hex(
3801 ObjectFormat::Sha1,
3802 "ce013625030ba8dba906f756967f9e9ca394464a",
3803 )
3804 .expect("test operation should succeed");
3805 let mut tx = store.transaction();
3806 tx.update(RefUpdate {
3807 name: "refs/heads/main".into(),
3808 expected: None,
3809 new: RefTarget::Symbolic("refs/heads/base".into()),
3810 reflog: None,
3811 });
3812 tx.commit().expect("test operation should succeed");
3813
3814 let result = store.apply_bundle_ref_updates(
3815 &[BundleRefUpdate {
3816 name: "refs/heads/main".into(),
3817 oid,
3818 }],
3819 None,
3820 );
3821
3822 assert!(result.is_err());
3823 assert_eq!(
3824 store
3825 .read_ref("refs/heads/main")
3826 .expect("test operation should succeed"),
3827 Some(RefTarget::Symbolic("refs/heads/base".into()))
3828 );
3829 fs::remove_dir_all(git_dir).expect("test operation should succeed");
3830 }
3831
3832 #[test]
3833 fn file_ref_store_expires_reflog_entries_by_timestamp() {
3834 let git_dir = temp_git_dir();
3835 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
3836 let first = ObjectId::from_hex(
3837 ObjectFormat::Sha1,
3838 "ce013625030ba8dba906f756967f9e9ca394464a",
3839 )
3840 .expect("test operation should succeed");
3841 let second = ObjectId::from_hex(
3842 ObjectFormat::Sha1,
3843 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
3844 )
3845 .expect("test operation should succeed");
3846 let mut tx = store.transaction();
3847 tx.update(RefUpdate {
3848 name: "refs/heads/main".into(),
3849 expected: None,
3850 new: RefTarget::Direct(first.clone()),
3851 reflog: Some(ReflogEntry {
3852 old_oid: zero_oid(ObjectFormat::Sha1).expect("test operation should succeed"),
3853 new_oid: first.clone(),
3854 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
3855 message: b"old".to_vec(),
3856 }),
3857 });
3858 tx.update(RefUpdate {
3859 name: "refs/heads/main".into(),
3860 expected: None,
3861 new: RefTarget::Direct(second.clone()),
3862 reflog: Some(ReflogEntry {
3863 old_oid: first,
3864 new_oid: second.clone(),
3865 committer: b"Git Rs <sley@example.invalid> 100 +0000".to_vec(),
3866 message: b"new".to_vec(),
3867 }),
3868 });
3869 tx.commit().expect("test operation should succeed");
3870
3871 let removed = store
3872 .expire_reflog_older_than("refs/heads/main", 50)
3873 .expect("test operation should succeed");
3874 assert_eq!(removed, 1);
3875 let log = store
3876 .read_reflog("refs/heads/main")
3877 .expect("test operation should succeed");
3878 assert_eq!(log.len(), 1);
3879 assert_eq!(log[0].new_oid, second);
3880 assert_eq!(log[0].message, b"new");
3881 assert!(
3882 !git_dir
3883 .join("logs")
3884 .join("refs")
3885 .join("heads")
3886 .join("main.lock")
3887 .exists()
3888 );
3889 fs::remove_dir_all(git_dir).expect("test operation should succeed");
3890 }
3891
3892 #[test]
3893 fn file_ref_store_creates_branch() {
3894 let git_dir = temp_git_dir();
3895 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
3896 let oid = ObjectId::from_hex(
3897 ObjectFormat::Sha1,
3898 "ce013625030ba8dba906f756967f9e9ca394464a",
3899 )
3900 .expect("test operation should succeed");
3901 let branch = store
3902 .create_branch(
3903 "feature",
3904 oid,
3905 b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
3906 b"branch: Created from main".to_vec(),
3907 )
3908 .expect("test operation should succeed");
3909 assert_eq!(branch.name, "refs/heads/feature");
3910 assert_eq!(
3911 store
3912 .read_ref("refs/heads/feature")
3913 .expect("test operation should succeed"),
3914 Some(RefTarget::Direct(oid))
3915 );
3916 fs::remove_dir_all(git_dir).expect("test operation should succeed");
3917 }
3918
3919 #[test]
3920 fn file_ref_store_deletes_loose_branch() {
3921 let git_dir = temp_git_dir();
3922 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
3923 let oid = ObjectId::from_hex(
3924 ObjectFormat::Sha1,
3925 "ce013625030ba8dba906f756967f9e9ca394464a",
3926 )
3927 .expect("test operation should succeed");
3928 store
3929 .create_branch(
3930 "feature",
3931 oid,
3932 b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
3933 b"branch: Created from main".to_vec(),
3934 )
3935 .expect("test operation should succeed");
3936 let deleted = store
3937 .delete_branch("feature")
3938 .expect("test operation should succeed");
3939 assert_eq!(deleted.name, "refs/heads/feature");
3940 assert_eq!(deleted.oid, oid);
3941 assert_eq!(
3942 store
3943 .read_ref("refs/heads/feature")
3944 .expect("test operation should succeed"),
3945 None
3946 );
3947 assert!(!git_dir.join("refs").join("heads").join("feature").exists());
3948 assert!(
3949 !git_dir
3950 .join("logs")
3951 .join("refs")
3952 .join("heads")
3953 .join("feature")
3954 .exists()
3955 );
3956 fs::remove_dir_all(git_dir).expect("test operation should succeed");
3957 }
3958
3959 #[test]
3960 fn file_ref_store_deletes_generic_loose_ref() {
3961 let git_dir = temp_git_dir();
3962 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
3963 let oid = ObjectId::from_hex(
3964 ObjectFormat::Sha1,
3965 "ce013625030ba8dba906f756967f9e9ca394464a",
3966 )
3967 .expect("test operation should succeed");
3968 let mut tx = store.transaction();
3969 tx.update(RefUpdate {
3970 name: "refs/heads/topic".into(),
3971 expected: None,
3972 new: RefTarget::Direct(oid),
3973 reflog: Some(ReflogEntry {
3974 old_oid: zero_oid(ObjectFormat::Sha1).expect("test operation should succeed"),
3975 new_oid: oid,
3976 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
3977 message: b"update by test".to_vec(),
3978 }),
3979 });
3980 tx.commit().expect("test operation should succeed");
3981 let deleted = store
3982 .delete_ref("refs/heads/topic")
3983 .expect("test operation should succeed");
3984 assert_eq!(deleted.name, "refs/heads/topic");
3985 assert_eq!(deleted.oid, oid);
3986 assert_eq!(
3987 store
3988 .read_ref("refs/heads/topic")
3989 .expect("test operation should succeed"),
3990 None
3991 );
3992 assert!(!git_dir.join("refs").join("heads").join("topic").exists());
3993 assert!(
3994 !git_dir
3995 .join("logs")
3996 .join("refs")
3997 .join("heads")
3998 .join("topic")
3999 .exists()
4000 );
4001 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4002 }
4003
4004 #[test]
4005 fn file_ref_store_delete_ref_checked_removes_reflog() {
4006 let git_dir = temp_git_dir();
4007 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4008 let oid = ObjectId::from_hex(
4009 ObjectFormat::Sha1,
4010 "ce013625030ba8dba906f756967f9e9ca394464a",
4011 )
4012 .expect("test operation should succeed");
4013 let mut tx = store.transaction();
4017 tx.update(RefUpdate {
4018 name: "refs/heads/main".into(),
4019 expected: None,
4020 new: RefTarget::Direct(oid),
4021 reflog: Some(ReflogEntry {
4022 old_oid: zero_oid(ObjectFormat::Sha1).expect("test operation should succeed"),
4023 new_oid: oid,
4024 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
4025 message: b"create main".to_vec(),
4026 }),
4027 });
4028 tx.commit().expect("test operation should succeed");
4029 assert!(
4030 git_dir
4031 .join("logs")
4032 .join("refs")
4033 .join("heads")
4034 .join("main")
4035 .exists(),
4036 "reflog file should exist before the checked delete"
4037 );
4038
4039 let deleted = store
4040 .delete_ref_checked(DeleteRef {
4041 name: "refs/heads/main".into(),
4042 expected_old: Some(oid),
4043 reflog: Some(DeleteRefReflog {
4044 committer: b"Git Rs <sley@example.invalid> 123 +0000".to_vec(),
4045 message: b"delete main".to_vec(),
4046 }),
4047 })
4048 .expect("test operation should succeed");
4049
4050 assert_eq!(deleted.name, "refs/heads/main");
4051 assert_eq!(deleted.oid, oid);
4052 assert_eq!(
4053 store
4054 .read_ref("refs/heads/main")
4055 .expect("test operation should succeed"),
4056 None
4057 );
4058 assert!(
4061 !git_dir
4062 .join("logs")
4063 .join("refs")
4064 .join("heads")
4065 .join("main")
4066 .exists(),
4067 "reflog file should be removed by the checked delete"
4068 );
4069 assert!(
4070 store
4071 .read_reflog("refs/heads/main")
4072 .expect("test operation should succeed")
4073 .is_empty()
4074 );
4075 assert!(
4076 !git_dir
4077 .join("refs")
4078 .join("heads")
4079 .join("main.lock")
4080 .exists()
4081 );
4082 assert!(!git_dir.join("packed-refs.lock").exists());
4083 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4084 }
4085
4086 #[test]
4087 fn file_ref_store_delete_ref_checked_stale_expected_leaves_ref_untouched() {
4088 let git_dir = temp_git_dir();
4089 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4090 let actual = ObjectId::from_hex(
4091 ObjectFormat::Sha1,
4092 "ce013625030ba8dba906f756967f9e9ca394464a",
4093 )
4094 .expect("test operation should succeed");
4095 let expected = ObjectId::from_hex(
4096 ObjectFormat::Sha1,
4097 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
4098 )
4099 .expect("test operation should succeed");
4100 let mut tx = store.transaction();
4101 tx.update(RefUpdate {
4102 name: "refs/heads/main".into(),
4103 expected: None,
4104 new: RefTarget::Direct(actual),
4105 reflog: None,
4106 });
4107 tx.commit().expect("test operation should succeed");
4108
4109 let err = store
4110 .delete_ref_checked(DeleteRef {
4111 name: "refs/heads/main".into(),
4112 expected_old: Some(expected),
4113 reflog: None,
4114 })
4115 .expect_err("stale expected must fail");
4116
4117 assert!(matches!(
4118 err,
4119 RefDeleteError::ExpectedMismatch {
4120 expected: Some(got_expected),
4121 actual: Some(got_actual),
4122 } if got_expected == expected && got_actual == actual
4123 ));
4124 assert_eq!(
4125 store
4126 .read_ref("refs/heads/main")
4127 .expect("test operation should succeed"),
4128 Some(RefTarget::Direct(actual))
4129 );
4130 assert!(
4131 !git_dir
4132 .join("refs")
4133 .join("heads")
4134 .join("main.lock")
4135 .exists()
4136 );
4137 assert!(!git_dir.join("packed-refs.lock").exists());
4138 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4139 }
4140
4141 #[test]
4142 fn file_ref_store_delete_ref_checked_missing_returns_not_found() {
4143 let git_dir = temp_git_dir();
4144 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4145
4146 let err = store
4147 .delete_ref_checked(DeleteRef {
4148 name: "refs/heads/missing".into(),
4149 expected_old: None,
4150 reflog: None,
4151 })
4152 .expect_err("missing ref must fail");
4153
4154 assert!(matches!(err, RefDeleteError::NotFound));
4155 assert!(
4156 !git_dir
4157 .join("refs")
4158 .join("heads")
4159 .join("missing.lock")
4160 .exists()
4161 );
4162 assert!(!git_dir.join("packed-refs.lock").exists());
4163 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4164 }
4165
4166 #[test]
4167 fn file_ref_store_delete_ref_checked_removes_packed_ref() {
4168 let git_dir = temp_git_dir();
4169 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4170 let oid = ObjectId::from_hex(
4171 ObjectFormat::Sha1,
4172 "ce013625030ba8dba906f756967f9e9ca394464a",
4173 )
4174 .expect("test operation should succeed");
4175 let other = ObjectId::from_hex(
4176 ObjectFormat::Sha1,
4177 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
4178 )
4179 .expect("test operation should succeed");
4180 store
4181 .write_packed_refs(&[
4182 PackedRef {
4183 reference: Ref {
4184 name: "refs/heads/main".into(),
4185 target: RefTarget::Direct(oid),
4186 },
4187 peeled: None,
4188 },
4189 PackedRef {
4190 reference: Ref {
4191 name: "refs/heads/other".into(),
4192 target: RefTarget::Direct(other),
4193 },
4194 peeled: None,
4195 },
4196 ])
4197 .expect("test operation should succeed");
4198
4199 store
4200 .delete_ref_checked(DeleteRef {
4201 name: "refs/heads/main".into(),
4202 expected_old: Some(oid),
4203 reflog: None,
4204 })
4205 .expect("test operation should succeed");
4206
4207 assert_eq!(
4208 store
4209 .read_ref("refs/heads/main")
4210 .expect("test operation should succeed"),
4211 None
4212 );
4213 assert_eq!(
4214 store
4215 .read_ref("refs/heads/other")
4216 .expect("test operation should succeed"),
4217 Some(RefTarget::Direct(other))
4218 );
4219 let packed =
4220 fs::read_to_string(git_dir.join("packed-refs")).expect("test operation should succeed");
4221 assert!(!packed.contains("refs/heads/main"));
4222 assert!(packed.contains("refs/heads/other"));
4223 assert!(
4224 !git_dir
4225 .join("refs")
4226 .join("heads")
4227 .join("main.lock")
4228 .exists()
4229 );
4230 assert!(!git_dir.join("packed-refs.lock").exists());
4231 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4232 }
4233
4234 #[test]
4235 fn file_ref_store_delete_ref_checked_lock_conflict_returns_locked() {
4236 let git_dir = temp_git_dir();
4237 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4238 let oid = ObjectId::from_hex(
4239 ObjectFormat::Sha1,
4240 "ce013625030ba8dba906f756967f9e9ca394464a",
4241 )
4242 .expect("test operation should succeed");
4243 let mut tx = store.transaction();
4244 tx.update(RefUpdate {
4245 name: "refs/heads/main".into(),
4246 expected: None,
4247 new: RefTarget::Direct(oid),
4248 reflog: None,
4249 });
4250 tx.commit().expect("test operation should succeed");
4251 fs::write(
4252 git_dir.join("refs").join("heads").join("main.lock"),
4253 b"held\n",
4254 )
4255 .expect("test operation should succeed");
4256
4257 let err = store
4258 .delete_ref_checked(DeleteRef {
4259 name: "refs/heads/main".into(),
4260 expected_old: Some(oid),
4261 reflog: None,
4262 })
4263 .expect_err("held lock must fail");
4264
4265 assert!(matches!(err, RefDeleteError::Locked));
4266 assert_eq!(
4267 store
4268 .read_ref("refs/heads/main")
4269 .expect("test operation should succeed"),
4270 Some(RefTarget::Direct(oid))
4271 );
4272 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4273 }
4274
4275 #[test]
4276 fn file_ref_store_reports_current_branch() {
4277 let git_dir = temp_git_dir();
4278 fs::write(git_dir.join("HEAD"), b"ref: refs/heads/main\n")
4279 .expect("test operation should succeed");
4280 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4281 assert_eq!(
4282 store
4283 .current_branch_ref()
4284 .expect("test operation should succeed"),
4285 Some("refs/heads/main".into())
4286 );
4287 assert_eq!(
4288 store
4289 .current_branch()
4290 .expect("test operation should succeed"),
4291 Some("main".into())
4292 );
4293 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4294 }
4295
4296 #[test]
4297 fn file_ref_store_resolves_linked_worktree_head_through_common_refs() {
4298 let common = temp_git_dir();
4299 let admin = common.join("worktrees").join("linked");
4300 fs::create_dir_all(&admin).expect("test operation should succeed");
4301 fs::write(admin.join("commondir"), "../..\n").expect("test operation should succeed");
4302 fs::write(admin.join("HEAD"), b"ref: refs/heads/topic\n")
4303 .expect("test operation should succeed");
4304 let oid = ObjectId::from_hex(
4305 ObjectFormat::Sha256,
4306 "08ffba112b648c22b5425f01bec2c37ffc524c4d48ef04337779df3973733050",
4307 )
4308 .expect("test operation should succeed");
4309 fs::create_dir_all(common.join("refs").join("heads"))
4310 .expect("test operation should succeed");
4311 fs::write(
4312 common.join("refs").join("heads").join("topic"),
4313 format!("{oid}\n"),
4314 )
4315 .expect("test operation should succeed");
4316
4317 let store = FileRefStore::new(&admin, ObjectFormat::Sha256);
4318 assert_eq!(
4319 store
4320 .read_ref("HEAD")
4321 .expect("test operation should succeed"),
4322 Some(RefTarget::Symbolic("refs/heads/topic".into()))
4323 );
4324 assert_eq!(
4325 store
4326 .read_ref("refs/heads/topic")
4327 .expect("test operation should succeed"),
4328 Some(RefTarget::Direct(oid))
4329 );
4330
4331 fs::remove_dir_all(common).expect("test operation should succeed");
4332 }
4333
4334 #[test]
4335 fn file_ref_store_creates_tag() {
4336 let git_dir = temp_git_dir();
4337 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4338 let oid = ObjectId::from_hex(
4339 ObjectFormat::Sha1,
4340 "ce013625030ba8dba906f756967f9e9ca394464a",
4341 )
4342 .expect("test operation should succeed");
4343 let tag = store
4344 .create_tag("v1.0", oid)
4345 .expect("test operation should succeed");
4346 assert_eq!(tag.name, "refs/tags/v1.0");
4347 assert_eq!(
4348 store
4349 .read_ref("refs/tags/v1.0")
4350 .expect("test operation should succeed"),
4351 Some(RefTarget::Direct(oid))
4352 );
4353 assert!(
4354 store
4355 .read_reflog("refs/tags/v1.0")
4356 .expect("test operation should succeed")
4357 .is_empty()
4358 );
4359 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4360 }
4361
4362 #[test]
4363 fn file_ref_store_deletes_loose_tag() {
4364 let git_dir = temp_git_dir();
4365 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4366 let oid = ObjectId::from_hex(
4367 ObjectFormat::Sha1,
4368 "ce013625030ba8dba906f756967f9e9ca394464a",
4369 )
4370 .expect("test operation should succeed");
4371 store
4372 .create_tag("v1.0", oid)
4373 .expect("test operation should succeed");
4374 let deleted = store
4375 .delete_tag("v1.0")
4376 .expect("test operation should succeed");
4377 assert_eq!(deleted.name, "refs/tags/v1.0");
4378 assert_eq!(deleted.oid, oid);
4379 assert_eq!(
4380 store
4381 .read_ref("refs/tags/v1.0")
4382 .expect("test operation should succeed"),
4383 None
4384 );
4385 assert!(!git_dir.join("refs").join("tags").join("v1.0").exists());
4386 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4387 }
4388
4389 #[test]
4390 fn file_ref_store_reads_packed_ref() {
4391 let git_dir = temp_git_dir();
4392 fs::write(
4393 git_dir.join("packed-refs"),
4394 b"ce013625030ba8dba906f756967f9e9ca394464a refs/heads/main\n",
4395 )
4396 .expect("test operation should succeed");
4397 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4398 assert!(matches!(
4399 store
4400 .read_ref("refs/heads/main")
4401 .expect("test operation should succeed"),
4402 Some(RefTarget::Direct(_))
4403 ));
4404 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4405 }
4406
4407 #[test]
4408 fn file_ref_store_lists_loose_refs_over_packed_refs() {
4409 let git_dir = temp_git_dir();
4410 fs::write(
4411 git_dir.join("packed-refs"),
4412 b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 refs/heads/main\n",
4413 )
4414 .expect("test operation should succeed");
4415 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4416 let oid = ObjectId::from_hex(
4417 ObjectFormat::Sha1,
4418 "ce013625030ba8dba906f756967f9e9ca394464a",
4419 )
4420 .expect("test operation should succeed");
4421 let mut tx = store.transaction();
4422 tx.update(RefUpdate {
4423 name: "refs/heads/main".into(),
4424 expected: None,
4425 new: RefTarget::Direct(oid),
4426 reflog: None,
4427 });
4428 tx.commit().expect("test operation should succeed");
4429 let refs = store.list_refs().expect("test operation should succeed");
4430 assert_eq!(refs.len(), 1);
4431 assert_eq!(refs[0].target, RefTarget::Direct(oid));
4432 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4433 }
4434
4435 #[test]
4436 fn file_ref_store_writes_packed_refs() {
4437 let git_dir = temp_git_dir();
4438 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4439 let oid = ObjectId::from_hex(
4440 ObjectFormat::Sha1,
4441 "ce013625030ba8dba906f756967f9e9ca394464a",
4442 )
4443 .expect("test operation should succeed");
4444 store
4445 .write_packed_refs(&[PackedRef {
4446 reference: Ref {
4447 name: "refs/heads/main".into(),
4448 target: RefTarget::Direct(oid),
4449 },
4450 peeled: None,
4451 }])
4452 .expect("test operation should succeed");
4453 assert_eq!(
4454 store
4455 .read_ref("refs/heads/main")
4456 .expect("test operation should succeed"),
4457 Some(RefTarget::Direct(oid))
4458 );
4459 let refs = store.list_refs().expect("test operation should succeed");
4460 assert_eq!(refs.len(), 1);
4461 assert_eq!(refs[0].target, RefTarget::Direct(oid));
4462 assert!(git_dir.join("packed-refs").exists());
4463 assert!(!git_dir.join("packed-refs.lock").exists());
4464 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4465 }
4466
4467 #[test]
4468 fn file_ref_store_checks_ref_prefix_in_packed_refs() {
4469 let git_dir = temp_git_dir();
4470 fs::write(
4471 git_dir.join("packed-refs"),
4472 b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 refs/heads/main\n\
4473 ce013625030ba8dba906f756967f9e9ca394464a refs/replace/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\n",
4474 )
4475 .expect("test operation should succeed");
4476 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4477 assert!(
4478 store
4479 .has_refs_with_prefix("refs/replace/")
4480 .expect("test operation should succeed")
4481 );
4482 assert!(
4483 !store
4484 .has_refs_with_prefix("refs/notes/")
4485 .expect("test operation should succeed")
4486 );
4487 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4488 }
4489
4490 #[test]
4491 fn file_ref_store_checks_ref_prefix_in_loose_refs() {
4492 let git_dir = temp_git_dir();
4493 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4494 let oid = ObjectId::from_hex(
4495 ObjectFormat::Sha1,
4496 "ce013625030ba8dba906f756967f9e9ca394464a",
4497 )
4498 .expect("test operation should succeed");
4499 let mut tx = store.transaction();
4500 tx.update(RefUpdate {
4501 name: "refs/replace/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391".into(),
4502 expected: None,
4503 new: RefTarget::Direct(oid),
4504 reflog: None,
4505 });
4506 tx.commit().expect("test operation should succeed");
4507 assert!(
4508 store
4509 .has_refs_with_prefix("refs/replace/")
4510 .expect("test operation should succeed")
4511 );
4512 assert!(
4513 !store
4514 .has_refs_with_prefix("refs/notes/")
4515 .expect("test operation should succeed")
4516 );
4517 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4518 }
4519
4520 #[test]
4521 fn file_ref_store_reads_reftable_stack_and_ignores_dummy_head() {
4522 let git_dir = temp_git_dir();
4523 write_reftable_config(&git_dir);
4524 fs::write(git_dir.join("HEAD"), b"ref: refs/heads/.invalid\n")
4525 .expect("test operation should succeed");
4526 let head_oid = ObjectId::from_hex(
4527 ObjectFormat::Sha1,
4528 "ce013625030ba8dba906f756967f9e9ca394464a",
4529 )
4530 .expect("test operation should succeed");
4531 let tag_oid = ObjectId::from_hex(
4532 ObjectFormat::Sha1,
4533 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
4534 )
4535 .expect("test operation should succeed");
4536 let peeled_oid = ObjectId::from_hex(
4537 ObjectFormat::Sha1,
4538 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
4539 )
4540 .expect("test operation should succeed");
4541 write_reftable_stack(
4542 &git_dir,
4543 &[(
4544 "000000000001-000000000001-rust.ref",
4545 vec![
4546 sley_formats::ReftableRefRecord {
4547 name: "HEAD".into(),
4548 update_index: 1,
4549 value: ReftableRefValue::Symbolic("refs/heads/main".into()),
4550 },
4551 sley_formats::ReftableRefRecord {
4552 name: "refs/heads/main".into(),
4553 update_index: 1,
4554 value: ReftableRefValue::Direct(head_oid),
4555 },
4556 sley_formats::ReftableRefRecord {
4557 name: "refs/tags/v1.0".into(),
4558 update_index: 1,
4559 value: ReftableRefValue::Peeled {
4560 target: tag_oid,
4561 peeled: peeled_oid,
4562 },
4563 },
4564 ],
4565 )],
4566 );
4567
4568 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4569 assert_eq!(
4570 store
4571 .read_ref("HEAD")
4572 .expect("test operation should succeed"),
4573 Some(RefTarget::Symbolic("refs/heads/main".into()))
4574 );
4575 assert_eq!(
4576 store
4577 .read_ref("refs/heads/main")
4578 .expect("test operation should succeed"),
4579 Some(RefTarget::Direct(head_oid))
4580 );
4581 assert_eq!(
4582 store
4583 .read_ref("refs/tags/v1.0")
4584 .expect("test operation should succeed"),
4585 Some(RefTarget::Direct(tag_oid))
4586 );
4587 let refs = store.list_refs().expect("test operation should succeed");
4588 assert_eq!(
4589 refs,
4590 vec![
4591 Ref {
4592 name: "refs/heads/main".into(),
4593 target: RefTarget::Direct(head_oid),
4594 },
4595 Ref {
4596 name: "refs/tags/v1.0".into(),
4597 target: RefTarget::Direct(tag_oid),
4598 },
4599 ]
4600 );
4601
4602 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4603 }
4604
4605 #[test]
4606 fn file_ref_store_applies_reftable_stack_overrides_and_deletions() {
4607 let git_dir = temp_git_dir();
4608 write_reftable_config(&git_dir);
4609 let first = ObjectId::from_hex(
4610 ObjectFormat::Sha1,
4611 "ce013625030ba8dba906f756967f9e9ca394464a",
4612 )
4613 .expect("test operation should succeed");
4614 let second = ObjectId::from_hex(
4615 ObjectFormat::Sha1,
4616 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
4617 )
4618 .expect("test operation should succeed");
4619 write_reftable_stack(
4620 &git_dir,
4621 &[
4622 (
4623 "000000000001-000000000001-base.ref",
4624 vec![
4625 sley_formats::ReftableRefRecord {
4626 name: "refs/heads/main".into(),
4627 update_index: 1,
4628 value: ReftableRefValue::Direct(first),
4629 },
4630 sley_formats::ReftableRefRecord {
4631 name: "refs/heads/topic".into(),
4632 update_index: 1,
4633 value: ReftableRefValue::Direct(second.clone()),
4634 },
4635 ],
4636 ),
4637 (
4638 "000000000002-000000000002-tip.ref",
4639 vec![
4640 sley_formats::ReftableRefRecord {
4641 name: "refs/heads/main".into(),
4642 update_index: 2,
4643 value: ReftableRefValue::Direct(second.clone()),
4644 },
4645 sley_formats::ReftableRefRecord {
4646 name: "refs/heads/topic".into(),
4647 update_index: 2,
4648 value: ReftableRefValue::Deletion,
4649 },
4650 ],
4651 ),
4652 ],
4653 );
4654
4655 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4656 assert_eq!(
4657 store
4658 .read_ref("refs/heads/main")
4659 .expect("test operation should succeed"),
4660 Some(RefTarget::Direct(second.clone()))
4661 );
4662 assert_eq!(
4663 store
4664 .read_ref("refs/heads/topic")
4665 .expect("test operation should succeed"),
4666 None
4667 );
4668 assert_eq!(
4669 store.list_refs().expect("test operation should succeed"),
4670 vec![Ref {
4671 name: "refs/heads/main".into(),
4672 target: RefTarget::Direct(second),
4673 }]
4674 );
4675
4676 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4677 }
4678
4679 #[test]
4680 fn file_ref_store_writes_reftable_transaction_table() {
4681 let git_dir = temp_git_dir();
4682 write_reftable_config(&git_dir);
4683 let first = ObjectId::from_hex(
4684 ObjectFormat::Sha1,
4685 "ce013625030ba8dba906f756967f9e9ca394464a",
4686 )
4687 .expect("test operation should succeed");
4688 let second = ObjectId::from_hex(
4689 ObjectFormat::Sha1,
4690 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
4691 )
4692 .expect("test operation should succeed");
4693 write_reftable_stack(
4694 &git_dir,
4695 &[(
4696 "000000000001-000000000001-base.ref",
4697 vec![sley_formats::ReftableRefRecord {
4698 name: "refs/heads/main".into(),
4699 update_index: 1,
4700 value: ReftableRefValue::Direct(first),
4701 }],
4702 )],
4703 );
4704
4705 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4706 let mut tx = store.transaction();
4707 tx.update(RefUpdate {
4708 name: "HEAD".into(),
4709 expected: None,
4710 new: RefTarget::Symbolic("refs/heads/main".into()),
4711 reflog: None,
4712 });
4713 tx.update(RefUpdate {
4714 name: "refs/heads/main".into(),
4715 expected: None,
4716 new: RefTarget::Direct(second.clone()),
4717 reflog: None,
4718 });
4719 tx.commit().expect("test operation should succeed");
4720
4721 assert_eq!(
4722 store
4723 .read_ref("HEAD")
4724 .expect("test operation should succeed"),
4725 Some(RefTarget::Symbolic("refs/heads/main".into()))
4726 );
4727 assert_eq!(
4728 store
4729 .read_ref("refs/heads/main")
4730 .expect("test operation should succeed"),
4731 Some(RefTarget::Direct(second.clone()))
4732 );
4733 assert_eq!(
4734 store
4735 .list_refs()
4736 .expect("test operation should succeed")
4737 .len(),
4738 1
4739 );
4740 assert!(!git_dir.join("HEAD").exists());
4741 let tables = fs::read_to_string(git_dir.join("reftable").join("tables.list"))
4742 .expect("test operation should succeed");
4743 assert_eq!(tables.lines().count(), 2);
4744 assert!(
4745 tables
4746 .lines()
4747 .last()
4748 .expect("test operation should succeed")
4749 .contains("sley"),
4750 "expected rust-written reftable in tables.list, got {tables}"
4751 );
4752
4753 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4754 }
4755
4756 #[test]
4757 fn file_ref_store_deletes_reftable_refs_with_tombstones() {
4758 let git_dir = temp_git_dir();
4759 write_reftable_config(&git_dir);
4760 let oid = ObjectId::from_hex(
4761 ObjectFormat::Sha1,
4762 "ce013625030ba8dba906f756967f9e9ca394464a",
4763 )
4764 .expect("test operation should succeed");
4765 write_reftable_stack(
4766 &git_dir,
4767 &[(
4768 "000000000001-000000000001-base.ref",
4769 vec![
4770 sley_formats::ReftableRefRecord {
4771 name: "refs/heads/main".into(),
4772 update_index: 1,
4773 value: ReftableRefValue::Direct(oid),
4774 },
4775 sley_formats::ReftableRefRecord {
4776 name: "refs/alias/main".into(),
4777 update_index: 1,
4778 value: ReftableRefValue::Symbolic("refs/heads/main".into()),
4779 },
4780 ],
4781 )],
4782 );
4783
4784 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4785 assert!(
4786 store
4787 .delete_symbolic_ref("refs/alias/main")
4788 .expect("test operation should succeed")
4789 );
4790 assert_eq!(
4791 store
4792 .read_ref("refs/alias/main")
4793 .expect("test operation should succeed"),
4794 None
4795 );
4796 let deleted = store
4797 .delete_ref("refs/heads/main")
4798 .expect("test operation should succeed");
4799 assert_eq!(deleted.oid, oid);
4800 assert_eq!(
4801 store
4802 .read_ref("refs/heads/main")
4803 .expect("test operation should succeed"),
4804 None
4805 );
4806 assert!(
4807 store
4808 .list_refs()
4809 .expect("test operation should succeed")
4810 .is_empty()
4811 );
4812 let tables = fs::read_to_string(git_dir.join("reftable").join("tables.list"))
4813 .expect("test operation should succeed");
4814 assert_eq!(tables.lines().count(), 3);
4815
4816 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4817 }
4818
4819 #[test]
4820 fn file_ref_store_deletes_packed_branch() {
4821 let git_dir = temp_git_dir();
4822 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4823 let branch_oid = ObjectId::from_hex(
4824 ObjectFormat::Sha1,
4825 "ce013625030ba8dba906f756967f9e9ca394464a",
4826 )
4827 .expect("test operation should succeed");
4828 let tag_oid = ObjectId::from_hex(
4829 ObjectFormat::Sha1,
4830 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
4831 )
4832 .expect("test operation should succeed");
4833 store
4834 .write_packed_refs(&[
4835 PackedRef {
4836 reference: Ref {
4837 name: "refs/heads/feature".into(),
4838 target: RefTarget::Direct(branch_oid),
4839 },
4840 peeled: None,
4841 },
4842 PackedRef {
4843 reference: Ref {
4844 name: "refs/tags/v1.0".into(),
4845 target: RefTarget::Direct(tag_oid),
4846 },
4847 peeled: None,
4848 },
4849 ])
4850 .expect("test operation should succeed");
4851 let deleted = store
4852 .delete_branch("feature")
4853 .expect("test operation should succeed");
4854 assert_eq!(deleted.name, "refs/heads/feature");
4855 assert_eq!(deleted.oid, branch_oid);
4856 assert_eq!(
4857 store
4858 .read_ref("refs/heads/feature")
4859 .expect("test operation should succeed"),
4860 None
4861 );
4862 assert_eq!(
4863 store
4864 .read_ref("refs/tags/v1.0")
4865 .expect("test operation should succeed"),
4866 Some(RefTarget::Direct(tag_oid))
4867 );
4868 assert!(!git_dir.join("packed-refs.lock").exists());
4869 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4870 }
4871
4872 #[test]
4873 fn file_ref_store_deletes_packed_tag() {
4874 let git_dir = temp_git_dir();
4875 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4876 let oid = ObjectId::from_hex(
4877 ObjectFormat::Sha1,
4878 "ce013625030ba8dba906f756967f9e9ca394464a",
4879 )
4880 .expect("test operation should succeed");
4881 store
4882 .write_packed_refs(&[PackedRef {
4883 reference: Ref {
4884 name: "refs/tags/v1.0".into(),
4885 target: RefTarget::Direct(oid),
4886 },
4887 peeled: None,
4888 }])
4889 .expect("test operation should succeed");
4890 let deleted = store
4891 .delete_tag("v1.0")
4892 .expect("test operation should succeed");
4893 assert_eq!(deleted.name, "refs/tags/v1.0");
4894 assert_eq!(deleted.oid, oid);
4895 assert_eq!(
4896 store
4897 .read_ref("refs/tags/v1.0")
4898 .expect("test operation should succeed"),
4899 None
4900 );
4901 assert!(!git_dir.join("packed-refs.lock").exists());
4902 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4903 }
4904
4905 #[test]
4906 fn file_ref_store_packs_loose_refs_and_prunes() {
4907 let git_dir = temp_git_dir();
4908 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4909 let main_oid = ObjectId::from_hex(
4910 ObjectFormat::Sha1,
4911 "ce013625030ba8dba906f756967f9e9ca394464a",
4912 )
4913 .expect("test operation should succeed");
4914 let tag_oid = ObjectId::from_hex(
4915 ObjectFormat::Sha1,
4916 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
4917 )
4918 .expect("test operation should succeed");
4919 let mut tx = store.transaction();
4920 tx.update(RefUpdate {
4921 name: "refs/heads/main".into(),
4922 expected: None,
4923 new: RefTarget::Direct(main_oid),
4924 reflog: None,
4925 });
4926 tx.update(RefUpdate {
4927 name: "refs/tags/v1.0".into(),
4928 expected: None,
4929 new: RefTarget::Direct(tag_oid),
4930 reflog: None,
4931 });
4932 tx.commit().expect("test operation should succeed");
4933
4934 let packed = store
4935 .pack_refs(true)
4936 .expect("test operation should succeed");
4937 assert_eq!(packed.len(), 2);
4938 assert_eq!(
4939 store
4940 .read_ref("refs/heads/main")
4941 .expect("test operation should succeed"),
4942 Some(RefTarget::Direct(main_oid))
4943 );
4944 assert_eq!(
4945 store
4946 .read_ref("refs/tags/v1.0")
4947 .expect("test operation should succeed"),
4948 Some(RefTarget::Direct(tag_oid))
4949 );
4950 assert!(!git_dir.join("refs").join("heads").join("main").exists());
4951 assert!(!git_dir.join("refs").join("tags").join("v1.0").exists());
4952 assert!(git_dir.join("packed-refs").exists());
4953 assert!(!git_dir.join("packed-refs.lock").exists());
4954 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4955 }
4956
4957 #[test]
4958 fn file_ref_store_packs_loose_refs_without_pruning() {
4959 let git_dir = temp_git_dir();
4960 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4961 let oid = ObjectId::from_hex(
4962 ObjectFormat::Sha1,
4963 "ce013625030ba8dba906f756967f9e9ca394464a",
4964 )
4965 .expect("test operation should succeed");
4966 let mut tx = store.transaction();
4967 tx.update(RefUpdate {
4968 name: "refs/heads/main".into(),
4969 expected: None,
4970 new: RefTarget::Direct(oid),
4971 reflog: None,
4972 });
4973 tx.commit().expect("test operation should succeed");
4974
4975 let packed = store
4976 .pack_refs(false)
4977 .expect("test operation should succeed");
4978 assert_eq!(packed.len(), 1);
4979 assert!(git_dir.join("refs").join("heads").join("main").exists());
4980 assert_eq!(
4981 store
4982 .read_ref("refs/heads/main")
4983 .expect("test operation should succeed"),
4984 Some(RefTarget::Direct(oid))
4985 );
4986 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4987 }
4988
4989 #[test]
4990 fn file_ref_store_packs_loose_refs_with_peeled_ids() {
4991 let git_dir = temp_git_dir();
4992 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4993 let tag_oid = ObjectId::from_hex(
4994 ObjectFormat::Sha1,
4995 "ce013625030ba8dba906f756967f9e9ca394464a",
4996 )
4997 .expect("test operation should succeed");
4998 let peeled_oid = ObjectId::from_hex(
4999 ObjectFormat::Sha1,
5000 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
5001 )
5002 .expect("test operation should succeed");
5003 let mut tx = store.transaction();
5004 tx.update(RefUpdate {
5005 name: "refs/tags/v1.0".into(),
5006 expected: None,
5007 new: RefTarget::Direct(tag_oid),
5008 reflog: None,
5009 });
5010 tx.commit().expect("test operation should succeed");
5011
5012 let packed = store
5013 .pack_refs_with_peeler(true, |name, oid| {
5014 if name == "refs/tags/v1.0" && oid == &tag_oid {
5015 Ok(Some(peeled_oid))
5016 } else {
5017 Ok(None)
5018 }
5019 })
5020 .expect("test operation should succeed");
5021 assert_eq!(packed.len(), 1);
5022 assert_eq!(packed[0].peeled, Some(peeled_oid));
5023 let bytes =
5024 fs::read_to_string(git_dir.join("packed-refs")).expect("test operation should succeed");
5025 assert!(bytes.contains(&format!("^{peeled_oid}\n")));
5026 assert!(!git_dir.join("refs").join("tags").join("v1.0").exists());
5027 fs::remove_dir_all(git_dir).expect("test operation should succeed");
5028 }
5029
5030 fn reflog_entry(new_oid: &ObjectId, timestamp: i64, message: &str) -> ReflogEntry {
5031 ReflogEntry {
5032 old_oid: zero_oid(new_oid.format()).expect("test operation should succeed"),
5033 new_oid: *new_oid,
5034 committer: format!("Git Rs <sley@example.invalid> {timestamp} +0000").into_bytes(),
5035 message: message.as_bytes().to_vec(),
5036 }
5037 }
5038
5039 #[test]
5040 fn expire_reflog_drops_old_entries_and_keeps_latest() {
5041 let oid_a = ObjectId::from_hex(
5042 ObjectFormat::Sha1,
5043 "ce013625030ba8dba906f756967f9e9ca394464a",
5044 )
5045 .expect("test operation should succeed");
5046 let oid_b = ObjectId::from_hex(
5047 ObjectFormat::Sha1,
5048 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
5049 )
5050 .expect("test operation should succeed");
5051 let oid_c = ObjectId::from_hex(
5052 ObjectFormat::Sha1,
5053 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
5054 )
5055 .expect("test operation should succeed");
5056 let entries = vec![
5057 reflog_entry(&oid_a, 10, "oldest"),
5058 reflog_entry(&oid_b, 100, "middle"),
5059 reflog_entry(&oid_c, 20, "latest"),
5060 ];
5061
5062 let retained =
5065 expire_reflog(&entries, 50, None, |_| true).expect("test operation should succeed");
5066 assert_eq!(retained.len(), 2);
5067 assert_eq!(retained[0].message, b"middle");
5068 assert_eq!(retained[1].message, b"latest");
5069 }
5070
5071 #[test]
5072 fn expire_reflog_applies_stricter_unreachable_cutoff() {
5073 let reachable = ObjectId::from_hex(
5074 ObjectFormat::Sha1,
5075 "ce013625030ba8dba906f756967f9e9ca394464a",
5076 )
5077 .expect("test operation should succeed");
5078 let unreachable = ObjectId::from_hex(
5079 ObjectFormat::Sha1,
5080 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
5081 )
5082 .expect("test operation should succeed");
5083 let tip = ObjectId::from_hex(
5084 ObjectFormat::Sha1,
5085 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
5086 )
5087 .expect("test operation should succeed");
5088 let entries = vec![
5091 reflog_entry(&reachable, 100, "reachable"),
5092 reflog_entry(&unreachable, 100, "unreachable"),
5093 reflog_entry(&tip, 200, "tip"),
5094 ];
5095 let retained = expire_reflog(&entries, 50, Some(150), |oid| {
5096 oid == &reachable || oid == &tip
5097 })
5098 .expect("test operation should succeed");
5099 assert_eq!(retained.len(), 2);
5100 assert_eq!(retained[0].message, b"reachable");
5101 assert_eq!(retained[1].message, b"tip");
5102 }
5103
5104 #[test]
5105 fn expire_reflog_keeps_single_entry_below_cutoff() {
5106 let oid = ObjectId::from_hex(
5107 ObjectFormat::Sha1,
5108 "ce013625030ba8dba906f756967f9e9ca394464a",
5109 )
5110 .expect("test operation should succeed");
5111 let entries = vec![reflog_entry(&oid, 1, "only")];
5112 let retained = expire_reflog(&entries, i64::MAX, Some(i64::MAX), |_| false)
5113 .expect("test operation should succeed");
5114 assert_eq!(retained.len(), 1);
5115 assert_eq!(retained[0].message, b"only");
5116 }
5117
5118 #[test]
5119 fn file_ref_store_expire_reflog_file_rewrites_and_dry_runs() {
5120 let git_dir = temp_git_dir();
5121 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5122 let first = ObjectId::from_hex(
5123 ObjectFormat::Sha1,
5124 "ce013625030ba8dba906f756967f9e9ca394464a",
5125 )
5126 .expect("test operation should succeed");
5127 let second = ObjectId::from_hex(
5128 ObjectFormat::Sha1,
5129 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
5130 )
5131 .expect("test operation should succeed");
5132 store
5133 .write_reflog(
5134 "refs/heads/main",
5135 &[
5136 reflog_entry(&first, 10, "old"),
5137 reflog_entry(&second, 100, "new"),
5138 ],
5139 )
5140 .expect("test operation should succeed");
5141
5142 let would_remove = store
5144 .expire_reflog_file("refs/heads/main", 50, None, false, |_| true)
5145 .expect("test operation should succeed");
5146 assert_eq!(would_remove, 1);
5147 assert_eq!(
5148 store
5149 .read_reflog("refs/heads/main")
5150 .expect("test operation should succeed")
5151 .len(),
5152 2
5153 );
5154
5155 let removed = store
5157 .expire_reflog_file("refs/heads/main", 50, None, true, |_| true)
5158 .expect("test operation should succeed");
5159 assert_eq!(removed, 1);
5160 let log = store
5161 .read_reflog("refs/heads/main")
5162 .expect("test operation should succeed");
5163 assert_eq!(log.len(), 1);
5164 assert_eq!(log[0].new_oid, second);
5165 assert!(
5166 !git_dir
5167 .join("logs")
5168 .join("refs")
5169 .join("heads")
5170 .join("main.lock")
5171 .exists()
5172 );
5173 fs::remove_dir_all(git_dir).expect("test operation should succeed");
5174 }
5175
5176 #[test]
5177 fn file_ref_transaction_commits_all_refs_atomically() {
5178 let git_dir = temp_git_dir();
5179 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5180 let main_oid = ObjectId::from_hex(
5181 ObjectFormat::Sha1,
5182 "ce013625030ba8dba906f756967f9e9ca394464a",
5183 )
5184 .expect("test operation should succeed");
5185 let topic_oid = ObjectId::from_hex(
5186 ObjectFormat::Sha1,
5187 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
5188 )
5189 .expect("test operation should succeed");
5190 let tag_oid = ObjectId::from_hex(
5191 ObjectFormat::Sha1,
5192 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
5193 )
5194 .expect("test operation should succeed");
5195 let mut tx = store.transaction();
5196 tx.update(RefUpdate {
5197 name: "refs/heads/main".into(),
5198 expected: None,
5199 new: RefTarget::Direct(main_oid),
5200 reflog: Some(reflog_entry(&main_oid, 0, "create main")),
5201 });
5202 tx.update(RefUpdate {
5203 name: "refs/heads/topic".into(),
5204 expected: None,
5205 new: RefTarget::Direct(topic_oid),
5206 reflog: None,
5207 });
5208 tx.update(RefUpdate {
5209 name: "refs/tags/v1.0".into(),
5210 expected: None,
5211 new: RefTarget::Direct(tag_oid),
5212 reflog: None,
5213 });
5214 tx.commit().expect("test operation should succeed");
5215
5216 assert_eq!(
5217 store
5218 .read_ref("refs/heads/main")
5219 .expect("test operation should succeed"),
5220 Some(RefTarget::Direct(main_oid))
5221 );
5222 assert_eq!(
5223 store
5224 .read_ref("refs/heads/topic")
5225 .expect("test operation should succeed"),
5226 Some(RefTarget::Direct(topic_oid))
5227 );
5228 assert_eq!(
5229 store
5230 .read_ref("refs/tags/v1.0")
5231 .expect("test operation should succeed"),
5232 Some(RefTarget::Direct(tag_oid))
5233 );
5234 let main_log = store
5235 .read_reflog("refs/heads/main")
5236 .expect("test operation should succeed");
5237 assert_eq!(main_log.len(), 1);
5238 assert_eq!(main_log[0].new_oid, main_oid);
5239 assert!(
5241 !git_dir
5242 .join("refs")
5243 .join("heads")
5244 .join("main.lock")
5245 .exists()
5246 );
5247 assert!(
5248 !git_dir
5249 .join("refs")
5250 .join("heads")
5251 .join("topic.lock")
5252 .exists()
5253 );
5254 assert!(!git_dir.join("refs").join("tags").join("v1.0.lock").exists());
5255 fs::remove_dir_all(git_dir).expect("test operation should succeed");
5256 }
5257
5258 #[test]
5259 fn file_ref_transaction_rolls_back_all_refs_on_expected_mismatch() {
5260 let git_dir = temp_git_dir();
5261 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5262 let old_topic = ObjectId::from_hex(
5263 ObjectFormat::Sha1,
5264 "ce013625030ba8dba906f756967f9e9ca394464a",
5265 )
5266 .expect("test operation should succeed");
5267 let new_main = ObjectId::from_hex(
5268 ObjectFormat::Sha1,
5269 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
5270 )
5271 .expect("test operation should succeed");
5272 let new_tag = ObjectId::from_hex(
5273 ObjectFormat::Sha1,
5274 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
5275 )
5276 .expect("test operation should succeed");
5277 let wrong_expected = ObjectId::from_hex(
5278 ObjectFormat::Sha1,
5279 "0000000000000000000000000000000000000001",
5280 )
5281 .expect("test operation should succeed");
5282
5283 let mut seed = store.transaction();
5286 seed.update(RefUpdate {
5287 name: "refs/heads/topic".into(),
5288 expected: None,
5289 new: RefTarget::Direct(old_topic.clone()),
5290 reflog: None,
5291 });
5292 seed.commit().expect("test operation should succeed");
5293
5294 let mut tx = store.transaction();
5295 tx.update(RefUpdate {
5297 name: "refs/heads/main".into(),
5298 expected: None,
5299 new: RefTarget::Direct(new_main.clone()),
5300 reflog: Some(reflog_entry(&new_main, 0, "create main")),
5301 });
5302 tx.update(RefUpdate {
5304 name: "refs/heads/topic".into(),
5305 expected: Some(RefTarget::Direct(wrong_expected)),
5306 new: RefTarget::Direct(new_main.clone()),
5307 reflog: None,
5308 });
5309 tx.update(RefUpdate {
5311 name: "refs/tags/v1.0".into(),
5312 expected: None,
5313 new: RefTarget::Direct(new_tag),
5314 reflog: None,
5315 });
5316 let result = tx.commit();
5317 assert!(result.is_err());
5318
5319 assert_eq!(
5322 store
5323 .read_ref("refs/heads/main")
5324 .expect("test operation should succeed"),
5325 None
5326 );
5327 assert_eq!(
5328 store
5329 .read_ref("refs/heads/topic")
5330 .expect("test operation should succeed"),
5331 Some(RefTarget::Direct(old_topic))
5332 );
5333 assert_eq!(
5334 store
5335 .read_ref("refs/tags/v1.0")
5336 .expect("test operation should succeed"),
5337 None
5338 );
5339 assert!(
5340 store
5341 .read_reflog("refs/heads/main")
5342 .expect("test operation should succeed")
5343 .is_empty()
5344 );
5345
5346 assert!(
5348 !git_dir
5349 .join("refs")
5350 .join("heads")
5351 .join("main.lock")
5352 .exists()
5353 );
5354 assert!(
5355 !git_dir
5356 .join("refs")
5357 .join("heads")
5358 .join("topic.lock")
5359 .exists()
5360 );
5361 assert!(!git_dir.join("refs").join("tags").join("v1.0.lock").exists());
5362 fs::remove_dir_all(git_dir).expect("test operation should succeed");
5363 }
5364
5365 #[test]
5366 fn file_ref_transaction_mixes_update_and_delete() {
5367 let git_dir = temp_git_dir();
5368 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5369 let old_main = ObjectId::from_hex(
5370 ObjectFormat::Sha1,
5371 "ce013625030ba8dba906f756967f9e9ca394464a",
5372 )
5373 .expect("test operation should succeed");
5374 let new_topic = ObjectId::from_hex(
5375 ObjectFormat::Sha1,
5376 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
5377 )
5378 .expect("test operation should succeed");
5379 let mut seed = store.transaction();
5380 seed.update(RefUpdate {
5381 name: "refs/heads/main".into(),
5382 expected: None,
5383 new: RefTarget::Direct(old_main),
5384 reflog: None,
5385 });
5386 seed.commit().expect("test operation should succeed");
5387
5388 let mut tx = store.transaction();
5389 tx.update(RefUpdate {
5390 name: "refs/heads/topic".into(),
5391 expected: None,
5392 new: RefTarget::Direct(new_topic),
5393 reflog: None,
5394 });
5395 tx.delete_with_precondition(
5396 "refs/heads/main",
5397 RefDeletePrecondition::Direct(Some(old_main)),
5398 None,
5399 );
5400 tx.commit().expect("test operation should succeed");
5401
5402 assert_eq!(
5403 store
5404 .read_ref("refs/heads/main")
5405 .expect("test operation should succeed"),
5406 None
5407 );
5408 assert_eq!(
5409 store
5410 .read_ref("refs/heads/topic")
5411 .expect("test operation should succeed"),
5412 Some(RefTarget::Direct(new_topic))
5413 );
5414 fs::remove_dir_all(git_dir).expect("test operation should succeed");
5415 }
5416
5417 #[test]
5418 fn file_ref_transaction_stale_delete_rolls_back_update() {
5419 let git_dir = temp_git_dir();
5420 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5421 let old_oid = ObjectId::from_hex(
5422 ObjectFormat::Sha1,
5423 "ce013625030ba8dba906f756967f9e9ca394464a",
5424 )
5425 .expect("test operation should succeed");
5426 let new_oid = ObjectId::from_hex(
5427 ObjectFormat::Sha1,
5428 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
5429 )
5430 .expect("test operation should succeed");
5431 let mut seed = store.transaction();
5432 for name in ["refs/heads/main", "refs/heads/topic"] {
5433 seed.update(RefUpdate {
5434 name: name.into(),
5435 expected: None,
5436 new: RefTarget::Direct(old_oid),
5437 reflog: None,
5438 });
5439 }
5440 seed.commit().expect("test operation should succeed");
5441
5442 let mut tx = store.transaction();
5443 tx.update(RefUpdate {
5444 name: "refs/heads/topic".into(),
5445 expected: None,
5446 new: RefTarget::Direct(new_oid),
5447 reflog: None,
5448 });
5449 tx.delete_with_precondition(
5450 "refs/heads/main",
5451 RefDeletePrecondition::Direct(Some(new_oid)),
5452 None,
5453 );
5454 let err = tx.commit().expect_err("stale delete must abort");
5455 assert!(err.to_string().contains("expected ref refs/heads/main"));
5456
5457 assert_eq!(
5458 store
5459 .read_ref("refs/heads/main")
5460 .expect("test operation should succeed"),
5461 Some(RefTarget::Direct(old_oid))
5462 );
5463 assert_eq!(
5464 store
5465 .read_ref("refs/heads/topic")
5466 .expect("test operation should succeed"),
5467 Some(RefTarget::Direct(old_oid))
5468 );
5469 fs::remove_dir_all(git_dir).expect("test operation should succeed");
5470 }
5471
5472 #[test]
5473 fn file_ref_transaction_rejects_duplicate_mixed_ref() {
5474 let git_dir = temp_git_dir();
5475 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5476 let oid = ObjectId::from_hex(
5477 ObjectFormat::Sha1,
5478 "ce013625030ba8dba906f756967f9e9ca394464a",
5479 )
5480 .expect("test operation should succeed");
5481 let mut tx = store.transaction();
5482 tx.update(RefUpdate {
5483 name: "refs/heads/main".into(),
5484 expected: None,
5485 new: RefTarget::Direct(oid),
5486 reflog: None,
5487 });
5488 tx.delete_with_precondition("refs/heads/main", RefDeletePrecondition::Any, None);
5489
5490 let err = tx.commit().expect_err("duplicate ref must fail");
5491 assert!(err.to_string().contains("refs/heads/main"));
5492 assert_eq!(
5493 store
5494 .read_ref("refs/heads/main")
5495 .expect("test operation should succeed"),
5496 None
5497 );
5498 fs::remove_dir_all(git_dir).expect("test operation should succeed");
5499 }
5500
5501 #[test]
5502 fn file_ref_transaction_deletes_symbolic_ref_with_immediate_expectation() {
5503 let git_dir = temp_git_dir();
5504 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5505 let oid = ObjectId::from_hex(
5506 ObjectFormat::Sha1,
5507 "ce013625030ba8dba906f756967f9e9ca394464a",
5508 )
5509 .expect("test operation should succeed");
5510 let mut seed = store.transaction();
5511 seed.update(RefUpdate {
5512 name: "refs/heads/main".into(),
5513 expected: None,
5514 new: RefTarget::Direct(oid),
5515 reflog: None,
5516 });
5517 seed.update(RefUpdate {
5518 name: "refs/aliases/main".into(),
5519 expected: None,
5520 new: RefTarget::Symbolic("refs/heads/main".into()),
5521 reflog: None,
5522 });
5523 seed.commit().expect("test operation should succeed");
5524
5525 let mut tx = store.transaction();
5526 tx.delete_with_precondition(
5527 "refs/aliases/main",
5528 RefDeletePrecondition::Immediate(RefTarget::Symbolic("refs/heads/main".into())),
5529 None,
5530 );
5531 tx.commit().expect("test operation should succeed");
5532
5533 assert_eq!(
5534 store
5535 .read_ref("refs/aliases/main")
5536 .expect("test operation should succeed"),
5537 None
5538 );
5539 assert_eq!(
5540 store
5541 .read_ref("refs/heads/main")
5542 .expect("test operation should succeed"),
5543 Some(RefTarget::Direct(oid))
5544 );
5545 fs::remove_dir_all(git_dir).expect("test operation should succeed");
5546 }
5547
5548 #[test]
5549 fn file_ref_transaction_rolls_back_delete_after_late_write_failure() {
5550 let git_dir = temp_git_dir();
5551 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5552 let old_oid = ObjectId::from_hex(
5553 ObjectFormat::Sha1,
5554 "ce013625030ba8dba906f756967f9e9ca394464a",
5555 )
5556 .expect("test operation should succeed");
5557 let new_oid = ObjectId::from_hex(
5558 ObjectFormat::Sha1,
5559 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
5560 )
5561 .expect("test operation should succeed");
5562 let mut seed = store.transaction();
5563 for name in ["refs/heads/main", "refs/heads/topic"] {
5564 seed.update(RefUpdate {
5565 name: name.into(),
5566 expected: None,
5567 new: RefTarget::Direct(old_oid),
5568 reflog: None,
5569 });
5570 }
5571 seed.commit().expect("test operation should succeed");
5572
5573 set_fail_loose_commit_action_for_test(Some(1));
5574 let mut tx = store.transaction();
5575 tx.delete_with_precondition(
5576 "refs/heads/main",
5577 RefDeletePrecondition::Direct(Some(old_oid)),
5578 None,
5579 );
5580 tx.update(RefUpdate {
5581 name: "refs/heads/topic".into(),
5582 expected: None,
5583 new: RefTarget::Direct(new_oid),
5584 reflog: None,
5585 });
5586 let err = tx.commit().expect_err("injected failure must abort");
5587 assert!(
5588 err.to_string()
5589 .contains("injected loose ref transaction failure")
5590 );
5591
5592 assert_eq!(
5593 store
5594 .read_ref("refs/heads/main")
5595 .expect("test operation should succeed"),
5596 Some(RefTarget::Direct(old_oid))
5597 );
5598 assert_eq!(
5599 store
5600 .read_ref("refs/heads/topic")
5601 .expect("test operation should succeed"),
5602 Some(RefTarget::Direct(old_oid))
5603 );
5604 assert!(
5605 !git_dir
5606 .join("refs")
5607 .join("heads")
5608 .join("main.lock")
5609 .exists()
5610 );
5611 assert!(
5612 !git_dir
5613 .join("refs")
5614 .join("heads")
5615 .join("topic.lock")
5616 .exists()
5617 );
5618 fs::remove_dir_all(git_dir).expect("test operation should succeed");
5619 }
5620
5621 fn temp_git_dir() -> PathBuf {
5622 let path = std::env::temp_dir().join(format!(
5623 "sley-refs-{}-{}",
5624 std::process::id(),
5625 TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
5626 ));
5627 fs::create_dir_all(&path).expect("test operation should succeed");
5628 path
5629 }
5630
5631 fn zero_oid(format: ObjectFormat) -> Result<ObjectId> {
5632 Ok(ObjectId::null(format))
5633 }
5634
5635 fn write_reftable_config(git_dir: &Path) {
5636 fs::write(
5637 git_dir.join("config"),
5638 b"[core]\n\trepositoryformatversion = 1\n[extensions]\n\trefStorage = reftable\n",
5639 )
5640 .expect("test operation should succeed");
5641 }
5642
5643 fn write_reftable_stack(
5644 git_dir: &Path,
5645 tables: &[(&str, Vec<sley_formats::ReftableRefRecord>)],
5646 ) {
5647 let reftable_dir = git_dir.join("reftable");
5648 fs::create_dir_all(&reftable_dir).expect("test operation should succeed");
5649 let mut list = String::new();
5650 for (idx, (name, refs)) in tables.iter().enumerate() {
5651 let update_index = (idx + 1) as u64;
5652 let bytes = sley_formats::Reftable::write_ref_only(
5653 ObjectFormat::Sha1,
5654 update_index,
5655 update_index,
5656 refs,
5657 )
5658 .expect("test operation should succeed");
5659 fs::write(reftable_dir.join(name), bytes).expect("test operation should succeed");
5660 list.push_str(name);
5661 list.push('\n');
5662 }
5663 fs::write(reftable_dir.join("tables.list"), list).expect("test operation should succeed");
5664 }
5665}