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