Skip to main content

Crate vcs_watch

Crate vcs_watch 

Source
Expand description

vcs-watch — filesystem-watch a git/jj repository and emit typed state-change events.

A RepoWatcher watches a repository’s .git/.jj state directory (and, optionally, the working tree), debounces the burst of writes a VCS operation makes, re-queries the repo state through vcs-core’s batched snapshot, and diffs it against the previous state to yield typed RepoEvents. Each settled change arrives as a RepoChange carrying both the new RepoSnapshot (to render a prompt/status line) and the deltas (to react). It’s the foundation for prompts, status bars, TUIs, and repo daemons.

Re-query-and-diff — rather than interpreting raw filesystem events — is what makes it robust: git’s ref temp-file renames, index.lock churn, and reflog noise all just coalesce into one “re-check the settled state” instead of being (mis)read as events. Noise that doesn’t move observable state emits nothing, and every emission carries the true current state, so a stray event can’t desync the consumer.

§The surface

  • RepoWatcher — a live watch over one repository. Start it with RepoWatcher::watch (defaults) or the Builder; drop it to stop the OS watch and the background task.
  • Builder (RepoWatcher::builder) — set the watch scope and timing, then build: working_tree to also watch the tree recursively, debounce (the quiet window), max_wait (the re-query ceiling under a continuous stream), requery_timeout (the per-re-query deadline). The DEFAULT_REQUERY_TIMEOUT et al. name the defaults.
  • RepoEvent — one typed delta, derived by diffing two snapshots: HeadMoved, BranchSwitched, BranchCreated / BranchDeleted, WorkingCopyChanged, and the upstream/ahead-behind/operation/conflict variants (#[non_exhaustive]).
  • RepoChange — a settled change: the fresh RepoSnapshot (render a status line off it) plus the non-empty events vec (react to it).
  • Consumption — pull changes with recv (Option<RepoChange>; None once the watch backend dies, is dropped, or otherwise ends), or, under the stream feature, poll the watcher as a futures_core::Stream. Both pull from the same channel and advance current, the last-pulled snapshot. A timed-out or transiently failed re-query is retried automatically with bounded exponential backoff — even with no new filesystem event — so a miss on the last signal isn’t stuck until the next one; a permanent OS-watch backend failure (e.g. the watched .git/.jj dir was removed) closes this channel, so recv/the stream observe it directly instead of requiring separate stats polling.
  • WatcherStats (stats) — lock-free health counters (re-queries run, changes emitted, skips, retries, recoveries, terminal failures, and the last skip’s WatcherErrorKind). Climbing skipped with flat changes means a wedged repo — poll it from a health check rather than inferring health from event silence.
  • Error — a setup/build failure: a Vcs baseline re-query error, an Io filesystem error, or a Notify filesystem-watch backend failure. The watch backend is a private dependency, so its failures are the opaque WatchError — classify them (is_path_not_found / is_watch_limit / io_error) and source-chain them through vcs-watch alone, with no direct dependency on the third-party watch crate to keep version-matched.

§Recipes

Watch with the defaults and react to each settled change:

use vcs_core::Repo;
use vcs_watch::RepoWatcher;
let repo = Repo::discover(".")?;
let mut watcher = RepoWatcher::watch(repo).await?;
while let Some(change) = watcher.recv().await {
    for event in &change.events {
        println!("{event:?}");
    }
    // `change.snapshot` is the fresh full state — render a status line off it.
}

Under the stream feature the watcher is a futures_core::Stream, so it drops into stream combinators and tokio::select! directly (needs futures/tokio-stream’s StreamExt in scope):

use futures::StreamExt;
use vcs_core::Repo;
use vcs_watch::RepoWatcher;
let repo = Repo::discover(".")?;
let mut watcher = RepoWatcher::watch(repo).await?;
while let Some(change) = watcher.next().await {
    println!("{} event(s)", change.events.len());
}

Runtime: unlike the rest of the toolkit (which hides tokio behind processkit), vcs-watch uses tokio at runtime — the watch task and the debounce timer run on the caller’s tokio runtime, so build/await it from within one.

§Testing

The debounce → ceiling → re-query pipeline is a free function over injected seams, so it is exercised hermetically on a paused clock (no real filesystem or sleeps); a consumer’s own watch code tests the same way it tests any vcs-core consumer — build the Repo over a fake runner (processkit’s ScriptedRunner) so the re-query returns canned state. See vcs-testkit’s guide.

§In-depth guide

Beyond this page, this crate ships a full how-to guide — rendered on docs.rs from docs/. See the guide module.

Re-exports§

pub use processkit;

Modules§

guide
vcs-watch — repo-event stream

Structs§

Builder
Builder for a RepoWatcher — set the watch scope and debounce timing, then build.
RepoChange
A batch of changes observed in one settled re-query: the new full RepoSnapshot (ready to render a prompt/status line) plus the typed RepoEvents that produced it. A RepoWatcher only yields a RepoChange when at least one event fired.
RepoSnapshot
A one-shot snapshot of the common repository state — branch, upstream tracking, ahead/behind, dirtiness, and operation state — gathered in a small fixed number of process spawns instead of a call per field. The data a prompt, status line, or TUI refresh needs. See Repo::snapshot.
RepoWatcher
A live watch over a repository, yielding RepoChanges as the repo’s state changes. Dropping it stops the filesystem watch and the background task.
WatchError
An opaque filesystem-watch backend failure — the watcher couldn’t start, or couldn’t register/deregister a watched path.
WatcherStats
A cheap point-in-time copy of the watcher’s health counters — see RepoWatcher::stats. Lets a long-running consumer notice a watcher that is silently skipping re-queries (e.g. a permanently wedged repository) instead of inferring health from event silence.

Enums§

Error
An error from setting up or running a RepoWatcher.
OperationState
Whether the working copy is mid-operation, unified across the backends’ different models: git exposes an in-progress merge, rebase, am, cherry-pick, revert, or bisect as on-disk state (MERGE_HEAD / a rebase-* dir / CHERRY_PICK_HEAD / REVERT_HEAD / BISECT_LOG), while jj has no multi-step operations — it records a conflict directly on the working-copy change.
RepoEvent
One typed change to a repository’s observable state, derived by diffing two consecutive RepoSnapshots (plus the branch set).
WatcherErrorKind
What the last skipped re-query failed on (see WatcherStats::last_error).

Constants§

DEFAULT_REQUERY_TIMEOUT
Default deadline on a single re-query (snapshot + branch list): a wedged command (e.g. a held index.lock with no client timeout configured) is killed and skipped instead of stalling the watch loop forever.

Type Aliases§

Result
Result specialised to the watcher Error.