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 pub fn append_reflog(&self, name: &str, entry: &ReflogEntry) -> Result<()> {
1469 validate_ref_name_for_read(name)?;
1470 let path = self.reflog_path(name);
1471 let parent = path
1472 .parent()
1473 .ok_or_else(|| GitError::InvalidPath("reflog path has no parent".into()))?;
1474 fs::create_dir_all(parent)?;
1475 let mut file = fs::OpenOptions::new()
1476 .create(true)
1477 .append(true)
1478 .open(path)?;
1479 file.write_all(&entry.to_line())?;
1480 file.sync_all()?;
1481 Ok(())
1482 }
1483
1484 fn ref_path(&self, name: &str) -> PathBuf {
1485 self.ref_base_dir(name).join(name)
1486 }
1487
1488 fn reflog_path(&self, name: &str) -> PathBuf {
1489 self.ref_base_dir(name).join("logs").join(name)
1490 }
1491
1492 fn ref_base_dir(&self, name: &str) -> &Path {
1493 if name == "HEAD" {
1494 &self.git_dir
1495 } else {
1496 &self.common_dir
1497 }
1498 }
1499
1500 fn check_ref_directory_conflict(&self, name: &str) -> Result<()> {
1501 let components = name.split('/').collect::<Vec<_>>();
1502 for index in 1..components.len() {
1503 let ancestor = components[..index].join("/");
1504 if self.read_ref_unchecked(&ancestor)?.is_some() {
1505 return Err(ref_directory_conflict_error(name, &ancestor));
1506 }
1507 }
1508 let child_prefix = format!("{name}/");
1509 for reference in self.list_refs()? {
1510 if reference.name.starts_with(&child_prefix) {
1511 return Err(ref_directory_conflict_error(name, &reference.name));
1512 }
1513 }
1514 Ok(())
1515 }
1516}
1517
1518fn reftable_ref_target(value: ReftableRefValue) -> Result<Option<RefTarget>> {
1519 match value {
1520 ReftableRefValue::Deletion => Ok(None),
1521 ReftableRefValue::Direct(oid) | ReftableRefValue::Peeled { target: oid, .. } => {
1522 Ok(Some(RefTarget::Direct(oid)))
1523 }
1524 ReftableRefValue::Symbolic(target) => Ok(Some(RefTarget::Symbolic(target))),
1525 }
1526}
1527
1528fn reftable_value_from_ref_target(target: &RefTarget) -> ReftableRefValue {
1529 match target {
1530 RefTarget::Direct(oid) => ReftableRefValue::Direct(*oid),
1531 RefTarget::Symbolic(target) => ReftableRefValue::Symbolic(target.clone()),
1532 }
1533}
1534
1535fn reftable_table_name(update_index: u64) -> String {
1536 let nanos = SystemTime::now()
1537 .duration_since(UNIX_EPOCH)
1538 .map(|duration| duration.as_nanos())
1539 .unwrap_or(0);
1540 format!("0x{update_index:012x}-0x{update_index:012x}-sley-{nanos:x}.ref")
1541}
1542
1543fn repository_common_dir(git_dir: &Path) -> PathBuf {
1544 if let Some(common_dir) = std::env::var_os("GIT_COMMON_DIR") {
1545 return PathBuf::from(common_dir);
1546 }
1547 let commondir = git_dir.join("commondir");
1548 if let Ok(value) = fs::read_to_string(&commondir) {
1549 let path = PathBuf::from(value.trim());
1550 let common = if path.is_absolute() {
1551 path
1552 } else {
1553 git_dir.join(path)
1554 };
1555 return fs::canonicalize(&common).unwrap_or(common);
1556 }
1557 git_dir.to_path_buf()
1558}
1559
1560pub struct FileRefTransaction<'a> {
1561 store: &'a FileRefStore,
1562 changes: Vec<QueuedRefChange>,
1563}
1564
1565struct QueuedUpdate {
1568 name: String,
1569 precondition: RefPrecondition,
1570 new: RefTarget,
1571 reflog: Option<ReflogEntry>,
1572}
1573
1574struct QueuedDelete {
1575 name: String,
1576 precondition: RefDeletePrecondition,
1577}
1578
1579enum QueuedRefChange {
1580 Update(QueuedUpdate),
1581 Delete(QueuedDelete),
1582}
1583
1584#[derive(Debug, Clone, PartialEq, Eq)]
1586pub enum RefDeletePrecondition {
1587 Any,
1589 Immediate(RefTarget),
1591 Direct(Option<ObjectId>),
1593 Peeled(ObjectId),
1595}
1596
1597impl<'a> FileRefTransaction<'a> {
1598 pub fn update(&mut self, update: RefUpdate) {
1603 self.changes.push(QueuedRefChange::Update(QueuedUpdate {
1604 name: update.name,
1605 precondition: RefPrecondition::from_expected(update.expected),
1606 new: update.new,
1607 reflog: update.reflog,
1608 }));
1609 }
1610
1611 pub fn update_to(
1617 &mut self,
1618 name: impl Into<String>,
1619 new: RefTarget,
1620 precondition: RefPrecondition,
1621 reflog: Option<ReflogEntry>,
1622 ) {
1623 self.changes.push(QueuedRefChange::Update(QueuedUpdate {
1624 name: name.into(),
1625 precondition,
1626 new,
1627 reflog,
1628 }));
1629 }
1630
1631 pub fn delete(&mut self, delete: DeleteRef) {
1636 self.delete_with_precondition(
1637 delete.name,
1638 RefDeletePrecondition::Direct(delete.expected_old),
1639 delete.reflog,
1640 );
1641 }
1642
1643 pub fn delete_with_precondition(
1649 &mut self,
1650 name: impl Into<String>,
1651 precondition: RefDeletePrecondition,
1652 _reflog: Option<DeleteRefReflog>,
1653 ) {
1654 self.changes.push(QueuedRefChange::Delete(QueuedDelete {
1655 name: name.into(),
1656 precondition,
1657 }));
1658 }
1659
1660 pub fn commit(self) -> Result<()> {
1681 let FileRefTransaction { store, changes } = self;
1682 let changes = coalesce_ref_changes(changes)?;
1683 if store.uses_reftable()? {
1684 return store.commit_reftable(changes);
1685 }
1686 store.commit_loose(changes)
1687 }
1688}
1689
1690impl FileRefStore {
1691 fn commit_reftable(&self, changes: Vec<CoalescedRefChange>) -> Result<()> {
1692 let mut records = Vec::with_capacity(changes.len());
1693 let mut reflogs = Vec::new();
1694 let mut delete_names = Vec::new();
1695 for change in changes {
1696 match change {
1697 CoalescedRefChange::Update(update) => {
1698 if !matches!(update.precondition, RefPrecondition::Any) {
1699 let current = self.read_ref(&update.name)?;
1700 if !update.precondition.is_satisfied_by(current.as_ref()) {
1701 return Err(GitError::Transaction(
1702 update.precondition.describe(&update.name),
1703 ));
1704 }
1705 }
1706 records.push(ReftableRefRecord {
1707 name: update.name.clone(),
1708 update_index: 0,
1709 value: reftable_value_from_ref_target(&update.new),
1710 });
1711 for entry in update.reflog {
1712 reflogs.push((update.name.clone(), entry));
1713 }
1714 }
1715 CoalescedRefChange::Delete(delete) => {
1716 let current = self.read_ref(&delete.name)?;
1717 verify_delete_precondition(
1721 self,
1722 &delete.name,
1723 current.as_ref(),
1724 &delete.precondition,
1725 )?;
1726 records.push(ReftableRefRecord {
1727 name: delete.name.clone(),
1728 update_index: 0,
1729 value: ReftableRefValue::Deletion,
1730 });
1731 delete_names.push(delete.name.clone());
1732 }
1733 }
1734 }
1735 self.append_reftable_records(records)?;
1736 for name in &delete_names {
1740 self.remove_reflog_file(name);
1741 }
1742 for (name, entry) in reflogs {
1743 self.append_reflog(&name, &entry)?;
1744 }
1745 Ok(())
1746 }
1747
1748 fn commit_loose(&self, changes: Vec<CoalescedRefChange>) -> Result<()> {
1751 let has_delete = changes
1752 .iter()
1753 .any(|change| matches!(change, CoalescedRefChange::Delete(_)));
1754 let mut pending = Vec::with_capacity(changes.len() + usize::from(has_delete));
1755 for change in &changes {
1757 let name = change.name();
1758 if matches!(change, CoalescedRefChange::Update(_))
1759 && let Err(err) = self.check_ref_directory_conflict(name)
1760 {
1761 release_pending_locks(&pending);
1762 return Err(err);
1763 }
1764 let path = self.ref_path(name);
1765 let parent = path
1766 .parent()
1767 .ok_or_else(|| GitError::InvalidPath("ref path has no parent".into()))?;
1768 if let Err(err) = fs::create_dir_all(parent) {
1769 release_pending_locks(&pending);
1770 if err.kind() == std::io::ErrorKind::NotADirectory {
1771 return Err(ref_directory_conflict_error(
1772 name,
1773 &parent_to_ref_name(self.ref_base_dir(name), parent),
1774 ));
1775 }
1776 return Err(GitError::Io(err.to_string()));
1777 }
1778 let lock_path = match lock_path_for(&path) {
1779 Ok(lock_path) => lock_path,
1780 Err(err) => {
1781 release_pending_locks(&pending);
1782 return Err(err);
1783 }
1784 };
1785 if let Err(err) = fs::OpenOptions::new()
1786 .write(true)
1787 .create_new(true)
1788 .open(&lock_path)
1789 {
1790 release_pending_locks(&pending);
1791 return Err(GitError::Io(format!("could not lock ref {name}: {err}")));
1792 }
1793 let action = match change {
1794 CoalescedRefChange::Update(update) => PendingPathAction::Write {
1795 contents: write_loose_ref(&Ref {
1796 name: update.name.clone(),
1797 target: update.new.clone(),
1798 }),
1799 },
1800 CoalescedRefChange::Delete(_) => PendingPathAction::Delete,
1801 };
1802 pending.push(PendingPathChange {
1803 name: name.to_string(),
1804 path,
1805 lock_path,
1806 original: None,
1807 action,
1808 });
1809 }
1810
1811 let packed_path = self.common_dir.join("packed-refs");
1812 let mut packed_refs = Vec::new();
1813 if has_delete {
1814 let packed_lock_path = match lock_path_for(&packed_path) {
1815 Ok(lock_path) => lock_path,
1816 Err(err) => {
1817 release_pending_locks(&pending);
1818 return Err(err);
1819 }
1820 };
1821 if let Err(err) = fs::OpenOptions::new()
1822 .write(true)
1823 .create_new(true)
1824 .open(&packed_lock_path)
1825 {
1826 release_pending_locks(&pending);
1827 return Err(GitError::Io(format!("could not lock packed-refs: {err}")));
1828 }
1829 let packed_original = match fs::read(&packed_path) {
1830 Ok(bytes) => Some(bytes),
1831 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
1832 Err(err) => {
1833 release_pending_locks(&pending);
1834 let _ = fs::remove_file(&packed_lock_path);
1835 return Err(GitError::Io(err.to_string()));
1836 }
1837 };
1838 packed_refs = match &packed_original {
1839 Some(bytes) => match parse_packed_refs(self.format, bytes) {
1840 Ok(refs) => refs,
1841 Err(err) => {
1842 release_pending_locks(&pending);
1843 let _ = fs::remove_file(&packed_lock_path);
1844 return Err(err);
1845 }
1846 },
1847 None => Vec::new(),
1848 };
1849 pending.push(PendingPathChange {
1850 name: "packed-refs".into(),
1851 path: packed_path.clone(),
1852 lock_path: packed_lock_path,
1853 original: packed_original,
1854 action: PendingPathAction::ReleaseLock,
1855 });
1856 }
1857
1858 let mut reflogs = Vec::new();
1862 let mut delete_names = BTreeSet::new();
1863 for index in 0..changes.len() {
1864 match &changes[index] {
1865 CoalescedRefChange::Update(update) => {
1866 if !matches!(update.precondition, RefPrecondition::Any) {
1867 let current = if has_delete {
1868 match self.read_ref_from_locked_packed(&update.name, &packed_refs) {
1869 Ok(current) => current,
1870 Err(err) => {
1871 release_pending_locks(&pending);
1872 return Err(err);
1873 }
1874 }
1875 } else {
1876 match self.read_ref(&update.name) {
1877 Ok(current) => current,
1878 Err(err) => {
1879 release_pending_locks(&pending);
1880 return Err(err);
1881 }
1882 }
1883 };
1884 if !update.precondition.is_satisfied_by(current.as_ref()) {
1885 release_pending_locks(&pending);
1886 return Err(GitError::Transaction(
1887 update.precondition.describe(&update.name),
1888 ));
1889 }
1890 }
1891 pending[index].original = match read_optional_file(&pending[index].path) {
1892 Ok(original) => original,
1893 Err(err) => {
1894 release_pending_locks(&pending);
1895 return Err(err);
1896 }
1897 };
1898 for entry in &update.reflog {
1899 reflogs.push((update.name.clone(), entry.clone()));
1900 }
1901 }
1902 CoalescedRefChange::Delete(delete) => {
1903 let state = match self.read_locked_ref_state(&delete.name, &packed_refs) {
1904 Ok(state) => state,
1905 Err(err) => {
1906 release_pending_locks(&pending);
1907 return Err(err);
1908 }
1909 };
1910 if let Err(err) = verify_delete_precondition(
1914 self,
1915 &delete.name,
1916 state.current.as_ref(),
1917 &delete.precondition,
1918 ) {
1919 release_pending_locks(&pending);
1920 return Err(err);
1921 }
1922 pending[index].original = if state.has_loose {
1923 match read_optional_file(&pending[index].path) {
1924 Ok(original) => original,
1925 Err(err) => {
1926 release_pending_locks(&pending);
1927 return Err(err);
1928 }
1929 }
1930 } else {
1931 None
1932 };
1933 delete_names.insert(delete.name.clone());
1934 }
1935 }
1936 }
1937
1938 if has_delete {
1939 let old_len = packed_refs.len();
1940 packed_refs.retain(|reference| !delete_names.contains(&reference.reference.name));
1941 if packed_refs.len() != old_len {
1942 let packed_bytes = match write_packed_refs(&packed_refs) {
1943 Ok(bytes) => bytes,
1944 Err(err) => {
1945 release_pending_locks(&pending);
1946 return Err(err);
1947 }
1948 };
1949 if let Some(packed) = pending.last_mut() {
1950 packed.action = PendingPathAction::Write {
1951 contents: packed_bytes,
1952 };
1953 }
1954 }
1955 }
1956
1957 for change in &pending {
1960 if let Err(err) = stage_pending_change(change) {
1961 release_pending_locks(&pending);
1962 return Err(err);
1963 }
1964 }
1965
1966 for index in 0..pending.len() {
1969 if let Err(err) = maybe_fail_loose_commit_action(index) {
1970 rollback_after_apply(&pending, index);
1971 return Err(err);
1972 }
1973 if let Err(err) = apply_pending_change(&pending[index]) {
1974 rollback_after_apply(&pending, index + 1);
1975 return Err(err);
1976 }
1977 }
1978
1979 for change in &pending {
1980 if matches!(change.action, PendingPathAction::Delete) && change.original.is_some() {
1981 self.prune_empty_ref_dirs(&change.name);
1982 }
1983 }
1984 for name in &delete_names {
1988 self.remove_reflog_file(name);
1989 }
1990 for (name, entry) in reflogs {
1992 self.append_reflog(&name, &entry)?;
1993 }
1994 Ok(())
1995 }
1996
1997 fn read_ref_from_locked_packed(
1998 &self,
1999 name: &str,
2000 packed_refs: &[PackedRef],
2001 ) -> Result<Option<RefTarget>> {
2002 let state = self.read_locked_ref_state(name, packed_refs)?;
2003 Ok(state.current)
2004 }
2005
2006 fn read_locked_ref_state(
2007 &self,
2008 name: &str,
2009 packed_refs: &[PackedRef],
2010 ) -> Result<LockedRefState> {
2011 let loose = self.read_loose_ref(name)?;
2012 let packed_index = packed_refs
2013 .iter()
2014 .position(|reference| reference.reference.name == name);
2015 let current = if let Some(reference) = loose.as_ref() {
2016 Some(reference.target.clone())
2017 } else {
2018 packed_index.map(|index| packed_refs[index].reference.target.clone())
2019 };
2020 Ok(LockedRefState {
2021 current,
2022 has_loose: loose.is_some(),
2023 })
2024 }
2025}
2026
2027struct LockedRefState {
2028 current: Option<RefTarget>,
2029 has_loose: bool,
2030}
2031
2032enum CoalescedRefChange {
2033 Update(CoalescedRefUpdate),
2034 Delete(CoalescedRefDelete),
2035}
2036
2037impl CoalescedRefChange {
2038 fn name(&self) -> &str {
2039 match self {
2040 Self::Update(update) => &update.name,
2041 Self::Delete(delete) => &delete.name,
2042 }
2043 }
2044}
2045
2046struct CoalescedRefUpdate {
2048 name: String,
2049 precondition: RefPrecondition,
2050 new: RefTarget,
2051 reflog: Vec<ReflogEntry>,
2052}
2053
2054struct CoalescedRefDelete {
2055 name: String,
2056 precondition: RefDeletePrecondition,
2057}
2058
2059fn coalesce_ref_changes(changes: Vec<QueuedRefChange>) -> Result<Vec<CoalescedRefChange>> {
2060 let has_delete = changes
2061 .iter()
2062 .any(|change| matches!(change, QueuedRefChange::Delete(_)));
2063 if !has_delete {
2064 let updates = changes
2065 .into_iter()
2066 .map(|change| match change {
2067 QueuedRefChange::Update(update) => update,
2068 QueuedRefChange::Delete(_) => unreachable!("has_delete was false"),
2069 })
2070 .collect::<Vec<_>>();
2071 return coalesce_ref_updates(updates).map(|updates| {
2072 updates
2073 .into_iter()
2074 .map(CoalescedRefChange::Update)
2075 .collect()
2076 });
2077 }
2078
2079 let mut seen = BTreeSet::new();
2080 let mut coalesced = Vec::with_capacity(changes.len());
2081 for change in changes {
2082 let name = match &change {
2083 QueuedRefChange::Update(update) => &update.name,
2084 QueuedRefChange::Delete(delete) => &delete.name,
2085 };
2086 validate_ref_name_for_update(name)?;
2087 if !seen.insert(name.clone()) {
2088 return Err(GitError::Transaction(format!(
2089 "ref {name} appears more than once in transaction"
2090 )));
2091 }
2092 coalesced.push(match change {
2093 QueuedRefChange::Update(update) => CoalescedRefChange::Update(CoalescedRefUpdate {
2094 name: update.name,
2095 precondition: update.precondition,
2096 new: update.new,
2097 reflog: update.reflog.into_iter().collect(),
2098 }),
2099 QueuedRefChange::Delete(delete) => CoalescedRefChange::Delete(CoalescedRefDelete {
2100 name: delete.name,
2101 precondition: delete.precondition,
2102 }),
2103 });
2104 }
2105 Ok(coalesced)
2106}
2107
2108fn coalesce_ref_updates(updates: Vec<QueuedUpdate>) -> Result<Vec<CoalescedRefUpdate>> {
2113 let mut order: Vec<String> = Vec::new();
2114 let mut by_name: HashMap<String, CoalescedRefUpdate> = HashMap::new();
2115 for update in updates {
2116 validate_ref_name_for_update(&update.name)?;
2117 match by_name.get_mut(&update.name) {
2118 Some(existing) => {
2119 existing.new = update.new;
2120 if let Some(entry) = update.reflog {
2121 existing.reflog.push(entry);
2122 }
2123 }
2124 None => {
2125 order.push(update.name.clone());
2126 by_name.insert(
2127 update.name.clone(),
2128 CoalescedRefUpdate {
2129 name: update.name,
2130 precondition: update.precondition,
2131 new: update.new,
2132 reflog: update.reflog.into_iter().collect(),
2133 },
2134 );
2135 }
2136 }
2137 }
2138 let mut coalesced = Vec::with_capacity(order.len());
2139 for name in order {
2140 if let Some(update) = by_name.remove(&name) {
2141 coalesced.push(update);
2142 }
2143 }
2144 Ok(coalesced)
2145}
2146
2147struct PendingPathChange {
2150 name: String,
2151 path: PathBuf,
2152 lock_path: PathBuf,
2153 original: Option<Vec<u8>>,
2154 action: PendingPathAction,
2155}
2156
2157enum PendingPathAction {
2158 Write { contents: Vec<u8> },
2159 Delete,
2160 ReleaseLock,
2161}
2162
2163struct RefDirPruneGuard<'a> {
2164 store: &'a FileRefStore,
2165 name: String,
2166}
2167
2168impl Drop for RefDirPruneGuard<'_> {
2169 fn drop(&mut self) {
2170 self.store.prune_empty_ref_dirs(&self.name);
2171 }
2172}
2173
2174struct DeleteLock {
2175 path: PathBuf,
2176 file: Option<fs::File>,
2177 active: bool,
2178}
2179
2180impl DeleteLock {
2181 fn acquire(path: PathBuf) -> std::result::Result<Self, RefDeleteError> {
2182 match fs::OpenOptions::new()
2183 .write(true)
2184 .create_new(true)
2185 .open(&path)
2186 {
2187 Ok(file) => Ok(Self {
2188 path,
2189 file: Some(file),
2190 active: true,
2191 }),
2192 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
2193 Err(RefDeleteError::Locked)
2194 }
2195 Err(err) => Err(RefDeleteError::Io(err)),
2196 }
2197 }
2198
2199 fn write_all(&mut self, bytes: &[u8]) -> std::result::Result<(), RefDeleteError> {
2200 let Some(file) = self.file.as_mut() else {
2201 return Err(RefDeleteError::Io(std::io::Error::other(
2202 "lock file is already closed",
2203 )));
2204 };
2205 file.set_len(0)?;
2206 file.write_all(bytes)?;
2207 file.sync_all()?;
2208 Ok(())
2209 }
2210
2211 fn close(mut self) -> PathBuf {
2212 self.active = false;
2213 let _ = self.file.take();
2214 self.path.clone()
2215 }
2216
2217 fn remove(mut self) {
2218 self.active = false;
2219 let _ = self.file.take();
2220 let _ = fs::remove_file(&self.path);
2221 }
2222}
2223
2224impl Drop for DeleteLock {
2225 fn drop(&mut self) {
2226 if self.active {
2227 let _ = self.file.take();
2228 let _ = fs::remove_file(&self.path);
2229 }
2230 }
2231}
2232
2233fn checked_delete_oid(
2234 expected: Option<ObjectId>,
2235 current: Option<RefTarget>,
2236) -> std::result::Result<ObjectId, RefDeleteError> {
2237 let Some(current) = current else {
2238 return Err(RefDeleteError::NotFound);
2239 };
2240 let RefTarget::Direct(actual) = current else {
2241 return Err(RefDeleteError::ExpectedMismatch {
2242 expected,
2243 actual: None,
2244 });
2245 };
2246 if let Some(expected_oid) = expected
2247 && expected_oid != actual
2248 {
2249 return Err(RefDeleteError::ExpectedMismatch {
2250 expected: Some(expected_oid),
2251 actual: Some(actual),
2252 });
2253 }
2254 Ok(actual)
2255}
2256
2257fn verify_delete_precondition(
2263 store: &FileRefStore,
2264 name: &str,
2265 current: Option<&RefTarget>,
2266 precondition: &RefDeletePrecondition,
2267) -> Result<()> {
2268 let Some(current) = current else {
2269 return Err(GitError::Transaction(format!("ref {name} not found")));
2270 };
2271 match precondition {
2272 RefDeletePrecondition::Any => {
2273 peeled_oid_for_delete(store, current)?;
2274 Ok(())
2275 }
2276 RefDeletePrecondition::Immediate(expected) if current == expected => {
2277 peeled_oid_for_delete(store, current)?;
2278 Ok(())
2279 }
2280 RefDeletePrecondition::Immediate(_) => Err(delete_precondition_mismatch(name)),
2281 RefDeletePrecondition::Direct(expected) => {
2282 let RefTarget::Direct(actual) = current else {
2283 return Err(delete_precondition_mismatch(name));
2284 };
2285 if let Some(expected) = expected
2286 && expected != actual
2287 {
2288 return Err(delete_precondition_mismatch(name));
2289 }
2290 Ok(())
2291 }
2292 RefDeletePrecondition::Peeled(expected) => {
2293 let actual = peeled_oid_for_delete(store, current)?;
2294 if actual == Some(*expected) {
2295 Ok(())
2296 } else {
2297 Err(delete_precondition_mismatch(name))
2298 }
2299 }
2300 }
2301}
2302
2303fn peeled_oid_for_delete(store: &FileRefStore, target: &RefTarget) -> Result<Option<ObjectId>> {
2304 match target {
2305 RefTarget::Direct(oid) => Ok(Some(*oid)),
2306 RefTarget::Symbolic(name) => resolve_ref_peeled(store, name),
2307 }
2308}
2309
2310fn delete_precondition_mismatch(name: &str) -> GitError {
2311 GitError::Transaction(format!("expected ref {name} to match"))
2312}
2313
2314fn ref_delete_error_from_git(err: GitError) -> RefDeleteError {
2315 match err {
2316 GitError::InvalidPath(_) => RefDeleteError::InvalidName,
2317 GitError::NotFound(_) => RefDeleteError::NotFound,
2318 GitError::Io(message) if message.contains("File exists") => RefDeleteError::Locked,
2319 GitError::Io(message) if message.contains("could not lock") => RefDeleteError::Locked,
2320 GitError::Transaction(message) if message.contains("could not lock") => {
2321 RefDeleteError::Locked
2322 }
2323 other => RefDeleteError::Io(std::io::Error::other(other.to_string())),
2324 }
2325}
2326
2327fn read_optional_file(path: &Path) -> Result<Option<Vec<u8>>> {
2328 match fs::read(path) {
2329 Ok(bytes) => Ok(Some(bytes)),
2330 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
2331 Err(err) => Err(GitError::Io(err.to_string())),
2332 }
2333}
2334
2335fn stage_lock_file(lock_path: &Path, contents: &[u8]) -> Result<()> {
2336 let mut file = fs::OpenOptions::new()
2337 .write(true)
2338 .truncate(true)
2339 .open(lock_path)?;
2340 file.write_all(contents)?;
2341 file.sync_all()?;
2342 Ok(())
2343}
2344
2345fn stage_pending_change(change: &PendingPathChange) -> Result<()> {
2346 match &change.action {
2347 PendingPathAction::Write { contents } => stage_lock_file(&change.lock_path, contents),
2348 PendingPathAction::Delete => stage_lock_file(&change.lock_path, b"delete\n"),
2349 PendingPathAction::ReleaseLock => Ok(()),
2350 }
2351}
2352
2353fn apply_pending_change(change: &PendingPathChange) -> Result<()> {
2354 match &change.action {
2355 PendingPathAction::Write { .. } => {
2356 fs::rename(&change.lock_path, &change.path).map_err(|err| GitError::Io(err.to_string()))
2357 }
2358 PendingPathAction::Delete => {
2359 if change.original.is_some() {
2360 fs::remove_file(&change.path).map_err(|err| GitError::Io(err.to_string()))?;
2361 }
2362 fs::remove_file(&change.lock_path).map_err(|err| GitError::Io(err.to_string()))
2363 }
2364 PendingPathAction::ReleaseLock => {
2365 fs::remove_file(&change.lock_path).map_err(|err| GitError::Io(err.to_string()))
2366 }
2367 }
2368}
2369
2370fn release_pending_locks(pending: &[PendingPathChange]) {
2373 for change in pending {
2374 let _ = fs::remove_file(&change.lock_path);
2375 }
2376}
2377
2378fn rollback_after_apply(pending: &[PendingPathChange], applied: usize) {
2382 for change in pending.iter().take(applied) {
2383 if matches!(change.action, PendingPathAction::ReleaseLock) {
2384 let _ = fs::remove_file(&change.lock_path);
2385 continue;
2386 }
2387 match &change.original {
2388 Some(bytes) => {
2389 let _ = restore_file_atomically(&change.path, bytes);
2390 }
2391 None => {
2392 let _ = fs::remove_file(&change.path);
2393 }
2394 }
2395 let _ = fs::remove_file(&change.lock_path);
2396 }
2397 for change in pending.iter().skip(applied) {
2398 let _ = fs::remove_file(&change.lock_path);
2399 }
2400}
2401
2402#[cfg(test)]
2403thread_local! {
2404 static FAIL_LOOSE_COMMIT_ACTION: std::cell::Cell<Option<usize>> =
2405 const { std::cell::Cell::new(None) };
2406}
2407
2408#[cfg(test)]
2409fn set_fail_loose_commit_action_for_test(index: Option<usize>) {
2410 FAIL_LOOSE_COMMIT_ACTION.with(|cell| cell.set(index));
2411}
2412
2413#[cfg(test)]
2414fn maybe_fail_loose_commit_action(index: usize) -> Result<()> {
2415 let should_fail = FAIL_LOOSE_COMMIT_ACTION.with(|cell| cell.get() == Some(index));
2416 if should_fail {
2417 FAIL_LOOSE_COMMIT_ACTION.with(|cell| cell.set(None));
2418 return Err(GitError::Io(format!(
2419 "injected loose ref transaction failure at action {index}"
2420 )));
2421 }
2422 Ok(())
2423}
2424
2425#[cfg(not(test))]
2426fn maybe_fail_loose_commit_action(_index: usize) -> Result<()> {
2427 Ok(())
2428}
2429
2430fn restore_file_atomically(path: &Path, bytes: &[u8]) -> Result<()> {
2433 write_locked(path, bytes)
2434}
2435
2436#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2437pub struct FullRefName<'a> {
2438 name: &'a str,
2439}
2440
2441impl<'a> FullRefName<'a> {
2442 pub fn new(name: &'a str) -> Result<Self> {
2443 validate_ref_name(name)?;
2444 Ok(Self { name })
2445 }
2446
2447 pub fn as_str(&self) -> &str {
2448 self.name
2449 }
2450
2451 pub fn into_str(self) -> &'a str {
2452 self.name
2453 }
2454
2455 pub fn to_owned(&self) -> FullRefNameBuf {
2456 FullRefNameBuf {
2457 name: self.name.to_string(),
2458 }
2459 }
2460
2461 pub fn as_branch(&self) -> Result<BranchRefName<'a>> {
2462 BranchRefName::from_full_ref(*self)
2463 }
2464
2465 pub fn as_tag(&self) -> Result<TagRefName<'a>> {
2466 TagRefName::from_full_ref(*self)
2467 }
2468
2469 pub fn as_remote(&self) -> Result<RemoteRefName<'a>> {
2470 RemoteRefName::from_full_ref(*self)
2471 }
2472}
2473
2474impl AsRef<str> for FullRefName<'_> {
2475 fn as_ref(&self) -> &str {
2476 self.as_str()
2477 }
2478}
2479
2480impl fmt::Display for FullRefName<'_> {
2481 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2482 f.write_str(self.as_str())
2483 }
2484}
2485
2486#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2487pub struct FullRefNameBuf {
2488 name: String,
2489}
2490
2491impl FullRefNameBuf {
2492 pub fn new(name: impl Into<String>) -> Result<Self> {
2493 let name = name.into();
2494 validate_ref_name(&name)?;
2495 Ok(Self { name })
2496 }
2497
2498 pub fn as_ref_name(&self) -> FullRefName<'_> {
2499 FullRefName { name: &self.name }
2500 }
2501
2502 pub fn as_str(&self) -> &str {
2503 &self.name
2504 }
2505
2506 pub fn into_string(self) -> String {
2507 self.name
2508 }
2509
2510 pub fn as_branch(&self) -> Result<BranchRefName<'_>> {
2511 self.as_ref_name().as_branch()
2512 }
2513
2514 pub fn as_tag(&self) -> Result<TagRefName<'_>> {
2515 self.as_ref_name().as_tag()
2516 }
2517
2518 pub fn as_remote(&self) -> Result<RemoteRefName<'_>> {
2519 self.as_ref_name().as_remote()
2520 }
2521}
2522
2523impl AsRef<str> for FullRefNameBuf {
2524 fn as_ref(&self) -> &str {
2525 self.as_str()
2526 }
2527}
2528
2529impl Borrow<str> for FullRefNameBuf {
2530 fn borrow(&self) -> &str {
2531 self.as_str()
2532 }
2533}
2534
2535impl Deref for FullRefNameBuf {
2536 type Target = str;
2537
2538 fn deref(&self) -> &Self::Target {
2539 self.as_str()
2540 }
2541}
2542
2543impl fmt::Display for FullRefNameBuf {
2544 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2545 f.write_str(self.as_str())
2546 }
2547}
2548
2549#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2550pub struct BranchRefName<'a> {
2551 name: &'a str,
2552}
2553
2554impl<'a> BranchRefName<'a> {
2555 pub const PREFIX: &'static str = "refs/heads/";
2556
2557 pub fn from_full(name: &'a str) -> Result<Self> {
2558 let full = FullRefName::new(name)?;
2559 Self::from_full_ref(full)
2560 }
2561
2562 pub fn from_full_ref(name: FullRefName<'a>) -> Result<Self> {
2563 validate_namespaced_ref(name.as_str(), Self::PREFIX, "branch")?;
2564 Ok(Self {
2565 name: name.into_str(),
2566 })
2567 }
2568
2569 pub fn as_full_ref_name(&self) -> FullRefName<'a> {
2570 FullRefName { name: self.name }
2571 }
2572
2573 pub fn as_str(&self) -> &str {
2574 self.name
2575 }
2576
2577 pub fn branch_name(&self) -> &str {
2578 self.short_name()
2579 }
2580
2581 pub fn short_name(&self) -> &str {
2582 &self.name[Self::PREFIX.len()..]
2583 }
2584
2585 pub fn into_str(self) -> &'a str {
2586 self.name
2587 }
2588
2589 pub fn to_owned(&self) -> BranchRefNameBuf {
2590 BranchRefNameBuf {
2591 name: self.name.to_string(),
2592 }
2593 }
2594}
2595
2596impl AsRef<str> for BranchRefName<'_> {
2597 fn as_ref(&self) -> &str {
2598 self.as_str()
2599 }
2600}
2601
2602impl fmt::Display for BranchRefName<'_> {
2603 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2604 f.write_str(self.as_str())
2605 }
2606}
2607
2608impl<'a> From<BranchRefName<'a>> for FullRefName<'a> {
2609 fn from(name: BranchRefName<'a>) -> Self {
2610 name.as_full_ref_name()
2611 }
2612}
2613
2614#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2615pub struct BranchRefNameBuf {
2616 name: String,
2617}
2618
2619impl BranchRefNameBuf {
2620 pub fn from_branch_name(branch: &str) -> Result<Self> {
2621 validate_short_ref_name("branch", branch)?;
2622 let name = format!("{}{}", BranchRefName::PREFIX, branch);
2623 Self::from_full(name)
2624 }
2625
2626 pub fn from_full(name: impl Into<String>) -> Result<Self> {
2627 let name = name.into();
2628 BranchRefName::from_full(&name)?;
2629 Ok(Self { name })
2630 }
2631
2632 pub fn as_ref_name(&self) -> BranchRefName<'_> {
2633 BranchRefName { name: &self.name }
2634 }
2635
2636 pub fn as_full_ref_name(&self) -> FullRefName<'_> {
2637 FullRefName { name: &self.name }
2638 }
2639
2640 pub fn as_str(&self) -> &str {
2641 &self.name
2642 }
2643
2644 pub fn branch_name(&self) -> &str {
2645 self.short_name()
2646 }
2647
2648 pub fn short_name(&self) -> &str {
2649 &self.name[BranchRefName::PREFIX.len()..]
2650 }
2651
2652 pub fn into_string(self) -> String {
2653 self.name
2654 }
2655}
2656
2657impl AsRef<str> for BranchRefNameBuf {
2658 fn as_ref(&self) -> &str {
2659 self.as_str()
2660 }
2661}
2662
2663impl Borrow<str> for BranchRefNameBuf {
2664 fn borrow(&self) -> &str {
2665 self.as_str()
2666 }
2667}
2668
2669impl Deref for BranchRefNameBuf {
2670 type Target = str;
2671
2672 fn deref(&self) -> &Self::Target {
2673 self.as_str()
2674 }
2675}
2676
2677impl fmt::Display for BranchRefNameBuf {
2678 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2679 f.write_str(self.as_str())
2680 }
2681}
2682
2683impl From<BranchRefNameBuf> for FullRefNameBuf {
2684 fn from(name: BranchRefNameBuf) -> Self {
2685 Self { name: name.name }
2686 }
2687}
2688
2689#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2690pub struct TagRefName<'a> {
2691 name: &'a str,
2692}
2693
2694impl<'a> TagRefName<'a> {
2695 pub const PREFIX: &'static str = "refs/tags/";
2696
2697 pub fn from_full(name: &'a str) -> Result<Self> {
2698 let full = FullRefName::new(name)?;
2699 Self::from_full_ref(full)
2700 }
2701
2702 pub fn from_full_ref(name: FullRefName<'a>) -> Result<Self> {
2703 validate_namespaced_ref(name.as_str(), Self::PREFIX, "tag")?;
2704 Ok(Self {
2705 name: name.into_str(),
2706 })
2707 }
2708
2709 pub fn as_full_ref_name(&self) -> FullRefName<'a> {
2710 FullRefName { name: self.name }
2711 }
2712
2713 pub fn as_str(&self) -> &str {
2714 self.name
2715 }
2716
2717 pub fn tag_name(&self) -> &str {
2718 self.short_name()
2719 }
2720
2721 pub fn short_name(&self) -> &str {
2722 &self.name[Self::PREFIX.len()..]
2723 }
2724
2725 pub fn into_str(self) -> &'a str {
2726 self.name
2727 }
2728
2729 pub fn to_owned(&self) -> TagRefNameBuf {
2730 TagRefNameBuf {
2731 name: self.name.to_string(),
2732 }
2733 }
2734}
2735
2736impl AsRef<str> for TagRefName<'_> {
2737 fn as_ref(&self) -> &str {
2738 self.as_str()
2739 }
2740}
2741
2742impl fmt::Display for TagRefName<'_> {
2743 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2744 f.write_str(self.as_str())
2745 }
2746}
2747
2748impl<'a> From<TagRefName<'a>> for FullRefName<'a> {
2749 fn from(name: TagRefName<'a>) -> Self {
2750 name.as_full_ref_name()
2751 }
2752}
2753
2754#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2755pub struct TagRefNameBuf {
2756 name: String,
2757}
2758
2759impl TagRefNameBuf {
2760 pub fn from_tag_name(tag: &str) -> Result<Self> {
2761 if tag.starts_with('-') || tag == "HEAD" {
2764 return Err(GitError::InvalidPath(format!("invalid tag name {tag}")));
2765 }
2766 Self::from_tag_name_unrestricted(tag)
2767 }
2768
2769 pub fn from_tag_name_unrestricted(tag: &str) -> Result<Self> {
2774 let name = format!("{}{}", TagRefName::PREFIX, tag);
2775 check_refname_format(&name, false)?;
2776 Ok(Self { name })
2777 }
2778
2779 pub fn from_full(name: impl Into<String>) -> Result<Self> {
2780 let name = name.into();
2781 TagRefName::from_full(&name)?;
2782 Ok(Self { name })
2783 }
2784
2785 pub fn as_ref_name(&self) -> TagRefName<'_> {
2786 TagRefName { name: &self.name }
2787 }
2788
2789 pub fn as_full_ref_name(&self) -> FullRefName<'_> {
2790 FullRefName { name: &self.name }
2791 }
2792
2793 pub fn as_str(&self) -> &str {
2794 &self.name
2795 }
2796
2797 pub fn tag_name(&self) -> &str {
2798 self.short_name()
2799 }
2800
2801 pub fn short_name(&self) -> &str {
2802 &self.name[TagRefName::PREFIX.len()..]
2803 }
2804
2805 pub fn into_string(self) -> String {
2806 self.name
2807 }
2808}
2809
2810impl AsRef<str> for TagRefNameBuf {
2811 fn as_ref(&self) -> &str {
2812 self.as_str()
2813 }
2814}
2815
2816impl Borrow<str> for TagRefNameBuf {
2817 fn borrow(&self) -> &str {
2818 self.as_str()
2819 }
2820}
2821
2822impl Deref for TagRefNameBuf {
2823 type Target = str;
2824
2825 fn deref(&self) -> &Self::Target {
2826 self.as_str()
2827 }
2828}
2829
2830impl fmt::Display for TagRefNameBuf {
2831 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2832 f.write_str(self.as_str())
2833 }
2834}
2835
2836impl From<TagRefNameBuf> for FullRefNameBuf {
2837 fn from(name: TagRefNameBuf) -> Self {
2838 Self { name: name.name }
2839 }
2840}
2841
2842#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2843pub struct RemoteRefName<'a> {
2844 name: &'a str,
2845}
2846
2847impl<'a> RemoteRefName<'a> {
2848 pub const PREFIX: &'static str = "refs/remotes/";
2849
2850 pub fn from_full(name: &'a str) -> Result<Self> {
2851 let full = FullRefName::new(name)?;
2852 Self::from_full_ref(full)
2853 }
2854
2855 pub fn from_full_ref(name: FullRefName<'a>) -> Result<Self> {
2856 validate_namespaced_ref(name.as_str(), Self::PREFIX, "remote")?;
2857 Ok(Self {
2858 name: name.into_str(),
2859 })
2860 }
2861
2862 pub fn as_full_ref_name(&self) -> FullRefName<'a> {
2863 FullRefName { name: self.name }
2864 }
2865
2866 pub fn as_str(&self) -> &str {
2867 self.name
2868 }
2869
2870 pub fn short_name(&self) -> &str {
2871 &self.name[Self::PREFIX.len()..]
2872 }
2873
2874 pub fn remote_name(&self) -> &str {
2875 match self.short_name().split_once('/') {
2876 Some((remote, _branch)) => remote,
2877 None => self.short_name(),
2878 }
2879 }
2880
2881 pub fn remote_branch(&self) -> Option<&str> {
2882 self.short_name()
2883 .split_once('/')
2884 .map(|(_remote, branch)| branch)
2885 }
2886
2887 pub fn into_str(self) -> &'a str {
2888 self.name
2889 }
2890
2891 pub fn to_owned(&self) -> RemoteRefNameBuf {
2892 RemoteRefNameBuf {
2893 name: self.name.to_string(),
2894 }
2895 }
2896}
2897
2898impl AsRef<str> for RemoteRefName<'_> {
2899 fn as_ref(&self) -> &str {
2900 self.as_str()
2901 }
2902}
2903
2904impl fmt::Display for RemoteRefName<'_> {
2905 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2906 f.write_str(self.as_str())
2907 }
2908}
2909
2910impl<'a> From<RemoteRefName<'a>> for FullRefName<'a> {
2911 fn from(name: RemoteRefName<'a>) -> Self {
2912 name.as_full_ref_name()
2913 }
2914}
2915
2916#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2917pub struct RemoteRefNameBuf {
2918 name: String,
2919}
2920
2921impl RemoteRefNameBuf {
2922 pub fn from_short_name(name: &str) -> Result<Self> {
2923 validate_short_ref_name("remote ref", name)?;
2924 let name = format!("{}{}", RemoteRefName::PREFIX, name);
2925 Self::from_full(name)
2926 }
2927
2928 pub fn from_remote_branch(remote: &str, branch: &str) -> Result<Self> {
2929 validate_remote_name(remote)?;
2930 validate_short_ref_name("remote branch", branch)?;
2931 let name = format!("{}{}/{}", RemoteRefName::PREFIX, remote, branch);
2932 Self::from_full(name)
2933 }
2934
2935 pub fn from_full(name: impl Into<String>) -> Result<Self> {
2936 let name = name.into();
2937 RemoteRefName::from_full(&name)?;
2938 Ok(Self { name })
2939 }
2940
2941 pub fn as_ref_name(&self) -> RemoteRefName<'_> {
2942 RemoteRefName { name: &self.name }
2943 }
2944
2945 pub fn as_full_ref_name(&self) -> FullRefName<'_> {
2946 FullRefName { name: &self.name }
2947 }
2948
2949 pub fn as_str(&self) -> &str {
2950 &self.name
2951 }
2952
2953 pub fn short_name(&self) -> &str {
2954 &self.name[RemoteRefName::PREFIX.len()..]
2955 }
2956
2957 pub fn remote_name(&self) -> &str {
2958 match self.short_name().split_once('/') {
2959 Some((remote, _branch)) => remote,
2960 None => self.short_name(),
2961 }
2962 }
2963
2964 pub fn remote_branch(&self) -> Option<&str> {
2965 self.short_name()
2966 .split_once('/')
2967 .map(|(_remote, branch)| branch)
2968 }
2969
2970 pub fn into_string(self) -> String {
2971 self.name
2972 }
2973}
2974
2975impl AsRef<str> for RemoteRefNameBuf {
2976 fn as_ref(&self) -> &str {
2977 self.as_str()
2978 }
2979}
2980
2981impl Borrow<str> for RemoteRefNameBuf {
2982 fn borrow(&self) -> &str {
2983 self.as_str()
2984 }
2985}
2986
2987impl Deref for RemoteRefNameBuf {
2988 type Target = str;
2989
2990 fn deref(&self) -> &Self::Target {
2991 self.as_str()
2992 }
2993}
2994
2995impl fmt::Display for RemoteRefNameBuf {
2996 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2997 f.write_str(self.as_str())
2998 }
2999}
3000
3001impl From<RemoteRefNameBuf> for FullRefNameBuf {
3002 fn from(name: RemoteRefNameBuf) -> Self {
3003 Self { name: name.name }
3004 }
3005}
3006
3007pub fn branch_ref_name(branch: &str) -> Result<String> {
3008 BranchRefNameBuf::from_branch_name(branch).map(BranchRefNameBuf::into_string)
3009}
3010
3011pub fn tag_ref_name(tag: &str) -> Result<String> {
3012 TagRefNameBuf::from_tag_name(tag).map(TagRefNameBuf::into_string)
3013}
3014
3015fn write_locked(path: &Path, bytes: &[u8]) -> Result<()> {
3016 let lock_path = lock_path_for(path)?;
3017 {
3018 let mut file = fs::OpenOptions::new()
3019 .write(true)
3020 .create_new(true)
3021 .open(&lock_path)?;
3022 file.write_all(bytes)?;
3023 file.sync_all()?;
3024 }
3025 match fs::rename(&lock_path, path) {
3026 Ok(()) => Ok(()),
3027 Err(err) => {
3028 let _ = fs::remove_file(lock_path);
3029 Err(GitError::Io(err.to_string()))
3030 }
3031 }
3032}
3033
3034fn lock_path_for(path: &Path) -> Result<PathBuf> {
3035 let file_name = path
3036 .file_name()
3037 .ok_or_else(|| GitError::InvalidPath("ref path has no filename".into()))?;
3038 let mut lock_name = file_name.to_os_string();
3039 lock_name.push(".lock");
3040 Ok(path.with_file_name(lock_name))
3041}
3042
3043pub fn check_refname_format(name: &str, allow_onelevel: bool) -> Result<()> {
3045 if name.is_empty()
3046 || name == "@"
3047 || name.starts_with('/')
3048 || name.ends_with('/')
3049 || name.ends_with('.')
3050 || name.contains("..")
3051 || name.contains("//")
3052 || name.contains("@{")
3053 || (!allow_onelevel && !name.contains('/'))
3054 {
3055 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
3056 }
3057 for component in name.split('/') {
3058 if component.is_empty() || component.starts_with('.') || component.ends_with(".lock") {
3059 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
3060 }
3061 for (idx, byte) in component.bytes().enumerate() {
3062 if byte <= b' '
3063 || byte == 0x7f
3064 || matches!(byte, b'~' | b'^' | b':' | b'?' | b'*' | b'[' | b'\\')
3065 {
3066 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
3067 }
3068 if byte == b'.' && component.as_bytes().get(idx + 1) == Some(&b'.') {
3069 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
3070 }
3071 if byte == b'@' && component.as_bytes().get(idx + 1) == Some(&b'{') {
3072 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
3073 }
3074 }
3075 }
3076 Ok(())
3077}
3078
3079pub fn validate_symref_name(name: &str) -> Result<()> {
3081 if name == "HEAD" {
3082 return Ok(());
3083 }
3084 check_refname_format(name, true)
3085}
3086
3087pub fn validate_symref_target(name: &str) -> Result<()> {
3089 check_refname_format(name, true)
3090}
3091
3092fn prune_empty_dirs_up_to(start: &Path, boundary: &Path) {
3097 let mut dir = start.to_path_buf();
3098 while dir.starts_with(boundary) && dir != *boundary {
3099 if fs::remove_dir(&dir).is_err() {
3100 break;
3101 }
3102 dir = match dir.parent() {
3103 Some(parent) => parent.to_path_buf(),
3104 None => break,
3105 };
3106 }
3107}
3108
3109pub fn resolve_ref_peeled(store: &FileRefStore, name: &str) -> Result<Option<ObjectId>> {
3110 let mut current = name.to_string();
3111 for _ in 0..16 {
3112 match store.read_ref(¤t)? {
3113 Some(RefTarget::Direct(oid)) => return Ok(Some(oid)),
3114 Some(RefTarget::Symbolic(next)) => current = next,
3115 None => return Ok(None),
3116 }
3117 }
3118 Ok(None)
3119}
3120
3121fn validate_ref_name_for_read(name: &str) -> Result<()> {
3122 if validate_ref_name(name).is_ok() {
3123 return Ok(());
3124 }
3125 if is_root_ref_syntax(name) {
3126 return Ok(());
3127 }
3128 validate_symref_name(name)
3129}
3130
3131fn validate_ref_name_for_update(name: &str) -> Result<()> {
3132 if validate_ref_name(name).is_ok() {
3133 return Ok(());
3134 }
3135 if is_root_ref_syntax(name) {
3136 return Ok(());
3137 }
3138 validate_symref_name(name)
3139}
3140
3141fn is_root_ref_syntax(name: &str) -> bool {
3146 !name.is_empty()
3147 && name
3148 .bytes()
3149 .all(|b| b.is_ascii_uppercase() || b == b'-' || b == b'_')
3150}
3151
3152pub fn validate_ref_name(name: &str) -> Result<()> {
3153 if name == "HEAD" {
3154 return Ok(());
3155 }
3156 let path = Path::new(name);
3157 if !name.starts_with("refs/")
3158 || name.contains("..")
3159 || name.contains('\\')
3160 || name.ends_with('/')
3161 || name.ends_with(".lock")
3162 || path.is_absolute()
3163 || path.components().any(|component| {
3164 matches!(
3165 component,
3166 std::path::Component::ParentDir | std::path::Component::Prefix(_)
3167 )
3168 })
3169 {
3170 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
3171 }
3172 Ok(())
3173}
3174
3175fn ref_directory_conflict_error(new_ref: &str, existing_ref: &str) -> GitError {
3176 GitError::Transaction(format!(
3177 "cannot lock ref '{new_ref}': '{existing_ref}' exists; cannot create '{new_ref}'"
3178 ))
3179}
3180
3181fn parent_to_ref_name(base: &Path, parent: &Path) -> String {
3182 match parent.strip_prefix(base) {
3183 Ok(suffix) => suffix.to_string_lossy().replace('\\', "/"),
3184 Err(_) => parent.to_string_lossy().into_owned(),
3185 }
3186}
3187
3188fn validate_namespaced_ref(name: &str, prefix: &str, kind: &str) -> Result<()> {
3189 validate_ref_name(name)?;
3190 if name
3191 .strip_prefix(prefix)
3192 .is_none_or(|short_name| short_name.is_empty())
3193 {
3194 return Err(GitError::InvalidPath(format!(
3195 "invalid {kind} ref name {name}"
3196 )));
3197 }
3198 Ok(())
3199}
3200
3201fn validate_short_ref_name(kind: &str, name: &str) -> Result<()> {
3202 if name.is_empty()
3203 || name.starts_with('-')
3204 || name.starts_with('/')
3205 || name.ends_with('/')
3206 || name.contains(' ')
3207 || name.contains('\\')
3208 {
3209 return Err(GitError::InvalidPath(format!("invalid {kind} name {name}")));
3210 }
3211 Ok(())
3212}
3213
3214fn validate_remote_name(remote: &str) -> Result<()> {
3215 validate_short_ref_name("remote", remote)?;
3216 if remote.contains('/') {
3217 return Err(GitError::InvalidPath(format!(
3218 "invalid remote name {remote}"
3219 )));
3220 }
3221 Ok(())
3222}
3223
3224fn prepare_bundle_ref_updates<F>(
3225 refs: &[BundleRefUpdate],
3226 reflog: Option<&BundleRefUpdateReflog>,
3227 mut read_ref: F,
3228) -> Result<(Vec<RefUpdate>, Vec<AppliedBundleRefUpdate>)>
3229where
3230 F: FnMut(&str, &ObjectId) -> Result<Option<RefTarget>>,
3231{
3232 let mut seen = BTreeSet::new();
3233 let mut updates = Vec::with_capacity(refs.len());
3234 let mut applied = Vec::with_capacity(refs.len());
3235 for bundle_ref in refs {
3236 validate_ref_name(&bundle_ref.name)?;
3237 if !seen.insert(bundle_ref.name.clone()) {
3238 return Err(GitError::Transaction(format!(
3239 "duplicate bundle ref {}",
3240 bundle_ref.name
3241 )));
3242 }
3243 let old_oid = match read_ref(&bundle_ref.name, &bundle_ref.oid)? {
3244 Some(RefTarget::Direct(oid)) => Some(oid),
3245 Some(RefTarget::Symbolic(target)) => {
3246 return Err(GitError::Transaction(format!(
3247 "bundle ref {} would overwrite symbolic ref {target}",
3248 bundle_ref.name
3249 )));
3250 }
3251 None => None,
3252 };
3253 let reflog = match reflog {
3254 Some(reflog) => Some(ReflogEntry {
3255 old_oid: match &old_oid {
3256 Some(oid) => *oid,
3257 None => null_oid(bundle_ref.oid.format())?,
3258 },
3259 new_oid: bundle_ref.oid,
3260 committer: reflog.committer.clone(),
3261 message: reflog.message.clone(),
3262 }),
3263 None => None,
3264 };
3265 updates.push(RefUpdate {
3266 name: bundle_ref.name.clone(),
3267 expected: old_oid.map(RefTarget::Direct),
3268 new: RefTarget::Direct(bundle_ref.oid),
3269 reflog,
3270 });
3271 applied.push(AppliedBundleRefUpdate {
3272 name: bundle_ref.name.clone(),
3273 old_oid,
3274 new_oid: bundle_ref.oid,
3275 });
3276 }
3277 Ok((updates, applied))
3278}
3279
3280fn null_oid(format: ObjectFormat) -> Result<ObjectId> {
3281 Ok(ObjectId::null(format))
3282}
3283
3284#[cfg(test)]
3285mod tests {
3286 use super::*;
3287 use std::sync::atomic::{AtomicU64, Ordering};
3288
3289 static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
3290
3291 #[test]
3292 fn loose_ref_round_trips_direct() {
3293 let oid = "ce013625030ba8dba906f756967f9e9ca394464a";
3294 let reference = parse_loose_ref(ObjectFormat::Sha1, "refs/heads/main", oid.as_bytes())
3295 .expect("test operation should succeed");
3296 assert_eq!(write_loose_ref(&reference), format!("{oid}\n").into_bytes());
3297 }
3298
3299 #[test]
3300 fn symref_names_allow_onelevel_pseudo_refs() {
3301 for name in ["NOTHEAD", "FOO", "ORIG_HEAD", "TEST_SYMREF"] {
3302 validate_symref_name(name).expect("symref name should be valid");
3303 }
3304 assert!(validate_ref_name("NOTHEAD").is_err());
3305 assert!(validate_symref_target("refs/heads/foo").is_ok());
3306 assert!(validate_symref_target("ORIG_HEAD").is_ok());
3307 assert!(validate_symref_target("foo..bar").is_err());
3308 }
3309
3310 #[test]
3311 fn resolve_ref_peeled_follows_symref_chains() {
3312 let git_dir = temp_git_dir();
3313 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
3314 let oid = ObjectId::from_hex(
3315 ObjectFormat::Sha1,
3316 "ce013625030ba8dba906f756967f9e9ca394464a",
3317 )
3318 .expect("test operation should succeed");
3319 let mut tx = store.transaction();
3320 tx.update(RefUpdate {
3321 name: "refs/heads/target".into(),
3322 expected: None,
3323 new: RefTarget::Direct(oid),
3324 reflog: None,
3325 });
3326 tx.commit().expect("seed target ref");
3327 let mut tx = store.transaction();
3328 tx.update(RefUpdate {
3329 name: "refs/heads/alias".into(),
3330 expected: None,
3331 new: RefTarget::Symbolic("refs/heads/target".into()),
3332 reflog: None,
3333 });
3334 tx.commit().expect("seed alias ref");
3335 let mut tx = store.transaction();
3336 tx.update(RefUpdate {
3337 name: "ORIG_HEAD".into(),
3338 expected: None,
3339 new: RefTarget::Symbolic("refs/heads/alias".into()),
3340 reflog: None,
3341 });
3342 tx.commit().expect("seed ORIG_HEAD symref");
3343 assert_eq!(
3344 resolve_ref_peeled(&store, "ORIG_HEAD").expect("resolve ORIG_HEAD"),
3345 Some(oid)
3346 );
3347 let _ = fs::remove_dir_all(git_dir);
3348 }
3349
3350 #[test]
3351 fn symref_directory_conflict_is_reported_gracefully() {
3352 let git_dir = temp_git_dir();
3353 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
3354 let oid = ObjectId::from_hex(
3355 ObjectFormat::Sha1,
3356 "ce013625030ba8dba906f756967f9e9ca394464a",
3357 )
3358 .expect("test operation should succeed");
3359 let mut tx = store.transaction();
3360 tx.update(RefUpdate {
3361 name: "refs/heads/df".into(),
3362 expected: None,
3363 new: RefTarget::Direct(oid),
3364 reflog: None,
3365 });
3366 tx.commit().expect("seed branch ref");
3367
3368 let mut tx = store.transaction();
3369 tx.update(RefUpdate {
3370 name: "refs/heads/df/conflict".into(),
3371 expected: None,
3372 new: RefTarget::Symbolic("refs/heads/df".into()),
3373 reflog: None,
3374 });
3375 let err = tx.commit().expect_err("child ref should conflict");
3376 assert!(
3377 matches!(err, GitError::Transaction(message) if message.contains(
3378 "cannot lock ref 'refs/heads/df/conflict'"
3379 ) && message.contains("refs/heads/df"))
3380 );
3381 let _ = fs::remove_dir_all(git_dir);
3382 }
3383
3384 #[test]
3385 fn transaction_checks_expected_value() {
3386 let oid = ObjectId::from_hex(
3387 ObjectFormat::Sha1,
3388 "ce013625030ba8dba906f756967f9e9ca394464a",
3389 )
3390 .expect("test operation should succeed");
3391 let mut store = RefStore::new();
3392 let mut tx = store.transaction();
3393 tx.update(RefUpdate {
3394 name: "refs/heads/main".into(),
3395 expected: None,
3396 new: RefTarget::Direct(oid),
3397 reflog: None,
3398 });
3399 tx.commit().expect("test operation should succeed");
3400 assert_eq!(store.get("refs/heads/main"), Some(&RefTarget::Direct(oid)));
3401 }
3402
3403 #[test]
3404 fn packed_refs_parse_peeled_refs() {
3405 let packed = b"# pack-refs with: peeled fully-peeled sorted \n\
3406ce013625030ba8dba906f756967f9e9ca394464a refs/tags/v1\n\
3407^e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\n";
3408 let refs =
3409 parse_packed_refs(ObjectFormat::Sha1, packed).expect("test operation should succeed");
3410 assert_eq!(refs.len(), 1);
3411 assert_eq!(refs[0].reference.name, "refs/tags/v1");
3412 assert_eq!(
3413 refs[0]
3414 .peeled
3415 .as_ref()
3416 .expect("test operation should succeed")
3417 .to_hex(),
3418 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"
3419 );
3420 }
3421
3422 #[test]
3423 fn packed_refs_write_sorted_with_peeled_refs() {
3424 let head_oid = ObjectId::from_hex(
3425 ObjectFormat::Sha1,
3426 "ce013625030ba8dba906f756967f9e9ca394464a",
3427 )
3428 .expect("test operation should succeed");
3429 let tag_oid = ObjectId::from_hex(
3430 ObjectFormat::Sha1,
3431 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
3432 )
3433 .expect("test operation should succeed");
3434 let peeled_oid = ObjectId::from_hex(
3435 ObjectFormat::Sha1,
3436 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
3437 )
3438 .expect("test operation should succeed");
3439 let refs = vec![
3440 PackedRef {
3441 reference: Ref {
3442 name: "refs/tags/v1".into(),
3443 target: RefTarget::Direct(tag_oid),
3444 },
3445 peeled: Some(peeled_oid),
3446 },
3447 PackedRef {
3448 reference: Ref {
3449 name: "refs/heads/main".into(),
3450 target: RefTarget::Direct(head_oid),
3451 },
3452 peeled: None,
3453 },
3454 ];
3455 let bytes = write_packed_refs(&refs).expect("test operation should succeed");
3456 let expected = format!(
3457 "# pack-refs with: peeled fully-peeled sorted \n\
3458{head_oid} refs/heads/main\n\
3459{tag_oid} refs/tags/v1\n\
3460^{peeled_oid}\n"
3461 );
3462 assert_eq!(
3463 String::from_utf8(bytes.clone()).expect("test operation should succeed"),
3464 expected
3465 );
3466 let parsed =
3467 parse_packed_refs(ObjectFormat::Sha1, &bytes).expect("test operation should succeed");
3468 assert_eq!(parsed[0], refs[1]);
3469 assert_eq!(parsed[1], refs[0]);
3470 }
3471
3472 #[test]
3473 fn full_ref_name_validates_and_round_trips_owned() {
3474 let full = FullRefName::new("refs/heads/main").expect("valid full branch ref");
3475 assert_eq!(full.as_str(), "refs/heads/main");
3476 assert_eq!(full.to_string(), "refs/heads/main");
3477 assert_eq!(full.to_owned().into_string(), "refs/heads/main");
3478
3479 let head = FullRefNameBuf::new("HEAD").expect("valid HEAD ref");
3480 assert_eq!(head.as_ref_name().into_str(), "HEAD");
3481
3482 assert!(FullRefName::new("main").is_err());
3483 assert!(FullRefNameBuf::new("refs/heads/bad.lock").is_err());
3484 }
3485
3486 #[test]
3487 fn branch_ref_name_helpers_validate_short_and_full_names() {
3488 let branch =
3489 BranchRefNameBuf::from_branch_name("feature/topic").expect("valid branch short name");
3490 assert_eq!(branch.as_str(), "refs/heads/feature/topic");
3491 assert_eq!(branch.branch_name(), "feature/topic");
3492 assert_eq!(
3493 branch.as_full_ref_name().as_str(),
3494 "refs/heads/feature/topic"
3495 );
3496 assert_eq!(
3497 branch_ref_name("feature/topic").expect("valid branch short name"),
3498 branch.as_str()
3499 );
3500
3501 let borrowed = BranchRefName::from_full("refs/heads/main").expect("valid full branch ref");
3502 assert_eq!(borrowed.branch_name(), "main");
3503 assert_eq!(borrowed.to_owned().into_string(), "refs/heads/main");
3504 assert_eq!(
3505 FullRefName::new("refs/heads/main")
3506 .expect("valid full branch ref")
3507 .as_branch()
3508 .expect("full ref is a branch")
3509 .branch_name(),
3510 "main"
3511 );
3512
3513 assert!(BranchRefName::from_full("refs/tags/main").is_err());
3514 assert!(BranchRefName::from_full("refs/heads").is_err());
3515 assert!(BranchRefNameBuf::from_branch_name("-bad").is_err());
3516 }
3517
3518 #[test]
3519 fn tag_ref_name_helpers_validate_short_and_full_names() {
3520 let tag = TagRefNameBuf::from_tag_name("v1.0").expect("valid tag short name");
3521 assert_eq!(tag.as_str(), "refs/tags/v1.0");
3522 assert_eq!(tag.tag_name(), "v1.0");
3523 assert_eq!(tag.as_full_ref_name().as_str(), "refs/tags/v1.0");
3524 assert_eq!(
3525 tag_ref_name("v1.0").expect("valid tag short name"),
3526 tag.as_str()
3527 );
3528
3529 let borrowed = TagRefName::from_full("refs/tags/release/1").expect("valid full tag ref");
3530 assert_eq!(borrowed.tag_name(), "release/1");
3531 assert_eq!(borrowed.to_owned().into_string(), "refs/tags/release/1");
3532 assert_eq!(
3533 FullRefName::new("refs/tags/release/1")
3534 .expect("valid full tag ref")
3535 .as_tag()
3536 .expect("full ref is a tag")
3537 .tag_name(),
3538 "release/1"
3539 );
3540
3541 assert!(TagRefName::from_full("refs/heads/v1.0").is_err());
3542 assert!(TagRefName::from_full("refs/tags").is_err());
3543 assert!(TagRefNameBuf::from_tag_name("bad tag").is_err());
3544 }
3545
3546 #[test]
3547 fn remote_ref_name_helpers_validate_namespace_and_components() {
3548 let remote = RemoteRefNameBuf::from_remote_branch("origin", "feature/topic")
3549 .expect("valid remote branch ref");
3550 assert_eq!(remote.as_str(), "refs/remotes/origin/feature/topic");
3551 assert_eq!(remote.short_name(), "origin/feature/topic");
3552 assert_eq!(remote.remote_name(), "origin");
3553 assert_eq!(remote.remote_branch(), Some("feature/topic"));
3554 assert_eq!(
3555 remote.as_full_ref_name().as_str(),
3556 "refs/remotes/origin/feature/topic"
3557 );
3558
3559 let head =
3560 RemoteRefName::from_full("refs/remotes/origin/HEAD").expect("valid remote HEAD ref");
3561 assert_eq!(head.remote_name(), "origin");
3562 assert_eq!(head.remote_branch(), Some("HEAD"));
3563 assert_eq!(
3564 FullRefName::new("refs/remotes/upstream/main")
3565 .expect("valid full remote ref")
3566 .as_remote()
3567 .expect("full ref is remote-tracking")
3568 .remote_name(),
3569 "upstream"
3570 );
3571
3572 let short =
3573 RemoteRefNameBuf::from_short_name("origin/main").expect("valid remote short ref");
3574 assert_eq!(short.as_str(), "refs/remotes/origin/main");
3575
3576 assert!(RemoteRefName::from_full("refs/heads/origin/main").is_err());
3577 assert!(RemoteRefName::from_full("refs/remotes/").is_err());
3578 assert!(RemoteRefNameBuf::from_remote_branch("origin/fork", "main").is_err());
3579 }
3580
3581 #[test]
3582 fn file_ref_store_writes_ref_and_reflog() {
3583 let git_dir = temp_git_dir();
3584 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
3585 let oid = ObjectId::from_hex(
3586 ObjectFormat::Sha1,
3587 "ce013625030ba8dba906f756967f9e9ca394464a",
3588 )
3589 .expect("test operation should succeed");
3590 let mut tx = store.transaction();
3591 tx.update(RefUpdate {
3592 name: "refs/heads/main".into(),
3593 expected: None,
3594 new: RefTarget::Direct(oid),
3595 reflog: Some(ReflogEntry {
3596 old_oid: zero_oid(ObjectFormat::Sha1).expect("test operation should succeed"),
3597 new_oid: oid,
3598 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
3599 message: b"update by test".to_vec(),
3600 }),
3601 });
3602 tx.commit().expect("test operation should succeed");
3603 assert_eq!(
3604 store
3605 .read_ref("refs/heads/main")
3606 .expect("test operation should succeed"),
3607 Some(RefTarget::Direct(oid))
3608 );
3609 let log = store
3610 .read_reflog("refs/heads/main")
3611 .expect("test operation should succeed");
3612 assert_eq!(log.len(), 1);
3613 assert_eq!(log[0].message, b"update by test");
3614 fs::remove_dir_all(git_dir).expect("test operation should succeed");
3615 }
3616
3617 #[test]
3618 fn file_ref_store_applies_bundle_refs_with_reflog() {
3619 let git_dir = temp_git_dir();
3620 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
3621 let old_main = ObjectId::from_hex(
3622 ObjectFormat::Sha1,
3623 "ce013625030ba8dba906f756967f9e9ca394464a",
3624 )
3625 .expect("test operation should succeed");
3626 let new_main = ObjectId::from_hex(
3627 ObjectFormat::Sha1,
3628 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
3629 )
3630 .expect("test operation should succeed");
3631 let tag_oid = ObjectId::from_hex(
3632 ObjectFormat::Sha1,
3633 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
3634 )
3635 .expect("test operation should succeed");
3636 let mut tx = store.transaction();
3637 tx.update(RefUpdate {
3638 name: "refs/heads/main".into(),
3639 expected: None,
3640 new: RefTarget::Direct(old_main.clone()),
3641 reflog: None,
3642 });
3643 tx.commit().expect("test operation should succeed");
3644
3645 let applied = store
3646 .apply_bundle_ref_updates(
3647 &[
3648 BundleRefUpdate {
3649 name: "refs/heads/main".into(),
3650 oid: new_main.clone(),
3651 },
3652 BundleRefUpdate {
3653 name: "refs/tags/v1.0".into(),
3654 oid: tag_oid,
3655 },
3656 ],
3657 Some(BundleRefUpdateReflog {
3658 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
3659 message: b"bundle: import refs".to_vec(),
3660 }),
3661 )
3662 .expect("test operation should succeed");
3663
3664 assert_eq!(
3665 applied,
3666 vec![
3667 AppliedBundleRefUpdate {
3668 name: "refs/heads/main".into(),
3669 old_oid: Some(old_main.clone()),
3670 new_oid: new_main.clone(),
3671 },
3672 AppliedBundleRefUpdate {
3673 name: "refs/tags/v1.0".into(),
3674 old_oid: None,
3675 new_oid: tag_oid,
3676 }
3677 ]
3678 );
3679 assert_eq!(
3680 store
3681 .read_ref("refs/heads/main")
3682 .expect("test operation should succeed"),
3683 Some(RefTarget::Direct(new_main.clone()))
3684 );
3685 assert_eq!(
3686 store
3687 .read_ref("refs/tags/v1.0")
3688 .expect("test operation should succeed"),
3689 Some(RefTarget::Direct(tag_oid))
3690 );
3691 let main_log = store
3692 .read_reflog("refs/heads/main")
3693 .expect("test operation should succeed");
3694 assert_eq!(main_log.len(), 1);
3695 assert_eq!(main_log[0].old_oid, old_main);
3696 assert_eq!(main_log[0].new_oid, new_main);
3697 assert_eq!(main_log[0].message, b"bundle: import refs");
3698 let tag_log = store
3699 .read_reflog("refs/tags/v1.0")
3700 .expect("test operation should succeed");
3701 assert_eq!(tag_log.len(), 1);
3702 assert_eq!(
3703 tag_log[0].old_oid,
3704 zero_oid(ObjectFormat::Sha1).expect("test operation should succeed")
3705 );
3706 assert_eq!(tag_log[0].new_oid, tag_oid);
3707 fs::remove_dir_all(git_dir).expect("test operation should succeed");
3708 }
3709
3710 #[test]
3711 fn file_ref_store_rejects_bad_bundle_ref_before_writing() {
3712 let git_dir = temp_git_dir();
3713 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
3714 let oid = ObjectId::from_hex(
3715 ObjectFormat::Sha1,
3716 "ce013625030ba8dba906f756967f9e9ca394464a",
3717 )
3718 .expect("test operation should succeed");
3719
3720 let result = store.apply_bundle_ref_updates(
3721 &[
3722 BundleRefUpdate {
3723 name: "refs/heads/main".into(),
3724 oid,
3725 },
3726 BundleRefUpdate {
3727 name: "refs/heads/bad.lock".into(),
3728 oid,
3729 },
3730 ],
3731 None,
3732 );
3733
3734 assert!(result.is_err());
3735 assert_eq!(
3736 store
3737 .read_ref("refs/heads/main")
3738 .expect("test operation should succeed"),
3739 None
3740 );
3741 fs::remove_dir_all(git_dir).expect("test operation should succeed");
3742 }
3743
3744 #[test]
3745 fn file_ref_store_rejects_bundle_ref_over_symbolic_ref() {
3746 let git_dir = temp_git_dir();
3747 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
3748 let oid = ObjectId::from_hex(
3749 ObjectFormat::Sha1,
3750 "ce013625030ba8dba906f756967f9e9ca394464a",
3751 )
3752 .expect("test operation should succeed");
3753 let mut tx = store.transaction();
3754 tx.update(RefUpdate {
3755 name: "refs/heads/main".into(),
3756 expected: None,
3757 new: RefTarget::Symbolic("refs/heads/base".into()),
3758 reflog: None,
3759 });
3760 tx.commit().expect("test operation should succeed");
3761
3762 let result = store.apply_bundle_ref_updates(
3763 &[BundleRefUpdate {
3764 name: "refs/heads/main".into(),
3765 oid,
3766 }],
3767 None,
3768 );
3769
3770 assert!(result.is_err());
3771 assert_eq!(
3772 store
3773 .read_ref("refs/heads/main")
3774 .expect("test operation should succeed"),
3775 Some(RefTarget::Symbolic("refs/heads/base".into()))
3776 );
3777 fs::remove_dir_all(git_dir).expect("test operation should succeed");
3778 }
3779
3780 #[test]
3781 fn file_ref_store_expires_reflog_entries_by_timestamp() {
3782 let git_dir = temp_git_dir();
3783 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
3784 let first = ObjectId::from_hex(
3785 ObjectFormat::Sha1,
3786 "ce013625030ba8dba906f756967f9e9ca394464a",
3787 )
3788 .expect("test operation should succeed");
3789 let second = ObjectId::from_hex(
3790 ObjectFormat::Sha1,
3791 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
3792 )
3793 .expect("test operation should succeed");
3794 let mut tx = store.transaction();
3795 tx.update(RefUpdate {
3796 name: "refs/heads/main".into(),
3797 expected: None,
3798 new: RefTarget::Direct(first.clone()),
3799 reflog: Some(ReflogEntry {
3800 old_oid: zero_oid(ObjectFormat::Sha1).expect("test operation should succeed"),
3801 new_oid: first.clone(),
3802 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
3803 message: b"old".to_vec(),
3804 }),
3805 });
3806 tx.update(RefUpdate {
3807 name: "refs/heads/main".into(),
3808 expected: None,
3809 new: RefTarget::Direct(second.clone()),
3810 reflog: Some(ReflogEntry {
3811 old_oid: first,
3812 new_oid: second.clone(),
3813 committer: b"Git Rs <sley@example.invalid> 100 +0000".to_vec(),
3814 message: b"new".to_vec(),
3815 }),
3816 });
3817 tx.commit().expect("test operation should succeed");
3818
3819 let removed = store
3820 .expire_reflog_older_than("refs/heads/main", 50)
3821 .expect("test operation should succeed");
3822 assert_eq!(removed, 1);
3823 let log = store
3824 .read_reflog("refs/heads/main")
3825 .expect("test operation should succeed");
3826 assert_eq!(log.len(), 1);
3827 assert_eq!(log[0].new_oid, second);
3828 assert_eq!(log[0].message, b"new");
3829 assert!(
3830 !git_dir
3831 .join("logs")
3832 .join("refs")
3833 .join("heads")
3834 .join("main.lock")
3835 .exists()
3836 );
3837 fs::remove_dir_all(git_dir).expect("test operation should succeed");
3838 }
3839
3840 #[test]
3841 fn file_ref_store_creates_branch() {
3842 let git_dir = temp_git_dir();
3843 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
3844 let oid = ObjectId::from_hex(
3845 ObjectFormat::Sha1,
3846 "ce013625030ba8dba906f756967f9e9ca394464a",
3847 )
3848 .expect("test operation should succeed");
3849 let branch = store
3850 .create_branch(
3851 "feature",
3852 oid,
3853 b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
3854 b"branch: Created from main".to_vec(),
3855 )
3856 .expect("test operation should succeed");
3857 assert_eq!(branch.name, "refs/heads/feature");
3858 assert_eq!(
3859 store
3860 .read_ref("refs/heads/feature")
3861 .expect("test operation should succeed"),
3862 Some(RefTarget::Direct(oid))
3863 );
3864 fs::remove_dir_all(git_dir).expect("test operation should succeed");
3865 }
3866
3867 #[test]
3868 fn file_ref_store_deletes_loose_branch() {
3869 let git_dir = temp_git_dir();
3870 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
3871 let oid = ObjectId::from_hex(
3872 ObjectFormat::Sha1,
3873 "ce013625030ba8dba906f756967f9e9ca394464a",
3874 )
3875 .expect("test operation should succeed");
3876 store
3877 .create_branch(
3878 "feature",
3879 oid,
3880 b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
3881 b"branch: Created from main".to_vec(),
3882 )
3883 .expect("test operation should succeed");
3884 let deleted = store
3885 .delete_branch("feature")
3886 .expect("test operation should succeed");
3887 assert_eq!(deleted.name, "refs/heads/feature");
3888 assert_eq!(deleted.oid, oid);
3889 assert_eq!(
3890 store
3891 .read_ref("refs/heads/feature")
3892 .expect("test operation should succeed"),
3893 None
3894 );
3895 assert!(!git_dir.join("refs").join("heads").join("feature").exists());
3896 assert!(
3897 !git_dir
3898 .join("logs")
3899 .join("refs")
3900 .join("heads")
3901 .join("feature")
3902 .exists()
3903 );
3904 fs::remove_dir_all(git_dir).expect("test operation should succeed");
3905 }
3906
3907 #[test]
3908 fn file_ref_store_deletes_generic_loose_ref() {
3909 let git_dir = temp_git_dir();
3910 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
3911 let oid = ObjectId::from_hex(
3912 ObjectFormat::Sha1,
3913 "ce013625030ba8dba906f756967f9e9ca394464a",
3914 )
3915 .expect("test operation should succeed");
3916 let mut tx = store.transaction();
3917 tx.update(RefUpdate {
3918 name: "refs/heads/topic".into(),
3919 expected: None,
3920 new: RefTarget::Direct(oid),
3921 reflog: Some(ReflogEntry {
3922 old_oid: zero_oid(ObjectFormat::Sha1).expect("test operation should succeed"),
3923 new_oid: oid,
3924 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
3925 message: b"update by test".to_vec(),
3926 }),
3927 });
3928 tx.commit().expect("test operation should succeed");
3929 let deleted = store
3930 .delete_ref("refs/heads/topic")
3931 .expect("test operation should succeed");
3932 assert_eq!(deleted.name, "refs/heads/topic");
3933 assert_eq!(deleted.oid, oid);
3934 assert_eq!(
3935 store
3936 .read_ref("refs/heads/topic")
3937 .expect("test operation should succeed"),
3938 None
3939 );
3940 assert!(!git_dir.join("refs").join("heads").join("topic").exists());
3941 assert!(
3942 !git_dir
3943 .join("logs")
3944 .join("refs")
3945 .join("heads")
3946 .join("topic")
3947 .exists()
3948 );
3949 fs::remove_dir_all(git_dir).expect("test operation should succeed");
3950 }
3951
3952 #[test]
3953 fn file_ref_store_delete_ref_checked_removes_reflog() {
3954 let git_dir = temp_git_dir();
3955 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
3956 let oid = ObjectId::from_hex(
3957 ObjectFormat::Sha1,
3958 "ce013625030ba8dba906f756967f9e9ca394464a",
3959 )
3960 .expect("test operation should succeed");
3961 let mut tx = store.transaction();
3965 tx.update(RefUpdate {
3966 name: "refs/heads/main".into(),
3967 expected: None,
3968 new: RefTarget::Direct(oid),
3969 reflog: Some(ReflogEntry {
3970 old_oid: zero_oid(ObjectFormat::Sha1).expect("test operation should succeed"),
3971 new_oid: oid,
3972 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
3973 message: b"create main".to_vec(),
3974 }),
3975 });
3976 tx.commit().expect("test operation should succeed");
3977 assert!(
3978 git_dir
3979 .join("logs")
3980 .join("refs")
3981 .join("heads")
3982 .join("main")
3983 .exists(),
3984 "reflog file should exist before the checked delete"
3985 );
3986
3987 let deleted = store
3988 .delete_ref_checked(DeleteRef {
3989 name: "refs/heads/main".into(),
3990 expected_old: Some(oid),
3991 reflog: Some(DeleteRefReflog {
3992 committer: b"Git Rs <sley@example.invalid> 123 +0000".to_vec(),
3993 message: b"delete main".to_vec(),
3994 }),
3995 })
3996 .expect("test operation should succeed");
3997
3998 assert_eq!(deleted.name, "refs/heads/main");
3999 assert_eq!(deleted.oid, oid);
4000 assert_eq!(
4001 store
4002 .read_ref("refs/heads/main")
4003 .expect("test operation should succeed"),
4004 None
4005 );
4006 assert!(
4009 !git_dir
4010 .join("logs")
4011 .join("refs")
4012 .join("heads")
4013 .join("main")
4014 .exists(),
4015 "reflog file should be removed by the checked delete"
4016 );
4017 assert!(
4018 store
4019 .read_reflog("refs/heads/main")
4020 .expect("test operation should succeed")
4021 .is_empty()
4022 );
4023 assert!(
4024 !git_dir
4025 .join("refs")
4026 .join("heads")
4027 .join("main.lock")
4028 .exists()
4029 );
4030 assert!(!git_dir.join("packed-refs.lock").exists());
4031 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4032 }
4033
4034 #[test]
4035 fn file_ref_store_delete_ref_checked_stale_expected_leaves_ref_untouched() {
4036 let git_dir = temp_git_dir();
4037 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4038 let actual = ObjectId::from_hex(
4039 ObjectFormat::Sha1,
4040 "ce013625030ba8dba906f756967f9e9ca394464a",
4041 )
4042 .expect("test operation should succeed");
4043 let expected = ObjectId::from_hex(
4044 ObjectFormat::Sha1,
4045 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
4046 )
4047 .expect("test operation should succeed");
4048 let mut tx = store.transaction();
4049 tx.update(RefUpdate {
4050 name: "refs/heads/main".into(),
4051 expected: None,
4052 new: RefTarget::Direct(actual),
4053 reflog: None,
4054 });
4055 tx.commit().expect("test operation should succeed");
4056
4057 let err = store
4058 .delete_ref_checked(DeleteRef {
4059 name: "refs/heads/main".into(),
4060 expected_old: Some(expected),
4061 reflog: None,
4062 })
4063 .expect_err("stale expected must fail");
4064
4065 assert!(matches!(
4066 err,
4067 RefDeleteError::ExpectedMismatch {
4068 expected: Some(got_expected),
4069 actual: Some(got_actual),
4070 } if got_expected == expected && got_actual == actual
4071 ));
4072 assert_eq!(
4073 store
4074 .read_ref("refs/heads/main")
4075 .expect("test operation should succeed"),
4076 Some(RefTarget::Direct(actual))
4077 );
4078 assert!(
4079 !git_dir
4080 .join("refs")
4081 .join("heads")
4082 .join("main.lock")
4083 .exists()
4084 );
4085 assert!(!git_dir.join("packed-refs.lock").exists());
4086 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4087 }
4088
4089 #[test]
4090 fn file_ref_store_delete_ref_checked_missing_returns_not_found() {
4091 let git_dir = temp_git_dir();
4092 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4093
4094 let err = store
4095 .delete_ref_checked(DeleteRef {
4096 name: "refs/heads/missing".into(),
4097 expected_old: None,
4098 reflog: None,
4099 })
4100 .expect_err("missing ref must fail");
4101
4102 assert!(matches!(err, RefDeleteError::NotFound));
4103 assert!(
4104 !git_dir
4105 .join("refs")
4106 .join("heads")
4107 .join("missing.lock")
4108 .exists()
4109 );
4110 assert!(!git_dir.join("packed-refs.lock").exists());
4111 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4112 }
4113
4114 #[test]
4115 fn file_ref_store_delete_ref_checked_removes_packed_ref() {
4116 let git_dir = temp_git_dir();
4117 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4118 let oid = ObjectId::from_hex(
4119 ObjectFormat::Sha1,
4120 "ce013625030ba8dba906f756967f9e9ca394464a",
4121 )
4122 .expect("test operation should succeed");
4123 let other = ObjectId::from_hex(
4124 ObjectFormat::Sha1,
4125 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
4126 )
4127 .expect("test operation should succeed");
4128 store
4129 .write_packed_refs(&[
4130 PackedRef {
4131 reference: Ref {
4132 name: "refs/heads/main".into(),
4133 target: RefTarget::Direct(oid),
4134 },
4135 peeled: None,
4136 },
4137 PackedRef {
4138 reference: Ref {
4139 name: "refs/heads/other".into(),
4140 target: RefTarget::Direct(other),
4141 },
4142 peeled: None,
4143 },
4144 ])
4145 .expect("test operation should succeed");
4146
4147 store
4148 .delete_ref_checked(DeleteRef {
4149 name: "refs/heads/main".into(),
4150 expected_old: Some(oid),
4151 reflog: None,
4152 })
4153 .expect("test operation should succeed");
4154
4155 assert_eq!(
4156 store
4157 .read_ref("refs/heads/main")
4158 .expect("test operation should succeed"),
4159 None
4160 );
4161 assert_eq!(
4162 store
4163 .read_ref("refs/heads/other")
4164 .expect("test operation should succeed"),
4165 Some(RefTarget::Direct(other))
4166 );
4167 let packed =
4168 fs::read_to_string(git_dir.join("packed-refs")).expect("test operation should succeed");
4169 assert!(!packed.contains("refs/heads/main"));
4170 assert!(packed.contains("refs/heads/other"));
4171 assert!(
4172 !git_dir
4173 .join("refs")
4174 .join("heads")
4175 .join("main.lock")
4176 .exists()
4177 );
4178 assert!(!git_dir.join("packed-refs.lock").exists());
4179 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4180 }
4181
4182 #[test]
4183 fn file_ref_store_delete_ref_checked_lock_conflict_returns_locked() {
4184 let git_dir = temp_git_dir();
4185 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4186 let oid = ObjectId::from_hex(
4187 ObjectFormat::Sha1,
4188 "ce013625030ba8dba906f756967f9e9ca394464a",
4189 )
4190 .expect("test operation should succeed");
4191 let mut tx = store.transaction();
4192 tx.update(RefUpdate {
4193 name: "refs/heads/main".into(),
4194 expected: None,
4195 new: RefTarget::Direct(oid),
4196 reflog: None,
4197 });
4198 tx.commit().expect("test operation should succeed");
4199 fs::write(
4200 git_dir.join("refs").join("heads").join("main.lock"),
4201 b"held\n",
4202 )
4203 .expect("test operation should succeed");
4204
4205 let err = store
4206 .delete_ref_checked(DeleteRef {
4207 name: "refs/heads/main".into(),
4208 expected_old: Some(oid),
4209 reflog: None,
4210 })
4211 .expect_err("held lock must fail");
4212
4213 assert!(matches!(err, RefDeleteError::Locked));
4214 assert_eq!(
4215 store
4216 .read_ref("refs/heads/main")
4217 .expect("test operation should succeed"),
4218 Some(RefTarget::Direct(oid))
4219 );
4220 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4221 }
4222
4223 #[test]
4224 fn file_ref_store_reports_current_branch() {
4225 let git_dir = temp_git_dir();
4226 fs::write(git_dir.join("HEAD"), b"ref: refs/heads/main\n")
4227 .expect("test operation should succeed");
4228 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4229 assert_eq!(
4230 store
4231 .current_branch_ref()
4232 .expect("test operation should succeed"),
4233 Some("refs/heads/main".into())
4234 );
4235 assert_eq!(
4236 store
4237 .current_branch()
4238 .expect("test operation should succeed"),
4239 Some("main".into())
4240 );
4241 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4242 }
4243
4244 #[test]
4245 fn file_ref_store_resolves_linked_worktree_head_through_common_refs() {
4246 let common = temp_git_dir();
4247 let admin = common.join("worktrees").join("linked");
4248 fs::create_dir_all(&admin).expect("test operation should succeed");
4249 fs::write(admin.join("commondir"), "../..\n").expect("test operation should succeed");
4250 fs::write(admin.join("HEAD"), b"ref: refs/heads/topic\n")
4251 .expect("test operation should succeed");
4252 let oid = ObjectId::from_hex(
4253 ObjectFormat::Sha256,
4254 "08ffba112b648c22b5425f01bec2c37ffc524c4d48ef04337779df3973733050",
4255 )
4256 .expect("test operation should succeed");
4257 fs::create_dir_all(common.join("refs").join("heads"))
4258 .expect("test operation should succeed");
4259 fs::write(
4260 common.join("refs").join("heads").join("topic"),
4261 format!("{oid}\n"),
4262 )
4263 .expect("test operation should succeed");
4264
4265 let store = FileRefStore::new(&admin, ObjectFormat::Sha256);
4266 assert_eq!(
4267 store
4268 .read_ref("HEAD")
4269 .expect("test operation should succeed"),
4270 Some(RefTarget::Symbolic("refs/heads/topic".into()))
4271 );
4272 assert_eq!(
4273 store
4274 .read_ref("refs/heads/topic")
4275 .expect("test operation should succeed"),
4276 Some(RefTarget::Direct(oid))
4277 );
4278
4279 fs::remove_dir_all(common).expect("test operation should succeed");
4280 }
4281
4282 #[test]
4283 fn file_ref_store_creates_tag() {
4284 let git_dir = temp_git_dir();
4285 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4286 let oid = ObjectId::from_hex(
4287 ObjectFormat::Sha1,
4288 "ce013625030ba8dba906f756967f9e9ca394464a",
4289 )
4290 .expect("test operation should succeed");
4291 let tag = store
4292 .create_tag("v1.0", oid)
4293 .expect("test operation should succeed");
4294 assert_eq!(tag.name, "refs/tags/v1.0");
4295 assert_eq!(
4296 store
4297 .read_ref("refs/tags/v1.0")
4298 .expect("test operation should succeed"),
4299 Some(RefTarget::Direct(oid))
4300 );
4301 assert!(
4302 store
4303 .read_reflog("refs/tags/v1.0")
4304 .expect("test operation should succeed")
4305 .is_empty()
4306 );
4307 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4308 }
4309
4310 #[test]
4311 fn file_ref_store_deletes_loose_tag() {
4312 let git_dir = temp_git_dir();
4313 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4314 let oid = ObjectId::from_hex(
4315 ObjectFormat::Sha1,
4316 "ce013625030ba8dba906f756967f9e9ca394464a",
4317 )
4318 .expect("test operation should succeed");
4319 store
4320 .create_tag("v1.0", oid)
4321 .expect("test operation should succeed");
4322 let deleted = store
4323 .delete_tag("v1.0")
4324 .expect("test operation should succeed");
4325 assert_eq!(deleted.name, "refs/tags/v1.0");
4326 assert_eq!(deleted.oid, oid);
4327 assert_eq!(
4328 store
4329 .read_ref("refs/tags/v1.0")
4330 .expect("test operation should succeed"),
4331 None
4332 );
4333 assert!(!git_dir.join("refs").join("tags").join("v1.0").exists());
4334 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4335 }
4336
4337 #[test]
4338 fn file_ref_store_reads_packed_ref() {
4339 let git_dir = temp_git_dir();
4340 fs::write(
4341 git_dir.join("packed-refs"),
4342 b"ce013625030ba8dba906f756967f9e9ca394464a refs/heads/main\n",
4343 )
4344 .expect("test operation should succeed");
4345 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4346 assert!(matches!(
4347 store
4348 .read_ref("refs/heads/main")
4349 .expect("test operation should succeed"),
4350 Some(RefTarget::Direct(_))
4351 ));
4352 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4353 }
4354
4355 #[test]
4356 fn file_ref_store_lists_loose_refs_over_packed_refs() {
4357 let git_dir = temp_git_dir();
4358 fs::write(
4359 git_dir.join("packed-refs"),
4360 b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 refs/heads/main\n",
4361 )
4362 .expect("test operation should succeed");
4363 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4364 let oid = ObjectId::from_hex(
4365 ObjectFormat::Sha1,
4366 "ce013625030ba8dba906f756967f9e9ca394464a",
4367 )
4368 .expect("test operation should succeed");
4369 let mut tx = store.transaction();
4370 tx.update(RefUpdate {
4371 name: "refs/heads/main".into(),
4372 expected: None,
4373 new: RefTarget::Direct(oid),
4374 reflog: None,
4375 });
4376 tx.commit().expect("test operation should succeed");
4377 let refs = store.list_refs().expect("test operation should succeed");
4378 assert_eq!(refs.len(), 1);
4379 assert_eq!(refs[0].target, RefTarget::Direct(oid));
4380 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4381 }
4382
4383 #[test]
4384 fn file_ref_store_writes_packed_refs() {
4385 let git_dir = temp_git_dir();
4386 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4387 let oid = ObjectId::from_hex(
4388 ObjectFormat::Sha1,
4389 "ce013625030ba8dba906f756967f9e9ca394464a",
4390 )
4391 .expect("test operation should succeed");
4392 store
4393 .write_packed_refs(&[PackedRef {
4394 reference: Ref {
4395 name: "refs/heads/main".into(),
4396 target: RefTarget::Direct(oid),
4397 },
4398 peeled: None,
4399 }])
4400 .expect("test operation should succeed");
4401 assert_eq!(
4402 store
4403 .read_ref("refs/heads/main")
4404 .expect("test operation should succeed"),
4405 Some(RefTarget::Direct(oid))
4406 );
4407 let refs = store.list_refs().expect("test operation should succeed");
4408 assert_eq!(refs.len(), 1);
4409 assert_eq!(refs[0].target, RefTarget::Direct(oid));
4410 assert!(git_dir.join("packed-refs").exists());
4411 assert!(!git_dir.join("packed-refs.lock").exists());
4412 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4413 }
4414
4415 #[test]
4416 fn file_ref_store_checks_ref_prefix_in_packed_refs() {
4417 let git_dir = temp_git_dir();
4418 fs::write(
4419 git_dir.join("packed-refs"),
4420 b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 refs/heads/main\n\
4421 ce013625030ba8dba906f756967f9e9ca394464a refs/replace/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\n",
4422 )
4423 .expect("test operation should succeed");
4424 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4425 assert!(
4426 store
4427 .has_refs_with_prefix("refs/replace/")
4428 .expect("test operation should succeed")
4429 );
4430 assert!(
4431 !store
4432 .has_refs_with_prefix("refs/notes/")
4433 .expect("test operation should succeed")
4434 );
4435 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4436 }
4437
4438 #[test]
4439 fn file_ref_store_checks_ref_prefix_in_loose_refs() {
4440 let git_dir = temp_git_dir();
4441 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4442 let oid = ObjectId::from_hex(
4443 ObjectFormat::Sha1,
4444 "ce013625030ba8dba906f756967f9e9ca394464a",
4445 )
4446 .expect("test operation should succeed");
4447 let mut tx = store.transaction();
4448 tx.update(RefUpdate {
4449 name: "refs/replace/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391".into(),
4450 expected: None,
4451 new: RefTarget::Direct(oid),
4452 reflog: None,
4453 });
4454 tx.commit().expect("test operation should succeed");
4455 assert!(
4456 store
4457 .has_refs_with_prefix("refs/replace/")
4458 .expect("test operation should succeed")
4459 );
4460 assert!(
4461 !store
4462 .has_refs_with_prefix("refs/notes/")
4463 .expect("test operation should succeed")
4464 );
4465 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4466 }
4467
4468 #[test]
4469 fn file_ref_store_reads_reftable_stack_and_ignores_dummy_head() {
4470 let git_dir = temp_git_dir();
4471 write_reftable_config(&git_dir);
4472 fs::write(git_dir.join("HEAD"), b"ref: refs/heads/.invalid\n")
4473 .expect("test operation should succeed");
4474 let head_oid = ObjectId::from_hex(
4475 ObjectFormat::Sha1,
4476 "ce013625030ba8dba906f756967f9e9ca394464a",
4477 )
4478 .expect("test operation should succeed");
4479 let tag_oid = ObjectId::from_hex(
4480 ObjectFormat::Sha1,
4481 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
4482 )
4483 .expect("test operation should succeed");
4484 let peeled_oid = ObjectId::from_hex(
4485 ObjectFormat::Sha1,
4486 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
4487 )
4488 .expect("test operation should succeed");
4489 write_reftable_stack(
4490 &git_dir,
4491 &[(
4492 "000000000001-000000000001-rust.ref",
4493 vec![
4494 sley_formats::ReftableRefRecord {
4495 name: "HEAD".into(),
4496 update_index: 1,
4497 value: ReftableRefValue::Symbolic("refs/heads/main".into()),
4498 },
4499 sley_formats::ReftableRefRecord {
4500 name: "refs/heads/main".into(),
4501 update_index: 1,
4502 value: ReftableRefValue::Direct(head_oid),
4503 },
4504 sley_formats::ReftableRefRecord {
4505 name: "refs/tags/v1.0".into(),
4506 update_index: 1,
4507 value: ReftableRefValue::Peeled {
4508 target: tag_oid,
4509 peeled: peeled_oid,
4510 },
4511 },
4512 ],
4513 )],
4514 );
4515
4516 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4517 assert_eq!(
4518 store
4519 .read_ref("HEAD")
4520 .expect("test operation should succeed"),
4521 Some(RefTarget::Symbolic("refs/heads/main".into()))
4522 );
4523 assert_eq!(
4524 store
4525 .read_ref("refs/heads/main")
4526 .expect("test operation should succeed"),
4527 Some(RefTarget::Direct(head_oid))
4528 );
4529 assert_eq!(
4530 store
4531 .read_ref("refs/tags/v1.0")
4532 .expect("test operation should succeed"),
4533 Some(RefTarget::Direct(tag_oid))
4534 );
4535 let refs = store.list_refs().expect("test operation should succeed");
4536 assert_eq!(
4537 refs,
4538 vec![
4539 Ref {
4540 name: "refs/heads/main".into(),
4541 target: RefTarget::Direct(head_oid),
4542 },
4543 Ref {
4544 name: "refs/tags/v1.0".into(),
4545 target: RefTarget::Direct(tag_oid),
4546 },
4547 ]
4548 );
4549
4550 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4551 }
4552
4553 #[test]
4554 fn file_ref_store_applies_reftable_stack_overrides_and_deletions() {
4555 let git_dir = temp_git_dir();
4556 write_reftable_config(&git_dir);
4557 let first = ObjectId::from_hex(
4558 ObjectFormat::Sha1,
4559 "ce013625030ba8dba906f756967f9e9ca394464a",
4560 )
4561 .expect("test operation should succeed");
4562 let second = ObjectId::from_hex(
4563 ObjectFormat::Sha1,
4564 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
4565 )
4566 .expect("test operation should succeed");
4567 write_reftable_stack(
4568 &git_dir,
4569 &[
4570 (
4571 "000000000001-000000000001-base.ref",
4572 vec![
4573 sley_formats::ReftableRefRecord {
4574 name: "refs/heads/main".into(),
4575 update_index: 1,
4576 value: ReftableRefValue::Direct(first),
4577 },
4578 sley_formats::ReftableRefRecord {
4579 name: "refs/heads/topic".into(),
4580 update_index: 1,
4581 value: ReftableRefValue::Direct(second.clone()),
4582 },
4583 ],
4584 ),
4585 (
4586 "000000000002-000000000002-tip.ref",
4587 vec![
4588 sley_formats::ReftableRefRecord {
4589 name: "refs/heads/main".into(),
4590 update_index: 2,
4591 value: ReftableRefValue::Direct(second.clone()),
4592 },
4593 sley_formats::ReftableRefRecord {
4594 name: "refs/heads/topic".into(),
4595 update_index: 2,
4596 value: ReftableRefValue::Deletion,
4597 },
4598 ],
4599 ),
4600 ],
4601 );
4602
4603 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4604 assert_eq!(
4605 store
4606 .read_ref("refs/heads/main")
4607 .expect("test operation should succeed"),
4608 Some(RefTarget::Direct(second.clone()))
4609 );
4610 assert_eq!(
4611 store
4612 .read_ref("refs/heads/topic")
4613 .expect("test operation should succeed"),
4614 None
4615 );
4616 assert_eq!(
4617 store.list_refs().expect("test operation should succeed"),
4618 vec![Ref {
4619 name: "refs/heads/main".into(),
4620 target: RefTarget::Direct(second),
4621 }]
4622 );
4623
4624 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4625 }
4626
4627 #[test]
4628 fn file_ref_store_writes_reftable_transaction_table() {
4629 let git_dir = temp_git_dir();
4630 write_reftable_config(&git_dir);
4631 let first = ObjectId::from_hex(
4632 ObjectFormat::Sha1,
4633 "ce013625030ba8dba906f756967f9e9ca394464a",
4634 )
4635 .expect("test operation should succeed");
4636 let second = ObjectId::from_hex(
4637 ObjectFormat::Sha1,
4638 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
4639 )
4640 .expect("test operation should succeed");
4641 write_reftable_stack(
4642 &git_dir,
4643 &[(
4644 "000000000001-000000000001-base.ref",
4645 vec![sley_formats::ReftableRefRecord {
4646 name: "refs/heads/main".into(),
4647 update_index: 1,
4648 value: ReftableRefValue::Direct(first),
4649 }],
4650 )],
4651 );
4652
4653 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4654 let mut tx = store.transaction();
4655 tx.update(RefUpdate {
4656 name: "HEAD".into(),
4657 expected: None,
4658 new: RefTarget::Symbolic("refs/heads/main".into()),
4659 reflog: None,
4660 });
4661 tx.update(RefUpdate {
4662 name: "refs/heads/main".into(),
4663 expected: None,
4664 new: RefTarget::Direct(second.clone()),
4665 reflog: None,
4666 });
4667 tx.commit().expect("test operation should succeed");
4668
4669 assert_eq!(
4670 store
4671 .read_ref("HEAD")
4672 .expect("test operation should succeed"),
4673 Some(RefTarget::Symbolic("refs/heads/main".into()))
4674 );
4675 assert_eq!(
4676 store
4677 .read_ref("refs/heads/main")
4678 .expect("test operation should succeed"),
4679 Some(RefTarget::Direct(second.clone()))
4680 );
4681 assert_eq!(
4682 store
4683 .list_refs()
4684 .expect("test operation should succeed")
4685 .len(),
4686 1
4687 );
4688 assert!(!git_dir.join("HEAD").exists());
4689 let tables = fs::read_to_string(git_dir.join("reftable").join("tables.list"))
4690 .expect("test operation should succeed");
4691 assert_eq!(tables.lines().count(), 2);
4692 assert!(
4693 tables
4694 .lines()
4695 .last()
4696 .expect("test operation should succeed")
4697 .contains("sley"),
4698 "expected rust-written reftable in tables.list, got {tables}"
4699 );
4700
4701 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4702 }
4703
4704 #[test]
4705 fn file_ref_store_deletes_reftable_refs_with_tombstones() {
4706 let git_dir = temp_git_dir();
4707 write_reftable_config(&git_dir);
4708 let oid = ObjectId::from_hex(
4709 ObjectFormat::Sha1,
4710 "ce013625030ba8dba906f756967f9e9ca394464a",
4711 )
4712 .expect("test operation should succeed");
4713 write_reftable_stack(
4714 &git_dir,
4715 &[(
4716 "000000000001-000000000001-base.ref",
4717 vec![
4718 sley_formats::ReftableRefRecord {
4719 name: "refs/heads/main".into(),
4720 update_index: 1,
4721 value: ReftableRefValue::Direct(oid),
4722 },
4723 sley_formats::ReftableRefRecord {
4724 name: "refs/alias/main".into(),
4725 update_index: 1,
4726 value: ReftableRefValue::Symbolic("refs/heads/main".into()),
4727 },
4728 ],
4729 )],
4730 );
4731
4732 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4733 assert!(
4734 store
4735 .delete_symbolic_ref("refs/alias/main")
4736 .expect("test operation should succeed")
4737 );
4738 assert_eq!(
4739 store
4740 .read_ref("refs/alias/main")
4741 .expect("test operation should succeed"),
4742 None
4743 );
4744 let deleted = store
4745 .delete_ref("refs/heads/main")
4746 .expect("test operation should succeed");
4747 assert_eq!(deleted.oid, oid);
4748 assert_eq!(
4749 store
4750 .read_ref("refs/heads/main")
4751 .expect("test operation should succeed"),
4752 None
4753 );
4754 assert!(
4755 store
4756 .list_refs()
4757 .expect("test operation should succeed")
4758 .is_empty()
4759 );
4760 let tables = fs::read_to_string(git_dir.join("reftable").join("tables.list"))
4761 .expect("test operation should succeed");
4762 assert_eq!(tables.lines().count(), 3);
4763
4764 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4765 }
4766
4767 #[test]
4768 fn file_ref_store_deletes_packed_branch() {
4769 let git_dir = temp_git_dir();
4770 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4771 let branch_oid = ObjectId::from_hex(
4772 ObjectFormat::Sha1,
4773 "ce013625030ba8dba906f756967f9e9ca394464a",
4774 )
4775 .expect("test operation should succeed");
4776 let tag_oid = ObjectId::from_hex(
4777 ObjectFormat::Sha1,
4778 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
4779 )
4780 .expect("test operation should succeed");
4781 store
4782 .write_packed_refs(&[
4783 PackedRef {
4784 reference: Ref {
4785 name: "refs/heads/feature".into(),
4786 target: RefTarget::Direct(branch_oid),
4787 },
4788 peeled: None,
4789 },
4790 PackedRef {
4791 reference: Ref {
4792 name: "refs/tags/v1.0".into(),
4793 target: RefTarget::Direct(tag_oid),
4794 },
4795 peeled: None,
4796 },
4797 ])
4798 .expect("test operation should succeed");
4799 let deleted = store
4800 .delete_branch("feature")
4801 .expect("test operation should succeed");
4802 assert_eq!(deleted.name, "refs/heads/feature");
4803 assert_eq!(deleted.oid, branch_oid);
4804 assert_eq!(
4805 store
4806 .read_ref("refs/heads/feature")
4807 .expect("test operation should succeed"),
4808 None
4809 );
4810 assert_eq!(
4811 store
4812 .read_ref("refs/tags/v1.0")
4813 .expect("test operation should succeed"),
4814 Some(RefTarget::Direct(tag_oid))
4815 );
4816 assert!(!git_dir.join("packed-refs.lock").exists());
4817 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4818 }
4819
4820 #[test]
4821 fn file_ref_store_deletes_packed_tag() {
4822 let git_dir = temp_git_dir();
4823 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4824 let oid = ObjectId::from_hex(
4825 ObjectFormat::Sha1,
4826 "ce013625030ba8dba906f756967f9e9ca394464a",
4827 )
4828 .expect("test operation should succeed");
4829 store
4830 .write_packed_refs(&[PackedRef {
4831 reference: Ref {
4832 name: "refs/tags/v1.0".into(),
4833 target: RefTarget::Direct(oid),
4834 },
4835 peeled: None,
4836 }])
4837 .expect("test operation should succeed");
4838 let deleted = store
4839 .delete_tag("v1.0")
4840 .expect("test operation should succeed");
4841 assert_eq!(deleted.name, "refs/tags/v1.0");
4842 assert_eq!(deleted.oid, oid);
4843 assert_eq!(
4844 store
4845 .read_ref("refs/tags/v1.0")
4846 .expect("test operation should succeed"),
4847 None
4848 );
4849 assert!(!git_dir.join("packed-refs.lock").exists());
4850 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4851 }
4852
4853 #[test]
4854 fn file_ref_store_packs_loose_refs_and_prunes() {
4855 let git_dir = temp_git_dir();
4856 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4857 let main_oid = ObjectId::from_hex(
4858 ObjectFormat::Sha1,
4859 "ce013625030ba8dba906f756967f9e9ca394464a",
4860 )
4861 .expect("test operation should succeed");
4862 let tag_oid = ObjectId::from_hex(
4863 ObjectFormat::Sha1,
4864 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
4865 )
4866 .expect("test operation should succeed");
4867 let mut tx = store.transaction();
4868 tx.update(RefUpdate {
4869 name: "refs/heads/main".into(),
4870 expected: None,
4871 new: RefTarget::Direct(main_oid),
4872 reflog: None,
4873 });
4874 tx.update(RefUpdate {
4875 name: "refs/tags/v1.0".into(),
4876 expected: None,
4877 new: RefTarget::Direct(tag_oid),
4878 reflog: None,
4879 });
4880 tx.commit().expect("test operation should succeed");
4881
4882 let packed = store
4883 .pack_refs(true)
4884 .expect("test operation should succeed");
4885 assert_eq!(packed.len(), 2);
4886 assert_eq!(
4887 store
4888 .read_ref("refs/heads/main")
4889 .expect("test operation should succeed"),
4890 Some(RefTarget::Direct(main_oid))
4891 );
4892 assert_eq!(
4893 store
4894 .read_ref("refs/tags/v1.0")
4895 .expect("test operation should succeed"),
4896 Some(RefTarget::Direct(tag_oid))
4897 );
4898 assert!(!git_dir.join("refs").join("heads").join("main").exists());
4899 assert!(!git_dir.join("refs").join("tags").join("v1.0").exists());
4900 assert!(git_dir.join("packed-refs").exists());
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_without_pruning() {
4907 let git_dir = temp_git_dir();
4908 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4909 let oid = ObjectId::from_hex(
4910 ObjectFormat::Sha1,
4911 "ce013625030ba8dba906f756967f9e9ca394464a",
4912 )
4913 .expect("test operation should succeed");
4914 let mut tx = store.transaction();
4915 tx.update(RefUpdate {
4916 name: "refs/heads/main".into(),
4917 expected: None,
4918 new: RefTarget::Direct(oid),
4919 reflog: None,
4920 });
4921 tx.commit().expect("test operation should succeed");
4922
4923 let packed = store
4924 .pack_refs(false)
4925 .expect("test operation should succeed");
4926 assert_eq!(packed.len(), 1);
4927 assert!(git_dir.join("refs").join("heads").join("main").exists());
4928 assert_eq!(
4929 store
4930 .read_ref("refs/heads/main")
4931 .expect("test operation should succeed"),
4932 Some(RefTarget::Direct(oid))
4933 );
4934 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4935 }
4936
4937 #[test]
4938 fn file_ref_store_packs_loose_refs_with_peeled_ids() {
4939 let git_dir = temp_git_dir();
4940 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
4941 let tag_oid = ObjectId::from_hex(
4942 ObjectFormat::Sha1,
4943 "ce013625030ba8dba906f756967f9e9ca394464a",
4944 )
4945 .expect("test operation should succeed");
4946 let peeled_oid = ObjectId::from_hex(
4947 ObjectFormat::Sha1,
4948 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
4949 )
4950 .expect("test operation should succeed");
4951 let mut tx = store.transaction();
4952 tx.update(RefUpdate {
4953 name: "refs/tags/v1.0".into(),
4954 expected: None,
4955 new: RefTarget::Direct(tag_oid),
4956 reflog: None,
4957 });
4958 tx.commit().expect("test operation should succeed");
4959
4960 let packed = store
4961 .pack_refs_with_peeler(true, |name, oid| {
4962 if name == "refs/tags/v1.0" && oid == &tag_oid {
4963 Ok(Some(peeled_oid))
4964 } else {
4965 Ok(None)
4966 }
4967 })
4968 .expect("test operation should succeed");
4969 assert_eq!(packed.len(), 1);
4970 assert_eq!(packed[0].peeled, Some(peeled_oid));
4971 let bytes =
4972 fs::read_to_string(git_dir.join("packed-refs")).expect("test operation should succeed");
4973 assert!(bytes.contains(&format!("^{peeled_oid}\n")));
4974 assert!(!git_dir.join("refs").join("tags").join("v1.0").exists());
4975 fs::remove_dir_all(git_dir).expect("test operation should succeed");
4976 }
4977
4978 fn reflog_entry(new_oid: &ObjectId, timestamp: i64, message: &str) -> ReflogEntry {
4979 ReflogEntry {
4980 old_oid: zero_oid(new_oid.format()).expect("test operation should succeed"),
4981 new_oid: *new_oid,
4982 committer: format!("Git Rs <sley@example.invalid> {timestamp} +0000").into_bytes(),
4983 message: message.as_bytes().to_vec(),
4984 }
4985 }
4986
4987 #[test]
4988 fn expire_reflog_drops_old_entries_and_keeps_latest() {
4989 let oid_a = ObjectId::from_hex(
4990 ObjectFormat::Sha1,
4991 "ce013625030ba8dba906f756967f9e9ca394464a",
4992 )
4993 .expect("test operation should succeed");
4994 let oid_b = ObjectId::from_hex(
4995 ObjectFormat::Sha1,
4996 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
4997 )
4998 .expect("test operation should succeed");
4999 let oid_c = ObjectId::from_hex(
5000 ObjectFormat::Sha1,
5001 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
5002 )
5003 .expect("test operation should succeed");
5004 let entries = vec![
5005 reflog_entry(&oid_a, 10, "oldest"),
5006 reflog_entry(&oid_b, 100, "middle"),
5007 reflog_entry(&oid_c, 20, "latest"),
5008 ];
5009
5010 let retained =
5013 expire_reflog(&entries, 50, None, |_| true).expect("test operation should succeed");
5014 assert_eq!(retained.len(), 2);
5015 assert_eq!(retained[0].message, b"middle");
5016 assert_eq!(retained[1].message, b"latest");
5017 }
5018
5019 #[test]
5020 fn expire_reflog_applies_stricter_unreachable_cutoff() {
5021 let reachable = ObjectId::from_hex(
5022 ObjectFormat::Sha1,
5023 "ce013625030ba8dba906f756967f9e9ca394464a",
5024 )
5025 .expect("test operation should succeed");
5026 let unreachable = ObjectId::from_hex(
5027 ObjectFormat::Sha1,
5028 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
5029 )
5030 .expect("test operation should succeed");
5031 let tip = ObjectId::from_hex(
5032 ObjectFormat::Sha1,
5033 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
5034 )
5035 .expect("test operation should succeed");
5036 let entries = vec![
5039 reflog_entry(&reachable, 100, "reachable"),
5040 reflog_entry(&unreachable, 100, "unreachable"),
5041 reflog_entry(&tip, 200, "tip"),
5042 ];
5043 let retained = expire_reflog(&entries, 50, Some(150), |oid| {
5044 oid == &reachable || oid == &tip
5045 })
5046 .expect("test operation should succeed");
5047 assert_eq!(retained.len(), 2);
5048 assert_eq!(retained[0].message, b"reachable");
5049 assert_eq!(retained[1].message, b"tip");
5050 }
5051
5052 #[test]
5053 fn expire_reflog_keeps_single_entry_below_cutoff() {
5054 let oid = ObjectId::from_hex(
5055 ObjectFormat::Sha1,
5056 "ce013625030ba8dba906f756967f9e9ca394464a",
5057 )
5058 .expect("test operation should succeed");
5059 let entries = vec![reflog_entry(&oid, 1, "only")];
5060 let retained = expire_reflog(&entries, i64::MAX, Some(i64::MAX), |_| false)
5061 .expect("test operation should succeed");
5062 assert_eq!(retained.len(), 1);
5063 assert_eq!(retained[0].message, b"only");
5064 }
5065
5066 #[test]
5067 fn file_ref_store_expire_reflog_file_rewrites_and_dry_runs() {
5068 let git_dir = temp_git_dir();
5069 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5070 let first = ObjectId::from_hex(
5071 ObjectFormat::Sha1,
5072 "ce013625030ba8dba906f756967f9e9ca394464a",
5073 )
5074 .expect("test operation should succeed");
5075 let second = ObjectId::from_hex(
5076 ObjectFormat::Sha1,
5077 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
5078 )
5079 .expect("test operation should succeed");
5080 store
5081 .write_reflog(
5082 "refs/heads/main",
5083 &[
5084 reflog_entry(&first, 10, "old"),
5085 reflog_entry(&second, 100, "new"),
5086 ],
5087 )
5088 .expect("test operation should succeed");
5089
5090 let would_remove = store
5092 .expire_reflog_file("refs/heads/main", 50, None, false, |_| true)
5093 .expect("test operation should succeed");
5094 assert_eq!(would_remove, 1);
5095 assert_eq!(
5096 store
5097 .read_reflog("refs/heads/main")
5098 .expect("test operation should succeed")
5099 .len(),
5100 2
5101 );
5102
5103 let removed = store
5105 .expire_reflog_file("refs/heads/main", 50, None, true, |_| true)
5106 .expect("test operation should succeed");
5107 assert_eq!(removed, 1);
5108 let log = store
5109 .read_reflog("refs/heads/main")
5110 .expect("test operation should succeed");
5111 assert_eq!(log.len(), 1);
5112 assert_eq!(log[0].new_oid, second);
5113 assert!(
5114 !git_dir
5115 .join("logs")
5116 .join("refs")
5117 .join("heads")
5118 .join("main.lock")
5119 .exists()
5120 );
5121 fs::remove_dir_all(git_dir).expect("test operation should succeed");
5122 }
5123
5124 #[test]
5125 fn file_ref_transaction_commits_all_refs_atomically() {
5126 let git_dir = temp_git_dir();
5127 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5128 let main_oid = ObjectId::from_hex(
5129 ObjectFormat::Sha1,
5130 "ce013625030ba8dba906f756967f9e9ca394464a",
5131 )
5132 .expect("test operation should succeed");
5133 let topic_oid = ObjectId::from_hex(
5134 ObjectFormat::Sha1,
5135 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
5136 )
5137 .expect("test operation should succeed");
5138 let tag_oid = ObjectId::from_hex(
5139 ObjectFormat::Sha1,
5140 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
5141 )
5142 .expect("test operation should succeed");
5143 let mut tx = store.transaction();
5144 tx.update(RefUpdate {
5145 name: "refs/heads/main".into(),
5146 expected: None,
5147 new: RefTarget::Direct(main_oid),
5148 reflog: Some(reflog_entry(&main_oid, 0, "create main")),
5149 });
5150 tx.update(RefUpdate {
5151 name: "refs/heads/topic".into(),
5152 expected: None,
5153 new: RefTarget::Direct(topic_oid),
5154 reflog: None,
5155 });
5156 tx.update(RefUpdate {
5157 name: "refs/tags/v1.0".into(),
5158 expected: None,
5159 new: RefTarget::Direct(tag_oid),
5160 reflog: None,
5161 });
5162 tx.commit().expect("test operation should succeed");
5163
5164 assert_eq!(
5165 store
5166 .read_ref("refs/heads/main")
5167 .expect("test operation should succeed"),
5168 Some(RefTarget::Direct(main_oid))
5169 );
5170 assert_eq!(
5171 store
5172 .read_ref("refs/heads/topic")
5173 .expect("test operation should succeed"),
5174 Some(RefTarget::Direct(topic_oid))
5175 );
5176 assert_eq!(
5177 store
5178 .read_ref("refs/tags/v1.0")
5179 .expect("test operation should succeed"),
5180 Some(RefTarget::Direct(tag_oid))
5181 );
5182 let main_log = store
5183 .read_reflog("refs/heads/main")
5184 .expect("test operation should succeed");
5185 assert_eq!(main_log.len(), 1);
5186 assert_eq!(main_log[0].new_oid, main_oid);
5187 assert!(
5189 !git_dir
5190 .join("refs")
5191 .join("heads")
5192 .join("main.lock")
5193 .exists()
5194 );
5195 assert!(
5196 !git_dir
5197 .join("refs")
5198 .join("heads")
5199 .join("topic.lock")
5200 .exists()
5201 );
5202 assert!(!git_dir.join("refs").join("tags").join("v1.0.lock").exists());
5203 fs::remove_dir_all(git_dir).expect("test operation should succeed");
5204 }
5205
5206 #[test]
5207 fn file_ref_transaction_rolls_back_all_refs_on_expected_mismatch() {
5208 let git_dir = temp_git_dir();
5209 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5210 let old_topic = ObjectId::from_hex(
5211 ObjectFormat::Sha1,
5212 "ce013625030ba8dba906f756967f9e9ca394464a",
5213 )
5214 .expect("test operation should succeed");
5215 let new_main = ObjectId::from_hex(
5216 ObjectFormat::Sha1,
5217 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
5218 )
5219 .expect("test operation should succeed");
5220 let new_tag = ObjectId::from_hex(
5221 ObjectFormat::Sha1,
5222 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
5223 )
5224 .expect("test operation should succeed");
5225 let wrong_expected = ObjectId::from_hex(
5226 ObjectFormat::Sha1,
5227 "0000000000000000000000000000000000000001",
5228 )
5229 .expect("test operation should succeed");
5230
5231 let mut seed = store.transaction();
5234 seed.update(RefUpdate {
5235 name: "refs/heads/topic".into(),
5236 expected: None,
5237 new: RefTarget::Direct(old_topic.clone()),
5238 reflog: None,
5239 });
5240 seed.commit().expect("test operation should succeed");
5241
5242 let mut tx = store.transaction();
5243 tx.update(RefUpdate {
5245 name: "refs/heads/main".into(),
5246 expected: None,
5247 new: RefTarget::Direct(new_main.clone()),
5248 reflog: Some(reflog_entry(&new_main, 0, "create main")),
5249 });
5250 tx.update(RefUpdate {
5252 name: "refs/heads/topic".into(),
5253 expected: Some(RefTarget::Direct(wrong_expected)),
5254 new: RefTarget::Direct(new_main.clone()),
5255 reflog: None,
5256 });
5257 tx.update(RefUpdate {
5259 name: "refs/tags/v1.0".into(),
5260 expected: None,
5261 new: RefTarget::Direct(new_tag),
5262 reflog: None,
5263 });
5264 let result = tx.commit();
5265 assert!(result.is_err());
5266
5267 assert_eq!(
5270 store
5271 .read_ref("refs/heads/main")
5272 .expect("test operation should succeed"),
5273 None
5274 );
5275 assert_eq!(
5276 store
5277 .read_ref("refs/heads/topic")
5278 .expect("test operation should succeed"),
5279 Some(RefTarget::Direct(old_topic))
5280 );
5281 assert_eq!(
5282 store
5283 .read_ref("refs/tags/v1.0")
5284 .expect("test operation should succeed"),
5285 None
5286 );
5287 assert!(
5288 store
5289 .read_reflog("refs/heads/main")
5290 .expect("test operation should succeed")
5291 .is_empty()
5292 );
5293
5294 assert!(
5296 !git_dir
5297 .join("refs")
5298 .join("heads")
5299 .join("main.lock")
5300 .exists()
5301 );
5302 assert!(
5303 !git_dir
5304 .join("refs")
5305 .join("heads")
5306 .join("topic.lock")
5307 .exists()
5308 );
5309 assert!(!git_dir.join("refs").join("tags").join("v1.0.lock").exists());
5310 fs::remove_dir_all(git_dir).expect("test operation should succeed");
5311 }
5312
5313 #[test]
5314 fn file_ref_transaction_mixes_update_and_delete() {
5315 let git_dir = temp_git_dir();
5316 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5317 let old_main = ObjectId::from_hex(
5318 ObjectFormat::Sha1,
5319 "ce013625030ba8dba906f756967f9e9ca394464a",
5320 )
5321 .expect("test operation should succeed");
5322 let new_topic = ObjectId::from_hex(
5323 ObjectFormat::Sha1,
5324 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
5325 )
5326 .expect("test operation should succeed");
5327 let mut seed = store.transaction();
5328 seed.update(RefUpdate {
5329 name: "refs/heads/main".into(),
5330 expected: None,
5331 new: RefTarget::Direct(old_main),
5332 reflog: None,
5333 });
5334 seed.commit().expect("test operation should succeed");
5335
5336 let mut tx = store.transaction();
5337 tx.update(RefUpdate {
5338 name: "refs/heads/topic".into(),
5339 expected: None,
5340 new: RefTarget::Direct(new_topic),
5341 reflog: None,
5342 });
5343 tx.delete_with_precondition(
5344 "refs/heads/main",
5345 RefDeletePrecondition::Direct(Some(old_main)),
5346 None,
5347 );
5348 tx.commit().expect("test operation should succeed");
5349
5350 assert_eq!(
5351 store
5352 .read_ref("refs/heads/main")
5353 .expect("test operation should succeed"),
5354 None
5355 );
5356 assert_eq!(
5357 store
5358 .read_ref("refs/heads/topic")
5359 .expect("test operation should succeed"),
5360 Some(RefTarget::Direct(new_topic))
5361 );
5362 fs::remove_dir_all(git_dir).expect("test operation should succeed");
5363 }
5364
5365 #[test]
5366 fn file_ref_transaction_stale_delete_rolls_back_update() {
5367 let git_dir = temp_git_dir();
5368 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5369 let old_oid = ObjectId::from_hex(
5370 ObjectFormat::Sha1,
5371 "ce013625030ba8dba906f756967f9e9ca394464a",
5372 )
5373 .expect("test operation should succeed");
5374 let new_oid = ObjectId::from_hex(
5375 ObjectFormat::Sha1,
5376 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
5377 )
5378 .expect("test operation should succeed");
5379 let mut seed = store.transaction();
5380 for name in ["refs/heads/main", "refs/heads/topic"] {
5381 seed.update(RefUpdate {
5382 name: name.into(),
5383 expected: None,
5384 new: RefTarget::Direct(old_oid),
5385 reflog: None,
5386 });
5387 }
5388 seed.commit().expect("test operation should succeed");
5389
5390 let mut tx = store.transaction();
5391 tx.update(RefUpdate {
5392 name: "refs/heads/topic".into(),
5393 expected: None,
5394 new: RefTarget::Direct(new_oid),
5395 reflog: None,
5396 });
5397 tx.delete_with_precondition(
5398 "refs/heads/main",
5399 RefDeletePrecondition::Direct(Some(new_oid)),
5400 None,
5401 );
5402 let err = tx.commit().expect_err("stale delete must abort");
5403 assert!(err.to_string().contains("expected ref refs/heads/main"));
5404
5405 assert_eq!(
5406 store
5407 .read_ref("refs/heads/main")
5408 .expect("test operation should succeed"),
5409 Some(RefTarget::Direct(old_oid))
5410 );
5411 assert_eq!(
5412 store
5413 .read_ref("refs/heads/topic")
5414 .expect("test operation should succeed"),
5415 Some(RefTarget::Direct(old_oid))
5416 );
5417 fs::remove_dir_all(git_dir).expect("test operation should succeed");
5418 }
5419
5420 #[test]
5421 fn file_ref_transaction_rejects_duplicate_mixed_ref() {
5422 let git_dir = temp_git_dir();
5423 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5424 let oid = ObjectId::from_hex(
5425 ObjectFormat::Sha1,
5426 "ce013625030ba8dba906f756967f9e9ca394464a",
5427 )
5428 .expect("test operation should succeed");
5429 let mut tx = store.transaction();
5430 tx.update(RefUpdate {
5431 name: "refs/heads/main".into(),
5432 expected: None,
5433 new: RefTarget::Direct(oid),
5434 reflog: None,
5435 });
5436 tx.delete_with_precondition("refs/heads/main", RefDeletePrecondition::Any, None);
5437
5438 let err = tx.commit().expect_err("duplicate ref must fail");
5439 assert!(err.to_string().contains("refs/heads/main"));
5440 assert_eq!(
5441 store
5442 .read_ref("refs/heads/main")
5443 .expect("test operation should succeed"),
5444 None
5445 );
5446 fs::remove_dir_all(git_dir).expect("test operation should succeed");
5447 }
5448
5449 #[test]
5450 fn file_ref_transaction_deletes_symbolic_ref_with_immediate_expectation() {
5451 let git_dir = temp_git_dir();
5452 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5453 let oid = ObjectId::from_hex(
5454 ObjectFormat::Sha1,
5455 "ce013625030ba8dba906f756967f9e9ca394464a",
5456 )
5457 .expect("test operation should succeed");
5458 let mut seed = store.transaction();
5459 seed.update(RefUpdate {
5460 name: "refs/heads/main".into(),
5461 expected: None,
5462 new: RefTarget::Direct(oid),
5463 reflog: None,
5464 });
5465 seed.update(RefUpdate {
5466 name: "refs/aliases/main".into(),
5467 expected: None,
5468 new: RefTarget::Symbolic("refs/heads/main".into()),
5469 reflog: None,
5470 });
5471 seed.commit().expect("test operation should succeed");
5472
5473 let mut tx = store.transaction();
5474 tx.delete_with_precondition(
5475 "refs/aliases/main",
5476 RefDeletePrecondition::Immediate(RefTarget::Symbolic("refs/heads/main".into())),
5477 None,
5478 );
5479 tx.commit().expect("test operation should succeed");
5480
5481 assert_eq!(
5482 store
5483 .read_ref("refs/aliases/main")
5484 .expect("test operation should succeed"),
5485 None
5486 );
5487 assert_eq!(
5488 store
5489 .read_ref("refs/heads/main")
5490 .expect("test operation should succeed"),
5491 Some(RefTarget::Direct(oid))
5492 );
5493 fs::remove_dir_all(git_dir).expect("test operation should succeed");
5494 }
5495
5496 #[test]
5497 fn file_ref_transaction_rolls_back_delete_after_late_write_failure() {
5498 let git_dir = temp_git_dir();
5499 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5500 let old_oid = ObjectId::from_hex(
5501 ObjectFormat::Sha1,
5502 "ce013625030ba8dba906f756967f9e9ca394464a",
5503 )
5504 .expect("test operation should succeed");
5505 let new_oid = ObjectId::from_hex(
5506 ObjectFormat::Sha1,
5507 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
5508 )
5509 .expect("test operation should succeed");
5510 let mut seed = store.transaction();
5511 for name in ["refs/heads/main", "refs/heads/topic"] {
5512 seed.update(RefUpdate {
5513 name: name.into(),
5514 expected: None,
5515 new: RefTarget::Direct(old_oid),
5516 reflog: None,
5517 });
5518 }
5519 seed.commit().expect("test operation should succeed");
5520
5521 set_fail_loose_commit_action_for_test(Some(1));
5522 let mut tx = store.transaction();
5523 tx.delete_with_precondition(
5524 "refs/heads/main",
5525 RefDeletePrecondition::Direct(Some(old_oid)),
5526 None,
5527 );
5528 tx.update(RefUpdate {
5529 name: "refs/heads/topic".into(),
5530 expected: None,
5531 new: RefTarget::Direct(new_oid),
5532 reflog: None,
5533 });
5534 let err = tx.commit().expect_err("injected failure must abort");
5535 assert!(
5536 err.to_string()
5537 .contains("injected loose ref transaction failure")
5538 );
5539
5540 assert_eq!(
5541 store
5542 .read_ref("refs/heads/main")
5543 .expect("test operation should succeed"),
5544 Some(RefTarget::Direct(old_oid))
5545 );
5546 assert_eq!(
5547 store
5548 .read_ref("refs/heads/topic")
5549 .expect("test operation should succeed"),
5550 Some(RefTarget::Direct(old_oid))
5551 );
5552 assert!(
5553 !git_dir
5554 .join("refs")
5555 .join("heads")
5556 .join("main.lock")
5557 .exists()
5558 );
5559 assert!(
5560 !git_dir
5561 .join("refs")
5562 .join("heads")
5563 .join("topic.lock")
5564 .exists()
5565 );
5566 fs::remove_dir_all(git_dir).expect("test operation should succeed");
5567 }
5568
5569 fn temp_git_dir() -> PathBuf {
5570 let path = std::env::temp_dir().join(format!(
5571 "sley-refs-{}-{}",
5572 std::process::id(),
5573 TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
5574 ));
5575 fs::create_dir_all(&path).expect("test operation should succeed");
5576 path
5577 }
5578
5579 fn zero_oid(format: ObjectFormat) -> Result<ObjectId> {
5580 Ok(ObjectId::null(format))
5581 }
5582
5583 fn write_reftable_config(git_dir: &Path) {
5584 fs::write(
5585 git_dir.join("config"),
5586 b"[core]\n\trepositoryformatversion = 1\n[extensions]\n\trefStorage = reftable\n",
5587 )
5588 .expect("test operation should succeed");
5589 }
5590
5591 fn write_reftable_stack(
5592 git_dir: &Path,
5593 tables: &[(&str, Vec<sley_formats::ReftableRefRecord>)],
5594 ) {
5595 let reftable_dir = git_dir.join("reftable");
5596 fs::create_dir_all(&reftable_dir).expect("test operation should succeed");
5597 let mut list = String::new();
5598 for (idx, (name, refs)) in tables.iter().enumerate() {
5599 let update_index = (idx + 1) as u64;
5600 let bytes = sley_formats::Reftable::write_ref_only(
5601 ObjectFormat::Sha1,
5602 update_index,
5603 update_index,
5604 refs,
5605 )
5606 .expect("test operation should succeed");
5607 fs::write(reftable_dir.join(name), bytes).expect("test operation should succeed");
5608 list.push_str(name);
5609 list.push('\n');
5610 }
5611 fs::write(reftable_dir.join("tables.list"), list).expect("test operation should succeed");
5612 }
5613}