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