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
13 changes: 7 additions & 6 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,13 @@ exact corresponding diffs in a deliberate order.
explanations, plus the version 1 ExplainDocument and its optional attribution metadata.
- `src/authoring/git.ts`: captures staged, unstaged, deleted, renamed, and untracked text
and binary files from an immutable Git base commit, optionally reading the index and
limiting the capture to literal selected paths or away from literal excluded paths.
Positive paths narrow the diff before Git pairs renames; exclusions drop paths from the
result, before any file is read, so a detected rename crossing an exclusion keeps both
paths while only the included side is read. A text side keeps its UTF-8 content; a binary
side keeps only its byte size and SHA-256 content hash. Git itself may read excluded
content while detecting renames.
limiting the capture with literal `--path` values or Git `--pathspec` expressions, and
dropping literal `--exclude` paths. Literal paths disable Git pathspec magic while
pathspec expressions keep it; both narrow the diff before Git pairs renames. Exclusions
drop paths from the result, before any file is read, so a detected rename crossing an
exclusion keeps both paths while only the included side is read. A text side keeps its
UTF-8 content; a binary side keeps only its byte size and SHA-256 content hash. Git itself
may read excluded content while detecting renames.
- `src/format/status.ts`: names the direction of a rename that crosses an exclusion and
labels the omitted side.
- `src/authoring/capture.ts`: derives text change blocks, file-level binary change blocks,
Expand Down
67 changes: 43 additions & 24 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,35 +73,53 @@ publication tokens:
Capture selected working-tree changes:

```bash
diffwalk inspect --staged # staged changes only
diffwalk inspect -- src/a.ts src/b.ts # selected paths
diffwalk inspect --staged -- src/a.ts # both
diffwalk inspect --base main # working tree relative to main
diffwalk inspect --exclude experiments # omit a file or directory
diffwalk inspect --staged # staged changes only
diffwalk inspect --path src/a.ts --path src/b.ts # selected literal paths
diffwalk inspect --staged --path src/a.ts # both
diffwalk inspect --base main # working tree relative to main
diffwalk inspect --exclude experiments # omit a file or directory
diffwalk inspect --exclude notes.md --exclude experiments
diffwalk inspect --exclude src/legacy -- src # exclusion wins inside a selected path
diffwalk inspect --exclude src/legacy --path src # exclusion wins inside a selected path
diffwalk inspect --pathspec ':(glob)src/**/*.ts' # Git glob pathspec
diffwalk inspect --pathspec ':(exclude)pnpm-lock.yaml' # Git exclusion pathspec
diffwalk inspect --pathspec ':(glob)src/**/*.ts' --exclude src/legacy
```

`--path` is repeatable and takes literal file or directory paths relative to the
repository root. Diffwalk disables Git pathspec magic for these values, so `*`, `?`,
`[`, and a leading `:` name literal filenames; `--path 'notes[1].md'` selects that
exact file. A directory selects everything under it on path boundaries.

`--pathspec` is repeatable and hands each expression to Git as a pathspec, so Git's
pathspec semantics apply. `:(glob)src/**/*.ts` selects matching files with a glob, and
`:(exclude)pnpm-lock.yaml` drops a file from the scope. Pass the expression directly
after the option and quote it so the shell does not expand it before Git receives it.
Because the expression reaches Git, an exclusion pathspec is part of the initial Git
scope. `--path` and `--pathspec` are mutually exclusive; choose one selection mode per
capture.

