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