# How Tuicr's VCS Abstraction Layer Unifies Git, Mercurial, and Jujutsu

> Discover how Tuicr's VCS abstraction layer unifies Git, Mercurial, and Jujutsu. Access multiple version control systems through a single interface for seamless terminal UI operations.

- Repository: [Almog Gavra/tuicr](https://github.com/agavra/tuicr)
- Tags: internals
- Published: 2026-08-01

---

**Tuicr implements a Rust trait-based VCS abstraction layer that exposes Git, Mercurial, and Jujutsu through a single `VcsBackend` interface, enabling the terminal UI to perform version control operations without knowing which specific VCS drives the repository.**

Tuicr is a terminal-based code review tool designed to work across heterogeneous version control environments. The project's **VCS abstraction layer** eliminates vendor-specific complexity by defining a common API that normalizes the behavioral differences between Git, Mercurial (hg), and Jujutsu (jj), allowing high-level components like the diff renderer and forge integration to operate on generic data structures.

## Core Trait: `VcsBackend` in [`src/vcs/traits.rs`](https://github.com/agavra/tuicr/blob/main/src/vcs/traits.rs)

The foundation of the abstraction resides in [[`src/vcs/traits.rs`](https://github.com/agavra/tuicr/blob/main/src/vcs/traits.rs)](https://github.com/agavra/tuicr/blob/main/src/vcs/traits.rs#L59), where the `VcsBackend` trait declares all operations required by the application:

```rust
pub trait VcsBackend: Send {
    fn info(&self) -> &VcsInfo;
    fn get_working_tree_diff(&self, highlighter: &SyntaxHighlighter) -> Result<Vec<DiffFile>>;
    fn get_staged_diff(&self, highlighter: &SyntaxHighlighter) -> Result<Vec<DiffFile>>;
    fn get_change_status(&self) -> Result<VcsChangeStatus>;
    fn list_changed_paths(&self, kind: ChangeKind) -> Result<Vec<PathBuf>>;
    fn fetch_context_lines(&self, ...) -> Result<Vec<DiffLine>>;
    fn get_recent_commits(&self, offset: usize, limit: usize) -> Result<Vec<CommitInfo>>;
    fn resolve_revision_range(&self, revisions: &str) -> Result<ResolvedRevisionRange<'static>>;
    fn stage_file(&self, path: &Path) -> Result<()>;
}

```

Each method returns a `Result<T>` using the crate's error types, allowing concrete backends to surface VCS-specific failures as standardized `TuicrError` variants. Default implementations return `UnsupportedOperation`, meaning backends only override methods they actually support. The trait also accepts a `SyntaxHighlighter` parameter, enabling backends to hand off raw diff text for immediate syntax highlighting.

## Auto-Detecting Repositories with `detect_vcs`

Repository detection logic lives in [[`src/vcs/mod.rs`](https://github.com/agavra/tuicr/blob/main/src/vcs/mod.rs)](https://github.com/agavra/tuicr/blob/main/src/vcs/mod.rs#L24) inside the `detect_vcs` function. This dispatcher attempts discovery in a specific order to handle overlapping repository formats:

```rust
pub fn detect_vcs(
    git_backend_preference: GitBackendPreference,
    whitespace_mode: DiffWhitespaceMode,
) -> Result<Box<dyn VcsBackend>> {
    // 1. Try Jujutsu first (jj repos contain .git directories)
    if let Ok(backend) = JjBackend::discover(whitespace_mode) {
        return Ok(Box::new(backend));
    }

    // 2. Fall back to Git (libgit2 or CLI)
    if let Ok(backend) = GitBackend::discover(git_backend_preference, whitespace_mode) {
        return Ok(Box::new(backend));
    }

    // 3. Finally try Mercurial
    if let Ok(backend) = HgBackend::discover(whitespace_mode) {
        return Ok(Box::new(backend));
    }

    Err(TuicrError::NotARepository)
}

```

**Jujutsu** is checked first because `jj` repositories contain `.git` directories; detecting Git first would incorrectly treat them as plain Git repos. **Git** follows as the most common VCS, and **Mercurial** is tried last. The function returns a `Box<dyn VcsBackend>`, providing dynamic dispatch that lets the rest of the codebase interact with any backend through a uniform interface.

## Concrete Backend Implementations

### Git Backend: Libgit2 and CLI Variants

The Git implementation in [[`src/vcs/git/mod.rs`](https://github.com/agavra/tuicr/blob/main/src/vcs/git/mod.rs)](https://github.com/agavra/tuicr/blob/main/src/vcs/git/mod.rs) uses an enum to wrap two distinct strategies:

```rust
pub enum GitBackend {
    Libgit2(Libgit2Backend),
    Cli(GitCliBackend),
}

```

During discovery, `GitBackend::discover` checks for **reftable**, **split-index**, or **sparse-checkout** configurations—features not yet supported by libgit2. If any are present, or if the user specifies `GitBackendPreference::Cli`, the system selects the CLI variant. Each `VcsBackend` method simply forwards to the active implementation:

```rust
fn get_working_tree_diff(&self, highlighter: &SyntaxHighlighter) -> Result<Vec<DiffFile>> {
    match self {
        Self::Libgit2(backend) => backend.get_working_tree_diff(highlighter),
        Self::Cli(backend) => backend.get_working_tree_diff(highlighter),
    }
}

```

The CLI backend ([`src/vcs/git/cli.rs`](https://github.com/agavra/tuicr/blob/main/src/vcs/git/cli.rs)) executes raw `git` commands and parses output through the common diff parser, ensuring consistent behavior regardless of which Git driver is active.

### Mercurial Backend: `HgBackend` CLI Integration

Located in [[`src/vcs/hg/mod.rs`](https://github.com/agavra/tuicr/blob/main/src/vcs/hg/mod.rs)](https://github.com/agavra/tuicr/blob/main/src/vcs/hg/mod.rs), the Mercurial backend operates entirely through the `hg` command-line interface. Discovery runs `hg root` to locate the repository root, while diff generation executes `hg diff` with optional whitespace flags:

```rust
let args = self.diff_args(&["diff"]);
let diff_output = run_hg_command(&self.info.root_path, args.iter().copied())?;
let mut files = diff_parser::parse_unified_diff(&diff_output, DiffFormat::Hg, highlighter)?;

```

All operations—including context line fetching, file line counting, and revision resolution—delegate to `run_hg_command`, standardizing error handling and argument formatting across Mercurial operations.

### Jujutsu Backend: `JjBackend` with Change IDs

The Jujutsu implementation in [[`src/vcs/jj/mod.rs`](https://github.com/agavra/tuicr/blob/main/src/vcs/jj/mod.rs)](https://github.com/agavra/tuicr/blob/main/src/vcs/jj/mod.rs) mirrors the Mercurial structure but adapts to Jujutsu's unique concepts. It utilizes **change IDs** (like `@` and `@-`) rather than traditional commit hashes, and derives branch information from **bookmarks**:

```rust
let head_commit = run_jj_command(&root_path,
    ["log", "-r", "@", "--no-graph", "-T", "change_id.short()"])
    .map(|s| s.trim().to_string())
    .unwrap_or_else(|_| "unknown".to_string());

let branch_name = run_jj_command(&root_path,
    ["log", "-r", "@", "--no-graph", "-T", "bookmarks"])
    .ok()
    .map(|s| s.trim().to_string())
    .filter(|s| !s.is_empty());

```

The backend requests Git-style diff output via `jj diff --git`, allowing reuse of the unified diff parser shared with the Git backend. All file content retrieval uses `jj file show`, maintaining consistency with Jujutsu's content-addressed storage model.

## Unified Diff Processing

Regardless of the underlying VCS, raw diff text flows through [[`src/vcs/diff_parser.rs`](https://github.com/agavra/tuicr/blob/main/src/vcs/diff_parser.rs)](https://github.com/agavra/tuicr/blob/main/src/vcs/diff_parser.rs), which understands two formats:

- **GitStyle**: Used by Git and Jujutsu (`DiffFormat::GitStyle`)
- **Hg**: Used by Mercurial (`DiffFormat::Hg`)

The parser produces `Vec<DiffFile>` structures containing hunks, line metadata, and paths. For files requiring full-file context (such as Vue or Svelte components), `apply_container_full_file_highlight` in [`src/vcs/mod.rs`](https://github.com/agavra/tuicr/blob/main/src/vcs/mod.rs) batches content retrieval via `hg cat` or `jj file show` and reapplies syntax highlighting spans across the entire file.

## Consuming the Backend in the Application

The TUI initializes the backend during startup in [`src/app.rs`](https://github.com/agavra/tuicr/blob/main/src/app.rs):

```rust
let vcs = detect_vcs(git_backend_preference, diff_whitespace_mode)?;
self.vcs = vcs; // Stored as Box<dyn VcsBackend>

```

UI actions request diffs through the generic interface:

```rust
let diff = self.vcs.get_working_tree_diff(&self.syntax_highlighter)?;

```

All scrolling, comment anchoring, and gap expansion logic operates on the resulting `DiffFile` structures, remaining completely agnostic to whether the source is a Git repository, Mercurial checkout, or Jujutsu working copy. The forge integration layer similarly uses `fetch_context_lines` to retrieve remote file content for gap expansion, treating local and remote sources identically through the trait boundary.

## Summary

Tuicr's **VCS abstraction layer** delivers a unified interface for heterogeneous version control systems through these key design decisions:

- **Trait-based polymorphism**: The `VcsBackend` trait in [`src/vcs/traits.rs`](https://github.com/agavra/tuicr/blob/main/src/vcs/traits.rs) defines a common contract that Git, Mercurial, and Jujutsu implementations satisfy.
- **Dynamic dispatch**: `detect_vcs` returns `Box<dyn VcsBackend>`, enabling runtime backend selection without compile-time coupling.
- **Intelligent detection**: The discovery order (Jujutsu → Git → Mercurial) correctly handles repositories where VCS metadata overlaps.
- **Dual Git strategies**: Automatic fallback from libgit2 to CLI when encountering reftable, split-index, or sparse-checkout repositories.
- **Shared parsing pipeline**: A common diff parser normalizes output from `git diff`, `hg diff`, and `jj diff --git` into uniform data structures.
- **Implementation isolation**: UI components, forge integrations, and CLI commands interact solely with the trait, ensuring new VCS support requires only implementing the interface methods.

## Frequently Asked Questions

### How does Tuicr decide which VCS backend to use?

Tuicr's `detect_vcs` function attempts repository discovery in a strict sequence: Jujutsu first (to avoid misidentifying `jj` repos as Git), then Git, then Mercurial. This ordering ensures that repositories containing nested metadata (such as Jujutsu's `.git` directories) are correctly identified by their primary VCS.

### Can Tuicr use both libgit2 and the Git CLI?

Yes. The `GitBackend` enum supports both `Libgit2Backend` and `GitCliBackend` variants. The system automatically selects the CLI implementation when it detects repository features unsupported by libgit2, such as reftable storage, split-index mode, or sparse-checkout configurations. Users can also force CLI mode via configuration.

### What happens if a VCS doesn't support a specific operation?

Methods on the `VcsBackend` trait provide default implementations that return `UnsupportedOperation` errors. Concrete backends only override methods they support. For example, calling `get_staged_diff` on a Mercurial repository returns this error because Mercurial's staging area concept differs fundamentally from Git's index.

### How does the abstraction handle different diff formats?

All backends normalize diff output before parsing. Git and Jujutsu produce standard unified diff format, while Mercurial uses its own variant. The `diff_parser` module in [`src/vcs/diff_parser.rs`](https://github.com/agavra/tuicr/blob/main/src/vcs/diff_parser.rs) accepts a `DiffFormat` parameter to handle these variations, converting all inputs into a common `Vec<DiffFile>` structure consumed by the UI.