Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .claude-plugin/skills/revdiff/references/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Then uncomment and edit the values you want to change.
| `--init-themes` | | Write bundled theme files to themes dir and exit | |
| `--init-all-themes` | | Write all gallery themes (bundled + community) to themes dir and exit | |
| `--install-theme` | | Install theme(s) from gallery or local file (repeatable) | |
| `-A`, `--all-files` | | Browse all git-tracked files, not just diffs (CLI-only, not saved in config) | `false` |
| `-A`, `--all-files` | | Browse all tracked files (git and jj only), not just diffs (CLI-only, not saved in config) | `false` |
| `-I`, `--include` | `REVDIFF_INCLUDE` | Include only files matching prefix (may be repeated; comma-separated in env) | |
| `-X`, `--exclude` | `REVDIFF_EXCLUDE` | Exclude files matching prefix (may be repeated; comma-separated in env) | |
| `-F`, `--only` | | Show only matching files (may be repeated, matches by path or suffix) | |
Expand Down
6 changes: 3 additions & 3 deletions .claude-plugin/skills/revdiff/references/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ revdiff main..feature # same as above, git dot-dot syntax
revdiff main...feature # changes since feature diverged from main
revdiff --only=model.go # review only files matching model.go
revdiff --only=ui/model.go --only=README.md # review specific files
revdiff --all-files # browse all git-tracked files in a project
revdiff --all-files # browse all tracked files (git or jj)
revdiff --all-files --exclude vendor # browse all files, excluding vendor directory
revdiff --include src # include only src/ files
revdiff --include src --exclude src/vendor # include src/ but exclude src/vendor/
Expand All @@ -37,9 +37,9 @@ When reviewing a single markdown file in context-only mode (e.g., `revdiff --onl

## All-Files Mode

Use `--all-files` (`-A`) to browse all git-tracked files, not just diffs. Turns revdiff into a general-purpose code annotation tool. All files shown in context-only mode with full annotation and syntax highlighting support.
Use `--all-files` (`-A`) to browse all tracked files, not just diffs. Turns revdiff into a general-purpose code annotation tool. All files shown in context-only mode with full annotation and syntax highlighting support.

- Requires a git repository (uses `git ls-files` for file discovery)
- Requires a git or jj repository (uses `git ls-files` / `jj file list` for file discovery; Mercurial is not supported)
- Mutually exclusive with refs, `--staged`, and `--only`
- Combine with `--include` (`-I`) to narrow to specific paths and `--exclude` (`-X`) to filter out unwanted paths

Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ TUI for reviewing diffs, files, and documents with inline annotations, built wit

## Project Structure
- `app/` - composition root (`package main`), split by concern: `main.go` (entrypoint + `run()`), `config.go` (options/parsing), `stdin.go` (stdin mode), `renderer_setup.go` (VCS wiring), `themes.go` (theme CLI + adapter), `history_save.go` (session save)
- `app/diff/` - VCS interaction (git + hg), unified diff parsing, VCS detection, Mercurial support
- `app/diff/` - VCS interaction (git + hg + jj), unified diff parsing, VCS detection, Mercurial + Jujutsu support
- `app/ui/` - bubbletea TUI package. Single `Model` struct with state grouped into sub-structs (`cfg`, `layout`, `file`, `modes`, `nav`, `search`, `annot`), methods split across files by concern (~500 lines each). Each source file has a matching `_test.go`. See `app/ui/doc.go` for package docs, `docs/ARCHITECTURE.md` for file-by-file breakdown. Does not import `app/theme` or `app/fsutil` — theme operations go through the `ThemeCatalog` interface
- `app/ui/style/` - color/style resolution: hex-to-ANSI, lipgloss styles, SGR tracking, HSL math. Types: `Resolver`, `Renderer`, `SGR`
- `app/ui/sidepane/` - file tree + markdown TOC components with cursor/offset management
Expand Down Expand Up @@ -78,7 +78,7 @@ TUI for reviewing diffs, files, and documents with inline annotations, built wit
- Highlighted lines are pre-computed once per file load, stored parallel to `diffLines`
- `DiffLine.Content` has no `+`/`-` prefix - prefix is re-added at render time
- Tab replacement happens at render time in `renderDiffLine`, not in diff parsing
- `setupVCSRenderer()` (in `renderer_setup.go`) detects VCS via `diff.DetectVCS()` (walks up looking for `.git`/`.hg`); if no VCS is found and `--only` is set, uses `FileReader` for standalone file review. `--stdin` skips VCS lookup entirely, validates non-TTY stdin, reads payload before starting Bubble Tea, and reopens `/dev/tty` for interactive key input. `--all-files` is git-only (not supported in hg repos).
- `setupVCSRenderer()` (in `renderer_setup.go`) detects VCS via `diff.DetectVCS()` (walks up looking for `.jj`, `.git`, `.hg` — in that precedence order; `.jj` wins over `.git` in colocated repos); if no VCS is found and `--only` is set, uses `FileReader` for standalone file review. `--stdin` skips VCS lookup entirely, validates non-TTY stdin, reads payload before starting Bubble Tea, and reopens `/dev/tty` for interactive key input. `--all-files` is supported for git and jj; not supported in hg.
- `--all-files` mode uses `DirectoryReader` (git ls-files) to list all tracked files; `--include` wraps any renderer with `IncludeFilter` for prefix-based inclusion, `--exclude` wraps with `ExcludeFilter` for prefix-based exclusion (include narrows first, then exclude removes). `--include` is mutually exclusive with `--only`. `--all-files` is mutually exclusive with refs, `--staged`, and `--only`. `--stdin` is mutually exclusive with refs, `--staged`, `--only`, `--all-files`, `--include`, and `--exclude`.
- `diff.readReaderAsContext()` is the shared parser for file-backed and stdin-backed context-only views. Preserve its behavior if you change binary detection, line-length handling, or line numbering.
- Overlay popups managed by `overlay.Manager`. `Compose()` uses ANSI-aware compositing via `charmbracelet/x/ansi.Cut`. `HandleKey()` returns `Outcome` — Model switches on `OutcomeKind` for side effects (file jumps, theme apply/persist)
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Built for a specific use case: reviewing code changes, plans, and documents with
- Horizontal scroll overflow indicators: truncated diff lines show `«` / `»` markers at the edges to signal hidden content off-screen
- Line numbers: side-by-side old/new line number gutter for diffs, single column for full-context files, toggle with `L`
- Mercurial support: auto-detects hg repos, translates git-style refs (HEAD, HEAD~N) to Mercurial revsets
- Jujutsu support: auto-detects jj repos (including colocated git+jj), translates git-style refs to jj revsets (`HEAD` → `@-`, `HEAD~N` → `@` plus N+1 dashes); `--all-files` supported
- Blame gutter: shows author name and commit age per line, toggle with `B`
- Annotate any line in the diff (added, removed, or context) plus file-level notes
- Single-file auto-detection: when a diff contains exactly one file, hides the tree pane and gives full terminal width to the diff view
Expand All @@ -37,7 +38,7 @@ Built for a specific use case: reviewing code changes, plans, and documents with

## Requirements

- `git` or `hg` (used to generate diffs; optional when using `--only` or `--stdin`)
- `git`, `hg`, or `jj` (used to generate diffs; optional when using `--only` or `--stdin`)

## Installation

Expand Down
2 changes: 1 addition & 1 deletion app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type options struct {
Blame bool `long:"blame" ini-name:"blame" env:"REVDIFF_BLAME" description:"show blame gutter on startup"`
WordDiff bool `long:"word-diff" ini-name:"word-diff" env:"REVDIFF_WORD_DIFF" description:"highlight intra-line word-level changes in paired add/remove lines"`
ChromaStyle string `long:"chroma-style" ini-name:"chroma-style" env:"REVDIFF_CHROMA_STYLE" default:"catppuccin-macchiato" description:"chroma style for syntax highlighting"`
AllFiles bool `long:"all-files" short:"A" no-ini:"true" description:"browse all tracked files, not just diffs (git only)"`
AllFiles bool `long:"all-files" short:"A" no-ini:"true" description:"browse all tracked files, not just diffs (git and jj only)"`
Stdin bool `long:"stdin" no-ini:"true" description:"review stdin as a scratch buffer"`
StdinName string `long:"stdin-name" no-ini:"true" description:"synthetic file name for stdin content"`
Exclude []string `long:"exclude" short:"X" ini-name:"exclude" env:"REVDIFF_EXCLUDE" env-delim:"," description:"exclude files matching prefix (may be repeated)"`
Expand Down
54 changes: 42 additions & 12 deletions app/diff/directory.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package diff

import (
"bytes"
"context"
"errors"
"fmt"
Expand All @@ -10,38 +11,67 @@ import (
"strings"
)

// DirectoryReader is a Renderer that lists all git-tracked files and reads them as context lines.
// DirectoryReader is a Renderer that lists all tracked files and reads them as context lines.
// used for --all-files mode where every tracked file is browsable, not just changed files.
// the file-listing strategy is pluggable so git, jj, or any future VCS can plug in.
type DirectoryReader struct {
workDir string
workDir string
listSource string // human label used in error messages ("git ls-files", "jj file list")
listFiles func() ([]byte, error)
splitSep byte // separator between entries in the listFiles output (NUL or newline)
}

// NewDirectoryReader creates a DirectoryReader rooted at the given working directory.
// the directory must be inside a git repository.
func NewDirectoryReader(workDir string) *DirectoryReader {
return &DirectoryReader{workDir: workDir}
dr := &DirectoryReader{
workDir: workDir,
listSource: "git ls-files",
splitSep: '\x00',
}
dr.listFiles = func() ([]byte, error) {
// use -z for NUL-separated output to avoid C-quoting of paths with non-ASCII characters
cmd := exec.CommandContext(context.Background(), "git", "ls-files", "-z")
cmd.Dir = workDir
return cmd.Output()
}
return dr
}

// NewJjDirectoryReader creates a DirectoryReader backed by `jj file list`.
// jj file list emits one path per line (no -z / NUL mode), and jj auto-tracks
// every file in the working copy so its output is equivalent to git ls-files.
func NewJjDirectoryReader(workDir string) *DirectoryReader {
dr := &DirectoryReader{
workDir: workDir,
listSource: "jj file list",
splitSep: '\n',
}
dr.listFiles = func() ([]byte, error) {
cmd := exec.CommandContext(context.Background(), "jj", "file", "list")
cmd.Dir = workDir
return cmd.Output()
}
return dr
}

// ChangedFiles returns all git-tracked files as sorted relative paths.
// ChangedFiles returns all tracked files as sorted relative paths.
// ref and staged parameters are ignored since all tracked files are returned.
func (dr *DirectoryReader) ChangedFiles(_ string, _ bool) ([]FileEntry, error) {
// use -z for NUL-separated output to avoid C-quoting of paths with non-ASCII characters
cmd := exec.CommandContext(context.Background(), "git", "ls-files", "-z")
cmd.Dir = dr.workDir
out, err := cmd.Output()
out, err := dr.listFiles()
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
stderr := strings.TrimSpace(string(exitErr.Stderr))
if stderr != "" {
return nil, fmt.Errorf("git ls-files: %s", stderr)
return nil, fmt.Errorf("%s: %s", dr.listSource, stderr)
}
}
return nil, fmt.Errorf("git ls-files: %w", err)
return nil, fmt.Errorf("%s: %w", dr.listSource, err)
}

entries := make([]FileEntry, 0, strings.Count(string(out), "\x00"))
for entry := range strings.SplitSeq(string(out), "\x00") {
entries := make([]FileEntry, 0, bytes.Count(out, []byte{dr.splitSep}))
for entry := range strings.SplitSeq(string(out), string(dr.splitSep)) {
if entry == "" {
continue
}
Expand Down
Loading