pub struct References<'a> { /* private fields */ }Expand description
A reference store using the opened Repository’s configured backend.
Reftable operations read owned bounded stack snapshots. Each public read takes a fresh
snapshot; separate reads or symbolic hops are not a shared snapshot. Use
super::reftable::Snapshot to retain a complete generation. Reads make one attempt and
report missing tables during concurrent compaction; callers decide whether to retry.
Self::with_reftable_limits selects per-stack budgets. Binary logs preserve arbitrary
fields through the codec; Self::reflog interprets signed timestamps and removes one trailing
message newline. Self::imported_reflog preserves files bytes and binary fields with bounded
retention, recoverable roots and wider numeric interpretation. Other uppercase pseudorefs are
preserved by the codec and compaction but remain outside RefName’s operation namespace.
Reftable writes hold common/private tables.list.lock files in path order, validate every
edit and result budget, and publish one immutable table per affected stack. Each stack’s
references and logs become visible together. A linked-worktree edit can span two stacks:
publication follows path order and failures report completed per-reference/per-log effects.
No automatic compaction, expiry, fsync or retry occurs. Failed list publication may leave an
unlisted immutable table; readers ignore it. Explicit super::reftable::compact preserves
live logs and reports obsolete-file cleanup failures. Owned locks use best-effort cleanup;
termination can leave stale locks requiring caller-authorized recovery.
The following storage details describe the files backend.
Supports SHA-1 and SHA-256 references on Unix and Windows local filesystems. Repository
configuration selects the format; mixed-format targets and expectations fail before locks are
acquired. HEAD, refs/bisect/, refs/rewritten/, and refs/worktree/ use the current
worktree’s Git directory; other refs/ names and packed refs use the common directory.
Cross-worktree aliases and other pseudorefs are not supported. Filesystem symlinks within these
paths are rejected, including legacy symlink HEADs.
Operations are synchronous. Reads are live, not a snapshot across multiple refs or symbolic
hops. Loose files take precedence, including malformed loose files (no fallback on errors).
Packed lookup validates the whole file and allocates proportional to its size; there is no
configurable byte limit. Handles from one Repository share the last parse, reused while
the file keeps its device, inode, size and timestamps. Peel records are validated but never
returned as resolution.
Writes require a trusted repository on a local filesystem with exclusive file creation and
atomic rename semantics. Cooperating writers must honor Git’s .lock protocol; adversarial
path replacement, network filesystems, and changing repository configuration/layout while a
handle is in use are outside this contract. Windows uses UTF-8 names and rejects Win32 device
names, invalid filename characters, and trailing-dot components. Unix retains byte names.
Case and Unicode aliases inherit the volume’s behavior; no portable alias mapping is promised.
On Windows lock handles exclude delete sharing until cleanup; publication uses a separate
same-directory temporary file. Cleanup closes the owned handle before removing its path.
Unix cleanup checks device/inode identity and leaves replacement locks untouched.
Permissions follow ordinary file creation and the process umask on Unix; Windows inherits
directory ACLs. Existing destination ACLs and crash durability are not preserved guarantees.
use girt::refs::{RefName, Target};
use girt::{InitKind, ObjectFormat, Repository};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r.git"),
InitKind::Bare,
)?;
let refs = repo.references()?;
let head = RefName::HEAD;
assert_eq!(
refs.read(&head)?,
Some(Target::Symbolic(RefName::new("refs/heads/main")?))
);
assert!(refs.list()?.is_empty()); // HEAD is not one of the enumerated refs/ names.
Implementations§
Source§impl References<'_>
impl References<'_>
Sourcepub fn list(&self) -> Result<Vec<Reference>, ReferenceError>
pub fn list(&self) -> Result<Vec<Reference>, ReferenceError>
Lists all refs/ names in bytewise name order, without resolving symbolic refs.
Reftable merges bounded common/private stack snapshots using References’s contract.
The loose/packed traversal details below describe the files backend.
Includes branches, tags, other shared namespaces, and the current worktree’s private
refs. Excludes HEAD, other pseudorefs, other worktrees’ private refs, dot-prefixed entries
and .lock entries. Empty directories contribute no refs. Loose targets shadow packed
targets, even if symbolic; a malformed loose target fails instead of falling back.
Reads and validates packed-refs once, then walks loose files without locks. The owned result is stable after return but is not a point-in-time snapshot: concurrent creation or deletion may be included or missed, and values can come from different times. A loose file that disappears during traversal is ignored; a previously read packed value can remain in the result. Callers must use an expected value when acting on these observations. Memory use is proportional to packed data, discovered refs, and pending directories; there is no configurable resource limit.
§Errors
Reports I/O errors, invalid names or targets, conflicting effective names, filesystem symlinks and non-regular files, or malformed/unsupported packed data. Returns no partial list on error and does not modify storage.
use girt::refs::{Expected, RefEdit, RefName, Reflog, Target};
use girt::{Commit, CommitFields, InitKind, ObjectFormat, Repository, Signature, Tree};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r.git"),
InitKind::Bare,
)?;
let loose = repo.loose_objects();
let person = Signature {
name: b"A. Writer".to_vec(),
email: b"a@example.com".to_vec(),
seconds: 1_700_000_000,
offset_minutes: 0,
};
let tree = loose.write_tree(&Tree::new(ObjectFormat::Sha1, vec![])?)?;
let id = loose.write_commit(&Commit::new(CommitFields {
tree,
parents: vec![],
author: person.clone(),
committer: person.clone(),
extra_headers: vec![],
message: b"Initial snapshot\n".to_vec(),
})?)?;
let refs = repo.references()?;
let main = RefName::new("refs/heads/main")?;
let edit = RefEdit::new(
main.clone(),
Some(Target::Direct(id)),
Expected::Absent,
Reflog::Append {
committer: person,
message: b"commit (initial)".to_vec(),
},
);
refs.transaction(&[edit])?;
let listed = refs.list()?;
assert_eq!(listed.len(), 1); // HEAD is a pseudoref, excluded from list().
assert_eq!(listed[0].name, main);
assert_eq!(listed[0].target, Target::Direct(id));Examples found in repository?
6fn main() -> Result<(), Box<dyn std::error::Error>> {
7 let directory = tempfile::tempdir()?;
8 let repo = Repository::init(
9 girt::ObjectFormat::Sha1,
10 directory.path().join("repo"),
11 InitKind::Bare,
12 )?;
13
14 let objects = repo.loose_objects();
15 let mut editor = girt::tree::TreeEditor::new(&objects);
16 editor.set(b"README.md", objects.write_blob(b"# Project\n")?)?;
17 let tree = editor.write()?;
18
19 let author = Signature::now(b"A. Writer", b"writer@example.com")?;
20 let builder = Commit::builder(tree, author.clone(), b"First snapshot\n");
21 let first = objects.write_commit(&builder.build()?)?;
22
23 let refs = repo.references()?;
24 let head = RefName::HEAD;
25 let branch = refs.update_resolved_without_reflog(&head, first, Expected::Absent)?;
26
27 let second_commit = Commit::builder(tree, author, b"Advance branch\n")
28 .parent(first)
29 .build()?;
30 let second = objects.write_commit(&second_commit)?;
31 refs.update_resolved_without_reflog(&head, second, Expected::Value(Target::Direct(first)))?;
32
33 assert_eq!(refs.resolve_with(&head, 8)?.id, Some(second));
34 assert_eq!(refs.read(&head)?, Some(Target::Symbolic(branch.clone())));
35
36 let branches = refs.list_namespace(&RefName::new(b"refs/heads")?)?;
37 assert_eq!(branches[0].name, branch);
38
39 // Delete the observed terminal branch, keeping HEAD symbolic and now unborn.
40 refs.delete_resolved_without_reflog(&head, Expected::Value(branches[0].target.clone()))?;
41 assert_eq!(refs.read(&head)?, Some(Target::Symbolic(branch)));
42 assert_eq!(refs.resolve_with(&head, 8)?.id, None);
43 assert!(refs.list()?.is_empty());
44
45 println!("Published {first}, advanced to {second}, then deleted main without reflogs");
46 Ok(())
47}Sourcepub fn list_controlled(
&self,
max_entries: usize,
max_bytes: usize,
cancel: &AtomicBool,
) -> Result<Vec<Reference>, ReferenceError>
pub fn list_controlled( &self, max_entries: usize, max_bytes: usize, cancel: &AtomicBool, ) -> Result<Vec<Reference>, ReferenceError>
Lists references with aggregate files-backend byte and traversal limits.
The entry limit covers packed names and loose directory entries before they are retained;
the byte limit covers packed-refs and loose file contents. Reftable uses this handle’s
super::reftable::StackLimits for encoded/decoded data and applies the entry limit to
its bounded merged output. Cancellation is checked between files and directory entries.
A failed inventory returns no partial list and must not justify removing objects.
§Errors
Returns ReferenceError::Limit or ReferenceError::Cancelled at those boundaries,
alongside the malformed, unsupported and I/O failures of Self::list.
use girt::refs::{Expected, RefName, Target};
use girt::{InitKind, ObjectFormat, Repository};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r.git"),
InitKind::Bare,
)?;
let id = repo.loose_objects().write_blob(b"tagged manifest")?;
let refs = repo.references()?;
let name = RefName::new("refs/tags/one")?;
refs.update_without_reflog(&name, Target::Direct(id), Expected::Absent)?;
let cancel = std::sync::atomic::AtomicBool::new(false);
assert!(matches!(
refs.list_controlled(0, 4096, &cancel),
Err(girt::refs::ReferenceError::Limit(_))
));
assert_eq!(refs.list_controlled(100, 4096, &cancel)?.len(), 1);Sourcepub fn list_observations_controlled(
&self,
max_entries: usize,
max_bytes: usize,
cancel: &AtomicBool,
) -> Result<Vec<ReferenceObservation>, ReferenceError>
pub fn list_observations_controlled( &self, max_entries: usize, max_bytes: usize, cancel: &AtomicBool, ) -> Result<Vec<ReferenceObservation>, ReferenceError>
Lists stored targets with their captured peeled hints under inventory limits.
Ordering, namespace selection, loose shadowing and live-read guarantees match
Self::list; limits and cancellation match Self::list_controlled. Each hint comes
from the same effective record as its target, without a per-name reread. A loose target
replaces the complete packed observation and clears its hint, even when IDs are equal.
Hints do not establish object existence or type; see ReferenceObservation.
§Errors
Returns the same errors as Self::list_controlled, without a partial inventory.
use std::sync::atomic::AtomicBool;
use girt::{InitKind, ObjectFormat, Repository};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("repo"),
InitKind::Bare,
)?;
let refs = repo.references()?;
let observations =
refs.list_observations_controlled(100, 1024 * 1024, &AtomicBool::new(false))?;
assert!(observations.is_empty()); // HEAD is not a refs/ inventory entry.
Sourcepub fn list_namespace(
&self,
namespace: &RefName,
) -> Result<Vec<Reference>, ReferenceError>
pub fn list_namespace( &self, namespace: &RefName, ) -> Result<Vec<Reference>, ReferenceError>
Lists a full name and its descendants, using / as the namespace boundary.
Use refs/heads for branches and refs/tags for tags. refs/heads/topic includes that
exact ref or its descendants, but not refs/heads/topic-two. Missing namespaces return
an empty list. Only selected loose paths are read; packed-refs is always fully validated.
Ordering, symbolic behavior, private namespaces and live-read guarantees match
Self::list.
§Errors
Reports errors from Self::list within the selected loose namespace or anywhere in
packed-refs. HEAD is unsupported here; use Self::read for pseudorefs.
use girt::refs::{Expected, RefName, Target};
use girt::{InitKind, ObjectId, Repository};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
girt::ObjectFormat::Sha1,
directory.path().join("repo"),
InitKind::Bare,
)?;
let refs = repo.references()?;
let tag = RefName::new(b"refs/tags/example")?;
let value = Target::Direct(ObjectId::Sha1([1; 20]));
// Reference storage does not require the target object to exist.
refs.update_without_reflog(&tag, value.clone(), Expected::Absent)?;
let tags = refs.list_namespace(&RefName::new(b"refs/tags")?)?;
assert_eq!(tags[0].name, tag);
refs.delete_without_reflog(&tag, Expected::Value(tags[0].target.clone()))?;
assert!(refs.list()?.is_empty());Examples found in repository?
6fn main() -> Result<(), Box<dyn std::error::Error>> {
7 let directory = tempfile::tempdir()?;
8 let repo = Repository::init(
9 girt::ObjectFormat::Sha1,
10 directory.path().join("repo"),
11 InitKind::Bare,
12 )?;
13
14 let objects = repo.loose_objects();
15 let mut editor = girt::tree::TreeEditor::new(&objects);
16 editor.set(b"README.md", objects.write_blob(b"# Project\n")?)?;
17 let tree = editor.write()?;
18
19 let author = Signature::now(b"A. Writer", b"writer@example.com")?;
20 let builder = Commit::builder(tree, author.clone(), b"First snapshot\n");
21 let first = objects.write_commit(&builder.build()?)?;
22
23 let refs = repo.references()?;
24 let head = RefName::HEAD;
25 let branch = refs.update_resolved_without_reflog(&head, first, Expected::Absent)?;
26
27 let second_commit = Commit::builder(tree, author, b"Advance branch\n")
28 .parent(first)
29 .build()?;
30 let second = objects.write_commit(&second_commit)?;
31 refs.update_resolved_without_reflog(&head, second, Expected::Value(Target::Direct(first)))?;
32
33 assert_eq!(refs.resolve_with(&head, 8)?.id, Some(second));
34 assert_eq!(refs.read(&head)?, Some(Target::Symbolic(branch.clone())));
35
36 let branches = refs.list_namespace(&RefName::new(b"refs/heads")?)?;
37 assert_eq!(branches[0].name, branch);
38
39 // Delete the observed terminal branch, keeping HEAD symbolic and now unborn.
40 refs.delete_resolved_without_reflog(&head, Expected::Value(branches[0].target.clone()))?;
41 assert_eq!(refs.read(&head)?, Some(Target::Symbolic(branch)));
42 assert_eq!(refs.resolve_with(&head, 8)?.id, None);
43 assert!(refs.list()?.is_empty());
44
45 println!("Published {first}, advanced to {second}, then deleted main without reflogs");
46 Ok(())
47}Sourcepub fn list_namespace_observations(
&self,
namespace: &RefName,
) -> Result<Vec<ReferenceObservation>, ReferenceError>
pub fn list_namespace_observations( &self, namespace: &RefName, ) -> Result<Vec<ReferenceObservation>, ReferenceError>
Lists a namespace like Self::list_namespace, keeping packed peeled hints.
A hint is the peeled target recorded in packed-refs for an annotated tag, so callers can avoid reading the tag object. Hints are unverified; loose references have none.
§Errors
Returns the errors of Self::list_namespace.
use girt::refs::{Expected, RefName, Target};
use girt::{InitKind, ObjectFormat, Repository};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r.git"),
InitKind::Bare,
)?;
let id = repo.loose_objects().write_blob(b"tagged manifest")?;
let refs = repo.references()?;
let name = RefName::new("refs/tags/one")?;
refs.update_without_reflog(&name, Target::Direct(id), Expected::Absent)?;
let tags = refs.list_namespace_observations(&RefName::new("refs/tags")?)?;
assert_eq!(tags.len(), 1);
assert_eq!(tags[0].name, name);Sourcepub fn list_prefix(
&self,
prefix: impl AsRef<[u8]>,
) -> Result<Vec<Reference>, ReferenceError>
pub fn list_prefix( &self, prefix: impl AsRef<[u8]>, ) -> Result<Vec<Reference>, ReferenceError>
Lists references whose full names start with the bytes of prefix.
Unlike Self::list_namespace, the prefix need not end at a /: refs/heads/fe selects
refs/heads/feature and refs/heads/fe/x, and refs/heads/ selects every branch. Loose
reads are confined to the namespace up to the last / in prefix; a prefix without one
below refs/ enumerates every reference before filtering. HEAD is never listed. Ordering,
symbolic behavior and live-read guarantees match Self::list.
§Errors
Reports the errors of Self::list_namespace for the enumerated namespace.
use girt::refs::{RefEdit, RefName, Reflog};
use girt::{InitKind, ObjectFormat, Repository};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r.git"),
InitKind::Bare,
)?;
let id = repo.loose_objects().write_blob(b"tip")?;
let refs = repo.references()?;
let edits = ["feature", "fe/x", "main"].map(|branch| {
Ok(RefEdit::create(
RefName::branch(branch)?,
id,
Reflog::Preserve,
))
});
refs.transaction(
&edits
.into_iter()
.collect::<Result<Vec<_>, girt::refs::InvalidRefName>>()?,
)?;
let names: Vec<_> = refs
.list_prefix("refs/heads/fe")?
.into_iter()
.map(|r| r.name)
.collect();
assert_eq!(
names,
[RefName::branch("fe/x")?, RefName::branch("feature")?]
);Sourcepub fn list_prefix_observations(
&self,
prefix: impl AsRef<[u8]>,
) -> Result<Vec<ReferenceObservation>, ReferenceError>
pub fn list_prefix_observations( &self, prefix: impl AsRef<[u8]>, ) -> Result<Vec<ReferenceObservation>, ReferenceError>
Lists references by byte prefix like Self::list_prefix, keeping packed peeled hints.
Hints follow Self::list_namespace_observations.
§Errors
Returns the errors of Self::list_prefix.
use girt::{InitKind, ObjectFormat, Repository};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r.git"),
InitKind::Bare,
)?;
assert!(
repo.references()?
.list_prefix_observations("refs/tags/v")?
.is_empty()
);Source§impl References<'_>
impl References<'_>
Sourcepub fn imported_reflog_names(
&self,
max_entries: usize,
cancel: &AtomicBool,
) -> Result<Vec<RefName>, ReferenceError>
pub fn imported_reflog_names( &self, max_entries: usize, cancel: &AtomicBool, ) -> Result<Vec<RefName>, ReferenceError>
Enumerates stored histories even when their references have been deleted.
The limit applies to all visited directory entries and decoded names. The result is a live observation, not an atomic snapshot with reference or object reads. A caller planning deletion must exclude writers and repeat discovery after acquiring its execution boundary.
§Errors
Reports malformed names, symlinks, non-file entries, I/O, cancellation, or an exhausted entry or reftable stack budget. No partial list is returned on failure.
Source§impl References<'_>
impl References<'_>
Sourcepub fn imported_reflog(
&self,
name: &RefName,
limits: ReflogLimits,
cancel: &AtomicBool,
) -> Result<Option<ImportedReflog>, ReferenceError>
pub fn imported_reflog( &self, name: &RefName, limits: ReflogLimits, cancel: &AtomicBool, ) -> Result<Option<ImportedReflog>, ReferenceError>
Reads bounded imported history for one name, without canonical append validation.
Returns None for an absent files log or a binary log with no live records. Routes shared
and per-worktree names like reference operations. Files preserve exact bytes; binary data
preserves decoded fields after tombstones and precedence. Binary stack budgets remain
separate from the selected-log budgets; both the snapshot and retained output can coexist.
§Errors
Opening/path failures and reftable snapshot failures return an error, never an empty root
set. Files I/O after opening and selected-log limits/cancellation are reported in the owned
result. Check ImportedReflog::is_complete before considering roots complete.
use girt::refs::{Expected, RefEdit, RefName, Reflog, Target};
use girt::{Commit, CommitFields, InitKind, ObjectFormat, Repository, Signature, Tree};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r.git"),
InitKind::Bare,
)?;
let loose = repo.loose_objects();
let person = Signature {
name: b"A. Writer".to_vec(),
email: b"a@example.com".to_vec(),
seconds: 1_700_000_000,
offset_minutes: 0,
};
let tree = loose.write_tree(&Tree::new(ObjectFormat::Sha1, vec![])?)?;
let id = loose.write_commit(&Commit::new(CommitFields {
tree,
parents: vec![],
author: person.clone(),
committer: person.clone(),
extra_headers: vec![],
message: b"Initial snapshot\n".to_vec(),
})?)?;
let refs = repo.references()?;
let main = RefName::new("refs/heads/main")?;
let edit = RefEdit::new(
main.clone(),
Some(Target::Direct(id)),
Expected::Absent,
Reflog::Append {
committer: person,
message: b"commit (initial)".to_vec(),
},
);
refs.transaction(&[edit])?;
let log = refs
.imported_reflog(
&main,
girt::refs::ReflogLimits::default(),
&std::sync::atomic::AtomicBool::new(false),
)?
.ok_or("reflog missing")?;
assert!(log.is_complete());
assert_eq!(log.records()[0].fields()?.new, id);Examples found in repository?
30fn probe(
31 format: ObjectFormat,
32 count: usize,
33 backend: Backend,
34) -> Result<(), Box<dyn std::error::Error>> {
35 let root = tempfile::tempdir()?;
36 let repo =
37 Repository::init_with_backend(format, root.path().join("repo"), InitKind::Bare, backend)?;
38
39 let tip = ObjectId::from_hex(format, &"12".repeat(format.digest_len()))?;
40 let record = format!(
41 "{} {tip} A <a@b> 1700000000 +0000\tfixture\n",
42 ObjectId::null(format)
43 );
44 let bytes = record.repeat(count);
45
46 if backend == Backend::Files {
47 std::fs::create_dir_all(repo.git_dir().join("logs"))?;
48 std::fs::write(repo.git_dir().join("logs/HEAD"), &bytes)?;
49 } else {
50 write_binary(&repo, tip, count)?;
51 }
52
53 let refs = repo.references()?;
54 let name = RefName::HEAD;
55 let cancel = AtomicBool::new(false);
56 let before = descriptors();
57 let retained = refs
58 .imported_reflog(&name, ReflogLimits::default(), &cancel)?
59 .unwrap();
60 let during = descriptors();
61 assert!(retained.is_complete());
62
63 let payload: usize = retained
64 .records()
65 .iter()
66 .map(|r| match r {
67 ImportedRecord::File { bytes, .. } => Ok(bytes.len()),
68 ImportedRecord::Reftable { value, .. } => Ok(value.name.len()
69 + value.email.len()
70 + value.message.len()
71 + 2 * format.digest_len()
72 + 18),
73 _ => Err("unsupported imported reflog record"),
74 })
75 .sum::<Result<usize, _>>()?;
76
77 drop(retained);
78 let after = descriptors();
79
80 let mut samples = Vec::new();
81 for _ in 0..30 {
82 let start = Instant::now();
83 let log = refs
84 .imported_reflog(&name, ReflogLimits::default(), &cancel)?
85 .unwrap();
86 assert!(log.is_complete());
87 assert_eq!(log.recoverable_roots().count(), count);
88 drop(log);
89 samples.push(start.elapsed().as_nanos());
90 }
91
92 samples.sort_unstable();
93 println!(
94 "backend={backend:?} format={format} records={count} payload={payload} min_ns={} median_ns={} max_ns={} descriptors={before:?}/{during:?}/{after:?}",
95 samples[0], samples[15], samples[29]
96 );
97
98 Ok(())
99}Source§impl References<'_>
impl References<'_>
Sourcepub fn read_observation_following_leaf_symlinks(
&self,
name: &RefName,
max_symlink_hops: usize,
max_bytes: usize,
) -> Result<Option<ReferenceObservation>, ReferenceError>
pub fn read_observation_following_leaf_symlinks( &self, name: &RefName, max_symlink_hops: usize, max_bytes: usize, ) -> Result<Option<ReferenceObservation>, ReferenceError>
Reads one reference while explicitly permitting final-file filesystem symlinks.
Relative link destinations are relative to their containing directory. Link text such as
refs/heads/main is a filesystem path, not Git’s legacy Git-directory-relative symbolic
reference syntax. Only ref: ... file contents produce a symbolic Target. Symlink
ancestors of the original name or any followed destination remain unsupported; destinations
outside the repository are otherwise permitted. This method never changes write/CAS policy.
A successful loose read retains the requested name and clears the packed peeled hint. Missing paths and followed links ending at a directory permit normal packed fallback. Malformed contents and other I/O errors do not. An original directory remains a conflict. Per-worktree names never fall back to packed storage. Observations are owned but live reads are not atomic across link hops; this is not a sandbox against adversarial path replacement.
max_symlink_hops bounds followed links, including cycles. max_bytes bounds aggregate
link destination bytes and file bytes, including packed fallback. Files are read with at
most one excess byte to detect exhaustion. Reftable uses Self::read_observation and
its configured stack limits instead; these filesystem limits do not apply there.
§Errors
Reports unsupported ancestors/nonregular files, I/O and malformed data, namespace conflicts,
and ReferenceError::Limit on exhausted byte or link budgets. No storage is modified.
use girt::refs::RefName;
use girt::{InitKind, ObjectFormat, Repository};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("repo"),
InitKind::Bare,
)?;
let observation = repo
.references()?
.read_observation_following_leaf_symlinks(&RefName::HEAD, 32, 64 * 1024)?;
assert!(observation.is_some());Source§impl References<'_>
impl References<'_>
Sourcepub fn pack(
&self,
objects: &Objects,
options: PackRefsOptions,
) -> Result<PackRefsReport, PackRefsError>
pub fn pack( &self, objects: &Objects, options: PackRefsOptions, ) -> Result<PackRefsReport, PackRefsError>
Moves loose direct references into packed-refs and removes the loose files.
Holds packed-refs.lock throughout. The replacement is sorted, records the fully peeled
target of every annotated tag, and is synced to disk with its directory before any loose
file is removed. An existing ^ line is kept when its objects can no longer be read. Each
loose file is then removed under its own lock, only if it still holds the packed value;
a locked or changed file is kept and reported. On non-Unix hosts no loose file is removed,
following the deferral of destructive maintenance there: no nonadmin sync makes the rename
durable first. Every packed name is then reported as retained. Readers observe either the
old state or loose files that shadow equal packed records.
Symbolic references, HEAD, per-worktree references and refs that are not direct stay
loose. Malformed loose files and references to missing objects are skipped, as Git skips
them. Existing packed records are kept, including those whose objects are missing.
§Errors
Returns ReferenceError::Unsupported for reftable. Returns ReferenceError::Locked
when packed-refs.lock remains held, and PackRefsError::Peel for a corrupt object.
All of these have no effects. PackRefsError::Unconfirmed reports a visible
replacement whose directory sync failed, with nothing pruned; PackRefsError::Prune
reports a removal failure after a durable replacement.
use girt::refs::PackRefsOptions;
use girt::{InitKind, ObjectFormat, PackLimits, Repository};
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r"),
InitKind::Bare,
)?;
let report = repo.references()?.pack(
&repo.objects_with(PackLimits::default())?,
PackRefsOptions::ALL,
)?;
assert!(report.rewritten);
assert_eq!(report.packed, 0);Source§impl References<'_>
impl References<'_>
Sourcepub fn has_reflog(&self, name: &RefName) -> Result<bool, ReferenceError>
pub fn has_reflog(&self, name: &RefName) -> Result<bool, ReferenceError>
Reports whether a reflog exists without reading its complete history.
Files storage rejects symlinks and non-regular log paths. Reftable reads a bounded snapshot; absence means no live log records. This is a live observation, not a lock or a promise that a subsequent append will succeed.
§Errors
Returns unsafe path, filesystem or reftable snapshot failures.
Sourcepub fn reflog(
&self,
name: &RefName,
) -> Result<Option<Vec<ReflogEntry>>, ReferenceError>
pub fn reflog( &self, name: &RefName, ) -> Result<Option<Vec<ReflogEntry>>, ReferenceError>
Reads a complete reflog in oldest-to-newest order, or returns None if absent.
Reftable uses a bounded snapshot, omits tombstones and strips one trailing message newline.
It returns None when no live log records remain. Unsigned timestamps beyond i64 fail
interpretation; use super::reftable::Snapshot for lossless access. The remaining
live-file details below describe the files backend.
Uses the same worktree routing as references. Reads are live and may see an incomplete
append by another writer. Prefer Self::imported_reflog for bounded recoverable reads.
Memory is proportional to the whole log; this reader does not stream records or expire
history. crate::Repository::expire_reflogs separately expires eligible files or
reftable records under caller-owned maintenance isolation on Unix.
§Errors
Reports filesystem, symlink, and malformed-record errors, including a truncated tail.
let log = refs.reflog(&main)?.ok_or("log missing")?;
assert!(log[0].old.is_null()); // Zero marks creation, not a stored empty object.
assert_eq!(log[0].new, id);
assert_eq!(log[0].committer.seconds, 1_700_000_000);
assert_eq!(log[0].message, b"commit (initial)");Examples found in repository?
5fn main() -> Result<(), Box<dyn std::error::Error>> {
6 let directory = tempfile::tempdir()?;
7 let repo = Repository::init(
8 girt::ObjectFormat::Sha256,
9 directory.path().join("repository"),
10 girt::InitKind::Bare,
11 )?;
12
13 let objects = repo.loose_objects();
14 let tree = objects.write_tree(&repo.new_tree(Vec::new())?)?;
15
16 let committer = Signature {
17 name: b"Example Writer".to_vec(),
18 email: b"writer@example.com".to_vec(),
19 seconds: 1700000000,
20 offset_minutes: 0,
21 };
22
23 let commit = Commit::new(CommitFields {
24 tree,
25 parents: Vec::new(),
26 author: committer.clone(),
27 committer: committer.clone(),
28 extra_headers: Vec::new(),
29 message: b"Initial commit\n".to_vec(),
30 })?;
31 let id = objects.write_commit(&commit)?;
32
33 let head = RefName::HEAD;
34 let tag = RefName::tag("initial")?;
35 let edits = [
36 {
37 let input = RefEdit::new(
38 head.clone(),
39 Some(girt::refs::Target::Direct(id)),
40 Expected::Absent,
41 Reflog::Append {
42 committer: committer.clone(),
43 message: b"initial publication".to_vec(),
44 },
45 );
46 input.with_dereference(girt::refs::Dereference::Follow)
47 },
48 RefEdit::create(
49 tag.clone(),
50 id,
51 Reflog::Append {
52 committer: committer.clone(),
53 message: b"mark initial".to_vec(),
54 },
55 ),
56 ];
57
58 let refs = repo.references()?;
59 match refs.transaction(&edits) {
60 Ok(outcomes) => println!("Published: {outcomes:?}"),
61 Err(TransactionError::Prepare { operation, source }) => {
62 eprintln!("No refs or logs changed; operation {operation:?}: {source}");
63 return Err(source.into());
64 }
65 Err(error @ TransactionError::Publish { .. }) => {
66 // Inspect per-ref and per-log outcomes before deciding whether/how to retry.
67 eprintln!("Partial publication: {error:?}");
68 return Err(error.into());
69 }
70 Err(error) => return Err(error.into()),
71 }
72
73 refs.transaction(&[RefEdit::delete(
74 tag.clone(),
75 id,
76 Reflog::Append {
77 committer,
78 message: b"remove temporary tag".to_vec(),
79 },
80 )])?;
81
82 assert_eq!(refs.reflog(&head)?.unwrap().len(), 1);
83 assert_eq!(refs.reflog(&tag)?.unwrap().len(), 2);
84 assert_eq!(refs.read(&tag)?, None);
85
86 // A retention ref may already exist at this value, but must never overwrite another value.
87 let keep = RefName::new("refs/jj/keep-example")?;
88 refs.transaction(&[RefEdit::new(
89 keep.clone(),
90 Some(girt::refs::Target::Direct(id)),
91 Expected::AbsentOr(girt::refs::Target::Direct(id)),
92 Reflog::Preserve,
93 )])?;
94 refs.transaction(&[RefEdit::new(keep, None, Expected::Exists, Reflog::Delete)])?;
95 Ok(())
96}Source§impl References<'_>
impl References<'_>
Sourcepub fn transaction_controlled(
&self,
edits: &[RefEdit],
cancel: &AtomicBool,
) -> Result<Vec<RefEditOutcome>, TransactionError>
pub fn transaction_controlled( &self, edits: &[RefEdit], cancel: &AtomicBool, ) -> Result<Vec<RefEditOutcome>, TransactionError>
Applies a conditional batch with cooperative cancellation at stack boundaries.
Reftable checks cancellation between opening/decoding tables and before each stack publication. Linked-worktree transactions can report already published common/private effects. Files-backend cancellation is checked only before the existing transaction starts.
§Errors
Returns TransactionError::Prepare before publication or TransactionError::Publish
with exact completed effects. Never retry a partial result without inspecting current state.
Sourcepub fn with_reftable_limits(self, limits: StackLimits) -> Self
pub fn with_reftable_limits(self, limits: StackLimits) -> Self
Selects reftable stack budgets for subsequent operations on this handle.
Files-backend operations are unaffected. Budgets apply separately to the common and private worktree stack; a linked-worktree operation may retain both bounded snapshots at once.
Source§impl<'a> References<'a>
impl<'a> References<'a>
Sourcepub const DEFAULT_SYMREF_DEPTH: usize = 4
pub const DEFAULT_SYMREF_DEPTH: usize = 4
The number of symbolic hops Self::resolve follows: four, as git rev-parse does.
Sourcepub fn read(&self, name: &RefName) -> Result<Option<Target>, ReferenceError>
pub fn read(&self, name: &RefName) -> Result<Option<Target>, ReferenceError>
Reads a stored direct or symbolic target without following symbolic links.
Reftable uses the snapshot/publication contract on References. The files-backend
storage details below do not apply to reftable.
Returns None for an absent name. Packed records are consulted only when the loose file
is absent; per-worktree names never use packed fallback. A directory at the exact name
denotes a namespace conflict, not absence.
§Errors
Reports I/O, malformed loose/packed data, unsupported packed traits, and filesystem symlinks.
use girt::refs::{RefName, Target};
use girt::{InitKind, ObjectFormat, Repository};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r.git"),
InitKind::Bare,
)?;
let refs = repo.references()?;
let head = RefName::HEAD;
let main = RefName::new("refs/heads/main")?;
assert_eq!(refs.read(&head)?, Some(Target::Symbolic(main.clone())));
assert_eq!(refs.read(&main)?, None); // Unborn branch, not a malformed HEAD.
Examples found in repository?
6fn main() -> Result<(), Box<dyn std::error::Error>> {
7 let directory = tempfile::tempdir()?;
8 let repo = Repository::init(
9 girt::ObjectFormat::Sha1,
10 directory.path().join("repo"),
11 InitKind::Bare,
12 )?;
13
14 let objects = repo.loose_objects();
15 let mut editor = girt::tree::TreeEditor::new(&objects);
16 editor.set(b"README.md", objects.write_blob(b"# Project\n")?)?;
17 let tree = editor.write()?;
18
19 let author = Signature::now(b"A. Writer", b"writer@example.com")?;
20 let builder = Commit::builder(tree, author.clone(), b"First snapshot\n");
21 let first = objects.write_commit(&builder.build()?)?;
22
23 let refs = repo.references()?;
24 let head = RefName::HEAD;
25 let branch = refs.update_resolved_without_reflog(&head, first, Expected::Absent)?;
26
27 let second_commit = Commit::builder(tree, author, b"Advance branch\n")
28 .parent(first)
29 .build()?;
30 let second = objects.write_commit(&second_commit)?;
31 refs.update_resolved_without_reflog(&head, second, Expected::Value(Target::Direct(first)))?;
32
33 assert_eq!(refs.resolve_with(&head, 8)?.id, Some(second));
34 assert_eq!(refs.read(&head)?, Some(Target::Symbolic(branch.clone())));
35
36 let branches = refs.list_namespace(&RefName::new(b"refs/heads")?)?;
37 assert_eq!(branches[0].name, branch);
38
39 // Delete the observed terminal branch, keeping HEAD symbolic and now unborn.
40 refs.delete_resolved_without_reflog(&head, Expected::Value(branches[0].target.clone()))?;
41 assert_eq!(refs.read(&head)?, Some(Target::Symbolic(branch)));
42 assert_eq!(refs.resolve_with(&head, 8)?.id, None);
43 assert!(refs.list()?.is_empty());
44
45 println!("Published {first}, advanced to {second}, then deleted main without reflogs");
46 Ok(())
47}More examples
5fn main() -> Result<(), Box<dyn std::error::Error>> {
6 let directory = tempfile::tempdir()?;
7 let repo = Repository::init(
8 girt::ObjectFormat::Sha256,
9 directory.path().join("repository"),
10 girt::InitKind::Bare,
11 )?;
12
13 let objects = repo.loose_objects();
14 let tree = objects.write_tree(&repo.new_tree(Vec::new())?)?;
15
16 let committer = Signature {
17 name: b"Example Writer".to_vec(),
18 email: b"writer@example.com".to_vec(),
19 seconds: 1700000000,
20 offset_minutes: 0,
21 };
22
23 let commit = Commit::new(CommitFields {
24 tree,
25 parents: Vec::new(),
26 author: committer.clone(),
27 committer: committer.clone(),
28 extra_headers: Vec::new(),
29 message: b"Initial commit\n".to_vec(),
30 })?;
31 let id = objects.write_commit(&commit)?;
32
33 let head = RefName::HEAD;
34 let tag = RefName::tag("initial")?;
35 let edits = [
36 {
37 let input = RefEdit::new(
38 head.clone(),
39 Some(girt::refs::Target::Direct(id)),
40 Expected::Absent,
41 Reflog::Append {
42 committer: committer.clone(),
43 message: b"initial publication".to_vec(),
44 },
45 );
46 input.with_dereference(girt::refs::Dereference::Follow)
47 },
48 RefEdit::create(
49 tag.clone(),
50 id,
51 Reflog::Append {
52 committer: committer.clone(),
53 message: b"mark initial".to_vec(),
54 },
55 ),
56 ];
57
58 let refs = repo.references()?;
59 match refs.transaction(&edits) {
60 Ok(outcomes) => println!("Published: {outcomes:?}"),
61 Err(TransactionError::Prepare { operation, source }) => {
62 eprintln!("No refs or logs changed; operation {operation:?}: {source}");
63 return Err(source.into());
64 }
65 Err(error @ TransactionError::Publish { .. }) => {
66 // Inspect per-ref and per-log outcomes before deciding whether/how to retry.
67 eprintln!("Partial publication: {error:?}");
68 return Err(error.into());
69 }
70 Err(error) => return Err(error.into()),
71 }
72
73 refs.transaction(&[RefEdit::delete(
74 tag.clone(),
75 id,
76 Reflog::Append {
77 committer,
78 message: b"remove temporary tag".to_vec(),
79 },
80 )])?;
81
82 assert_eq!(refs.reflog(&head)?.unwrap().len(), 1);
83 assert_eq!(refs.reflog(&tag)?.unwrap().len(), 2);
84 assert_eq!(refs.read(&tag)?, None);
85
86 // A retention ref may already exist at this value, but must never overwrite another value.
87 let keep = RefName::new("refs/jj/keep-example")?;
88 refs.transaction(&[RefEdit::new(
89 keep.clone(),
90 Some(girt::refs::Target::Direct(id)),
91 Expected::AbsentOr(girt::refs::Target::Direct(id)),
92 Reflog::Preserve,
93 )])?;
94 refs.transaction(&[RefEdit::new(keep, None, Expected::Exists, Reflog::Delete)])?;
95 Ok(())
96}12fn main() -> Result<(), Box<dyn std::error::Error>> {
13 let root = tempfile::tempdir()?;
14 let source = Repository::init(
15 girt::ObjectFormat::Sha1,
16 root.path().join("source"),
17 InitKind::Bare,
18 )?;
19
20 let destination_path = root.path().join("destination");
21 let destination =
22 Repository::init(girt::ObjectFormat::Sha1, &destination_path, InitKind::Bare)?;
23
24 let objects = source.loose_objects();
25 let tree = objects.write_tree(&Tree::new(girt::ObjectFormat::Sha1, Vec::new())?)?;
26
27 let identity = Signature {
28 name: b"Fetch Example".to_vec(),
29 email: b"fetch@example.com".to_vec(),
30 seconds: 1700000000,
31 offset_minutes: 0,
32 };
33 let commit = objects.write_commit(&Commit::new(CommitFields {
34 tree,
35 parents: Vec::new(),
36 author: identity.clone(),
37 committer: identity.clone(),
38 extra_headers: Vec::new(),
39 message: b"Original fetch example\n".to_vec(),
40 })?)?;
41
42 source.references()?.update_without_reflog(
43 &RefName::new("refs/heads/main")?,
44 Target::Direct(commit),
45 Expected::Absent,
46 )?;
47
48 // This explicit config snapshot could instead be destination.config(). The example uses a
49 // literal local endpoint token so config escaping is independent of temporary path spelling.
50 let config = Config::parse(
51 b"[remote \"origin\"]\nurl = local-example\nfetch = +refs/heads/*:refs/remotes/origin/*\n",
52 )?;
53
54 let remote = Remote::find(&config, b"origin")?.ok_or("missing remote")?;
55 assert_eq!(remote.fetch_url(), Some(b"local-example".as_slice()));
56
57 // Consumer endpoint policy resolves this token to the disposable source, with no credentials.
58 // An HTTP/SSH consumer constructs its explicit remote and chooses receive_http/receive_ssh.
59 let request = FetchRequest::prepare(
60 destination,
61 remote.fetch_refspecs().clone(),
62 BTreeSet::new(),
63 Reflog::Append {
64 committer: identity,
65 message: b"fetch origin".to_vec(),
66 },
67 )?;
68
69 let cancel = AtomicBool::new(false);
70 let control = TransportControl::new(&cancel)
71 .with_deadline(Some(Instant::now() + Duration::from_secs(30)));
72 let ready = request.receive_local(
73 source.git_dir(),
74 &KnownHistory::default(),
75 FetchLimits::default(),
76 control,
77 girt::fetch::LocalFetchObserver::default(),
78 )?;
79
80 let report = match ready.finish(FetchUpdateLimits::default(), &cancel) {
81 Ok(report) => report,
82 Err(error) => {
83 // Retain both parts: installed objects may remain, and a transaction error can contain
84 // partial reference/reflog effects. Retrying requires a fresh destination snapshot.
85 eprintln!(
86 "Fetch effects: {:?}; failure: {}",
87 error.report, error.source
88 );
89 return Err(error.into());
90 }
91 };
92
93 let destination = Repository::open(&destination_path)?;
94 assert_eq!(
95 destination
96 .references()?
97 .read(&RefName::new("refs/remotes/origin/main")?)?,
98 Some(Target::Direct(commit))
99 );
100 assert!(!destination.git_dir().join("FETCH_HEAD").exists());
101
102 println!(
103 "Validated {} objects, installed {:?}, published {} reference(s)",
104 report.objects,
105 report.installed,
106 report.references.len()
107 );
108
109 // TempDir removes both repositories after all synchronous work has completed.
110 Ok(())
111}13fn main() -> Result<(), Box<dyn std::error::Error>> {
14 let root = tempfile::tempdir()?;
15 let source = Repository::init(
16 girt::ObjectFormat::Sha1,
17 root.path().join("source"),
18 InitKind::Bare,
19 )?;
20 let destination_path = root.path().join("destination");
21
22 let objects = source.loose_objects();
23 let blob = objects.write_blob(b"hello from the clone\n")?;
24 let tree = objects.write_tree(&Tree::new(
25 girt::ObjectFormat::Sha1,
26 vec![TreeEntry {
27 mode: EntryMode::Blob,
28 name: b"hello.txt".to_vec(),
29 id: blob,
30 }],
31 )?)?;
32
33 let identity = Signature {
34 name: b"Fetch Example".to_vec(),
35 email: b"fetch@example.com".to_vec(),
36 seconds: 1700000000,
37 offset_minutes: 0,
38 };
39 let commit = objects.write_commit(&Commit::new(CommitFields {
40 tree,
41 parents: Vec::new(),
42 author: identity.clone(),
43 committer: identity.clone(),
44 extra_headers: Vec::new(),
45 message: b"Original fetch example\n".to_vec(),
46 })?)?;
47
48 source.references()?.update_without_reflog(
49 &RefName::branch("main")?,
50 Target::Direct(commit),
51 Expected::Absent,
52 )?;
53
54 // The persisted URL is credential-free metadata. Transport selection stays explicit.
55 let source_path = std::fs::canonicalize(source.git_dir())?;
56 let url = source_path
57 .to_str()
58 .ok_or("example needs a UTF-8 temporary path")?;
59 let request = CloneRequest::prepare_tracking(
60 &destination_path,
61 InitKind::Worktree,
62 url.as_bytes(),
63 BranchSelection::Default,
64 Reflog::Preserve,
65 )?;
66
67 let cancel = AtomicBool::new(false);
68 let ready = request.receive_local(
69 &source_path,
70 FetchLimits::default(),
71 TransportControl::new(&cancel)
72 .with_deadline(Some(Instant::now() + Duration::from_secs(30))),
73 |_| ControlFlow::Continue(()),
74 )?;
75
76 assert!(!destination_path.exists());
77
78 // HTTP/SSH callers receive an owned download instead. Move it to a bounded worker, validate
79 // synchronously, then finish here. Join that worker even after requesting cancellation.
80 let report = match ready.finish(FetchUpdateLimits::default(), &cancel) {
81 Ok(report) => report,
82 Err(error) => {
83 // Preserve both reports: nested fetch/transaction failures retain partial effects.
84 // Production code should inspect the destination; never delete it automatically.
85 eprintln!(
86 "Clone effects: {:?}; failure: {}",
87 error.report, error.source
88 );
89 return Err(error.into());
90 }
91 };
92
93 let repository = report
94 .repository
95 .as_ref()
96 .ok_or("missing completed repository")?;
97 assert_eq!(
98 repository
99 .references()?
100 .read(&RefName::new("refs/heads/main")?)?,
101 Some(Target::Direct(commit))
102 );
103 assert!(!repository.git_dir().join("index").exists());
104 assert_eq!(std::fs::read_dir(&destination_path)?.count(), 1); // Only .git; no checkout.
105
106 let remote = Remote::find(repository.config(), b"origin")?.ok_or("missing origin")?;
107 assert_eq!(remote.fetch_url(), Some(url.as_bytes()));
108
109 // Pass remote.fetch_refspecs().clone() to FetchRequest::prepare for subsequent fetch.
110 println!(
111 "Created no-checkout repository at {} with {:?}",
112 destination_path.display(),
113 report.head
114 );
115 drop(report);
116
117 // The configured workflow composes the same core clone with checkout. Explicit empty inputs
118 // and environment isolate this disposable example from the caller's machine settings.
119 let checked_out = root.path().join("checked-out");
120 let outcome = girt::workflow::clone_with(
121 url,
122 &checked_out,
123 &girt::workflow::CloneOptions::default()
124 .with_checkout(cfg!(any(target_os = "linux", target_os = "macos"))),
125 &girt::config::ConfigInputs::default(),
126 &girt::workflow::Environment::default(),
127 &mut girt::workflow::NoCallbacks,
128 )?;
129
130 if cfg!(any(target_os = "linux", target_os = "macos")) {
131 assert!(
132 outcome
133 .checkout
134 .as_ref()
135 .is_some_and(|report| report.index_published)
136 );
137 }
138
139 if cfg!(any(target_os = "linux", target_os = "macos")) {
140 assert_eq!(
141 std::fs::read(checked_out.join("hello.txt"))?,
142 b"hello from the clone\n"
143 );
144 }
145
146 drop(outcome);
147 root.close()?; // Cleanup belongs to this disposable example, not the clone operation.
148 Ok(())
149}Sourcepub fn read_observation(
&self,
name: &RefName,
) -> Result<Option<ReferenceObservation>, ReferenceError>
pub fn read_observation( &self, name: &RefName, ) -> Result<Option<ReferenceObservation>, ReferenceError>
Captures a stored target and its associated storage-provided peeled hint in one read.
Loose values take precedence and always have no hint, even when they equal a packed target. Packed target and hint come from the same parsed file record; reftable uses one winning record from a merged stack snapshot. No symbolic resolution or object read occurs. The owned result remains unchanged after publication, but is not a lock or a guarantee about current storage. Use its target as a transaction expectation before acting on it.
§Errors
Reports the same errors and preserves loose-before-packed precedence as Self::read.
use girt::refs::RefName;
use girt::{InitKind, ObjectFormat, Repository};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("repo"),
InitKind::Bare,
)?;
let refs = repo.references()?;
let observation = refs
.read_observation(&RefName::HEAD)?
.ok_or("HEAD absent")?;
assert_eq!(observation.peeled_hint, None);Sourcepub fn resolve(&self, name: &RefName) -> Result<Resolution, ReferenceError>
pub fn resolve(&self, name: &RefName) -> Result<Resolution, ReferenceError>
Follows symbolic links as far as Git does and returns the terminal name and identity.
Equivalent to Self::resolve_with with a depth of 4 (Self::DEFAULT_SYMREF_DEPTH):
git rev-parse resolves a chain of four symbolic references and reports a fifth as
dangling. Use Self::resolve_with to choose another depth.
§Errors
Reports the errors of Self::resolve_with.
use girt::refs::RefName;
use girt::{InitKind, ObjectFormat, Repository};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r.git"),
InitKind::Bare,
)?;
let resolution = repo.references()?.resolve(&RefName::HEAD)?;
assert_eq!(resolution.name, RefName::branch("main")?);
assert_eq!(resolution.id, None); // Unborn.
Sourcepub fn resolve_with(
&self,
name: &RefName,
max_depth: usize,
) -> Result<Resolution, ReferenceError>
pub fn resolve_with( &self, name: &RefName, max_depth: usize, ) -> Result<Resolution, ReferenceError>
Follows at most max_depth symbolic links and returns the terminal name and identity.
A missing initial name or dangling/unborn target returns id: None; a missing object is
not detected. Zero depth permits a direct or missing ref but no symbolic hop. Reads across
hops are not atomic. This does not peel tags or open objects.
§Errors
Reports ReferenceError::Cycle, ReferenceError::Depth, or a Self::read failure.
use girt::refs::{RefName, Target};
use girt::{InitKind, ObjectFormat, Repository};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r.git"),
InitKind::Bare,
)?;
let refs = repo.references()?;
let head = RefName::HEAD;
let main = RefName::new("refs/heads/main")?;
let resolution = refs.resolve(&head)?;
assert_eq!(resolution.name, main);
assert_eq!(resolution.id, None); // Resolution can succeed without a branch ID.
Examples found in repository?
6fn main() -> Result<(), Box<dyn std::error::Error>> {
7 let directory = tempfile::tempdir()?;
8 let repo = Repository::init(
9 girt::ObjectFormat::Sha1,
10 directory.path().join("repo"),
11 InitKind::Bare,
12 )?;
13
14 let objects = repo.loose_objects();
15 let mut editor = girt::tree::TreeEditor::new(&objects);
16 editor.set(b"README.md", objects.write_blob(b"# Project\n")?)?;
17 let tree = editor.write()?;
18
19 let author = Signature::now(b"A. Writer", b"writer@example.com")?;
20 let builder = Commit::builder(tree, author.clone(), b"First snapshot\n");
21 let first = objects.write_commit(&builder.build()?)?;
22
23 let refs = repo.references()?;
24 let head = RefName::HEAD;
25 let branch = refs.update_resolved_without_reflog(&head, first, Expected::Absent)?;
26
27 let second_commit = Commit::builder(tree, author, b"Advance branch\n")
28 .parent(first)
29 .build()?;
30 let second = objects.write_commit(&second_commit)?;
31 refs.update_resolved_without_reflog(&head, second, Expected::Value(Target::Direct(first)))?;
32
33 assert_eq!(refs.resolve_with(&head, 8)?.id, Some(second));
34 assert_eq!(refs.read(&head)?, Some(Target::Symbolic(branch.clone())));
35
36 let branches = refs.list_namespace(&RefName::new(b"refs/heads")?)?;
37 assert_eq!(branches[0].name, branch);
38
39 // Delete the observed terminal branch, keeping HEAD symbolic and now unborn.
40 refs.delete_resolved_without_reflog(&head, Expected::Value(branches[0].target.clone()))?;
41 assert_eq!(refs.read(&head)?, Some(Target::Symbolic(branch)));
42 assert_eq!(refs.resolve_with(&head, 8)?.id, None);
43 assert!(refs.list()?.is_empty());
44
45 println!("Published {first}, advanced to {second}, then deleted main without reflogs");
46 Ok(())
47}Sourcepub fn update_without_reflog(
&self,
name: &RefName,
target: Target,
expected: Expected,
) -> Result<(), ReferenceError>
pub fn update_without_reflog( &self, name: &RefName, target: Target, expected: Expected, ) -> Result<(), ReferenceError>
Replaces the named reference itself, without dereferencing it or writing reflogs.
Reftable uses the snapshot/publication contract on References. The files-backend
storage details below do not apply to reftable.
expected compares its stored target, including any packed value. The new loose file
shadows packed data; packed-refs is never rewritten. Symbolic targets may be dangling or
cyclic, except HEAD itself must point into refs/. IDs must be nonzero, but object
existence, type, and fast-forward rules are not
checked. Use Self::update_resolved_without_reflog to advance a symbolic ref’s target.
This is a low-level publication primitive, not full git update-ref equivalence.
It ignores core.logAllRefUpdates, leaves existing reflogs unchanged, and runs no hooks.
Prior tips gain no new reflog retention or recovery entry; reflog selectors may be stale,
and old objects can become eligible for pruning. Callers must deliberately accept that
behavior. Choose this for one stored-name edit when name history is deliberately omitted;
choose Self::transaction for a batch or explicit reflog actions and per-edit receipts.
Locks packed-refs.lock, then <name>.lock, using exclusive creation. Checks the
precondition and packed namespace while locked, writes an owned temporary file, and
atomically replaces the destination while retaining the lock. Existing locks are never
overwritten or removed. A failure before rename preserves existing reference bytes;
newly created empty parent directories may remain. Owned lock cleanup is best effort on
errors/unwind; process termination or cleanup I/O failure may leave stale locks. No
file/directory fsync is performed: successful visibility does not promise survival of a
crash or power loss, or durability of objects.
§Errors
Reports lock contention, expectation mismatch, namespace conflict (including empty
directories), malformed existing data, unsupported paths, zero IDs, and I/O failures.
No retry is performed. See References for filesystem/concurrent-writer assumptions.
use girt::refs::{Expected, RefName, Target};
use girt::{InitKind, ObjectFormat, Repository};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r.git"),
InitKind::Bare,
)?;
let id = repo.loose_objects().write_blob(b"tagged manifest")?;
let refs = repo.references()?;
let name = RefName::new("refs/tags/one")?;
refs.update_without_reflog(&name, Target::Direct(id), Expected::Absent)?;
assert_eq!(refs.read(&name)?, Some(Target::Direct(id)));
assert!(!refs.has_reflog(&name)?); // Deliberately bypasses name-history recording.
Examples found in repository?
19fn main() -> Result<(), Box<dyn std::error::Error>> {
20 let root = tempfile::tempdir()?;
21 let repo = Repository::init_with_backend(
22 ObjectFormat::Sha1,
23 root.path().join("repo"),
24 InitKind::Bare,
25 Backend::Reftable,
26 )?;
27
28 let before = descriptors();
29 let refs = repo.references()?;
30 let name = RefName::new(b"refs/tags/probe")?;
31 for byte in 1..=16 {
32 refs.update_without_reflog(
33 &name,
34 Target::Direct(ObjectId::Sha1([byte; 20])),
35 Expected::Any,
36 )?;
37 }
38
39 let directory = repo.git_dir().join("reftable");
40 let snapshot = Snapshot::read(
41 &directory,
42 ObjectFormat::Sha1,
43 StackLimits::default(),
44 &AtomicBool::new(false),
45 )?;
46 let retained = descriptors();
47
48 let report = compact(
49 &directory,
50 ObjectFormat::Sha1,
51 StackLimits::default(),
52 &AtomicBool::new(false),
53 )?;
54 let after_compaction = descriptors();
55
56 assert!(report.retained.is_empty());
57 assert_eq!(snapshot.table.references.len(), 2);
58 assert!(
59 Snapshot::read(
60 &directory,
61 ObjectFormat::Sha1,
62 StackLimits::default(),
63 &AtomicBool::new(true)
64 )
65 .is_err()
66 );
67
68 drop(snapshot);
69 let after_drop = descriptors();
70 println!(
71 "descriptors before={before:?} retained={retained:?} compacted={after_compaction:?} dropped={after_drop:?}"
72 );
73 println!("compacted_tables={}", report.input_tables);
74
75 Ok(())
76}More examples
20fn main() -> Result<(), Box<dyn std::error::Error>> {
21 let (_source_root, source) = repository()?;
22 let (_destination_root, destination) = repository()?;
23
24 let objects = source.loose_objects();
25 let blob = objects.write_blob(b"Fetched through native local storage\n")?;
26 let tree = objects.write_tree(&Tree::new(
27 girt::ObjectFormat::Sha1,
28 vec![TreeEntry {
29 mode: EntryMode::Blob,
30 name: b"hello.txt".to_vec(),
31 id: blob,
32 }],
33 )?)?;
34
35 let author = Signature::now(b"Fetch Example", b"fetch@example.com")?;
36 let commit = objects.write_commit(&Commit::new(CommitFields {
37 tree,
38 parents: vec![],
39 author: author.clone(),
40 committer: author,
41 extra_headers: vec![],
42 message: b"Local fetch fixture\n".to_vec(),
43 })?)?;
44
45 let branch = RefName::branch("main")?;
46 source.references()?.update_without_reflog(
47 &branch,
48 Target::Direct(commit),
49 Expected::Absent,
50 )?;
51
52 let cancel = AtomicBool::new(false);
53 let received = receive_local(
54 source.git_dir(),
55 |advertisement| {
56 advertisement
57 .refs
58 .iter()
59 .filter(|reference| {
60 reference.name == branch
61 && reference.kind == girt::fetch::AdvertisementKind::Tip
62 })
63 .map(|reference| reference.id)
64 .collect()
65 },
66 girt::fetch::LocalFetchRequest::new(FetchLimits::default()),
67 TransportControl::new(&cancel)
68 .with_deadline(Some(Instant::now() + Duration::from_secs(30))),
69 girt::fetch::LocalFetchObserver::default(),
70 )?;
71
72 let result = received.install(&destination, girt::PackLimits::default(), &cancel)?;
73
74 // Updating a ref is a separate, conditional operation, explicitly without a reflog.
75 destination.references()?.update_without_reflog(
76 &branch,
77 Target::Direct(commit),
78 Expected::Absent,
79 )?;
80
81 let fetched = destination.objects()?;
82 let restored = fetched.read(blob)?.ok_or("missing fetched blob")?;
83 assert_eq!(restored.data(), b"Fetched through native local storage\n");
84
85 println!(
86 "Fetched {} objects ({} pack bytes); published {commit} without a reflog",
87 result.objects,
88 received.pack_bytes()
89 );
90
91 let known = KnownHistory::new(&fetched, &[commit], FetchLimits::default(), &cancel)?;
92 let repeated = receive_local(
93 source.git_dir(),
94 |_| vec![commit],
95 girt::fetch::LocalFetchRequest::new(FetchLimits::default()).with_known(&known),
96 TransportControl::new(&cancel)
97 .with_deadline(Some(Instant::now() + Duration::from_secs(30))),
98 girt::fetch::LocalFetchObserver::default(),
99 )?;
100
101 // Recheck known dependencies in the destination even when no pack was needed.
102 repeated.install(&destination, girt::PackLimits::default(), &cancel)?;
103 assert_eq!(repeated.pack_bytes(), 0);
104 println!(
105 "Repeated fetch: {} objects, {} pack bytes",
106 repeated.object_count(),
107 repeated.pack_bytes()
108 );
109
110 Ok(())
111}12fn main() -> Result<(), Box<dyn std::error::Error>> {
13 let root = tempfile::tempdir()?;
14 let source = Repository::init(
15 girt::ObjectFormat::Sha1,
16 root.path().join("source"),
17 InitKind::Bare,
18 )?;
19
20 let destination_path = root.path().join("destination");
21 let destination =
22 Repository::init(girt::ObjectFormat::Sha1, &destination_path, InitKind::Bare)?;
23
24 let objects = source.loose_objects();
25 let tree = objects.write_tree(&Tree::new(girt::ObjectFormat::Sha1, Vec::new())?)?;
26
27 let identity = Signature {
28 name: b"Fetch Example".to_vec(),
29 email: b"fetch@example.com".to_vec(),
30 seconds: 1700000000,
31 offset_minutes: 0,
32 };
33 let commit = objects.write_commit(&Commit::new(CommitFields {
34 tree,
35 parents: Vec::new(),
36 author: identity.clone(),
37 committer: identity.clone(),
38 extra_headers: Vec::new(),
39 message: b"Original fetch example\n".to_vec(),
40 })?)?;
41
42 source.references()?.update_without_reflog(
43 &RefName::new("refs/heads/main")?,
44 Target::Direct(commit),
45 Expected::Absent,
46 )?;
47
48 // This explicit config snapshot could instead be destination.config(). The example uses a
49 // literal local endpoint token so config escaping is independent of temporary path spelling.
50 let config = Config::parse(
51 b"[remote \"origin\"]\nurl = local-example\nfetch = +refs/heads/*:refs/remotes/origin/*\n",
52 )?;
53
54 let remote = Remote::find(&config, b"origin")?.ok_or("missing remote")?;
55 assert_eq!(remote.fetch_url(), Some(b"local-example".as_slice()));
56
57 // Consumer endpoint policy resolves this token to the disposable source, with no credentials.
58 // An HTTP/SSH consumer constructs its explicit remote and chooses receive_http/receive_ssh.
59 let request = FetchRequest::prepare(
60 destination,
61 remote.fetch_refspecs().clone(),
62 BTreeSet::new(),
63 Reflog::Append {
64 committer: identity,
65 message: b"fetch origin".to_vec(),
66 },
67 )?;
68
69 let cancel = AtomicBool::new(false);
70 let control = TransportControl::new(&cancel)
71 .with_deadline(Some(Instant::now() + Duration::from_secs(30)));
72 let ready = request.receive_local(
73 source.git_dir(),
74 &KnownHistory::default(),
75 FetchLimits::default(),
76 control,
77 girt::fetch::LocalFetchObserver::default(),
78 )?;
79
80 let report = match ready.finish(FetchUpdateLimits::default(), &cancel) {
81 Ok(report) => report,
82 Err(error) => {
83 // Retain both parts: installed objects may remain, and a transaction error can contain
84 // partial reference/reflog effects. Retrying requires a fresh destination snapshot.
85 eprintln!(
86 "Fetch effects: {:?}; failure: {}",
87 error.report, error.source
88 );
89 return Err(error.into());
90 }
91 };
92
93 let destination = Repository::open(&destination_path)?;
94 assert_eq!(
95 destination
96 .references()?
97 .read(&RefName::new("refs/remotes/origin/main")?)?,
98 Some(Target::Direct(commit))
99 );
100 assert!(!destination.git_dir().join("FETCH_HEAD").exists());
101
102 println!(
103 "Validated {} objects, installed {:?}, published {} reference(s)",
104 report.objects,
105 report.installed,
106 report.references.len()
107 );
108
109 // TempDir removes both repositories after all synchronous work has completed.
110 Ok(())
111}13fn main() -> Result<(), Box<dyn std::error::Error>> {
14 let root = tempfile::tempdir()?;
15 let source = Repository::init(
16 girt::ObjectFormat::Sha1,
17 root.path().join("source"),
18 InitKind::Bare,
19 )?;
20 let destination_path = root.path().join("destination");
21
22 let objects = source.loose_objects();
23 let blob = objects.write_blob(b"hello from the clone\n")?;
24 let tree = objects.write_tree(&Tree::new(
25 girt::ObjectFormat::Sha1,
26 vec![TreeEntry {
27 mode: EntryMode::Blob,
28 name: b"hello.txt".to_vec(),
29 id: blob,
30 }],
31 )?)?;
32
33 let identity = Signature {
34 name: b"Fetch Example".to_vec(),
35 email: b"fetch@example.com".to_vec(),
36 seconds: 1700000000,
37 offset_minutes: 0,
38 };
39 let commit = objects.write_commit(&Commit::new(CommitFields {
40 tree,
41 parents: Vec::new(),
42 author: identity.clone(),
43 committer: identity.clone(),
44 extra_headers: Vec::new(),
45 message: b"Original fetch example\n".to_vec(),
46 })?)?;
47
48 source.references()?.update_without_reflog(
49 &RefName::branch("main")?,
50 Target::Direct(commit),
51 Expected::Absent,
52 )?;
53
54 // The persisted URL is credential-free metadata. Transport selection stays explicit.
55 let source_path = std::fs::canonicalize(source.git_dir())?;
56 let url = source_path
57 .to_str()
58 .ok_or("example needs a UTF-8 temporary path")?;
59 let request = CloneRequest::prepare_tracking(
60 &destination_path,
61 InitKind::Worktree,
62 url.as_bytes(),
63 BranchSelection::Default,
64 Reflog::Preserve,
65 )?;
66
67 let cancel = AtomicBool::new(false);
68 let ready = request.receive_local(
69 &source_path,
70 FetchLimits::default(),
71 TransportControl::new(&cancel)
72 .with_deadline(Some(Instant::now() + Duration::from_secs(30))),
73 |_| ControlFlow::Continue(()),
74 )?;
75
76 assert!(!destination_path.exists());
77
78 // HTTP/SSH callers receive an owned download instead. Move it to a bounded worker, validate
79 // synchronously, then finish here. Join that worker even after requesting cancellation.
80 let report = match ready.finish(FetchUpdateLimits::default(), &cancel) {
81 Ok(report) => report,
82 Err(error) => {
83 // Preserve both reports: nested fetch/transaction failures retain partial effects.
84 // Production code should inspect the destination; never delete it automatically.
85 eprintln!(
86 "Clone effects: {:?}; failure: {}",
87 error.report, error.source
88 );
89 return Err(error.into());
90 }
91 };
92
93 let repository = report
94 .repository
95 .as_ref()
96 .ok_or("missing completed repository")?;
97 assert_eq!(
98 repository
99 .references()?
100 .read(&RefName::new("refs/heads/main")?)?,
101 Some(Target::Direct(commit))
102 );
103 assert!(!repository.git_dir().join("index").exists());
104 assert_eq!(std::fs::read_dir(&destination_path)?.count(), 1); // Only .git; no checkout.
105
106 let remote = Remote::find(repository.config(), b"origin")?.ok_or("missing origin")?;
107 assert_eq!(remote.fetch_url(), Some(url.as_bytes()));
108
109 // Pass remote.fetch_refspecs().clone() to FetchRequest::prepare for subsequent fetch.
110 println!(
111 "Created no-checkout repository at {} with {:?}",
112 destination_path.display(),
113 report.head
114 );
115 drop(report);
116
117 // The configured workflow composes the same core clone with checkout. Explicit empty inputs
118 // and environment isolate this disposable example from the caller's machine settings.
119 let checked_out = root.path().join("checked-out");
120 let outcome = girt::workflow::clone_with(
121 url,
122 &checked_out,
123 &girt::workflow::CloneOptions::default()
124 .with_checkout(cfg!(any(target_os = "linux", target_os = "macos"))),
125 &girt::config::ConfigInputs::default(),
126 &girt::workflow::Environment::default(),
127 &mut girt::workflow::NoCallbacks,
128 )?;
129
130 if cfg!(any(target_os = "linux", target_os = "macos")) {
131 assert!(
132 outcome
133 .checkout
134 .as_ref()
135 .is_some_and(|report| report.index_published)
136 );
137 }
138
139 if cfg!(any(target_os = "linux", target_os = "macos")) {
140 assert_eq!(
141 std::fs::read(checked_out.join("hello.txt"))?,
142 b"hello from the clone\n"
143 );
144 }
145
146 drop(outcome);
147 root.close()?; // Cleanup belongs to this disposable example, not the clone operation.
148 Ok(())
149}Sourcepub fn update_resolved_without_reflog(
&self,
name: &RefName,
id: ObjectId,
expected: Expected,
) -> Result<RefName, ReferenceError>
pub fn update_resolved_without_reflog( &self, name: &RefName, id: ObjectId, expected: Expected, ) -> Result<RefName, ReferenceError>
Advances a terminal direct/missing reference while keeping the symbolic chain unchanged.
Reftable uses the snapshot/publication contract on References. The files-backend
storage details below do not apply to reftable.
Holds packed-refs.lock and every visited name’s lock through publication, with the same
no-reflog, namespace, cleanup, concurrency and durability contracts as
Self::update_without_reflog. expected applies to the terminal name, so Absent can
publish an unborn branch through HEAD. Returns the name that was updated. At most 32
symbolic hops are allowed. Changing a symbolic reference itself requires the other method.
§Errors
Reports cycle/depth failures or any error from Self::update_without_reflog. Acquired
locks are released on failure; no reference is published until the whole chain is locked.
use girt::refs::{Expected, RefName, Target};
use girt::{InitKind, ObjectFormat, Repository};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r.git"),
InitKind::Bare,
)?;
let id = repo.loose_objects().write_blob(b"tagged manifest")?;
let refs = repo.references()?;
let name = RefName::new("refs/tags/one")?;
let alias = RefName::new("refs/meta/current")?;
refs.update_without_reflog(&alias, Target::Symbolic(name.clone()), Expected::Absent)?;
assert_eq!(
refs.update_resolved_without_reflog(&alias, id, Expected::Absent)?,
name
);
assert_eq!(refs.read(&alias)?, Some(Target::Symbolic(name)));Examples found in repository?
6fn main() -> Result<(), Box<dyn std::error::Error>> {
7 let directory = tempfile::tempdir()?;
8 let repo = Repository::init(
9 girt::ObjectFormat::Sha1,
10 directory.path().join("repo"),
11 InitKind::Bare,
12 )?;
13
14 let objects = repo.loose_objects();
15 let mut editor = girt::tree::TreeEditor::new(&objects);
16 editor.set(b"README.md", objects.write_blob(b"# Project\n")?)?;
17 let tree = editor.write()?;
18
19 let author = Signature::now(b"A. Writer", b"writer@example.com")?;
20 let builder = Commit::builder(tree, author.clone(), b"First snapshot\n");
21 let first = objects.write_commit(&builder.build()?)?;
22
23 let refs = repo.references()?;
24 let head = RefName::HEAD;
25 let branch = refs.update_resolved_without_reflog(&head, first, Expected::Absent)?;
26
27 let second_commit = Commit::builder(tree, author, b"Advance branch\n")
28 .parent(first)
29 .build()?;
30 let second = objects.write_commit(&second_commit)?;
31 refs.update_resolved_without_reflog(&head, second, Expected::Value(Target::Direct(first)))?;
32
33 assert_eq!(refs.resolve_with(&head, 8)?.id, Some(second));
34 assert_eq!(refs.read(&head)?, Some(Target::Symbolic(branch.clone())));
35
36 let branches = refs.list_namespace(&RefName::new(b"refs/heads")?)?;
37 assert_eq!(branches[0].name, branch);
38
39 // Delete the observed terminal branch, keeping HEAD symbolic and now unborn.
40 refs.delete_resolved_without_reflog(&head, Expected::Value(branches[0].target.clone()))?;
41 assert_eq!(refs.read(&head)?, Some(Target::Symbolic(branch)));
42 assert_eq!(refs.resolve_with(&head, 8)?.id, None);
43 assert!(refs.list()?.is_empty());
44
45 println!("Published {first}, advanced to {second}, then deleted main without reflogs");
46 Ok(())
47}Sourcepub fn delete_without_reflog(
&self,
name: &RefName,
expected: Expected,
) -> Result<(), ReferenceError>
pub fn delete_without_reflog( &self, name: &RefName, expected: Expected, ) -> Result<(), ReferenceError>
Deletes the named reference itself, without dereferencing it or changing reflogs.
Compares expected with the stored loose-over-packed target under packed-refs.lock
and the name’s lock. Any and Absent permit an already absent name. A symbolic value
is compared and removed as a name; its target is untouched. Use
Self::delete_resolved_without_reflog to delete the terminal ref instead.
Removes the packed record and its peel line before unlinking the loose file, so deletion
cannot uncover an older packed value. Unrelated packed bytes (including header traits,
order and peel records) are preserved. A separate temporary file publishes packed data
while the packed lock remains held through loose deletion. Both locks are held until
completion. Empty parent directories and existing reflogs remain; hooks are not run.
The no-reflog and filesystem assumptions of Self::update_without_reflog apply.
Reads remain live: a reader with previously read packed data can observe a stale value. This is not a snapshot, multi-ref transaction, or crash-durable operation. A subsequent writer can recreate the name after locks are released.
§Errors
Lock, namespace, malformed-data and expectation failures preserve reference bytes.
Lock acquisition may leave empty parent directories. Packed publication failure preserves
both values. If packed publication succeeds but loose removal fails, returns
ReferenceError::PackedDeleted: the loose value remains, with no rollback of packed
storage. Other I/O failures use ReferenceError::Io. Lock/temporary cleanup is best
effort, including after success; cleanup failure or termination can leave stale files.
No fsync, retries, reflog cleanup, or object deletion is performed.
use girt::refs::{Expected, RefName, Target};
use girt::{InitKind, ObjectFormat, Repository};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r.git"),
InitKind::Bare,
)?;
let id = repo.loose_objects().write_blob(b"tagged manifest")?;
let refs = repo.references()?;
let name = RefName::new("refs/tags/one")?;
refs.update_without_reflog(&name, Target::Direct(id), Expected::Absent)?;
refs.delete_without_reflog(&name, Expected::Value(Target::Direct(id)))?;
assert!(refs.read(&name)?.is_none());
assert!(
repo.objects_with(girt::PackLimits::default())?
.read_with(id, girt::ReadLimits::default())?
.is_some()
);Sourcepub fn delete_resolved_without_reflog(
&self,
name: &RefName,
expected: Expected,
) -> Result<RefName, ReferenceError>
pub fn delete_resolved_without_reflog( &self, name: &RefName, expected: Expected, ) -> Result<RefName, ReferenceError>
Deletes the terminal direct/missing ref while preserving every symbolic name in its chain.
expected applies to the terminal stored value. Holds the packed lock and every visited
name’s lock through deletion; at most 32 symbolic hops are allowed. Returns the terminal
name, including when already absent. Deleting a branch through HEAD leaves HEAD unborn;
deleting through an alias leaves the alias dangling.
§Errors
Reports cycle/depth failures or errors from Self::delete_without_reflog, with the same
packed-first ordering, partial-failure, cleanup, live-reader and no-reflog contracts.
Examples found in repository?
6fn main() -> Result<(), Box<dyn std::error::Error>> {
7 let directory = tempfile::tempdir()?;
8 let repo = Repository::init(
9 girt::ObjectFormat::Sha1,
10 directory.path().join("repo"),
11 InitKind::Bare,
12 )?;
13
14 let objects = repo.loose_objects();
15 let mut editor = girt::tree::TreeEditor::new(&objects);
16 editor.set(b"README.md", objects.write_blob(b"# Project\n")?)?;
17 let tree = editor.write()?;
18
19 let author = Signature::now(b"A. Writer", b"writer@example.com")?;
20 let builder = Commit::builder(tree, author.clone(), b"First snapshot\n");
21 let first = objects.write_commit(&builder.build()?)?;
22
23 let refs = repo.references()?;
24 let head = RefName::HEAD;
25 let branch = refs.update_resolved_without_reflog(&head, first, Expected::Absent)?;
26
27 let second_commit = Commit::builder(tree, author, b"Advance branch\n")
28 .parent(first)
29 .build()?;
30 let second = objects.write_commit(&second_commit)?;
31 refs.update_resolved_without_reflog(&head, second, Expected::Value(Target::Direct(first)))?;
32
33 assert_eq!(refs.resolve_with(&head, 8)?.id, Some(second));
34 assert_eq!(refs.read(&head)?, Some(Target::Symbolic(branch.clone())));
35
36 let branches = refs.list_namespace(&RefName::new(b"refs/heads")?)?;
37 assert_eq!(branches[0].name, branch);
38
39 // Delete the observed terminal branch, keeping HEAD symbolic and now unborn.
40 refs.delete_resolved_without_reflog(&head, Expected::Value(branches[0].target.clone()))?;
41 assert_eq!(refs.read(&head)?, Some(Target::Symbolic(branch)));
42 assert_eq!(refs.resolve_with(&head, 8)?.id, None);
43 assert!(refs.list()?.is_empty());
44
45 println!("Published {first}, advanced to {second}, then deleted main without reflogs");
46 Ok(())
47}Source§impl References<'_>
impl References<'_>
Sourcepub fn transaction(
&self,
edits: &[RefEdit],
) -> Result<Vec<RefEditOutcome>, TransactionError>
pub fn transaction( &self, edits: &[RefEdit], ) -> Result<Vec<RefEditOutcome>, TransactionError>
Applies a conditional batch with explicit reflog policy.
Reftable publishes references and logs atomically within each stack, with explicit partial
effects across linked-worktree stacks; see References. The remaining storage details
below describe the files backend.
Preparation holds packed-refs.lock, discovers symbolic chains, locks their union in
name-byte order, and rechecks every stored chain value and precondition. Reflog locks follow
in name-byte order. Duplicate/overlapping chains and ancestor/descendant names are rejected,
including delete/create namespace swaps. Existing packed conflicts are rejected. Contended
locks are retried for Git’s default durations (FilesTransactionOptions::GIT_DEFAULT);
locks are never stolen. A changed chain fails rather than being retried.
Publication first removes all selected packed records in one replacement. It then publishes refs in caller order, appending each operation’s requested logs after its ref succeeds. All owned locks remain held until return. Readers can observe any intermediate state. An I/O error stops publication, returning per-ref and per-log effects; no rollback occurs. In particular, a ref can advance with a missing or partial reflog entry.
Logs use append writes, never whole-file replacement. Ref locks coordinate writers of the
same destination; log locks coordinate girt transactions. Git can append automatic HEAD logs
without honoring those log locks, so ordering against such appends is not promised. Reflog
expiry and external log rewriting must be excluded by the caller while writing.
Reflog::Delete removes only the edited destination’s log after reference deletion.
Direct branch edits do not automatically log HEAD or discover aliases; use a resolved HEAD
edit to log that chain. A stored direct replacement with append logging locks and resolves
the old symbolic chain, recording its terminal ID (zero when unborn) in only the edited
name’s log. Its precondition still compares the original stored value, and the old branch
and its log remain unchanged. A stored symbolic replacement additionally locks the new
chain and records its terminal ID, or zero if unborn. Stored symbolic deletion logs the
old terminal ID and zero, without deleting the target.
No hooks, config/environment policy, object validation, fsync, crash recovery, or
filesystem-wide atomic visibility is provided. Cleanup is best effort; termination or
cleanup failure can leave locks/temporary files. See References for filesystem trust.
§Errors
TransactionError::Prepare preserves ref/log contents on malformed data, invalid edits,
precondition mismatch, namespace conflicts, or lock failure. TransactionError::Publish
reports effects at meaningful publication boundaries. An empty batch is a successful no-op.
// Publish a child only if the branch still has the stored value we observed.
let observed = refs.read(&main)?.ok_or("branch missing")?;
let mut fields = loose.read_commit(id, 4096)?.to_fields()?;
fields.parents = vec![id];
fields.message = b"Next snapshot\n".to_vec();
let next = loose.write_commit(&Commit::new(fields)?)?;
let advance = RefEdit::new(main.clone(), Some(Target::Direct(next)), Expected::Value(observed),
Reflog::Append { committer: Signature { name: b"A".to_vec(), email: b"a@example.com".to_vec(),
seconds: 1_700_000_001, offset_minutes: 0 }, message: b"commit: next".to_vec() });
refs.transaction(&[advance])?;
assert_eq!(refs.read(&main)?, Some(Target::Direct(next)));Examples found in repository?
5fn main() -> Result<(), Box<dyn std::error::Error>> {
6 let directory = tempfile::tempdir()?;
7 let repo = Repository::init(
8 girt::ObjectFormat::Sha256,
9 directory.path().join("repository"),
10 girt::InitKind::Bare,
11 )?;
12
13 let objects = repo.loose_objects();
14 let tree = objects.write_tree(&repo.new_tree(Vec::new())?)?;
15
16 let committer = Signature {
17 name: b"Example Writer".to_vec(),
18 email: b"writer@example.com".to_vec(),
19 seconds: 1700000000,
20 offset_minutes: 0,
21 };
22
23 let commit = Commit::new(CommitFields {
24 tree,
25 parents: Vec::new(),
26 author: committer.clone(),
27 committer: committer.clone(),
28 extra_headers: Vec::new(),
29 message: b"Initial commit\n".to_vec(),
30 })?;
31 let id = objects.write_commit(&commit)?;
32
33 let head = RefName::HEAD;
34 let tag = RefName::tag("initial")?;
35 let edits = [
36 {
37 let input = RefEdit::new(
38 head.clone(),
39 Some(girt::refs::Target::Direct(id)),
40 Expected::Absent,
41 Reflog::Append {
42 committer: committer.clone(),
43 message: b"initial publication".to_vec(),
44 },
45 );
46 input.with_dereference(girt::refs::Dereference::Follow)
47 },
48 RefEdit::create(
49 tag.clone(),
50 id,
51 Reflog::Append {
52 committer: committer.clone(),
53 message: b"mark initial".to_vec(),
54 },
55 ),
56 ];
57
58 let refs = repo.references()?;
59 match refs.transaction(&edits) {
60 Ok(outcomes) => println!("Published: {outcomes:?}"),
61 Err(TransactionError::Prepare { operation, source }) => {
62 eprintln!("No refs or logs changed; operation {operation:?}: {source}");
63 return Err(source.into());
64 }
65 Err(error @ TransactionError::Publish { .. }) => {
66 // Inspect per-ref and per-log outcomes before deciding whether/how to retry.
67 eprintln!("Partial publication: {error:?}");
68 return Err(error.into());
69 }
70 Err(error) => return Err(error.into()),
71 }
72
73 refs.transaction(&[RefEdit::delete(
74 tag.clone(),
75 id,
76 Reflog::Append {
77 committer,
78 message: b"remove temporary tag".to_vec(),
79 },
80 )])?;
81
82 assert_eq!(refs.reflog(&head)?.unwrap().len(), 1);
83 assert_eq!(refs.reflog(&tag)?.unwrap().len(), 2);
84 assert_eq!(refs.read(&tag)?, None);
85
86 // A retention ref may already exist at this value, but must never overwrite another value.
87 let keep = RefName::new("refs/jj/keep-example")?;
88 refs.transaction(&[RefEdit::new(
89 keep.clone(),
90 Some(girt::refs::Target::Direct(id)),
91 Expected::AbsentOr(girt::refs::Target::Direct(id)),
92 Reflog::Preserve,
93 )])?;
94 refs.transaction(&[RefEdit::new(keep, None, Expected::Exists, Reflog::Delete)])?;
95 Ok(())
96}Sourcepub fn transaction_files_with_options(
&self,
edits: &[RefEdit],
options: FilesTransactionOptions,
cancel: &AtomicBool,
) -> Result<Vec<RefEditOutcome>, TransactionError>
pub fn transaction_files_with_options( &self, edits: &[RefEdit], options: FilesTransactionOptions, cancel: &AtomicBool, ) -> Result<Vec<RefEditOutcome>, TransactionError>
Applies a files-only transaction with cancellable, per-acquisition lock waits.
Holds the packed lock first, then reference locks in name-byte order, then reflog locks in name-byte order. Each packed/reference acquisition gets its own configured budget; earlier locks remain held. Reflog contention always fails immediately. This sorted order need not match another writer’s edit order, so contention timing can differ.
Cancellation is checked before acquisitions, during waits at intervals of at most 20 ms
excluding scheduling/filesystem delays, and after preparation before publication. Callers
choosing LockWait::UntilCancelled must arrange cancellation if an unbounded wait is
unacceptable. Cancellation does not interrupt publication once it starts.
Uses the same locked rechecks, expected values, cleanup and publication as
Self::transaction. It never refreshes preconditions or retries preparation/publication.
No configuration or environment is read. The ordinary transaction entry point uses
FilesTransactionOptions::GIT_DEFAULT; no-reflog single-name operations fail immediately.
§Errors
Reftable returns TransactionError::Prepare with ReferenceError::Unsupported before
effects. Cancellation, timeout and other preparation failures preserve ref/log contents.
Publication failures retain their effects in TransactionError::Publish; never retry
them without inspecting current state.
use std::sync::atomic::AtomicBool;
use girt::refs::{Expected, FilesTransactionOptions, RefEdit, RefName, Reflog, Target};
use girt::{InitKind, ObjectFormat, Repository};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r.git"),
InitKind::Bare,
)?;
let id = repo.loose_objects().write_blob(b"manifest")?;
let name = RefName::new("refs/tags/exported")?;
let edits = [RefEdit::new(
name.clone(),
Some(Target::Direct(id)),
Expected::Absent,
Reflog::Preserve,
)];
let options = FilesTransactionOptions::GIT_DEFAULT.with_reference_lock_wait(
girt::refs::LockWait::For(std::time::Duration::from_millis(25)),
); // Shorten loose-ref waits while retaining the one-second packed-refs wait.
let outcomes = repo.references()?.transaction_files_with_options(
&edits,
options,
&AtomicBool::new(false),
)?;
assert_eq!(outcomes.len(), 1);Sourcepub fn transaction_files_with_stored_log_ids(
&self,
edits: &[RefEdit],
options: FilesTransactionOptions,
cancel: &AtomicBool,
) -> Result<Vec<RefEditOutcome>, TransactionError>
pub fn transaction_files_with_stored_log_ids( &self, edits: &[RefEdit], options: FilesTransactionOptions, cancel: &AtomicBool, ) -> Result<Vec<RefEditOutcome>, TransactionError>
Applies a files-only transaction whose reflogs identify stored values without resolving symbolic references.
Every edit must use Dereference::Stored and a direct
replacement or deletion. The old reflog ID is the stored direct ID, or null for a
symbolic or absent value; the new ID is the replacement ID, or null for deletion.
Symbolic chains are never traversed or locked. Only the edited name is rechecked under
its reference lock, and the caller’s exact expected value remains authoritative.
Conditional append policies compare stored targets.
Uses the same packed-reference coordination, reflog selection, lock waits, cancellation,
cleanup and publication outcomes as Self::transaction_files_with_options. In particular,
existing-log selection happens under the log lock, and cancellation cannot interrupt
publication once it starts. No caller-supplied reflog IDs are accepted.
§Errors
Rejects reftable, dereferencing edits and symbolic replacements before acquiring locks.
All edits are validated before preparation. Preparation errors preserve reference and log
contents; TransactionError::Publish reports any partial publication and must not be
retried without inspecting current state.
use std::sync::atomic::AtomicBool;
use girt::refs::{Expected, FilesTransactionOptions, RefEdit, RefName, Reflog, Target};
use girt::{InitKind, ObjectFormat, Repository, Signature};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r.git"),
InitKind::Bare,
)?;
let old = repo.loose_objects().write_blob(b"old")?;
let new = repo.loose_objects().write_blob(b"replacement")?;
let refs = repo.references()?;
let base = RefName::new("refs/tags/base")?;
let alias = RefName::new("refs/tags/alias")?;
refs.update_without_reflog(&base, Target::Direct(old), Expected::Absent)?;
refs.update_without_reflog(&alias, Target::Symbolic(base.clone()), Expected::Absent)?;
let edit = RefEdit::new(
alias.clone(),
Some(Target::Direct(new)),
Expected::Value(Target::Symbolic(base.clone())),
Reflog::Append {
committer: Signature {
name: b"A".to_vec(),
email: b"a@example.com".to_vec(),
seconds: 1_700_000_000,
offset_minutes: 0,
},
message: b"replace alias".to_vec(),
},
);
let outcomes = refs.transaction_files_with_stored_log_ids(
&[edit],
FilesTransactionOptions::default(),
&AtomicBool::new(false),
)?;
assert_eq!(outcomes[0].name, alias);
let log = refs.reflog(&alias)?.ok_or("log absent")?;
assert!(log[0].old.is_null()); // The stored old value was symbolic, not the resolved `old` ID.
assert_eq!(log[0].new, new);
assert_eq!(refs.read(&base)?, Some(Target::Direct(old))); // The target was never edited.