Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ clap = { version = "4.6.1", features = [
clap-verbosity-flag = "3.0.4"
clap_complete = { version = "4.6.5", features = ["unstable-dynamic"] }
clap_mangen = "0.3.0"
crossterm = "0.29.0"
dialoguer = { version = "0.12.0", features = ["fuzzy-select"] }
env_logger = "0.11.10"
git-workon-lib = { version = "0.13.2", path = "./git-workon-lib" }
Expand All @@ -49,10 +50,17 @@ ratatui = "0.30"
rusqlite = { version = "0.40", features = ["bundled"] }
serde_json = "1.0"
serial_test = "3"
similar = "3.1.1"
thiserror = "2.0.18"
tree-sitter = "0.26"
tree-sitter-highlight = "0.26"
tree-sitter-javascript = "0.25.0"
tree-sitter-json = "0.24.8"
tree-sitter-lua = "0.5.0"
tree-sitter-md = { version = "0.5.3", features = ["parser"] }
tree-sitter-rust = "0.24"
tree-sitter-toml-ng = "0.7.0"
tree-sitter-typescript = "0.23.2"
unicode-width = "0.2.2"

# The profile that 'dist' will build with
Expand Down
2 changes: 1 addition & 1 deletion docs/rfc/workon-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ evidence, not to the conclusion.
- **M0 — workspace plumbing.** New member crate `git-workon-review` (lib+bin, clap, error model matching workspace: thiserror+miette). Toolchain bump (ratatui/tree-sitter won't meet 1.68.2; resolved: workspace-wide `rust-version = 1.88` — no crate had ever inherited the old value, so there was no lib MSRV to preserve). Lib hygiene (drop unused dialoguer/env_logger). CI: tree-sitter C builds. Release posture per [ADR-033](../adr/033-review-crate-workspace-placement.md): `publish = false` keeps the crate out of release-plz and cargo-dist entirely; release-plz wiring is deliberately deferred to the M3 flip — do NOT add a release-plz.toml entry in M0. Acceptance: `cargo build --workspace` green, empty `git-workon-review` binary runs and prints help.
- **M1 — fixture extensions + lib stack capabilities (test-first).** Fixture: sqlite metadata mode (also finally exercises the lib's primary read path), index-state builders. Lib: `parentBranchRevision` read (both formats) + needs-restack; git-inference StackModel; changeset assembly API (`Vec<Changeset> {branch, base_ref, head_ref, title, current, needs_restack}` + uncommitted layer). Acceptance: existing lib tests green + new capabilities spec'd against fixtures in both metadata formats.
- **M2 — trap corpus port.** Diff parser + patch synthesis in the review lib, the six trap items as tests, git2-vs-CLI verdict rendered (and the write-path decision recorded here). Acceptance: round-trip corpus green against real repos. — DONE (2026-07-06): corpus green on both backends; verdict recorded above.
- **M3 — renderer + uncommitted source.** Port spike modules; wire changeset → parsed diff → SBS/inline render; file nav; the uncommitted source end-to-end. Acceptance: dogfood-able read-only review of a dirty worktree.
- **M3 — renderer + uncommitted source.** Port spike modules; wire changeset → parsed diff → SBS/inline render; file nav; the uncommitted source end-to-end. Acceptance: dogfood-able read-only review of a dirty worktree. — DONE (2026-07-06): combined-zoom read-only review with SBS + inline layouts, collapsed context gaps, word-diff emphasis, tree-sitter highlighting (spike's 8 grammars; syntect deferred), file/hunk nav; dogfooded against a dirty worktree. Port note: the spike's `compose_segments` had a latent first-match span-precedence bug that silently dropped word-level emphasis — fixed here (reverse-order lookup), pinned by a three-way bg test in `render.rs`.
- **M4 — staging verbs + zoom states.** Queue, hunk/file/line ops (visual-style line selection), the `_gate` zoom matrix, attributed rendering. Acceptance: prototype staging parity, index watcher stable under external writes.
- **M5 — stack + ref sources, outline.** Changeset navigation, outline panel, needs-restack markers, focus semantics (open at current branch; uncommitted adjacent-after, focused when present).
- **M6 — comments + integration.** Comment store + `mcp` subcommand; `$NVIM`/`$EDITOR` edit jump; git-workon external dispatch + completion delegation. Acceptance: full agent loop — review, comment, agent addresses via MCP, re-review.
Expand Down
14 changes: 11 additions & 3 deletions git-workon-review/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,22 @@ vendored = ["git-workon-lib/vendored", "git2/vendored-libgit2", "git2/vendored-o

[dependencies]
clap.workspace = true
crossterm.workspace = true
git-workon-lib.workspace = true
git2.workspace = true
miette.workspace = true
ratatui.workspace = true
similar.workspace = true
thiserror.workspace = true
tree-sitter.workspace = true
tree-sitter-highlight.workspace = true
tree-sitter-javascript.workspace = true
tree-sitter-json.workspace = true
tree-sitter-lua.workspace = true
tree-sitter-md.workspace = true
tree-sitter-rust.workspace = true
tree-sitter-toml-ng.workspace = true
tree-sitter-typescript.workspace = true

[package.metadata.dist]
# Redundant with publish = false today; load-bearing at the M3 flip so
Expand All @@ -48,6 +59,3 @@ dist = false
assert_cmd.workspace = true
git-workon-fixture.workspace = true
predicates.workspace = true
tree-sitter.workspace = true
tree-sitter-highlight.workspace = true
tree-sitter-rust.workspace = true
55 changes: 43 additions & 12 deletions git-workon-review/src/acquire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,75 @@
//! (a committed rev pair, or "uncommitted"); this module only knows *how* to turn that into
//! git2 diffs and then a [`DiffModel`].

use git2::{DiffOptions, Oid, Repository};
use git2::{DiffFindOptions, DiffOptions, Oid, Repository};
use workon::{Changeset, ChangesetSource};

use crate::error::DiffError;
use crate::model::DiffModel;

/// The two working-tree diffs a review session needs: the index against `HEAD` (staged), and
/// the working tree against the index (unstaged, including untracked content).
/// The working-tree diffs a review session needs: the index against `HEAD` (staged), the
/// working tree against the index (unstaged, including untracked content), and the fused
/// `HEAD` ↔ worktree view (combined) the M3 renderer reviews by default.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorktreeDiffs {
pub staged: DiffModel,
pub unstaged: DiffModel,
/// `HEAD`'s tree diffed straight against the working tree (index consulted only for
/// untracked/ignore filtering), fusing staged and unstaged hunks on the same file into one
/// diff — the combined-zoom view the M3 renderer reviews (locked design decision #2).
pub combined: DiffModel,
}

/// Diff `HEAD`'s tree against the index (staged) and the index against the working tree
/// (unstaged), for a [`ChangesetSource::Uncommitted`] changeset.
/// Diff `HEAD`'s tree against the index (staged), the index against the working tree
/// (unstaged), and `HEAD`'s tree against the working tree directly (combined), for a
/// [`ChangesetSource::Uncommitted`] changeset.
///
/// The unstaged side sets `include_untracked`/`recurse_untracked_dirs`/
/// The unstaged and combined sides both set `include_untracked`/`recurse_untracked_dirs`/
/// `show_untracked_content` so untracked files carry real content in the model (git2 gives
/// `Delta::Untracked` natively here — no `/dev/null` header synthesis needed).
/// `Delta::Untracked` natively here — no `/dev/null` header synthesis needed). `find_similar`
/// runs on all three diffs before materialization so worktree renames (e.g. an untracked file
/// that replaces a tracked one under a new name) surface as [`crate::model::FileStatus::Renamed`]
/// rather than a delete+add pair — the read side already handles that status (corpus-proven).
///
/// The two untracked-including diffs (unstaged, combined) pass explicit
/// [`DiffFindOptions::for_untracked`] — plain `find_similar(None)`'s default flags (just
/// `GIT_DIFF_FIND_RENAMES`) do NOT pair an untracked file with a workdir deletion; libgit2
/// requires `for_untracked` opted in separately for that side of the match. The staged diff
/// never sees untracked deltas, so `None` (matching [`diff_committed`]'s convention) is enough
/// there.
pub fn diff_uncommitted(repo: &Repository) -> Result<WorktreeDiffs, DiffError> {
let head_tree = repo.head()?.peel_to_tree()?;

let mut staged_opts = DiffOptions::new();
staged_opts.context_lines(3);
let staged_diff = repo.diff_tree_to_index(Some(&head_tree), None, Some(&mut staged_opts))?;
let mut staged_diff =
repo.diff_tree_to_index(Some(&head_tree), None, Some(&mut staged_opts))?;
staged_diff.find_similar(None)?;
let staged = DiffModel::from_git2(&staged_diff)?;

let mut unstaged_opts = DiffOptions::new();
unstaged_opts
let mut worktree_opts = DiffOptions::new();
worktree_opts
.include_untracked(true)
.recurse_untracked_dirs(true)
.show_untracked_content(true)
.context_lines(3);
let unstaged_diff = repo.diff_index_to_workdir(None, Some(&mut unstaged_opts))?;
let mut untracked_find = DiffFindOptions::new();
untracked_find.renames(true).for_untracked(true);

let mut unstaged_diff = repo.diff_index_to_workdir(None, Some(&mut worktree_opts))?;
unstaged_diff.find_similar(Some(&mut untracked_find))?;
let unstaged = DiffModel::from_git2(&unstaged_diff)?;

Ok(WorktreeDiffs { staged, unstaged })
let mut combined_diff =
repo.diff_tree_to_workdir_with_index(Some(&head_tree), Some(&mut worktree_opts))?;
combined_diff.find_similar(Some(&mut untracked_find))?;
let combined = DiffModel::from_git2(&combined_diff)?;

Ok(WorktreeDiffs {
staged,
unstaged,
combined,
})
}

/// Diff `base`'s tree against `head`'s tree, for a [`ChangesetSource::Committed`] changeset —
Expand Down
Loading