Commit Selectors

Commit selectors describe which commits to load from a workspace. The Workspace::checkout method accepts any type implementing the CommitSelector trait and returns a TribleSet containing data from those commits. It currently supports individual commit handles, lists of handles and a handful of higher level selectors.

Most selectors operate on ranges inspired by Git's two‑dot syntax. a..b selects all commits reachable from b that are not reachable from a. In set terms it computes ancestors(b) - ancestors(a). Omitting the start defaults a to the empty set so ..b yields ancestors(b). Omitting the end defaults b to the current HEAD, making a.. resolve to ancestors(HEAD) - ancestors(a) while .. expands to ancestors(HEAD).

#![allow(unused)]
fn main() {
// Check out the entire history of the current branch
let history = ws.checkout(ancestors(ws.head()))?;
}

While convenient, the range-based design makes it difficult to compose complex queries over the commit graph.

Implemented selectors

CommitSelector is implemented for:

  • CommitHandle – a single commit.
  • Vec<CommitHandle> and &[CommitHandle] – explicit lists of commits.
  • ancestors(commit) – a commit and all of its ancestors.
  • nth_ancestor(commit, n) – follows the first-parent chain n steps.
  • parents(commit) – direct parents of a commit.
  • symmetric_diff(a, b) – commits reachable from either a or b but not both.
  • Standard ranges: a..b, a.., ..b and .. following the two‑dot semantics described above.
  • filter(selector, predicate) – retains commits for which predicate returns true.
  • history_of(entity) – commits touching a specific entity (built on filter).
  • time_range(start, end) – commits whose timestamps intersect the inclusive range.

A future redesign could mirror Git's revision selection semantics. Instead of passing ranges, callers would construct commit sets derived from reachability. Primitive functions like ancestors(<commit>) and descendants(<commit>) would produce sets. Higher level combinators such as union, intersection and difference would then let users express queries like "A minus B" or "ancestors of A intersect B". Each selector would return a CommitSet patch of commit handles for checkout to load.

This approach aligns with Git's mental model and keeps selection logic separate from workspace mutation. It also opens the door for additional operations on commit sets without complicating the core API.

Filtering commits

The filter selector wraps another selector and keeps only the commits for which a user provided closure returns true. The closure receives the commit metadata and its payload, allowing inspection of authors, timestamps or the data itself. Selectors compose, so you can further narrow a range:

#![allow(unused)]
fn main() {
use hifitime::Epoch;
use tribles::repo::{filter, time_range};

let since = Epoch::from_unix_seconds(1_609_459_200.0); // 2020-12-01
let now = Epoch::now().unwrap();
let recent = ws.checkout(filter(time_range(since, now), |_, payload| {
    payload.iter().any(|t| t.e() == &my_entity)
}))?;
}

Higher level helpers can build on this primitive. For example history_of(entity) filters ancestors(HEAD) to commits touching a specific entity:

#![allow(unused)]
fn main() {
let changes = ws.checkout(history_of(my_entity))?;
}

Git Comparison

The table below summarizes Git's revision grammar. Each row links back to the official documentation. Forms that rely on reflogs or reference objects other than commits are listed for completeness but are unlikely to be implemented.

Git SyntaxPlanned EquivalentReferenceStatus
Acommit(A)gitrevisionsImplemented
A^/A^Nnth_parent(A, N)gitrevisionsNot planned
A~Nnth_ancestor(A, N)gitrevisionsImplemented
A^@parents(A)gitrevisionsImplemented
A^!A minus parents(A)gitrevisionsUnimplemented
A^-NA minus nth_parent(A, N)gitrevisionsNot planned
A^0commit(A)gitrevisionsImplemented
A^{}deref_tag(A)gitrevisionsUnimplemented
A^{type}object_of_type(A, type)gitrevisionsNot planned: non-commit object
A^{/text}search_from(A, text)gitrevisionsNot planned: requires commit message search
:/textsearch_repo(text)gitrevisionsNot planned: requires repository search
A:pathblob_at(A, path)gitrevisionsNot planned: selects a blob not a commit
:[N:]pathindex_blob(path, N)gitrevisionsNot planned: selects from the index
A..Brange(A, B)gitrevisionsImplemented
A...Bsymmetric_diff(A, B)gitrevisionsImplemented
^Aexclude(reachable(A))gitrevisionsUnimplemented
A@{upstream}upstream_of(A)gitrevisionsNot planned: depends on remote config
A@{push}push_target_of(A)gitrevisionsNot planned: depends on remote config
A@{N}reflog(A, N)gitrevisionsNot planned: relies on reflog history
A@{<date>}reflog_at(A, date)gitrevisionsNot planned: relies on reflog history
@{N}reflog(HEAD, N)gitrevisionsNot planned: relies on reflog history
@{-N}previous_checkout(N)gitrevisionsNot planned: relies on reflog history

Only a subset of Git's revision grammar will likely be supported. Selectors relying on reflog history, remote configuration, or searching commits and blobs add complexity with little benefit for workspace checkout. They are listed above for completeness but remain unplanned for now.

TimeRange

Commits record when they were made via a timestamp attribute of type NsTAIInterval. When creating a commit this interval defaults to (now, now) but other tools could provide a wider range if the clock precision is uncertain. The TimeRange selector uses this interval to gather commits whose timestamps fall between two Epoch values:

#![allow(unused)]
fn main() {
use hifitime::Epoch;
use tribles::repo::time_range;

let since = Epoch::from_unix_seconds(1_609_459_200.0); // 2020-12-01
let now = Epoch::now().unwrap();
let tribles = ws.checkout(time_range(since, now))?;
}

This walks the history from HEAD and returns only those commits whose timestamp interval intersects the inclusive range.

Internally it uses filter(ancestors(HEAD), ..) to check each commit's timestamp range.