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::env;
14use std::fmt;
15use std::fs;
16use std::io::Write;
17use std::ops::Deref;
18use std::path::{Path, PathBuf};
19use std::thread;
20use std::time::Duration;
21use std::time::{SystemTime, UNIX_EPOCH};
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum RefTarget {
25 Direct(ObjectId),
26 Symbolic(String),
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct Ref {
31 pub name: String,
32 pub target: RefTarget,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct RefDelete {
37 pub name: String,
38 pub oid: ObjectId,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct DeleteRef {
43 pub name: String,
44 pub expected_old: Option<ObjectId>,
45 pub reflog: Option<DeleteRefReflog>,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct DeleteRefReflog {
50 pub committer: Vec<u8>,
51 pub message: Vec<u8>,
52}
53
54#[derive(Debug)]
55pub enum RefDeleteError {
56 NotFound,
57 ExpectedMismatch {
58 expected: Option<ObjectId>,
59 actual: Option<ObjectId>,
60 },
61 Locked,
62 InvalidName,
63 Io(std::io::Error),
64}
65
66impl fmt::Display for RefDeleteError {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 match self {
69 Self::NotFound => f.write_str("ref not found"),
70 Self::ExpectedMismatch { expected, actual } => {
71 write!(
72 f,
73 "ref expected old oid mismatch: expected {:?}, actual {:?}",
74 expected, actual
75 )
76 }
77 Self::Locked => f.write_str("ref is locked"),
78 Self::InvalidName => f.write_str("invalid ref name"),
79 Self::Io(err) => write!(f, "io error: {err}"),
80 }
81 }
82}
83
84impl std::error::Error for RefDeleteError {}
85
86impl From<std::io::Error> for RefDeleteError {
87 fn from(value: std::io::Error) -> Self {
88 Self::Io(value)
89 }
90}
91
92fn parse_leading_oid(format: ObjectFormat, value: &str) -> Option<ObjectId> {
95 let hexsz = format.hex_len();
96 let bytes = value.as_bytes();
97 if bytes.len() < hexsz {
98 return None;
99 }
100 if bytes.len() > hexsz && !bytes[hexsz].is_ascii_whitespace() {
101 return None;
102 }
103 ObjectId::from_hex(format, &value[..hexsz]).ok()
104}
105
106pub fn parse_loose_ref(format: ObjectFormat, name: impl Into<String>, bytes: &[u8]) -> Result<Ref> {
107 let name = name.into();
108 let value = std::str::from_utf8(bytes)
109 .map_err(|err| GitError::InvalidFormat(err.to_string()))?
110 .trim_end_matches('\n');
111 if name == "FETCH_HEAD" {
112 let oid = value
113 .lines()
114 .find_map(|line| line.split_whitespace().next())
115 .ok_or_else(|| GitError::InvalidFormat("FETCH_HEAD is empty".into()))?;
116 return Ok(Ref {
117 name,
118 target: RefTarget::Direct(ObjectId::from_hex(format, oid)?),
119 });
120 }
121 let target = if let Some(symbolic) = value.strip_prefix("ref: ") {
122 RefTarget::Symbolic(symbolic.to_string())
123 } else {
124 let oid = parse_leading_oid(format, value).ok_or_else(|| {
129 GitError::InvalidFormat(format!(
130 "reference {name} has neither a valid OID nor a target"
131 ))
132 })?;
133 RefTarget::Direct(oid)
134 };
135 Ok(Ref { name, target })
136}
137
138pub fn write_loose_ref(reference: &Ref) -> Vec<u8> {
139 match &reference.target {
140 RefTarget::Direct(oid) => format!("{oid}\n").into_bytes(),
141 RefTarget::Symbolic(target) => format!("ref: {target}\n").into_bytes(),
142 }
143}
144
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct PackedRef {
147 pub reference: Ref,
148 pub peeled: Option<ObjectId>,
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum PackRefDecision {
153 Pack { peeled: Option<ObjectId> },
154 Skip,
155}
156
157pub fn parse_packed_refs(format: ObjectFormat, bytes: &[u8]) -> Result<Vec<PackedRef>> {
158 parse_packed_refs_filtered(format, bytes, |_| true)
159}
160
161fn parse_packed_refs_with_prefix(
162 format: ObjectFormat,
163 bytes: &[u8],
164 prefix: &str,
165) -> Result<Vec<PackedRef>> {
166 if !bytes.is_empty() && !bytes.ends_with(b"\n") {
167 let line_start = bytes
168 .iter()
169 .rposition(|byte| *byte == b'\n')
170 .map_or(0, |index| index + 1);
171 let line = String::from_utf8_lossy(&bytes[line_start..]);
172 return Err(GitError::InvalidFormat(format!(
173 "fatal: unterminated line in .git/packed-refs: {line}"
174 )));
175 }
176 let text =
177 std::str::from_utf8(bytes).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
178 let mut refs: Vec<PackedRef> = Vec::new();
179 let mut saw_ref = false;
180 let mut included_last_ref = false;
181 for raw_line in text.lines() {
182 let line = raw_line.trim_end();
183 if line.is_empty() || line.starts_with('#') {
184 continue;
185 }
186 if let Some(peeled) = line.strip_prefix('^') {
187 if !saw_ref {
188 return Err(GitError::InvalidFormat(
189 "peeled packed ref without preceding ref".into(),
190 ));
191 }
192 if included_last_ref {
193 let oid = ObjectId::from_hex(format, peeled)?;
194 if let Some(last) = refs.last_mut() {
195 last.peeled = Some(oid);
196 }
197 }
198 continue;
199 }
200 let (oid, name) = line
201 .split_once(' ')
202 .ok_or_else(|| packed_refs_unexpected_line(line))?;
203 if oid.len() != format.hex_len()
210 || !oid.bytes().all(|byte| byte.is_ascii_hexdigit())
211 || validate_ref_name_for_read(name).is_err()
212 {
213 return Err(packed_refs_unexpected_line(line));
214 }
215 saw_ref = true;
216 included_last_ref = name.starts_with(prefix);
217 if !included_last_ref {
218 continue;
219 }
220 let oid = ObjectId::from_hex(format, oid)?;
221 refs.push(PackedRef {
222 reference: Ref {
223 name: name.into(),
224 target: RefTarget::Direct(oid),
225 },
226 peeled: None,
227 });
228 }
229 Ok(refs)
230}
231
232fn parse_packed_refs_filtered(
233 format: ObjectFormat,
234 bytes: &[u8],
235 mut include: impl FnMut(&str) -> bool,
236) -> Result<Vec<PackedRef>> {
237 if !bytes.is_empty() && !bytes.ends_with(b"\n") {
238 let line_start = bytes
239 .iter()
240 .rposition(|byte| *byte == b'\n')
241 .map_or(0, |index| index + 1);
242 let line = String::from_utf8_lossy(&bytes[line_start..]);
243 return Err(GitError::InvalidFormat(format!(
244 "fatal: unterminated line in .git/packed-refs: {line}"
245 )));
246 }
247 let text =
248 std::str::from_utf8(bytes).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
249 let mut refs: Vec<PackedRef> = Vec::new();
250 let mut saw_ref = false;
251 let mut included_last_ref = false;
252 for raw_line in text.lines() {
253 let line = raw_line.trim_end();
254 if line.is_empty() || line.starts_with('#') {
255 continue;
256 }
257 if let Some(peeled) = line.strip_prefix('^') {
258 let oid = ObjectId::from_hex(format, peeled)?;
259 if !saw_ref {
260 return Err(GitError::InvalidFormat(
261 "peeled packed ref without preceding ref".into(),
262 ));
263 }
264 if included_last_ref && let Some(last) = refs.last_mut() {
265 last.peeled = Some(oid);
266 }
267 continue;
268 }
269 let (oid, name) = line
270 .split_once(' ')
271 .ok_or_else(|| packed_refs_unexpected_line(line))?;
272 if oid.len() != format.hex_len()
273 || !oid.bytes().all(|byte| byte.is_ascii_hexdigit())
274 || validate_ref_name_for_read(name).is_err()
275 {
276 return Err(packed_refs_unexpected_line(line));
277 }
278 let oid = ObjectId::from_hex(format, oid)?;
279 saw_ref = true;
280 included_last_ref = include(name);
281 if included_last_ref {
282 refs.push(PackedRef {
283 reference: Ref {
284 name: name.into(),
285 target: RefTarget::Direct(oid),
286 },
287 peeled: None,
288 });
289 }
290 }
291 Ok(refs)
292}
293
294fn packed_ref_names_with_prefix(
295 format: ObjectFormat,
296 bytes: &[u8],
297 prefix: &str,
298 strip_prefix: bool,
299 names: &mut Vec<String>,
300) -> Result<()> {
301 if !bytes.is_empty() && !bytes.ends_with(b"\n") {
302 let line_start = bytes
303 .iter()
304 .rposition(|byte| *byte == b'\n')
305 .map_or(0, |index| index + 1);
306 let line = String::from_utf8_lossy(&bytes[line_start..]);
307 return Err(GitError::InvalidFormat(format!(
308 "fatal: unterminated line in .git/packed-refs: {line}"
309 )));
310 }
311 let text =
312 std::str::from_utf8(bytes).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
313 let mut saw_ref = false;
314 for raw_line in text.lines() {
315 let line = raw_line.trim_end();
316 if line.is_empty() || line.starts_with('#') {
317 continue;
318 }
319 if line.starts_with('^') {
320 if !saw_ref {
321 return Err(GitError::InvalidFormat(
322 "peeled packed ref without preceding ref".into(),
323 ));
324 }
325 continue;
326 }
327 let (oid, name) = line
328 .split_once(' ')
329 .ok_or_else(|| packed_refs_unexpected_line(line))?;
330 if oid.len() != format.hex_len()
334 || !oid.bytes().all(|byte| byte.is_ascii_hexdigit())
335 || validate_ref_name_for_read(name).is_err()
336 {
337 return Err(packed_refs_unexpected_line(line));
338 }
339 saw_ref = true;
340 if !name.starts_with(prefix) {
341 continue;
342 }
343 let name = if strip_prefix {
344 &name[prefix.len()..]
345 } else {
346 name
347 };
348 names.push(name.to_owned());
349 }
350 Ok(())
351}
352
353fn packed_refs_unexpected_line(line: &str) -> GitError {
354 GitError::InvalidFormat(format!(
355 "fatal: unexpected line in .git/packed-refs: {line}"
356 ))
357}
358
359fn packed_refs_have_prefix(format: ObjectFormat, bytes: &[u8], prefix: &str) -> Result<bool> {
360 if !bytes.is_empty() && !bytes.ends_with(b"\n") {
361 let line_start = bytes
362 .iter()
363 .rposition(|byte| *byte == b'\n')
364 .map_or(0, |index| index + 1);
365 let line = String::from_utf8_lossy(&bytes[line_start..]);
366 return Err(GitError::InvalidFormat(format!(
367 "fatal: unterminated line in .git/packed-refs: {line}"
368 )));
369 }
370 let text =
371 std::str::from_utf8(bytes).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
372 let mut found = false;
373 let mut saw_ref = false;
374 for raw_line in text.lines() {
375 let line = raw_line.trim_end();
376 if line.is_empty() || line.starts_with('#') {
377 continue;
378 }
379 if let Some(peeled) = line.strip_prefix('^') {
380 ObjectId::from_hex(format, peeled)?;
381 if !saw_ref {
382 return Err(GitError::InvalidFormat(
383 "peeled packed ref without preceding ref".into(),
384 ));
385 }
386 continue;
387 }
388 let (oid, name) = line
389 .split_once(' ')
390 .ok_or_else(|| packed_refs_unexpected_line(line))?;
391 if oid.len() != format.hex_len()
392 || !oid.bytes().all(|byte| byte.is_ascii_hexdigit())
393 || validate_ref_name_for_read(name).is_err()
394 {
395 return Err(packed_refs_unexpected_line(line));
396 }
397 ObjectId::from_hex(format, oid)?;
398 saw_ref = true;
399 found |= name.starts_with(prefix);
400 }
401 Ok(found)
402}
403
404pub fn write_packed_refs(refs: &[PackedRef]) -> Result<Vec<u8>> {
405 let mut refs = refs.to_vec();
406 refs.sort_by(|left, right| left.reference.name.cmp(&right.reference.name));
407 let mut out = b"# pack-refs with: peeled fully-peeled sorted \n".to_vec();
408 for packed in refs {
409 validate_ref_name(&packed.reference.name)?;
410 let RefTarget::Direct(oid) = &packed.reference.target else {
411 return Err(GitError::InvalidFormat(format!(
412 "packed ref {} is symbolic",
413 packed.reference.name
414 )));
415 };
416 out.extend_from_slice(oid.to_hex().as_bytes());
417 out.push(b' ');
418 out.extend_from_slice(packed.reference.name.as_bytes());
419 out.push(b'\n');
420 if let Some(peeled) = packed.peeled {
421 out.push(b'^');
422 out.extend_from_slice(peeled.to_hex().as_bytes());
423 out.push(b'\n');
424 }
425 }
426 Ok(out)
427}
428
429#[derive(Debug, Clone, PartialEq, Eq)]
430pub struct ReflogEntry {
431 pub old_oid: ObjectId,
432 pub new_oid: ObjectId,
433 pub committer: Vec<u8>,
434 pub message: Vec<u8>,
435}
436
437impl ReflogEntry {
438 pub fn to_line(&self) -> Vec<u8> {
439 let mut out = Vec::new();
440 out.extend_from_slice(self.old_oid.to_hex().as_bytes());
441 out.push(b' ');
442 out.extend_from_slice(self.new_oid.to_hex().as_bytes());
443 out.push(b' ');
444 out.extend_from_slice(&self.committer);
445 if !self.message.is_empty() {
446 out.push(b'\t');
447 out.extend_from_slice(&self.message);
448 }
449 out.push(b'\n');
450 out
451 }
452
453 pub fn timestamp_seconds(&self) -> Result<i64> {
454 let committer = std::str::from_utf8(&self.committer)
455 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
456 let Some((before_tz, _tz)) = committer.rsplit_once(' ') else {
457 return Err(GitError::InvalidFormat(
458 "reflog committer is missing timezone".into(),
459 ));
460 };
461 let Some((_identity, timestamp)) = before_tz.rsplit_once(' ') else {
462 return Err(GitError::InvalidFormat(
463 "reflog committer is missing timestamp".into(),
464 ));
465 };
466 timestamp
467 .parse::<i64>()
468 .map_err(|err| GitError::InvalidFormat(err.to_string()))
469 }
470}
471
472pub fn parse_reflog(format: ObjectFormat, bytes: &[u8]) -> Result<Vec<ReflogEntry>> {
473 let text =
474 std::str::from_utf8(bytes).map_err(|err| GitError::InvalidFormat(err.to_string()))?;
475 let mut entries = Vec::new();
476 for line in text.lines() {
477 let mut parts = line.splitn(3, ' ');
478 let old = parts
479 .next()
480 .ok_or_else(|| GitError::InvalidFormat("missing reflog old oid".into()))?;
481 let new = parts
482 .next()
483 .ok_or_else(|| GitError::InvalidFormat("missing reflog new oid".into()))?;
484 let rest = parts
485 .next()
486 .ok_or_else(|| GitError::InvalidFormat("missing reflog committer".into()))?;
487 let (committer, message) = rest.split_once('\t').unwrap_or((rest, ""));
488 entries.push(ReflogEntry {
489 old_oid: ObjectId::from_hex(format, old)?,
490 new_oid: ObjectId::from_hex(format, new)?,
491 committer: committer.as_bytes().to_vec(),
492 message: message.as_bytes().to_vec(),
493 });
494 }
495 Ok(entries)
496}
497
498pub fn expire_reflog(
516 entries: &[ReflogEntry],
517 cutoff_unix: i64,
518 expire_unreachable_cutoff: Option<i64>,
519 is_reachable: impl Fn(&ObjectId) -> bool,
520) -> Result<Vec<ReflogEntry>> {
521 let last_index = entries.len().checked_sub(1);
522 let mut retained = Vec::with_capacity(entries.len());
523 for (index, entry) in entries.iter().enumerate() {
524 if Some(index) == last_index {
527 retained.push(entry.clone());
528 continue;
529 }
530 let timestamp = entry.timestamp_seconds()?;
531 let mut expired = timestamp < cutoff_unix;
532 if let Some(unreachable_cutoff) = expire_unreachable_cutoff
533 && !is_reachable(&entry.new_oid)
534 {
535 expired = expired || timestamp < unreachable_cutoff;
536 }
537 if !expired {
538 retained.push(entry.clone());
539 }
540 }
541 Ok(retained)
542}
543
544#[derive(Debug, Default, Clone)]
545pub struct RefStore {
546 refs: HashMap<String, RefTarget>,
547 reflogs: BTreeMap<String, Vec<ReflogEntry>>,
548}
549
550impl RefStore {
551 pub fn new() -> Self {
552 Self::default()
553 }
554
555 pub fn get(&self, name: &str) -> Option<&RefTarget> {
556 self.refs.get(name)
557 }
558
559 pub fn transaction(&mut self) -> RefTransaction<'_> {
560 RefTransaction {
561 store: self,
562 updates: Vec::new(),
563 }
564 }
565
566 pub fn reflog(&self, name: &str) -> &[ReflogEntry] {
567 self.reflogs
568 .get(name)
569 .map(Vec::as_slice)
570 .unwrap_or_default()
571 }
572}
573
574#[derive(Debug)]
575pub struct RefUpdate {
576 pub name: String,
577 pub expected: Option<RefTarget>,
578 pub new: RefTarget,
579 pub reflog: Option<ReflogEntry>,
580}
581
582#[derive(Debug, Clone, PartialEq, Eq)]
590pub enum RefPrecondition {
591 Any,
593 MustExist,
595 MustNotExist,
597 MustExistAndMatch(RefTarget),
599 ExistingMustMatch(RefTarget),
602}
603
604impl RefPrecondition {
605 fn from_expected(expected: Option<RefTarget>) -> Self {
607 match expected {
608 None => Self::Any,
609 Some(target) => Self::MustExistAndMatch(target),
610 }
611 }
612
613 fn is_satisfied_by(&self, current: Option<&RefTarget>) -> bool {
616 match self {
617 Self::Any => true,
618 Self::MustExist => current.is_some(),
619 Self::MustNotExist => current.is_none(),
620 Self::MustExistAndMatch(target) => current == Some(target),
621 Self::ExistingMustMatch(target) => match current {
622 None => true,
623 Some(current) => current == target,
624 },
625 }
626 }
627
628 fn describe(&self, name: &str) -> String {
630 match self {
631 Self::Any => format!("ref {name} precondition not met"),
632 Self::MustExist => format!("expected ref {name} to exist"),
633 Self::MustNotExist => format!("expected ref {name} to not already exist"),
634 Self::MustExistAndMatch(_) => format!("expected ref {name} to match"),
635 Self::ExistingMustMatch(_) => {
636 format!("expected ref {name} to match its current value")
637 }
638 }
639 }
640}
641
642pub struct RefTransaction<'a> {
643 store: &'a mut RefStore,
644 updates: Vec<RefUpdate>,
645}
646
647impl<'a> RefTransaction<'a> {
648 pub fn update(&mut self, update: RefUpdate) {
649 self.updates.push(update);
650 }
651
652 pub fn commit(self) -> Result<()> {
653 for update in &self.updates {
654 if let Some(expected) = &update.expected
655 && self.store.refs.get(&update.name) != Some(expected)
656 {
657 return Err(GitError::Transaction(format!(
658 "expected ref {} to match",
659 update.name
660 )));
661 }
662 }
663 for update in self.updates {
664 self.store.refs.insert(update.name.clone(), update.new);
665 if let Some(entry) = update.reflog {
666 self.store
667 .reflogs
668 .entry(update.name)
669 .or_default()
670 .push(entry);
671 }
672 }
673 Ok(())
674 }
675}
676
677#[derive(Debug, Clone)]
678pub struct FileRefStore {
679 git_dir: PathBuf,
680 common_dir: PathBuf,
681 storage_dir: PathBuf,
682 format: ObjectFormat,
683 reftable_lock_timeout_millis: Option<u64>,
684 combine_reftable_logs: bool,
685}
686
687#[derive(Debug, Clone, PartialEq, Eq)]
688pub struct BranchCreate {
689 pub name: String,
690 pub oid: ObjectId,
691}
692
693#[derive(Debug, Clone, PartialEq, Eq)]
694pub struct BranchDelete {
695 pub name: String,
696 pub oid: ObjectId,
697}
698
699#[derive(Debug, Clone, PartialEq, Eq)]
700pub struct TagCreate {
701 pub name: String,
702 pub oid: ObjectId,
703}
704
705#[derive(Debug, Clone, PartialEq, Eq)]
706pub struct TagDelete {
707 pub name: String,
708 pub oid: ObjectId,
709}
710
711#[derive(Debug, Clone, PartialEq, Eq)]
712pub struct BundleRefUpdate {
713 pub name: String,
714 pub oid: ObjectId,
715}
716
717#[derive(Debug, Clone, PartialEq, Eq)]
718pub struct BundleRefUpdateReflog {
719 pub committer: Vec<u8>,
720 pub message: Vec<u8>,
721}
722
723#[derive(Debug, Clone, PartialEq, Eq)]
724pub struct AppliedBundleRefUpdate {
725 pub name: String,
726 pub old_oid: Option<ObjectId>,
727 pub new_oid: ObjectId,
728}
729
730fn configured_ref_storage_backend(common_dir: &Path) -> Option<(RefBackendKind, Option<PathBuf>)> {
731 if let Ok(value) = env::var("GIT_REFERENCE_BACKEND")
732 && let Ok((kind, path)) = parse_ref_storage_backend_value(&value)
733 {
734 return Some((
735 kind,
736 path.map(|path| ref_storage_path_from_config(common_dir, path)),
737 ));
738 }
739 let config = GitConfig::read(common_dir.join("config")).ok()?;
740 let value = config.get("extensions", None, "refStorage")?;
741 parse_ref_storage_backend_value(value)
742 .ok()
743 .map(|(kind, path)| {
744 (
745 kind,
746 path.map(|path| ref_storage_path_from_config(common_dir, path)),
747 )
748 })
749}
750
751fn ref_storage_path_from_config(common_dir: &Path, path: PathBuf) -> PathBuf {
752 if path.is_absolute() {
753 path
754 } else {
755 common_dir.join(path)
756 }
757}
758
759fn parse_ref_storage_backend_value(value: &str) -> Result<(RefBackendKind, Option<PathBuf>)> {
760 let (backend, path) = if let Some((backend, path)) = value.split_once("://") {
761 if path.is_empty() {
762 return Err(GitError::InvalidFormat(format!(
763 "invalid value for 'extensions.refstorage': '{value}'"
764 )));
765 }
766 (backend, Some(PathBuf::from(path)))
767 } else {
768 (value, None)
769 };
770 let kind = match backend {
771 "files" => RefBackendKind::Files,
772 "reftable" => RefBackendKind::Reftable,
773 _ => {
774 return Err(GitError::InvalidFormat(format!(
775 "invalid value for 'extensions.refstorage': '{value}'"
776 )));
777 }
778 };
779 Ok((kind, path))
780}
781
782#[derive(Debug, Clone, Copy, PartialEq, Eq)]
783enum RefBackendKind {
784 Files,
785 Reftable,
786}
787
788impl FileRefStore {
789 pub fn new(git_dir: impl Into<PathBuf>, format: ObjectFormat) -> Self {
790 let git_dir = git_dir.into();
791 let common_dir = repository_common_dir(&git_dir);
792 let configured = configured_ref_storage_backend(&common_dir);
793 let storage_dir = match configured {
794 Some((_, Some(path))) => path,
795 Some((RefBackendKind::Reftable, None)) if git_dir != common_dir => git_dir.clone(),
796 _ => common_dir.clone(),
797 };
798 Self {
799 git_dir,
800 common_dir,
801 storage_dir,
802 format,
803 reftable_lock_timeout_millis: None,
804 combine_reftable_logs: false,
805 }
806 }
807
808 pub fn with_reftable_lock_timeout_millis(mut self, timeout_millis: Option<u64>) -> Self {
809 self.reftable_lock_timeout_millis = timeout_millis;
810 self
811 }
812
813 pub fn with_reftable_combined_logs(mut self, combine: bool) -> Self {
814 self.combine_reftable_logs = combine;
815 self
816 }
817
818 pub fn read_ref(&self, name: &str) -> Result<Option<RefTarget>> {
819 validate_ref_name_for_read(name)?;
820 self.read_ref_unchecked(name)
821 }
822
823 fn read_ref_unchecked(&self, name: &str) -> Result<Option<RefTarget>> {
824 if self.uses_reftable()? {
825 let (store, name) = self.reftable_store_for_ref(name)?;
826 if let Some(target) = store.read_reftable_ref(&name)? {
827 return Ok(Some(target));
828 }
829 if name != "HEAD" && is_root_ref_syntax(&name) {
830 return Ok(self
831 .read_loose_ref(&name)?
832 .map(|reference| reference.target));
833 }
834 return Ok(None);
835 }
836 if let Some(reference) = self.read_loose_ref(name)? {
837 return Ok(Some(reference.target));
838 }
839 if let Some(reference) = self.read_packed_ref(name)? {
840 return Ok(Some(reference.reference.target));
841 }
842 Ok(None)
843 }
844
845 pub fn raw_ref_exists(&self, name: &str) -> Result<bool> {
855 if self.uses_reftable()? {
856 let (store, name) = self.reftable_store_for_ref(name)?;
857 if store.read_reftable_ref(&name)?.is_some() {
858 return Ok(true);
859 }
860 if name != "HEAD" && is_root_ref_syntax(&name) {
861 return Ok(self.read_loose_ref(&name)?.is_some());
862 }
863 return Ok(false);
864 }
865 let base = if is_root_ref_syntax(name) {
869 &self.git_dir
870 } else {
871 &self.common_dir
872 };
873 let path = base.join(name);
874 match fs::symlink_metadata(&path) {
875 Ok(meta) if meta.is_dir() => {
876 Ok(self.read_packed_ref(name)?.is_some())
879 }
880 Ok(_) => Ok(true),
881 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
882 Ok(self.read_packed_ref(name)?.is_some())
883 }
884 Err(err) => Err(err.into()),
885 }
886 }
887
888 pub fn read_reflog(&self, name: &str) -> Result<Vec<ReflogEntry>> {
889 validate_ref_name_for_read(name)?;
890 if self.uses_reftable()? {
891 return self.read_reftable_logs(name);
892 }
893 let path = self.reflog_path(name);
894 if !path.exists() {
895 return Ok(Vec::new());
896 }
897 parse_reflog(self.format, &fs::read(path)?)
898 }
899
900 pub fn write_reflog(&self, name: &str, entries: &[ReflogEntry]) -> Result<()> {
901 validate_ref_name_for_read(name)?;
902 if self.uses_reftable()? {
903 return self.rewrite_reftable_logs(name, entries, true);
904 }
905 let path = self.reflog_path(name);
906 let parent = path
907 .parent()
908 .ok_or_else(|| GitError::InvalidPath("reflog path has no parent".into()))?;
909 fs::create_dir_all(parent)?;
910 let mut bytes = Vec::new();
911 for entry in entries {
912 bytes.extend_from_slice(&entry.to_line());
913 }
914 write_locked(&path, &bytes)
915 }
916
917 pub fn import_snapshot(
918 &self,
919 refs: &[Ref],
920 reflogs: &[(String, Vec<ReflogEntry>)],
921 pack_files_refs: bool,
922 ) -> Result<()> {
923 if self.uses_reftable()? {
924 let ref_records = refs
925 .iter()
926 .map(|reference| ReftableRefRecord {
927 name: reference.name.clone(),
928 update_index: 0,
929 value: reftable_value_from_ref_target(&reference.target),
930 })
931 .collect::<Vec<_>>();
932 let mut log_records = Vec::new();
933 let mut update_index = 1u64;
934 for (name, entries) in reflogs {
935 for entry in entries {
936 log_records.push(ReftableLogRecord {
937 refname: name.clone(),
938 update_index,
939 value: ReftableLogValue::Update(reftable_update_from_reflog(entry)?),
940 });
941 update_index = update_index.checked_add(1).ok_or_else(|| {
942 GitError::InvalidFormat("reftable update index overflow".into())
943 })?;
944 }
945 }
946 if !ref_records.is_empty() || !log_records.is_empty() {
947 self.append_reftable_table_spanning(ref_records, log_records)?;
948 }
949 return Ok(());
950 }
951
952 let mut tx = self.transaction();
953 for reference in refs {
954 tx.update(RefUpdate {
955 name: reference.name.clone(),
956 expected: None,
957 new: reference.target.clone(),
958 reflog: None,
959 });
960 }
961 tx.commit()?;
962 for (name, entries) in reflogs {
963 self.write_reflog(name, entries)?;
964 }
965 if pack_files_refs {
966 let _ = self.pack_refs(true)?;
967 }
968 Ok(())
969 }
970
971 pub fn expire_reflog_older_than(&self, name: &str, cutoff_seconds: i64) -> Result<usize> {
972 validate_ref_name_for_read(name)?;
973 if self.uses_reftable()? {
974 let entries = self.read_reftable_logs(name)?;
975 let original_len = entries.len();
976 let mut retained = Vec::new();
977 for entry in entries {
978 if entry.timestamp_seconds()? >= cutoff_seconds {
979 retained.push(entry);
980 }
981 }
982 let removed = original_len - retained.len();
983 if removed > 0 {
984 self.rewrite_reftable_logs(name, &retained, true)?;
985 }
986 return Ok(removed);
987 }
988 let path = self.reflog_path(name);
989 if !path.exists() {
990 return Ok(0);
991 }
992 let entries = parse_reflog(self.format, &fs::read(&path)?)?;
993 let original_len = entries.len();
994 let mut retained = Vec::new();
995 for entry in entries {
996 if entry.timestamp_seconds()? >= cutoff_seconds {
997 retained.push(entry);
998 }
999 }
1000 let mut bytes = Vec::new();
1001 for entry in &retained {
1002 bytes.extend_from_slice(&entry.to_line());
1003 }
1004 write_locked(&path, &bytes)?;
1005 Ok(original_len - retained.len())
1006 }
1007
1008 pub fn expire_reflog_file(
1019 &self,
1020 name: &str,
1021 cutoff_unix: i64,
1022 expire_unreachable_cutoff: Option<i64>,
1023 write: bool,
1024 is_reachable: impl Fn(&ObjectId) -> bool,
1025 ) -> Result<usize> {
1026 validate_ref_name(name)?;
1027 if self.uses_reftable()? {
1028 let entries = self.read_reftable_logs(name)?;
1029 let original_len = entries.len();
1030 let retained = expire_reflog(
1031 &entries,
1032 cutoff_unix,
1033 expire_unreachable_cutoff,
1034 is_reachable,
1035 )?;
1036 let removed = original_len - retained.len();
1037 if write && removed > 0 {
1038 self.rewrite_reftable_logs(name, &retained, true)?;
1039 }
1040 return Ok(removed);
1041 }
1042 let path = self.reflog_path(name);
1043 if !path.exists() {
1044 return Ok(0);
1045 }
1046 let entries = parse_reflog(self.format, &fs::read(&path)?)?;
1047 let original_len = entries.len();
1048 let retained = expire_reflog(
1049 &entries,
1050 cutoff_unix,
1051 expire_unreachable_cutoff,
1052 is_reachable,
1053 )?;
1054 let removed = original_len - retained.len();
1055 if write && removed > 0 {
1056 let mut bytes = Vec::new();
1057 for entry in &retained {
1058 bytes.extend_from_slice(&entry.to_line());
1059 }
1060 write_locked(&path, &bytes)?;
1061 }
1062 Ok(removed)
1063 }
1064
1065 pub fn list_refs(&self) -> Result<Vec<Ref>> {
1066 self.list_refs_with_prefix("refs/")
1067 }
1068
1069 pub fn list_refs_with_prefix(&self, prefix: &str) -> Result<Vec<Ref>> {
1070 if self.uses_reftable()? {
1071 let mut refs = BTreeMap::<String, Ref>::new();
1072 for reference in self
1073 .reftable_store_with_storage(self.shared_reftable_storage_dir())
1074 .list_reftable_refs_with_prefix(prefix)?
1075 {
1076 refs.insert(reference.name.clone(), reference);
1077 }
1078 if self.git_dir != self.common_dir {
1079 for reference in self.list_reftable_refs_with_prefix(prefix)? {
1080 if reftable_current_worktree_ref(&reference.name) {
1081 refs.insert(reference.name.clone(), reference);
1082 }
1083 }
1084 }
1085 return Ok(refs.into_values().collect());
1086 }
1087 let mut refs = Vec::new();
1088 let packed_path = self.storage_dir.join("packed-refs");
1089 if packed_path.exists() {
1090 for packed in
1091 parse_packed_refs_with_prefix(self.format, &fs::read(packed_path)?, prefix)?
1092 {
1093 refs.push(packed.reference);
1094 }
1095 }
1096 let mut loose_refs = BTreeMap::new();
1097 self.collect_loose_refs_with_prefix(prefix, &mut loose_refs)?;
1098 if !loose_refs.is_empty() {
1099 refs.retain(|reference| !loose_refs.contains_key(&reference.name));
1100 refs.extend(loose_refs.into_values());
1101 }
1102 refs.retain(|reference| reference.name.starts_with(prefix));
1103 refs.retain(|reference| {
1104 if validate_ref_name(&reference.name).is_ok() {
1105 true
1106 } else {
1107 warn_broken_ref_name(&reference.name);
1108 false
1109 }
1110 });
1111 refs.sort_by(|left, right| left.name.cmp(&right.name));
1112 Ok(refs)
1113 }
1114
1115 pub fn list_ref_names_with_prefix(&self, prefix: &str) -> Result<Vec<String>> {
1116 if self.uses_reftable()? {
1117 return Ok(self
1118 .list_refs_with_prefix(prefix)?
1119 .into_iter()
1120 .map(|reference| reference.name)
1121 .collect());
1122 }
1123 let mut names = Vec::new();
1124 let packed_path = self.storage_dir.join("packed-refs");
1125 if packed_path.exists() {
1126 packed_ref_names_with_prefix(
1127 self.format,
1128 &fs::read(packed_path)?,
1129 prefix,
1130 false,
1131 &mut names,
1132 )?;
1133 }
1134 let mut loose_names = BTreeSet::new();
1135 self.collect_loose_ref_names_with_prefix(prefix, &mut loose_names)?;
1136 if !loose_names.is_empty() {
1137 names.retain(|name| !loose_names.contains(name));
1138 names.extend(loose_names);
1139 }
1140 names.retain(|name| name.starts_with(prefix));
1141 names.retain(|name| {
1142 if validate_ref_name(name).is_ok() {
1143 true
1144 } else {
1145 warn_broken_ref_name(name);
1146 false
1147 }
1148 });
1149 names.sort();
1150 names.dedup();
1151 Ok(names)
1152 }
1153
1154 pub fn list_short_ref_names_with_prefix(&self, prefix: &str) -> Result<Vec<String>> {
1155 if self.uses_reftable()? {
1156 let mut names = self
1157 .list_reftable_refs_with_prefix(prefix)?
1158 .into_iter()
1159 .filter_map(|reference| reference.name.strip_prefix(prefix).map(str::to_owned))
1160 .collect::<Vec<_>>();
1161 names.sort();
1162 return Ok(names);
1163 }
1164 let mut names = Vec::new();
1165 let packed_path = self.storage_dir.join("packed-refs");
1166 if packed_path.exists() {
1167 packed_ref_names_with_prefix(
1168 self.format,
1169 &fs::read(packed_path)?,
1170 prefix,
1171 true,
1172 &mut names,
1173 )?;
1174 }
1175 let mut loose_full_names = BTreeSet::new();
1176 self.collect_loose_ref_names_with_prefix(prefix, &mut loose_full_names)?;
1177 let loose_names = loose_full_names
1178 .into_iter()
1179 .filter_map(|name| {
1180 if validate_ref_name(&name).is_ok() {
1181 name.strip_prefix(prefix).map(str::to_owned)
1182 } else {
1183 warn_broken_ref_name(&name);
1184 None
1185 }
1186 })
1187 .collect::<BTreeSet<_>>();
1188 if !loose_names.is_empty() {
1189 names.retain(|name| !loose_names.contains(name));
1190 names.extend(loose_names);
1191 }
1192 names.sort();
1193 names.dedup();
1194 Ok(names)
1195 }
1196
1197 pub fn list_all_refs(&self) -> Result<Vec<Ref>> {
1198 let mut refs = self.list_refs()?;
1199 let mut seen = refs
1200 .iter()
1201 .map(|reference| reference.name.clone())
1202 .collect::<BTreeSet<_>>();
1203 for reference in self.list_root_refs()? {
1204 if seen.insert(reference.name.clone()) {
1205 refs.push(reference);
1206 }
1207 }
1208 refs.sort_by(|left, right| left.name.cmp(&right.name));
1209 Ok(refs)
1210 }
1211
1212 pub fn list_reflog_names(&self) -> Result<Vec<String>> {
1213 let mut names = BTreeSet::new();
1214 if self.uses_reftable()? {
1215 for table in self.reftables()? {
1216 for record in table.logs {
1217 names.insert(record.refname);
1218 }
1219 }
1220 let mut live = Vec::new();
1221 for name in names {
1222 if !self.read_reftable_logs(&name)?.is_empty() {
1223 live.push(name);
1224 }
1225 }
1226 return Ok(live);
1227 }
1228 self.collect_reflog_names(&self.storage_dir.join("logs"), "logs", &mut names)?;
1229 let worktree_logs = self.git_dir.join("logs");
1230 if worktree_logs != self.storage_dir.join("logs") {
1231 self.collect_reflog_names(&worktree_logs, "logs", &mut names)?;
1232 }
1233 Ok(names.into_iter().collect())
1234 }
1235
1236 pub fn has_refs_with_prefix(&self, prefix: &str) -> Result<bool> {
1237 if self.uses_reftable()? {
1238 return Ok(self
1239 .list_reftable_refs()?
1240 .iter()
1241 .any(|reference| reference.name.starts_with(prefix)));
1242 }
1243 let packed_path = self.storage_dir.join("packed-refs");
1244 if packed_path.exists()
1245 && packed_refs_have_prefix(self.format, &fs::read(&packed_path)?, prefix)?
1246 {
1247 return Ok(true);
1248 }
1249 self.loose_refs_have_prefix(prefix)
1250 }
1251
1252 pub fn write_packed_refs(&self, refs: &[PackedRef]) -> Result<()> {
1253 self.write_packed_refs_with_timeout(refs, 0)
1254 }
1255
1256 pub fn write_packed_refs_with_timeout(
1257 &self,
1258 refs: &[PackedRef],
1259 timeout_millis: u64,
1260 ) -> Result<()> {
1261 let path = self.packed_refs_write_path()?;
1262 write_locked_with_timeout(&path, &write_packed_refs(refs)?, timeout_millis)
1263 }
1264
1265 pub fn pack_refs(&self, prune_loose: bool) -> Result<Vec<PackedRef>> {
1266 self.pack_refs_with_peeler(prune_loose, |_, _| Ok(None))
1267 }
1268
1269 pub fn pack_refs_with_peeler<F>(&self, prune_loose: bool, mut peel: F) -> Result<Vec<PackedRef>>
1270 where
1271 F: FnMut(&str, &ObjectId) -> Result<Option<ObjectId>>,
1272 {
1273 self.pack_refs_selected_with_timeout(
1274 prune_loose,
1275 false,
1276 0,
1277 |_| true,
1278 |name, oid| peel(name, oid).map(|peeled| PackRefDecision::Pack { peeled }),
1279 )
1280 }
1281
1282 pub fn pack_refs_selected_with_timeout<F, S>(
1283 &self,
1284 prune_loose: bool,
1285 auto: bool,
1286 timeout_millis: u64,
1287 mut should_pack: S,
1288 mut decide: F,
1289 ) -> Result<Vec<PackedRef>>
1290 where
1291 F: FnMut(&str, &ObjectId) -> Result<PackRefDecision>,
1292 S: FnMut(&str) -> bool,
1293 {
1294 if self.uses_reftable()? {
1295 self.compact_reftable_stack()?;
1296 return Ok(Vec::new());
1297 }
1298 let mut packed_refs = BTreeMap::new();
1299 let packed_path = self.storage_dir.join("packed-refs");
1300 if packed_path.exists() {
1301 for packed in parse_packed_refs(self.format, &fs::read(&packed_path)?)? {
1302 packed_refs.insert(packed.reference.name.clone(), packed);
1303 }
1304 }
1305
1306 let mut loose_refs = BTreeMap::new();
1307 let refs_dir = self.storage_dir.join("refs");
1308 if refs_dir.exists() {
1309 self.collect_loose_refs(&refs_dir, "refs", &mut loose_refs)?;
1310 }
1311 let loose_refs = loose_refs
1312 .into_values()
1313 .filter(|reference| packable_loose_ref_name(&reference.name))
1314 .filter(|reference| should_pack(&reference.name))
1315 .collect::<Vec<_>>();
1316 if auto && !pack_refs_auto_required_for(&packed_path, loose_refs.len())? {
1317 return Ok(packed_refs.into_values().collect());
1318 }
1319 let mut packed_loose_names = Vec::new();
1320 for reference in loose_refs {
1321 let RefTarget::Direct(oid) = reference.target else {
1322 continue;
1323 };
1324 let peeled = match decide(&reference.name, &oid)? {
1325 PackRefDecision::Pack { peeled } => peeled,
1326 PackRefDecision::Skip => continue,
1327 };
1328 packed_loose_names.push(reference.name.clone());
1329 packed_refs.insert(
1330 reference.name.clone(),
1331 PackedRef {
1332 reference: Ref {
1333 name: reference.name,
1334 target: RefTarget::Direct(oid),
1335 },
1336 peeled,
1337 },
1338 );
1339 }
1340
1341 let refs = packed_refs.into_values().collect::<Vec<_>>();
1342 self.write_packed_refs_with_timeout(&refs, timeout_millis)?;
1343 if prune_loose {
1344 for name in packed_loose_names {
1345 self.delete_loose_ref(&name)?;
1346 }
1347 }
1348 Ok(refs)
1349 }
1350
1351 pub fn pack_refs_auto_required<S>(&self, mut should_pack: S) -> Result<bool>
1352 where
1353 S: FnMut(&str) -> bool,
1354 {
1355 if self.uses_reftable()? {
1356 return Ok(true);
1357 }
1358 let mut loose_refs = BTreeMap::new();
1359 let refs_dir = self.storage_dir.join("refs");
1360 if refs_dir.exists() {
1361 self.collect_loose_refs(&refs_dir, "refs", &mut loose_refs)?;
1362 }
1363 let count = loose_refs
1364 .values()
1365 .filter(|reference| packable_loose_ref_name(&reference.name))
1366 .filter(|reference| matches!(reference.target, RefTarget::Direct(_)))
1367 .filter(|reference| should_pack(&reference.name))
1368 .count();
1369 pack_refs_auto_required_for(&self.storage_dir.join("packed-refs"), count)
1370 }
1371
1372 fn packed_refs_write_path(&self) -> Result<PathBuf> {
1373 let path = self.storage_dir.join("packed-refs");
1374 match fs::symlink_metadata(&path) {
1375 Ok(meta) if meta.file_type().is_symlink() => {
1376 let target = fs::read_link(&path)?;
1377 if target.is_absolute() {
1378 Ok(target)
1379 } else {
1380 let parent = path.parent().ok_or_else(|| {
1381 GitError::InvalidPath("packed-refs path has no parent".into())
1382 })?;
1383 Ok(parent.join(target))
1384 }
1385 }
1386 Ok(_) => Ok(path),
1387 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(path),
1388 Err(err) => Err(err.into()),
1389 }
1390 }
1391
1392 pub fn current_branch_ref(&self) -> Result<Option<String>> {
1393 match self.read_ref("HEAD")? {
1394 Some(RefTarget::Symbolic(name)) if name.starts_with("refs/heads/") => Ok(Some(name)),
1395 _ => Ok(None),
1396 }
1397 }
1398
1399 pub fn current_branch(&self) -> Result<Option<String>> {
1400 Ok(self
1401 .current_branch_ref()?
1402 .and_then(|name| name.strip_prefix("refs/heads/").map(str::to_string)))
1403 }
1404
1405 pub fn transaction(&self) -> FileRefTransaction<'_> {
1406 FileRefTransaction {
1407 store: self,
1408 changes: Vec::new(),
1409 hook: None,
1410 }
1411 }
1412
1413 pub fn create_branch(
1414 &self,
1415 branch: &str,
1416 start: ObjectId,
1417 committer: Vec<u8>,
1418 message: Vec<u8>,
1419 ) -> Result<BranchCreate> {
1420 let name = branch_ref_name(branch)?;
1421 if self.read_ref(&name)?.is_some() {
1422 return Err(GitError::Transaction(format!(
1423 "branch {branch} already exists"
1424 )));
1425 }
1426 let zero = ObjectId::null(self.format);
1427 let mut tx = self.transaction();
1428 tx.update(RefUpdate {
1429 name: name.clone(),
1430 expected: None,
1431 new: RefTarget::Direct(start),
1432 reflog: Some(ReflogEntry {
1433 old_oid: zero,
1434 new_oid: start,
1435 committer,
1436 message,
1437 }),
1438 });
1439 tx.commit()?;
1440 Ok(BranchCreate { name, oid: start })
1441 }
1442
1443 pub fn delete_branch(&self, branch: &str) -> Result<BranchDelete> {
1444 let name = branch_ref_name_for_read(branch)?;
1445 if matches!(self.read_ref("HEAD")?, Some(RefTarget::Symbolic(head)) if head == name) {
1446 return Err(GitError::Transaction(format!(
1447 "cannot delete branch {branch} checked out at HEAD"
1448 )));
1449 }
1450 let oid = self.delete_direct_ref(&name, "branch", branch)?;
1451 self.remove_reflog_file(&name);
1452 Ok(BranchDelete { name, oid })
1453 }
1454
1455 pub fn move_branch(
1456 &self,
1457 old_branch: &str,
1458 new_branch: &str,
1459 force: bool,
1460 committer: Vec<u8>,
1461 ) -> Result<()> {
1462 self.copy_or_move_branch(old_branch, new_branch, force, false, committer)
1463 }
1464
1465 pub fn copy_branch(
1466 &self,
1467 old_branch: &str,
1468 new_branch: &str,
1469 force: bool,
1470 committer: Vec<u8>,
1471 ) -> Result<()> {
1472 self.copy_or_move_branch(old_branch, new_branch, force, true, committer)
1473 }
1474
1475 fn conflicting_ref_for_path(&self, new_name: &str, exclude: &str) -> Result<Option<String>> {
1482 for reference in self.list_refs()? {
1483 let name = &reference.name;
1484 if name == new_name || name == exclude {
1485 continue;
1486 }
1487 if new_name.starts_with(&format!("{name}/")) {
1489 return Ok(Some(name.clone()));
1490 }
1491 if name.starts_with(&format!("{new_name}/")) {
1493 return Ok(Some(name.clone()));
1494 }
1495 }
1496 Ok(None)
1497 }
1498
1499 fn copy_or_move_branch(
1500 &self,
1501 old_branch: &str,
1502 new_branch: &str,
1503 force: bool,
1504 copy: bool,
1505 committer: Vec<u8>,
1506 ) -> Result<()> {
1507 let old_name = branch_ref_name_for_source(old_branch)?;
1508 let new_name = branch_ref_name(new_branch)?;
1509 if old_name == new_name {
1510 return Ok(());
1511 }
1512 let Some(target) = self.read_ref(&old_name)? else {
1513 return Err(GitError::reference_not_found(format!(
1514 "branch {old_branch}"
1515 )));
1516 };
1517 let RefTarget::Direct(oid) = target else {
1518 return Err(GitError::InvalidFormat(format!(
1519 "branch {old_branch} is symbolic"
1520 )));
1521 };
1522 let conflict_exclude = if copy { "" } else { &old_name };
1530 if let Some(conflict) = self.conflicting_ref_for_path(&new_name, conflict_exclude)? {
1531 return Err(GitError::Transaction(format!(
1532 "'{conflict}' exists; cannot create '{new_name}'"
1533 )));
1534 }
1535 let dest_entry = self.read_ref(&new_name)?;
1539 let dest_resolves = resolve_ref_peeled(self, &new_name)?.is_some();
1540 if dest_resolves && !force {
1541 return Err(GitError::Transaction(format!(
1542 "branch {new_branch} already exists"
1543 )));
1544 }
1545 match dest_entry {
1549 Some(RefTarget::Symbolic(_)) => {
1550 self.delete_symbolic_ref(&new_name)?;
1551 self.remove_reflog_file(&new_name);
1552 }
1553 Some(RefTarget::Direct(_)) => {
1554 let _ = self.delete_direct_ref(&new_name, "branch", new_branch)?;
1555 self.remove_reflog_file(&new_name);
1556 }
1557 None => {}
1558 }
1559
1560 let mut reflog = self.read_reflog(&old_name)?;
1563 reflog.push(ReflogEntry {
1564 old_oid: oid,
1565 new_oid: oid,
1566 committer,
1567 message: if copy {
1568 format!("Branch: copied {old_name} to {new_name}").into_bytes()
1569 } else {
1570 format!("Branch: renamed {old_name} to {new_name}").into_bytes()
1571 },
1572 });
1573
1574 if !copy {
1579 let _ = self.delete_direct_ref(&old_name, "branch", old_branch)?;
1580 self.remove_reflog_file(&old_name);
1581 }
1582
1583 self.write_loose_ref(&Ref {
1584 name: new_name.clone(),
1585 target: RefTarget::Direct(oid),
1586 })?;
1587 self.write_reflog(&new_name, &reflog)?;
1588
1589 if !copy
1590 && matches!(self.read_ref("HEAD")?, Some(RefTarget::Symbolic(head)) if head == old_name)
1591 {
1592 self.write_loose_ref(&Ref {
1593 name: "HEAD".into(),
1594 target: RefTarget::Symbolic(new_name),
1595 })?;
1596 }
1597 Ok(())
1598 }
1599
1600 pub fn create_tag(&self, tag: &str, target: ObjectId) -> Result<TagCreate> {
1601 let name = tag_ref_name(tag)?;
1602 if self.read_ref(&name)?.is_some() {
1603 return Err(GitError::Transaction(format!("tag {tag} already exists")));
1604 }
1605 let mut tx = self.transaction();
1606 tx.update(RefUpdate {
1607 name: name.clone(),
1608 expected: None,
1609 new: RefTarget::Direct(target),
1610 reflog: None,
1611 });
1612 tx.commit()?;
1613 Ok(TagCreate { name, oid: target })
1614 }
1615
1616 pub fn apply_bundle_ref_updates(
1617 &self,
1618 refs: &[BundleRefUpdate],
1619 reflog: Option<BundleRefUpdateReflog>,
1620 ) -> Result<Vec<AppliedBundleRefUpdate>> {
1621 let (updates, applied) = prepare_bundle_ref_updates(refs, reflog.as_ref(), |name, oid| {
1622 if oid.format() != self.format {
1623 return Err(GitError::InvalidObjectId(format!(
1624 "bundle ref {name} has {} object id for {} repository",
1625 oid.format().name(),
1626 self.format.name()
1627 )));
1628 }
1629 self.read_ref(name)
1630 })?;
1631 let mut tx = self.transaction();
1632 for update in updates {
1633 tx.update(update);
1634 }
1635 tx.commit()?;
1636 Ok(applied)
1637 }
1638
1639 pub fn delete_tag(&self, tag: &str) -> Result<TagDelete> {
1640 let name = TagRefNameBuf::from_tag_name_unrestricted(tag)?.into_string();
1641 let oid = self.delete_direct_ref(&name, "tag", tag)?;
1642 self.remove_reflog_file(&name);
1643 Ok(TagDelete { name, oid })
1644 }
1645
1646 pub fn delete_ref(&self, name: &str) -> Result<RefDelete> {
1647 validate_ref_name_for_read(name)?;
1648 let oid = self.delete_direct_ref(name, "ref", name)?;
1649 self.remove_reflog_file(name);
1650 Ok(RefDelete {
1651 name: name.into(),
1652 oid,
1653 })
1654 }
1655
1656 pub fn delete_ref_checked(
1657 &self,
1658 delete: DeleteRef,
1659 ) -> std::result::Result<RefDelete, RefDeleteError> {
1660 validate_ref_name_for_read(&delete.name).map_err(|_| RefDeleteError::InvalidName)?;
1661 if self.uses_reftable().map_err(ref_delete_error_from_git)? {
1662 return self.delete_reftable_ref_checked(delete);
1663 }
1664 self.delete_files_ref_checked(delete)
1665 }
1666
1667 pub fn delete_symbolic_ref(&self, name: &str) -> Result<bool> {
1668 validate_ref_name_for_read(name)?;
1669 if self.uses_reftable()? {
1670 let Some(target) = self.read_ref(name)? else {
1671 return Ok(false);
1672 };
1673 if !matches!(target, RefTarget::Symbolic(_)) {
1674 return Ok(false);
1675 }
1676 self.append_reftable_records(vec![ReftableRefRecord {
1677 name: name.to_string(),
1678 update_index: 0,
1679 value: ReftableRefValue::Deletion,
1680 }])?;
1681 self.remove_reflog_file(name);
1682 return Ok(true);
1683 }
1684 let Some(reference) = self.read_loose_ref(name)? else {
1685 return Ok(false);
1686 };
1687 if !matches!(reference.target, RefTarget::Symbolic(_)) {
1688 return Ok(false);
1689 }
1690 self.delete_loose_ref(name)?;
1691 self.remove_reflog_file(name);
1692 Ok(true)
1693 }
1694
1695 fn delete_direct_ref(&self, name: &str, kind: &str, short_name: &str) -> Result<ObjectId> {
1696 if self.uses_reftable()? {
1697 let Some(target) = self.read_ref(name)? else {
1698 return Err(GitError::reference_not_found(format!(
1699 "{kind} {short_name}"
1700 )));
1701 };
1702 let RefTarget::Direct(oid) = target else {
1703 return Err(GitError::InvalidFormat(format!(
1704 "{kind} {short_name} is symbolic"
1705 )));
1706 };
1707 self.append_reftable_records(vec![ReftableRefRecord {
1708 name: name.to_string(),
1709 update_index: 0,
1710 value: ReftableRefValue::Deletion,
1711 }])?;
1712 self.remove_reflog_file(name);
1715 return Ok(oid);
1716 }
1717 let Some(reference) = self.read_loose_ref(name)? else {
1718 return self.delete_packed_ref(name, kind, short_name);
1719 };
1720 let oid = match reference.target {
1721 RefTarget::Direct(oid) => oid,
1722 RefTarget::Symbolic(target) => {
1723 return Err(GitError::InvalidFormat(format!(
1724 "{kind} {short_name} is symbolic to {target}"
1725 )));
1726 }
1727 };
1728 self.delete_loose_ref(name)?;
1729 match self.delete_packed_ref(name, kind, short_name) {
1735 Ok(_) | Err(GitError::NotFound(_)) => {}
1736 Err(err) => return Err(err),
1737 }
1738 Ok(oid)
1739 }
1740
1741 fn delete_packed_ref(&self, name: &str, kind: &str, short_name: &str) -> Result<ObjectId> {
1742 let path = self.storage_dir.join("packed-refs");
1743 if !path.exists() {
1744 return Err(GitError::reference_not_found(format!(
1745 "{kind} {short_name}"
1746 )));
1747 }
1748 let mut refs = parse_packed_refs(self.format, &fs::read(&path)?)?;
1749 let Some(index) = refs
1750 .iter()
1751 .position(|reference| reference.reference.name == name)
1752 else {
1753 return Err(GitError::reference_not_found(format!(
1754 "{kind} {short_name}"
1755 )));
1756 };
1757 let removed = refs.remove(index);
1758 let RefTarget::Direct(oid) = removed.reference.target else {
1759 return Err(GitError::InvalidFormat(format!(
1760 "{kind} {short_name} is symbolic"
1761 )));
1762 };
1763 self.write_packed_refs(&refs)?;
1764 Ok(oid)
1765 }
1766
1767 fn delete_reftable_ref_checked(
1768 &self,
1769 delete: DeleteRef,
1770 ) -> std::result::Result<RefDelete, RefDeleteError> {
1771 let target = self
1772 .read_ref(&delete.name)
1773 .map_err(ref_delete_error_from_git)?;
1774 let oid = checked_delete_oid(delete.expected_old, target)?;
1775 self.append_reftable_records(vec![ReftableRefRecord {
1776 name: delete.name.clone(),
1777 update_index: 0,
1778 value: ReftableRefValue::Deletion,
1779 }])
1780 .map_err(ref_delete_error_from_git)?;
1781 self.remove_reflog_file(&delete.name);
1784 Ok(RefDelete {
1785 name: delete.name,
1786 oid,
1787 })
1788 }
1789
1790 fn delete_files_ref_checked(
1791 &self,
1792 delete: DeleteRef,
1793 ) -> std::result::Result<RefDelete, RefDeleteError> {
1794 let name = delete.name;
1795 let path = self.ref_path(&name);
1796 let parent = path.parent().ok_or(RefDeleteError::InvalidName)?;
1797 fs::create_dir_all(parent).map_err(RefDeleteError::from)?;
1798
1799 let loose_lock_path = lock_path_for(&path).map_err(|_| RefDeleteError::InvalidName)?;
1800 let _prune_guard = RefDirPruneGuard {
1801 store: self,
1802 name: name.clone(),
1803 };
1804 let loose_lock = DeleteLock::acquire(loose_lock_path)?;
1805
1806 let packed_path = self.storage_dir.join("packed-refs");
1807 let packed_lock_path =
1808 lock_path_for(&packed_path).map_err(|_| RefDeleteError::InvalidName)?;
1809 let mut packed_lock = DeleteLock::acquire(packed_lock_path)?;
1810
1811 let loose_ref = self
1812 .read_loose_ref(&name)
1813 .map_err(ref_delete_error_from_git)?;
1814 let packed_original = match fs::read(&packed_path) {
1815 Ok(bytes) => Some(bytes),
1816 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
1817 Err(err) => return Err(RefDeleteError::Io(err)),
1818 };
1819 let mut packed_refs = match &packed_original {
1820 Some(bytes) => {
1821 parse_packed_refs(self.format, bytes).map_err(ref_delete_error_from_git)?
1822 }
1823 None => Vec::new(),
1824 };
1825 let packed_index = packed_refs
1826 .iter()
1827 .position(|reference| reference.reference.name == name);
1828
1829 let current = if let Some(reference) = loose_ref.as_ref() {
1830 Some(reference.target.clone())
1831 } else {
1832 packed_index.map(|index| packed_refs[index].reference.target.clone())
1833 };
1834 let oid = checked_delete_oid(delete.expected_old, current)?;
1835
1836 let packed_changed = if let Some(index) = packed_index {
1837 packed_refs.remove(index);
1838 true
1839 } else {
1840 false
1841 };
1842
1843 if packed_changed {
1844 let packed_bytes =
1845 write_packed_refs(&packed_refs).map_err(ref_delete_error_from_git)?;
1846 packed_lock.write_all(&packed_bytes)?;
1847 let lock_path = packed_lock.close();
1848 if let Err(err) = fs::rename(&lock_path, &packed_path) {
1849 let _ = fs::remove_file(&lock_path);
1850 return Err(RefDeleteError::Io(err));
1851 }
1852 } else {
1853 packed_lock.remove();
1854 }
1855
1856 if loose_ref.is_some()
1857 && let Err(err) = fs::remove_file(&path)
1858 {
1859 if packed_changed && let Some(bytes) = packed_original.as_ref() {
1860 let _ = restore_file_atomically(&packed_path, bytes);
1861 }
1862 return Err(RefDeleteError::Io(err));
1863 }
1864 loose_lock.remove();
1865
1866 self.remove_reflog_file(&name);
1869 Ok(RefDelete { name, oid })
1870 }
1871
1872 fn read_loose_ref(&self, name: &str) -> Result<Option<Ref>> {
1873 let path = self.ref_path(name);
1874 if !path.exists() {
1875 return Ok(None);
1876 }
1877 if path.is_dir() {
1878 return Ok(None);
1879 }
1880 Ok(Some(parse_loose_ref(self.format, name, &fs::read(path)?)?))
1881 }
1882
1883 fn read_packed_ref(&self, name: &str) -> Result<Option<PackedRef>> {
1884 let path = self.storage_dir.join("packed-refs");
1885 if !path.exists() {
1886 return Ok(None);
1887 }
1888 Ok(parse_packed_refs(self.format, &fs::read(path)?)?
1889 .into_iter()
1890 .find(|reference| reference.reference.name == name))
1891 }
1892
1893 fn read_reftable_ref(&self, name: &str) -> Result<Option<RefTarget>> {
1894 for table in self.reftables()?.into_iter().rev() {
1895 if let Some(record) = table.refs.into_iter().find(|record| record.name == name) {
1896 return reftable_ref_target(record.value);
1897 }
1898 }
1899 Ok(None)
1900 }
1901
1902 fn list_reftable_refs(&self) -> Result<Vec<Ref>> {
1903 self.list_reftable_refs_with_prefix("refs/")
1904 }
1905
1906 fn list_reftable_refs_with_prefix(&self, prefix: &str) -> Result<Vec<Ref>> {
1907 let mut refs = BTreeMap::<String, Ref>::new();
1908 for table in self.reftables()? {
1909 for record in table.refs {
1910 if !record.name.starts_with("refs/") || !record.name.starts_with(prefix) {
1911 continue;
1912 }
1913 match reftable_ref_target(record.value)? {
1914 Some(target) => {
1915 refs.insert(
1916 record.name.clone(),
1917 Ref {
1918 name: record.name,
1919 target,
1920 },
1921 );
1922 }
1923 None => {
1924 refs.remove(&record.name);
1925 }
1926 }
1927 }
1928 }
1929 Ok(refs.into_values().collect())
1930 }
1931
1932 fn list_root_refs(&self) -> Result<Vec<Ref>> {
1933 if self.uses_reftable()? {
1934 let mut refs = BTreeMap::<String, Ref>::new();
1935 for table in self.reftables()? {
1936 for record in table.refs {
1937 if record.name.starts_with("refs/") || !is_root_ref_syntax(&record.name) {
1938 continue;
1939 }
1940 match reftable_ref_target(record.value)? {
1941 Some(target) => {
1942 refs.insert(
1943 record.name.clone(),
1944 Ref {
1945 name: record.name,
1946 target,
1947 },
1948 );
1949 }
1950 None => {
1951 refs.remove(&record.name);
1952 }
1953 }
1954 }
1955 }
1956 return Ok(refs.into_values().collect());
1957 }
1958
1959 let mut refs = Vec::new();
1960 for entry in fs::read_dir(&self.git_dir)? {
1961 let entry = entry?;
1962 if !entry.file_type()?.is_file() {
1963 continue;
1964 }
1965 let name = entry.file_name().to_string_lossy().into_owned();
1966 if !is_root_ref_syntax(&name)
1967 || (name != "HEAD" && !name.ends_with("_HEAD"))
1968 || name == "FETCH_HEAD"
1969 || name == "MERGE_HEAD"
1970 {
1971 continue;
1972 }
1973 if let Ok(reference) = parse_loose_ref(self.format, name, &fs::read(entry.path())?) {
1974 refs.push(reference);
1975 }
1976 }
1977 refs.sort_by(|left, right| left.name.cmp(&right.name));
1978 Ok(refs)
1979 }
1980
1981 fn reftables(&self) -> Result<Vec<Reftable>> {
1982 let reftable_dir = self.storage_dir.join("reftable");
1983 let tables_list = reftable_dir.join("tables.list");
1984 for _ in 0..10 {
1985 let text = match fs::read_to_string(&tables_list) {
1986 Ok(text) => text,
1987 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
1988 if !tables_list.exists() {
1989 return Ok(Vec::new());
1990 }
1991 thread::sleep(Duration::from_millis(10));
1992 continue;
1993 }
1994 Err(err) => return Err(err.into()),
1995 };
1996 let mut tables = Vec::new();
1997 let mut reload = false;
1998 for raw_line in text.lines() {
1999 let line = raw_line.trim();
2000 if line.is_empty() {
2001 continue;
2002 }
2003 if line.contains('/')
2004 || line.contains('\\')
2005 || Path::new(line).components().count() != 1
2006 {
2007 return Err(GitError::InvalidPath(format!(
2008 "invalid reftable table name {line}"
2009 )));
2010 }
2011 let bytes = match fs::read(reftable_dir.join(line)) {
2012 Ok(bytes) => bytes,
2013 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
2014 reload = true;
2015 break;
2016 }
2017 Err(err) => return Err(err.into()),
2018 };
2019 let table = Reftable::parse(&bytes)?;
2020 if table.header.object_format != self.format {
2021 return Err(GitError::InvalidFormat(format!(
2022 "reftable {line} has {} object ids in {} repository",
2023 table.header.object_format.name(),
2024 self.format.name()
2025 )));
2026 }
2027 tables.push(table);
2028 }
2029 if reload {
2030 thread::sleep(Duration::from_millis(10));
2031 continue;
2032 }
2033 return Ok(tables);
2034 }
2035 Err(GitError::Io(format!(
2036 "cannot read stable reftable stack {}",
2037 tables_list.display()
2038 )))
2039 }
2040
2041 fn reftable_store_with_storage(&self, storage_dir: PathBuf) -> FileRefStore {
2042 FileRefStore {
2043 git_dir: self.git_dir.clone(),
2044 common_dir: self.common_dir.clone(),
2045 storage_dir,
2046 format: self.format,
2047 reftable_lock_timeout_millis: self.reftable_lock_timeout_millis,
2048 combine_reftable_logs: self.combine_reftable_logs,
2049 }
2050 }
2051
2052 fn shared_reftable_storage_dir(&self) -> PathBuf {
2063 match configured_ref_storage_backend(&self.common_dir) {
2064 Some((_, Some(path))) => path,
2065 _ => self.common_dir.clone(),
2066 }
2067 }
2068
2069 fn reftable_store_for_ref(&self, name: &str) -> Result<(FileRefStore, String)> {
2070 if let Some((worktree, rewritten)) = reftable_other_worktree_ref(name) {
2071 let storage_dir = self.common_dir.join("worktrees").join(worktree);
2072 return Ok((
2073 self.reftable_store_with_storage(storage_dir),
2074 rewritten.to_string(),
2075 ));
2076 }
2077 if reftable_current_worktree_ref(name) {
2078 return Ok((
2079 self.reftable_store_with_storage(self.git_dir.clone()),
2080 name.to_string(),
2081 ));
2082 }
2083 Ok((
2084 self.reftable_store_with_storage(self.shared_reftable_storage_dir()),
2085 name.to_string(),
2086 ))
2087 }
2088
2089 pub fn uses_reftable(&self) -> Result<bool> {
2090 if let Ok(value) = env::var("GIT_REFERENCE_BACKEND") {
2091 return Ok(parse_ref_storage_backend_value(&value)?.0 == RefBackendKind::Reftable);
2092 }
2093 let config_path = self.common_dir.join("config");
2094 if !config_path.exists() {
2095 return Ok(false);
2096 }
2097 let config = GitConfig::parse(&fs::read(config_path)?)?;
2098 let Some(value) = config.get("extensions", None, "refStorage") else {
2099 return Ok(false);
2100 };
2101 Ok(parse_ref_storage_backend_value(value)?.0 == RefBackendKind::Reftable)
2102 }
2103
2104 pub fn reftable_table_count(&self) -> Result<usize> {
2105 Ok(self.reftable_table_names()?.len())
2106 }
2107
2108 fn append_reftable_records(&self, records: Vec<ReftableRefRecord>) -> Result<()> {
2109 if records.is_empty() {
2110 return Ok(());
2111 }
2112 self.append_reftable_table(records, Vec::new())?;
2113 Ok(())
2114 }
2115
2116 fn next_reftable_update_index(&self, table_names: &[String]) -> Result<u64> {
2117 let reftable_dir = self.storage_dir.join("reftable");
2118 let mut max_update_index = 0;
2119 for name in table_names {
2120 let table = Reftable::parse(&fs::read(reftable_dir.join(name))?)?;
2121 max_update_index = max_update_index.max(table.header.max_update_index);
2122 }
2123 max_update_index
2124 .checked_add(1)
2125 .ok_or_else(|| GitError::InvalidFormat("reftable update index overflow".into()))
2126 }
2127
2128 fn reftable_table_names(&self) -> Result<Vec<String>> {
2130 self.reftable_table_names_from(&self.storage_dir.join("reftable").join("tables.list"))
2131 }
2132
2133 fn reftable_table_names_from(&self, tables_list: &Path) -> Result<Vec<String>> {
2134 if !tables_list.exists() {
2135 return Ok(Vec::new());
2136 }
2137 Ok(fs::read_to_string(tables_list)?
2138 .lines()
2139 .map(str::trim)
2140 .filter(|line| !line.is_empty())
2141 .map(str::to_string)
2142 .collect())
2143 }
2144
2145 fn reftable_lock_timeout_millis(&self) -> Result<u64> {
2146 if let Some(timeout_millis) = self.reftable_lock_timeout_millis {
2147 return Ok(timeout_millis);
2148 }
2149 let config_path = self.common_dir.join("config");
2150 let Ok(config) = GitConfig::read(config_path) else {
2151 return Ok(0);
2152 };
2153 Ok(config
2154 .get("reftable", None, "lockTimeout")
2155 .and_then(|value| value.parse::<u64>().ok())
2156 .unwrap_or(0))
2157 }
2158
2159 fn acquire_reftable_list_lock(&self, list_path: PathBuf) -> Result<ReftableListLock> {
2160 let lock_path = lock_path_for(&list_path)?;
2161 let timeout_millis = self.reftable_lock_timeout_millis()?;
2162 ReftableListLock::acquire(list_path, lock_path, timeout_millis)
2163 }
2164
2165 fn append_reftable_table(
2170 &self,
2171 mut refs: Vec<ReftableRefRecord>,
2172 mut logs: Vec<ReftableLogRecord>,
2173 ) -> Result<u64> {
2174 let reftable_dir = self.storage_dir.join("reftable");
2175 fs::create_dir_all(&reftable_dir)?;
2176 let list_path = reftable_dir.join("tables.list");
2177 let list_lock = self.acquire_reftable_list_lock(list_path.clone())?;
2178 let mut table_names = self.reftable_table_names_from(&list_path)?;
2179 let update_index = self.next_reftable_update_index(&table_names)?;
2180 for record in &mut refs {
2181 record.update_index = update_index;
2182 }
2183 for record in &mut logs {
2184 record.update_index = update_index;
2185 }
2186 let table_name = reftable_table_name(update_index, update_index);
2187 let bytes = Reftable::write(self.format, update_index, update_index, &refs, &logs)?;
2188 let table_path = reftable_dir.join(&table_name);
2189 write_locked(&table_path, &bytes)?;
2190 self.apply_reftable_shared_file_mode(&table_path)?;
2191 table_names.push(table_name);
2192 let mut list = Vec::new();
2193 for name in &table_names {
2194 list.extend_from_slice(name.as_bytes());
2195 list.push(b'\n');
2196 }
2197 list_lock.commit(&list)?;
2198 self.apply_reftable_shared_file_mode(&list_path)?;
2199 if logs.is_empty() && table_names.len() > 6 {
2200 self.auto_compact_reftable_stack()?;
2201 }
2202 Ok(update_index)
2203 }
2204
2205 pub fn compact_reftable_stack(&self) -> Result<()> {
2206 let old_names = self.reftable_table_names()?;
2207 self.compact_reftable_stack_range(0, old_names.len(), true)
2208 }
2209
2210 fn compact_reftable_stack_range(
2211 &self,
2212 start: usize,
2213 end: usize,
2214 fail_on_locked_table: bool,
2215 ) -> Result<()> {
2216 let reftable_dir = self.storage_dir.join("reftable");
2217 let list_path = reftable_dir.join("tables.list");
2218 let list_lock = self.acquire_reftable_list_lock(list_path.clone())?;
2219 let old_names = self.reftable_table_names_from(&list_path)?;
2220 if start >= end || end > old_names.len() {
2221 return Ok(());
2222 }
2223 let compact_names = old_names[start..end].to_vec();
2224 if compact_names.is_empty() {
2225 return Ok(());
2226 }
2227 if fail_on_locked_table {
2228 for name in &compact_names {
2229 if reftable_dir.join(format!("{name}.lock")).exists() {
2230 return Err(GitError::Io(format!(
2231 "cannot lock references: {}: File exists",
2232 reftable_dir.join(format!("{name}.lock")).display()
2233 )));
2234 }
2235 }
2236 }
2237
2238 let mut refs: BTreeMap<String, ReftableRefRecord> = BTreeMap::new();
2239 let mut logs: BTreeMap<(String, u64), ReftableLogRecord> = BTreeMap::new();
2240 let mut min_index = u64::MAX;
2241 let mut max_index = 0u64;
2242 let drop_tombstones = start == 0;
2243 for name in &compact_names {
2244 let table = Reftable::parse(&fs::read(reftable_dir.join(name))?)?;
2245 for record in table.refs {
2246 match record.value {
2247 ReftableRefValue::Deletion if drop_tombstones => {
2248 refs.remove(&record.name);
2249 }
2250 _ => {
2251 min_index = min_index.min(record.update_index);
2252 max_index = max_index.max(record.update_index);
2253 refs.insert(record.name.clone(), record);
2254 }
2255 }
2256 }
2257 for record in table.logs {
2258 let key = (record.refname.clone(), record.update_index);
2259 match record.value {
2260 ReftableLogValue::Deletion if drop_tombstones => {
2261 logs.remove(&key);
2262 }
2263 _ => {
2264 min_index = min_index.min(record.update_index);
2265 max_index = max_index.max(record.update_index);
2266 logs.insert(key, record);
2267 }
2268 }
2269 }
2270 }
2271
2272 if refs.is_empty() && logs.is_empty() {
2273 min_index = compact_names
2274 .iter()
2275 .filter_map(|name| Reftable::parse(&fs::read(reftable_dir.join(name)).ok()?).ok())
2276 .map(|table| table.header.min_update_index)
2277 .min()
2278 .unwrap_or(1);
2279 max_index = min_index;
2280 }
2281
2282 let table_name = reftable_table_name(min_index, max_index);
2283 let refs = refs.into_values().collect::<Vec<_>>();
2284 let logs = logs.into_values().collect::<Vec<_>>();
2285 let bytes = Reftable::write(self.format, min_index, max_index, &refs, &logs)?;
2286 let table_path = reftable_dir.join(&table_name);
2287 write_locked(&table_path, &bytes)?;
2288 self.apply_reftable_shared_file_mode(&table_path)?;
2289 let mut list = Vec::new();
2290 for name in &old_names[..start] {
2291 list.extend_from_slice(name.as_bytes());
2292 list.push(b'\n');
2293 }
2294 list.extend_from_slice(table_name.as_bytes());
2295 list.push(b'\n');
2296 for name in &old_names[end..] {
2297 list.extend_from_slice(name.as_bytes());
2298 list.push(b'\n');
2299 }
2300 list_lock.commit(&list)?;
2301 self.apply_reftable_shared_file_mode(&list_path)?;
2302 for name in compact_names {
2303 if name != table_name {
2304 let _ = fs::remove_file(reftable_dir.join(name));
2305 }
2306 }
2307 Ok(())
2308 }
2309
2310 fn apply_reftable_shared_file_mode(&self, path: &Path) -> Result<()> {
2311 #[cfg(unix)]
2312 {
2313 use std::os::unix::fs::PermissionsExt;
2314
2315 let config_path = self.common_dir.join("config");
2316 let Ok(config) = GitConfig::read(config_path) else {
2317 return Ok(());
2318 };
2319 let Some(value) = config.get("core", None, "sharedRepository") else {
2320 return Ok(());
2321 };
2322 let mode_or = match value {
2323 "1" | "group" | "true" => 0o660,
2324 "2" | "all" | "world" | "everybody" => 0o664,
2325 _ => return Ok(()),
2326 };
2327 let metadata = fs::metadata(path)?;
2328 let old_mode = metadata.permissions().mode();
2329 let mut permissions = metadata.permissions();
2330 permissions.set_mode((old_mode | mode_or) & 0o7777);
2331 fs::set_permissions(path, permissions)?;
2332 }
2333 #[cfg(not(unix))]
2334 {
2335 let _ = path;
2336 }
2337 Ok(())
2338 }
2339
2340 fn auto_compact_reftable_stack(&self) -> Result<()> {
2341 if reftable_autocompaction_disabled() {
2342 return Ok(());
2343 }
2344 let names = self.reftable_table_names()?;
2345 if names.len() <= 1 {
2346 return Ok(());
2347 }
2348 let reftable_dir = self.storage_dir.join("reftable");
2349 let start = names
2350 .iter()
2351 .rposition(|name| reftable_dir.join(format!("{name}.lock")).exists())
2352 .map_or(0, |index| index + 1);
2353 if names.len().saturating_sub(start) > 1 {
2354 self.compact_reftable_stack_range(start, names.len(), false)?;
2355 }
2356 Ok(())
2357 }
2358
2359 fn read_reftable_logs(&self, name: &str) -> Result<Vec<ReflogEntry>> {
2366 let mut by_index: BTreeMap<u64, Option<ReftableLogUpdate>> = BTreeMap::new();
2369 for table in self.reftables()? {
2370 for record in table.logs {
2371 if record.refname != name {
2372 continue;
2373 }
2374 match record.value {
2375 ReftableLogValue::Deletion => {
2376 by_index.insert(record.update_index, None);
2377 }
2378 ReftableLogValue::Update(update) => {
2379 by_index.insert(record.update_index, Some(update));
2380 }
2381 }
2382 }
2383 }
2384 let null = ObjectId::null(self.format);
2385 let mut entries = Vec::new();
2386 for update in by_index.into_values().flatten() {
2387 if update.old_oid == null && update.new_oid == null {
2389 continue;
2390 }
2391 entries.push(reflog_entry_from_reftable(update));
2392 }
2393 Ok(entries)
2394 }
2395
2396 fn collect_loose_refs(
2397 &self,
2398 dir: &Path,
2399 prefix: &str,
2400 refs: &mut BTreeMap<String, Ref>,
2401 ) -> Result<()> {
2402 for entry in fs::read_dir(dir)? {
2403 let entry = entry?;
2404 let path = entry.path();
2405 let name = format!("{prefix}/{}", entry.file_name().to_string_lossy());
2406 if path.is_dir() {
2407 self.collect_loose_refs(&path, &name, refs)?;
2408 } else if !name.ends_with(".lock") {
2409 match parse_loose_ref(self.format, name.clone(), &fs::read(path)?) {
2413 Ok(reference) if ref_target_is_broken(&reference.target) => {
2414 warn_broken_ref(&name);
2415 }
2416 Ok(reference) => {
2417 refs.insert(name, reference);
2418 }
2419 Err(_) => warn_broken_ref(&name),
2420 }
2421 }
2422 }
2423 Ok(())
2424 }
2425
2426 fn collect_loose_refs_with_prefix(
2427 &self,
2428 prefix: &str,
2429 refs: &mut BTreeMap<String, Ref>,
2430 ) -> Result<()> {
2431 if !safe_ref_prefix_for_directory_scan(prefix) {
2432 return self.collect_all_loose_refs(refs);
2433 }
2434
2435 let trimmed = prefix.trim_end_matches('/');
2436 if prefix.ends_with('/') {
2437 let candidate = self.storage_dir.join(trimmed);
2438 match fs::metadata(&candidate) {
2439 Ok(meta) if meta.is_dir() => {
2440 self.collect_loose_refs(&candidate, trimmed, refs)?;
2441 return Ok(());
2442 }
2443 Ok(_) => return Ok(()),
2444 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
2445 Err(err) => return Err(err.into()),
2446 }
2447 }
2448
2449 let Some((parent_prefix, _)) = trimmed.rsplit_once('/') else {
2450 return self.collect_all_loose_refs(refs);
2451 };
2452 let parent = self.storage_dir.join(parent_prefix);
2453 match fs::metadata(&parent) {
2454 Ok(meta) if meta.is_dir() => self.collect_loose_refs(&parent, parent_prefix, refs),
2455 Ok(_) => Ok(()),
2456 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
2457 Err(err) => Err(err.into()),
2458 }
2459 }
2460
2461 fn collect_loose_ref_names(
2462 &self,
2463 dir: &Path,
2464 prefix: &str,
2465 names: &mut BTreeSet<String>,
2466 ) -> Result<()> {
2467 for entry in fs::read_dir(dir)? {
2468 let entry = entry?;
2469 let path = entry.path();
2470 let name = format!("{prefix}/{}", entry.file_name().to_string_lossy());
2471 if path.is_dir() {
2472 self.collect_loose_ref_names(&path, &name, names)?;
2473 } else if !name.ends_with(".lock") {
2474 names.insert(name);
2475 }
2476 }
2477 Ok(())
2478 }
2479
2480 fn collect_loose_ref_names_with_prefix(
2481 &self,
2482 prefix: &str,
2483 names: &mut BTreeSet<String>,
2484 ) -> Result<()> {
2485 if !safe_ref_prefix_for_directory_scan(prefix) {
2486 return self.collect_all_loose_ref_names(names);
2487 }
2488
2489 let trimmed = prefix.trim_end_matches('/');
2490 if prefix.ends_with('/') {
2491 let candidate = self.storage_dir.join(trimmed);
2492 match fs::metadata(&candidate) {
2493 Ok(meta) if meta.is_dir() => {
2494 self.collect_loose_ref_names(&candidate, trimmed, names)?;
2495 return Ok(());
2496 }
2497 Ok(_) => return Ok(()),
2498 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
2499 Err(err) => return Err(err.into()),
2500 }
2501 }
2502
2503 let Some((parent_prefix, _)) = trimmed.rsplit_once('/') else {
2504 return self.collect_all_loose_ref_names(names);
2505 };
2506 let parent = self.storage_dir.join(parent_prefix);
2507 match fs::metadata(&parent) {
2508 Ok(meta) if meta.is_dir() => {
2509 self.collect_loose_ref_names(&parent, parent_prefix, names)
2510 }
2511 Ok(_) => Ok(()),
2512 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
2513 Err(err) => Err(err.into()),
2514 }
2515 }
2516
2517 fn collect_all_loose_ref_names(&self, names: &mut BTreeSet<String>) -> Result<()> {
2518 let refs_dir = self.storage_dir.join("refs");
2519 if refs_dir.exists() {
2520 self.collect_loose_ref_names(&refs_dir, "refs", names)?;
2521 }
2522 Ok(())
2523 }
2524
2525 fn collect_all_loose_refs(&self, refs: &mut BTreeMap<String, Ref>) -> Result<()> {
2526 let refs_dir = self.storage_dir.join("refs");
2527 if refs_dir.exists() {
2528 self.collect_loose_refs(&refs_dir, "refs", refs)?;
2529 }
2530 Ok(())
2531 }
2532
2533 fn collect_reflog_names(
2534 &self,
2535 dir: &Path,
2536 prefix: &str,
2537 names: &mut BTreeSet<String>,
2538 ) -> Result<()> {
2539 let Ok(entries) = fs::read_dir(dir) else {
2540 return Ok(());
2541 };
2542 for entry in entries {
2543 let entry = entry?;
2544 let path = entry.path();
2545 let name = format!("{prefix}/{}", entry.file_name().to_string_lossy());
2546 if path.is_dir() {
2547 self.collect_reflog_names(&path, &name, names)?;
2548 } else if let Some(name) = name.strip_prefix("logs/") {
2549 names.insert(name.to_string());
2550 }
2551 }
2552 Ok(())
2553 }
2554
2555 fn loose_refs_have_prefix(&self, prefix: &str) -> Result<bool> {
2556 if !prefix.starts_with("refs/") || !prefix.ends_with('/') {
2557 return Ok(self
2558 .list_refs()?
2559 .iter()
2560 .any(|reference| reference.name.starts_with(prefix)));
2561 }
2562 let loose_prefix = prefix.trim_end_matches('/');
2563 let dir = self.common_dir.join(loose_prefix);
2564 match fs::metadata(&dir) {
2565 Ok(meta) if meta.is_dir() => {
2566 let mut refs = BTreeMap::new();
2567 self.collect_loose_refs(&dir, loose_prefix, &mut refs)?;
2568 Ok(!refs.is_empty())
2569 }
2570 Ok(_) => Ok(false),
2571 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
2572 Err(err) => Err(err.into()),
2573 }
2574 }
2575
2576 fn write_loose_ref(&self, reference: &Ref) -> Result<()> {
2577 if self.uses_reftable()? {
2578 let (store, name) = self.reftable_store_for_ref(&reference.name)?;
2579 store.append_reftable_records(vec![ReftableRefRecord {
2580 name,
2581 update_index: 0,
2582 value: reftable_value_from_ref_target(&reference.target),
2583 }])?;
2584 return Ok(());
2585 }
2586 let path = self.ref_path(&reference.name);
2587 let parent = path
2588 .parent()
2589 .ok_or_else(|| GitError::InvalidPath("ref path has no parent".into()))?;
2590 fs::create_dir_all(parent)?;
2591 write_locked(&path, &write_loose_ref(reference))
2592 }
2593
2594 fn delete_loose_ref(&self, name: &str) -> Result<()> {
2595 let path = self.ref_path(name);
2596 let lock_path = lock_path_for(&path)?;
2597 {
2598 let mut file = fs::OpenOptions::new()
2599 .write(true)
2600 .create_new(true)
2601 .open(&lock_path)?;
2602 file.write_all(b"delete\n")?;
2603 file.sync_all()?;
2604 }
2605 match fs::remove_file(&path) {
2606 Ok(()) => {
2607 fs::remove_file(lock_path)?;
2608 self.prune_empty_ref_dirs(name);
2609 Ok(())
2610 }
2611 Err(err) => {
2612 let _ = fs::remove_file(lock_path);
2613 Err(GitError::Io(err.to_string()))
2614 }
2615 }
2616 }
2617
2618 fn prune_empty_ref_dirs(&self, name: &str) {
2624 let base = self.ref_base_dir(name).to_path_buf();
2625 let refs_root = base.join("refs");
2626 if let Some(parent) = self.ref_path(name).parent() {
2627 prune_empty_dirs_up_to(parent, &refs_root);
2628 }
2629 }
2630
2631 fn remove_reflog_file(&self, name: &str) {
2637 if matches!(self.uses_reftable(), Ok(true)) {
2641 let _ = self.tombstone_reftable_logs(name);
2642 return;
2643 }
2644 let path = self.reflog_path(name);
2645 let _ = fs::remove_file(&path);
2646 let base = self.ref_base_dir(name).to_path_buf();
2647 let logs_refs_root = base.join("logs").join("refs");
2648 if let Some(parent) = path.parent() {
2649 prune_empty_dirs_up_to(parent, &logs_refs_root);
2650 }
2651 }
2652
2653 fn tombstone_reftable_logs(&self, name: &str) -> Result<()> {
2657 self.rewrite_reftable_logs(name, &[], false)
2658 }
2659
2660 fn head_reflog_mirror(
2672 head_branch: Option<&str>,
2673 reflogs: &[(String, ReflogEntry)],
2674 ) -> Vec<(String, ReflogEntry)> {
2675 let Some(head_branch) = head_branch else {
2676 return Vec::new();
2677 };
2678 if reflogs.iter().any(|(name, _)| name == "HEAD") {
2681 return Vec::new();
2682 }
2683 reflogs
2684 .iter()
2685 .filter(|(name, _)| name == head_branch)
2686 .map(|(_, entry)| ("HEAD".to_string(), entry.clone()))
2687 .collect()
2688 }
2689
2690 fn head_symref_target(&self) -> Option<String> {
2694 match self.read_ref("HEAD") {
2695 Ok(Some(RefTarget::Symbolic(branch))) => Some(branch),
2696 _ => None,
2697 }
2698 }
2699
2700 pub fn append_reflog(&self, name: &str, entry: &ReflogEntry) -> Result<()> {
2701 validate_ref_name_for_read(name)?;
2702 if self.uses_reftable()? {
2703 let update = reftable_update_from_reflog(entry)?;
2704 self.append_reftable_table(
2705 Vec::new(),
2706 vec![ReftableLogRecord {
2707 refname: name.to_string(),
2708 update_index: 0,
2709 value: ReftableLogValue::Update(update),
2710 }],
2711 )?;
2712 self.auto_compact_reftable_stack()?;
2713 return Ok(());
2714 }
2715 let path = self.reflog_path(name);
2716 let parent = path
2717 .parent()
2718 .ok_or_else(|| GitError::InvalidPath("reflog path has no parent".into()))?;
2719 fs::create_dir_all(parent)?;
2720 let mut file = fs::OpenOptions::new()
2721 .create(true)
2722 .append(true)
2723 .open(path)?;
2724 file.write_all(&entry.to_line())?;
2725 Ok(())
2726 }
2727
2728 fn rewrite_reftable_logs(
2736 &self,
2737 name: &str,
2738 entries: &[ReflogEntry],
2739 preserve_empty: bool,
2740 ) -> Result<()> {
2741 let mut live_indexes: BTreeSet<u64> = BTreeSet::new();
2744 let mut deleted_indexes: BTreeSet<u64> = BTreeSet::new();
2745 for table in self.reftables()? {
2746 for record in table.logs {
2747 if record.refname != name {
2748 continue;
2749 }
2750 match record.value {
2751 ReftableLogValue::Deletion => {
2752 deleted_indexes.insert(record.update_index);
2753 live_indexes.remove(&record.update_index);
2754 }
2755 ReftableLogValue::Update(_) => {
2756 live_indexes.insert(record.update_index);
2757 deleted_indexes.remove(&record.update_index);
2758 }
2759 }
2760 }
2761 }
2762
2763 let table_names = self.reftable_table_names()?;
2764 let base = self.next_reftable_update_index(&table_names)?;
2765 let mut logs: Vec<ReftableLogRecord> = Vec::new();
2766 for index in &live_indexes {
2768 logs.push(ReftableLogRecord {
2769 refname: name.to_string(),
2770 update_index: *index,
2771 value: ReftableLogValue::Deletion,
2772 });
2773 }
2774 for (offset, entry) in entries.iter().enumerate() {
2777 let update_index = base
2778 .checked_add(offset as u64)
2779 .ok_or_else(|| GitError::InvalidFormat("reftable update index overflow".into()))?;
2780 logs.push(ReftableLogRecord {
2781 refname: name.to_string(),
2782 update_index,
2783 value: ReftableLogValue::Update(reftable_update_from_reflog(entry)?),
2784 });
2785 }
2786 if entries.is_empty() && preserve_empty {
2787 let null = ObjectId::null(self.format);
2788 logs.push(ReftableLogRecord {
2789 refname: name.to_string(),
2790 update_index: base,
2791 value: ReftableLogValue::Update(ReftableLogUpdate {
2792 old_oid: null,
2793 new_oid: null,
2794 name: String::new(),
2795 email: String::new(),
2796 time: 0,
2797 tz_offset: 0,
2798 message: String::new(),
2799 }),
2800 });
2801 }
2802 if logs.is_empty() {
2803 return Ok(());
2804 }
2805 let leave_empty_rewrite_tombstones_separate = entries.is_empty();
2806 if leave_empty_rewrite_tombstones_separate
2807 && !reftable_autocompaction_disabled()
2808 && self.reftable_table_names()?.len() > 1
2809 {
2810 self.compact_reftable_stack()?;
2811 }
2812 self.append_reftable_table_spanning(Vec::new(), logs)?;
2813 if !leave_empty_rewrite_tombstones_separate {
2814 self.auto_compact_reftable_stack()?;
2815 }
2816 Ok(())
2817 }
2818
2819 fn append_reftable_table_spanning(
2824 &self,
2825 mut refs: Vec<ReftableRefRecord>,
2826 logs: Vec<ReftableLogRecord>,
2827 ) -> Result<u64> {
2828 let reftable_dir = self.storage_dir.join("reftable");
2829 fs::create_dir_all(&reftable_dir)?;
2830 let list_path = reftable_dir.join("tables.list");
2831 let list_lock = self.acquire_reftable_list_lock(list_path.clone())?;
2832 let mut table_names = self.reftable_table_names_from(&list_path)?;
2833 let alloc_index = self.next_reftable_update_index(&table_names)?;
2834 for record in &mut refs {
2835 record.update_index = alloc_index;
2836 }
2837 let mut min_index = alloc_index;
2838 let mut max_index = alloc_index;
2839 for record in &logs {
2840 min_index = min_index.min(record.update_index);
2841 max_index = max_index.max(record.update_index);
2842 }
2843 for record in &refs {
2844 min_index = min_index.min(record.update_index);
2845 max_index = max_index.max(record.update_index);
2846 }
2847 let table_name = reftable_table_name(min_index, max_index);
2848 let bytes = Reftable::write(self.format, min_index, max_index, &refs, &logs)?;
2849 let table_path = reftable_dir.join(&table_name);
2850 write_locked(&table_path, &bytes)?;
2851 self.apply_reftable_shared_file_mode(&table_path)?;
2852 table_names.push(table_name);
2853 let mut list = Vec::new();
2854 for name in &table_names {
2855 list.extend_from_slice(name.as_bytes());
2856 list.push(b'\n');
2857 }
2858 list_lock.commit(&list)?;
2859 self.apply_reftable_shared_file_mode(&list_path)?;
2860 Ok(max_index)
2861 }
2862
2863 fn ref_path(&self, name: &str) -> PathBuf {
2864 self.ref_base_dir(name).join(name)
2865 }
2866
2867 fn reflog_path(&self, name: &str) -> PathBuf {
2868 self.ref_base_dir(name).join("logs").join(name)
2869 }
2870
2871 fn ref_base_dir(&self, name: &str) -> &Path {
2872 if self.storage_dir != self.common_dir {
2873 return &self.storage_dir;
2874 }
2875 if is_root_ref_syntax(name)
2876 || name.starts_with("refs/worktree/")
2877 || name.starts_with("refs/rewritten/")
2878 {
2879 &self.git_dir
2880 } else {
2881 &self.common_dir
2882 }
2883 }
2884
2885 fn check_ref_directory_conflict_targeted(&self, name: &str) -> Result<()> {
2886 match self.refname_directory_conflict(name)? {
2887 Some(conflict) => Err(ref_directory_conflict_error(name, &conflict)),
2888 None => Ok(()),
2889 }
2890 }
2891
2892 pub fn refname_directory_conflict(&self, name: &str) -> Result<Option<String>> {
2898 let components = name.split('/').collect::<Vec<_>>();
2899 let mut ancestors = Vec::new();
2900 for index in 1..components.len() {
2901 let ancestor = components[..index].join("/");
2902 if self.loose_ref_file_exists_for_conflict(&ancestor)? {
2903 return Ok(Some(ancestor));
2904 }
2905 ancestors.push(ancestor);
2906 }
2907 let child_prefix = format!("{name}/");
2908 if let Some(existing) = self.first_loose_ref_name_with_prefix(&child_prefix)? {
2909 return Ok(Some(existing));
2910 }
2911 if let Some(existing) =
2912 self.first_packed_ref_directory_conflict(&ancestors, &child_prefix)?
2913 {
2914 return Ok(Some(existing));
2915 }
2916 Ok(None)
2917 }
2918
2919 fn loose_ref_file_exists_for_conflict(&self, name: &str) -> Result<bool> {
2920 let path = self.ref_path(name);
2921 match fs::symlink_metadata(path) {
2922 Ok(meta) => Ok(!meta.is_dir()),
2923 Err(err)
2924 if err.kind() == std::io::ErrorKind::NotFound
2925 || err.kind() == std::io::ErrorKind::NotADirectory =>
2926 {
2927 Ok(false)
2928 }
2929 Err(err) => Err(err.into()),
2930 }
2931 }
2932
2933 fn first_packed_ref_directory_conflict(
2934 &self,
2935 ancestors: &[String],
2936 child_prefix: &str,
2937 ) -> Result<Option<String>> {
2938 let packed_path = self.storage_dir.join("packed-refs");
2939 let text = match fs::read_to_string(packed_path) {
2940 Ok(text) => text,
2941 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
2942 Err(err) => return Err(err.into()),
2943 };
2944 for raw_line in text.lines() {
2945 let line = raw_line.trim_end();
2946 if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
2947 continue;
2948 }
2949 let (_, name) = line
2950 .split_once(' ')
2951 .ok_or_else(|| packed_refs_unexpected_line(line))?;
2952 if ancestors.iter().any(|ancestor| ancestor == name) || name.starts_with(child_prefix) {
2953 return Ok(Some(name.to_owned()));
2954 }
2955 }
2956 Ok(None)
2957 }
2958
2959 fn first_loose_ref_name_with_prefix(&self, prefix: &str) -> Result<Option<String>> {
2960 if !prefix.starts_with("refs/") || !prefix.ends_with('/') {
2961 return Ok(None);
2962 }
2963 let trimmed = prefix.trim_end_matches('/');
2964 let dir = self.ref_base_dir(trimmed).join(trimmed);
2965 self.first_loose_ref_name_in_dir(&dir, trimmed)
2966 }
2967
2968 fn first_loose_ref_name_in_dir(&self, dir: &Path, prefix: &str) -> Result<Option<String>> {
2969 let entries = match fs::read_dir(dir) {
2970 Ok(entries) => entries,
2971 Err(err)
2972 if err.kind() == std::io::ErrorKind::NotFound
2973 || err.kind() == std::io::ErrorKind::NotADirectory =>
2974 {
2975 return Ok(None);
2976 }
2977 Err(err) => return Err(err.into()),
2978 };
2979 for entry in entries {
2980 let entry = entry?;
2981 let path = entry.path();
2982 let name = format!("{prefix}/{}", entry.file_name().to_string_lossy());
2983 if path.is_dir() {
2984 if let Some(found) = self.first_loose_ref_name_in_dir(&path, &name)? {
2985 return Ok(Some(found));
2986 }
2987 } else if !name.ends_with(".lock") {
2988 return Ok(Some(name));
2989 }
2990 }
2991 Ok(None)
2992 }
2993}
2994
2995fn reftable_ref_target(value: ReftableRefValue) -> Result<Option<RefTarget>> {
2996 match value {
2997 ReftableRefValue::Deletion => Ok(None),
2998 ReftableRefValue::Direct(oid) | ReftableRefValue::Peeled { target: oid, .. } => {
2999 Ok(Some(RefTarget::Direct(oid)))
3000 }
3001 ReftableRefValue::Symbolic(target) => Ok(Some(RefTarget::Symbolic(target))),
3002 }
3003}
3004
3005fn reftable_value_from_ref_target(target: &RefTarget) -> ReftableRefValue {
3006 match target {
3007 RefTarget::Direct(oid) => ReftableRefValue::Direct(*oid),
3008 RefTarget::Symbolic(target) => ReftableRefValue::Symbolic(target.clone()),
3009 }
3010}
3011
3012fn reflog_entry_from_reftable(update: ReftableLogUpdate) -> ReflogEntry {
3019 let committer = format!(
3020 "{} <{}> {} {}",
3021 update.name,
3022 update.email,
3023 update.time,
3024 format_reflog_tz(update.tz_offset),
3025 );
3026 let mut message = update.message.into_bytes();
3031 if message.last() == Some(&b'\n') {
3032 message.pop();
3033 }
3034 ReflogEntry {
3035 old_oid: update.old_oid,
3036 new_oid: update.new_oid,
3037 committer: committer.into_bytes(),
3038 message,
3039 }
3040}
3041
3042fn reftable_update_from_reflog(entry: &ReflogEntry) -> Result<ReftableLogUpdate> {
3045 let committer = std::str::from_utf8(&entry.committer)
3046 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
3047 let (name, email, time, tz_offset) = split_committer_ident(committer)?;
3048 let mut message = std::str::from_utf8(&entry.message)
3049 .map_err(|err| GitError::InvalidFormat(err.to_string()))?
3050 .to_string();
3051 if !message.ends_with('\n') {
3055 message.push('\n');
3056 }
3057 Ok(ReftableLogUpdate {
3058 old_oid: entry.old_oid,
3059 new_oid: entry.new_oid,
3060 name,
3061 email,
3062 time,
3063 tz_offset,
3064 message,
3065 })
3066}
3067
3068fn split_committer_ident(committer: &str) -> Result<(String, String, u64, i16)> {
3073 let open = committer.find(" <").ok_or_else(|| {
3074 GitError::InvalidFormat("reflog committer is missing email opener".into())
3075 })?;
3076 let name = committer[..open].to_string();
3077 let after_open = open + 2;
3078 let close = committer[after_open..].find('>').ok_or_else(|| {
3079 GitError::InvalidFormat("reflog committer is missing email closer".into())
3080 })?;
3081 let email = committer[after_open..after_open + close].to_string();
3082 let rest = committer[after_open + close + 1..].trim();
3083 let (time_str, tz_str) = rest.split_once(' ').ok_or_else(|| {
3084 GitError::InvalidFormat("reflog committer is missing timestamp/timezone".into())
3085 })?;
3086 let time = time_str
3087 .trim()
3088 .parse::<u64>()
3089 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
3090 let tz_offset = parse_reflog_tz(tz_str.trim())?;
3091 Ok((name, email, time, tz_offset))
3092}
3093
3094fn format_reflog_tz(tz_offset: i16) -> String {
3097 let sign = if tz_offset < 0 { '-' } else { '+' };
3098 let magnitude = tz_offset.unsigned_abs();
3099 format!("{sign}{magnitude:04}")
3100}
3101
3102fn parse_reflog_tz(tz: &str) -> Result<i16> {
3105 let (sign, digits) = match tz.strip_prefix('-') {
3106 Some(rest) => (-1i16, rest),
3107 None => (1i16, tz.strip_prefix('+').unwrap_or(tz)),
3108 };
3109 let magnitude = digits
3110 .parse::<i16>()
3111 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
3112 Ok(sign * magnitude)
3113}
3114
3115fn reftable_table_name(min_update_index: u64, max_update_index: u64) -> String {
3121 let nanos = SystemTime::now()
3122 .duration_since(UNIX_EPOCH)
3123 .map(|duration| duration.as_nanos())
3124 .unwrap_or(0);
3125 let salt = (nanos as u64) ^ (u64::from(std::process::id()) << 16);
3128 format!(
3129 "0x{min_update_index:012x}-0x{max_update_index:012x}-{:08x}.ref",
3130 salt as u32
3131 )
3132}
3133
3134fn reftable_autocompaction_disabled() -> bool {
3135 std::env::var("GIT_TEST_REFTABLE_AUTOCOMPACTION")
3136 .is_ok_and(|value| value.eq_ignore_ascii_case("false"))
3137}
3138
3139#[cfg(test)]
3144fn reftable_table_name_is_valid(name: &str) -> bool {
3145 fn hex_prefix(s: &str) -> Option<&str> {
3146 let body = s
3148 .strip_prefix("0x")
3149 .or_else(|| s.strip_prefix("0X"))
3150 .unwrap_or(s);
3151 let consumed = body
3152 .find(|c: char| !c.is_ascii_hexdigit())
3153 .unwrap_or(body.len());
3154 if consumed == 0 {
3155 return None;
3156 }
3157 Some(&body[consumed..])
3158 }
3159 let Some(rest) = hex_prefix(name) else {
3160 return false;
3161 };
3162 let Some(rest) = rest.strip_prefix('-') else {
3163 return false;
3164 };
3165 let Some(rest) = hex_prefix(rest) else {
3166 return false;
3167 };
3168 let Some(rest) = rest.strip_prefix('-') else {
3169 return false;
3170 };
3171 let Some(rest) = hex_prefix(rest) else {
3172 return false;
3173 };
3174 rest == ".ref" || rest == ".log"
3175}
3176
3177fn repository_common_dir(git_dir: &Path) -> PathBuf {
3178 if let Some(common_dir) = std::env::var_os("GIT_COMMON_DIR") {
3179 return PathBuf::from(common_dir);
3180 }
3181 let commondir = git_dir.join("commondir");
3182 if let Ok(value) = fs::read_to_string(&commondir) {
3183 let path = PathBuf::from(value.trim());
3184 let common = if path.is_absolute() {
3185 path
3186 } else {
3187 git_dir.join(path)
3188 };
3189 return fs::canonicalize(&common).unwrap_or(common);
3190 }
3191 git_dir.to_path_buf()
3192}
3193
3194#[derive(Clone, Copy, PartialEq, Eq, Debug)]
3198pub enum RefTransactionPhase {
3199 Preparing,
3202 Prepared,
3205 Committed,
3207 Aborted,
3209}
3210
3211impl RefTransactionPhase {
3212 pub fn as_str(self) -> &'static str {
3214 match self {
3215 RefTransactionPhase::Preparing => "preparing",
3216 RefTransactionPhase::Prepared => "prepared",
3217 RefTransactionPhase::Committed => "committed",
3218 RefTransactionPhase::Aborted => "aborted",
3219 }
3220 }
3221}
3222
3223#[derive(Clone, Debug)]
3229pub struct RefTransactionHookUpdate {
3230 pub old_value: String,
3231 pub new_value: String,
3232 pub refname: String,
3233}
3234
3235pub trait ReferenceTransactionHook {
3247 fn run(&self, phase: RefTransactionPhase, updates: &[RefTransactionHookUpdate])
3248 -> Result<bool>;
3249}
3250
3251pub struct FileRefTransaction<'a> {
3252 store: &'a FileRefStore,
3253 changes: Vec<QueuedRefChange>,
3254 hook: Option<&'a dyn ReferenceTransactionHook>,
3255}
3256
3257struct QueuedUpdate {
3260 name: String,
3261 precondition: RefPrecondition,
3262 new: RefTarget,
3263 reflog: Option<ReflogEntry>,
3264}
3265
3266struct QueuedDelete {
3267 name: String,
3268 precondition: RefDeletePrecondition,
3269}
3270
3271enum QueuedRefChange {
3272 Update(QueuedUpdate),
3273 Delete(QueuedDelete),
3274}
3275
3276#[derive(Debug, Clone, PartialEq, Eq)]
3278pub enum RefDeletePrecondition {
3279 Any,
3281 Immediate(RefTarget),
3283 Direct(Option<ObjectId>),
3285 Peeled(ObjectId),
3287}
3288
3289impl<'a> FileRefTransaction<'a> {
3290 pub fn with_hook(mut self, hook: &'a dyn ReferenceTransactionHook) -> Self {
3296 self.hook = Some(hook);
3297 self
3298 }
3299
3300 pub fn update(&mut self, update: RefUpdate) {
3305 self.changes.push(QueuedRefChange::Update(QueuedUpdate {
3306 name: update.name,
3307 precondition: RefPrecondition::from_expected(update.expected),
3308 new: update.new,
3309 reflog: update.reflog,
3310 }));
3311 }
3312
3313 pub fn update_to(
3319 &mut self,
3320 name: impl Into<String>,
3321 new: RefTarget,
3322 precondition: RefPrecondition,
3323 reflog: Option<ReflogEntry>,
3324 ) {
3325 self.changes.push(QueuedRefChange::Update(QueuedUpdate {
3326 name: name.into(),
3327 precondition,
3328 new,
3329 reflog,
3330 }));
3331 }
3332
3333 pub fn delete(&mut self, delete: DeleteRef) {
3338 self.delete_with_precondition(
3339 delete.name,
3340 RefDeletePrecondition::Direct(delete.expected_old),
3341 delete.reflog,
3342 );
3343 }
3344
3345 pub fn delete_with_precondition(
3351 &mut self,
3352 name: impl Into<String>,
3353 precondition: RefDeletePrecondition,
3354 _reflog: Option<DeleteRefReflog>,
3355 ) {
3356 self.changes.push(QueuedRefChange::Delete(QueuedDelete {
3357 name: name.into(),
3358 precondition,
3359 }));
3360 }
3361
3362 pub fn commit(self) -> Result<()> {
3383 let FileRefTransaction {
3384 store,
3385 changes,
3386 hook,
3387 } = self;
3388 let changes = coalesce_ref_changes(changes)?;
3389 let hook_updates = hook.map(|_| hook_updates_for_changes(store.format, &changes));
3394 if let (Some(hook), Some(updates)) = (hook, hook_updates.as_ref())
3395 && hook.run(RefTransactionPhase::Preparing, updates)?
3396 {
3397 return Err(ref_transaction_hook_abort(RefTransactionPhase::Preparing));
3398 }
3399 if store.uses_reftable()? {
3400 store.commit_reftable(changes)
3401 } else {
3402 store.commit_loose_hooked(changes, hook, hook_updates.as_deref())
3403 }
3404 }
3405}
3406
3407fn ref_transaction_hook_abort(phase: RefTransactionPhase) -> GitError {
3410 GitError::Transaction(format!(
3411 "in '{}' phase, update aborted by the reference-transaction hook",
3412 phase.as_str()
3413 ))
3414}
3415
3416fn hook_updates_for_changes(
3422 format: ObjectFormat,
3423 changes: &[CoalescedRefChange],
3424) -> Vec<RefTransactionHookUpdate> {
3425 let zero = ObjectId::null(format).to_string();
3426 changes
3427 .iter()
3428 .map(|change| match change {
3429 CoalescedRefChange::Update(update) => RefTransactionHookUpdate {
3430 old_value: hook_old_value(&zero, &update.precondition),
3431 new_value: hook_target_value(&zero, Some(&update.new)),
3432 refname: update.name.clone(),
3433 },
3434 CoalescedRefChange::Delete(delete) => RefTransactionHookUpdate {
3435 old_value: hook_delete_old_value(&zero, &delete.precondition),
3436 new_value: zero.clone(),
3437 refname: delete.name.clone(),
3438 },
3439 })
3440 .collect()
3441}
3442
3443fn hook_old_value(zero: &str, precondition: &RefPrecondition) -> String {
3447 match precondition {
3448 RefPrecondition::Any | RefPrecondition::MustExist => zero.to_string(),
3449 RefPrecondition::MustNotExist => zero.to_string(),
3450 RefPrecondition::MustExistAndMatch(target) | RefPrecondition::ExistingMustMatch(target) => {
3451 hook_target_value(zero, Some(target))
3452 }
3453 }
3454}
3455
3456fn hook_delete_old_value(zero: &str, precondition: &RefDeletePrecondition) -> String {
3459 match precondition {
3460 RefDeletePrecondition::Any => zero.to_string(),
3461 RefDeletePrecondition::Immediate(target) => hook_target_value(zero, Some(target)),
3462 RefDeletePrecondition::Direct(Some(oid)) | RefDeletePrecondition::Peeled(oid) => {
3463 oid.to_string()
3464 }
3465 RefDeletePrecondition::Direct(None) => zero.to_string(),
3466 }
3467}
3468
3469fn hook_target_value(zero: &str, target: Option<&RefTarget>) -> String {
3472 match target {
3473 None => zero.to_string(),
3474 Some(RefTarget::Direct(oid)) => oid.to_string(),
3475 Some(RefTarget::Symbolic(name)) => format!("ref:{name}"),
3476 }
3477}
3478
3479impl FileRefStore {
3480 fn commit_reftable(&self, changes: Vec<CoalescedRefChange>) -> Result<()> {
3481 let routed = self.route_reftable_changes(changes)?;
3482 if routed.len() != 1 || routed[0].0.storage_dir != self.storage_dir {
3483 for (store, changes) in routed {
3484 store.commit_reftable_local(changes)?;
3485 }
3486 return Ok(());
3487 }
3488 let mut routed = routed;
3489 if let Some((_, changes)) = routed.pop() {
3490 self.commit_reftable_local(changes)
3491 } else {
3492 Ok(())
3493 }
3494 }
3495
3496 fn commit_reftable_local(&self, changes: Vec<CoalescedRefChange>) -> Result<()> {
3497 let has_reflogs = changes.iter().any(|change| {
3499 matches!(change, CoalescedRefChange::Update(update) if !update.reflog.is_empty())
3500 });
3501 let head_branch = if has_reflogs {
3502 self.head_symref_target()
3503 } else {
3504 None
3505 };
3506 if changes
3507 .iter()
3508 .any(|change| matches!(change, CoalescedRefChange::Update(_)))
3509 {
3510 let mut names = self
3511 .list_refs()?
3512 .into_iter()
3513 .map(|reference| reference.name)
3514 .collect::<BTreeSet<_>>();
3515 for change in &changes {
3516 if let CoalescedRefChange::Update(update) = change {
3517 names.insert(update.name.clone());
3518 }
3519 }
3520 for change in &changes {
3521 if let CoalescedRefChange::Update(update) = change {
3522 check_ref_directory_conflict_in_names(&update.name, &names)?;
3523 }
3524 }
3525 }
3526 let mut records = Vec::with_capacity(changes.len());
3527 let mut reflogs = Vec::new();
3528 let mut delete_names = Vec::new();
3529 for change in changes {
3530 match change {
3531 CoalescedRefChange::Update(update) => {
3532 if !matches!(update.precondition, RefPrecondition::Any) {
3533 let current = self.read_ref(&update.name)?;
3534 if !update.precondition.is_satisfied_by(current.as_ref()) {
3535 return Err(GitError::Transaction(
3536 update.precondition.describe(&update.name),
3537 ));
3538 }
3539 }
3540 records.push(ReftableRefRecord {
3541 name: update.name.clone(),
3542 update_index: 0,
3543 value: reftable_value_from_ref_target(&update.new),
3544 });
3545 for entry in update.reflog {
3546 reflogs.push((update.name.clone(), entry));
3547 }
3548 }
3549 CoalescedRefChange::Delete(delete) => {
3550 let current = self.read_ref(&delete.name)?;
3551 verify_delete_precondition(
3555 self,
3556 &delete.name,
3557 current.as_ref(),
3558 &delete.precondition,
3559 )?;
3560 records.push(ReftableRefRecord {
3561 name: delete.name.clone(),
3562 update_index: 0,
3563 value: ReftableRefValue::Deletion,
3564 });
3565 delete_names.push(delete.name.clone());
3566 }
3567 }
3568 }
3569 if self.combine_reftable_logs || self.git_dir != self.common_dir {
3570 let head_mirror = Self::head_reflog_mirror(head_branch.as_deref(), &reflogs);
3571 reflogs.extend(head_mirror);
3572 let log_records = reflogs
3573 .into_iter()
3574 .map(|(name, entry)| {
3575 Ok(ReftableLogRecord {
3576 refname: name,
3577 update_index: 0,
3578 value: ReftableLogValue::Update(reftable_update_from_reflog(&entry)?),
3579 })
3580 })
3581 .collect::<Result<Vec<_>>>()?;
3582 self.append_reftable_table(records, log_records)?;
3583 for name in &delete_names {
3584 self.remove_reflog_file(name);
3585 }
3586 return Ok(());
3587 }
3588 self.append_reftable_records(records)?;
3589 for name in &delete_names {
3593 self.remove_reflog_file(name);
3594 }
3595 let head_mirror = Self::head_reflog_mirror(head_branch.as_deref(), &reflogs);
3596 reflogs.extend(head_mirror);
3597 for (name, entry) in reflogs {
3598 self.append_reflog(&name, &entry)?;
3599 }
3600 Ok(())
3601 }
3602
3603 fn route_reftable_changes(
3604 &self,
3605 changes: Vec<CoalescedRefChange>,
3606 ) -> Result<Vec<(FileRefStore, Vec<CoalescedRefChange>)>> {
3607 let mut grouped = BTreeMap::<PathBuf, (FileRefStore, Vec<CoalescedRefChange>)>::new();
3608 for change in changes {
3609 let name = match &change {
3610 CoalescedRefChange::Update(update) => update.name.as_str(),
3611 CoalescedRefChange::Delete(delete) => delete.name.as_str(),
3612 };
3613 let (store, rewritten) = self.reftable_store_for_ref(name)?;
3614 let rewritten_change = match change {
3615 CoalescedRefChange::Update(mut update) => {
3616 update.name = rewritten;
3617 CoalescedRefChange::Update(update)
3618 }
3619 CoalescedRefChange::Delete(mut delete) => {
3620 delete.name = rewritten;
3621 CoalescedRefChange::Delete(delete)
3622 }
3623 };
3624 grouped
3625 .entry(store.storage_dir.clone())
3626 .or_insert_with(|| (store, Vec::new()))
3627 .1
3628 .push(rewritten_change);
3629 }
3630 Ok(grouped.into_values().collect())
3631 }
3632
3633 #[allow(dead_code)]
3636 fn commit_loose(&self, changes: Vec<CoalescedRefChange>) -> Result<()> {
3637 self.commit_loose_hooked(changes, None, None)
3638 }
3639
3640 fn commit_loose_hooked(
3646 &self,
3647 changes: Vec<CoalescedRefChange>,
3648 hook: Option<&dyn ReferenceTransactionHook>,
3649 hook_updates: Option<&[RefTransactionHookUpdate]>,
3650 ) -> Result<()> {
3651 let has_reflogs = changes.iter().any(|change| {
3653 matches!(change, CoalescedRefChange::Update(update) if !update.reflog.is_empty())
3654 });
3655 let head_branch = if has_reflogs {
3656 self.head_symref_target()
3657 } else {
3658 None
3659 };
3660 let has_delete = changes
3661 .iter()
3662 .any(|change| matches!(change, CoalescedRefChange::Delete(_)));
3663 let update_count = changes
3664 .iter()
3665 .filter(|change| matches!(change, CoalescedRefChange::Update(_)))
3666 .count();
3667 let targeted_conflict_check = update_count == 1 && !has_delete;
3668 let conflict_names = if update_count > 0 && !targeted_conflict_check {
3669 let mut names = self
3670 .list_refs()?
3671 .into_iter()
3672 .map(|reference| reference.name)
3673 .collect::<BTreeSet<_>>();
3674 for change in &changes {
3675 if let CoalescedRefChange::Update(update) = change {
3676 names.insert(update.name.clone());
3677 }
3678 }
3679 Some(names)
3680 } else {
3681 None
3682 };
3683 let mut pending = Vec::with_capacity(changes.len() + usize::from(has_delete));
3684 for change in &changes {
3686 let name = change.name();
3687 if matches!(change, CoalescedRefChange::Update(_)) {
3688 let conflict_result = if targeted_conflict_check {
3689 self.check_ref_directory_conflict_targeted(name)
3690 } else if let Some(conflict_names) = conflict_names.as_ref() {
3691 check_ref_directory_conflict_in_names(name, conflict_names)
3692 } else {
3693 Ok(())
3694 };
3695 if let Err(err) = conflict_result {
3696 release_pending_locks(&pending);
3697 return Err(err);
3698 }
3699 }
3700 let path = self.ref_path(name);
3701 let parent = path
3702 .parent()
3703 .ok_or_else(|| GitError::InvalidPath("ref path has no parent".into()))?;
3704 if let Err(err) = fs::create_dir_all(parent) {
3705 release_pending_locks(&pending);
3706 if err.kind() == std::io::ErrorKind::NotADirectory {
3707 return Err(ref_directory_conflict_error(
3708 name,
3709 &parent_to_ref_name(self.ref_base_dir(name), parent),
3710 ));
3711 }
3712 return Err(GitError::Io(err.to_string()));
3713 }
3714 let lock_path = match lock_path_for(&path) {
3715 Ok(lock_path) => lock_path,
3716 Err(err) => {
3717 release_pending_locks(&pending);
3718 return Err(err);
3719 }
3720 };
3721 if let Err(err) = fs::OpenOptions::new()
3722 .write(true)
3723 .create_new(true)
3724 .open(&lock_path)
3725 {
3726 release_pending_locks(&pending);
3727 return Err(GitError::Io(format!("could not lock ref {name}: {err}")));
3728 }
3729 let action = match change {
3730 CoalescedRefChange::Update(update) => PendingPathAction::Write {
3731 contents: write_loose_ref(&Ref {
3732 name: update.name.clone(),
3733 target: update.new.clone(),
3734 }),
3735 },
3736 CoalescedRefChange::Delete(_) => PendingPathAction::Delete,
3737 };
3738 pending.push(PendingPathChange {
3739 name: name.to_string(),
3740 path,
3741 lock_path,
3742 original: None,
3743 action,
3744 });
3745 }
3746
3747 let packed_path = self.storage_dir.join("packed-refs");
3748 let mut packed_refs = Vec::new();
3749 let mut use_packed_snapshot = false;
3750 if has_delete {
3751 let packed_lock_path = match lock_path_for(&packed_path) {
3752 Ok(lock_path) => lock_path,
3753 Err(err) => {
3754 release_pending_locks(&pending);
3755 return Err(err);
3756 }
3757 };
3758 if let Err(err) = fs::OpenOptions::new()
3759 .write(true)
3760 .create_new(true)
3761 .open(&packed_lock_path)
3762 {
3763 release_pending_locks(&pending);
3764 return Err(GitError::Io(format!("could not lock packed-refs: {err}")));
3765 }
3766 let packed_original = match fs::read(&packed_path) {
3767 Ok(bytes) => Some(bytes),
3768 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
3769 Err(err) => {
3770 release_pending_locks(&pending);
3771 let _ = fs::remove_file(&packed_lock_path);
3772 return Err(GitError::Io(err.to_string()));
3773 }
3774 };
3775 packed_refs = match &packed_original {
3776 Some(bytes) => match parse_packed_refs(self.format, bytes) {
3777 Ok(refs) => refs,
3778 Err(err) => {
3779 release_pending_locks(&pending);
3780 let _ = fs::remove_file(&packed_lock_path);
3781 return Err(err);
3782 }
3783 },
3784 None => Vec::new(),
3785 };
3786 use_packed_snapshot = true;
3787 pending.push(PendingPathChange {
3788 name: "packed-refs".into(),
3789 path: packed_path.clone(),
3790 lock_path: packed_lock_path,
3791 original: packed_original,
3792 action: PendingPathAction::ReleaseLock,
3793 });
3794 } else if packed_path.exists() {
3795 packed_refs = parse_packed_refs(self.format, &fs::read(&packed_path)?)?;
3796 use_packed_snapshot = true;
3797 }
3798 let packed_ref_targets = packed_refs
3799 .iter()
3800 .map(|reference| {
3801 (
3802 reference.reference.name.clone(),
3803 reference.reference.target.clone(),
3804 )
3805 })
3806 .collect::<HashMap<_, _>>();
3807
3808 let mut reflogs = Vec::new();
3812 let mut delete_names = BTreeSet::new();
3813 for index in 0..changes.len() {
3814 match &changes[index] {
3815 CoalescedRefChange::Update(update) => {
3816 let current = if use_packed_snapshot {
3817 match self.read_ref_from_packed_snapshot(&update.name, &packed_ref_targets)
3818 {
3819 Ok(current) => current,
3820 Err(err) => {
3821 release_pending_locks(&pending);
3822 return Err(err);
3823 }
3824 }
3825 } else {
3826 match self.read_ref(&update.name) {
3827 Ok(current) => current,
3828 Err(err) => {
3829 release_pending_locks(&pending);
3830 return Err(err);
3831 }
3832 }
3833 };
3834 if !matches!(update.precondition, RefPrecondition::Any)
3835 && !update.precondition.is_satisfied_by(current.as_ref())
3836 {
3837 release_pending_locks(&pending);
3838 return Err(GitError::Transaction(
3839 update.precondition.describe(&update.name),
3840 ));
3841 }
3842 pending[index].original = match read_optional_file(&pending[index].path) {
3843 Ok(original) => original,
3844 Err(err) => {
3845 release_pending_locks(&pending);
3846 return Err(err);
3847 }
3848 };
3849 if pending[index].original.is_none() && current.as_ref() == Some(&update.new) {
3850 pending[index].action = PendingPathAction::ReleaseLock;
3851 }
3852 for entry in &update.reflog {
3853 reflogs.push((update.name.clone(), entry.clone()));
3854 }
3855 }
3856 CoalescedRefChange::Delete(delete) => {
3857 let state = match self.read_locked_ref_state(&delete.name, &packed_ref_targets)
3858 {
3859 Ok(state) => state,
3860 Err(err) => {
3861 release_pending_locks(&pending);
3862 return Err(err);
3863 }
3864 };
3865 if let Err(err) = verify_delete_precondition(
3869 self,
3870 &delete.name,
3871 state.current.as_ref(),
3872 &delete.precondition,
3873 ) {
3874 release_pending_locks(&pending);
3875 return Err(err);
3876 }
3877 pending[index].original = if state.has_loose {
3878 match read_optional_file(&pending[index].path) {
3879 Ok(original) => original,
3880 Err(err) => {
3881 release_pending_locks(&pending);
3882 return Err(err);
3883 }
3884 }
3885 } else {
3886 None
3887 };
3888 delete_names.insert(delete.name.clone());
3889 }
3890 }
3891 }
3892
3893 if has_delete {
3894 let old_len = packed_refs.len();
3895 packed_refs.retain(|reference| !delete_names.contains(&reference.reference.name));
3896 if packed_refs.len() != old_len {
3897 let packed_bytes = match write_packed_refs(&packed_refs) {
3898 Ok(bytes) => bytes,
3899 Err(err) => {
3900 release_pending_locks(&pending);
3901 return Err(err);
3902 }
3903 };
3904 if let Some(packed) = pending.last_mut() {
3905 packed.action = PendingPathAction::Write {
3906 contents: packed_bytes,
3907 };
3908 }
3909 }
3910 }
3911
3912 for change in &pending {
3915 if let Err(err) = stage_pending_change(change) {
3916 release_pending_locks(&pending);
3917 return Err(err);
3918 }
3919 }
3920
3921 if let (Some(hook), Some(updates)) = (hook, hook_updates)
3925 && hook.run(RefTransactionPhase::Prepared, updates)?
3926 {
3927 release_pending_locks(&pending);
3928 return Err(ref_transaction_hook_abort(RefTransactionPhase::Prepared));
3929 }
3930
3931 for index in 0..pending.len() {
3934 if let Err(err) = maybe_fail_loose_commit_action(index) {
3935 rollback_after_apply(&pending, index);
3936 return Err(err);
3937 }
3938 if let Err(err) = apply_pending_change(&pending[index]) {
3939 rollback_after_apply(&pending, index + 1);
3940 return Err(err);
3941 }
3942 if matches!(pending[index].action, PendingPathAction::Delete) {
3943 self.prune_empty_ref_dirs(&pending[index].name);
3944 }
3945 }
3946
3947 for name in &delete_names {
3951 self.remove_reflog_file(name);
3952 }
3953 let head_mirror = Self::head_reflog_mirror(head_branch.as_deref(), &reflogs);
3956 reflogs.extend(head_mirror);
3957 for (name, entry) in reflogs {
3959 self.append_reflog(&name, &entry)?;
3960 }
3961 if let (Some(hook), Some(updates)) = (hook, hook_updates) {
3964 hook.run(RefTransactionPhase::Committed, updates)?;
3965 }
3966 Ok(())
3967 }
3968
3969 fn read_ref_from_packed_snapshot(
3970 &self,
3971 name: &str,
3972 packed_refs: &HashMap<String, RefTarget>,
3973 ) -> Result<Option<RefTarget>> {
3974 let state = self.read_locked_ref_state(name, packed_refs)?;
3975 Ok(state.current)
3976 }
3977
3978 fn read_locked_ref_state(
3979 &self,
3980 name: &str,
3981 packed_refs: &HashMap<String, RefTarget>,
3982 ) -> Result<LockedRefState> {
3983 let loose = self.read_loose_ref(name)?;
3984 let current = if let Some(reference) = loose.as_ref() {
3985 Some(reference.target.clone())
3986 } else {
3987 packed_refs.get(name).cloned()
3988 };
3989 Ok(LockedRefState {
3990 current,
3991 has_loose: loose.is_some(),
3992 })
3993 }
3994}
3995
3996struct LockedRefState {
3997 current: Option<RefTarget>,
3998 has_loose: bool,
3999}
4000
4001enum CoalescedRefChange {
4002 Update(CoalescedRefUpdate),
4003 Delete(CoalescedRefDelete),
4004}
4005
4006impl CoalescedRefChange {
4007 fn name(&self) -> &str {
4008 match self {
4009 Self::Update(update) => &update.name,
4010 Self::Delete(delete) => &delete.name,
4011 }
4012 }
4013}
4014
4015struct CoalescedRefUpdate {
4017 name: String,
4018 precondition: RefPrecondition,
4019 new: RefTarget,
4020 reflog: Vec<ReflogEntry>,
4021}
4022
4023struct CoalescedRefDelete {
4024 name: String,
4025 precondition: RefDeletePrecondition,
4026}
4027
4028fn coalesce_ref_changes(changes: Vec<QueuedRefChange>) -> Result<Vec<CoalescedRefChange>> {
4029 let has_delete = changes
4030 .iter()
4031 .any(|change| matches!(change, QueuedRefChange::Delete(_)));
4032 if !has_delete {
4033 let updates = changes
4034 .into_iter()
4035 .map(|change| match change {
4036 QueuedRefChange::Update(update) => update,
4037 QueuedRefChange::Delete(_) => unreachable!("has_delete was false"),
4038 })
4039 .collect::<Vec<_>>();
4040 return coalesce_ref_updates(updates).map(|updates| {
4041 updates
4042 .into_iter()
4043 .map(CoalescedRefChange::Update)
4044 .collect()
4045 });
4046 }
4047
4048 let mut seen = BTreeSet::new();
4049 let mut coalesced = Vec::with_capacity(changes.len());
4050 for change in changes {
4051 let name = match &change {
4052 QueuedRefChange::Update(update) => &update.name,
4053 QueuedRefChange::Delete(delete) => &delete.name,
4054 };
4055 match &change {
4056 QueuedRefChange::Update(update) => validate_ref_name_for_update(&update.name)?,
4057 QueuedRefChange::Delete(delete) => validate_ref_name_for_read(&delete.name)?,
4058 }
4059 if !seen.insert(name.clone()) {
4060 return Err(GitError::Transaction(format!(
4061 "ref {name} appears more than once in transaction"
4062 )));
4063 }
4064 coalesced.push(match change {
4065 QueuedRefChange::Update(update) => CoalescedRefChange::Update(CoalescedRefUpdate {
4066 name: update.name,
4067 precondition: update.precondition,
4068 new: update.new,
4069 reflog: update.reflog.into_iter().collect(),
4070 }),
4071 QueuedRefChange::Delete(delete) => CoalescedRefChange::Delete(CoalescedRefDelete {
4072 name: delete.name,
4073 precondition: delete.precondition,
4074 }),
4075 });
4076 }
4077 Ok(coalesced)
4078}
4079
4080fn coalesce_ref_updates(updates: Vec<QueuedUpdate>) -> Result<Vec<CoalescedRefUpdate>> {
4085 let mut order: Vec<String> = Vec::new();
4086 let mut by_name: HashMap<String, CoalescedRefUpdate> = HashMap::new();
4087 for update in updates {
4088 validate_ref_name_for_update(&update.name)?;
4089 match by_name.get_mut(&update.name) {
4090 Some(existing) => {
4091 existing.new = update.new;
4092 if let Some(entry) = update.reflog {
4093 existing.reflog.push(entry);
4094 }
4095 }
4096 None => {
4097 order.push(update.name.clone());
4098 by_name.insert(
4099 update.name.clone(),
4100 CoalescedRefUpdate {
4101 name: update.name,
4102 precondition: update.precondition,
4103 new: update.new,
4104 reflog: update.reflog.into_iter().collect(),
4105 },
4106 );
4107 }
4108 }
4109 }
4110 let mut coalesced = Vec::with_capacity(order.len());
4111 for name in order {
4112 if let Some(update) = by_name.remove(&name) {
4113 coalesced.push(update);
4114 }
4115 }
4116 Ok(coalesced)
4117}
4118
4119struct PendingPathChange {
4122 name: String,
4123 path: PathBuf,
4124 lock_path: PathBuf,
4125 original: Option<Vec<u8>>,
4126 action: PendingPathAction,
4127}
4128
4129enum PendingPathAction {
4130 Write { contents: Vec<u8> },
4131 Delete,
4132 ReleaseLock,
4133}
4134
4135struct RefDirPruneGuard<'a> {
4136 store: &'a FileRefStore,
4137 name: String,
4138}
4139
4140impl Drop for RefDirPruneGuard<'_> {
4141 fn drop(&mut self) {
4142 self.store.prune_empty_ref_dirs(&self.name);
4143 }
4144}
4145
4146struct DeleteLock {
4147 path: PathBuf,
4148 file: Option<fs::File>,
4149 active: bool,
4150}
4151
4152impl DeleteLock {
4153 fn acquire(path: PathBuf) -> std::result::Result<Self, RefDeleteError> {
4154 match fs::OpenOptions::new()
4155 .write(true)
4156 .create_new(true)
4157 .open(&path)
4158 {
4159 Ok(file) => Ok(Self {
4160 path,
4161 file: Some(file),
4162 active: true,
4163 }),
4164 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
4165 Err(RefDeleteError::Locked)
4166 }
4167 Err(err) => Err(RefDeleteError::Io(err)),
4168 }
4169 }
4170
4171 fn write_all(&mut self, bytes: &[u8]) -> std::result::Result<(), RefDeleteError> {
4172 let Some(file) = self.file.as_mut() else {
4173 return Err(RefDeleteError::Io(std::io::Error::other(
4174 "lock file is already closed",
4175 )));
4176 };
4177 file.set_len(0)?;
4178 file.write_all(bytes)?;
4179 file.sync_all()?;
4180 Ok(())
4181 }
4182
4183 fn close(mut self) -> PathBuf {
4184 self.active = false;
4185 let _ = self.file.take();
4186 self.path.clone()
4187 }
4188
4189 fn remove(mut self) {
4190 self.active = false;
4191 let _ = self.file.take();
4192 let _ = fs::remove_file(&self.path);
4193 }
4194}
4195
4196impl Drop for DeleteLock {
4197 fn drop(&mut self) {
4198 if self.active {
4199 let _ = self.file.take();
4200 let _ = fs::remove_file(&self.path);
4201 }
4202 }
4203}
4204
4205struct ReftableListLock {
4206 list_path: PathBuf,
4207 lock_path: PathBuf,
4208 file: Option<fs::File>,
4209 active: bool,
4210}
4211
4212impl ReftableListLock {
4213 fn acquire(list_path: PathBuf, lock_path: PathBuf, timeout_millis: u64) -> Result<Self> {
4214 let start = SystemTime::now();
4215 loop {
4216 match fs::OpenOptions::new()
4217 .write(true)
4218 .create_new(true)
4219 .open(&lock_path)
4220 {
4221 Ok(file) => {
4222 return Ok(Self {
4223 list_path,
4224 lock_path,
4225 file: Some(file),
4226 active: true,
4227 });
4228 }
4229 Err(err)
4230 if err.kind() == std::io::ErrorKind::AlreadyExists && timeout_millis > 0 =>
4231 {
4232 let elapsed = start
4233 .elapsed()
4234 .unwrap_or_else(|_| Duration::from_millis(timeout_millis + 1));
4235 if elapsed.as_millis() as u64 >= timeout_millis {
4236 return Err(GitError::Io(format!(
4237 "cannot lock references: {}: File exists",
4238 lock_path.display()
4239 )));
4240 }
4241 thread::sleep(Duration::from_millis(50));
4242 }
4243 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
4244 return Err(GitError::Io(format!(
4245 "cannot lock references: {}: File exists",
4246 lock_path.display()
4247 )));
4248 }
4249 Err(err) => return Err(err.into()),
4250 }
4251 }
4252 }
4253
4254 fn commit(mut self, bytes: &[u8]) -> Result<()> {
4255 let Some(mut file) = self.file.take() else {
4256 return Err(GitError::Io("reftable list lock is already closed".into()));
4257 };
4258 file.set_len(0)?;
4259 file.write_all(bytes)?;
4260 file.sync_all()?;
4261 drop(file);
4262 fs::rename(&self.lock_path, &self.list_path)
4263 .map_err(|err| GitError::Io(err.to_string()))?;
4264 self.active = false;
4265 Ok(())
4266 }
4267}
4268
4269impl Drop for ReftableListLock {
4270 fn drop(&mut self) {
4271 if self.active {
4272 let _ = self.file.take();
4273 let _ = fs::remove_file(&self.lock_path);
4274 }
4275 }
4276}
4277
4278fn checked_delete_oid(
4279 expected: Option<ObjectId>,
4280 current: Option<RefTarget>,
4281) -> std::result::Result<ObjectId, RefDeleteError> {
4282 let Some(current) = current else {
4283 return Err(RefDeleteError::NotFound);
4284 };
4285 let RefTarget::Direct(actual) = current else {
4286 return Err(RefDeleteError::ExpectedMismatch {
4287 expected,
4288 actual: None,
4289 });
4290 };
4291 if let Some(expected_oid) = expected
4292 && expected_oid != actual
4293 {
4294 return Err(RefDeleteError::ExpectedMismatch {
4295 expected: Some(expected_oid),
4296 actual: Some(actual),
4297 });
4298 }
4299 Ok(actual)
4300}
4301
4302fn verify_delete_precondition(
4308 store: &FileRefStore,
4309 name: &str,
4310 current: Option<&RefTarget>,
4311 precondition: &RefDeletePrecondition,
4312) -> Result<()> {
4313 let Some(current) = current else {
4314 return Err(GitError::Transaction(format!("ref {name} not found")));
4315 };
4316 match precondition {
4317 RefDeletePrecondition::Any => {
4318 peeled_oid_for_delete(store, current)?;
4319 Ok(())
4320 }
4321 RefDeletePrecondition::Immediate(expected) if current == expected => {
4322 peeled_oid_for_delete(store, current)?;
4323 Ok(())
4324 }
4325 RefDeletePrecondition::Immediate(_) => Err(delete_precondition_mismatch(name)),
4326 RefDeletePrecondition::Direct(expected) => {
4327 let RefTarget::Direct(actual) = current else {
4328 return Err(delete_precondition_mismatch(name));
4329 };
4330 if let Some(expected) = expected
4331 && expected != actual
4332 {
4333 return Err(delete_precondition_mismatch(name));
4334 }
4335 Ok(())
4336 }
4337 RefDeletePrecondition::Peeled(expected) => {
4338 let actual = peeled_oid_for_delete(store, current)?;
4339 if actual == Some(*expected) {
4340 Ok(())
4341 } else {
4342 Err(delete_precondition_mismatch(name))
4343 }
4344 }
4345 }
4346}
4347
4348fn peeled_oid_for_delete(store: &FileRefStore, target: &RefTarget) -> Result<Option<ObjectId>> {
4349 match target {
4350 RefTarget::Direct(oid) => Ok(Some(*oid)),
4351 RefTarget::Symbolic(name) => resolve_ref_peeled(store, name),
4352 }
4353}
4354
4355fn delete_precondition_mismatch(name: &str) -> GitError {
4356 GitError::Transaction(format!("expected ref {name} to match"))
4357}
4358
4359fn ref_delete_error_from_git(err: GitError) -> RefDeleteError {
4360 match err {
4361 GitError::InvalidPath(_) => RefDeleteError::InvalidName,
4362 GitError::NotFound(_) => RefDeleteError::NotFound,
4363 GitError::Io(message) if message.contains("File exists") => RefDeleteError::Locked,
4364 GitError::Io(message) if message.contains("could not lock") => RefDeleteError::Locked,
4365 GitError::Transaction(message) if message.contains("could not lock") => {
4366 RefDeleteError::Locked
4367 }
4368 other => RefDeleteError::Io(std::io::Error::other(other.to_string())),
4369 }
4370}
4371
4372fn read_optional_file(path: &Path) -> Result<Option<Vec<u8>>> {
4373 match fs::read(path) {
4374 Ok(bytes) => Ok(Some(bytes)),
4375 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
4376 Err(err) if err.kind() == std::io::ErrorKind::IsADirectory => Ok(None),
4377 Err(err) => Err(GitError::Io(err.to_string())),
4378 }
4379}
4380
4381fn remove_empty_dir_tree(path: &Path) -> std::io::Result<()> {
4385 let meta = match fs::symlink_metadata(path) {
4386 Ok(meta) => meta,
4387 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
4388 Err(err) => return Err(err),
4389 };
4390 if !meta.is_dir() {
4391 return Ok(());
4392 }
4393 for entry in fs::read_dir(path)? {
4394 let entry = entry?;
4395 if entry.file_type()?.is_dir() {
4396 remove_empty_dir_tree(&entry.path())?;
4397 } else {
4398 return Err(std::io::Error::other("directory not empty"));
4399 }
4400 }
4401 fs::remove_dir(path)
4402}
4403
4404fn stage_lock_file(lock_path: &Path, contents: &[u8]) -> Result<()> {
4405 let mut file = fs::OpenOptions::new()
4406 .write(true)
4407 .truncate(true)
4408 .open(lock_path)?;
4409 file.write_all(contents)?;
4410 Ok(())
4411}
4412
4413fn stage_pending_change(change: &PendingPathChange) -> Result<()> {
4414 match &change.action {
4415 PendingPathAction::Write { contents } => stage_lock_file(&change.lock_path, contents),
4416 PendingPathAction::Delete => stage_lock_file(&change.lock_path, b"delete\n"),
4417 PendingPathAction::ReleaseLock => Ok(()),
4418 }
4419}
4420
4421fn apply_pending_change(change: &PendingPathChange) -> Result<()> {
4422 match &change.action {
4423 PendingPathAction::Write { .. } => {
4424 remove_empty_dir_tree(&change.path).map_err(|err| GitError::Io(err.to_string()))?;
4428 fs::rename(&change.lock_path, &change.path).map_err(|err| GitError::Io(err.to_string()))
4429 }
4430 PendingPathAction::Delete => {
4431 if change.original.is_some() {
4432 fs::remove_file(&change.path).map_err(|err| GitError::Io(err.to_string()))?;
4433 }
4434 fs::remove_file(&change.lock_path).map_err(|err| GitError::Io(err.to_string()))
4435 }
4436 PendingPathAction::ReleaseLock => {
4437 fs::remove_file(&change.lock_path).map_err(|err| GitError::Io(err.to_string()))
4438 }
4439 }
4440}
4441
4442fn release_pending_locks(pending: &[PendingPathChange]) {
4445 for change in pending {
4446 let _ = fs::remove_file(&change.lock_path);
4447 }
4448}
4449
4450fn rollback_after_apply(pending: &[PendingPathChange], applied: usize) {
4454 for change in pending.iter().take(applied) {
4455 if matches!(change.action, PendingPathAction::ReleaseLock) {
4456 let _ = fs::remove_file(&change.lock_path);
4457 continue;
4458 }
4459 match &change.original {
4460 Some(bytes) => {
4461 let _ = restore_file_atomically(&change.path, bytes);
4462 }
4463 None => {
4464 let _ = fs::remove_file(&change.path);
4465 }
4466 }
4467 let _ = fs::remove_file(&change.lock_path);
4468 }
4469 for change in pending.iter().skip(applied) {
4470 let _ = fs::remove_file(&change.lock_path);
4471 }
4472}
4473
4474#[cfg(test)]
4475thread_local! {
4476 static FAIL_LOOSE_COMMIT_ACTION: std::cell::Cell<Option<usize>> =
4477 const { std::cell::Cell::new(None) };
4478}
4479
4480#[cfg(test)]
4481fn set_fail_loose_commit_action_for_test(index: Option<usize>) {
4482 FAIL_LOOSE_COMMIT_ACTION.with(|cell| cell.set(index));
4483}
4484
4485#[cfg(test)]
4486fn maybe_fail_loose_commit_action(index: usize) -> Result<()> {
4487 let should_fail = FAIL_LOOSE_COMMIT_ACTION.with(|cell| cell.get() == Some(index));
4488 if should_fail {
4489 FAIL_LOOSE_COMMIT_ACTION.with(|cell| cell.set(None));
4490 return Err(GitError::Io(format!(
4491 "injected loose ref transaction failure at action {index}"
4492 )));
4493 }
4494 Ok(())
4495}
4496
4497#[cfg(not(test))]
4498fn maybe_fail_loose_commit_action(_index: usize) -> Result<()> {
4499 Ok(())
4500}
4501
4502fn restore_file_atomically(path: &Path, bytes: &[u8]) -> Result<()> {
4505 if let Some(parent) = path.parent() {
4506 fs::create_dir_all(parent)?;
4507 }
4508 write_locked(path, bytes)
4509}
4510
4511#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
4512pub struct FullRefName<'a> {
4513 name: &'a str,
4514}
4515
4516impl<'a> FullRefName<'a> {
4517 pub fn new(name: &'a str) -> Result<Self> {
4518 validate_ref_name(name)?;
4519 Ok(Self { name })
4520 }
4521
4522 pub fn as_str(&self) -> &str {
4523 self.name
4524 }
4525
4526 pub fn into_str(self) -> &'a str {
4527 self.name
4528 }
4529
4530 pub fn to_owned(&self) -> FullRefNameBuf {
4531 FullRefNameBuf {
4532 name: self.name.to_string(),
4533 }
4534 }
4535
4536 pub fn as_branch(&self) -> Result<BranchRefName<'a>> {
4537 BranchRefName::from_full_ref(*self)
4538 }
4539
4540 pub fn as_tag(&self) -> Result<TagRefName<'a>> {
4541 TagRefName::from_full_ref(*self)
4542 }
4543
4544 pub fn as_remote(&self) -> Result<RemoteRefName<'a>> {
4545 RemoteRefName::from_full_ref(*self)
4546 }
4547}
4548
4549impl AsRef<str> for FullRefName<'_> {
4550 fn as_ref(&self) -> &str {
4551 self.as_str()
4552 }
4553}
4554
4555impl fmt::Display for FullRefName<'_> {
4556 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4557 f.write_str(self.as_str())
4558 }
4559}
4560
4561#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4562pub struct FullRefNameBuf {
4563 name: String,
4564}
4565
4566impl FullRefNameBuf {
4567 pub fn new(name: impl Into<String>) -> Result<Self> {
4568 let name = name.into();
4569 validate_ref_name(&name)?;
4570 Ok(Self { name })
4571 }
4572
4573 pub fn as_ref_name(&self) -> FullRefName<'_> {
4574 FullRefName { name: &self.name }
4575 }
4576
4577 pub fn as_str(&self) -> &str {
4578 &self.name
4579 }
4580
4581 pub fn into_string(self) -> String {
4582 self.name
4583 }
4584
4585 pub fn as_branch(&self) -> Result<BranchRefName<'_>> {
4586 self.as_ref_name().as_branch()
4587 }
4588
4589 pub fn as_tag(&self) -> Result<TagRefName<'_>> {
4590 self.as_ref_name().as_tag()
4591 }
4592
4593 pub fn as_remote(&self) -> Result<RemoteRefName<'_>> {
4594 self.as_ref_name().as_remote()
4595 }
4596}
4597
4598impl AsRef<str> for FullRefNameBuf {
4599 fn as_ref(&self) -> &str {
4600 self.as_str()
4601 }
4602}
4603
4604impl Borrow<str> for FullRefNameBuf {
4605 fn borrow(&self) -> &str {
4606 self.as_str()
4607 }
4608}
4609
4610impl Deref for FullRefNameBuf {
4611 type Target = str;
4612
4613 fn deref(&self) -> &Self::Target {
4614 self.as_str()
4615 }
4616}
4617
4618impl fmt::Display for FullRefNameBuf {
4619 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4620 f.write_str(self.as_str())
4621 }
4622}
4623
4624#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
4625pub struct BranchRefName<'a> {
4626 name: &'a str,
4627}
4628
4629impl<'a> BranchRefName<'a> {
4630 pub const PREFIX: &'static str = "refs/heads/";
4631
4632 pub fn from_full(name: &'a str) -> Result<Self> {
4633 let full = FullRefName::new(name)?;
4634 Self::from_full_ref(full)
4635 }
4636
4637 pub fn from_full_ref(name: FullRefName<'a>) -> Result<Self> {
4638 validate_namespaced_ref(name.as_str(), Self::PREFIX, "branch")?;
4639 Ok(Self {
4640 name: name.into_str(),
4641 })
4642 }
4643
4644 pub fn as_full_ref_name(&self) -> FullRefName<'a> {
4645 FullRefName { name: self.name }
4646 }
4647
4648 pub fn as_str(&self) -> &str {
4649 self.name
4650 }
4651
4652 pub fn branch_name(&self) -> &str {
4653 self.short_name()
4654 }
4655
4656 pub fn short_name(&self) -> &str {
4657 &self.name[Self::PREFIX.len()..]
4658 }
4659
4660 pub fn into_str(self) -> &'a str {
4661 self.name
4662 }
4663
4664 pub fn to_owned(&self) -> BranchRefNameBuf {
4665 BranchRefNameBuf {
4666 name: self.name.to_string(),
4667 }
4668 }
4669}
4670
4671impl AsRef<str> for BranchRefName<'_> {
4672 fn as_ref(&self) -> &str {
4673 self.as_str()
4674 }
4675}
4676
4677impl fmt::Display for BranchRefName<'_> {
4678 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4679 f.write_str(self.as_str())
4680 }
4681}
4682
4683impl<'a> From<BranchRefName<'a>> for FullRefName<'a> {
4684 fn from(name: BranchRefName<'a>) -> Self {
4685 name.as_full_ref_name()
4686 }
4687}
4688
4689#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4690pub struct BranchRefNameBuf {
4691 name: String,
4692}
4693
4694impl BranchRefNameBuf {
4695 pub fn from_branch_name(branch: &str) -> Result<Self> {
4696 validate_short_ref_name("branch", branch)?;
4697 let name = format!("{}{}", BranchRefName::PREFIX, branch);
4698 Self::from_full(name)
4699 }
4700
4701 pub fn from_full(name: impl Into<String>) -> Result<Self> {
4702 let name = name.into();
4703 BranchRefName::from_full(&name)?;
4704 Ok(Self { name })
4705 }
4706
4707 pub fn as_ref_name(&self) -> BranchRefName<'_> {
4708 BranchRefName { name: &self.name }
4709 }
4710
4711 pub fn as_full_ref_name(&self) -> FullRefName<'_> {
4712 FullRefName { name: &self.name }
4713 }
4714
4715 pub fn as_str(&self) -> &str {
4716 &self.name
4717 }
4718
4719 pub fn branch_name(&self) -> &str {
4720 self.short_name()
4721 }
4722
4723 pub fn short_name(&self) -> &str {
4724 &self.name[BranchRefName::PREFIX.len()..]
4725 }
4726
4727 pub fn into_string(self) -> String {
4728 self.name
4729 }
4730}
4731
4732impl AsRef<str> for BranchRefNameBuf {
4733 fn as_ref(&self) -> &str {
4734 self.as_str()
4735 }
4736}
4737
4738impl Borrow<str> for BranchRefNameBuf {
4739 fn borrow(&self) -> &str {
4740 self.as_str()
4741 }
4742}
4743
4744impl Deref for BranchRefNameBuf {
4745 type Target = str;
4746
4747 fn deref(&self) -> &Self::Target {
4748 self.as_str()
4749 }
4750}
4751
4752impl fmt::Display for BranchRefNameBuf {
4753 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4754 f.write_str(self.as_str())
4755 }
4756}
4757
4758impl From<BranchRefNameBuf> for FullRefNameBuf {
4759 fn from(name: BranchRefNameBuf) -> Self {
4760 Self { name: name.name }
4761 }
4762}
4763
4764#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
4765pub struct TagRefName<'a> {
4766 name: &'a str,
4767}
4768
4769impl<'a> TagRefName<'a> {
4770 pub const PREFIX: &'static str = "refs/tags/";
4771
4772 pub fn from_full(name: &'a str) -> Result<Self> {
4773 let full = FullRefName::new(name)?;
4774 Self::from_full_ref(full)
4775 }
4776
4777 pub fn from_full_ref(name: FullRefName<'a>) -> Result<Self> {
4778 validate_namespaced_ref(name.as_str(), Self::PREFIX, "tag")?;
4779 Ok(Self {
4780 name: name.into_str(),
4781 })
4782 }
4783
4784 pub fn as_full_ref_name(&self) -> FullRefName<'a> {
4785 FullRefName { name: self.name }
4786 }
4787
4788 pub fn as_str(&self) -> &str {
4789 self.name
4790 }
4791
4792 pub fn tag_name(&self) -> &str {
4793 self.short_name()
4794 }
4795
4796 pub fn short_name(&self) -> &str {
4797 &self.name[Self::PREFIX.len()..]
4798 }
4799
4800 pub fn into_str(self) -> &'a str {
4801 self.name
4802 }
4803
4804 pub fn to_owned(&self) -> TagRefNameBuf {
4805 TagRefNameBuf {
4806 name: self.name.to_string(),
4807 }
4808 }
4809}
4810
4811impl AsRef<str> for TagRefName<'_> {
4812 fn as_ref(&self) -> &str {
4813 self.as_str()
4814 }
4815}
4816
4817impl fmt::Display for TagRefName<'_> {
4818 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4819 f.write_str(self.as_str())
4820 }
4821}
4822
4823impl<'a> From<TagRefName<'a>> for FullRefName<'a> {
4824 fn from(name: TagRefName<'a>) -> Self {
4825 name.as_full_ref_name()
4826 }
4827}
4828
4829#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4830pub struct TagRefNameBuf {
4831 name: String,
4832}
4833
4834impl TagRefNameBuf {
4835 pub fn from_tag_name(tag: &str) -> Result<Self> {
4836 if tag.starts_with('-') || tag == "HEAD" {
4839 return Err(GitError::InvalidPath(format!("invalid tag name {tag}")));
4840 }
4841 Self::from_tag_name_unrestricted(tag)
4842 }
4843
4844 pub fn from_tag_name_unrestricted(tag: &str) -> Result<Self> {
4849 let name = format!("{}{}", TagRefName::PREFIX, tag);
4850 check_refname_format(&name, false)?;
4851 Ok(Self { name })
4852 }
4853
4854 pub fn from_full(name: impl Into<String>) -> Result<Self> {
4855 let name = name.into();
4856 TagRefName::from_full(&name)?;
4857 Ok(Self { name })
4858 }
4859
4860 pub fn as_ref_name(&self) -> TagRefName<'_> {
4861 TagRefName { name: &self.name }
4862 }
4863
4864 pub fn as_full_ref_name(&self) -> FullRefName<'_> {
4865 FullRefName { name: &self.name }
4866 }
4867
4868 pub fn as_str(&self) -> &str {
4869 &self.name
4870 }
4871
4872 pub fn tag_name(&self) -> &str {
4873 self.short_name()
4874 }
4875
4876 pub fn short_name(&self) -> &str {
4877 &self.name[TagRefName::PREFIX.len()..]
4878 }
4879
4880 pub fn into_string(self) -> String {
4881 self.name
4882 }
4883}
4884
4885impl AsRef<str> for TagRefNameBuf {
4886 fn as_ref(&self) -> &str {
4887 self.as_str()
4888 }
4889}
4890
4891impl Borrow<str> for TagRefNameBuf {
4892 fn borrow(&self) -> &str {
4893 self.as_str()
4894 }
4895}
4896
4897impl Deref for TagRefNameBuf {
4898 type Target = str;
4899
4900 fn deref(&self) -> &Self::Target {
4901 self.as_str()
4902 }
4903}
4904
4905impl fmt::Display for TagRefNameBuf {
4906 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4907 f.write_str(self.as_str())
4908 }
4909}
4910
4911impl From<TagRefNameBuf> for FullRefNameBuf {
4912 fn from(name: TagRefNameBuf) -> Self {
4913 Self { name: name.name }
4914 }
4915}
4916
4917#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
4918pub struct RemoteRefName<'a> {
4919 name: &'a str,
4920}
4921
4922impl<'a> RemoteRefName<'a> {
4923 pub const PREFIX: &'static str = "refs/remotes/";
4924
4925 pub fn from_full(name: &'a str) -> Result<Self> {
4926 let full = FullRefName::new(name)?;
4927 Self::from_full_ref(full)
4928 }
4929
4930 pub fn from_full_ref(name: FullRefName<'a>) -> Result<Self> {
4931 validate_namespaced_ref(name.as_str(), Self::PREFIX, "remote")?;
4932 Ok(Self {
4933 name: name.into_str(),
4934 })
4935 }
4936
4937 pub fn as_full_ref_name(&self) -> FullRefName<'a> {
4938 FullRefName { name: self.name }
4939 }
4940
4941 pub fn as_str(&self) -> &str {
4942 self.name
4943 }
4944
4945 pub fn short_name(&self) -> &str {
4946 &self.name[Self::PREFIX.len()..]
4947 }
4948
4949 pub fn remote_name(&self) -> &str {
4950 match self.short_name().split_once('/') {
4951 Some((remote, _branch)) => remote,
4952 None => self.short_name(),
4953 }
4954 }
4955
4956 pub fn remote_branch(&self) -> Option<&str> {
4957 self.short_name()
4958 .split_once('/')
4959 .map(|(_remote, branch)| branch)
4960 }
4961
4962 pub fn into_str(self) -> &'a str {
4963 self.name
4964 }
4965
4966 pub fn to_owned(&self) -> RemoteRefNameBuf {
4967 RemoteRefNameBuf {
4968 name: self.name.to_string(),
4969 }
4970 }
4971}
4972
4973impl AsRef<str> for RemoteRefName<'_> {
4974 fn as_ref(&self) -> &str {
4975 self.as_str()
4976 }
4977}
4978
4979impl fmt::Display for RemoteRefName<'_> {
4980 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4981 f.write_str(self.as_str())
4982 }
4983}
4984
4985impl<'a> From<RemoteRefName<'a>> for FullRefName<'a> {
4986 fn from(name: RemoteRefName<'a>) -> Self {
4987 name.as_full_ref_name()
4988 }
4989}
4990
4991#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4992pub struct RemoteRefNameBuf {
4993 name: String,
4994}
4995
4996impl RemoteRefNameBuf {
4997 pub fn from_short_name(name: &str) -> Result<Self> {
4998 validate_short_ref_name("remote ref", name)?;
4999 let name = format!("{}{}", RemoteRefName::PREFIX, name);
5000 Self::from_full(name)
5001 }
5002
5003 pub fn from_remote_branch(remote: &str, branch: &str) -> Result<Self> {
5004 validate_remote_name(remote)?;
5005 validate_short_ref_name("remote branch", branch)?;
5006 let name = format!("{}{}/{}", RemoteRefName::PREFIX, remote, branch);
5007 Self::from_full(name)
5008 }
5009
5010 pub fn from_full(name: impl Into<String>) -> Result<Self> {
5011 let name = name.into();
5012 RemoteRefName::from_full(&name)?;
5013 Ok(Self { name })
5014 }
5015
5016 pub fn as_ref_name(&self) -> RemoteRefName<'_> {
5017 RemoteRefName { name: &self.name }
5018 }
5019
5020 pub fn as_full_ref_name(&self) -> FullRefName<'_> {
5021 FullRefName { name: &self.name }
5022 }
5023
5024 pub fn as_str(&self) -> &str {
5025 &self.name
5026 }
5027
5028 pub fn short_name(&self) -> &str {
5029 &self.name[RemoteRefName::PREFIX.len()..]
5030 }
5031
5032 pub fn remote_name(&self) -> &str {
5033 match self.short_name().split_once('/') {
5034 Some((remote, _branch)) => remote,
5035 None => self.short_name(),
5036 }
5037 }
5038
5039 pub fn remote_branch(&self) -> Option<&str> {
5040 self.short_name()
5041 .split_once('/')
5042 .map(|(_remote, branch)| branch)
5043 }
5044
5045 pub fn into_string(self) -> String {
5046 self.name
5047 }
5048}
5049
5050impl AsRef<str> for RemoteRefNameBuf {
5051 fn as_ref(&self) -> &str {
5052 self.as_str()
5053 }
5054}
5055
5056impl Borrow<str> for RemoteRefNameBuf {
5057 fn borrow(&self) -> &str {
5058 self.as_str()
5059 }
5060}
5061
5062impl Deref for RemoteRefNameBuf {
5063 type Target = str;
5064
5065 fn deref(&self) -> &Self::Target {
5066 self.as_str()
5067 }
5068}
5069
5070impl fmt::Display for RemoteRefNameBuf {
5071 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5072 f.write_str(self.as_str())
5073 }
5074}
5075
5076impl From<RemoteRefNameBuf> for FullRefNameBuf {
5077 fn from(name: RemoteRefNameBuf) -> Self {
5078 Self { name: name.name }
5079 }
5080}
5081
5082pub fn branch_ref_name(branch: &str) -> Result<String> {
5083 BranchRefNameBuf::from_branch_name(branch).map(BranchRefNameBuf::into_string)
5084}
5085
5086pub fn branch_ref_name_for_read(branch: &str) -> Result<String> {
5087 let name = format!("{}{}", BranchRefName::PREFIX, branch);
5088 if validate_ref_name(&name).is_err() {
5089 if name.contains("..") {
5090 validate_ref_path_safe_for_read(&name)?;
5091 } else {
5092 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
5093 }
5094 }
5095 Ok(name)
5096}
5097
5098pub fn branch_ref_name_for_source(branch: &str) -> Result<String> {
5099 if branch.starts_with("--") {
5100 return Err(GitError::InvalidPath(format!(
5101 "invalid branch name {branch}"
5102 )));
5103 }
5104 let name = format!("{}{}", BranchRefName::PREFIX, branch);
5105 check_refname_format(&name, false)?;
5106 Ok(name)
5107}
5108
5109pub fn tag_ref_name(tag: &str) -> Result<String> {
5110 TagRefNameBuf::from_tag_name(tag).map(TagRefNameBuf::into_string)
5111}
5112
5113fn write_locked(path: &Path, bytes: &[u8]) -> Result<()> {
5114 write_locked_with_timeout(path, bytes, 0)
5115}
5116
5117fn write_locked_with_timeout(path: &Path, bytes: &[u8], timeout_millis: u64) -> Result<()> {
5118 let lock_path = lock_path_for(path)?;
5119 let start = SystemTime::now();
5120 loop {
5121 match fs::OpenOptions::new()
5122 .write(true)
5123 .create_new(true)
5124 .open(&lock_path)
5125 {
5126 Ok(mut file) => {
5127 file.write_all(bytes)?;
5128 file.sync_all()?;
5129 break;
5130 }
5131 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists && timeout_millis > 0 => {
5132 let elapsed = start
5133 .elapsed()
5134 .unwrap_or_else(|_| Duration::from_millis(timeout_millis + 1));
5135 if elapsed.as_millis() as u64 >= timeout_millis {
5136 return Err(GitError::Io(format!(
5137 "could not lock {}: File exists",
5138 path.display()
5139 )));
5140 }
5141 thread::sleep(Duration::from_millis(50));
5142 }
5143 Err(err) => return Err(err.into()),
5144 }
5145 }
5146 match fs::rename(&lock_path, path) {
5147 Ok(()) => Ok(()),
5148 Err(err) => {
5149 let _ = fs::remove_file(lock_path);
5150 Err(GitError::Io(err.to_string()))
5151 }
5152 }
5153}
5154
5155fn lock_path_for(path: &Path) -> Result<PathBuf> {
5156 let file_name = path
5157 .file_name()
5158 .ok_or_else(|| GitError::InvalidPath("ref path has no filename".into()))?;
5159 let mut lock_name = file_name.to_os_string();
5160 lock_name.push(".lock");
5161 Ok(path.with_file_name(lock_name))
5162}
5163
5164pub fn check_refname_format(name: &str, allow_onelevel: bool) -> Result<()> {
5166 if name.is_empty()
5167 || name == "@"
5168 || name.starts_with('/')
5169 || name.ends_with('/')
5170 || name.ends_with('.')
5171 || name.contains("..")
5172 || name.contains("//")
5173 || name.contains("@{")
5174 || (!allow_onelevel && !name.contains('/'))
5175 {
5176 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
5177 }
5178 for component in name.split('/') {
5179 if component.is_empty() || component.starts_with('.') || component.ends_with(".lock") {
5180 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
5181 }
5182 for (idx, byte) in component.bytes().enumerate() {
5183 if byte <= b' '
5184 || byte == 0x7f
5185 || matches!(byte, b'~' | b'^' | b':' | b'?' | b'*' | b'[' | b'\\')
5186 {
5187 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
5188 }
5189 if byte == b'.' && component.as_bytes().get(idx + 1) == Some(&b'.') {
5190 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
5191 }
5192 if byte == b'@' && component.as_bytes().get(idx + 1) == Some(&b'{') {
5193 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
5194 }
5195 }
5196 }
5197 Ok(())
5198}
5199
5200pub fn validate_symref_name(name: &str) -> Result<()> {
5202 if name == "HEAD" {
5203 return Ok(());
5204 }
5205 check_refname_format(name, true)
5206}
5207
5208pub fn validate_symref_target(name: &str) -> Result<()> {
5210 check_refname_format(name, true)
5211}
5212
5213fn prune_empty_dirs_up_to(start: &Path, boundary: &Path) {
5218 let mut dir = start.to_path_buf();
5219 while dir.starts_with(boundary) && dir != *boundary {
5220 if fs::remove_dir(&dir).is_err() {
5221 break;
5222 }
5223 dir = match dir.parent() {
5224 Some(parent) => parent.to_path_buf(),
5225 None => break,
5226 };
5227 }
5228}
5229
5230fn packable_loose_ref_name(name: &str) -> bool {
5231 name.starts_with("refs/")
5232 && !name.starts_with("refs/bisect/")
5233 && !name.starts_with("refs/worktree/")
5234 && !name.starts_with("refs/rewritten/")
5235}
5236
5237fn pack_refs_auto_required_for(packed_path: &Path, loose_count: usize) -> Result<bool> {
5238 let packed_size = match fs::metadata(packed_path) {
5239 Ok(meta) => meta.len() as usize,
5240 Err(err) if err.kind() == std::io::ErrorKind::NotFound => 0,
5241 Err(err) => return Err(err.into()),
5242 };
5243 let estimated_packed_refs = packed_size / 100;
5244 let log2 = if estimated_packed_refs == 0 {
5245 0
5246 } else {
5247 usize::BITS as usize - estimated_packed_refs.leading_zeros() as usize - 1
5248 };
5249 let limit = (log2 * 5).max(16);
5250 Ok(loose_count >= limit)
5251}
5252
5253pub fn resolve_ref_peeled(store: &FileRefStore, name: &str) -> Result<Option<ObjectId>> {
5254 let mut current = name.to_string();
5255 for _ in 0..16 {
5256 match store.read_ref(¤t)? {
5257 Some(RefTarget::Direct(oid)) => return Ok(Some(oid)),
5258 Some(RefTarget::Symbolic(next)) => {
5259 if validate_ref_name_for_read(&next).is_err() {
5260 return Ok(None);
5261 }
5262 current = next;
5263 }
5264 None => return Ok(None),
5265 }
5266 }
5267 Ok(None)
5268}
5269
5270pub fn validate_ref_name_for_read(name: &str) -> Result<()> {
5271 if validate_ref_name(name).is_ok() {
5272 return Ok(());
5273 }
5274 if is_root_ref_syntax(name) {
5275 return Ok(());
5276 }
5277 if check_refname_format(name, true).is_ok() {
5278 return Ok(());
5279 }
5280 validate_ref_path_safe_for_read(name)
5281}
5282
5283pub fn validate_ref_name_for_update(name: &str) -> Result<()> {
5284 if validate_ref_name(name).is_ok() {
5285 return Ok(());
5286 }
5287 if is_root_ref_syntax(name) {
5288 return Ok(());
5289 }
5290 check_refname_format(name, true)
5291}
5292
5293pub fn refname_is_safe(refname: &str) -> bool {
5307 if let Some(rest) = refname.strip_prefix("refs/") {
5308 if rest.is_empty() || rest.starts_with('/') || rest.ends_with('/') || rest.contains('\\') {
5309 return false;
5310 }
5311 let path = Path::new(rest);
5312 !path.is_absolute()
5313 && !path.components().any(|component| {
5314 matches!(
5315 component,
5316 std::path::Component::ParentDir
5317 | std::path::Component::Prefix(_)
5318 | std::path::Component::RootDir
5319 )
5320 })
5321 } else {
5322 !refname.is_empty() && refname.bytes().all(|b| b.is_ascii_uppercase() || b == b'_')
5323 }
5324}
5325
5326fn is_root_ref_syntax(name: &str) -> bool {
5331 !name.is_empty()
5332 && name
5333 .bytes()
5334 .all(|b| b.is_ascii_uppercase() || b == b'-' || b == b'_')
5335}
5336
5337fn reftable_current_worktree_ref(name: &str) -> bool {
5338 is_root_ref_syntax(name)
5339 || name.starts_with("refs/bisect/")
5340 || name.starts_with("refs/worktree/")
5341 || name.starts_with("refs/rewritten/")
5342}
5343
5344fn reftable_other_worktree_ref(name: &str) -> Option<(&str, &str)> {
5345 let rest = name.strip_prefix("worktrees/")?;
5346 let (worktree, rewritten) = rest.split_once('/')?;
5347 if worktree.is_empty() || rewritten.is_empty() {
5348 return None;
5349 }
5350 Some((worktree, rewritten))
5351}
5352
5353pub fn validate_ref_name(name: &str) -> Result<()> {
5354 if name == "HEAD" {
5355 return Ok(());
5356 }
5357 if !name.starts_with("refs/") || check_refname_format(name, false).is_err() {
5358 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
5359 }
5360 Ok(())
5361}
5362
5363fn validate_ref_path_safe_for_read(name: &str) -> Result<()> {
5364 let path = Path::new(name);
5365 if !name.starts_with("refs/")
5366 || name.starts_with('/')
5367 || name.contains('\\')
5368 || path.is_absolute()
5369 || path.components().any(|component| {
5370 matches!(
5371 component,
5372 std::path::Component::ParentDir | std::path::Component::Prefix(_)
5373 )
5374 })
5375 {
5376 return Err(GitError::InvalidPath(format!("invalid ref name {name}")));
5377 }
5378 Ok(())
5379}
5380
5381fn safe_ref_prefix_for_directory_scan(prefix: &str) -> bool {
5382 let path = Path::new(prefix);
5383 prefix.starts_with("refs/")
5384 && !prefix.starts_with('/')
5385 && !prefix.contains('\\')
5386 && !path.is_absolute()
5387 && !path.components().any(|component| {
5388 matches!(
5389 component,
5390 std::path::Component::ParentDir | std::path::Component::Prefix(_)
5391 )
5392 })
5393}
5394
5395fn warn_broken_ref_name(name: &str) {
5396 eprintln!("warning: ignoring ref with broken name {name}");
5397}
5398
5399fn warn_broken_ref(name: &str) {
5400 eprintln!("warning: ignoring broken ref {name}");
5401}
5402
5403fn ref_target_is_broken(target: &RefTarget) -> bool {
5405 matches!(target, RefTarget::Direct(oid) if oid.is_null())
5406}
5407
5408fn ref_directory_conflict_error(new_ref: &str, existing_ref: &str) -> GitError {
5409 GitError::Transaction(format!(
5410 "cannot lock ref '{new_ref}': '{existing_ref}' exists; cannot create '{new_ref}'"
5411 ))
5412}
5413
5414fn check_ref_directory_conflict_in_names(name: &str, names: &BTreeSet<String>) -> Result<()> {
5415 let components = name.split('/').collect::<Vec<_>>();
5416 for index in 1..components.len() {
5417 let ancestor = components[..index].join("/");
5418 if names.contains(&ancestor) {
5419 return Err(ref_directory_conflict_error(name, &ancestor));
5420 }
5421 }
5422 let child_prefix = format!("{name}/");
5423 if let Some(existing) = names.range(child_prefix.clone()..).next()
5424 && existing.starts_with(&child_prefix)
5425 {
5426 return Err(ref_directory_conflict_error(name, existing));
5427 }
5428 Ok(())
5429}
5430
5431fn parent_to_ref_name(base: &Path, parent: &Path) -> String {
5432 match parent.strip_prefix(base) {
5433 Ok(suffix) => suffix.to_string_lossy().replace('\\', "/"),
5434 Err(_) => parent.to_string_lossy().into_owned(),
5435 }
5436}
5437
5438fn validate_namespaced_ref(name: &str, prefix: &str, kind: &str) -> Result<()> {
5439 validate_ref_name(name)?;
5440 if name
5441 .strip_prefix(prefix)
5442 .is_none_or(|short_name| short_name.is_empty())
5443 {
5444 return Err(GitError::InvalidPath(format!(
5445 "invalid {kind} ref name {name}"
5446 )));
5447 }
5448 Ok(())
5449}
5450
5451fn validate_short_ref_name(kind: &str, name: &str) -> Result<()> {
5452 if name.is_empty()
5453 || name.starts_with('-')
5454 || name.starts_with('/')
5455 || name.ends_with('/')
5456 || name.contains(' ')
5457 || name.contains('\\')
5458 {
5459 return Err(GitError::InvalidPath(format!("invalid {kind} name {name}")));
5460 }
5461 Ok(())
5462}
5463
5464fn validate_remote_name(remote: &str) -> Result<()> {
5465 validate_short_ref_name("remote", remote)?;
5466 if remote.contains('/') {
5467 return Err(GitError::InvalidPath(format!(
5468 "invalid remote name {remote}"
5469 )));
5470 }
5471 Ok(())
5472}
5473
5474fn prepare_bundle_ref_updates<F>(
5475 refs: &[BundleRefUpdate],
5476 reflog: Option<&BundleRefUpdateReflog>,
5477 mut read_ref: F,
5478) -> Result<(Vec<RefUpdate>, Vec<AppliedBundleRefUpdate>)>
5479where
5480 F: FnMut(&str, &ObjectId) -> Result<Option<RefTarget>>,
5481{
5482 let mut seen = BTreeSet::new();
5483 let mut updates = Vec::with_capacity(refs.len());
5484 let mut applied = Vec::with_capacity(refs.len());
5485 for bundle_ref in refs {
5486 validate_ref_name(&bundle_ref.name)?;
5487 if !seen.insert(bundle_ref.name.clone()) {
5488 return Err(GitError::Transaction(format!(
5489 "duplicate bundle ref {}",
5490 bundle_ref.name
5491 )));
5492 }
5493 let old_oid = match read_ref(&bundle_ref.name, &bundle_ref.oid)? {
5494 Some(RefTarget::Direct(oid)) => Some(oid),
5495 Some(RefTarget::Symbolic(target)) => {
5496 return Err(GitError::Transaction(format!(
5497 "bundle ref {} would overwrite symbolic ref {target}",
5498 bundle_ref.name
5499 )));
5500 }
5501 None => None,
5502 };
5503 let reflog = match reflog {
5504 Some(reflog) => Some(ReflogEntry {
5505 old_oid: match &old_oid {
5506 Some(oid) => *oid,
5507 None => null_oid(bundle_ref.oid.format())?,
5508 },
5509 new_oid: bundle_ref.oid,
5510 committer: reflog.committer.clone(),
5511 message: reflog.message.clone(),
5512 }),
5513 None => None,
5514 };
5515 updates.push(RefUpdate {
5516 name: bundle_ref.name.clone(),
5517 expected: old_oid.map(RefTarget::Direct),
5518 new: RefTarget::Direct(bundle_ref.oid),
5519 reflog,
5520 });
5521 applied.push(AppliedBundleRefUpdate {
5522 name: bundle_ref.name.clone(),
5523 old_oid,
5524 new_oid: bundle_ref.oid,
5525 });
5526 }
5527 Ok((updates, applied))
5528}
5529
5530fn null_oid(format: ObjectFormat) -> Result<ObjectId> {
5531 Ok(ObjectId::null(format))
5532}
5533
5534#[cfg(test)]
5535mod tests {
5536 use super::*;
5537 use std::sync::atomic::{AtomicU64, Ordering};
5538
5539 static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
5540
5541 #[test]
5542 fn loose_ref_round_trips_direct() {
5543 let oid = "ce013625030ba8dba906f756967f9e9ca394464a";
5544 let reference = parse_loose_ref(ObjectFormat::Sha1, "refs/heads/main", oid.as_bytes())
5545 .expect("test operation should succeed");
5546 assert_eq!(write_loose_ref(&reference), format!("{oid}\n").into_bytes());
5547 }
5548
5549 #[test]
5550 fn loose_fetch_head_reads_first_object_id() {
5551 let oid = ObjectId::from_hex(
5552 ObjectFormat::Sha1,
5553 "ce013625030ba8dba906f756967f9e9ca394464a",
5554 )
5555 .expect("test operation should succeed");
5556 let bytes = b"ce013625030ba8dba906f756967f9e9ca394464a\t\tbranch 'main' of ../sub\n";
5557 let reference = parse_loose_ref(ObjectFormat::Sha1, "FETCH_HEAD", bytes)
5558 .expect("test operation should succeed");
5559 assert_eq!(reference.target, RefTarget::Direct(oid));
5560 }
5561
5562 #[test]
5563 fn symref_names_allow_onelevel_pseudo_refs() {
5564 for name in ["NOTHEAD", "FOO", "ORIG_HEAD", "TEST_SYMREF"] {
5565 validate_symref_name(name).expect("symref name should be valid");
5566 }
5567 assert!(validate_ref_name("NOTHEAD").is_err());
5568 assert!(validate_symref_target("refs/heads/foo").is_ok());
5569 assert!(validate_symref_target("ORIG_HEAD").is_ok());
5570 assert!(validate_symref_target("foo..bar").is_err());
5571 }
5572
5573 #[test]
5574 fn resolve_ref_peeled_follows_symref_chains() {
5575 let git_dir = temp_git_dir();
5576 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5577 let oid = ObjectId::from_hex(
5578 ObjectFormat::Sha1,
5579 "ce013625030ba8dba906f756967f9e9ca394464a",
5580 )
5581 .expect("test operation should succeed");
5582 let mut tx = store.transaction();
5583 tx.update(RefUpdate {
5584 name: "refs/heads/target".into(),
5585 expected: None,
5586 new: RefTarget::Direct(oid),
5587 reflog: None,
5588 });
5589 tx.commit().expect("seed target ref");
5590 let mut tx = store.transaction();
5591 tx.update(RefUpdate {
5592 name: "refs/heads/alias".into(),
5593 expected: None,
5594 new: RefTarget::Symbolic("refs/heads/target".into()),
5595 reflog: None,
5596 });
5597 tx.commit().expect("seed alias ref");
5598 let mut tx = store.transaction();
5599 tx.update(RefUpdate {
5600 name: "ORIG_HEAD".into(),
5601 expected: None,
5602 new: RefTarget::Symbolic("refs/heads/alias".into()),
5603 reflog: None,
5604 });
5605 tx.commit().expect("seed ORIG_HEAD symref");
5606 assert_eq!(
5607 resolve_ref_peeled(&store, "ORIG_HEAD").expect("resolve ORIG_HEAD"),
5608 Some(oid)
5609 );
5610 let _ = fs::remove_dir_all(git_dir);
5611 }
5612
5613 #[test]
5614 fn symref_directory_conflict_is_reported_gracefully() {
5615 let git_dir = temp_git_dir();
5616 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5617 let oid = ObjectId::from_hex(
5618 ObjectFormat::Sha1,
5619 "ce013625030ba8dba906f756967f9e9ca394464a",
5620 )
5621 .expect("test operation should succeed");
5622 let mut tx = store.transaction();
5623 tx.update(RefUpdate {
5624 name: "refs/heads/df".into(),
5625 expected: None,
5626 new: RefTarget::Direct(oid),
5627 reflog: None,
5628 });
5629 tx.commit().expect("seed branch ref");
5630
5631 let mut tx = store.transaction();
5632 tx.update(RefUpdate {
5633 name: "refs/heads/df/conflict".into(),
5634 expected: None,
5635 new: RefTarget::Symbolic("refs/heads/df".into()),
5636 reflog: None,
5637 });
5638 let err = tx.commit().expect_err("child ref should conflict");
5639 assert!(
5640 matches!(err, GitError::Transaction(message) if message.contains(
5641 "cannot lock ref 'refs/heads/df/conflict'"
5642 ) && message.contains("refs/heads/df"))
5643 );
5644 let _ = fs::remove_dir_all(git_dir);
5645 }
5646
5647 #[test]
5648 fn transaction_checks_expected_value() {
5649 let oid = ObjectId::from_hex(
5650 ObjectFormat::Sha1,
5651 "ce013625030ba8dba906f756967f9e9ca394464a",
5652 )
5653 .expect("test operation should succeed");
5654 let mut store = RefStore::new();
5655 let mut tx = store.transaction();
5656 tx.update(RefUpdate {
5657 name: "refs/heads/main".into(),
5658 expected: None,
5659 new: RefTarget::Direct(oid),
5660 reflog: None,
5661 });
5662 tx.commit().expect("test operation should succeed");
5663 assert_eq!(store.get("refs/heads/main"), Some(&RefTarget::Direct(oid)));
5664 }
5665
5666 #[test]
5667 fn packed_refs_parse_peeled_refs() {
5668 let packed = b"# pack-refs with: peeled fully-peeled sorted \n\
5669ce013625030ba8dba906f756967f9e9ca394464a refs/tags/v1\n\
5670^e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\n";
5671 let refs =
5672 parse_packed_refs(ObjectFormat::Sha1, packed).expect("test operation should succeed");
5673 assert_eq!(refs.len(), 1);
5674 assert_eq!(refs[0].reference.name, "refs/tags/v1");
5675 assert_eq!(
5676 refs[0]
5677 .peeled
5678 .as_ref()
5679 .expect("test operation should succeed")
5680 .to_hex(),
5681 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"
5682 );
5683 }
5684
5685 #[test]
5686 fn packed_refs_write_sorted_with_peeled_refs() {
5687 let head_oid = ObjectId::from_hex(
5688 ObjectFormat::Sha1,
5689 "ce013625030ba8dba906f756967f9e9ca394464a",
5690 )
5691 .expect("test operation should succeed");
5692 let tag_oid = ObjectId::from_hex(
5693 ObjectFormat::Sha1,
5694 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
5695 )
5696 .expect("test operation should succeed");
5697 let peeled_oid = ObjectId::from_hex(
5698 ObjectFormat::Sha1,
5699 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
5700 )
5701 .expect("test operation should succeed");
5702 let refs = vec![
5703 PackedRef {
5704 reference: Ref {
5705 name: "refs/tags/v1".into(),
5706 target: RefTarget::Direct(tag_oid),
5707 },
5708 peeled: Some(peeled_oid),
5709 },
5710 PackedRef {
5711 reference: Ref {
5712 name: "refs/heads/main".into(),
5713 target: RefTarget::Direct(head_oid),
5714 },
5715 peeled: None,
5716 },
5717 ];
5718 let bytes = write_packed_refs(&refs).expect("test operation should succeed");
5719 let expected = format!(
5720 "# pack-refs with: peeled fully-peeled sorted \n\
5721{head_oid} refs/heads/main\n\
5722{tag_oid} refs/tags/v1\n\
5723^{peeled_oid}\n"
5724 );
5725 assert_eq!(
5726 String::from_utf8(bytes.clone()).expect("test operation should succeed"),
5727 expected
5728 );
5729 let parsed =
5730 parse_packed_refs(ObjectFormat::Sha1, &bytes).expect("test operation should succeed");
5731 assert_eq!(parsed[0], refs[1]);
5732 assert_eq!(parsed[1], refs[0]);
5733 }
5734
5735 #[test]
5736 fn full_ref_name_validates_and_round_trips_owned() {
5737 let full = FullRefName::new("refs/heads/main").expect("valid full branch ref");
5738 assert_eq!(full.as_str(), "refs/heads/main");
5739 assert_eq!(full.to_string(), "refs/heads/main");
5740 assert_eq!(full.to_owned().into_string(), "refs/heads/main");
5741
5742 let head = FullRefNameBuf::new("HEAD").expect("valid HEAD ref");
5743 assert_eq!(head.as_ref_name().into_str(), "HEAD");
5744
5745 assert!(FullRefName::new("main").is_err());
5746 assert!(FullRefNameBuf::new("refs/heads/bad.lock").is_err());
5747 }
5748
5749 #[test]
5750 fn branch_ref_name_helpers_validate_short_and_full_names() {
5751 let branch =
5752 BranchRefNameBuf::from_branch_name("feature/topic").expect("valid branch short name");
5753 assert_eq!(branch.as_str(), "refs/heads/feature/topic");
5754 assert_eq!(branch.branch_name(), "feature/topic");
5755 assert_eq!(
5756 branch.as_full_ref_name().as_str(),
5757 "refs/heads/feature/topic"
5758 );
5759 assert_eq!(
5760 branch_ref_name("feature/topic").expect("valid branch short name"),
5761 branch.as_str()
5762 );
5763
5764 let borrowed = BranchRefName::from_full("refs/heads/main").expect("valid full branch ref");
5765 assert_eq!(borrowed.branch_name(), "main");
5766 assert_eq!(borrowed.to_owned().into_string(), "refs/heads/main");
5767 assert_eq!(
5768 FullRefName::new("refs/heads/main")
5769 .expect("valid full branch ref")
5770 .as_branch()
5771 .expect("full ref is a branch")
5772 .branch_name(),
5773 "main"
5774 );
5775
5776 assert!(BranchRefName::from_full("refs/tags/main").is_err());
5777 assert!(BranchRefName::from_full("refs/heads").is_err());
5778 assert!(BranchRefNameBuf::from_branch_name("-bad").is_err());
5779 }
5780
5781 #[test]
5782 fn tag_ref_name_helpers_validate_short_and_full_names() {
5783 let tag = TagRefNameBuf::from_tag_name("v1.0").expect("valid tag short name");
5784 assert_eq!(tag.as_str(), "refs/tags/v1.0");
5785 assert_eq!(tag.tag_name(), "v1.0");
5786 assert_eq!(tag.as_full_ref_name().as_str(), "refs/tags/v1.0");
5787 assert_eq!(
5788 tag_ref_name("v1.0").expect("valid tag short name"),
5789 tag.as_str()
5790 );
5791
5792 let borrowed = TagRefName::from_full("refs/tags/release/1").expect("valid full tag ref");
5793 assert_eq!(borrowed.tag_name(), "release/1");
5794 assert_eq!(borrowed.to_owned().into_string(), "refs/tags/release/1");
5795 assert_eq!(
5796 FullRefName::new("refs/tags/release/1")
5797 .expect("valid full tag ref")
5798 .as_tag()
5799 .expect("full ref is a tag")
5800 .tag_name(),
5801 "release/1"
5802 );
5803
5804 assert!(TagRefName::from_full("refs/heads/v1.0").is_err());
5805 assert!(TagRefName::from_full("refs/tags").is_err());
5806 assert!(TagRefNameBuf::from_tag_name("bad tag").is_err());
5807 }
5808
5809 #[test]
5810 fn remote_ref_name_helpers_validate_namespace_and_components() {
5811 let remote = RemoteRefNameBuf::from_remote_branch("origin", "feature/topic")
5812 .expect("valid remote branch ref");
5813 assert_eq!(remote.as_str(), "refs/remotes/origin/feature/topic");
5814 assert_eq!(remote.short_name(), "origin/feature/topic");
5815 assert_eq!(remote.remote_name(), "origin");
5816 assert_eq!(remote.remote_branch(), Some("feature/topic"));
5817 assert_eq!(
5818 remote.as_full_ref_name().as_str(),
5819 "refs/remotes/origin/feature/topic"
5820 );
5821
5822 let head =
5823 RemoteRefName::from_full("refs/remotes/origin/HEAD").expect("valid remote HEAD ref");
5824 assert_eq!(head.remote_name(), "origin");
5825 assert_eq!(head.remote_branch(), Some("HEAD"));
5826 assert_eq!(
5827 FullRefName::new("refs/remotes/upstream/main")
5828 .expect("valid full remote ref")
5829 .as_remote()
5830 .expect("full ref is remote-tracking")
5831 .remote_name(),
5832 "upstream"
5833 );
5834
5835 let short =
5836 RemoteRefNameBuf::from_short_name("origin/main").expect("valid remote short ref");
5837 assert_eq!(short.as_str(), "refs/remotes/origin/main");
5838
5839 assert!(RemoteRefName::from_full("refs/heads/origin/main").is_err());
5840 assert!(RemoteRefName::from_full("refs/remotes/").is_err());
5841 assert!(RemoteRefNameBuf::from_remote_branch("origin/fork", "main").is_err());
5842 }
5843
5844 #[test]
5845 fn file_ref_store_writes_ref_and_reflog() {
5846 let git_dir = temp_git_dir();
5847 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5848 let oid = ObjectId::from_hex(
5849 ObjectFormat::Sha1,
5850 "ce013625030ba8dba906f756967f9e9ca394464a",
5851 )
5852 .expect("test operation should succeed");
5853 let mut tx = store.transaction();
5854 tx.update(RefUpdate {
5855 name: "refs/heads/main".into(),
5856 expected: None,
5857 new: RefTarget::Direct(oid),
5858 reflog: Some(ReflogEntry {
5859 old_oid: zero_oid(ObjectFormat::Sha1).expect("test operation should succeed"),
5860 new_oid: oid,
5861 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
5862 message: b"update by test".to_vec(),
5863 }),
5864 });
5865 tx.commit().expect("test operation should succeed");
5866 assert_eq!(
5867 store
5868 .read_ref("refs/heads/main")
5869 .expect("test operation should succeed"),
5870 Some(RefTarget::Direct(oid))
5871 );
5872 let log = store
5873 .read_reflog("refs/heads/main")
5874 .expect("test operation should succeed");
5875 assert_eq!(log.len(), 1);
5876 assert_eq!(log[0].message, b"update by test");
5877 fs::remove_dir_all(git_dir).expect("test operation should succeed");
5878 }
5879
5880 #[test]
5881 fn file_ref_store_applies_bundle_refs_with_reflog() {
5882 let git_dir = temp_git_dir();
5883 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5884 let old_main = ObjectId::from_hex(
5885 ObjectFormat::Sha1,
5886 "ce013625030ba8dba906f756967f9e9ca394464a",
5887 )
5888 .expect("test operation should succeed");
5889 let new_main = ObjectId::from_hex(
5890 ObjectFormat::Sha1,
5891 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
5892 )
5893 .expect("test operation should succeed");
5894 let tag_oid = ObjectId::from_hex(
5895 ObjectFormat::Sha1,
5896 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
5897 )
5898 .expect("test operation should succeed");
5899 let mut tx = store.transaction();
5900 tx.update(RefUpdate {
5901 name: "refs/heads/main".into(),
5902 expected: None,
5903 new: RefTarget::Direct(old_main.clone()),
5904 reflog: None,
5905 });
5906 tx.commit().expect("test operation should succeed");
5907
5908 let applied = store
5909 .apply_bundle_ref_updates(
5910 &[
5911 BundleRefUpdate {
5912 name: "refs/heads/main".into(),
5913 oid: new_main.clone(),
5914 },
5915 BundleRefUpdate {
5916 name: "refs/tags/v1.0".into(),
5917 oid: tag_oid,
5918 },
5919 ],
5920 Some(BundleRefUpdateReflog {
5921 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
5922 message: b"bundle: import refs".to_vec(),
5923 }),
5924 )
5925 .expect("test operation should succeed");
5926
5927 assert_eq!(
5928 applied,
5929 vec![
5930 AppliedBundleRefUpdate {
5931 name: "refs/heads/main".into(),
5932 old_oid: Some(old_main.clone()),
5933 new_oid: new_main.clone(),
5934 },
5935 AppliedBundleRefUpdate {
5936 name: "refs/tags/v1.0".into(),
5937 old_oid: None,
5938 new_oid: tag_oid,
5939 }
5940 ]
5941 );
5942 assert_eq!(
5943 store
5944 .read_ref("refs/heads/main")
5945 .expect("test operation should succeed"),
5946 Some(RefTarget::Direct(new_main.clone()))
5947 );
5948 assert_eq!(
5949 store
5950 .read_ref("refs/tags/v1.0")
5951 .expect("test operation should succeed"),
5952 Some(RefTarget::Direct(tag_oid))
5953 );
5954 let main_log = store
5955 .read_reflog("refs/heads/main")
5956 .expect("test operation should succeed");
5957 assert_eq!(main_log.len(), 1);
5958 assert_eq!(main_log[0].old_oid, old_main);
5959 assert_eq!(main_log[0].new_oid, new_main);
5960 assert_eq!(main_log[0].message, b"bundle: import refs");
5961 let tag_log = store
5962 .read_reflog("refs/tags/v1.0")
5963 .expect("test operation should succeed");
5964 assert_eq!(tag_log.len(), 1);
5965 assert_eq!(
5966 tag_log[0].old_oid,
5967 zero_oid(ObjectFormat::Sha1).expect("test operation should succeed")
5968 );
5969 assert_eq!(tag_log[0].new_oid, tag_oid);
5970 fs::remove_dir_all(git_dir).expect("test operation should succeed");
5971 }
5972
5973 #[test]
5974 fn file_ref_store_rejects_bad_bundle_ref_before_writing() {
5975 let git_dir = temp_git_dir();
5976 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
5977 let oid = ObjectId::from_hex(
5978 ObjectFormat::Sha1,
5979 "ce013625030ba8dba906f756967f9e9ca394464a",
5980 )
5981 .expect("test operation should succeed");
5982
5983 let result = store.apply_bundle_ref_updates(
5984 &[
5985 BundleRefUpdate {
5986 name: "refs/heads/main".into(),
5987 oid,
5988 },
5989 BundleRefUpdate {
5990 name: "refs/heads/bad.lock".into(),
5991 oid,
5992 },
5993 ],
5994 None,
5995 );
5996
5997 assert!(result.is_err());
5998 assert_eq!(
5999 store
6000 .read_ref("refs/heads/main")
6001 .expect("test operation should succeed"),
6002 None
6003 );
6004 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6005 }
6006
6007 #[test]
6008 fn file_ref_store_rejects_bundle_ref_over_symbolic_ref() {
6009 let git_dir = temp_git_dir();
6010 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6011 let oid = ObjectId::from_hex(
6012 ObjectFormat::Sha1,
6013 "ce013625030ba8dba906f756967f9e9ca394464a",
6014 )
6015 .expect("test operation should succeed");
6016 let mut tx = store.transaction();
6017 tx.update(RefUpdate {
6018 name: "refs/heads/main".into(),
6019 expected: None,
6020 new: RefTarget::Symbolic("refs/heads/base".into()),
6021 reflog: None,
6022 });
6023 tx.commit().expect("test operation should succeed");
6024
6025 let result = store.apply_bundle_ref_updates(
6026 &[BundleRefUpdate {
6027 name: "refs/heads/main".into(),
6028 oid,
6029 }],
6030 None,
6031 );
6032
6033 assert!(result.is_err());
6034 assert_eq!(
6035 store
6036 .read_ref("refs/heads/main")
6037 .expect("test operation should succeed"),
6038 Some(RefTarget::Symbolic("refs/heads/base".into()))
6039 );
6040 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6041 }
6042
6043 #[test]
6044 fn file_ref_store_expires_reflog_entries_by_timestamp() {
6045 let git_dir = temp_git_dir();
6046 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6047 let first = ObjectId::from_hex(
6048 ObjectFormat::Sha1,
6049 "ce013625030ba8dba906f756967f9e9ca394464a",
6050 )
6051 .expect("test operation should succeed");
6052 let second = ObjectId::from_hex(
6053 ObjectFormat::Sha1,
6054 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
6055 )
6056 .expect("test operation should succeed");
6057 let mut tx = store.transaction();
6058 tx.update(RefUpdate {
6059 name: "refs/heads/main".into(),
6060 expected: None,
6061 new: RefTarget::Direct(first.clone()),
6062 reflog: Some(ReflogEntry {
6063 old_oid: zero_oid(ObjectFormat::Sha1).expect("test operation should succeed"),
6064 new_oid: first.clone(),
6065 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
6066 message: b"old".to_vec(),
6067 }),
6068 });
6069 tx.update(RefUpdate {
6070 name: "refs/heads/main".into(),
6071 expected: None,
6072 new: RefTarget::Direct(second.clone()),
6073 reflog: Some(ReflogEntry {
6074 old_oid: first,
6075 new_oid: second.clone(),
6076 committer: b"Git Rs <sley@example.invalid> 100 +0000".to_vec(),
6077 message: b"new".to_vec(),
6078 }),
6079 });
6080 tx.commit().expect("test operation should succeed");
6081
6082 let removed = store
6083 .expire_reflog_older_than("refs/heads/main", 50)
6084 .expect("test operation should succeed");
6085 assert_eq!(removed, 1);
6086 let log = store
6087 .read_reflog("refs/heads/main")
6088 .expect("test operation should succeed");
6089 assert_eq!(log.len(), 1);
6090 assert_eq!(log[0].new_oid, second);
6091 assert_eq!(log[0].message, b"new");
6092 assert!(
6093 !git_dir
6094 .join("logs")
6095 .join("refs")
6096 .join("heads")
6097 .join("main.lock")
6098 .exists()
6099 );
6100 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6101 }
6102
6103 #[test]
6104 fn file_ref_store_creates_branch() {
6105 let git_dir = temp_git_dir();
6106 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6107 let oid = ObjectId::from_hex(
6108 ObjectFormat::Sha1,
6109 "ce013625030ba8dba906f756967f9e9ca394464a",
6110 )
6111 .expect("test operation should succeed");
6112 let branch = store
6113 .create_branch(
6114 "feature",
6115 oid,
6116 b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
6117 b"branch: Created from main".to_vec(),
6118 )
6119 .expect("test operation should succeed");
6120 assert_eq!(branch.name, "refs/heads/feature");
6121 assert_eq!(
6122 store
6123 .read_ref("refs/heads/feature")
6124 .expect("test operation should succeed"),
6125 Some(RefTarget::Direct(oid))
6126 );
6127 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6128 }
6129
6130 #[test]
6131 fn file_ref_store_deletes_loose_branch() {
6132 let git_dir = temp_git_dir();
6133 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6134 let oid = ObjectId::from_hex(
6135 ObjectFormat::Sha1,
6136 "ce013625030ba8dba906f756967f9e9ca394464a",
6137 )
6138 .expect("test operation should succeed");
6139 store
6140 .create_branch(
6141 "feature",
6142 oid,
6143 b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
6144 b"branch: Created from main".to_vec(),
6145 )
6146 .expect("test operation should succeed");
6147 let deleted = store
6148 .delete_branch("feature")
6149 .expect("test operation should succeed");
6150 assert_eq!(deleted.name, "refs/heads/feature");
6151 assert_eq!(deleted.oid, oid);
6152 assert_eq!(
6153 store
6154 .read_ref("refs/heads/feature")
6155 .expect("test operation should succeed"),
6156 None
6157 );
6158 assert!(!git_dir.join("refs").join("heads").join("feature").exists());
6159 assert!(
6160 !git_dir
6161 .join("logs")
6162 .join("refs")
6163 .join("heads")
6164 .join("feature")
6165 .exists()
6166 );
6167 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6168 }
6169
6170 #[test]
6171 fn file_ref_store_deletes_generic_loose_ref() {
6172 let git_dir = temp_git_dir();
6173 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6174 let oid = ObjectId::from_hex(
6175 ObjectFormat::Sha1,
6176 "ce013625030ba8dba906f756967f9e9ca394464a",
6177 )
6178 .expect("test operation should succeed");
6179 let mut tx = store.transaction();
6180 tx.update(RefUpdate {
6181 name: "refs/heads/topic".into(),
6182 expected: None,
6183 new: RefTarget::Direct(oid),
6184 reflog: Some(ReflogEntry {
6185 old_oid: zero_oid(ObjectFormat::Sha1).expect("test operation should succeed"),
6186 new_oid: oid,
6187 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
6188 message: b"update by test".to_vec(),
6189 }),
6190 });
6191 tx.commit().expect("test operation should succeed");
6192 let deleted = store
6193 .delete_ref("refs/heads/topic")
6194 .expect("test operation should succeed");
6195 assert_eq!(deleted.name, "refs/heads/topic");
6196 assert_eq!(deleted.oid, oid);
6197 assert_eq!(
6198 store
6199 .read_ref("refs/heads/topic")
6200 .expect("test operation should succeed"),
6201 None
6202 );
6203 assert!(!git_dir.join("refs").join("heads").join("topic").exists());
6204 assert!(
6205 !git_dir
6206 .join("logs")
6207 .join("refs")
6208 .join("heads")
6209 .join("topic")
6210 .exists()
6211 );
6212 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6213 }
6214
6215 #[test]
6216 fn file_ref_store_delete_ref_checked_removes_reflog() {
6217 let git_dir = temp_git_dir();
6218 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6219 let oid = ObjectId::from_hex(
6220 ObjectFormat::Sha1,
6221 "ce013625030ba8dba906f756967f9e9ca394464a",
6222 )
6223 .expect("test operation should succeed");
6224 let mut tx = store.transaction();
6228 tx.update(RefUpdate {
6229 name: "refs/heads/main".into(),
6230 expected: None,
6231 new: RefTarget::Direct(oid),
6232 reflog: Some(ReflogEntry {
6233 old_oid: zero_oid(ObjectFormat::Sha1).expect("test operation should succeed"),
6234 new_oid: oid,
6235 committer: b"Git Rs <sley@example.invalid> 0 +0000".to_vec(),
6236 message: b"create main".to_vec(),
6237 }),
6238 });
6239 tx.commit().expect("test operation should succeed");
6240 assert!(
6241 git_dir
6242 .join("logs")
6243 .join("refs")
6244 .join("heads")
6245 .join("main")
6246 .exists(),
6247 "reflog file should exist before the checked delete"
6248 );
6249
6250 let deleted = store
6251 .delete_ref_checked(DeleteRef {
6252 name: "refs/heads/main".into(),
6253 expected_old: Some(oid),
6254 reflog: Some(DeleteRefReflog {
6255 committer: b"Git Rs <sley@example.invalid> 123 +0000".to_vec(),
6256 message: b"delete main".to_vec(),
6257 }),
6258 })
6259 .expect("test operation should succeed");
6260
6261 assert_eq!(deleted.name, "refs/heads/main");
6262 assert_eq!(deleted.oid, oid);
6263 assert_eq!(
6264 store
6265 .read_ref("refs/heads/main")
6266 .expect("test operation should succeed"),
6267 None
6268 );
6269 assert!(
6272 !git_dir
6273 .join("logs")
6274 .join("refs")
6275 .join("heads")
6276 .join("main")
6277 .exists(),
6278 "reflog file should be removed by the checked delete"
6279 );
6280 assert!(
6281 store
6282 .read_reflog("refs/heads/main")
6283 .expect("test operation should succeed")
6284 .is_empty()
6285 );
6286 assert!(
6287 !git_dir
6288 .join("refs")
6289 .join("heads")
6290 .join("main.lock")
6291 .exists()
6292 );
6293 assert!(!git_dir.join("packed-refs.lock").exists());
6294 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6295 }
6296
6297 #[test]
6298 fn file_ref_store_delete_ref_checked_stale_expected_leaves_ref_untouched() {
6299 let git_dir = temp_git_dir();
6300 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6301 let actual = ObjectId::from_hex(
6302 ObjectFormat::Sha1,
6303 "ce013625030ba8dba906f756967f9e9ca394464a",
6304 )
6305 .expect("test operation should succeed");
6306 let expected = ObjectId::from_hex(
6307 ObjectFormat::Sha1,
6308 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
6309 )
6310 .expect("test operation should succeed");
6311 let mut tx = store.transaction();
6312 tx.update(RefUpdate {
6313 name: "refs/heads/main".into(),
6314 expected: None,
6315 new: RefTarget::Direct(actual),
6316 reflog: None,
6317 });
6318 tx.commit().expect("test operation should succeed");
6319
6320 let err = store
6321 .delete_ref_checked(DeleteRef {
6322 name: "refs/heads/main".into(),
6323 expected_old: Some(expected),
6324 reflog: None,
6325 })
6326 .expect_err("stale expected must fail");
6327
6328 assert!(matches!(
6329 err,
6330 RefDeleteError::ExpectedMismatch {
6331 expected: Some(got_expected),
6332 actual: Some(got_actual),
6333 } if got_expected == expected && got_actual == actual
6334 ));
6335 assert_eq!(
6336 store
6337 .read_ref("refs/heads/main")
6338 .expect("test operation should succeed"),
6339 Some(RefTarget::Direct(actual))
6340 );
6341 assert!(
6342 !git_dir
6343 .join("refs")
6344 .join("heads")
6345 .join("main.lock")
6346 .exists()
6347 );
6348 assert!(!git_dir.join("packed-refs.lock").exists());
6349 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6350 }
6351
6352 #[test]
6353 fn file_ref_store_delete_ref_checked_missing_returns_not_found() {
6354 let git_dir = temp_git_dir();
6355 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6356
6357 let err = store
6358 .delete_ref_checked(DeleteRef {
6359 name: "refs/heads/missing".into(),
6360 expected_old: None,
6361 reflog: None,
6362 })
6363 .expect_err("missing ref must fail");
6364
6365 assert!(matches!(err, RefDeleteError::NotFound));
6366 assert!(
6367 !git_dir
6368 .join("refs")
6369 .join("heads")
6370 .join("missing.lock")
6371 .exists()
6372 );
6373 assert!(!git_dir.join("packed-refs.lock").exists());
6374 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6375 }
6376
6377 #[test]
6378 fn file_ref_store_delete_ref_checked_removes_packed_ref() {
6379 let git_dir = temp_git_dir();
6380 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6381 let oid = ObjectId::from_hex(
6382 ObjectFormat::Sha1,
6383 "ce013625030ba8dba906f756967f9e9ca394464a",
6384 )
6385 .expect("test operation should succeed");
6386 let other = ObjectId::from_hex(
6387 ObjectFormat::Sha1,
6388 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
6389 )
6390 .expect("test operation should succeed");
6391 store
6392 .write_packed_refs(&[
6393 PackedRef {
6394 reference: Ref {
6395 name: "refs/heads/main".into(),
6396 target: RefTarget::Direct(oid),
6397 },
6398 peeled: None,
6399 },
6400 PackedRef {
6401 reference: Ref {
6402 name: "refs/heads/other".into(),
6403 target: RefTarget::Direct(other),
6404 },
6405 peeled: None,
6406 },
6407 ])
6408 .expect("test operation should succeed");
6409
6410 store
6411 .delete_ref_checked(DeleteRef {
6412 name: "refs/heads/main".into(),
6413 expected_old: Some(oid),
6414 reflog: None,
6415 })
6416 .expect("test operation should succeed");
6417
6418 assert_eq!(
6419 store
6420 .read_ref("refs/heads/main")
6421 .expect("test operation should succeed"),
6422 None
6423 );
6424 assert_eq!(
6425 store
6426 .read_ref("refs/heads/other")
6427 .expect("test operation should succeed"),
6428 Some(RefTarget::Direct(other))
6429 );
6430 let packed =
6431 fs::read_to_string(git_dir.join("packed-refs")).expect("test operation should succeed");
6432 assert!(!packed.contains("refs/heads/main"));
6433 assert!(packed.contains("refs/heads/other"));
6434 assert!(
6435 !git_dir
6436 .join("refs")
6437 .join("heads")
6438 .join("main.lock")
6439 .exists()
6440 );
6441 assert!(!git_dir.join("packed-refs.lock").exists());
6442 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6443 }
6444
6445 #[test]
6446 fn file_ref_store_delete_ref_checked_lock_conflict_returns_locked() {
6447 let git_dir = temp_git_dir();
6448 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6449 let oid = ObjectId::from_hex(
6450 ObjectFormat::Sha1,
6451 "ce013625030ba8dba906f756967f9e9ca394464a",
6452 )
6453 .expect("test operation should succeed");
6454 let mut tx = store.transaction();
6455 tx.update(RefUpdate {
6456 name: "refs/heads/main".into(),
6457 expected: None,
6458 new: RefTarget::Direct(oid),
6459 reflog: None,
6460 });
6461 tx.commit().expect("test operation should succeed");
6462 fs::write(
6463 git_dir.join("refs").join("heads").join("main.lock"),
6464 b"held\n",
6465 )
6466 .expect("test operation should succeed");
6467
6468 let err = store
6469 .delete_ref_checked(DeleteRef {
6470 name: "refs/heads/main".into(),
6471 expected_old: Some(oid),
6472 reflog: None,
6473 })
6474 .expect_err("held lock must fail");
6475
6476 assert!(matches!(err, RefDeleteError::Locked));
6477 assert_eq!(
6478 store
6479 .read_ref("refs/heads/main")
6480 .expect("test operation should succeed"),
6481 Some(RefTarget::Direct(oid))
6482 );
6483 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6484 }
6485
6486 #[test]
6487 fn file_ref_store_reports_current_branch() {
6488 let git_dir = temp_git_dir();
6489 fs::write(git_dir.join("HEAD"), b"ref: refs/heads/main\n")
6490 .expect("test operation should succeed");
6491 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6492 assert_eq!(
6493 store
6494 .current_branch_ref()
6495 .expect("test operation should succeed"),
6496 Some("refs/heads/main".into())
6497 );
6498 assert_eq!(
6499 store
6500 .current_branch()
6501 .expect("test operation should succeed"),
6502 Some("main".into())
6503 );
6504 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6505 }
6506
6507 #[test]
6508 fn file_ref_store_resolves_linked_worktree_head_through_common_refs() {
6509 let common = temp_git_dir();
6510 let admin = common.join("worktrees").join("linked");
6511 fs::create_dir_all(&admin).expect("test operation should succeed");
6512 fs::write(admin.join("commondir"), "../..\n").expect("test operation should succeed");
6513 fs::write(admin.join("HEAD"), b"ref: refs/heads/topic\n")
6514 .expect("test operation should succeed");
6515 let oid = ObjectId::from_hex(
6516 ObjectFormat::Sha256,
6517 "08ffba112b648c22b5425f01bec2c37ffc524c4d48ef04337779df3973733050",
6518 )
6519 .expect("test operation should succeed");
6520 fs::create_dir_all(common.join("refs").join("heads"))
6521 .expect("test operation should succeed");
6522 fs::write(
6523 common.join("refs").join("heads").join("topic"),
6524 format!("{oid}\n"),
6525 )
6526 .expect("test operation should succeed");
6527
6528 let store = FileRefStore::new(&admin, ObjectFormat::Sha256);
6529 assert_eq!(
6530 store
6531 .read_ref("HEAD")
6532 .expect("test operation should succeed"),
6533 Some(RefTarget::Symbolic("refs/heads/topic".into()))
6534 );
6535 assert_eq!(
6536 store
6537 .read_ref("refs/heads/topic")
6538 .expect("test operation should succeed"),
6539 Some(RefTarget::Direct(oid))
6540 );
6541
6542 fs::remove_dir_all(common).expect("test operation should succeed");
6543 }
6544
6545 #[test]
6546 fn file_ref_store_creates_tag() {
6547 let git_dir = temp_git_dir();
6548 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6549 let oid = ObjectId::from_hex(
6550 ObjectFormat::Sha1,
6551 "ce013625030ba8dba906f756967f9e9ca394464a",
6552 )
6553 .expect("test operation should succeed");
6554 let tag = store
6555 .create_tag("v1.0", oid)
6556 .expect("test operation should succeed");
6557 assert_eq!(tag.name, "refs/tags/v1.0");
6558 assert_eq!(
6559 store
6560 .read_ref("refs/tags/v1.0")
6561 .expect("test operation should succeed"),
6562 Some(RefTarget::Direct(oid))
6563 );
6564 assert!(
6565 store
6566 .read_reflog("refs/tags/v1.0")
6567 .expect("test operation should succeed")
6568 .is_empty()
6569 );
6570 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6571 }
6572
6573 #[test]
6574 fn file_ref_store_deletes_loose_tag() {
6575 let git_dir = temp_git_dir();
6576 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6577 let oid = ObjectId::from_hex(
6578 ObjectFormat::Sha1,
6579 "ce013625030ba8dba906f756967f9e9ca394464a",
6580 )
6581 .expect("test operation should succeed");
6582 store
6583 .create_tag("v1.0", oid)
6584 .expect("test operation should succeed");
6585 let deleted = store
6586 .delete_tag("v1.0")
6587 .expect("test operation should succeed");
6588 assert_eq!(deleted.name, "refs/tags/v1.0");
6589 assert_eq!(deleted.oid, oid);
6590 assert_eq!(
6591 store
6592 .read_ref("refs/tags/v1.0")
6593 .expect("test operation should succeed"),
6594 None
6595 );
6596 assert!(!git_dir.join("refs").join("tags").join("v1.0").exists());
6597 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6598 }
6599
6600 #[test]
6601 fn file_ref_store_reads_packed_ref() {
6602 let git_dir = temp_git_dir();
6603 fs::write(
6604 git_dir.join("packed-refs"),
6605 b"ce013625030ba8dba906f756967f9e9ca394464a refs/heads/main\n",
6606 )
6607 .expect("test operation should succeed");
6608 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6609 assert!(matches!(
6610 store
6611 .read_ref("refs/heads/main")
6612 .expect("test operation should succeed"),
6613 Some(RefTarget::Direct(_))
6614 ));
6615 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6616 }
6617
6618 #[test]
6619 fn file_ref_store_lists_loose_refs_over_packed_refs() {
6620 let git_dir = temp_git_dir();
6621 fs::write(
6622 git_dir.join("packed-refs"),
6623 b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 refs/heads/main\n",
6624 )
6625 .expect("test operation should succeed");
6626 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6627 let oid = ObjectId::from_hex(
6628 ObjectFormat::Sha1,
6629 "ce013625030ba8dba906f756967f9e9ca394464a",
6630 )
6631 .expect("test operation should succeed");
6632 let mut tx = store.transaction();
6633 tx.update(RefUpdate {
6634 name: "refs/heads/main".into(),
6635 expected: None,
6636 new: RefTarget::Direct(oid),
6637 reflog: None,
6638 });
6639 tx.commit().expect("test operation should succeed");
6640 let refs = store.list_refs().expect("test operation should succeed");
6641 assert_eq!(refs.len(), 1);
6642 assert_eq!(refs[0].target, RefTarget::Direct(oid));
6643 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6644 }
6645
6646 #[test]
6647 fn file_ref_store_lists_refs_with_prefix_and_preserves_loose_shadowing() {
6648 let git_dir = temp_git_dir();
6649 fs::write(
6650 git_dir.join("packed-refs"),
6651 b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 refs/heads/main\n\
6652 18f002b4484b838b205a48b1e9e6763ba5e3a607 refs/heads/topic\n\
6653 ce013625030ba8dba906f756967f9e9ca394464a refs/tags/v1.0\n",
6654 )
6655 .expect("test operation should succeed");
6656 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6657 let loose_main = ObjectId::from_hex(
6658 ObjectFormat::Sha1,
6659 "ce013625030ba8dba906f756967f9e9ca394464a",
6660 )
6661 .expect("test operation should succeed");
6662 let packed_topic = ObjectId::from_hex(
6663 ObjectFormat::Sha1,
6664 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
6665 )
6666 .expect("test operation should succeed");
6667 let mut tx = store.transaction();
6668 tx.update(RefUpdate {
6669 name: "refs/heads/main".into(),
6670 expected: None,
6671 new: RefTarget::Direct(loose_main),
6672 reflog: None,
6673 });
6674 tx.commit().expect("test operation should succeed");
6675
6676 assert_eq!(
6677 store
6678 .list_refs_with_prefix("refs/heads/")
6679 .expect("test operation should succeed"),
6680 vec![
6681 Ref {
6682 name: "refs/heads/main".into(),
6683 target: RefTarget::Direct(loose_main),
6684 },
6685 Ref {
6686 name: "refs/heads/topic".into(),
6687 target: RefTarget::Direct(packed_topic),
6688 },
6689 ]
6690 );
6691 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6692 }
6693
6694 #[test]
6695 fn file_ref_store_writes_packed_refs() {
6696 let git_dir = temp_git_dir();
6697 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6698 let oid = ObjectId::from_hex(
6699 ObjectFormat::Sha1,
6700 "ce013625030ba8dba906f756967f9e9ca394464a",
6701 )
6702 .expect("test operation should succeed");
6703 store
6704 .write_packed_refs(&[PackedRef {
6705 reference: Ref {
6706 name: "refs/heads/main".into(),
6707 target: RefTarget::Direct(oid),
6708 },
6709 peeled: None,
6710 }])
6711 .expect("test operation should succeed");
6712 assert_eq!(
6713 store
6714 .read_ref("refs/heads/main")
6715 .expect("test operation should succeed"),
6716 Some(RefTarget::Direct(oid))
6717 );
6718 let refs = store.list_refs().expect("test operation should succeed");
6719 assert_eq!(refs.len(), 1);
6720 assert_eq!(refs[0].target, RefTarget::Direct(oid));
6721 assert!(git_dir.join("packed-refs").exists());
6722 assert!(!git_dir.join("packed-refs.lock").exists());
6723 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6724 }
6725
6726 #[test]
6727 fn file_ref_store_checks_ref_prefix_in_packed_refs() {
6728 let git_dir = temp_git_dir();
6729 fs::write(
6730 git_dir.join("packed-refs"),
6731 b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 refs/heads/main\n\
6732 ce013625030ba8dba906f756967f9e9ca394464a refs/replace/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\n",
6733 )
6734 .expect("test operation should succeed");
6735 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6736 assert!(
6737 store
6738 .has_refs_with_prefix("refs/replace/")
6739 .expect("test operation should succeed")
6740 );
6741 assert!(
6742 !store
6743 .has_refs_with_prefix("refs/notes/")
6744 .expect("test operation should succeed")
6745 );
6746 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6747 }
6748
6749 #[test]
6750 fn file_ref_store_checks_ref_prefix_in_loose_refs() {
6751 let git_dir = temp_git_dir();
6752 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6753 let oid = ObjectId::from_hex(
6754 ObjectFormat::Sha1,
6755 "ce013625030ba8dba906f756967f9e9ca394464a",
6756 )
6757 .expect("test operation should succeed");
6758 let mut tx = store.transaction();
6759 tx.update(RefUpdate {
6760 name: "refs/replace/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391".into(),
6761 expected: None,
6762 new: RefTarget::Direct(oid),
6763 reflog: None,
6764 });
6765 tx.commit().expect("test operation should succeed");
6766 assert!(
6767 store
6768 .has_refs_with_prefix("refs/replace/")
6769 .expect("test operation should succeed")
6770 );
6771 assert!(
6772 !store
6773 .has_refs_with_prefix("refs/notes/")
6774 .expect("test operation should succeed")
6775 );
6776 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6777 }
6778
6779 #[test]
6780 fn file_ref_store_reads_reftable_stack_and_ignores_dummy_head() {
6781 let git_dir = temp_git_dir();
6782 write_reftable_config(&git_dir);
6783 fs::write(git_dir.join("HEAD"), b"ref: refs/heads/.invalid\n")
6784 .expect("test operation should succeed");
6785 let head_oid = ObjectId::from_hex(
6786 ObjectFormat::Sha1,
6787 "ce013625030ba8dba906f756967f9e9ca394464a",
6788 )
6789 .expect("test operation should succeed");
6790 let tag_oid = ObjectId::from_hex(
6791 ObjectFormat::Sha1,
6792 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
6793 )
6794 .expect("test operation should succeed");
6795 let peeled_oid = ObjectId::from_hex(
6796 ObjectFormat::Sha1,
6797 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
6798 )
6799 .expect("test operation should succeed");
6800 write_reftable_stack(
6801 &git_dir,
6802 &[(
6803 "0x000000000001-0x000000000001-00000000.ref",
6804 vec![
6805 sley_formats::ReftableRefRecord {
6806 name: "HEAD".into(),
6807 update_index: 1,
6808 value: ReftableRefValue::Symbolic("refs/heads/main".into()),
6809 },
6810 sley_formats::ReftableRefRecord {
6811 name: "refs/heads/main".into(),
6812 update_index: 1,
6813 value: ReftableRefValue::Direct(head_oid),
6814 },
6815 sley_formats::ReftableRefRecord {
6816 name: "refs/tags/v1.0".into(),
6817 update_index: 1,
6818 value: ReftableRefValue::Peeled {
6819 target: tag_oid,
6820 peeled: peeled_oid,
6821 },
6822 },
6823 ],
6824 )],
6825 );
6826
6827 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6828 assert_eq!(
6829 store
6830 .read_ref("HEAD")
6831 .expect("test operation should succeed"),
6832 Some(RefTarget::Symbolic("refs/heads/main".into()))
6833 );
6834 assert_eq!(
6835 store
6836 .read_ref("refs/heads/main")
6837 .expect("test operation should succeed"),
6838 Some(RefTarget::Direct(head_oid))
6839 );
6840 assert_eq!(
6841 store
6842 .read_ref("refs/tags/v1.0")
6843 .expect("test operation should succeed"),
6844 Some(RefTarget::Direct(tag_oid))
6845 );
6846 let refs = store.list_refs().expect("test operation should succeed");
6847 assert_eq!(
6848 refs,
6849 vec![
6850 Ref {
6851 name: "refs/heads/main".into(),
6852 target: RefTarget::Direct(head_oid),
6853 },
6854 Ref {
6855 name: "refs/tags/v1.0".into(),
6856 target: RefTarget::Direct(tag_oid),
6857 },
6858 ]
6859 );
6860 assert_eq!(
6861 store
6862 .list_refs_with_prefix("refs/tags/")
6863 .expect("test operation should succeed"),
6864 vec![Ref {
6865 name: "refs/tags/v1.0".into(),
6866 target: RefTarget::Direct(tag_oid),
6867 }]
6868 );
6869
6870 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6871 }
6872
6873 #[test]
6874 fn file_ref_store_reads_loose_fetch_head_in_reftable_repo() {
6875 let git_dir = temp_git_dir();
6876 write_reftable_config(&git_dir);
6877 fs::write(git_dir.join("HEAD"), b"ref: refs/heads/.invalid\n")
6878 .expect("test operation should succeed");
6879 fs::create_dir_all(git_dir.join("reftable")).expect("test operation should succeed");
6880 fs::write(git_dir.join("reftable").join("tables.list"), b"")
6881 .expect("test operation should succeed");
6882 let oid = ObjectId::from_hex(
6883 ObjectFormat::Sha1,
6884 "ce013625030ba8dba906f756967f9e9ca394464a",
6885 )
6886 .expect("test operation should succeed");
6887 fs::write(
6888 git_dir.join("FETCH_HEAD"),
6889 b"ce013625030ba8dba906f756967f9e9ca394464a\t\tbranch 'main' of ../sub\n",
6890 )
6891 .expect("test operation should succeed");
6892
6893 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6894 assert_eq!(
6895 store
6896 .read_ref("FETCH_HEAD")
6897 .expect("test operation should succeed"),
6898 Some(RefTarget::Direct(oid))
6899 );
6900 assert!(
6901 store
6902 .raw_ref_exists("FETCH_HEAD")
6903 .expect("test operation should succeed")
6904 );
6905
6906 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6907 }
6908
6909 #[test]
6910 fn file_ref_store_empty_reftable_reflog_rewrite_keeps_marker() {
6911 let git_dir = temp_git_dir();
6912 write_reftable_config(&git_dir);
6913 fs::create_dir_all(git_dir.join("reftable")).expect("test operation should succeed");
6914 fs::write(git_dir.join("reftable").join("tables.list"), b"")
6915 .expect("test operation should succeed");
6916 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6917
6918 store
6919 .write_reflog("refs/heads/main", &[])
6920 .expect("test operation should succeed");
6921
6922 assert!(
6923 store
6924 .read_reflog("refs/heads/main")
6925 .expect("test operation should succeed")
6926 .is_empty()
6927 );
6928 let tables = store.reftables().expect("test operation should succeed");
6929 let marker = tables
6930 .iter()
6931 .flat_map(|table| table.logs.iter())
6932 .find(|record| record.refname == "refs/heads/main")
6933 .expect("empty reflog marker should exist");
6934 let ReftableLogValue::Update(update) = &marker.value else {
6935 panic!("empty reflog marker should be an update");
6936 };
6937 let null = ObjectId::null(ObjectFormat::Sha1);
6938 assert_eq!(update.old_oid, null);
6939 assert_eq!(update.new_oid, null);
6940
6941 fs::remove_dir_all(git_dir).expect("test operation should succeed");
6942 }
6943
6944 #[test]
6945 fn file_ref_store_applies_reftable_stack_overrides_and_deletions() {
6946 let git_dir = temp_git_dir();
6947 write_reftable_config(&git_dir);
6948 let first = ObjectId::from_hex(
6949 ObjectFormat::Sha1,
6950 "ce013625030ba8dba906f756967f9e9ca394464a",
6951 )
6952 .expect("test operation should succeed");
6953 let second = ObjectId::from_hex(
6954 ObjectFormat::Sha1,
6955 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
6956 )
6957 .expect("test operation should succeed");
6958 write_reftable_stack(
6959 &git_dir,
6960 &[
6961 (
6962 "0x000000000001-0x000000000001-00000000.ref",
6963 vec![
6964 sley_formats::ReftableRefRecord {
6965 name: "refs/heads/main".into(),
6966 update_index: 1,
6967 value: ReftableRefValue::Direct(first),
6968 },
6969 sley_formats::ReftableRefRecord {
6970 name: "refs/heads/topic".into(),
6971 update_index: 1,
6972 value: ReftableRefValue::Direct(second.clone()),
6973 },
6974 ],
6975 ),
6976 (
6977 "000000000002-000000000002-tip.ref",
6978 vec![
6979 sley_formats::ReftableRefRecord {
6980 name: "refs/heads/main".into(),
6981 update_index: 2,
6982 value: ReftableRefValue::Direct(second.clone()),
6983 },
6984 sley_formats::ReftableRefRecord {
6985 name: "refs/heads/topic".into(),
6986 update_index: 2,
6987 value: ReftableRefValue::Deletion,
6988 },
6989 ],
6990 ),
6991 ],
6992 );
6993
6994 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
6995 assert_eq!(
6996 store
6997 .read_ref("refs/heads/main")
6998 .expect("test operation should succeed"),
6999 Some(RefTarget::Direct(second.clone()))
7000 );
7001 assert_eq!(
7002 store
7003 .read_ref("refs/heads/topic")
7004 .expect("test operation should succeed"),
7005 None
7006 );
7007 assert_eq!(
7008 store.list_refs().expect("test operation should succeed"),
7009 vec![Ref {
7010 name: "refs/heads/main".into(),
7011 target: RefTarget::Direct(second),
7012 }]
7013 );
7014
7015 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7016 }
7017
7018 #[test]
7019 fn file_ref_store_writes_reftable_transaction_table() {
7020 let git_dir = temp_git_dir();
7021 write_reftable_config(&git_dir);
7022 let first = ObjectId::from_hex(
7023 ObjectFormat::Sha1,
7024 "ce013625030ba8dba906f756967f9e9ca394464a",
7025 )
7026 .expect("test operation should succeed");
7027 let second = ObjectId::from_hex(
7028 ObjectFormat::Sha1,
7029 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7030 )
7031 .expect("test operation should succeed");
7032 write_reftable_stack(
7033 &git_dir,
7034 &[(
7035 "0x000000000001-0x000000000001-00000000.ref",
7036 vec![sley_formats::ReftableRefRecord {
7037 name: "refs/heads/main".into(),
7038 update_index: 1,
7039 value: ReftableRefValue::Direct(first),
7040 }],
7041 )],
7042 );
7043
7044 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7045 let mut tx = store.transaction();
7046 tx.update(RefUpdate {
7047 name: "HEAD".into(),
7048 expected: None,
7049 new: RefTarget::Symbolic("refs/heads/main".into()),
7050 reflog: None,
7051 });
7052 tx.update(RefUpdate {
7053 name: "refs/heads/main".into(),
7054 expected: None,
7055 new: RefTarget::Direct(second.clone()),
7056 reflog: None,
7057 });
7058 tx.commit().expect("test operation should succeed");
7059
7060 assert_eq!(
7061 store
7062 .read_ref("HEAD")
7063 .expect("test operation should succeed"),
7064 Some(RefTarget::Symbolic("refs/heads/main".into()))
7065 );
7066 assert_eq!(
7067 store
7068 .read_ref("refs/heads/main")
7069 .expect("test operation should succeed"),
7070 Some(RefTarget::Direct(second.clone()))
7071 );
7072 assert_eq!(
7073 store
7074 .list_refs()
7075 .expect("test operation should succeed")
7076 .len(),
7077 1
7078 );
7079 assert!(!git_dir.join("HEAD").exists());
7080 let tables = fs::read_to_string(git_dir.join("reftable").join("tables.list"))
7081 .expect("test operation should succeed");
7082 assert_eq!(tables.lines().count(), 2);
7083 let last = tables
7084 .lines()
7085 .last()
7086 .expect("test operation should succeed");
7087 assert!(
7091 last.starts_with("0x") && last.ends_with(".ref"),
7092 "expected git-format reftable name in tables.list, got {tables}"
7093 );
7094 assert!(
7095 reftable_table_name_is_valid(last),
7096 "rust-written reftable name must parse as git's hex format, got {last}"
7097 );
7098
7099 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7100 }
7101
7102 #[test]
7103 fn file_ref_store_deletes_reftable_refs_with_tombstones() {
7104 let git_dir = temp_git_dir();
7105 write_reftable_config(&git_dir);
7106 let oid = ObjectId::from_hex(
7107 ObjectFormat::Sha1,
7108 "ce013625030ba8dba906f756967f9e9ca394464a",
7109 )
7110 .expect("test operation should succeed");
7111 write_reftable_stack(
7112 &git_dir,
7113 &[(
7114 "0x000000000001-0x000000000001-00000000.ref",
7115 vec![
7116 sley_formats::ReftableRefRecord {
7117 name: "refs/heads/main".into(),
7118 update_index: 1,
7119 value: ReftableRefValue::Direct(oid),
7120 },
7121 sley_formats::ReftableRefRecord {
7122 name: "refs/alias/main".into(),
7123 update_index: 1,
7124 value: ReftableRefValue::Symbolic("refs/heads/main".into()),
7125 },
7126 ],
7127 )],
7128 );
7129
7130 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7131 assert!(
7132 store
7133 .delete_symbolic_ref("refs/alias/main")
7134 .expect("test operation should succeed")
7135 );
7136 assert_eq!(
7137 store
7138 .read_ref("refs/alias/main")
7139 .expect("test operation should succeed"),
7140 None
7141 );
7142 let deleted = store
7143 .delete_ref("refs/heads/main")
7144 .expect("test operation should succeed");
7145 assert_eq!(deleted.oid, oid);
7146 assert_eq!(
7147 store
7148 .read_ref("refs/heads/main")
7149 .expect("test operation should succeed"),
7150 None
7151 );
7152 assert!(
7153 store
7154 .list_refs()
7155 .expect("test operation should succeed")
7156 .is_empty()
7157 );
7158 let tables = fs::read_to_string(git_dir.join("reftable").join("tables.list"))
7159 .expect("test operation should succeed");
7160 assert_eq!(tables.lines().count(), 3);
7161
7162 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7163 }
7164
7165 #[test]
7166 fn file_ref_store_deletes_packed_branch() {
7167 let git_dir = temp_git_dir();
7168 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7169 let branch_oid = ObjectId::from_hex(
7170 ObjectFormat::Sha1,
7171 "ce013625030ba8dba906f756967f9e9ca394464a",
7172 )
7173 .expect("test operation should succeed");
7174 let tag_oid = ObjectId::from_hex(
7175 ObjectFormat::Sha1,
7176 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7177 )
7178 .expect("test operation should succeed");
7179 store
7180 .write_packed_refs(&[
7181 PackedRef {
7182 reference: Ref {
7183 name: "refs/heads/feature".into(),
7184 target: RefTarget::Direct(branch_oid),
7185 },
7186 peeled: None,
7187 },
7188 PackedRef {
7189 reference: Ref {
7190 name: "refs/tags/v1.0".into(),
7191 target: RefTarget::Direct(tag_oid),
7192 },
7193 peeled: None,
7194 },
7195 ])
7196 .expect("test operation should succeed");
7197 let deleted = store
7198 .delete_branch("feature")
7199 .expect("test operation should succeed");
7200 assert_eq!(deleted.name, "refs/heads/feature");
7201 assert_eq!(deleted.oid, branch_oid);
7202 assert_eq!(
7203 store
7204 .read_ref("refs/heads/feature")
7205 .expect("test operation should succeed"),
7206 None
7207 );
7208 assert_eq!(
7209 store
7210 .read_ref("refs/tags/v1.0")
7211 .expect("test operation should succeed"),
7212 Some(RefTarget::Direct(tag_oid))
7213 );
7214 assert!(!git_dir.join("packed-refs.lock").exists());
7215 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7216 }
7217
7218 #[test]
7219 fn file_ref_store_deletes_packed_tag() {
7220 let git_dir = temp_git_dir();
7221 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7222 let oid = ObjectId::from_hex(
7223 ObjectFormat::Sha1,
7224 "ce013625030ba8dba906f756967f9e9ca394464a",
7225 )
7226 .expect("test operation should succeed");
7227 store
7228 .write_packed_refs(&[PackedRef {
7229 reference: Ref {
7230 name: "refs/tags/v1.0".into(),
7231 target: RefTarget::Direct(oid),
7232 },
7233 peeled: None,
7234 }])
7235 .expect("test operation should succeed");
7236 let deleted = store
7237 .delete_tag("v1.0")
7238 .expect("test operation should succeed");
7239 assert_eq!(deleted.name, "refs/tags/v1.0");
7240 assert_eq!(deleted.oid, oid);
7241 assert_eq!(
7242 store
7243 .read_ref("refs/tags/v1.0")
7244 .expect("test operation should succeed"),
7245 None
7246 );
7247 assert!(!git_dir.join("packed-refs.lock").exists());
7248 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7249 }
7250
7251 #[test]
7252 fn file_ref_store_packs_loose_refs_and_prunes() {
7253 let git_dir = temp_git_dir();
7254 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7255 let main_oid = ObjectId::from_hex(
7256 ObjectFormat::Sha1,
7257 "ce013625030ba8dba906f756967f9e9ca394464a",
7258 )
7259 .expect("test operation should succeed");
7260 let tag_oid = ObjectId::from_hex(
7261 ObjectFormat::Sha1,
7262 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7263 )
7264 .expect("test operation should succeed");
7265 let mut tx = store.transaction();
7266 tx.update(RefUpdate {
7267 name: "refs/heads/main".into(),
7268 expected: None,
7269 new: RefTarget::Direct(main_oid),
7270 reflog: None,
7271 });
7272 tx.update(RefUpdate {
7273 name: "refs/tags/v1.0".into(),
7274 expected: None,
7275 new: RefTarget::Direct(tag_oid),
7276 reflog: None,
7277 });
7278 tx.commit().expect("test operation should succeed");
7279
7280 let packed = store
7281 .pack_refs(true)
7282 .expect("test operation should succeed");
7283 assert_eq!(packed.len(), 2);
7284 assert_eq!(
7285 store
7286 .read_ref("refs/heads/main")
7287 .expect("test operation should succeed"),
7288 Some(RefTarget::Direct(main_oid))
7289 );
7290 assert_eq!(
7291 store
7292 .read_ref("refs/tags/v1.0")
7293 .expect("test operation should succeed"),
7294 Some(RefTarget::Direct(tag_oid))
7295 );
7296 assert!(!git_dir.join("refs").join("heads").join("main").exists());
7297 assert!(!git_dir.join("refs").join("tags").join("v1.0").exists());
7298 assert!(git_dir.join("packed-refs").exists());
7299 assert!(!git_dir.join("packed-refs.lock").exists());
7300 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7301 }
7302
7303 #[test]
7304 fn file_ref_store_packs_loose_refs_without_pruning() {
7305 let git_dir = temp_git_dir();
7306 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7307 let oid = ObjectId::from_hex(
7308 ObjectFormat::Sha1,
7309 "ce013625030ba8dba906f756967f9e9ca394464a",
7310 )
7311 .expect("test operation should succeed");
7312 let mut tx = store.transaction();
7313 tx.update(RefUpdate {
7314 name: "refs/heads/main".into(),
7315 expected: None,
7316 new: RefTarget::Direct(oid),
7317 reflog: None,
7318 });
7319 tx.commit().expect("test operation should succeed");
7320
7321 let packed = store
7322 .pack_refs(false)
7323 .expect("test operation should succeed");
7324 assert_eq!(packed.len(), 1);
7325 assert!(git_dir.join("refs").join("heads").join("main").exists());
7326 assert_eq!(
7327 store
7328 .read_ref("refs/heads/main")
7329 .expect("test operation should succeed"),
7330 Some(RefTarget::Direct(oid))
7331 );
7332 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7333 }
7334
7335 #[test]
7336 fn file_ref_store_packs_loose_refs_with_peeled_ids() {
7337 let git_dir = temp_git_dir();
7338 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7339 let tag_oid = ObjectId::from_hex(
7340 ObjectFormat::Sha1,
7341 "ce013625030ba8dba906f756967f9e9ca394464a",
7342 )
7343 .expect("test operation should succeed");
7344 let peeled_oid = ObjectId::from_hex(
7345 ObjectFormat::Sha1,
7346 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7347 )
7348 .expect("test operation should succeed");
7349 let mut tx = store.transaction();
7350 tx.update(RefUpdate {
7351 name: "refs/tags/v1.0".into(),
7352 expected: None,
7353 new: RefTarget::Direct(tag_oid),
7354 reflog: None,
7355 });
7356 tx.commit().expect("test operation should succeed");
7357
7358 let packed = store
7359 .pack_refs_with_peeler(true, |name, oid| {
7360 if name == "refs/tags/v1.0" && oid == &tag_oid {
7361 Ok(Some(peeled_oid))
7362 } else {
7363 Ok(None)
7364 }
7365 })
7366 .expect("test operation should succeed");
7367 assert_eq!(packed.len(), 1);
7368 assert_eq!(packed[0].peeled, Some(peeled_oid));
7369 let bytes =
7370 fs::read_to_string(git_dir.join("packed-refs")).expect("test operation should succeed");
7371 assert!(bytes.contains(&format!("^{peeled_oid}\n")));
7372 assert!(!git_dir.join("refs").join("tags").join("v1.0").exists());
7373 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7374 }
7375
7376 fn reflog_entry(new_oid: &ObjectId, timestamp: i64, message: &str) -> ReflogEntry {
7377 ReflogEntry {
7378 old_oid: zero_oid(new_oid.format()).expect("test operation should succeed"),
7379 new_oid: *new_oid,
7380 committer: format!("Git Rs <sley@example.invalid> {timestamp} +0000").into_bytes(),
7381 message: message.as_bytes().to_vec(),
7382 }
7383 }
7384
7385 #[test]
7386 fn expire_reflog_drops_old_entries_and_keeps_latest() {
7387 let oid_a = ObjectId::from_hex(
7388 ObjectFormat::Sha1,
7389 "ce013625030ba8dba906f756967f9e9ca394464a",
7390 )
7391 .expect("test operation should succeed");
7392 let oid_b = ObjectId::from_hex(
7393 ObjectFormat::Sha1,
7394 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7395 )
7396 .expect("test operation should succeed");
7397 let oid_c = ObjectId::from_hex(
7398 ObjectFormat::Sha1,
7399 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
7400 )
7401 .expect("test operation should succeed");
7402 let entries = vec![
7403 reflog_entry(&oid_a, 10, "oldest"),
7404 reflog_entry(&oid_b, 100, "middle"),
7405 reflog_entry(&oid_c, 20, "latest"),
7406 ];
7407
7408 let retained =
7411 expire_reflog(&entries, 50, None, |_| true).expect("test operation should succeed");
7412 assert_eq!(retained.len(), 2);
7413 assert_eq!(retained[0].message, b"middle");
7414 assert_eq!(retained[1].message, b"latest");
7415 }
7416
7417 #[test]
7418 fn expire_reflog_applies_stricter_unreachable_cutoff() {
7419 let reachable = ObjectId::from_hex(
7420 ObjectFormat::Sha1,
7421 "ce013625030ba8dba906f756967f9e9ca394464a",
7422 )
7423 .expect("test operation should succeed");
7424 let unreachable = ObjectId::from_hex(
7425 ObjectFormat::Sha1,
7426 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7427 )
7428 .expect("test operation should succeed");
7429 let tip = ObjectId::from_hex(
7430 ObjectFormat::Sha1,
7431 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
7432 )
7433 .expect("test operation should succeed");
7434 let entries = vec![
7437 reflog_entry(&reachable, 100, "reachable"),
7438 reflog_entry(&unreachable, 100, "unreachable"),
7439 reflog_entry(&tip, 200, "tip"),
7440 ];
7441 let retained = expire_reflog(&entries, 50, Some(150), |oid| {
7442 oid == &reachable || oid == &tip
7443 })
7444 .expect("test operation should succeed");
7445 assert_eq!(retained.len(), 2);
7446 assert_eq!(retained[0].message, b"reachable");
7447 assert_eq!(retained[1].message, b"tip");
7448 }
7449
7450 #[test]
7451 fn expire_reflog_keeps_single_entry_below_cutoff() {
7452 let oid = ObjectId::from_hex(
7453 ObjectFormat::Sha1,
7454 "ce013625030ba8dba906f756967f9e9ca394464a",
7455 )
7456 .expect("test operation should succeed");
7457 let entries = vec![reflog_entry(&oid, 1, "only")];
7458 let retained = expire_reflog(&entries, i64::MAX, Some(i64::MAX), |_| false)
7459 .expect("test operation should succeed");
7460 assert_eq!(retained.len(), 1);
7461 assert_eq!(retained[0].message, b"only");
7462 }
7463
7464 #[test]
7465 fn file_ref_store_expire_reflog_file_rewrites_and_dry_runs() {
7466 let git_dir = temp_git_dir();
7467 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7468 let first = ObjectId::from_hex(
7469 ObjectFormat::Sha1,
7470 "ce013625030ba8dba906f756967f9e9ca394464a",
7471 )
7472 .expect("test operation should succeed");
7473 let second = ObjectId::from_hex(
7474 ObjectFormat::Sha1,
7475 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7476 )
7477 .expect("test operation should succeed");
7478 store
7479 .write_reflog(
7480 "refs/heads/main",
7481 &[
7482 reflog_entry(&first, 10, "old"),
7483 reflog_entry(&second, 100, "new"),
7484 ],
7485 )
7486 .expect("test operation should succeed");
7487
7488 let would_remove = store
7490 .expire_reflog_file("refs/heads/main", 50, None, false, |_| true)
7491 .expect("test operation should succeed");
7492 assert_eq!(would_remove, 1);
7493 assert_eq!(
7494 store
7495 .read_reflog("refs/heads/main")
7496 .expect("test operation should succeed")
7497 .len(),
7498 2
7499 );
7500
7501 let removed = store
7503 .expire_reflog_file("refs/heads/main", 50, None, true, |_| true)
7504 .expect("test operation should succeed");
7505 assert_eq!(removed, 1);
7506 let log = store
7507 .read_reflog("refs/heads/main")
7508 .expect("test operation should succeed");
7509 assert_eq!(log.len(), 1);
7510 assert_eq!(log[0].new_oid, second);
7511 assert!(
7512 !git_dir
7513 .join("logs")
7514 .join("refs")
7515 .join("heads")
7516 .join("main.lock")
7517 .exists()
7518 );
7519 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7520 }
7521
7522 #[test]
7523 fn file_ref_transaction_commits_all_refs_atomically() {
7524 let git_dir = temp_git_dir();
7525 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7526 let main_oid = ObjectId::from_hex(
7527 ObjectFormat::Sha1,
7528 "ce013625030ba8dba906f756967f9e9ca394464a",
7529 )
7530 .expect("test operation should succeed");
7531 let topic_oid = ObjectId::from_hex(
7532 ObjectFormat::Sha1,
7533 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7534 )
7535 .expect("test operation should succeed");
7536 let tag_oid = ObjectId::from_hex(
7537 ObjectFormat::Sha1,
7538 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
7539 )
7540 .expect("test operation should succeed");
7541 let mut tx = store.transaction();
7542 tx.update(RefUpdate {
7543 name: "refs/heads/main".into(),
7544 expected: None,
7545 new: RefTarget::Direct(main_oid),
7546 reflog: Some(reflog_entry(&main_oid, 0, "create main")),
7547 });
7548 tx.update(RefUpdate {
7549 name: "refs/heads/topic".into(),
7550 expected: None,
7551 new: RefTarget::Direct(topic_oid),
7552 reflog: None,
7553 });
7554 tx.update(RefUpdate {
7555 name: "refs/tags/v1.0".into(),
7556 expected: None,
7557 new: RefTarget::Direct(tag_oid),
7558 reflog: None,
7559 });
7560 tx.commit().expect("test operation should succeed");
7561
7562 assert_eq!(
7563 store
7564 .read_ref("refs/heads/main")
7565 .expect("test operation should succeed"),
7566 Some(RefTarget::Direct(main_oid))
7567 );
7568 assert_eq!(
7569 store
7570 .read_ref("refs/heads/topic")
7571 .expect("test operation should succeed"),
7572 Some(RefTarget::Direct(topic_oid))
7573 );
7574 assert_eq!(
7575 store
7576 .read_ref("refs/tags/v1.0")
7577 .expect("test operation should succeed"),
7578 Some(RefTarget::Direct(tag_oid))
7579 );
7580 let main_log = store
7581 .read_reflog("refs/heads/main")
7582 .expect("test operation should succeed");
7583 assert_eq!(main_log.len(), 1);
7584 assert_eq!(main_log[0].new_oid, main_oid);
7585 assert!(
7587 !git_dir
7588 .join("refs")
7589 .join("heads")
7590 .join("main.lock")
7591 .exists()
7592 );
7593 assert!(
7594 !git_dir
7595 .join("refs")
7596 .join("heads")
7597 .join("topic.lock")
7598 .exists()
7599 );
7600 assert!(!git_dir.join("refs").join("tags").join("v1.0.lock").exists());
7601 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7602 }
7603
7604 #[test]
7605 fn file_ref_transaction_rolls_back_all_refs_on_expected_mismatch() {
7606 let git_dir = temp_git_dir();
7607 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7608 let old_topic = ObjectId::from_hex(
7609 ObjectFormat::Sha1,
7610 "ce013625030ba8dba906f756967f9e9ca394464a",
7611 )
7612 .expect("test operation should succeed");
7613 let new_main = ObjectId::from_hex(
7614 ObjectFormat::Sha1,
7615 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7616 )
7617 .expect("test operation should succeed");
7618 let new_tag = ObjectId::from_hex(
7619 ObjectFormat::Sha1,
7620 "18f002b4484b838b205a48b1e9e6763ba5e3a607",
7621 )
7622 .expect("test operation should succeed");
7623 let wrong_expected = ObjectId::from_hex(
7624 ObjectFormat::Sha1,
7625 "0000000000000000000000000000000000000001",
7626 )
7627 .expect("test operation should succeed");
7628
7629 let mut seed = store.transaction();
7632 seed.update(RefUpdate {
7633 name: "refs/heads/topic".into(),
7634 expected: None,
7635 new: RefTarget::Direct(old_topic.clone()),
7636 reflog: None,
7637 });
7638 seed.commit().expect("test operation should succeed");
7639
7640 let mut tx = store.transaction();
7641 tx.update(RefUpdate {
7643 name: "refs/heads/main".into(),
7644 expected: None,
7645 new: RefTarget::Direct(new_main.clone()),
7646 reflog: Some(reflog_entry(&new_main, 0, "create main")),
7647 });
7648 tx.update(RefUpdate {
7650 name: "refs/heads/topic".into(),
7651 expected: Some(RefTarget::Direct(wrong_expected)),
7652 new: RefTarget::Direct(new_main.clone()),
7653 reflog: None,
7654 });
7655 tx.update(RefUpdate {
7657 name: "refs/tags/v1.0".into(),
7658 expected: None,
7659 new: RefTarget::Direct(new_tag),
7660 reflog: None,
7661 });
7662 let result = tx.commit();
7663 assert!(result.is_err());
7664
7665 assert_eq!(
7668 store
7669 .read_ref("refs/heads/main")
7670 .expect("test operation should succeed"),
7671 None
7672 );
7673 assert_eq!(
7674 store
7675 .read_ref("refs/heads/topic")
7676 .expect("test operation should succeed"),
7677 Some(RefTarget::Direct(old_topic))
7678 );
7679 assert_eq!(
7680 store
7681 .read_ref("refs/tags/v1.0")
7682 .expect("test operation should succeed"),
7683 None
7684 );
7685 assert!(
7686 store
7687 .read_reflog("refs/heads/main")
7688 .expect("test operation should succeed")
7689 .is_empty()
7690 );
7691
7692 assert!(
7694 !git_dir
7695 .join("refs")
7696 .join("heads")
7697 .join("main.lock")
7698 .exists()
7699 );
7700 assert!(
7701 !git_dir
7702 .join("refs")
7703 .join("heads")
7704 .join("topic.lock")
7705 .exists()
7706 );
7707 assert!(!git_dir.join("refs").join("tags").join("v1.0.lock").exists());
7708 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7709 }
7710
7711 #[test]
7712 fn file_ref_transaction_mixes_update_and_delete() {
7713 let git_dir = temp_git_dir();
7714 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7715 let old_main = ObjectId::from_hex(
7716 ObjectFormat::Sha1,
7717 "ce013625030ba8dba906f756967f9e9ca394464a",
7718 )
7719 .expect("test operation should succeed");
7720 let new_topic = ObjectId::from_hex(
7721 ObjectFormat::Sha1,
7722 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7723 )
7724 .expect("test operation should succeed");
7725 let mut seed = store.transaction();
7726 seed.update(RefUpdate {
7727 name: "refs/heads/main".into(),
7728 expected: None,
7729 new: RefTarget::Direct(old_main),
7730 reflog: None,
7731 });
7732 seed.commit().expect("test operation should succeed");
7733
7734 let mut tx = store.transaction();
7735 tx.update(RefUpdate {
7736 name: "refs/heads/topic".into(),
7737 expected: None,
7738 new: RefTarget::Direct(new_topic),
7739 reflog: None,
7740 });
7741 tx.delete_with_precondition(
7742 "refs/heads/main",
7743 RefDeletePrecondition::Direct(Some(old_main)),
7744 None,
7745 );
7746 tx.commit().expect("test operation should succeed");
7747
7748 assert_eq!(
7749 store
7750 .read_ref("refs/heads/main")
7751 .expect("test operation should succeed"),
7752 None
7753 );
7754 assert_eq!(
7755 store
7756 .read_ref("refs/heads/topic")
7757 .expect("test operation should succeed"),
7758 Some(RefTarget::Direct(new_topic))
7759 );
7760 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7761 }
7762
7763 #[test]
7764 fn file_ref_transaction_rejects_deleted_descendant_parent_create() {
7765 let git_dir = temp_git_dir();
7766 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7767 let old_conflict = ObjectId::from_hex(
7768 ObjectFormat::Sha1,
7769 "ce013625030ba8dba906f756967f9e9ca394464a",
7770 )
7771 .expect("test operation should succeed");
7772 let new_parent = ObjectId::from_hex(
7773 ObjectFormat::Sha1,
7774 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7775 )
7776 .expect("test operation should succeed");
7777 let mut seed = store.transaction();
7778 seed.update(RefUpdate {
7779 name: "refs/heads/branch/conflict".into(),
7780 expected: None,
7781 new: RefTarget::Direct(old_conflict),
7782 reflog: None,
7783 });
7784 seed.commit().expect("test operation should succeed");
7785
7786 let mut tx = store.transaction();
7787 tx.delete_with_precondition(
7788 "refs/heads/branch/conflict",
7789 RefDeletePrecondition::Direct(Some(old_conflict)),
7790 None,
7791 );
7792 tx.update(RefUpdate {
7793 name: "refs/heads/branch".into(),
7794 expected: None,
7795 new: RefTarget::Direct(new_parent),
7796 reflog: None,
7797 });
7798 let err = tx
7799 .commit()
7800 .expect_err("D/F-conflicting delete plus create must fail");
7801 assert_eq!(
7802 err.to_string(),
7803 "transaction failed: cannot lock ref 'refs/heads/branch': 'refs/heads/branch/conflict' exists; cannot create 'refs/heads/branch'"
7804 );
7805
7806 assert_eq!(
7807 store
7808 .read_ref("refs/heads/branch/conflict")
7809 .expect("test operation should succeed"),
7810 Some(RefTarget::Direct(old_conflict))
7811 );
7812 assert_eq!(
7813 store
7814 .read_ref("refs/heads/branch")
7815 .expect("test operation should succeed"),
7816 None
7817 );
7818 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7819 }
7820
7821 #[test]
7822 fn file_ref_transaction_stale_delete_rolls_back_update() {
7823 let git_dir = temp_git_dir();
7824 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7825 let old_oid = ObjectId::from_hex(
7826 ObjectFormat::Sha1,
7827 "ce013625030ba8dba906f756967f9e9ca394464a",
7828 )
7829 .expect("test operation should succeed");
7830 let new_oid = ObjectId::from_hex(
7831 ObjectFormat::Sha1,
7832 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7833 )
7834 .expect("test operation should succeed");
7835 let mut seed = store.transaction();
7836 for name in ["refs/heads/main", "refs/heads/topic"] {
7837 seed.update(RefUpdate {
7838 name: name.into(),
7839 expected: None,
7840 new: RefTarget::Direct(old_oid),
7841 reflog: None,
7842 });
7843 }
7844 seed.commit().expect("test operation should succeed");
7845
7846 let mut tx = store.transaction();
7847 tx.update(RefUpdate {
7848 name: "refs/heads/topic".into(),
7849 expected: None,
7850 new: RefTarget::Direct(new_oid),
7851 reflog: None,
7852 });
7853 tx.delete_with_precondition(
7854 "refs/heads/main",
7855 RefDeletePrecondition::Direct(Some(new_oid)),
7856 None,
7857 );
7858 let err = tx.commit().expect_err("stale delete must abort");
7859 assert!(err.to_string().contains("expected ref refs/heads/main"));
7860
7861 assert_eq!(
7862 store
7863 .read_ref("refs/heads/main")
7864 .expect("test operation should succeed"),
7865 Some(RefTarget::Direct(old_oid))
7866 );
7867 assert_eq!(
7868 store
7869 .read_ref("refs/heads/topic")
7870 .expect("test operation should succeed"),
7871 Some(RefTarget::Direct(old_oid))
7872 );
7873 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7874 }
7875
7876 #[test]
7877 fn file_ref_transaction_rejects_duplicate_mixed_ref() {
7878 let git_dir = temp_git_dir();
7879 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7880 let oid = ObjectId::from_hex(
7881 ObjectFormat::Sha1,
7882 "ce013625030ba8dba906f756967f9e9ca394464a",
7883 )
7884 .expect("test operation should succeed");
7885 let mut tx = store.transaction();
7886 tx.update(RefUpdate {
7887 name: "refs/heads/main".into(),
7888 expected: None,
7889 new: RefTarget::Direct(oid),
7890 reflog: None,
7891 });
7892 tx.delete_with_precondition("refs/heads/main", RefDeletePrecondition::Any, None);
7893
7894 let err = tx.commit().expect_err("duplicate ref must fail");
7895 assert!(err.to_string().contains("refs/heads/main"));
7896 assert_eq!(
7897 store
7898 .read_ref("refs/heads/main")
7899 .expect("test operation should succeed"),
7900 None
7901 );
7902 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7903 }
7904
7905 #[test]
7906 fn file_ref_transaction_deletes_symbolic_ref_with_immediate_expectation() {
7907 let git_dir = temp_git_dir();
7908 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7909 let oid = ObjectId::from_hex(
7910 ObjectFormat::Sha1,
7911 "ce013625030ba8dba906f756967f9e9ca394464a",
7912 )
7913 .expect("test operation should succeed");
7914 let mut seed = store.transaction();
7915 seed.update(RefUpdate {
7916 name: "refs/heads/main".into(),
7917 expected: None,
7918 new: RefTarget::Direct(oid),
7919 reflog: None,
7920 });
7921 seed.update(RefUpdate {
7922 name: "refs/aliases/main".into(),
7923 expected: None,
7924 new: RefTarget::Symbolic("refs/heads/main".into()),
7925 reflog: None,
7926 });
7927 seed.commit().expect("test operation should succeed");
7928
7929 let mut tx = store.transaction();
7930 tx.delete_with_precondition(
7931 "refs/aliases/main",
7932 RefDeletePrecondition::Immediate(RefTarget::Symbolic("refs/heads/main".into())),
7933 None,
7934 );
7935 tx.commit().expect("test operation should succeed");
7936
7937 assert_eq!(
7938 store
7939 .read_ref("refs/aliases/main")
7940 .expect("test operation should succeed"),
7941 None
7942 );
7943 assert_eq!(
7944 store
7945 .read_ref("refs/heads/main")
7946 .expect("test operation should succeed"),
7947 Some(RefTarget::Direct(oid))
7948 );
7949 fs::remove_dir_all(git_dir).expect("test operation should succeed");
7950 }
7951
7952 #[test]
7953 fn file_ref_transaction_rolls_back_delete_after_late_write_failure() {
7954 let git_dir = temp_git_dir();
7955 let store = FileRefStore::new(&git_dir, ObjectFormat::Sha1);
7956 let old_oid = ObjectId::from_hex(
7957 ObjectFormat::Sha1,
7958 "ce013625030ba8dba906f756967f9e9ca394464a",
7959 )
7960 .expect("test operation should succeed");
7961 let new_oid = ObjectId::from_hex(
7962 ObjectFormat::Sha1,
7963 "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
7964 )
7965 .expect("test operation should succeed");
7966 let mut seed = store.transaction();
7967 for name in ["refs/heads/main", "refs/heads/topic"] {
7968 seed.update(RefUpdate {
7969 name: name.into(),
7970 expected: None,
7971 new: RefTarget::Direct(old_oid),
7972 reflog: None,
7973 });
7974 }
7975 seed.commit().expect("test operation should succeed");
7976
7977 set_fail_loose_commit_action_for_test(Some(1));
7978 let mut tx = store.transaction();
7979 tx.delete_with_precondition(
7980 "refs/heads/main",
7981 RefDeletePrecondition::Direct(Some(old_oid)),
7982 None,
7983 );
7984 tx.update(RefUpdate {
7985 name: "refs/heads/topic".into(),
7986 expected: None,
7987 new: RefTarget::Direct(new_oid),
7988 reflog: None,
7989 });
7990 let err = tx.commit().expect_err("injected failure must abort");
7991 assert!(
7992 err.to_string()
7993 .contains("injected loose ref transaction failure")
7994 );
7995
7996 assert_eq!(
7997 store
7998 .read_ref("refs/heads/main")
7999 .expect("test operation should succeed"),
8000 Some(RefTarget::Direct(old_oid))
8001 );
8002 assert_eq!(
8003 store
8004 .read_ref("refs/heads/topic")
8005 .expect("test operation should succeed"),
8006 Some(RefTarget::Direct(old_oid))
8007 );
8008 assert!(
8009 !git_dir
8010 .join("refs")
8011 .join("heads")
8012 .join("main.lock")
8013 .exists()
8014 );
8015 assert!(
8016 !git_dir
8017 .join("refs")
8018 .join("heads")
8019 .join("topic.lock")
8020 .exists()
8021 );
8022 fs::remove_dir_all(git_dir).expect("test operation should succeed");
8023 }
8024
8025 fn temp_git_dir() -> PathBuf {
8026 let path = std::env::temp_dir().join(format!(
8027 "sley-refs-{}-{}",
8028 std::process::id(),
8029 TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
8030 ));
8031 fs::create_dir_all(&path).expect("test operation should succeed");
8032 path
8033 }
8034
8035 fn zero_oid(format: ObjectFormat) -> Result<ObjectId> {
8036 Ok(ObjectId::null(format))
8037 }
8038
8039 fn write_reftable_config(git_dir: &Path) {
8040 fs::write(
8041 git_dir.join("config"),
8042 b"[core]\n\trepositoryformatversion = 1\n[extensions]\n\trefStorage = reftable\n",
8043 )
8044 .expect("test operation should succeed");
8045 }
8046
8047 fn write_reftable_stack(
8048 git_dir: &Path,
8049 tables: &[(&str, Vec<sley_formats::ReftableRefRecord>)],
8050 ) {
8051 let reftable_dir = git_dir.join("reftable");
8052 fs::create_dir_all(&reftable_dir).expect("test operation should succeed");
8053 let mut list = String::new();
8054 for (idx, (name, refs)) in tables.iter().enumerate() {
8055 let update_index = (idx + 1) as u64;
8056 let bytes = sley_formats::Reftable::write_ref_only(
8057 ObjectFormat::Sha1,
8058 update_index,
8059 update_index,
8060 refs,
8061 )
8062 .expect("test operation should succeed");
8063 fs::write(reftable_dir.join(name), bytes).expect("test operation should succeed");
8064 list.push_str(name);
8065 list.push('\n');
8066 }
8067 fs::write(reftable_dir.join("tables.list"), list).expect("test operation should succeed");
8068 }
8069}