pub struct Objects { /* private fields */ }Expand description
Reads loose objects and validated, pinned local pack/index files.
§Common operations
Use Self::read for an object payload and Self::refresh to discover newly published
packs without replacing a valid reader on failure. Self::compare_trees reports structural
tree changes; Self::detect_rewrites infers content matches separately. Loose-object writes
belong to LooseObjects, and reference updates belong to crate::refs.
Obtain this synchronous, blocking reader through crate::Repository::objects. Opening streams
index bytes in the repository’s object format in filename order and verifies index v1/v2
structure (table sizes, sorted identities, fanout, offsets that tile the pack), pack v2/v3
headers/counts, and index/pack trailer agreement. Like Git, it does not hash the whole index
at open. It reads only the pack header and trailer; body corruption is detected when used.
Reads check entry framing, v2 CRCs, zlib streams and delta programs throughout reconstruction,
and verify the requested object’s decoded identity, so a damaged index triggers verified
fallback or an error rather than returning other content. Legacy v1 indexes have no entry CRCs.
Self::verify_packs explicitly checks index checksums, whole-pack checksums and CRCs; neither
opening nor whole-pack verification is a full fsck.
Pack bytes are never retained in full. Bounded index bytes and offset tables stay in memory
so identity lookups avoid repeated file seeks. Each pair pins two handles, shared internally
through Arc. Reads are positioned, so simultaneous reads share no cursor or lock; a typical
entry’s header and compressed bytes come from one read. Cloning shares pack handles, offset
tables and delta base caches, while copying the bounded store topology. Each pack keeps
recently decoded entries, up to 96 MiB, as delta bases; a cached entry is still
identity-checked before it is returned. Failed packed entries are remembered and skipped by
subsequent content reads, shared by clones and preserved across refresh of unchanged pack
generations (pinned size, modification time, native identity and trailer checksum).
Metadata-only freshening of a still-pinned native artifact also preserves damage.
PackLimits bounds aggregate opening work, index memory and handles; ReadLimits bounds
each packed decode. Retention operations additionally share a single 256 KiB header window
across packs on the calling thread, dropped when the operation ends. Windows artifact handles
allow read/delete sharing and deny concurrent data writers.
Concurrent reads and caller-owned results multiply memory use. Cancellation is available through
crate::Repository::objects_controlled and Self::read_controlled.
Within each store, packs are searched in index filename order, followed by fresh loose reads.
Stores are searched primary first, then alternates in discovery order. A corrupt packed entry
falls through to the remaining copies, each fully verified. A corrupt loose copy normally
fails immediately; after packed damage, it also falls through to the remaining stores.
Known damaged entries remain excluded for this handle’s lifetime. Refresh retains small
generation records without retaining old artifact handles or index tables. These records grow
with distinct pack generations, including freshened metadata, opened by the handle and its
clones. Packs remain readable
after Git repacks/deletes the original files where the OS permits unlinking open files; refresh
to discover new packs. Artifacts must remain immutable while readers use them: pinned handles do
not defend against in-place writes (including truncation). Atomic path replacement leaves pinned
artifacts intact. Opening and refresh validate each pinned pair, but do not capture an atomic
directory or topology snapshot. Exclude writers when a point-in-time view is required.
Self::refresh makes one bounded attempt and leaves this reader unchanged on failure. Reads
never refresh implicitly. The object directory and its ancestors must be trusted, as with
LooseObjects. This is not a snapshot of loose files or repository references.
Overlapping readers may share validated index buffers and tables through a process-local weak cache. Keys include native identity, size, mtime and Unix ctime or Windows ChangeTime. Shared validation owns independent artifact pins while readers or in-flight opens use that generation. Dropping its last reader releases those pins and buffers once any in-flight open finishes. Dead weak cache records retain neither buffers nor handles, so they do not keep deleted packs alive or Windows names delete-pending. Other snapshots or external processes can still pin the files. Opens after all readers drop validate again. Metadata keys do not replace immutable publication or caller-owned exclusion of in-place writers.
REF_DELTA bases must be indexed in the same pack. Thin packs and cross-pack/loose bases return
ObjectReadError::MissingBase, even if the base exists elsewhere. Traversal is iterative,
detects cycles, and enforces ReadLimits. Multi-pack indexes accelerate verified reads when
usable; bitmap/reverse indexes are ignored; ordinary pack indexes remain required. Indexes are
publication markers: unindexed packs are ignored, while an index without its pack is an opening
error. A publisher must finish the pack before publishing its index. History queries use the
repository handle’s immutable shallow boundaries; raw reads preserve commit parents. Reopen the
repository or refresh its shallow snapshot, then create a new reader after Git deepening. Loose
objects, packs and boundaries are not captured in one filesystem transaction; exclude concurrent
depth changes while opening. Local alternate stores use the same format and aggregate pack
limits; see crate::Repository::objects_with_alternates for topology and precedence. Complete
partial-clone stores are readable; no missing-object fetch is attempted. Loose writes
use LooseObjects; validated received-pack installation uses
crate::fetch::ReceivedFetch::install.
The repository selects SHA-1 or SHA-256 for both artifacts. Their headers and filenames do
not select the format. Every .idx filename is a publication marker, including noncanonical
basenames; matching checksums and object identities validate contents independently of names.
§Example
Read an object by full identity, then parse its exact bytes when structured fields are needed.
Absence is Ok(None), not an error:
use girt::{InitKind, ObjectFormat, ObjectKind, PackLimits, ReadLimits, Repository, Tree};
let directory = tempfile::tempdir()?;
let repository = Repository::init(
ObjectFormat::Sha1,
directory.path().join("repo.git"),
InitKind::Bare,
)?;
let tree = Tree::new(ObjectFormat::Sha1, vec![])?;
let id = repository.loose_objects().write_tree(&tree)?;
let objects = repository.objects_with(PackLimits::default())?;
let object = objects
.read_with(id, ReadLimits::default())?
.expect("tree was just written");
assert_eq!(object.kind(), ObjectKind::Tree);
assert!(
Tree::parse(ObjectFormat::Sha1, object.data())?
.entries()
.is_empty()
);
let missing = ObjectFormat::Sha1.hash_object(ObjectKind::Blob, b"never written");
assert!(objects.read_with(missing, ReadLimits::default())?.is_none());Implementations§
Source§impl Objects
impl Objects
Sourcepub fn without_bitmaps(self) -> Self
pub fn without_bitmaps(self) -> Self
Returns this snapshot with bitmap shortcuts disabled, including after refresh.
Use this for independent graph verification or when derived metadata’s producer is not trusted. Queries still use the ordinary verified object reader.
Sourcepub fn reachable_difference(
&self,
include: &[ObjectId],
exclude: &[ObjectId],
) -> Result<Reachability, Error>
pub fn reachable_difference( &self, include: &[ObjectId], exclude: &[ObjectId], ) -> Result<Reachability, Error>
Finds every object reachable from include except those reachable from exclude.
Roots may be commits, tags, trees or blobs. Tags are peeled, commit trees and parents are followed, tree gitlinks are skipped, and this snapshot’s shallow boundaries stop parent traversal. Sets are independent: exclusion never prunes the include walk. Missing roots or reachable objects are errors, so a server should pass only recognized have IDs.
Valid pack/MIDX bitmaps may replace covered commit walks. Missing, stale, unsupported,
malformed or uncovered bitmap data falls back to walking. Bitmap checks establish inventory
consistency, not graph truth; use Self::without_bitmaps for an independent walk.
§Errors
Returns Error for missing, malformed or unreadable objects, mismatched edge types, or
exhausted aggregate limits. No partial set is returned.
Sourcepub fn reachable_difference_with(
&self,
include: &[ObjectId],
exclude: &[ObjectId],
limits: QueryLimits,
cancel: &AtomicBool,
) -> Result<Reachability, Error>
pub fn reachable_difference_with( &self, include: &[ObjectId], exclude: &[ObjectId], limits: QueryLimits, cancel: &AtomicBool, ) -> Result<Reachability, Error>
Finds a reachability difference under aggregate bounds and cooperative cancellation.
Limits cover both closures, metadata loading and repeated XOR expansion. Optional metadata that exceeds its bounds is skipped; graph-walk limits remain authoritative. A missing bitmap never makes an object unreachable. Filesystem reads and one bounded metadata parse or inventory verification cannot be interrupted.
§Errors
Returns the errors of Self::reachable_difference or Error::Cancelled.
Source§impl Objects
impl Objects
Sourcepub fn encode_pack_bitmap(
&self,
pack: ObjectId,
options: WriteOptions,
limits: WriteLimits,
cancel: &AtomicBool,
) -> Result<Written, WriteError>
pub fn encode_pack_bitmap( &self, pack: ObjectId, options: WriteOptions, limits: WriteLimits, cancel: &AtomicBool, ) -> Result<Written, WriteError>
Encodes a pack bitmap and reverse index from verified contents without changing storage.
The selected primary pack must contain its entire graph closure, including unselected commits’ dependencies. Existing bitmap claims are never trusted. Explicit selection is deduplicated; the default selects graph tips first and samples remaining commits by ID. Selection is deterministic, not Git’s compression/commit-selection heuristic. Records use EWAH runs/literals without XOR dependencies and include a lookup table.
Keep maintenance exclusion through encoding and installation: retained handles alone cannot prevent another process from retiring the named pack. Source generations are rechecked before returning. The returned artifacts are private bytes, not installed files.
§Errors
Returns WriteError for unavailable packs, incomplete or shallow graphs, corrupt sources,
invalid selections, cancellation or exhausted limits. No partial output is returned.
Sourcepub fn encode_multi_pack_bitmap(
&self,
midx: WriteOptions,
options: WriteOptions,
limits: WriteLimits,
cancel: &AtomicBool,
) -> Result<MultiPackWritten, WriteError>
pub fn encode_multi_pack_bitmap( &self, midx: WriteOptions, options: WriteOptions, limits: WriteLimits, cancel: &AtomicBool, ) -> Result<MultiPackWritten, WriteError>
Encodes a primary-store MIDX and associated bitmap/reverse index without changing files.
Uses the same graph/selection contract as Self::encode_pack_bitmap. The preferred pack
comes first in pseudo-pack order and wins duplicate IDs; absent an explicit preference, the
largest pack wins, with checksum order breaking ties. Output includes RIDX and BTMP chunks.
§Errors
Returns WriteError for incomplete inventories, invalid inputs, cancellation or bounds.
Source§impl Objects
impl Objects
Sourcepub fn blame(
&self,
commit: ObjectId,
path: impl AsRef<[u8]>,
) -> Result<Vec<Hunk>, Error>
pub fn blame( &self, commit: ObjectId, path: impl AsRef<[u8]>, ) -> Result<Vec<Hunk>, Error>
Attributes a regular file at a commit using default Limits.
Wraps Self::blame_with; see the module contract.
§Errors
Returns missing, malformed, unsupported, shallow-history or resource errors.
Sourcepub fn blame_with(
&self,
commit: ObjectId,
path: impl AsRef<[u8]>,
limits: Limits,
cancel: &AtomicBool,
) -> Result<Vec<Hunk>, Error>
pub fn blame_with( &self, commit: ObjectId, path: impl AsRef<[u8]>, limits: Limits, cancel: &AtomicBool, ) -> Result<Vec<Hunk>, Error>
Attributes lines with explicit whole-walk bounds and cancellation.
An identical parent file takes precedence; otherwise parents are considered in stored order, passing unchanged lines to the first matching parent. Missing objects are errors, never history roots. Both SHA-1 and SHA-256 object stores are supported. No repository data is changed.
§Errors
Returns Error without partial results. Increase the relevant limit or fetch missing
history before retrying. Cancellation is checked between reads and during comparisons.
Source§impl Objects
impl Objects
Sourcepub fn with_commit_graph(self, graph: CommitGraph) -> Result<Self, GraphError>
pub fn with_commit_graph(self, graph: CommitGraph) -> Result<Self, GraphError>
Attaches structurally checked auxiliary metadata for subsequent history queries.
This explicitly trusts the index’s object claims without decoding the named commits. Use
CommitGraph::verify first when object verification is required. Missing index records
use ordinary verified reads. Commit/parent query bounds and shallow boundaries still apply;
per-object byte limits apply only to fallback reads. Typed object reads always use objects.
Clones share the immutable index; storage refresh retains this index until replaced.
§Errors
Rejects a different hash format or cyclic indexed ancestry. Parsing has already rejected parent positions that name no record.
Examples found in repository?
8fn main() -> Result<(), Box<dyn std::error::Error>> {
9 let mut args = std::env::args().skip(1);
10 let path = args.next().ok_or("repository path required")?;
11 let rounds: usize = args.next().map_or(Ok(3), |r| r.parse())?;
12 for _ in 0..rounds {
13 let start = Instant::now();
14 let repo = Repository::open(&path)?;
15 let objects = repo
16 .objects_with(PackLimits::trusted())?
17 .without_commit_graph();
18 let opened = start.elapsed();
19 let start = Instant::now();
20 let bytes = std::fs::read(repo.object_dir().join("info/commit-graph"))?;
21 let read = start.elapsed();
22 let start = Instant::now();
23 let graph = CommitGraph::parse(&bytes, repo.object_format())?;
24 let parsed = start.elapsed();
25 let start = Instant::now();
26 let objects = objects.with_commit_graph(graph)?;
27 let attached = start.elapsed();
28 let start = Instant::now();
29 drop(objects);
30 let dropped = start.elapsed();
31 let start = Instant::now();
32 let full = Repository::open(&path)?.objects_with(PackLimits::trusted())?;
33 let total = start.elapsed();
34 drop(full);
35 eprintln!(
36 "repo+packs={opened:?} read={read:?} parse={parsed:?} attach={attached:?} drop={dropped:?} default_open={total:?}"
37 );
38 }
39 Ok(())
40}Sourcepub fn without_commit_graph(self) -> Self
pub fn without_commit_graph(self) -> Self
Removes the auxiliary index so history queries read commit objects directly.
Useful when object claims must be checked during each query rather than trusted from an index. This does not alter repository configuration or other readers.
Examples found in repository?
8fn main() -> Result<(), Box<dyn std::error::Error>> {
9 let mut args = std::env::args().skip(1);
10 let path = args.next().ok_or("repository path required")?;
11 let rounds: usize = args.next().map_or(Ok(3), |r| r.parse())?;
12 for _ in 0..rounds {
13 let start = Instant::now();
14 let repo = Repository::open(&path)?;
15 let objects = repo
16 .objects_with(PackLimits::trusted())?
17 .without_commit_graph();
18 let opened = start.elapsed();
19 let start = Instant::now();
20 let bytes = std::fs::read(repo.object_dir().join("info/commit-graph"))?;
21 let read = start.elapsed();
22 let start = Instant::now();
23 let graph = CommitGraph::parse(&bytes, repo.object_format())?;
24 let parsed = start.elapsed();
25 let start = Instant::now();
26 let objects = objects.with_commit_graph(graph)?;
27 let attached = start.elapsed();
28 let start = Instant::now();
29 drop(objects);
30 let dropped = start.elapsed();
31 let start = Instant::now();
32 let full = Repository::open(&path)?.objects_with(PackLimits::trusted())?;
33 let total = start.elapsed();
34 drop(full);
35 eprintln!(
36 "repo+packs={opened:?} read={read:?} parse={parsed:?} attach={attached:?} drop={dropped:?} default_open={total:?}"
37 );
38 }
39 Ok(())
40}More examples
12fn main() -> Result<(), Box<dyn std::error::Error>> {
13 let mut args = std::env::args().skip(1);
14 let path = args.next().ok_or("repository path required")?;
15 let tip: ObjectId = args.next().ok_or("tip ID required")?.parse()?;
16 let base: ObjectId = args.next().ok_or("base ID required")?.parse()?;
17 let trust = args.next().ok_or("objects, indexed or verified required")?;
18 let query = args.next().ok_or("walk, range or ancestor required")?;
19 let start = Instant::now();
20 let repo = Repository::open(path)?;
21 let mut objects = repo.objects_with(PackLimits::trusted())?;
22 eprintln!("open={:?}", start.elapsed());
23 match trust.as_str() {
24 "objects" => objects = objects.without_commit_graph(),
25 "indexed" => {}
26 "verified" => {
27 let graph = repo.commit_graph()?.ok_or("commit-graph required")?;
28 let start = Instant::now();
29 graph.verify(&objects)?;
30 eprintln!("object_verification={:?}", start.elapsed());
31 }
32 _ => return Err("unknown trust choice".into()),
33 }
34 let start = Instant::now();
35 let limits = HistoryLimits::trusted();
36 match query.as_str() {
37 "walk" => println!("{}", objects.walk_with(&[tip], limits)?.len()),
38 "range" => println!(
39 "{}",
40 objects.walk_range_with(&[tip], &[base], limits)?.len()
41 ),
42 "ancestor" => println!("{}", objects.is_ancestor_with(base, tip, limits)?),
43 _ => return Err("unknown query".into()),
44 }
45 eprintln!("query={:?}", start.elapsed());
46 Ok(())
47}Source§impl Objects
impl Objects
Sourcepub fn encode_multi_pack_index(
&self,
options: WriteOptions,
) -> Result<Vec<u8>, WriteError>
pub fn encode_multi_pack_index( &self, options: WriteOptions, ) -> Result<Vec<u8>, WriteError>
Encodes a monolithic v1 MIDX for the primary store with default bounds.
See Self::encode_multi_pack_index_with for source validation and publication contracts.
§Errors
Returns WriteError for invalid sources, selection or exhausted bounds.
use girt::Repository;
use girt::multi_pack_index::{MultiPackIndex, WriteOptions};
let repository = Repository::open("repo.git")?;
let objects = repository.objects()?;
let mut options = WriteOptions::default();
options.preferred_pack = objects
.stores()
.next()
.and_then(|store| store.packs().next())
.map(|pack| pack.checksum());
let bytes = objects.encode_multi_pack_index(options)?;
let index = MultiPackIndex::parse(&bytes, repository.object_format())?;
println!("{} unique packed objects", index.len());
// Keep these bytes private until installation can exclude repacking and pruning.Sourcepub fn encode_multi_pack_index_with(
&self,
options: WriteOptions,
limits: WriteLimits,
cancel: &AtomicBool,
) -> Result<Vec<u8>, WriteError>
pub fn encode_multi_pack_index_with( &self, options: WriteOptions, limits: WriteLimits, cancel: &AtomicBool, ) -> Result<Vec<u8>, WriteError>
Encodes verified primary-store indexes into caller-owned bytes without filesystem mutation.
Alternate packs are excluded. Every source index’s checksum and structure are checked before
its identities are copied. Duplicate objects use options; sorted IDs and names make output
deterministic for the same inputs. Pack payloads are not scanned. The caller must exclude
repacking or pruning while constructing and installing an artifact; a retained snapshot can
otherwise name packs already removed from the directory. Source generation changes refuse
encoding. Use the repository publication API for coordinated installation.
§Errors
Returns WriteError for changed or damaged indexes, unknown preferred packs, unsupported
filenames, cancellation or exhausted input/output bounds. No partial bytes are returned.
Source§impl Objects
impl Objects
Sourcepub fn walk_path_first_parent(
&self,
root: ObjectId,
path: &[u8],
) -> Result<Vec<ObjectId>, PathHistoryError>
pub fn walk_path_first_parent( &self, root: ObjectId, path: &[u8], ) -> Result<Vec<ObjectId>, PathHistoryError>
Finds commits changing a literal path along the first-parent chain, newest first.
Compares entry identity and mode against the first parent, or an empty snapshot at a root or declared shallow boundary. Includes additions and deletions. Directories compare their subtree identities; pathspecs and rename following are not performed. An explicitly attached graph can skip tree comparisons using Bloom negatives. Missing or uncertain filters always require ordinary comparison. Uses default history and per-read bounds.
§Errors
Reports invalid paths, missing/corrupt commits or trees on the selected path, cycles and exhausted bounds. Bloom-skipped trees and side-parent histories are not read or verified.
Sourcepub fn walk_path_first_parent_with(
&self,
root: ObjectId,
path: &[u8],
limits: HistoryLimits,
) -> Result<Vec<ObjectId>, PathHistoryError>
pub fn walk_path_first_parent_with( &self, root: ObjectId, path: &[u8], limits: HistoryLimits, ) -> Result<Vec<ObjectId>, PathHistoryError>
Finds first-parent path changes with explicit history and per-object read limits.
§Errors
Returns the same errors as Self::walk_path_first_parent. Commit bounds count visited
first-parent commits; parent bounds count followed first-parent edges. Each tree read uses
limits.read; memory includes returned IDs and the visited set, plus one decoded tree.
Source§impl Objects
impl Objects
Sourcepub fn walk_range(
&self,
include: &[ObjectId],
exclude: &[ObjectId],
) -> Result<Vec<ObjectId>, HistoryError>
pub fn walk_range( &self, include: &[ObjectId], exclude: &[ObjectId], ) -> Result<Vec<ObjectId>, HistoryError>
Collects commits reachable from include but not from exclude, using default bounds.
Every result appears once; result order is unspecified. With an attached index covering every root and a checked strict generation order, traversal stops when no interesting commit remains queued. Otherwise it collects both closures. Shallow boundaries stop edges.
§Errors
Returns history read, kind, parse, cycle or resource errors for explored commits. Indexed
metadata uses the explicit trust contract of Self::with_commit_graph. Ancestors beyond
a generation stopping boundary are not read or verified.
Sourcepub fn walk_range_with(
&self,
include: &[ObjectId],
exclude: &[ObjectId],
limits: HistoryLimits,
) -> Result<Vec<ObjectId>, HistoryError>
pub fn walk_range_with( &self, include: &[ObjectId], exclude: &[ObjectId], limits: HistoryLimits, ) -> Result<Vec<ObjectId>, HistoryError>
Collects a reachability difference under explicit commit, edge and fallback-read bounds.
§Errors
Returns the same errors as Self::walk_range. Bounds count both sides, including excluded
commits actually explored. Empty includes return immediately without storage access.
Examples found in repository?
12fn main() -> Result<(), Box<dyn std::error::Error>> {
13 let mut args = std::env::args().skip(1);
14 let path = args.next().ok_or("repository path required")?;
15 let tip: ObjectId = args.next().ok_or("tip ID required")?.parse()?;
16 let base: ObjectId = args.next().ok_or("base ID required")?.parse()?;
17 let trust = args.next().ok_or("objects, indexed or verified required")?;
18 let query = args.next().ok_or("walk, range or ancestor required")?;
19 let start = Instant::now();
20 let repo = Repository::open(path)?;
21 let mut objects = repo.objects_with(PackLimits::trusted())?;
22 eprintln!("open={:?}", start.elapsed());
23 match trust.as_str() {
24 "objects" => objects = objects.without_commit_graph(),
25 "indexed" => {}
26 "verified" => {
27 let graph = repo.commit_graph()?.ok_or("commit-graph required")?;
28 let start = Instant::now();
29 graph.verify(&objects)?;
30 eprintln!("object_verification={:?}", start.elapsed());
31 }
32 _ => return Err("unknown trust choice".into()),
33 }
34 let start = Instant::now();
35 let limits = HistoryLimits::trusted();
36 match query.as_str() {
37 "walk" => println!("{}", objects.walk_with(&[tip], limits)?.len()),
38 "range" => println!(
39 "{}",
40 objects.walk_range_with(&[tip], &[base], limits)?.len()
41 ),
42 "ancestor" => println!("{}", objects.is_ancestor_with(base, tip, limits)?),
43 _ => return Err("unknown query".into()),
44 }
45 eprintln!("query={:?}", start.elapsed());
46 Ok(())
47}More examples
12fn main() -> Result<(), Box<dyn std::error::Error>> {
13 let mut args = std::env::args().skip(1);
14 let path = args.next().ok_or("expected repository path")?;
15 let rounds: usize = args.next().ok_or("expected number of rounds")?.parse()?;
16 let start = Instant::now();
17 let repo = Repository::open(path)?;
18 let objects = repo.objects_with(PackLimits::trusted())?;
19 println!("open {:.1} ms", ms(start.elapsed()));
20 for pair in args {
21 let (left, right) = pair.split_once("...").ok_or("expected LEFT...RIGHT")?;
22 let (left, right): (ObjectId, ObjectId) = (left.parse()?, right.parse()?);
23 let mut counts = None;
24 let mut best_counts = Duration::MAX;
25 let mut walked = 0;
26 let mut best_walk = Duration::MAX;
27 for _ in 0..rounds {
28 let start = Instant::now();
29 counts = Some(black_box(objects.ahead_behind_with(
30 left,
31 right,
32 HistoryLimits::trusted(),
33 )?));
34 best_counts = best_counts.min(start.elapsed());
35 let start = Instant::now();
36 walked =
37 black_box(objects.walk_range_with(&[right], &[left], HistoryLimits::trusted())?)
38 .len();
39 best_walk = best_walk.min(start.elapsed());
40 }
41 let counts = counts.ok_or("expected at least one round")?;
42 println!(
43 "{pair}: ahead {} behind {} in {:.2} ms; range {walked} in {:.2} ms",
44 counts.ahead,
45 counts.behind,
46 ms(best_counts),
47 ms(best_walk),
48 );
49 }
50 Ok(())
51}Source§impl Objects
impl Objects
Sourcepub fn ahead_behind(
&self,
left: ObjectId,
right: ObjectId,
) -> Result<AheadBehind, HistoryError>
pub fn ahead_behind( &self, left: ObjectId, right: ObjectId, ) -> Result<AheadBehind, HistoryError>
Counts the commits only left reaches and the commits only right reaches.
This is git rev-list --left-right --count left...right: with left a branch and right
its upstream, AheadBehind::ahead is how many commits the branch has that the upstream
lacks. Equal tips give zero on both sides; disconnected histories count every commit.
With a commit-graph covering both tips, the walk visits commits in descending generation order and stops once every queued commit is reachable from both tips, so shared history below the merge base is not read. Without one, both complete ancestries are read. Both give exact counts; no timestamp heuristics are used.
let base = commit(vec![], b"base\n")?;
let local = commit(vec![base], b"local\n")?;
let upstream = commit(vec![base], b"upstream 1\n")?;
let upstream = commit(vec![upstream], b"upstream 2\n")?;
let counts = repo.objects()?.ahead_behind(local, upstream)?;
assert_eq!((counts.ahead, counts.behind), (1, 2));§Errors
Returns the errors of Self::walk_range.
Sourcepub fn ahead_behind_with(
&self,
left: ObjectId,
right: ObjectId,
limits: HistoryLimits,
) -> Result<AheadBehind, HistoryError>
pub fn ahead_behind_with( &self, left: ObjectId, right: ObjectId, limits: HistoryLimits, ) -> Result<AheadBehind, HistoryError>
Counts both sides of a symmetric range under explicit bounds.
§Errors
Returns the errors of Self::walk_range_with; bounds count commits explored on both
sides.
Examples found in repository?
12fn main() -> Result<(), Box<dyn std::error::Error>> {
13 let mut args = std::env::args().skip(1);
14 let path = args.next().ok_or("expected repository path")?;
15 let rounds: usize = args.next().ok_or("expected number of rounds")?.parse()?;
16 let start = Instant::now();
17 let repo = Repository::open(path)?;
18 let objects = repo.objects_with(PackLimits::trusted())?;
19 println!("open {:.1} ms", ms(start.elapsed()));
20 for pair in args {
21 let (left, right) = pair.split_once("...").ok_or("expected LEFT...RIGHT")?;
22 let (left, right): (ObjectId, ObjectId) = (left.parse()?, right.parse()?);
23 let mut counts = None;
24 let mut best_counts = Duration::MAX;
25 let mut walked = 0;
26 let mut best_walk = Duration::MAX;
27 for _ in 0..rounds {
28 let start = Instant::now();
29 counts = Some(black_box(objects.ahead_behind_with(
30 left,
31 right,
32 HistoryLimits::trusted(),
33 )?));
34 best_counts = best_counts.min(start.elapsed());
35 let start = Instant::now();
36 walked =
37 black_box(objects.walk_range_with(&[right], &[left], HistoryLimits::trusted())?)
38 .len();
39 best_walk = best_walk.min(start.elapsed());
40 }
41 let counts = counts.ok_or("expected at least one round")?;
42 println!(
43 "{pair}: ahead {} behind {} in {:.2} ms; range {walked} in {:.2} ms",
44 counts.ahead,
45 counts.behind,
46 ms(best_counts),
47 ms(best_walk),
48 );
49 }
50 Ok(())
51}Source§impl Objects
impl Objects
Sourcepub fn simplify_commit(
&self,
commit: &Commit,
parents: &[ObjectId],
paths: &Pathspec,
limits: TreeCompareLimits,
cancel: &AtomicBool,
) -> Result<Simplification, SimplifyError>
pub fn simplify_commit( &self, commit: &Commit, parents: &[ObjectId], paths: &Pathspec, limits: TreeCompareLimits, cancel: &AtomicBool, ) -> Result<Simplification, SimplifyError>
Decides whether commit appears in git log -- <paths> and which parents the walk follows.
A commit is TREESAME to a parent when no selected path differs between their trees. Following Git’s default simplification:
- a root commit is shown when its tree holds a selected path;
- a commit with one parent is shown unless it is TREESAME to it, and the parent is followed;
- a merge TREESAME to some parent is hidden, and only the first such parent is followed, so side branches that did not change the paths are not walked;
- any other merge is shown and every parent is followed.
Pass parents as the IDs the walk should consider, normally Commit::parents; pass an
empty slice at a shallow boundary, where the commit is compared with an empty tree like a
root. Parent commits are read with TreeCompareLimits::read to find their trees.
--full-history, --simplify-merges, parent rewriting and --follow are not provided.
use std::sync::atomic::AtomicBool;
use girt::pathspec::{Context, Pathspec};
use girt::tree::TreeEditor;
use girt::{Commit, CommitFields, InitKind, ObjectFormat, Repository, Signature};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r"),
InitKind::Bare,
)?;
let loose = repo.loose_objects();
let person = Signature {
name: b"A. Writer".to_vec(),
email: b"writer@example.com".to_vec(),
seconds: 1_700_000_000,
offset_minutes: 0,
};
let mut editor = TreeEditor::new(&loose);
let mut commit = |path: &[u8], parents: Vec<_>| -> Result<_, Box<dyn std::error::Error>> {
editor.set(path, loose.write_blob(path)?)?;
let fields = CommitFields {
tree: editor.write()?,
parents,
author: person.clone(),
committer: person.clone(),
extra_headers: vec![],
message: b"change\n".to_vec(),
};
Ok(loose.write_commit(&Commit::new(fields)?)?)
};
let first = commit(b"src/lib.rs", vec![])?;
let second = commit(b"docs/guide.md", vec![first])?;
let objects = repo.objects()?;
let paths = Pathspec::parse(["src"], &Context::new())?;
let cancel = AtomicBool::new(false);
let step = |id| -> Result<_, Box<dyn std::error::Error>> {
let commit = objects.read_commit(id)?;
let parents = commit.parents().to_vec();
Ok(objects.simplify_commit(&commit, &parents, &paths, Default::default(), &cancel)?)
};
// `second` only touched docs, so `git log -- src` skips it and continues to `first`.
assert!(!step(second)?.shown);
assert_eq!(step(second)?.follow, [first]);
assert!(step(first)?.shown);§Errors
Returns SimplifyError::Parent when a parent cannot be read and
SimplifyError::Trees when a tree comparison fails, including
TreeCompareError::Unsupported for attr: magic.
Source§impl Objects
impl Objects
Sourcepub fn walk_with(
&self,
roots: &[ObjectId],
limits: HistoryLimits,
) -> Result<Vec<ObjectId>, HistoryError>
pub fn walk_with( &self, roots: &[ObjectId], limits: HistoryLimits, ) -> Result<Vec<ObjectId>, HistoryError>
Collects all commits reachable from explicit roots, including the roots themselves.
Order is breadth-first: roots in supplied order, then parents in stored order. First discovery wins; duplicate roots and parents never duplicate output. This is not topological order: an ancestor supplied as a root can precede its descendant. Timestamps are ignored. Declared shallow roots are read and parsed but their parent edges are not followed, even when parent objects exist. Raw commit reads retain every parent. An empty root slice returns an empty vector without reading storage.
use girt::{
Commit, CommitFields, HistoryLimits, InitKind, ObjectFormat, PackLimits, Repository,
Signature, Tree,
};
let directory = tempfile::tempdir()?;
let repository = Repository::init(
ObjectFormat::Sha1,
directory.path().join("repo.git"),
InitKind::Bare,
)?;
let loose = repository.loose_objects();
let tree = loose.write_tree(&Tree::new(ObjectFormat::Sha1, vec![])?)?;
let person = Signature {
name: b"A. Writer".to_vec(),
email: b"writer@example.com".to_vec(),
seconds: 1_700_000_000,
offset_minutes: 0,
};
let commit = |parents, message: &[u8]| {
loose.write_commit(&Commit::new(CommitFields {
tree,
parents,
author: person.clone(),
committer: person.clone(),
extra_headers: vec![],
message: message.to_vec(),
})?)
};
let first = commit(vec![], b"first\n")?;
let second = commit(vec![first], b"second\n")?;
let objects = repository.objects_with(PackLimits::default())?;
assert_eq!(
objects.walk_with(&[second], HistoryLimits::default())?,
[second, first]
);
// Supplied root order wins over ancestry: the walk is not topological.
let limits = HistoryLimits::default();
assert_eq!(
objects.walk_with(&[first, second], limits)?,
[first, second]
);
assert!(objects.is_ancestor_with(first, second, HistoryLimits::default())?);
assert!(!objects.is_ancestor_with(second, first, HistoryLimits::default())?);§Errors
Returns HistoryError on any reachable missing, non-commit, malformed or corrupt object,
cycle, or exhausted bound. No partial result is returned and no files are changed. Commit
syntax uses Commit::parse without additional field validation; trees are not read.
Examples found in repository?
12fn main() -> Result<(), Box<dyn std::error::Error>> {
13 let mut args = std::env::args().skip(1);
14 let path = args.next().ok_or("repository path required")?;
15 let tip: ObjectId = args.next().ok_or("tip ID required")?.parse()?;
16 let base: ObjectId = args.next().ok_or("base ID required")?.parse()?;
17 let trust = args.next().ok_or("objects, indexed or verified required")?;
18 let query = args.next().ok_or("walk, range or ancestor required")?;
19 let start = Instant::now();
20 let repo = Repository::open(path)?;
21 let mut objects = repo.objects_with(PackLimits::trusted())?;
22 eprintln!("open={:?}", start.elapsed());
23 match trust.as_str() {
24 "objects" => objects = objects.without_commit_graph(),
25 "indexed" => {}
26 "verified" => {
27 let graph = repo.commit_graph()?.ok_or("commit-graph required")?;
28 let start = Instant::now();
29 graph.verify(&objects)?;
30 eprintln!("object_verification={:?}", start.elapsed());
31 }
32 _ => return Err("unknown trust choice".into()),
33 }
34 let start = Instant::now();
35 let limits = HistoryLimits::trusted();
36 match query.as_str() {
37 "walk" => println!("{}", objects.walk_with(&[tip], limits)?.len()),
38 "range" => println!(
39 "{}",
40 objects.walk_range_with(&[tip], &[base], limits)?.len()
41 ),
42 "ancestor" => println!("{}", objects.is_ancestor_with(base, tip, limits)?),
43 _ => return Err("unknown query".into()),
44 }
45 eprintln!("query={:?}", start.elapsed());
46 Ok(())
47}Sourcepub fn is_ancestor_with(
&self,
ancestor: ObjectId,
descendant: ObjectId,
limits: HistoryLimits,
) -> Result<bool, HistoryError>
pub fn is_ancestor_with( &self, ancestor: ObjectId, descendant: ObjectId, limits: HistoryLimits, ) -> Result<bool, HistoryError>
Tests whether ancestor is reachable from descendant, including equality.
§Errors
Without an attached commit-graph, validates both complete ancestries before answering.
With trusted indexed metadata and checked generation order, stops at the target
generation or an early match. Returns the same failures as Self::walk for explored data.
assert!(objects.is_ancestor_with(base, left, HistoryLimits::default())?);
assert!(objects.is_ancestor_with(left, left, HistoryLimits::default())?);
assert!(!objects.is_ancestor_with(left, right, HistoryLimits::default())?);Examples found in repository?
12fn main() -> Result<(), Box<dyn std::error::Error>> {
13 let mut args = std::env::args().skip(1);
14 let path = args.next().ok_or("repository path required")?;
15 let tip: ObjectId = args.next().ok_or("tip ID required")?.parse()?;
16 let base: ObjectId = args.next().ok_or("base ID required")?.parse()?;
17 let trust = args.next().ok_or("objects, indexed or verified required")?;
18 let query = args.next().ok_or("walk, range or ancestor required")?;
19 let start = Instant::now();
20 let repo = Repository::open(path)?;
21 let mut objects = repo.objects_with(PackLimits::trusted())?;
22 eprintln!("open={:?}", start.elapsed());
23 match trust.as_str() {
24 "objects" => objects = objects.without_commit_graph(),
25 "indexed" => {}
26 "verified" => {
27 let graph = repo.commit_graph()?.ok_or("commit-graph required")?;
28 let start = Instant::now();
29 graph.verify(&objects)?;
30 eprintln!("object_verification={:?}", start.elapsed());
31 }
32 _ => return Err("unknown trust choice".into()),
33 }
34 let start = Instant::now();
35 let limits = HistoryLimits::trusted();
36 match query.as_str() {
37 "walk" => println!("{}", objects.walk_with(&[tip], limits)?.len()),
38 "range" => println!(
39 "{}",
40 objects.walk_range_with(&[tip], &[base], limits)?.len()
41 ),
42 "ancestor" => println!("{}", objects.is_ancestor_with(base, tip, limits)?),
43 _ => return Err("unknown query".into()),
44 }
45 eprintln!("query={:?}", start.elapsed());
46 Ok(())
47}Sourcepub fn merge_bases_with(
&self,
left: ObjectId,
right: ObjectId,
limits: HistoryLimits,
) -> Result<Vec<ObjectId>, HistoryError>
pub fn merge_bases_with( &self, left: ObjectId, right: ObjectId, limits: HistoryLimits, ) -> Result<Vec<ObjectId>, HistoryError>
Returns all best common ancestors of two commits, sorted by raw object ID.
A best common ancestor has no distinct common descendant. Criss-cross histories can return several bases; disconnected histories return none and identical endpoints return themselves. Runs in O(V + E) graph work plus O(B log B) sorting for B bases, without timestamp assumptions.
§Errors
Validates both complete ancestries; returns the same failures as Self::walk.
assert_eq!(objects.merge_bases_with(left, right, HistoryLimits::default())?, [base]);
assert_eq!(objects.merge_bases_with(left, left, HistoryLimits::default())?, [left]);Sourcepub fn walk(&self, roots: &[ObjectId]) -> Result<Vec<ObjectId>, HistoryError>
pub fn walk(&self, roots: &[ObjectId]) -> Result<Vec<ObjectId>, HistoryError>
Collects the commits reachable from roots with HistoryLimits::default.
Equivalent to self.walk_with(roots, HistoryLimits::default()); see Self::walk_with.
§Errors
Returns the errors of Self::walk_with.
Examples found in repository?
83 pub fn operation(&self, gate: Option<Gate>) {
84 let repo = Repository::open(self.root.path()).unwrap();
85 let objects = repo.objects().unwrap();
86
87 if let Some(gate) = gate {
88 gate.started.send(()).unwrap();
89 gate.release.recv().unwrap();
90 }
91
92 let blob = objects.read(self.blob).unwrap().unwrap();
93 assert_eq!(blob.data().len(), 2 * 1024 * 1024);
94 assert_eq!(objects.walk(&[self.tip]).unwrap().len(), 512);
95 }Sourcepub fn is_ancestor(
&self,
ancestor: ObjectId,
descendant: ObjectId,
) -> Result<bool, HistoryError>
pub fn is_ancestor( &self, ancestor: ObjectId, descendant: ObjectId, ) -> Result<bool, HistoryError>
Tests ancestry with HistoryLimits::default.
Equivalent to self.is_ancestor_with(ancestor, descendant, HistoryLimits::default()); see
Self::is_ancestor_with.
§Errors
Returns the errors of Self::is_ancestor_with.
Examples found in repository?
18fn main() -> Result<(), Box<dyn std::error::Error>> {
19 let args = Args::parse();
20 let root = args.commit;
21 let git = Repo::discover(args.path)?;
22
23 for entry in git.log(root)? {
24 let (id, _) = entry?;
25 println!("{id}");
26 }
27
28 if let Some(other) = args.other_commit {
29 let objects = git.repository().objects()?;
30 println!("ancestor: {}", objects.is_ancestor(root, other)?);
31 println!("merge bases: {:?}", objects.merge_bases(root, other)?);
32 }
33
34 Ok(())
35}Sourcepub fn merge_bases(
&self,
left: ObjectId,
right: ObjectId,
) -> Result<Vec<ObjectId>, HistoryError>
pub fn merge_bases( &self, left: ObjectId, right: ObjectId, ) -> Result<Vec<ObjectId>, HistoryError>
Finds every best common ancestor with HistoryLimits::default.
Equivalent to self.merge_bases_with(left, right, HistoryLimits::default()); see
Self::merge_bases_with.
§Errors
Returns the errors of Self::merge_bases_with.
Examples found in repository?
18fn main() -> Result<(), Box<dyn std::error::Error>> {
19 let args = Args::parse();
20 let root = args.commit;
21 let git = Repo::discover(args.path)?;
22
23 for entry in git.log(root)? {
24 let (id, _) = entry?;
25 println!("{id}");
26 }
27
28 if let Some(other) = args.other_commit {
29 let objects = git.repository().objects()?;
30 println!("ancestor: {}", objects.is_ancestor(root, other)?);
31 println!("merge bases: {:?}", objects.merge_bases(root, other)?);
32 }
33
34 Ok(())
35}Source§impl Objects
impl Objects
Sourcepub fn merge_commits(
&self,
ours: ObjectId,
theirs: ObjectId,
) -> Result<CommitMerge, CommitError>
pub fn merge_commits( &self, ours: ObjectId, theirs: ObjectId, ) -> Result<CommitMerge, CommitError>
Merges commit tips using every best common ancestor and default bounds.
See Self::merge_commits_with for recursive-base and publication contracts.
§Errors
Returns CommitError on unrelated history, unreadable inputs, incomplete tree merging,
unresolved virtual bases, exhausted bounds or cancellation.
use girt::{Commit, InitKind, ObjectFormat, Repository, Signature, Tree};
let temporary = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
temporary.path().join("repo"),
InitKind::Bare,
)?;
let loose = repo.loose_objects();
let tree = loose.write_tree(&Tree::new(ObjectFormat::Sha1, vec![])?)?;
let person = Signature {
name: b"Merge Author".to_vec(),
email: b"merge@example.com".to_vec(),
seconds: 1_700_000_000,
offset_minutes: 0,
};
let base = loose.write_commit(&Commit::builder(tree, person.clone(), b"base\n").build()?)?;
let ours = loose.write_commit(
&Commit::builder(tree, person.clone(), b"ours\n")
.parent(base)
.build()?,
)?;
let theirs = loose.write_commit(
&Commit::builder(tree, person.clone(), b"theirs\n")
.parent(base)
.build()?,
)?;
let merged = repo.objects()?.merge_commits(ours, theirs)?;
assert!(merged.result.is_clean());
for (_, bytes) in &merged.result.blobs {
loose.write_blob(bytes)?;
}
for tree in &merged.result.trees {
loose.write_tree(tree)?;
}
let commit = Commit::builder(merged.result.tree.id(), person, b"merge\n")
.parents(merged.parents)
.build()?;
let id = loose.write_commit(&commit)?;
assert_eq!(repo.objects()?.read_commit(id)?.parents(), &[ours, theirs]);
// The object is stored; no reference has moved.Sourcepub fn merge_commits_with(
&self,
ours: ObjectId,
theirs: ObjectId,
options: CommitOptions,
cancel: &AtomicBool,
) -> Result<CommitMerge, CommitError>
pub fn merge_commits_with( &self, ours: ObjectId, theirs: ObjectId, options: CommitOptions, cancel: &AtomicBool, ) -> Result<CommitMerge, CommitError>
Computes a rename-aware commit merge without a working tree or object-store mutation.
Finds all best common ancestors using the existing history traversal. Multiple bases are
folded in object-ID order. Each fold recursively synthesizes the best common ancestors of
the accumulated original base tips and the next tip. Generated trees and blobs remain in
memory and are available to later folds, including rename scoring. A conflicted virtual
base returns CommitError::ConflictedBase rather than choosing a provisional ancestor.
Unrelated original tips require explicit opt-in. An empty base is always permitted inside recursive synthesis. Bounds apply independently to ancestry queries and tree comparisons; depth, base-set and virtual-merge limits bound composition. Individual history/storage reads and sorting are not interruptible; cancellation is checked between operations. No commit, index, working file or reference is written. Final conflicts remain a result.
§Errors
Returns CommitError with original causes and unresolved virtual-base data. No partial
result is installed, so retrying computation requires no repository rollback.
Sourcepub fn merge_commits_with_driver(
&self,
ours: ObjectId,
theirs: ObjectId,
options: CommitOptions,
cancel: &AtomicBool,
driver: &mut dyn TreeDriver,
) -> Result<CommitMerge, CommitError>
pub fn merge_commits_with_driver( &self, ours: ObjectId, theirs: ObjectId, options: CommitOptions, cancel: &AtomicBool, driver: &mut dyn TreeDriver, ) -> Result<CommitMerge, CommitError>
Merges commit tips using one path-driver instance for recursive bases and final content.
Uses Self::merge_commits_with’s bounds, history policy and in-memory result contract.
Callback side effects and attribute snapshot choice belong to the caller; no implicit
command runs. See super::TreeDriver for attribute and custom-driver composition.
§Errors
Returns CommitError, retaining any driver failure’s path and original source.
Source§impl Objects
impl Objects
Sourcepub fn merge_trees_with_driver(
&self,
base: ObjectId,
ours: ObjectId,
theirs: ObjectId,
options: TreeMergeOptions,
cancel: &AtomicBool,
driver: &mut dyn TreeDriver,
) -> Result<TreeMerge, RenameError>
pub fn merge_trees_with_driver( &self, base: ObjectId, ours: ObjectId, theirs: ObjectId, options: TreeMergeOptions, cancel: &AtomicBool, driver: &mut dyn TreeDriver, ) -> Result<TreeMerge, RenameError>
Merges renamed trees with explicit per-path content-driver dispatch.
Uses Self::merge_trees_with’s path/inference policy. The driver receives destination
paths after alignment and original base/side payloads; callback errors retain path/source.
No implicit attribute lookup or shell command runs. See TreeDriver for composition.
§Errors
Returns RenameError for inference/tree failures, including path-driver errors.
Source§impl Objects
impl Objects
Sourcepub fn merge_trees(
&self,
base: ObjectId,
ours: ObjectId,
theirs: ObjectId,
) -> Result<TreeMerge, RenameError>
pub fn merge_trees( &self, base: ObjectId, ours: ObjectId, theirs: ObjectId, ) -> Result<TreeMerge, RenameError>
Merges three trees with rename inference and default bounds, without writing objects.
See Self::merge_trees_with for conflict and unsupported-layout contracts.
§Errors
Returns RenameError on incomplete inference, unsupported alignment or tree failures.
use girt::{InitKind, ObjectFormat, Repository, Tree};
let temporary = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
temporary.path().join("repo"),
InitKind::Bare,
)?;
let empty = repo
.loose_objects()
.write_tree(&Tree::new(ObjectFormat::Sha1, vec![])?)?;
let result = repo.objects()?.merge_trees(empty, empty, empty)?;
assert!(result.is_clean());
assert_eq!(result.tree.id(), empty);Sourcepub fn merge_trees_with(
&self,
base: ObjectId,
ours: ObjectId,
theirs: ObjectId,
options: TreeMergeOptions,
cancel: &AtomicBool,
) -> Result<TreeMerge, RenameError>
pub fn merge_trees_with( &self, base: ObjectId, ours: ObjectId, theirs: ObjectId, options: TreeMergeOptions, cancel: &AtomicBool, ) -> Result<TreeMerge, RenameError>
Aligns renamed paths, then merges content and modes entirely in memory.
Base-to-side inference uses the existing rewrite detector. One-sided renames carry an opposite-side modification to the destination. Distinct rename destinations and rename/delete remain structured conflicts with original entries. Removed directories with one inferred destination relocate opposite-side additions and report directory-rename conflicts, matching Git’s default conservative directory-addition policy.
Destination collisions, ambiguous directory moves, skipped inexact inference and stored
empty subdirectories are explicit errors rather than deletion/addition approximations.
Input normalization trees and clean merged blobs stay in TreeMerge; install generated
objects before publishing its root. No attributes, configuration or submodules are opened.
Each traversal/comparison has its own bounds. Cancellation discards the entire result.
§Errors
Returns RenameError for inference, layout, object-read, content or traversal failures.
Source§impl Objects
impl Objects
Sourcepub fn merge_tree_entries(
&self,
base: ObjectId,
ours: ObjectId,
theirs: ObjectId,
) -> Result<TreeMerge, TreeError>
pub fn merge_tree_entries( &self, base: ObjectId, ours: ObjectId, theirs: ObjectId, ) -> Result<TreeMerge, TreeError>
Merges three tree snapshots by path with default bounds and no cancellation.
This primitive does not infer renames. See Self::merge_tree_entries_with for the
computation/publication boundary and TreeMerge for generated object ownership.
§Errors
Returns input object, content comparison, tree validation or traversal errors.
use girt::{InitKind, ObjectFormat, Repository, Tree};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("repo"),
InitKind::Bare,
)?;
let empty = Tree::new(repo.object_format(), vec![])?;
let id = repo.loose_objects().write_tree(&empty)?;
let result = repo.objects()?.merge_tree_entries(id, id, id)?;
assert!(result.is_clean());
assert_eq!(result.tree.id(), id);Sourcepub fn merge_tree_entries_with(
&self,
base: ObjectId,
ours: ObjectId,
theirs: ObjectId,
options: TreeOptions,
cancel: &AtomicBool,
) -> Result<TreeMerge, TreeError>
pub fn merge_tree_entries_with( &self, base: ObjectId, ours: ObjectId, theirs: ObjectId, options: TreeOptions, cancel: &AtomicBool, ) -> Result<TreeMerge, TreeError>
Merges entries at matching paths without rename inference or working-tree access.
Exact entry equality and unchanged-side selection precede content comparison. Directories merge recursively; regular-file modes and payloads merge independently. Different symlink targets, entry types and gitlinks remain structured conflicts. Add/add regular files use an empty content base. Modify/delete preserves the surviving entry provisionally. No submodule repository is opened. Paths and file payloads remain bytes.
Generated blobs/trees stay in memory; no object store, index, working file or reference is mutated. Read errors discard the entire result. Callers own input-store retention during computation and installation. Rename-aware callers must reconcile renamed paths separately.
§Errors
Returns TreeError on unreadable/wrong-kind inputs, content limits, cancellation,
traversal bounds or invalid generated trees. Reused input subtrees are not validated.
Source§impl Objects
impl Objects
Sourcepub fn header(
&self,
id: ObjectId,
limits: ReadLimits,
) -> Result<Option<ObjectHeader>, ObjectReadError>
pub fn header( &self, id: ObjectId, limits: ReadLimits, ) -> Result<Option<ObjectHeader>, ObjectReadError>
Reads id’s kind and size from its stored header without decoding the payload.
Uses the same store and pack precedence as Self::read. Packed deltas follow their
chain headers to the base kind; the size comes from the delta’s own header. This is the
lookup behind git cat-file --batch-check. The empty tree is reported even when not
stored, as Self::read does.
§Errors
Returns corrupt or unsupported headers, I/O errors, missing delta bases, or
ObjectReadError::Limit when the size exceeds ReadLimits::max_object_bytes or the
chain exceeds ReadLimits::max_delta_depth.
use girt::{InitKind, ObjectFormat, ObjectKind, PackLimits, ReadLimits, 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"seven\n")?;
let objects = repo.objects_with(PackLimits::default())?;
let header = objects
.header(id, ReadLimits::default())?
.ok_or("missing")?;
assert_eq!((header.kind(), header.size()), (ObjectKind::Blob, 6));Sourcepub fn headers(
&self,
ids: impl IntoIterator<Item = ObjectId>,
limits: ReadLimits,
) -> Result<Vec<(ObjectId, Option<ObjectHeader>)>, ObjectReadError>
pub fn headers( &self, ids: impl IntoIterator<Item = ObjectId>, limits: ReadLimits, ) -> Result<Vec<(ObjectId, Option<ObjectHeader>)>, ObjectReadError>
Reads the headers of many objects, visiting packed entries in pack order.
Equivalent to calling Self::header for each identity, but shared delta-chain headers
are read once per call and pack reads proceed in file order, which matters for listing a
whole repository. Results pair each identity with its header, or None when absent;
their order is unspecified. Use Self::headers_controlled to cancel.
§Errors
Returns the errors of Self::header.
Sourcepub fn headers_controlled(
&self,
ids: impl IntoIterator<Item = ObjectId>,
limits: ReadLimits,
cancelled: &AtomicBool,
) -> Result<Vec<(ObjectId, Option<ObjectHeader>)>, ObjectReadError>
pub fn headers_controlled( &self, ids: impl IntoIterator<Item = ObjectId>, limits: ReadLimits, cancelled: &AtomicBool, ) -> Result<Vec<(ObjectId, Option<ObjectHeader>)>, ObjectReadError>
Reads the headers of many objects as Self::headers does, checking cancelled between
objects.
§Errors
Returns the errors of Self::header or ObjectReadError::Cancelled.
Source§impl Objects
impl Objects
Sourcepub fn stores(&self) -> impl ExactSizeIterator<Item = ObjectStore<'_>>
pub fn stores(&self) -> impl ExactSizeIterator<Item = ObjectStore<'_>>
Returns this reader’s object stores in search order: the primary store, then alternates.
use girt::{InitKind, ObjectFormat, PackLimits, Repository};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r.git"),
InitKind::Bare,
)?;
let objects = repo.objects_with(PackLimits::default())?;
let stores: Vec<_> = objects.stores().collect();
assert_eq!(stores.len(), 1);
assert!(!stores[0].is_alternate());Sourcepub fn packs(&self) -> impl Iterator<Item = IndexedPack<'_>>
pub fn packs(&self) -> impl Iterator<Item = IndexedPack<'_>>
Returns every pinned pack in search order, across the primary store and alternates.
Equivalent to flattening ObjectStore::packs over Self::stores.
Examples found in repository?
13fn main() -> Result<(), Box<dyn std::error::Error>> {
14 let mut args = std::env::args().skip(1);
15 let path = args.next().ok_or("expected repository path")?;
16 let list = args.next().ok_or("expected object-ID list path")?;
17 let rounds: usize = args.next().ok_or("expected number of rounds")?.parse()?;
18 let mode = args.next().unwrap_or_else(|| "read".to_owned());
19 let ids = std::fs::read_to_string(list)?
20 .lines()
21 .map(str::parse)
22 .collect::<Result<Vec<ObjectId>, _>>()?;
23 let repo = Repository::open(path)?;
24 let start = Instant::now();
25 let objects = repo.objects()?;
26 let opening = start.elapsed();
27 let packs: Vec<_> = objects.packs().collect();
28 let bytes = if mode == "midx" {
29 std::fs::read(repo.object_dir().join("pack/multi-pack-index"))?
30 } else {
31 Vec::new()
32 };
33 let midx = if mode == "midx" {
34 Some(girt::multi_pack_index::MultiPackIndex::parse(
35 &bytes,
36 repo.object_format(),
37 )?)
38 } else {
39 None
40 };
41 let start = Instant::now();
42 let mut found = 0_usize;
43 for _ in 0..rounds {
44 for id in &ids {
45 let present = match mode.as_str() {
46 "read" => {
47 black_box(objects.read(*id)?.ok_or("object absent")?);
48 true
49 }
50 "contains" => objects.contains(*id)?,
51 "index-scan" => scan(&packs, *id)?,
52 "midx" => midx.as_ref().ok_or("index absent")?.find(*id).is_some(),
53 _ => return Err("expected read, contains, index-scan or midx".into()),
54 };
55 found += usize::from(black_box(present));
56 }
57 }
58 println!(
59 "objects={} rounds={rounds} mode={mode} found={found} open_ns={} read_ns={}",
60 ids.len(),
61 opening.as_nanos(),
62 start.elapsed().as_nanos(),
63 );
64 Ok(())
65}Sourcepub fn object_ids(
&self,
limits: ListLimits,
) -> Result<Vec<ObjectId>, ObjectReadError>
pub fn object_ids( &self, limits: ListLimits, ) -> Result<Vec<ObjectId>, ObjectReadError>
Collects every object identity visible to this reader, sorted and without duplicates.
Includes all pinned packs and the current loose objects of every store, alternates
included, as git cat-file --batch-all-objects lists them. The implicit empty tree is
not included unless it is stored. See Self::object_ids_controlled for limits.
§Errors
Returns the errors of Self::object_ids_controlled.
use girt::{InitKind, ListLimits, ObjectFormat, PackLimits, 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"listed\n")?;
let objects = repo.objects_with(PackLimits::default())?;
assert_eq!(objects.object_ids(ListLimits::default())?, [id]);Sourcepub fn object_ids_controlled(
&self,
limits: ListLimits,
cancelled: &AtomicBool,
) -> Result<Vec<ObjectId>, ObjectReadError>
pub fn object_ids_controlled( &self, limits: ListLimits, cancelled: &AtomicBool, ) -> Result<Vec<ObjectId>, ObjectReadError>
Collects every visible identity as Self::object_ids does, checking cancelled between
directory entries.
Packed identities and loose directory entries both charge ListLimits::max_entries.
The loose scan has the live semantics of ObjectStore::loose_objects.
§Errors
Returns ObjectReadError::Limit when the entry budget runs out,
ObjectReadError::Cancelled, or a loose directory I/O error with its path.
Source§impl Objects
impl Objects
Sourcepub fn prefix_matches(
&self,
prefix: ObjectIdPrefix,
) -> Result<Vec<ObjectId>, ObjectReadError>
pub fn prefix_matches( &self, prefix: ObjectIdPrefix, ) -> Result<Vec<ObjectId>, ObjectReadError>
Finds all distinct stored identities matching a prefix, in ascending order.
Searches loose filenames and pinned pack indexes in the primary store and alternates.
New loose files are visible; new packs require Self::refresh. Payloads are not decoded
or hash-verified, and unindexed packs are ignored, just as for ordinary object lookup.
§Errors
Reports format mismatch, directory I/O, or a changed/truncated pinned index.
Sourcepub fn resolve_prefix(
&self,
prefix: ObjectIdPrefix,
) -> Result<ObjectId, ResolvePrefixError>
pub fn resolve_prefix( &self, prefix: ObjectIdPrefix, ) -> Result<ObjectId, ResolvePrefixError>
Resolves a prefix to exactly one stored identity, independently of revision syntax.
§Errors
Returns ResolvePrefixError::Missing for no match and
ResolvePrefixError::Ambiguous with every candidate for multiple matches.
Storage and format errors are preserved as ResolvePrefixError::Read.
use girt::{InitKind, ObjectFormat, ObjectIdPrefix, PackLimits, Repository};
let dir = tempfile::tempdir()?;
let repo = Repository::init(ObjectFormat::Sha1, dir.path().join("repo"), InitKind::Bare)?;
let id = repo.loose_objects().write_blob(b"hello")?;
let objects = repo.objects_with(PackLimits::default())?;
let prefix = ObjectIdPrefix::from_hex(repo.object_format(), "b6fc4")?;
assert_eq!(objects.resolve_prefix(prefix)?, id);
assert_eq!(objects.abbreviate(id, 4)?.to_string(), "b6fc");Examples found in repository?
3fn main() -> Result<(), Box<dyn std::error::Error>> {
4 let mut args = std::env::args().skip(1);
5 let path = args
6 .next()
7 .ok_or("usage: short_ids <repository> <hex-prefix>")?;
8 let text = args.next().ok_or("missing hexadecimal prefix")?;
9
10 let repository = Repository::discover(path)?;
11 let objects = repository.objects()?;
12
13 let prefix = ObjectIdPrefix::from_hex(repository.object_format(), &text)?;
14 let id = objects.resolve_prefix(prefix)?;
15 println!("{}", objects.abbreviate(id, objects.auto_abbrev_len())?);
16 Ok(())
17}Sourcepub fn abbreviate(
&self,
id: ObjectId,
min_len: usize,
) -> Result<ObjectIdPrefix, ObjectReadError>
pub fn abbreviate( &self, id: ObjectId, min_len: usize, ) -> Result<ObjectIdPrefix, ObjectReadError>
Finds the shortest prefix distinguishing id from all other stored identities.
Clamps min_len to four through the full format width. The ID need not exist (for example,
a diff’s null sentinel); uniqueness is relative to the current storage view. Retain this
reader across calls to reuse pack indexes and lazy loose-directory snapshots. Each loose
bucket is scanned on its first abbreviation and retained until Self::refresh; clones
share these snapshots. Prefix resolution remains live. Concurrent writes can invalidate
uniqueness, so refresh before abbreviating against newly written objects.
§Errors
Reports format mismatch or storage inspection failure, as Self::prefix_matches does.
Examples found in repository?
3fn main() -> Result<(), Box<dyn std::error::Error>> {
4 let mut args = std::env::args().skip(1);
5 let path = args
6 .next()
7 .ok_or("usage: short_ids <repository> <hex-prefix>")?;
8 let text = args.next().ok_or("missing hexadecimal prefix")?;
9
10 let repository = Repository::discover(path)?;
11 let objects = repository.objects()?;
12
13 let prefix = ObjectIdPrefix::from_hex(repository.object_format(), &text)?;
14 let id = objects.resolve_prefix(prefix)?;
15 println!("{}", objects.abbreviate(id, objects.auto_abbrev_len())?);
16 Ok(())
17}Sourcepub fn auto_abbrev_len(&self) -> usize
pub fn auto_abbrev_len(&self) -> usize
Computes Git’s automatic minimum abbreviation length from the packed object count.
The estimate counts entries in each pinned pack, including duplicate copies and alternates.
Loose objects do not increase this estimate. The minimum is seven; each additional digit
provides four bits of space for the birthday-bound estimate. Call once per output operation,
then pass the result to Self::abbreviate to lengthen actual collisions.
Examples found in repository?
3fn main() -> Result<(), Box<dyn std::error::Error>> {
4 let mut args = std::env::args().skip(1);
5 let path = args
6 .next()
7 .ok_or("usage: short_ids <repository> <hex-prefix>")?;
8 let text = args.next().ok_or("missing hexadecimal prefix")?;
9
10 let repository = Repository::discover(path)?;
11 let objects = repository.objects()?;
12
13 let prefix = ObjectIdPrefix::from_hex(repository.object_format(), &text)?;
14 let id = objects.resolve_prefix(prefix)?;
15 println!("{}", objects.abbreviate(id, objects.auto_abbrev_len())?);
16 Ok(())
17}Source§impl Objects
impl Objects
Sourcepub fn read_commit_with(
&self,
id: ObjectId,
limits: ReadLimits,
) -> Result<Commit, ReadCommitError>
pub fn read_commit_with( &self, id: ObjectId, limits: ReadLimits, ) -> Result<Commit, ReadCommitError>
Reads and parses the commit id.
Use this when id came from a reference or a history walk and must be a commit. Unlike
Self::read, a missing object is an error, because a commit you were pointed at should
exist. Use Self::read to test whether an object exists or to accept any kind.
use girt::{
Commit, CommitFields, InitKind, ObjectFormat, PackLimits, ReadLimits, Repository,
Signature, Tree,
};
let directory = tempfile::tempdir()?;
let path = directory.path().join("repo.git");
let repository = Repository::init(ObjectFormat::Sha1, path, InitKind::Bare)?;
let loose = repository.loose_objects();
let tree = loose.write_tree(&Tree::new(ObjectFormat::Sha1, vec![])?)?;
let person = Signature {
name: b"A. Writer".to_vec(),
email: b"writer@example.com".to_vec(),
seconds: 1_700_000_000,
offset_minutes: 0,
};
let id = loose.write_commit(&Commit::new(CommitFields {
tree,
parents: vec![],
author: person.clone(),
committer: person,
extra_headers: vec![],
message: b"first\n".to_vec(),
})?)?;
let objects = repository.objects_with(PackLimits::default())?;
let commit = objects.read_commit_with(id, ReadLimits::default())?;
assert_eq!(commit.message(), b"first\n");
assert!(
objects
.read_commit_with(tree, ReadLimits::default())
.is_err()
);§Errors
Returns ReadCommitError::Missing if no store has id, ReadCommitError::Kind if
id names another kind of object, ReadCommitError::Parse for a malformed commit, and
ReadCommitError::Read for the failures of Self::read.
Sourcepub fn read_commit(&self, id: ObjectId) -> Result<Commit, ReadCommitError>
pub fn read_commit(&self, id: ObjectId) -> Result<Commit, ReadCommitError>
Reads and parses the commit id with ReadLimits::default.
Equivalent to self.read_commit_with(id, ReadLimits::default()); see
Self::read_commit_with.
§Errors
Returns the errors of Self::read_commit_with.
Sourcepub fn read_tree_with(
&self,
id: ObjectId,
limits: ReadLimits,
) -> Result<Tree, ReadTreeError>
pub fn read_tree_with( &self, id: ObjectId, limits: ReadLimits, ) -> Result<Tree, ReadTreeError>
Reads and parses the tree id.
Use this for a commit’s Commit::tree or a tree entry with crate::EntryMode::Tree.
A missing object is an error, as with Self::read_commit. The canonical empty tree is
present in every repository, as Self::read reports it.
use girt::{EntryMode, InitKind, ObjectFormat, ReadLimits, Repository, Tree, TreeEntry};
let directory = tempfile::tempdir()?;
let path = directory.path().join("repo.git");
let repository = Repository::init(ObjectFormat::Sha1, path, InitKind::Bare)?;
let loose = repository.loose_objects();
let blob = loose.write_blob(b"hello\n")?;
let entry = TreeEntry {
name: b"hello.txt".to_vec(),
mode: EntryMode::Blob,
id: blob,
};
let id = loose.write_tree(&Tree::new(ObjectFormat::Sha1, vec![entry])?)?;
let objects = repository.objects()?;
let tree = objects.read_tree_with(id, ReadLimits::default())?;
assert_eq!(tree.entries()[0].name, b"hello.txt");
assert_eq!(objects.read_blob(tree.entries()[0].id)?, b"hello\n");
assert!(objects.read_tree(blob).is_err());§Errors
Returns ReadTreeError::Missing if no store has id, ReadTreeError::Kind if id
names another kind of object, ReadTreeError::Parse for a malformed tree, and
ReadTreeError::Read for the failures of Self::read.
Sourcepub fn read_tree(&self, id: ObjectId) -> Result<Tree, ReadTreeError>
pub fn read_tree(&self, id: ObjectId) -> Result<Tree, ReadTreeError>
Reads and parses the tree id with ReadLimits::default.
Equivalent to self.read_tree_with(id, ReadLimits::default()); see
Self::read_tree_with.
§Errors
Returns the errors of Self::read_tree_with.
Sourcepub fn read_tag_with(
&self,
id: ObjectId,
limits: ReadLimits,
) -> Result<Tag, ReadTagError>
pub fn read_tag_with( &self, id: ObjectId, limits: ReadLimits, ) -> Result<Tag, ReadTagError>
Reads and parses the annotated tag id.
A missing object is an error, as with Self::read_commit. Lightweight tags are
references, not tag objects; resolve them through crate::refs.
use girt::{InitKind, ObjectFormat, ObjectKind, ReadLimits, Repository, Tag, TagFields};
let directory = tempfile::tempdir()?;
let path = directory.path().join("repo.git");
let repository = Repository::init(ObjectFormat::Sha1, path, InitKind::Bare)?;
let loose = repository.loose_objects();
let blob = loose.write_blob(b"release notes\n")?;
let id = loose.write_tag(&Tag::new(TagFields {
target: blob,
target_kind: ObjectKind::Blob,
name: b"v1.0".to_vec(),
tagger: None,
extra_headers: vec![],
message: b"first release\n".to_vec(),
})?)?;
let objects = repository.objects()?;
let tag = objects.read_tag_with(id, ReadLimits::default())?;
assert_eq!(tag.name(), b"v1.0");
assert_eq!(objects.read_tag(id)?.target(), blob);§Errors
Returns ReadTagError::Missing if no store has id, ReadTagError::Kind if id
names another kind of object, ReadTagError::Parse for a malformed tag, and
ReadTagError::Read for the failures of Self::read.
Sourcepub fn read_tag(&self, id: ObjectId) -> Result<Tag, ReadTagError>
pub fn read_tag(&self, id: ObjectId) -> Result<Tag, ReadTagError>
Reads and parses the annotated tag id with ReadLimits::default.
Equivalent to self.read_tag_with(id, ReadLimits::default()); see Self::read_tag_with.
§Errors
Returns the errors of Self::read_tag_with.
Sourcepub fn read_blob_with(
&self,
id: ObjectId,
limits: ReadLimits,
) -> Result<Vec<u8>, ReadBlobError>
pub fn read_blob_with( &self, id: ObjectId, limits: ReadLimits, ) -> Result<Vec<u8>, ReadBlobError>
Reads the blob id and returns its bytes.
Blob bytes need no parsing. A missing object is an error, as with Self::read_commit.
The bytes are copied out of the reader’s delta base cache when it shares them; borrow
Object::data from Self::read to avoid that copy. See Self::read_tree_with for an
example.
§Errors
Returns ReadBlobError::Missing if no store has id, ReadBlobError::Kind if id
names another kind of object, and ReadBlobError::Read for the failures of
Self::read.
Sourcepub fn read_blob(&self, id: ObjectId) -> Result<Vec<u8>, ReadBlobError>
pub fn read_blob(&self, id: ObjectId) -> Result<Vec<u8>, ReadBlobError>
Reads the blob id with ReadLimits::default and returns its bytes.
Equivalent to self.read_blob_with(id, ReadLimits::default()); see
Self::read_blob_with. The default limits refuse blobs over 64 MiB; pass larger
ReadLimits to Self::read_blob_with when the repository is trusted to hold them.
§Errors
Returns the errors of Self::read_blob_with.
Examples found in repository?
17fn main() -> girt::Result<()> {
18 let args = Args::parse();
19 let git = Repo::new(Repository::open(args.path)?);
20 let id = git.rev_parse(&args.revision)?;
21 let bytes = git.repository().objects()?.read_blob(id)?;
22 println!(
23 "Read {} bytes from {}",
24 bytes.len(),
25 git.repository().object_dir().display()
26 );
27 Ok(())
28}Source§impl Objects
impl Objects
Sourcepub fn store_directories(&self) -> impl Iterator<Item = &Path>
pub fn store_directories(&self) -> impl Iterator<Item = &Path>
Returns canonical object directories in search order, primary first.
Subsequent entries are alternates owned outside this repository. A maintenance planner must never treat their contents as owned pruning candidates.
Sourcepub fn shallow_roots(&self) -> &ShallowRoots
pub fn shallow_roots(&self) -> &ShallowRoots
Fixed history boundaries inherited from the repository handle, unaffected by later refresh.
Sourcepub fn object_format(&self) -> ObjectFormat
pub fn object_format(&self) -> ObjectFormat
Format shared by all objects in this store.
Sourcepub fn refresh(
&mut self,
packs: PackLimits,
alternates: AlternateLimits,
) -> Result<(), ObjectReadError>
pub fn refresh( &mut self, packs: PackLimits, alternates: AlternateLimits, ) -> Result<(), ObjectReadError>
Replaces this reader’s pack and alternate view after one successful bounded scan.
Also clears lazy loose-directory snapshots used by Self::abbreviate.
Rediscover from the original canonical primary directory using the supplied aggregate limits. Newly installed indexed packs and alternate topology become visible; removed packs disappear from this reader. Clones retain their previous pinned pairs and topology until they refresh or drop. Loose paths remain live in every reader, so an old reader can lose loose-only objects after GC or observe new loose objects without refresh. Shallow roots remain unchanged; refresh the repository’s shallow snapshot separately when needed.
Every pair is reopened and revalidated, even if its filename is unchanged. The old and new
views coexist until success: peak handles and tables can reach their combined limits,
multiplied by independently retained generations. Clone shares handles within a generation.
Like crate::Repository::objects, refresh validates index structure and pack
headers/trailers without hashing indexes or scanning pack bodies. Use
Self::verify_packs for index checksum, whole-pack checksum and CRC proof.
§Errors
Returns opening errors without changing this reader or its clones. An index whose pack is absent, a mismatched pair, malformed alternate metadata or corrupt index/header is an error, never absence. Pack body corruption is deferred to reads or explicit verification. There are no automatic retries, sleeps or filesystem changes. After a publisher finishes or a broken pair is repaired/removed, the caller may explicitly retry with its own finite attempt budget. Continuing publication may require another refresh even after success: directory enumeration and alternate discovery are not atomic snapshots.
use girt::{
AlternateLimits, InitKind, ObjectFormat, ObjectId, ObjectKind, PackLimits, PackObject,
PackWriteLimits, ReadLimits, Repository, write_pack,
};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r.git"),
InitKind::Bare,
)?;
let mut objects = repo.objects_with(PackLimits::default())?;
let retained = objects.clone();
let data = b"published only in a new pack";
let id = ObjectId::for_blob(ObjectFormat::Sha1, data);
let input = PackObject {
id,
kind: ObjectKind::Blob,
data,
};
let (mut pack, mut index) = (Vec::new(), Vec::new());
let written = write_pack(
ObjectFormat::Sha1,
&[input],
&mut pack,
&mut index,
PackWriteLimits::default(),
)?;
let base = repo
.object_dir()
.join("pack")
.join(format!("pack-{}", written.checksum));
// Private fixture only: shared installation requires a coordinated publication path.
std::fs::write(base.with_extension("pack"), pack)?;
std::fs::write(base.with_extension("idx"), index)?;
assert!(objects.read_with(id, ReadLimits::default())?.is_none());
objects.refresh(PackLimits::default(), AlternateLimits::default())?;
assert_eq!(
objects
.read_with(id, ReadLimits::default())?
.ok_or("missing")?
.data(),
data
);
// The clone retains the old generation.
assert!(retained.read_with(id, ReadLimits::default())?.is_none());Examples found in repository?
31fn main() -> Result<(), Box<dyn std::error::Error>> {
32 let args = Args::parse();
33 let operation = args.operation;
34
35 let start = Instant::now();
36 let repository = Repository::open(args.path)?;
37 let head = repository.head()?.id.ok_or("unborn HEAD")?;
38
39 let count = if operation == Operation::Repeat { 8 } else { 1 };
40 for _ in 0..count {
41 let mut objects = repository.objects_with(PackLimits::trusted())?;
42 let measured = Instant::now();
43
44 match operation {
45 Operation::Refresh => {
46 objects.refresh(PackLimits::trusted(), AlternateLimits::default())?
47 }
48 Operation::Open | Operation::Repeat => {}
49 Operation::Verify => objects.verify_packs(&AtomicBool::new(false))?,
50 }
51
52 let object = objects
53 .read_with(head, ReadLimits::trusted())?
54 .ok_or("missing HEAD")?;
55 std::hint::black_box(object);
56
57 if operation == Operation::Refresh {
58 println!("refresh_seconds={:.9}", measured.elapsed().as_secs_f64());
59 }
60 }
61
62 println!("total_seconds={:.9}", start.elapsed().as_secs_f64());
63 Ok(())
64}More examples
47fn main() -> Result<(), Box<dyn std::error::Error>> {
48 let args = Args::parse();
49 let repository = Repository::open(args.repository)?;
50 let id = args.id;
51
52 let baseline = handles();
53 let failed = repository.objects_with(PackLimits::default().with_max_index_bytes(0));
54 assert!(matches!(
55 failed,
56 Err(girt::ObjectReadError::Limit("pack index bytes"))
57 ));
58 assert_eq!(handles(), baseline);
59
60 if args.mode == Some(Mode::Cancel) {
61 let cancelled = AtomicBool::new(false);
62 let start = Instant::now();
63
64 let result = std::thread::scope(|scope| {
65 scope.spawn(|| {
66 std::thread::sleep(Duration::from_millis(10));
67 cancelled.store(true, std::sync::atomic::Ordering::Relaxed);
68 });
69 repository.objects_controlled(
70 PackLimits::default(),
71 AlternateLimits::default(),
72 &cancelled,
73 )
74 });
75
76 assert!(matches!(result, Err(girt::ObjectReadError::Cancelled)));
77 assert_eq!(handles(), baseline);
78
79 println!(
80 "cancel_us={} handles_before={baseline:?} handles_after={:?}",
81 start.elapsed().as_micros(),
82 handles()
83 );
84 return Ok(());
85 }
86
87 let cancelled = AtomicBool::new(false);
88 let start = Instant::now();
89 let mut objects = repository.objects_controlled(
90 PackLimits::default(),
91 AlternateLimits::default(),
92 &cancelled,
93 )?;
94
95 let opened = start.elapsed();
96 let retained = handles();
97 let clone = objects.clone();
98 let cloned = handles();
99
100 let failed = objects.refresh(
101 PackLimits::default().with_max_index_bytes(0),
102 AlternateLimits::default(),
103 );
104 assert!(matches!(
105 failed,
106 Err(girt::ObjectReadError::Limit("pack index bytes"))
107 ));
108 assert_eq!(handles(), cloned);
109
110 let start = Instant::now();
111 objects.refresh(PackLimits::default(), AlternateLimits::default())?;
112 let refreshed = start.elapsed();
113
114 let generations = handles();
115 assert_eq!(
116 generations,
117 baseline
118 .zip(retained)
119 .map(|(base, open)| base + 2 * (open - base))
120 );
121
122 println!(
123 "refresh_us={} handles_two_generations={generations:?}",
124 refreshed.as_micros()
125 );
126
127 let start = Instant::now();
128 let object = clone
129 .read_controlled(id, ReadLimits::default(), &cancelled)?
130 .ok_or("object missing")?;
131 let read = start.elapsed();
132
133 let start = Instant::now();
134 let second = clone
135 .read_with(id, ReadLimits::default())?
136 .ok_or("object missing")?;
137 let reread = start.elapsed();
138
139 assert_eq!(object, second);
140 println!(
141 "pid={} open_us={} first_read_us={} warm_read_us={} payload_bytes={} handles_before={baseline:?} handles_open={retained:?} handles_clone={cloned:?}",
142 std::process::id(),
143 opened.as_micros(),
144 read.as_micros(),
145 reread.as_micros(),
146 object.data().len()
147 );
148
149 drop(second);
150 drop(object);
151 drop(objects);
152 drop(clone);
153
154 println!("handles_after_drop={:?}", handles());
155 assert_eq!(handles(), baseline);
156
157 // Optional sampling interval is outside measured operations and retains no reader.
158 if args.mode == Some(Mode::Hold) {
159 std::thread::sleep(Duration::from_secs(1));
160 }
161 Ok(())
162}Sourcepub fn refresh_controlled(
&mut self,
packs: PackLimits,
alternates: AlternateLimits,
cancelled: &AtomicBool,
) -> Result<(), ObjectReadError>
pub fn refresh_controlled( &mut self, packs: PackLimits, alternates: AlternateLimits, cancelled: &AtomicBool, ) -> Result<(), ObjectReadError>
Refreshes with the same cooperative cancellation checkpoints as opening.
Cancellation before replacing the view releases all candidate handles and preserves the
current view. Blocking OS calls cannot be interrupted. See Self::refresh for resource,
publication, retained-reader and explicit retry contracts.
§Errors
Returns the errors from Self::refresh or ObjectReadError::Cancelled.
Sourcepub fn verify_packs(
&self,
cancelled: &AtomicBool,
) -> Result<(), ObjectReadError>
pub fn verify_packs( &self, cancelled: &AtomicBool, ) -> Result<(), ObjectReadError>
Verifies index checksums, whole-pack checksums and v2 entry CRCs in every pinned store, including alternates.
Opening checks index structure, pack framing and trailer agreement without hashing the
index or reading pack bodies. Use this operation when the complete raw-byte checksum proof
is required. It streams each pack with bounded scratch memory and checks cancellation
every 64 KiB. The artifact sizes already charged to PackLimits::max_bytes bound this
work. It changes no files or reader state; failure and cancellation leave the reader
usable for unaffected objects.
This is not a full fsck: it does not decode objects, verify their identities, validate payload syntax or check connectivity. Legacy v1 indexes have no entry CRCs. Artifacts must remain immutable throughout verification, as throughout this reader’s lifetime.
§Errors
Returns checksum/CRC corruption, artifact I/O errors or ObjectReadError::Cancelled.
Also refuses packed damage remembered by prior reads, including identity-only failures;
that contextual error retains the path, offset and original corruption cause.
use girt::{ObjectFormat, ObjectId, ObjectKind, PackObject, PackWriteLimits, write_pack};
let input = PackObject {
id: ObjectId::for_blob(ObjectFormat::Sha1, b"manifest"),
kind: ObjectKind::Blob,
data: b"manifest",
};
let (mut bytes, mut index) = (Vec::new(), Vec::new());
let written = write_pack(
ObjectFormat::Sha1,
&[input],
&mut bytes,
&mut index,
PackWriteLimits::default(),
)?;
let directory = tempfile::tempdir()?;
let repo = girt::Repository::init(
ObjectFormat::Sha1,
directory.path().join("r.git"),
girt::InitKind::Bare,
)?;
let base = repo
.object_dir()
.join("pack")
.join(format!("pack-{}", written.checksum));
// Private fixture only; shared repositories need a validated installation path.
std::fs::write(base.with_extension("pack"), bytes)?;
std::fs::write(base.with_extension("idx"), index)?;
let objects = repo.objects_with(girt::PackLimits::default())?;
objects.verify_packs(&std::sync::atomic::AtomicBool::new(false))?;
assert!(
objects
.read_with(input.id, girt::ReadLimits::default())?
.is_some()
);Examples found in repository?
31fn main() -> Result<(), Box<dyn std::error::Error>> {
32 let args = Args::parse();
33 let operation = args.operation;
34
35 let start = Instant::now();
36 let repository = Repository::open(args.path)?;
37 let head = repository.head()?.id.ok_or("unborn HEAD")?;
38
39 let count = if operation == Operation::Repeat { 8 } else { 1 };
40 for _ in 0..count {
41 let mut objects = repository.objects_with(PackLimits::trusted())?;
42 let measured = Instant::now();
43
44 match operation {
45 Operation::Refresh => {
46 objects.refresh(PackLimits::trusted(), AlternateLimits::default())?
47 }
48 Operation::Open | Operation::Repeat => {}
49 Operation::Verify => objects.verify_packs(&AtomicBool::new(false))?,
50 }
51
52 let object = objects
53 .read_with(head, ReadLimits::trusted())?
54 .ok_or("missing HEAD")?;
55 std::hint::black_box(object);
56
57 if operation == Operation::Refresh {
58 println!("refresh_seconds={:.9}", measured.elapsed().as_secs_f64());
59 }
60 }
61
62 println!("total_seconds={:.9}", start.elapsed().as_secs_f64());
63 Ok(())
64}Sourcepub fn read_with(
&self,
id: ObjectId,
limits: ReadLimits,
) -> Result<Option<Object>, ObjectReadError>
pub fn read_with( &self, id: ObjectId, limits: ReadLimits, ) -> Result<Option<Object>, ObjectReadError>
Reads an exact object by full identity in this store’s format; returns None only when
absent.
The payload is uninterpreted. Parse it separately if structured fields are needed. Searches each store’s pinned packs in filename order, then its live loose files, primary before alternates. Packed corruption retries remaining copies with full identity checks. Failed packed entries are remembered for this handle and its clones; refresh preserves evidence for the same artifacts. Reads never refresh storage implicitly.
§Errors
Returns storage errors for corrupt/unsupported objects, I/O failures, missing delta bases,
cycles, incompatible identity formats, or exhausted resource limits. An invalid loose object
fails immediately unless this read is already retrying packed corruption. If all copies are
absent or damaged after a packed failure, the error names that pack; its source chain
includes the entry offset and concrete corruption cause. Packed damage uses
ObjectReadError::Path with an InvalidData I/O source. Cancellation,
resource limits, unsupported headers, missing/external delta bases and ordinary I/O
failures do not mark entries bad or trigger fallback. Only the first packed corruption
cause is reported, including when a later loose copy is also corrupt. No filesystem changes
occur.
use girt::{InitKind, ObjectFormat, PackLimits, ReadLimits, 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"payload\n")?;
let objects = repo.objects_with(PackLimits::default())?;
let limits = ReadLimits::default().with_max_object_bytes(1024);
assert_eq!(
objects.read_with(id, limits)?.ok_or("missing blob")?.data(),
b"payload\n"
);
let absent = girt::ObjectId::for_blob(ObjectFormat::Sha1, b"not written");
assert!(objects.read_with(absent, limits)?.is_none());Examples found in repository?
31fn main() -> Result<(), Box<dyn std::error::Error>> {
32 let args = Args::parse();
33 let operation = args.operation;
34
35 let start = Instant::now();
36 let repository = Repository::open(args.path)?;
37 let head = repository.head()?.id.ok_or("unborn HEAD")?;
38
39 let count = if operation == Operation::Repeat { 8 } else { 1 };
40 for _ in 0..count {
41 let mut objects = repository.objects_with(PackLimits::trusted())?;
42 let measured = Instant::now();
43
44 match operation {
45 Operation::Refresh => {
46 objects.refresh(PackLimits::trusted(), AlternateLimits::default())?
47 }
48 Operation::Open | Operation::Repeat => {}
49 Operation::Verify => objects.verify_packs(&AtomicBool::new(false))?,
50 }
51
52 let object = objects
53 .read_with(head, ReadLimits::trusted())?
54 .ok_or("missing HEAD")?;
55 std::hint::black_box(object);
56
57 if operation == Operation::Refresh {
58 println!("refresh_seconds={:.9}", measured.elapsed().as_secs_f64());
59 }
60 }
61
62 println!("total_seconds={:.9}", start.elapsed().as_secs_f64());
63 Ok(())
64}More examples
47fn main() -> Result<(), Box<dyn std::error::Error>> {
48 let args = Args::parse();
49 let repository = Repository::open(args.repository)?;
50 let id = args.id;
51
52 let baseline = handles();
53 let failed = repository.objects_with(PackLimits::default().with_max_index_bytes(0));
54 assert!(matches!(
55 failed,
56 Err(girt::ObjectReadError::Limit("pack index bytes"))
57 ));
58 assert_eq!(handles(), baseline);
59
60 if args.mode == Some(Mode::Cancel) {
61 let cancelled = AtomicBool::new(false);
62 let start = Instant::now();
63
64 let result = std::thread::scope(|scope| {
65 scope.spawn(|| {
66 std::thread::sleep(Duration::from_millis(10));
67 cancelled.store(true, std::sync::atomic::Ordering::Relaxed);
68 });
69 repository.objects_controlled(
70 PackLimits::default(),
71 AlternateLimits::default(),
72 &cancelled,
73 )
74 });
75
76 assert!(matches!(result, Err(girt::ObjectReadError::Cancelled)));
77 assert_eq!(handles(), baseline);
78
79 println!(
80 "cancel_us={} handles_before={baseline:?} handles_after={:?}",
81 start.elapsed().as_micros(),
82 handles()
83 );
84 return Ok(());
85 }
86
87 let cancelled = AtomicBool::new(false);
88 let start = Instant::now();
89 let mut objects = repository.objects_controlled(
90 PackLimits::default(),
91 AlternateLimits::default(),
92 &cancelled,
93 )?;
94
95 let opened = start.elapsed();
96 let retained = handles();
97 let clone = objects.clone();
98 let cloned = handles();
99
100 let failed = objects.refresh(
101 PackLimits::default().with_max_index_bytes(0),
102 AlternateLimits::default(),
103 );
104 assert!(matches!(
105 failed,
106 Err(girt::ObjectReadError::Limit("pack index bytes"))
107 ));
108 assert_eq!(handles(), cloned);
109
110 let start = Instant::now();
111 objects.refresh(PackLimits::default(), AlternateLimits::default())?;
112 let refreshed = start.elapsed();
113
114 let generations = handles();
115 assert_eq!(
116 generations,
117 baseline
118 .zip(retained)
119 .map(|(base, open)| base + 2 * (open - base))
120 );
121
122 println!(
123 "refresh_us={} handles_two_generations={generations:?}",
124 refreshed.as_micros()
125 );
126
127 let start = Instant::now();
128 let object = clone
129 .read_controlled(id, ReadLimits::default(), &cancelled)?
130 .ok_or("object missing")?;
131 let read = start.elapsed();
132
133 let start = Instant::now();
134 let second = clone
135 .read_with(id, ReadLimits::default())?
136 .ok_or("object missing")?;
137 let reread = start.elapsed();
138
139 assert_eq!(object, second);
140 println!(
141 "pid={} open_us={} first_read_us={} warm_read_us={} payload_bytes={} handles_before={baseline:?} handles_open={retained:?} handles_clone={cloned:?}",
142 std::process::id(),
143 opened.as_micros(),
144 read.as_micros(),
145 reread.as_micros(),
146 object.data().len()
147 );
148
149 drop(second);
150 drop(object);
151 drop(objects);
152 drop(clone);
153
154 println!("handles_after_drop={:?}", handles());
155 assert_eq!(handles(), baseline);
156
157 // Optional sampling interval is outside measured operations and retains no reader.
158 if args.mode == Some(Mode::Hold) {
159 std::thread::sleep(Duration::from_secs(1));
160 }
161 Ok(())
162}Sourcepub fn contains(&self, id: ObjectId) -> Result<bool, ObjectReadError>
pub fn contains(&self, id: ObjectId) -> Result<bool, ObjectReadError>
Reports whether this snapshot holds id, without reading or verifying the stored copy.
Searches the same stores in the same order as Self::read: each store’s pinned pack
indexes, then its live loose files, primary before alternates. The canonical empty tree is
always present, as in Self::read. A pack published after this snapshot was taken is not
seen, so a false can be out of date; Self::refresh picks up new packs.
Use this to skip storing content the repository already has, as Git does when it writes an
object that is already packed. A true is the store’s claim, not proof that the copy
decodes: Self::read verifies identity and reports a damaged copy when the object is
read.
§Errors
Returns ObjectReadError::Loose wrapping a format error for a foreign-format id,
and storage errors when a pack index or loose object directory cannot be read. A
missing loose file is absence, not an error. No filesystem changes occur.
use girt::{InitKind, ObjectFormat, ObjectId, PackLimits, Repository};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r.git"),
InitKind::Bare,
)?;
let stored = repo.loose_objects().write_blob(b"payload\n")?;
let objects = repo.objects_with(PackLimits::default())?;
assert!(objects.contains(stored)?);
// Hash first, then store only what the repository lacks.
let content = b"new content\n";
let id = ObjectId::for_blob(ObjectFormat::Sha1, content);
assert!(!objects.contains(id)?);
assert_eq!(repo.loose_objects().write_blob(content)?, id);
assert!(objects.contains(id)?);Examples found in repository?
13fn main() -> Result<(), Box<dyn std::error::Error>> {
14 let mut args = std::env::args().skip(1);
15 let path = args.next().ok_or("expected repository path")?;
16 let list = args.next().ok_or("expected object-ID list path")?;
17 let rounds: usize = args.next().ok_or("expected number of rounds")?.parse()?;
18 let mode = args.next().unwrap_or_else(|| "read".to_owned());
19 let ids = std::fs::read_to_string(list)?
20 .lines()
21 .map(str::parse)
22 .collect::<Result<Vec<ObjectId>, _>>()?;
23 let repo = Repository::open(path)?;
24 let start = Instant::now();
25 let objects = repo.objects()?;
26 let opening = start.elapsed();
27 let packs: Vec<_> = objects.packs().collect();
28 let bytes = if mode == "midx" {
29 std::fs::read(repo.object_dir().join("pack/multi-pack-index"))?
30 } else {
31 Vec::new()
32 };
33 let midx = if mode == "midx" {
34 Some(girt::multi_pack_index::MultiPackIndex::parse(
35 &bytes,
36 repo.object_format(),
37 )?)
38 } else {
39 None
40 };
41 let start = Instant::now();
42 let mut found = 0_usize;
43 for _ in 0..rounds {
44 for id in &ids {
45 let present = match mode.as_str() {
46 "read" => {
47 black_box(objects.read(*id)?.ok_or("object absent")?);
48 true
49 }
50 "contains" => objects.contains(*id)?,
51 "index-scan" => scan(&packs, *id)?,
52 "midx" => midx.as_ref().ok_or("index absent")?.find(*id).is_some(),
53 _ => return Err("expected read, contains, index-scan or midx".into()),
54 };
55 found += usize::from(black_box(present));
56 }
57 }
58 println!(
59 "objects={} rounds={rounds} mode={mode} found={found} open_ns={} read_ns={}",
60 ids.len(),
61 opening.as_nanos(),
62 start.elapsed().as_nanos(),
63 );
64 Ok(())
65}Sourcepub fn read(&self, id: ObjectId) -> Result<Option<Object>, ObjectReadError>
pub fn read(&self, id: ObjectId) -> Result<Option<Object>, ObjectReadError>
Reads an exact object by full identity with ReadLimits::default; returns None only
when absent.
Equivalent to self.read_with(id, ReadLimits::default()); see Self::read_with for store
precedence and damage handling. The default limits refuse payloads over 64 MiB. Use
Self::read_commit, Self::read_tree, Self::read_tag or
Self::read_blob when the kind is known.
§Errors
Returns the errors of Self::read_with.
Examples found in repository?
21fn main() -> Result<(), Box<dyn std::error::Error>> {
22 let args = Args::parse();
23 let repository = Repository::open(args.path)?;
24 let objects = repository.objects()?;
25 let object = objects.read(args.id)?.ok_or("object absent")?;
26
27 eprintln!(
28 "{} {} ({} bytes)",
29 object.kind().as_str(),
30 object.id(),
31 object.data().len()
32 );
33 io::stdout().write_all(object.data())?;
34 Ok(())
35}More examples
83 pub fn operation(&self, gate: Option<Gate>) {
84 let repo = Repository::open(self.root.path()).unwrap();
85 let objects = repo.objects().unwrap();
86
87 if let Some(gate) = gate {
88 gate.started.send(()).unwrap();
89 gate.release.recv().unwrap();
90 }
91
92 let blob = objects.read(self.blob).unwrap().unwrap();
93 assert_eq!(blob.data().len(), 2 * 1024 * 1024);
94 assert_eq!(objects.walk(&[self.tip]).unwrap().len(), 512);
95 }7fn main() -> Result<(), Box<dyn std::error::Error>> {
8 let root = tempfile::tempdir()?;
9 let format = girt::ObjectFormat::Sha256;
10 let payload = b"explicit export\0\xff";
11 let id = ObjectId::for_blob(format, payload);
12
13 let inputs = [PackObject {
14 id,
15 kind: ObjectKind::Blob,
16 data: payload,
17 }];
18
19 let mut pack = BufWriter::new(File::create(root.path().join("export.pack"))?);
20 let mut index = BufWriter::new(File::create(root.path().join("export.idx"))?);
21 let written = write_pack(
22 format,
23 &inputs,
24 &mut pack,
25 &mut index,
26 PackWriteLimits::default(),
27 )?;
28
29 pack.flush()?;
30 index.flush()?;
31 drop((pack, index));
32
33 // Only this example owns the new repository. Prepare both files before opening any readers.
34 // A live repository needs a separate safe publication protocol, not this two-rename sequence.
35 let repo_path = root.path().join("private.git");
36 let repo = Repository::init(format, &repo_path, girt::InitKind::Bare)?;
37
38 let basename = format!("objects/pack/pack-{}", written.checksum);
39 fs::rename(
40 root.path().join("export.pack"),
41 repo_path.join(format!("{basename}.pack")),
42 )?;
43 fs::rename(
44 root.path().join("export.idx"),
45 repo_path.join(format!("{basename}.idx")),
46 )?;
47
48 let objects = repo.objects()?;
49 let restored = objects.read(id)?.ok_or("exported object missing")?;
50 assert_eq!(restored.kind(), ObjectKind::Blob);
51 assert_eq!(restored.data(), payload);
52
53 println!(
54 "Verified {} explicit object in {} pack bytes and {} index bytes",
55 written.objects, written.pack_bytes, written.index_bytes
56 );
57 Ok(())
58}13fn main() -> Result<(), Box<dyn std::error::Error>> {
14 let mut args = std::env::args().skip(1);
15 let path = args.next().ok_or("expected repository path")?;
16 let list = args.next().ok_or("expected object-ID list path")?;
17 let rounds: usize = args.next().ok_or("expected number of rounds")?.parse()?;
18 let mode = args.next().unwrap_or_else(|| "read".to_owned());
19 let ids = std::fs::read_to_string(list)?
20 .lines()
21 .map(str::parse)
22 .collect::<Result<Vec<ObjectId>, _>>()?;
23 let repo = Repository::open(path)?;
24 let start = Instant::now();
25 let objects = repo.objects()?;
26 let opening = start.elapsed();
27 let packs: Vec<_> = objects.packs().collect();
28 let bytes = if mode == "midx" {
29 std::fs::read(repo.object_dir().join("pack/multi-pack-index"))?
30 } else {
31 Vec::new()
32 };
33 let midx = if mode == "midx" {
34 Some(girt::multi_pack_index::MultiPackIndex::parse(
35 &bytes,
36 repo.object_format(),
37 )?)
38 } else {
39 None
40 };
41 let start = Instant::now();
42 let mut found = 0_usize;
43 for _ in 0..rounds {
44 for id in &ids {
45 let present = match mode.as_str() {
46 "read" => {
47 black_box(objects.read(*id)?.ok_or("object absent")?);
48 true
49 }
50 "contains" => objects.contains(*id)?,
51 "index-scan" => scan(&packs, *id)?,
52 "midx" => midx.as_ref().ok_or("index absent")?.find(*id).is_some(),
53 _ => return Err("expected read, contains, index-scan or midx".into()),
54 };
55 found += usize::from(black_box(present));
56 }
57 }
58 println!(
59 "objects={} rounds={rounds} mode={mode} found={found} open_ns={} read_ns={}",
60 ids.len(),
61 opening.as_nanos(),
62 start.elapsed().as_nanos(),
63 );
64 Ok(())
65}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}23fn main() -> Result<(), Box<dyn std::error::Error>> {
24 let (_source_root, source) = repository()?;
25 let (_destination_root, destination) = repository()?;
26
27 let loose = source.loose_objects();
28 let blob = loose.write_blob(b"Published by girt through native local storage\n")?;
29 let tree = loose.write_tree(&Tree::new(
30 girt::ObjectFormat::Sha1,
31 vec![TreeEntry {
32 mode: EntryMode::Blob,
33 name: b"hello.txt".to_vec(),
34 id: blob,
35 }],
36 )?)?;
37
38 let author = Signature::now(b"Push Example", b"push@example.com")?;
39 let commit = loose.write_commit(&Commit::new(CommitFields {
40 tree,
41 parents: vec![],
42 author: author.clone(),
43 committer: author.clone(),
44 extra_headers: vec![],
45 message: b"Local push fixture\n".to_vec(),
46 })?)?;
47
48 let tag = loose.write_tag(&Tag::new(TagFields {
49 target: commit,
50 target_kind: ObjectKind::Commit,
51 name: b"v1".to_vec(),
52 tagger: Some(author),
53 extra_headers: vec![],
54 message: b"First publication\n".to_vec(),
55 })?)?;
56
57 let cancel = AtomicBool::new(false);
58 let objects = source.objects()?;
59 let commands = vec![
60 PushCommand::new(RefName::new("refs/heads/main")?, None, commit)
61 .with_force(ForcePolicy::FastForwardOnly),
62 PushCommand::new(RefName::new("refs/tags/v1")?, None, tag)
63 .with_force(ForcePolicy::FastForwardOnly),
64 ];
65 let prepared = PreparedPush::new(&objects, commands, &[], PushLimits::default(), &cancel)?;
66
67 let report = send_local(
68 destination.git_dir(),
69 girt::push::LocalPushRequest::new(&prepared),
70 TransportControl::new(&cancel)
71 .with_deadline(Some(Instant::now() + Duration::from_secs(30))),
72 |_| {},
73 )?;
74
75 // A complete protocol exchange can still contain rejection or partial success.
76 if !report.all_succeeded() {
77 return Err(format!("native local push rejected updates: {report:?}").into());
78 }
79
80 let restored = destination
81 .objects()?
82 .read(blob)?
83 .ok_or("missing pushed blob")?;
84 assert_eq!(
85 restored.data(),
86 b"Published by girt through native local storage\n"
87 );
88
89 println!(
90 "Published branch and annotated tag: {} objects, {} pack bytes",
91 prepared.object_count(),
92 prepared.pack_bytes()
93 );
94
95 let repeated = PreparedPush::new(
96 &objects,
97 vec![
98 PushCommand::new(RefName::new("refs/heads/main")?, Some(commit), commit)
99 .with_force(ForcePolicy::FastForwardOnly),
100 ],
101 &[commit],
102 PushLimits::default(),
103 &cancel,
104 )?;
105
106 let report = send_local(
107 destination.git_dir(),
108 girt::push::LocalPushRequest::new(&repeated),
109 TransportControl::new(&cancel)
110 .with_deadline(Some(Instant::now() + Duration::from_secs(30))),
111 |_| {},
112 )?;
113
114 if !report.all_succeeded() {
115 return Err(format!("repeat rejected: {report:?}").into());
116 }
117 assert_eq!(repeated.object_count(), 0);
118 println!(
119 "Repeated push: {} objects, {} pack bytes",
120 repeated.object_count(),
121 repeated.pack_bytes()
122 );
123
124 Ok(())
125}Sourcepub fn read_controlled(
&self,
id: ObjectId,
limits: ReadLimits,
cancelled: &AtomicBool,
) -> Result<Option<Object>, ObjectReadError>
pub fn read_controlled( &self, id: ObjectId, limits: ReadLimits, cancelled: &AtomicBool, ) -> Result<Option<Object>, ObjectReadError>
Reads with cooperative cancellation between stores, pack lookups and decode chunks.
Loose reads, index lookups, object hashing and individual delta applications finish before the next checkpoint. Blocked filesystem calls cannot be interrupted. No partial object is returned; all per-read buffers are released when the call returns.
§Errors
Returns the same storage errors as Self::read, or ObjectReadError::Cancelled.
use girt::{InitKind, ObjectFormat, PackLimits, ReadLimits, 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"payload")?;
let objects = repo.objects_with(PackLimits::default())?;
let cancelled = std::sync::atomic::AtomicBool::new(true);
assert!(matches!(
objects.read_controlled(id, ReadLimits::default(), &cancelled),
Err(girt::ObjectReadError::Cancelled)
));
assert!(objects.read_with(id, ReadLimits::default())?.is_some());Examples found in repository?
47fn main() -> Result<(), Box<dyn std::error::Error>> {
48 let args = Args::parse();
49 let repository = Repository::open(args.repository)?;
50 let id = args.id;
51
52 let baseline = handles();
53 let failed = repository.objects_with(PackLimits::default().with_max_index_bytes(0));
54 assert!(matches!(
55 failed,
56 Err(girt::ObjectReadError::Limit("pack index bytes"))
57 ));
58 assert_eq!(handles(), baseline);
59
60 if args.mode == Some(Mode::Cancel) {
61 let cancelled = AtomicBool::new(false);
62 let start = Instant::now();
63
64 let result = std::thread::scope(|scope| {
65 scope.spawn(|| {
66 std::thread::sleep(Duration::from_millis(10));
67 cancelled.store(true, std::sync::atomic::Ordering::Relaxed);
68 });
69 repository.objects_controlled(
70 PackLimits::default(),
71 AlternateLimits::default(),
72 &cancelled,
73 )
74 });
75
76 assert!(matches!(result, Err(girt::ObjectReadError::Cancelled)));
77 assert_eq!(handles(), baseline);
78
79 println!(
80 "cancel_us={} handles_before={baseline:?} handles_after={:?}",
81 start.elapsed().as_micros(),
82 handles()
83 );
84 return Ok(());
85 }
86
87 let cancelled = AtomicBool::new(false);
88 let start = Instant::now();
89 let mut objects = repository.objects_controlled(
90 PackLimits::default(),
91 AlternateLimits::default(),
92 &cancelled,
93 )?;
94
95 let opened = start.elapsed();
96 let retained = handles();
97 let clone = objects.clone();
98 let cloned = handles();
99
100 let failed = objects.refresh(
101 PackLimits::default().with_max_index_bytes(0),
102 AlternateLimits::default(),
103 );
104 assert!(matches!(
105 failed,
106 Err(girt::ObjectReadError::Limit("pack index bytes"))
107 ));
108 assert_eq!(handles(), cloned);
109
110 let start = Instant::now();
111 objects.refresh(PackLimits::default(), AlternateLimits::default())?;
112 let refreshed = start.elapsed();
113
114 let generations = handles();
115 assert_eq!(
116 generations,
117 baseline
118 .zip(retained)
119 .map(|(base, open)| base + 2 * (open - base))
120 );
121
122 println!(
123 "refresh_us={} handles_two_generations={generations:?}",
124 refreshed.as_micros()
125 );
126
127 let start = Instant::now();
128 let object = clone
129 .read_controlled(id, ReadLimits::default(), &cancelled)?
130 .ok_or("object missing")?;
131 let read = start.elapsed();
132
133 let start = Instant::now();
134 let second = clone
135 .read_with(id, ReadLimits::default())?
136 .ok_or("object missing")?;
137 let reread = start.elapsed();
138
139 assert_eq!(object, second);
140 println!(
141 "pid={} open_us={} first_read_us={} warm_read_us={} payload_bytes={} handles_before={baseline:?} handles_open={retained:?} handles_clone={cloned:?}",
142 std::process::id(),
143 opened.as_micros(),
144 read.as_micros(),
145 reread.as_micros(),
146 object.data().len()
147 );
148
149 drop(second);
150 drop(object);
151 drop(objects);
152 drop(clone);
153
154 println!("handles_after_drop={:?}", handles());
155 assert_eq!(handles(), baseline);
156
157 // Optional sampling interval is outside measured operations and retains no reader.
158 if args.mode == Some(Mode::Hold) {
159 std::thread::sleep(Duration::from_secs(1));
160 }
161 Ok(())
162}Source§impl Objects
impl Objects
Sourcepub fn write_patch(
&self,
out: impl Write,
entries: &[Entry],
) -> Result<(), Error>
pub fn write_patch( &self, out: impl Write, entries: &[Entry], ) -> Result<(), Error>
Writes entries as a unified patch with default options and limits, without attributes or
cancellation.
Wraps Self::write_patch_with with Options::default, no DiffAttributes and
Limits::default. See the module documentation for the output format and
its differences from git diff; Repo::write_patch applies the
repository’s attributes.
§Errors
Returns Error for write, storage, limit and abbreviation failures.
Sourcepub fn write_patch_with(
&self,
out: impl Write,
entries: &[Entry],
options: Options,
attributes: Option<&mut DiffAttributes>,
limits: Limits,
cancel: &AtomicBool,
) -> Result<(), Error>
pub fn write_patch_with( &self, out: impl Write, entries: &[Entry], options: Options, attributes: Option<&mut DiffAttributes>, limits: Limits, cancel: &AtomicBool, ) -> Result<(), Error>
Writes entries as a unified patch under explicit options, attributes, limits and
cancellation.
Entries are rendered in slice order;
Comparison::entries already uses Git’s order.
A change between a regular file or symlink and a different type (Git’s T status)
is written as a deletion followed by an addition, as Git does. Gitlinks render as one
Subproject commit <id> line per side, as git diff does without --submodule. A rename
or copy prints its similarity from the entry. Identical identities print headers only.
With attributes, each side’s driver comes from its own path (the old path for the old
side of a rename); see DiffAttributes. Gitlink sides have no driver. Attribute files are
read as paths first need them.
Blob reads are synchronous. Cancellation is checked before each entry and inside each read and comparison; a single storage read or writer call cannot be interrupted.
§Errors
Returns Error on the first write, storage, attribute, driver, limit, abbreviation or
cancellation failure, after writing the complete patches of earlier entries and possibly
part of the failing entry. Attribute and driver failures occur before the entry’s first
line. Nothing is written to storage.
Examples found in repository?
41fn main() -> Result<(), Box<dyn std::error::Error>> {
42 let args = Args::parse();
43 let temp = tempfile::tempdir()?;
44 let mut out = io::BufWriter::new(io::stdout().lock());
45
46 let Some(path) = args.path else {
47 // The fixture compares two trees directly; no commits are needed.
48 let repo = Repository::init(ObjectFormat::Sha1, temp.path().join("r"), InitKind::Bare)?;
49 let loose = repo.loose_objects();
50 let mut editor = TreeEditor::new(&loose);
51 editor.set(b"notes.txt", loose.write_blob(b"one\ntwo\nthree\n")?)?;
52 editor.set(b"old name", loose.write_blob(b"moved\ncontent\n")?)?;
53 let old = editor.write()?;
54
55 editor.set(b"notes.txt", loose.write_blob(b"one\n2\nthree")?)?;
56 editor.remove(b"old name")?;
57 editor.set(b"new name", loose.write_blob(b"moved\ncontent\n")?)?;
58 let new = editor.write()?;
59
60 let git = Repo::new(repo);
61 git.write_patch(&mut out, old, new)?;
62 out.flush()?;
63 return Ok(());
64 };
65
66 if args.unified == 3 && !args.trusted && !args.no_indent_heuristic {
67 Repo::discover(path)?.write_patch(&mut out, &args.old, &args.new)?;
68 out.flush()?;
69 return Ok(());
70 }
71
72 // Explicit options and limits go through the core API the handle composes.
73 let environment = Environment::from_process();
74 let repo = Repository::discover_with_config(path, &ConfigInputs::git_default(&environment)?)?;
75
76 let (packs, rewrite_limits, limits) = if args.trusted {
77 let diff = DiffLimits::default()
78 .with_max_input_bytes(usize::MAX)
79 .with_max_lines(usize::MAX)
80 .with_max_trace(usize::MAX)
81 .with_max_work(usize::MAX);
82
83 // Keep the default rename candidate limit, which matches Git's, but lift resources.
84 let rewrites = rewrites::Limits::default()
85 .with_read(ReadLimits::trusted())
86 .with_max_blob_bytes(usize::MAX)
87 .with_max_spans(usize::MAX)
88 .with_max_work(usize::MAX);
89
90 (
91 PackLimits::trusted(),
92 rewrites,
93 Limits::default()
94 .with_read(ReadLimits::trusted())
95 .with_diff(diff),
96 )
97 } else {
98 (
99 PackLimits::default(),
100 rewrites::Limits::default(),
101 Limits::default(),
102 )
103 };
104
105 let objects = repo.objects_with(packs)?;
106 let resolver = Resolver::new(&repo, &objects)?;
107 let old = resolver.resolve_to(&args.old.parse()?, ObjectKind::Tree)?;
108 let new = resolver.resolve_to(&args.new.parse()?, ObjectKind::Tree)?;
109
110 let cancel = AtomicBool::new(false);
111 let comparison = objects.diff_trees_with(
112 Some(old),
113 Some(new),
114 rewrites::Options::default().with_track_empty(true),
115 rewrite_limits,
116 &cancel,
117 )?;
118
119 // Apply `.gitattributes` and `diff.<driver>` settings as `git diff` does.
120 let mut attributes = repo.diff_attributes(&environment)?;
121 objects.write_patch_with(
122 &mut out,
123 &comparison.entries,
124 Options::default()
125 .with_context_lines(args.unified)
126 .with_indent_heuristic(!args.no_indent_heuristic),
127 Some(&mut attributes),
128 limits,
129 &cancel,
130 )?;
131
132 out.flush()?;
133 Ok(())
134}Source§impl Objects
impl Objects
Sourcepub fn peel(
&self,
id: ObjectId,
limits: PeelLimits,
cancel: &AtomicBool,
) -> Result<PeeledObject, PeelError>
pub fn peel( &self, id: ObjectId, limits: PeelLimits, cancel: &AtomicBool, ) -> Result<PeeledObject, PeelError>
Resolves tags to a terminal object, checking each declared target kind.
Reads verified storage objects and decodes tag targets in the store’s format. Terminal commits must have decodable graph records; terminal trees remain uninterpreted, matching Git peeling. Metadata and canonical construction rules are separate. Cancellation is checked before and after each synchronous read, not during it. Memory is bounded by tag count and one per-read payload; total payload and per-read delta work are bounded separately. No files or references are changed and no partial result is returned.
§Errors
Returns contextual PeelError for missing/corrupt/wrong-kind targets, malformed tags,
cycles, limits or cancellation. Existing corrupt objects never become an absent result.
use std::sync::atomic::AtomicBool;
use girt::{
InitKind, ObjectFormat, ObjectKind, PackLimits, PeelLimits, Repository, Tag, TagFields,
};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r.git"),
InitKind::Bare,
)?;
let loose = repo.loose_objects();
let blob = loose.write_blob(b"manifest")?;
let tag = Tag::new(TagFields {
target: blob,
target_kind: ObjectKind::Blob,
name: b"v1".to_vec(),
tagger: None,
extra_headers: vec![],
message: b"Reviewed".to_vec(),
})?;
let inner = loose.write_tag(&tag)?;
let outer = Tag::new(TagFields {
target: inner,
target_kind: ObjectKind::Tag,
name: b"release".to_vec(),
tagger: None,
extra_headers: vec![],
message: b"Release pointer".to_vec(),
})?;
let tip = loose.write_tag(&outer)?;
let objects = repo.objects_with(PackLimits::default())?;
let peeled = objects.peel(tip, PeelLimits::default(), &AtomicBool::new(false))?;
assert_eq!(peeled.tags, [tip, inner]);
assert_eq!(peeled.target, blob);
assert_eq!(peeled.kind, ObjectKind::Blob);Source§impl Objects
impl Objects
Sourcepub fn autosquash(
&self,
commits: &[ObjectId],
max_commits: usize,
) -> Result<Todo, AutosquashError>
pub fn autosquash( &self, commits: &[ObjectId], max_commits: usize, ) -> Result<Todo, AutosquashError>
Creates a linear todo with Git’s fixup!, squash! and amend! subjects reordered.
Targets must precede the marked input in the supplied sequence. Exact subject or identity
prefix matches take precedence over subject-prefix matches. Unmatched markers remain picks.
Fixups attach after the target’s existing fixup group in input order; amend! selects fixup
message replacement. The caller executes the returned plan separately.
§Errors
Returns AutosquashError for the explicit count bound or unreadable inputs.
Source§impl Objects
impl Objects
Sourcepub fn rebase(
&self,
commits: &[ObjectId],
onto: ObjectId,
committer: Signature,
options: RebaseOptions,
cancel: &AtomicBool,
) -> Result<Rebase, RebaseError>
pub fn rebase( &self, commits: &[ObjectId], onto: ObjectId, committer: Signature, options: RebaseOptions, cancel: &AtomicBool, ) -> Result<Rebase, RebaseError>
Replays explicit ordered commits onto a destination entirely in memory.
The caller selects and orders the range; no ancestry walk, patch-ID filtering, fast-forward reuse or merge-topology reconstruction occurs. Each input creates a new single-parent commit or follows its explicit empty policy. Source authors are retained and the supplied committer is used for every new commit. Merge inputs require the replay mainline option.
Generated trees and blobs remain visible to subsequent steps through an in-memory overlay. The first conflict or stop-on-empty outcome returns completed progress and the unresolved replay. Skipped inputs map to the current tip. Errors or cancellation discard all progress; no object, state file, working file, index or reference has changed.
§Errors
Returns RebaseError for input bounds, cancellation, unreadable inputs or replay failure.
Sourcepub fn rebase_with_driver(
&self,
commits: &[ObjectId],
onto: ObjectId,
committer: Signature,
options: RebaseOptions,
cancel: &AtomicBool,
driver: &mut dyn TreeDriver,
) -> Result<Rebase, RebaseError>
pub fn rebase_with_driver( &self, commits: &[ObjectId], onto: ObjectId, committer: Signature, options: RebaseOptions, cancel: &AtomicBool, driver: &mut dyn TreeDriver, ) -> Result<Rebase, RebaseError>
Replays a sequence with the same caller-owned content driver at every step.
Uses Self::rebase’s explicit range, metadata, progress and publication contracts. Driver
snapshots and side effects remain caller-owned; no attributes or processes run implicitly.
§Errors
Returns RebaseError, retaining path-driver failures in the replay error source chain.
Source§impl Objects
impl Objects
Sourcepub fn execute_todo(
&self,
todo: &Todo,
onto: ObjectId,
committer: Signature,
options: Options,
cancel: &AtomicBool,
hooks: &mut dyn TodoHooks,
) -> Result<TodoRun, TodoExecutionError>
pub fn execute_todo( &self, todo: &Todo, onto: ObjectId, committer: Signature, options: Options, cancel: &AtomicBool, hooks: &mut dyn TodoHooks, ) -> Result<TodoRun, TodoExecutionError>
Executes a linear todo plan in memory, with explicit caller message and command hooks.
Supports noop/pick/reword/edit/squash/fixup/drop/exec/break/label/update-ref. Merge and reset commands are refused before any hook runs because this engine cannot resolve generated ancestry for recursive merge bases. Labels are checked for duplicates; topology execution remains unsupported. Fixup and squash replace the preceding commit’s tree/message while preserving its author and parents. Drop records no rewrite mapping.
No object, index, working file, state file or reference is installed by the executor. Hooks are explicitly caller-owned and may have side effects. For a stop, callers retain this result; resuming a complex edited plan requires a caller-owned continuation context.
§Errors
Returns TodoExecutionError for unsupported merges, unknown labels, missing predecessors,
invalid objects, replay/metadata failures, cancellation or a caller hook failure.
Source§impl Objects
impl Objects
Sourcepub fn cherry_pick(
&self,
source: ObjectId,
destination: ObjectId,
committer: Signature,
options: Options,
cancel: &AtomicBool,
) -> Result<Replay, Error>
pub fn cherry_pick( &self, source: ObjectId, destination: ObjectId, committer: Signature, options: Options, cancel: &AtomicBool, ) -> Result<Replay, Error>
Computes a cherry-pick using source author and explicit new committer metadata.
Source signatures and opaque headers are not copied. Message bytes are retained, with an optional provenance paragraph. Root sources use an empty base. Merge sources require a one-based mainline. Conflicts never propose a commit; empty policy is applied only after a clean merge. No objects, refs, index or working files are written.
§Errors
Returns Error for invalid mainline, unreadable inputs, invalid metadata or merge
failure.
Sourcepub fn revert(
&self,
source: ObjectId,
destination: ObjectId,
committer: Signature,
options: Options,
cancel: &AtomicBool,
) -> Result<Replay, Error>
pub fn revert( &self, source: ObjectId, destination: ObjectId, committer: Signature, options: Options, cancel: &AtomicBool, ) -> Result<Replay, Error>
Computes the inverse of a commit, using the supplied identity as author and committer.
Uses Self::cherry_pick’s computation and publication contract. The message uses Git’s
Revert "subject" title (Reapply when reverting a revert) and full source identity. For
merge sources the explanation also names the selected parent. The original commit is the
merge base; its parent supplies the inverse side. Root reverts select an empty inverse
side.
§Errors
Returns Error for invalid mainline, unreadable inputs, invalid metadata or merge
failure.
Sourcepub fn cherry_pick_with_driver(
&self,
source: ObjectId,
destination: ObjectId,
committer: Signature,
options: Options,
cancel: &AtomicBool,
driver: &mut dyn TreeDriver,
) -> Result<Replay, Error>
pub fn cherry_pick_with_driver( &self, source: ObjectId, destination: ObjectId, committer: Signature, options: Options, cancel: &AtomicBool, driver: &mut dyn TreeDriver, ) -> Result<Replay, Error>
Computes cherry-pick with caller-owned per-path attribute/content policy.
Uses Self::cherry_pick’s metadata, bounds and publication contract. No attribute
snapshot or external process is selected implicitly; driver side effects remain
caller-owned.
§Errors
Returns Error, preserving content-driver failures through the merge error source.
Sourcepub fn revert_with_driver(
&self,
source: ObjectId,
destination: ObjectId,
committer: Signature,
options: Options,
cancel: &AtomicBool,
driver: &mut dyn TreeDriver,
) -> Result<Replay, Error>
pub fn revert_with_driver( &self, source: ObjectId, destination: ObjectId, committer: Signature, options: Options, cancel: &AtomicBool, driver: &mut dyn TreeDriver, ) -> Result<Replay, Error>
Computes revert with caller-owned per-path attribute/content policy.
Uses Self::revert’s metadata, bounds and publication contract. Driver inputs use the
reversed base/side roles; callback side effects remain caller-owned.
§Errors
Returns Error, retaining original path-driver failures as merge error sources.
Source§impl Objects
impl Objects
Sourcepub fn detect_rewrites(
&self,
old: Option<ObjectId>,
new: Option<ObjectId>,
options: Options,
limits: Limits,
targets: Option<&[&[u8]]>,
cancel: &AtomicBool,
) -> Result<Vec<Rewrite>, Error>
pub fn detect_rewrites( &self, old: Option<ObjectId>, new: Option<ObjectId>, options: Options, limits: Limits, targets: Option<&[&[u8]]>, cancel: &AtomicBool, ) -> Result<Vec<Rewrite>, Error>
Infers renames and optional copies between two stored trees, without changing storage.
Only additions of regular/executable blobs are destinations. Deleted blobs are rename
sources; Copies::Modified additionally uses preimages of modified blobs, including
mode-only changes, and permits source reuse. Copies::ModifiedPostimage scores their
postimages but reports their preimage IDs. Symlinks, gitlinks and directories are never
results. File/directory replacements use the structural comparison’s leaf changes.
Both object formats are supported; candidate blobs are read and verified even for exact IDs.
Exact identity matches precede unique-basename pairing and exhaustive edited inference.
Unique unmatched basenames pair without copies at halfway from the minimum score to 100%.
This can prefer a basename pair over a more similar differently named destination.
Exact ties prefer an unused deleted source, then equal basename, then source raw path order.
Edited ties prefer higher rounded percentage, equal basename, source path, then
destination path; selected pairs greedily consume destinations and (without copies)
sources. This explicit deterministic policy does not promise Git’s every heuristic
pairing choice. Rewrite defines scoring.
targets is an optional set of exact destination byte paths, applied after all
inference and classification. It cannot change a tie or convert a copy into a rename.
Results sort by target bytes. None allows all; an empty slice returns no records but
still performs validation/inference. Paths need no OS/UTF-8 conversion.
Cancellation is checked before validation, between reads/candidates, during span scanning,
and before and after sorting. Individual storage reads, allocation, tree parsing and sorting
cannot be interrupted. Callers own worker scheduling and must leave cancel set until
return. No attributes, clean/smudge/textconv drivers, index or worktree are consulted: these
are comparisons of stored blob bytes. Callers own configuration and merge/history policy.
Self::detect_rewrites_or_exact instead returns the pairs already found when the
Options::candidate_limit skips exhaustive edited-content detection.
§Errors
Returns Error on invalid options, tree/blob storage failures, missing/wrong-kind blobs,
cancellation, or exhausted candidate/resource limits. Errors return no partial inference;
no repository state is changed. Exact matching is attempted before the candidate limit,
but storage and work bounds apply throughout. See Limits for memory accounting.
use std::sync::atomic::AtomicBool;
use girt::rewrites::{Kind, Limits, Options};
use girt::{EntryMode, InitKind, ObjectFormat, Repository, Tree, TreeEntry};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r"),
InitKind::Bare,
)?;
let loose = repo.loose_objects();
let tree = |files: &[(&[u8], &[u8])]| -> Result<_, Box<dyn std::error::Error>> {
let entries = files
.iter()
.map(|(name, bytes)| {
Ok(TreeEntry {
name: name.to_vec(),
mode: EntryMode::Blob,
id: loose.write_blob(bytes)?,
})
})
.collect::<Result<Vec<_>, Box<dyn std::error::Error>>>()?;
Ok(loose.write_tree(&Tree::new(ObjectFormat::Sha1, entries)?)?)
};
let old = tree(&[(b"before", b"same content\n")])?;
let new = tree(&[(b"after", b"same content\n")])?;
let records = repo.objects_with(Default::default())?.detect_rewrites(
Some(old),
Some(new),
Options::default(),
Limits::default(),
None,
&AtomicBool::new(false),
)?;
assert_eq!(records[0].kind, Kind::Rename);
assert_eq!(records[0].similarity, 100);
assert_eq!(records[0].source_id, records[0].target_id);Examples found in repository?
24fn main() -> girt::Result<()> {
25 let args = Args::parse();
26 let repo = Repository::open(args.repository)?;
27 let objects = repo.objects()?;
28
29 let records = objects.detect_rewrites(
30 Some(args.old_tree),
31 Some(args.new_tree),
32 Options::default(),
33 Limits::default(),
34 None,
35 &AtomicBool::new(false),
36 )?;
37
38 let mut output = io::stdout().lock();
39 for record in records {
40 write!(output, "{}{:03}\0", record.kind.letter(), record.similarity)?;
41 output.write_all(&record.source)?;
42 output.write_all(&[0])?;
43 output.write_all(&record.target)?;
44 output.write_all(&[0])?;
45 }
46 Ok(())
47}Sourcepub fn detect_rewrites_or_exact(
&self,
old: Option<ObjectId>,
new: Option<ObjectId>,
options: Options,
limits: Limits,
targets: Option<&[&[u8]]>,
cancel: &AtomicBool,
) -> Result<Detection, Error>
pub fn detect_rewrites_or_exact( &self, old: Option<ObjectId>, new: Option<ObjectId>, options: Options, limits: Limits, targets: Option<&[&[u8]]>, cancel: &AtomicBool, ) -> Result<Detection, Error>
Infers rewrites as Self::detect_rewrites does, keeping pre-limit matches.
When the remaining candidates exceed Options::candidate_limit, edited-content
detection is skipped and Detection::inexact_skipped says so. Exact and unique-basename
pairs are still returned. Callers decide whether to warn, retry with a higher limit, or
accept the pairs found before the limit.
§Errors
Returns Error on the same conditions as Self::detect_rewrites, except that an
exceeded candidate limit is reported in the result rather than as Error::Candidates.
§Keep exact renames when edited detection is too expensive
The fallback API, available since 0.10.0, keeps a useful result for large changes while making its limitation visible. Unpaired additions/deletions must remain visible to the caller. Other work limits and storage failures still return errors, never partial detection.
use std::sync::atomic::AtomicBool;
use girt::rewrites::{Error, Limits, Options};
use girt::{EntryMode, InitKind, ObjectFormat, Repository, Tree, TreeEntry};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r"),
InitKind::Bare,
)?;
let loose = repo.loose_objects();
let tree = |files: &[(&[u8], &[u8])]| -> Result<_, Box<dyn std::error::Error>> {
let entries = files
.iter()
.map(|(name, bytes)| {
Ok(TreeEntry {
name: name.to_vec(),
mode: EntryMode::Blob,
id: loose.write_blob(bytes)?,
})
})
.collect::<Result<Vec<_>, Box<dyn std::error::Error>>>()?;
Ok(loose.write_tree(&Tree::new(ObjectFormat::Sha1, entries)?)?)
};
let old = tree(&[
(b"exact-old", b"identical\n"),
(b"edited-old", b"line one\nline two\n"),
])?;
let new = tree(&[
(b"exact-new", b"identical\n"),
(b"edited-new", b"line one\nline THREE\n"),
])?;
let objects = repo.objects_with(Default::default())?;
let options = Options::default().with_candidate_limit(0);
let cancel = AtomicBool::new(false);
let detection = objects.detect_rewrites_or_exact(
Some(old),
Some(new),
options,
Limits::default(),
None,
&cancel,
)?;
assert_eq!(detection.rewrites.len(), 1); // The identical pair survived.
assert_eq!(detection.rewrites[0].source, b"exact-old");
let skipped = detection.inexact_skipped.expect("one edited pair remains");
assert_eq!((skipped.sources, skipped.targets, skipped.limit), (1, 1, 0));
assert_eq!(skipped.suggested_limit(), 1);
assert!(matches!(
objects.detect_rewrites(
Some(old),
Some(new),
options,
Limits::default(),
None,
&cancel
),
Err(Error::Candidates { .. })
));Source§impl Objects
impl Objects
Sourcepub fn apply_stash(
&self,
saved: &Snapshot,
input: ApplyInput,
options: ApplyOptions,
cancel: &AtomicBool,
) -> Result<Apply, Error>
pub fn apply_stash( &self, saved: &Snapshot, input: ApplyInput, options: ApplyOptions, cancel: &AtomicBool, ) -> Result<Apply, Error>
Reapplies saved changes onto an explicit current index, without writing repository state.
Uses the stash base as the common ancestor and the saved worktree as theirs. Optional index restoration merges the saved index delta first; a conflict there returns immediately with original sides. Ordinary clean apply keeps current staged changes and stages paths newly added by the stash, matching Git. A tracked conflict retains merge stages instead.
Saved untracked paths are checked against current tracked and caller-observed occupied paths, including ancestor/descendant collisions. Even identical existing files are refused. Guarded checkout must protect tracked worktree edits and revalidate the occupied observation. This does not run filters, discover untracked paths, install objects or drop a stash entry.
§Errors
Returns input/merge/preparation failures, bounds or Error::UntrackedCollision.
Existing merge limits and unsupported structural layouts remain explicit refusals.
Source§impl Objects
impl Objects
Sourcepub fn create_stash(
&self,
input: SaveInput,
author: Signature,
committer: Signature,
options: SaveOptions,
cancel: &AtomicBool,
) -> Result<Option<Save>, Error>
pub fn create_stash( &self, input: SaveInput, author: Signature, committer: Signature, options: SaveOptions, cancel: &AtomicBool, ) -> Result<Option<Save>, Error>
Constructs saved commits and cleanup, returning None when no selected changes exist.
Both identities are explicit, matching Git’s separate author/committer environment. Default index/untracked messages end in LF; the worktree message has no added final LF. No trees, commits, refs, index or files are written. Staged cleanup uses the existing merge engine; it can retain conflicts where Git’s patch-based cleanup refuses an overlapping worktree edit. Its conflict result never authorizes cleanup publication.
§Errors
Returns invalid input metadata, unsupported selection or object/merge errors. Tree roots are read, but this does not prove recursive object connectivity.
Source§impl Objects
impl Objects
Sourcepub fn compare_trees_with(
&self,
old: Option<ObjectId>,
new: Option<ObjectId>,
limits: TreeCompareLimits,
cancel: &AtomicBool,
) -> Result<Vec<TreeChange>, TreeCompareError>
pub fn compare_trees_with( &self, old: Option<ObjectId>, new: Option<ObjectId>, limits: TreeCompareLimits, cancel: &AtomicBool, ) -> Result<Vec<TreeChange>, TreeCompareError>
Compares two tree IDs recursively, using None for an empty side.
Returns leaf additions, removals and changes in ascending lexicographic raw full-path
byte order (shorter prefix first). Git tree ordering is validated before pairing entries
by byte name: files and directories with the same name must pair even when their positions
in Git order differ. A file/directory replacement yields a removal/addition at the file
path and additions/removals below the directory. Empty directories yield no leaf records.
No rename/copy detection, text diff, index or worktree access occurs; use
Self::compare_trees_matching to limit paths.
Equal subtree IDs, including equal roots, are trusted and skipped without any read. This establishes structural identity, not validity or existence. Other encountered trees are identity-verified by storage, parsed and structurally validated. Blob, symlink and gitlink targets are never read, even when changed; dangling or wrong-kind leaf targets therefore remain unvalidated. Gitlinks are not interpreted as local subtrees.
I/O and comparison are synchronous and read-only. Traversal uses an explicit stack, not
recursion. Set cancel from another thread and leave it set until return. Checks occur
before starting (even for equal roots), between tree reads and entry processing, and before
and after sorting output. Individual reads, parsing, validation and sorting cannot be
interrupted; cancellation is cooperative, without a wall-clock guarantee. Callers using
an async runtime should run the whole operation on a caller-bounded blocking worker.
§Errors
Returns TreeCompareError for a visited missing, wrong-kind, corrupt, malformed or
structurally invalid tree, cancellation, or an exhausted bound. Read/parse failures include
the tree ID and byte path (empty for the root). No partial results or filesystem mutations
are produced. Both object formats and the five modes accepted by Tree::parse are
supported.
use std::sync::atomic::AtomicBool;
use girt::{
EntryMode, InitKind, ObjectFormat, PackLimits, Repository, Tree, TreeCompareLimits,
TreeEntry,
};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r"),
InitKind::Bare,
)?;
let loose = repo.loose_objects();
let tree = |content: &[u8]| -> Result<_, Box<dyn std::error::Error>> {
let id = loose.write_blob(content)?;
let entry = TreeEntry {
mode: EntryMode::Blob,
name: b"notes.txt".to_vec(),
id,
};
Ok(loose.write_tree(&Tree::new(ObjectFormat::Sha1, vec![entry])?)?)
};
let (old, new) = (tree(b"draft\n")?, tree(b"final\n")?);
let objects = repo.objects_with(PackLimits::default())?;
let cancel = AtomicBool::new(false);
let limits = TreeCompareLimits::default();
let changes = objects.compare_trees_with(Some(old), Some(new), limits, &cancel)?;
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].path, b"notes.txt");
assert_ne!(
changes[0].old.map(|old| old.id),
changes[0].new.map(|new| new.id)
);
// `None` stands for an empty tree on either side.
assert!(
objects
.compare_trees_with(None, None, limits, &cancel)?
.is_empty()
);Sourcepub fn compare_trees_matching(
&self,
old: Option<ObjectId>,
new: Option<ObjectId>,
paths: &Pathspec,
limits: TreeCompareLimits,
cancel: &AtomicBool,
) -> Result<Vec<TreeChange>, TreeCompareError>
pub fn compare_trees_matching( &self, old: Option<ObjectId>, new: Option<ObjectId>, paths: &Pathspec, limits: TreeCompareLimits, cancel: &AtomicBool, ) -> Result<Vec<TreeChange>, TreeCompareError>
Self::compare_trees_with, keeping only changes at paths paths selects.
This is the comparison git diff-tree -r <old> <new> -- <pathspec> makes. Subtrees the
pathspec cannot reach are not read, and paths below a subtree it selects whole are not
tested, so a narrow pathspec over a large tree reads only the trees on the way to it.
use std::sync::atomic::AtomicBool;
use girt::pathspec::{Context, Pathspec};
use girt::tree::TreeEditor;
use girt::{InitKind, ObjectFormat, Repository};
let directory = tempfile::tempdir()?;
let repo = Repository::init(
ObjectFormat::Sha1,
directory.path().join("r"),
InitKind::Bare,
)?;
let loose = repo.loose_objects();
let mut editor = TreeEditor::new(&loose);
editor.set(b"src/lib.rs", loose.write_blob(b"old\n")?)?;
editor.set(b"docs/guide.md", loose.write_blob(b"old\n")?)?;
let old = editor.write()?;
editor.set(b"src/lib.rs", loose.write_blob(b"new\n")?)?;
editor.set(b"docs/guide.md", loose.write_blob(b"new\n")?)?;
let new = editor.write()?;
let paths = Pathspec::parse(["*.rs"], &Context::new())?;
let objects = repo.objects()?;
let changes = objects.compare_trees_matching(
Some(old),
Some(new),
&paths,
Default::default(),
&AtomicBool::new(false),
)?;
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].path, b"src/lib.rs");§Errors
Returns the errors of Self::compare_trees_with, and TreeCompareError::Unsupported
for a pathspec with attr: magic, which needs attributes a tree comparison does not load.
Sourcepub fn compare_trees(
&self,
old: Option<ObjectId>,
new: Option<ObjectId>,
) -> Result<Vec<TreeChange>, TreeCompareError>
pub fn compare_trees( &self, old: Option<ObjectId>, new: Option<ObjectId>, ) -> Result<Vec<TreeChange>, TreeCompareError>
Compares two trees with TreeCompareLimits::default and no cancellation.
Equivalent to self.compare_trees_with(old, new, TreeCompareLimits::default(), &never) with
a flag that is never set; see Self::compare_trees_with for ordering and
verification.
§Errors
Returns the errors of Self::compare_trees_with except cancellation.
Examples found in repository?
38fn main() -> girt::Result<()> {
39 let args = Args::parse();
40 let temporary;
41 let (repo, old, new) =
42 if let (Some(path), Some(old), Some(new)) = (args.repo, args.old, args.new) {
43 (Repository::open(path)?, old.0, new.0)
44 } else {
45 temporary = tempfile::tempdir()?;
46 let repo = Repository::init(
47 girt::ObjectFormat::Sha1,
48 temporary.path().join("sample"),
49 InitKind::Bare,
50 )?;
51
52 let store = repo.loose_objects();
53 let old_blob = store.write_blob(b"hello\n")?;
54 let new_blob = store.write_blob(b"hello, trees\n")?;
55
56 let old = store.write_tree(&Tree::new(
57 girt::ObjectFormat::Sha1,
58 vec![TreeEntry {
59 name: b"hello.txt".to_vec(),
60 mode: EntryMode::Blob,
61 id: old_blob,
62 }],
63 )?)?;
64 let new = store.write_tree(&Tree::new(
65 girt::ObjectFormat::Sha1,
66 vec![TreeEntry {
67 name: b"hello.txt".to_vec(),
68 mode: EntryMode::Executable,
69 id: new_blob,
70 }],
71 )?)?;
72
73 (repo, Some(old), Some(new))
74 };
75
76 let objects = repo.objects()?;
77 let changes = objects.compare_trees(old, new)?;
78
79 for change in changes {
80 // Git's quoting keeps non-UTF-8 names and control bytes distinguishable on a terminal.
81 println!(
82 "{} {}: {} -> {}",
83 change.status().letter(),
84 quote_path(&change.path),
85 describe(change.old),
86 describe(change.new)
87 );
88 }
89
90 Ok(())
91}More examples
8fn main() -> Result<(), Box<dyn std::error::Error>> {
9 let root = tempfile::tempdir()?;
10 let repo = Repository::init(
11 girt::ObjectFormat::Sha1,
12 root.path().join("repo"),
13 InitKind::Bare,
14 )?;
15
16 let store = repo.loose_objects();
17 let old_blob = store.write_blob(b"heading\r\nold\n")?;
18 let new_blob = store.write_blob(b"heading\r\nnew\xff")?;
19
20 let old = store.write_tree(&Tree::new(
21 girt::ObjectFormat::Sha1,
22 vec![TreeEntry {
23 name: b"example.txt".to_vec(),
24 mode: EntryMode::Blob,
25 id: old_blob,
26 }],
27 )?)?;
28 let new = store.write_tree(&Tree::new(
29 girt::ObjectFormat::Sha1,
30 vec![TreeEntry {
31 name: b"example.txt".to_vec(),
32 mode: EntryMode::Executable,
33 id: new_blob,
34 }],
35 )?)?;
36
37 let objects = repo.objects()?;
38 let cancel = AtomicBool::new(false);
39 let limits = DiffLimits::default();
40
41 let changes = objects.compare_trees(Some(old), Some(new))?;
42 for change in changes {
43 println!(
44 "{}: {:?} -> {:?}",
45 escaped(&change.path),
46 change.old.map(|v| v.mode),
47 change.new.map(|v| v.mode)
48 );
49
50 // Gitlinks require a submodule policy; the blob adapter deliberately rejects them.
51 let content = BlobContent::read(
52 &objects,
53 &change,
54 ReadLimits::default(),
55 limits.max_input_bytes,
56 &cancel,
57 )?;
58
59 match diff(
60 &content.old,
61 &content.new,
62 BinaryMode::Auto,
63 limits,
64 &cancel,
65 )? {
66 ContentDiff::Unchanged => println!(" unchanged content (check modes/existence above)"),
67 ContentDiff::Binary { old, new } => {
68 println!(" binary: {} -> {} bytes", old.len(), new.len())
69 }
70 ContentDiff::Text(text) => {
71 for edit in text.edits() {
72 println!(
73 " old lines {:?} -> new lines {:?} (zero-based)",
74 edit.old_lines, edit.new_lines
75 );
76 println!(" - {}", escaped(&text.old_bytes()[edit.old_bytes.clone()]));
77 println!(" + {}", escaped(&text.new_bytes()[edit.new_bytes.clone()]));
78 }
79 }
80 _ => return Err("unsupported content diff kind".into()),
81 }
82 }
83
84 Ok(())
85}Source§impl Objects
impl Objects
Sourcepub fn diff_trees(
&self,
old: Option<ObjectId>,
new: Option<ObjectId>,
) -> Result<Comparison, Error>
pub fn diff_trees( &self, old: Option<ObjectId>, new: Option<ObjectId>, ) -> Result<Comparison, Error>
Compares trees and infers renames using default options and limits, without cancellation.
Wraps Self::diff_trees_with with default options and limits.
§Errors
Returns storage, malformed-tree, resource or inference errors from the two explicit APIs.
Sourcepub fn diff_trees_with(
&self,
old: Option<ObjectId>,
new: Option<ObjectId>,
options: Options,
limits: Limits,
cancel: &AtomicBool,
) -> Result<Comparison, Error>
pub fn diff_trees_with( &self, old: Option<ObjectId>, new: Option<ObjectId>, options: Options, limits: Limits, cancel: &AtomicBool, ) -> Result<Comparison, Error>
Compares trees and infers rewrites under explicit policy, limits and cancellation. Copies retain their source change; renames consume deleted sources and added targets. The existing rewrite limits supply tree bounds for one structural traversal, reused during inference. Candidate-limit skips retain exact matches and are reported on the result; resource-budget exhaustion still returns an error. Memory grows with admitted tree changes and candidate content; path lookups use ordered maps and sets.
§Errors
Returns storage, malformed-tree, resource, cancellation or inference errors. Nothing is written.
Examples found in repository?
41fn main() -> Result<(), Box<dyn std::error::Error>> {
42 let args = Args::parse();
43 let temp = tempfile::tempdir()?;
44 let mut out = io::BufWriter::new(io::stdout().lock());
45
46 let Some(path) = args.path else {
47 // The fixture compares two trees directly; no commits are needed.
48 let repo = Repository::init(ObjectFormat::Sha1, temp.path().join("r"), InitKind::Bare)?;
49 let loose = repo.loose_objects();
50 let mut editor = TreeEditor::new(&loose);
51 editor.set(b"notes.txt", loose.write_blob(b"one\ntwo\nthree\n")?)?;
52 editor.set(b"old name", loose.write_blob(b"moved\ncontent\n")?)?;
53 let old = editor.write()?;
54
55 editor.set(b"notes.txt", loose.write_blob(b"one\n2\nthree")?)?;
56 editor.remove(b"old name")?;
57 editor.set(b"new name", loose.write_blob(b"moved\ncontent\n")?)?;
58 let new = editor.write()?;
59
60 let git = Repo::new(repo);
61 git.write_patch(&mut out, old, new)?;
62 out.flush()?;
63 return Ok(());
64 };
65
66 if args.unified == 3 && !args.trusted && !args.no_indent_heuristic {
67 Repo::discover(path)?.write_patch(&mut out, &args.old, &args.new)?;
68 out.flush()?;
69 return Ok(());
70 }
71
72 // Explicit options and limits go through the core API the handle composes.
73 let environment = Environment::from_process();
74 let repo = Repository::discover_with_config(path, &ConfigInputs::git_default(&environment)?)?;
75
76 let (packs, rewrite_limits, limits) = if args.trusted {
77 let diff = DiffLimits::default()
78 .with_max_input_bytes(usize::MAX)
79 .with_max_lines(usize::MAX)
80 .with_max_trace(usize::MAX)
81 .with_max_work(usize::MAX);
82
83 // Keep the default rename candidate limit, which matches Git's, but lift resources.
84 let rewrites = rewrites::Limits::default()
85 .with_read(ReadLimits::trusted())
86 .with_max_blob_bytes(usize::MAX)
87 .with_max_spans(usize::MAX)
88 .with_max_work(usize::MAX);
89
90 (
91 PackLimits::trusted(),
92 rewrites,
93 Limits::default()
94 .with_read(ReadLimits::trusted())
95 .with_diff(diff),
96 )
97 } else {
98 (
99 PackLimits::default(),
100 rewrites::Limits::default(),
101 Limits::default(),
102 )
103 };
104
105 let objects = repo.objects_with(packs)?;
106 let resolver = Resolver::new(&repo, &objects)?;
107 let old = resolver.resolve_to(&args.old.parse()?, ObjectKind::Tree)?;
108 let new = resolver.resolve_to(&args.new.parse()?, ObjectKind::Tree)?;
109
110 let cancel = AtomicBool::new(false);
111 let comparison = objects.diff_trees_with(
112 Some(old),
113 Some(new),
114 rewrites::Options::default().with_track_empty(true),
115 rewrite_limits,
116 &cancel,
117 )?;
118
119 // Apply `.gitattributes` and `diff.<driver>` settings as `git diff` does.
120 let mut attributes = repo.diff_attributes(&environment)?;
121 objects.write_patch_with(
122 &mut out,
123 &comparison.entries,
124 Options::default()
125 .with_context_lines(args.unified)
126 .with_indent_heuristic(!args.no_indent_heuristic),
127 Some(&mut attributes),
128 limits,
129 &cancel,
130 )?;
131
132 out.flush()?;
133 Ok(())
134}Sourcepub fn diff_trees_matching(
&self,
old: Option<ObjectId>,
new: Option<ObjectId>,
paths: &Pathspec,
options: Options,
limits: Limits,
cancel: &AtomicBool,
) -> Result<Comparison, Error>
pub fn diff_trees_matching( &self, old: Option<ObjectId>, new: Option<ObjectId>, paths: &Pathspec, options: Options, limits: Limits, cancel: &AtomicBool, ) -> Result<Comparison, Error>
Self::diff_trees_with, limited to paths paths selects.
As in git diff -M <old> <new> -- <pathspec>, the pathspec applies before inference: a
file moved from outside the pathspec to inside it is an addition, not a rename.
§Errors
Returns the errors of Self::diff_trees_with, and an unsupported-pathspec tree
comparison error for attr: magic.