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