`--exclude` is repeatable and takes literal file or directory paths relative to the
repository root. A directory excludes everything under it on path boundaries, so
`--exclude experiments` also omits `experiments/old.ts` but not `experiments.ts`.
Positive paths after `--` and `--exclude` values combine: a change is captured when it
matches the `--` list (or that list is empty) and matches no `--exclude` value, so an
exclusion always wins. Both selections are literal, so `--exclude 'notes[1].md'` names
that exact file and never a Git pattern. Exclusions apply to working-tree captures
(including untracked files) and to `--staged` captures, and are applied before Diffwalk
reads or validates files, so an unsupported file outside the requested scope cannot block
the capture.
Selection happens in two stages. Diffwalk hands Git the literal `--` paths first, and
Git detects renames among the changes that survive that scope. Diffwalk then drops
repository root. It composes with either selection mode. A directory excludes everything
under it on path boundaries, so `--exclude experiments` also omits `experiments/old.ts`
but not `experiments.ts`. The scope and `--exclude` values combine: a change is captured
when it matches the scope (or the scope is empty) and matches no `--exclude` value, so an
exclusion always wins. `--exclude` values are always literal, so `--exclude
'notes[1].md'` names that exact file and never a Git pattern. Exclusions apply to
working-tree captures (including untracked files) and to `--staged` captures, and are
applied before Diffwalk reads or validates files, so an unsupported file outside the
requested scope cannot block the capture.
Selection happens in two stages. Diffwalk hands Git the `--path` or `--pathspec` scope
first, and Git detects renames among the changes that survive it. Diffwalk then drops
`--exclude` paths from the result. Git may read excluded file contents while it looks
for similarities, so exclusions cannot hide content from Git's rename detection, but
Diffwalk never reads or validates an omitted side. Rename detection is a heuristic, and
a rename that crosses an initial `--` path boundary may lose its relationship and appear
as an ordinary addition or deletion; only `--exclude` keeps detected moves. When a
detected rename crosses an `--exclude` boundary, the review keeps both paths and shows
`Moved to excluded path` or `Moved from excluded path`, omits the excluded side's content
explicitly rather than as an empty file, and drops a rename whose both sides are excluded.
a rename that crosses an initial selection boundary may lose its relationship and appear
as an ordinary addition or deletion; only `--exclude` keeps detected moves, and only when
both sides survived the initial scope. This includes an exclusion pathspec, which Git
applies as part of the initial scope. When a detected rename crosses an `--exclude`
boundary, the review keeps both paths and shows `Moved to excluded path` or `Moved from
excluded path`, omits the excluded side's content explicitly rather than as an empty file,
and drops a rename whose both sides are excluded.
When the selection matches no changes, `inspect` stops with `Nothing to capture` instead
of writing an empty walk. Say which paths you excluded when you share the review, since
the review cannot show changes that were never captured.
Expand All @@ -113,8 +131,9 @@ diffwalk inspect <commit> # commit relative to its first parent
diffwalk inspect --from main --to feature # compare two committed revisions
```

Revision captures ignore local changes and cannot be limited by path or `--exclude`.
Single-commit inspection requires a parent, so it does not support root commits.
Revision captures ignore local changes and cannot be limited by `--path`, `--pathspec`,
or `--exclude`. Single-commit inspection requires a parent, so it does not support root
commits.

`.diffwalk/current` selects the walk used by later commands. Capturing the same source
and contents reuses it; a different capture creates a new walk and preserves previous walks.
Expand Down
50 changes: 30 additions & 20 deletions skills/diffwalk/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,32 @@ explained.
working-tree changes relative to `HEAD`. Use `--base <revision>` when the user
names a different working-tree base.
- Add `--staged` to capture the index instead of the working tree. Limit a
working-tree capture with literal paths after `--`, with repeatable
`--exclude <path>`, or both, for example `diffwalk inspect --staged -- src/a.ts`
or `diffwalk inspect --exclude experiments --exclude notes.md`. Paths are
literal file or directory names relative to the repository root: there is no
Git pathspec magic, `--exclude experiments` omits the whole directory, and an
exclusion always wins over a `--` path. Selection applies only to local captures
(the working tree or the index); commit and range captures reject it. When the
selection matches no changes, `inspect` stops with `Nothing to capture`. Prefer
`--exclude` over listing every included path.
- Selection runs in two stages: Git sees the literal `--` paths and detects renames
first, then Diffwalk drops `--exclude` paths. Git may read excluded content to detect
similarities, but Diffwalk never reads or validates the omitted side. A detected
rename crossing an exclusion keeps both paths and is reported as `Moved to excluded
path` or `Moved from excluded path`, with the excluded side marked `excluded` and no
content; explain that move instead of describing it as an addition or deletion. A
rename with both sides excluded is omitted. A rename crossing an initial `--` path
boundary can lose its relationship because Git filters before it detects renames.
working-tree capture with repeatable `--path <path>` for literal paths,
repeatable `--pathspec <expression>` for Git pathspecs, repeatable
`--exclude <path>`, or a selection option plus `--exclude`. For example,
`diffwalk inspect --staged --path src/a.ts`,
`diffwalk inspect --pathspec ':(glob)src/**/*.ts'`, or
`diffwalk inspect --pathspec ':(glob)src/**/*.ts' --exclude src/legacy`.
`--path` and `--pathspec` are mutually exclusive. `--path` values are literal
file or directory names relative to the repository root with Git pathspec magic
disabled, so a leading `:` or `*`, `?`, `[` stays literal and
`--path 'notes[1].md'` names that exact file. `--pathspec` values go to Git
directly, so quote them and use Git pathspec syntax: `:(glob)src/**/*.ts` for a
glob and `:(exclude)pnpm-lock.yaml` to drop a file. Selection applies only to
local captures (the working tree or the index); commit and range captures reject
it. When the selection matches no changes, `inspect` stops with
`Nothing to capture`. Prefer `--exclude` over listing every included path.
- Selection runs in two stages: Git sees the `--path` or `--pathspec` scope and
detects renames first, then Diffwalk drops literal `--exclude` paths. An exclusion
pathspec is part of the initial Git scope, not the later `--exclude` step. Git may
read excluded content to detect similarities, but Diffwalk never reads or validates
the omitted side. A detected rename crossing an `--exclude` keeps both paths and is
reported as `Moved to excluded path` or `Moved from excluded path`, with the
excluded side marked `excluded` and no content; explain that move instead of
describing it as an addition or deletion. A rename with both sides excluded is
omitted. A rename crossing an initial `--path` or `--pathspec` boundary, or whose
destination an exclusion pathspec drops, can lose its relationship because Git
filters before it detects renames.
- Run `diffwalk inspect <commit>` for one commit relative to its first parent. A
root commit has no first parent, so use an explicit range instead.
- Run `diffwalk inspect --from <revision> --to <revision>` for a committed range.
Expand Down Expand Up @@ -183,8 +192,9 @@ hide ownership or order; otherwise let Diffwalk's exact diff carry the code.
Diffwalk reports it. Symbolic links and non-file Git paths are still rejected at
capture time; do not bypass that boundary.
If the reported files are outside the requested review scope, rerun
`inspect -- <path>...` to include only the relevant files, or rerun
`inspect --exclude <path>...` to drop the irrelevant ones, and continue. Use
`inspect --path <path>...` or `inspect --pathspec <expression>...` to include only the
relevant files, or rerun `inspect --exclude <path>...` to drop the irrelevant ones, and
continue. Use
`--staged` only when the requested review scope is the index. Always state which
paths you excluded or omitted: an omission is intentional, and reviewers cannot see
changes that were never captured. Do not change files, staging, or Git ignore rules
Expand All @@ -210,7 +220,7 @@ hide ownership or order; otherwise let Diffwalk's exact diff carry the code.
## Commands

```bash
diffwalk inspect [revision] [--staged] [--base <revision>] [--from <revision> --to <revision>] [--exclude <path>...] [--output <capture-path>] [--explanations <yaml-path>] [-- <path>...]
diffwalk inspect [revision] [--staged] [--base <revision>] [--from <revision> --to <revision>] [--path <path>...] [--pathspec <expression>...] [--exclude <path>...] [--output <capture-path>] [--explanations <yaml-path>]
diffwalk walks
diffwalk use <walk-id>
diffwalk delete <walk-id>
Expand Down
28 changes: 21 additions & 7 deletions src/authoring/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface GitCapture {
export interface CaptureGitChangesOptions {
staged?: boolean
paths?: string[]
pathspecs?: string[]
exclude?: string[]
}

Expand All @@ -46,18 +47,31 @@ export async function captureGitChanges(
cwd = process.cwd(),
options: CaptureGitChangesOptions = {},
): Promise<GitCapture> {
const { staged = false, paths = [], exclude = [] } = options
const { staged = false, paths = [], pathspecs = [], exclude = [] } = options
const root = (await gitText(['rev-parse', '--show-toplevel'], cwd)).trim()
const baseCommit = (
await gitText(['rev-parse', '--verify', '--end-of-options', `${base}^{commit}`], root)
).trim()
// Positive paths narrow the diff before Git pairs renames. Exclusions stay out of the
// pathspec: Git must still see both sides of a rename so the move can be preserved, and
// the excluded side is dropped after detection, before Diffwalk reads any content.
const pathEnvironment = paths.length > 0 ? { GIT_LITERAL_PATHSPECS: '1' } : {}
// Scope selection reaches Git before it pairs renames. Literal `--path` values disable Git
// pathspec magic so special characters stay literal; `--pathspec` expressions keep Git's
// pathspec semantics, including exclusion magic. Every global pathspec mode is pinned so an
// inherited `GIT_*PATHSPECS` value cannot change the selection. Diffwalk's literal
// `--exclude` stays out of the pathspec: Git must still see both sides of a rename so the
// move can be preserved, and the excluded side is dropped after detection, before Diffwalk
// reads any content.
const scope = pathspecs.length > 0 ? pathspecs : paths
const pathEnvironment =
scope.length === 0
? {}
: {
GIT_LITERAL_PATHSPECS: pathspecs.length > 0 ? '0' : '1',
GIT_GLOB_PATHSPECS: '0',
GIT_NOGLOB_PATHSPECS: '0',
GIT_ICASE_PATHSPECS: '0',
}
const changes = parseGitChanges(
await gitBytes(
['diff', '--raw', '-z', '--find-renames', ...(staged ? ['--cached'] : []), baseCommit, '--', ...paths],
['diff', '--raw', '-z', '--find-renames', ...(staged ? ['--cached'] : []), baseCommit, '--', ...scope],
root,
pathEnvironment,
),
Expand All @@ -76,7 +90,7 @@ export async function captureGitChanges(
if (!staged) {
const untracked = splitNulls(
await gitBytes(
['ls-files', '--others', '--exclude-standard', '--exclude=.diffwalk/', '-z', '--', ...paths],
['ls-files', '--others', '--exclude-standard', '--exclude=.diffwalk/', '-z', '--', ...scope],
root,
pathEnvironment,
),
Expand Down
Loading