Skip to main content

References

Struct References 

Source
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<'_>

Source

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?
examples/publish_branch.rs (line 43)
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

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);
Source

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.
Source

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?
examples/publish_branch.rs (line 36)
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

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);
Source

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")?]
);
Source

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<'_>

Source

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<'_>

Source

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?
examples/reflog_resources.rs (line 58)
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<'_>

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<'_>

Source

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<'_>

Source

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.

Source

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?
examples/reference_transaction.rs (line 82)
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<'_>

Source

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.

Source

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>

Source

pub const DEFAULT_SYMREF_DEPTH: usize = 4

The number of symbolic hops Self::resolve follows: four, as git rev-parse does.

Source

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?
examples/publish_branch.rs (line 34)
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
Hide additional examples
examples/reference_transaction.rs (line 84)
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}
examples/fetch_remote.rs (line 97)
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}
examples/clone_repository.rs (line 100)
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}
Source

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);
Source

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.
Source

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?
examples/publish_branch.rs (line 33)
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

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?
examples/reftable_resources.rs (lines 32-36)
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
Hide additional examples
examples/fetch_local.rs (lines 46-50)
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}
examples/fetch_remote.rs (lines 42-46)
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}
examples/clone_repository.rs (lines 48-52)
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}
Source

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?
examples/publish_branch.rs (line 25)
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

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()
);
Source

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?
examples/publish_branch.rs (line 40)
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<'_>

Source

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?
examples/reference_transaction.rs (line 59)
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

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);
Source

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.

Trait Implementations§

Source§

impl<'a> Clone for References<'a>

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl<'a> Copy for References<'a>

Source§

impl<'a> Debug for References<'a>

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

§

impl<'a> Freeze for References<'a>

§

impl<'a> RefUnwindSafe for References<'a>

§

impl<'a> Send for References<'a>

§

impl<'a> Sync for References<'a>

§

impl<'a> Unpin for References<'a>

§

impl<'a> UnsafeUnpin for References<'a>

§

impl<'a> UnwindSafe for References<'a>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more