Skip to content

Latest commit

 

History

History
174 lines (131 loc) · 5.84 KB

File metadata and controls

174 lines (131 loc) · 5.84 KB

Snapshots

tsgit exposes a unified snapshot+join surface for querying the four git "sources" — tree, index, working tree, and stash — through a single pipeline API. This page introduces the mental model and links to the worked examples.

Mental model

A snapshot is a description, not data. Calling repo.snapshot.head() returns immediately and performs zero I/O. Reading happens only when you iterate the result:

const tree = repo.snapshot.head();
for await (const entry of tree.entries()) {
  console.log(entry.path, await entry.read());
}

Three properties hold for every snapshot kind (design §8.0, ADR-149):

  1. Lazy — no syscall until .entries() is consumed.
  2. Atomic per handle — the data captured on the first iteration is replayed for subsequent iterations on the same handle. Concurrent writes never disturb an in-flight for await loop.
  3. Fresh per call to the factoryrepo.snapshot.index() always re-reads the cache state at call time; an external write between two factory calls produces two different snapshots.

The factory

repo.snapshot returns a SnapshotFactory:

Method Returns Notes
head() TreeSnapshot the HEAD commit's tree
commit(oid) TreeSnapshot a specific commit's tree
tree(oid) TreeSnapshot a tree by oid (no peeling)
index() IndexSnapshot the current .git/index
workdir(opts) WorkdirSnapshot the working tree
mergeHead() Promise<TreeSnapshot | null> compound state
cherryPickHead() Promise<TreeSnapshot | null> compound state
revertHead() Promise<TreeSnapshot | null> compound state
fetchHead() Promise<TreeSnapshot | null> compound state
stashEntry(i) Promise<StashSnapshot | null> a stash entry (index + workdir + untracked)

The compound-state factories return Promise because they have to check whether the underlying ref file exists. They still don't parse the tree until iterated.

Why sources live on the factory, not repo.*

Every source is reached through repo.snapshot.*, never as a top-level repo.index / repo.workdir / repo.tree(rev) accessor. This is deliberate: snapshots are a power-tool (lazy handles with isolation semantics), kept off the everyday porcelain surface that returns plain structured data. Folding them onto repo.* would either duplicate the factory (repo.index()repo.snapshot.index()), drop capability (a bare repo.index getter cannot carry the options bag the method takes), or collide with a command namespace (repo.stash is the stash command). The factory keeps the surface cohesive — one place for all four sources.

To snapshot a tree at an arbitrary revision, resolve it first:

const tree = repo.snapshot.commit(await repo.revParse('v1.0'));

Worked example — status

Compare head, index, and workdir in a single pass:

import { join, count } from 'tsgit';

const rows = join({
  head: repo.snapshot.head(),
  index: repo.snapshot.index(),
  workdir: repo.snapshot.workdir(),
});

for await (const row of rows) {
  // row.head / row.index / row.workdir are optional — undefined when that
  // source has no entry at row.path
}

Worked example — diff

import { innerJoin } from 'tsgit';

const changed = innerJoin({
  before: repo.snapshot.commit(parentOid),
  after: repo.snapshot.head(),
});

for await (const row of changed) {
  // row.before.oid !== row.after.oid (when content differs)
}

Worked example — untracked

import { join } from 'tsgit';

const rows = join({
  index: repo.snapshot.index(),
  workdir: repo.snapshot.workdir(),
});

for await (const row of rows) {
  if (row.index === undefined && row.workdir !== undefined) {
    // untracked file
  }
}

Working with null-returning factories

Compound-state factories return Promise<TreeSnapshot | null>. You must null-check or wrap with requireSnapshot before passing to join:

import { requireSnapshot } from 'tsgit';

const theirs = await requireSnapshot(repo.snapshot.mergeHead(), 'no merge in progress');
const rows = join({ ours: repo.snapshot.head(), theirs });

Passing a Promise<… | null> directly into join is a type error by design — the row's slot type cannot be inferred through a promise wrapper.

Operators

The pipeline composes via async-iterable operators (see tsgit/application/primitives/snapshot-operators):

  • hashWorkdir({ concurrency }) — pre-warms WorkdirEntry.hash() calls.
  • loadBlob(slot, { maxInflightBytes }) — pre-loads blob bytes with a bounded byte budget (default 64 MiB).
  • verifyWorkdir({ onRace: 'throw' | 'skip' | 'emit' }) — re-lstats workdir entries on iteration for race detection.
  • groupByDir() — groups consecutive rows by parent directory.
  • count, toArray, first — terminal operators returning Promise<T>.

Order invariant

All snapshot+join iterables yield rows in canonical git path order. Operators consuming a row stream MUST preserve order; a downstream assertOrdered will throw ORDER_INVARIANT_VIOLATION if a stage reorders rows. This guarantees compositions like groupByDir see contiguous rows per directory without needing buffering.

Cancellation

Every iteration honours three signals composed by an AND-of-aborts:

  1. ctx.signal from openRepository,
  2. SnapshotOptions.signal per snapshot,
  3. JoinOptions.signal from the join call.

The first abort wins; downstream iterators surface the abort as OPERATION_ABORTED.

Further reading