Skip to content

Introduce Diffy v0.1.0: context-menu git diffing for VS Code#1

Merged
MelbourneDeveloper merged 17 commits into
mainfrom
agentpmo
May 27, 2026
Merged

Introduce Diffy v0.1.0: context-menu git diffing for VS Code#1
MelbourneDeveloper merged 17 commits into
mainfrom
agentpmo

Conversation

@MelbourneDeveloper
Copy link
Copy Markdown
Collaborator

TLDR

Initial implementation of the Diffy VS Code extension — pick two git states (commit / branch / tag / index / working copy) from existing SCM/editor/explorer context menus and diff them through `vscode.diff`, with the recent fix that the RefPicker now hides the current branch.

Details

Extension surface (`package.json`, `src/menus.ts`, `src/extension.ts`) — seven commands wired exclusively into menus VS Code already has: `scm/historyItem/context` (compareWith, compareWithWorkingCopy, compareWithPrevious, compareWithBranch, compareWithTag), `scm/resourceState/context` + `editor/title/context` + `explorer/context` (compareFileWithCommit / Branch / Tag), and the Command Palette (compareTwoCommits, compareFileWithCommit, reopenLast). No new views, sidebars, activity-bar icons, or webviews — `src/menus.ts` is the single source of truth and `scripts/sync-menus.mjs` keeps `package.json` byte-identical with a check mode for CI.

Git layer (`src/git/`, zero `vscode` imports) — `GitRunner` shells out to `git` via `child_process.spawn` returning `Result<string, GitError>`. `GitRepo` exposes `log`, `nameStatus`, `numstat`, `show`, `refs`, `revParse`, and `currentBranch` (the latter added in this branch via `git branch --show-current` to drive the new exclude-current-branch behavior). All porcelain parsing is NUL-delimited (`-z` everywhere) in `src/git/parsers.ts` — no regex over structured git output. Multi-root workspaces resolve via longest-prefix match in `src/git/repoMatch.ts`.

UI (`src/ui/`) — thin wrappers around `vscode.window.createQuickPick()` returning `Result<T, Cancelled>`. `FilePicker` stays open after selection (`ignoreFocusOut: true`) so a single comparison can drive many diffs. `RefPicker` accepts an optional `excludeBranchName` that is filtered out by the pure `src/ui/format/refFilter.ts` module — branch-only exclusion, so a tag with the same name is kept.

Compare flow (`src/commands/flow.ts`) — `pickRefAsSha` now looks up `repo.currentBranch()` and threads the name into `pickRef` so the user's own branch is hidden from the picker (a current-branch lookup failure logs and degrades silently rather than blocking the picker). `drillIntoFiles` records the comparison via `MementoStore.setLastComparison` then opens `FilePicker` to fan out per-file `vscode.diff` invocations.

Content provider (`src/providers/DiffyContentProvider.ts`) — implements `TextDocumentContentProvider` for scheme `diffy://`; URI codec and parse errors live in pure `src/ui/uri.ts` (`buildDiffyUri` / `parseDiffyUri`, round-tripping unicode, spaces, `#`, `?`).

Repo plumbing — `Makefile` exposes the 7 standard targets (`build`, `test`, `lint`, `fmt`, `clean`, `ci`, `setup`) plus repo-specific `package`; `test:coverage` is fail-fast, runs unit + E2E, enforces threshold from `coverage-thresholds.json` (default 95). TS in strict mode with `exactOptionalPropertyTypes`; ESLint + Prettier; pino logger writing to file + VS Code OutputChannel via `src/logger.ts`. `shipwright.json` + `schemas/shipwright.schema.json` define release metadata, validated by `scripts/validate-shipwright-manifest.mjs`. CI runs `.github/workflows/ci.yml`, release builds via `.github/workflows/release.yml`.

Test fixtures — `test-fixtures/repo-seed/seed.sh` builds a deterministic three-commit repo with a `feature` branch at commit 2 and a `v0.1.0` tag at commit 2, so the RefPicker E2E tests have a non-current branch to pick after `main` is hidden.

Brand assets — primary icon set at 128/256/512/1024 PNG + WebP under `docs/assets/diffy-icon-primary-*`; the root `icon.png` is byte-identical to the 128 px export (verified by sha1 in this branch's docs change). `docs/design-system.md` Logo Direction table pruned of contact-sheet / brackets / panes / delta / gutter rows that referenced files that no longer ship.

How Do The Automated Tests Prove It Works?

`make ci` is green on this branch: 141 unit + 35 E2E tests pass, coverage at 95.62 % statements / 87.34 % branches / 97.82 % functions / 95.62 % lines — above the 95 threshold in `coverage-thresholds.json`.

Spot-check coverage of the recent behavior added in this branch:

  • `src/test/unit/refFilter.test.ts` (7 tests, 100 % file coverage) — `filterRefs` returns refs untouched with no filters; `type=branch` / `type=tag` keep only the matching ref kind; `excludeBranchName='agentpmo'` drops the branch but keeps the same-named tag (this is the exact scenario the user reported); branch-typed filter + exclude name returns the remaining branches; `excludeBranchName=undefined` is a no-op (detached-HEAD case); excluding a name never removes tags.
  • `src/test/suite/commands.test.ts → compareWithBranch(historyItem={id:sha1}) → branch-filtered RefPicker hides current branch `main` → diff vs feature(=sha2)` — drives the SCM-history command end-to-end via `vscode.commands.executeCommand` and asserts the resulting `vscode.TabInputTextDiff` has `diffy://commit/${sha1}/` on the left, `diffy://commit/${sha2}/` on the right, and the human-readable label ` ↔ — …`.
  • `compareFileWithBranch(uri:a.txt) → branch RefPicker hides current branch → diff opens for feature(=sha2) vs working copy` — same shape against the editor-tab command; left is `diffy://commit/${sha2}/a.txt`, right is the on-disk `file://` URI, label is ` ↔ Working Copy — a.txt`.
  • `compareWith → SideB=pickRef → user picks the v0.1.0 tag → diff against that ref` — confirms the picker order is now (alphabetical, current-branch hidden) `feature`, `v0.1.0`.
  • Parser suite (`src/test/unit/parsers.test.ts`, 41 tests) — every `A/M/D/R/C` status, numstat binary marker (`-`/`-`), empty input, malformed inputs → `Err`, unicode + quoted paths.
  • URI round-trip (`src/test/unit/uri.test.ts`, 23 tests) — spaces, unicode, `#`, `?`, deeply nested paths.
  • Menu manifest (`src/test/unit/menus.test.ts`, 8 tests) — `package.json` `contributes.menus` and `contributes.commands` are byte-equal to what `src/menus.ts` would write; every `COMMAND_ID` has a title; SCM history menu contains all five commit-level commands; the proposed `contribSourceControlHistoryItemMenu` API is declared.

🤖 Generated with Claude Code

MelbourneDeveloper and others added 16 commits May 23, 2026 09:27
The Logo Direction table linked to concept assets that were removed
when the primary icon was finalized (contact-sheet, brackets, panes,
delta, gutter). Replace with the actual shipping set, add the new
1024 px export, and note that the root icon.png matches the 128 px
asset byte-for-byte.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Run prettier across all source and test files
- Remove unnecessary string concat in parsers.test.ts
- Cover expectOk/expectErr negative paths and parseIndexUri bad encoding
to push line coverage from 94.8% to 95.06% (threshold: 95%)
Windows CI was failing prettier --check because git core.autocrlf=true
was converting LF to CRLF on checkout, then prettier (configured with
endOfLine: lf) flagged every file. Pin all text files to LF via
.gitattributes so the working tree matches what prettier expects on
every platform.

Also re-format result.test.ts after the previous round of edits.
spawnSync without shell:true can't resolve npm/npx on Windows (they're
.cmd shims, not real executables). The CI test step was exiting before
any test could run because spawnSync returned status=null. Opt into
shell on win32 only — POSIX platforms keep direct execution.
vscode.Uri.fsPath returns native separators — backslashes on Windows.
Tests that asserted on multi-segment trailing paths (`dir/c.txt`,
`repo-seed/workspace`) failed CI on Windows because the regex used
forward slashes. Replace backslashes with slashes before matching so
the assertions stay readable and work cross-platform.

Single-segment matches like `/a\.txt$/` are unaffected.
… dup

- Replace the bash inline in the _coverage_check Makefile recipe with
  scripts/check-coverage-threshold.mjs. Windows runners invoked the
  recipe under PowerShell, which can't parse $$(...) substitutions
  or backslash line continuations.

- Flip c8.all from true to false in package.json. On Windows, c8's
  filesystem walker registered files via one path string while V8's
  per-process coverage records used a slightly different normalization,
  so every source file appeared twice in the report — once at 0%, once
  with real coverage — halving the global total. Turning off the
  all-files enumeration relies on V8 records alone, which match each
  other consistently.
Even with c8.all=false, the Mocha (Node) and Electron extension-host
runs were emitting V8 coverage URLs in two different shapes on Windows
— backslash-separated paths with mixed drive-letter case from one,
forward-slash paths from the other. c8 keys files by URL, so the same
file was counted twice and per-process coverage couldn't merge.

Walk $NODE_V8_COVERAGE after both test runs and rewrite every `url`
field to a canonical lowercase-drive forward-slash form. POSIX records
are already canonical, so this is a no-op outside Windows.
@MelbourneDeveloper MelbourneDeveloper enabled auto-merge (squash) May 27, 2026 09:04
The branch ruleset required a status check literally named "CI", but
the matrix only ever produced "CI (ubuntu-latest)", "CI (macos-latest)",
and "CI (windows-latest)" — the bare "CI" context was never reported,
so PRs sat in "Expected — Waiting for status to be reported" forever.

Collapse the workflow to one Ubuntu job named "CI". The job emits the
exact context the ruleset wants, all steps run sequentially in a single
runner, and the Windows-specific workarounds added earlier (gitattributes,
shell:win32 spawn, fsPath separator normalization, V8 URL canonicalization)
stay in the codebase so contributors developing on Windows still have a
working local loop.
@MelbourneDeveloper MelbourneDeveloper merged commit e23a4a6 into main May 27, 2026
1 check passed
@MelbourneDeveloper MelbourneDeveloper deleted the agentpmo branch May 27, 2026 10:01
MelbourneDeveloper added a commit that referenced this pull request May 28, 2026
<!-- agent-pmo:74cf183 -->
## TLDR
Initial implementation of the Diffy VS Code extension — pick two git
states (commit / branch / tag / index / working copy) from existing
SCM/editor/explorer context menus and diff them through \`vscode.diff\`,
with the recent fix that the RefPicker now hides the current branch.

## Details

**Extension surface (\`package.json\`, \`src/menus.ts\`,
\`src/extension.ts\`)** — seven commands wired exclusively into menus VS
Code already has: \`scm/historyItem/context\` (compareWith,
compareWithWorkingCopy, compareWithPrevious, compareWithBranch,
compareWithTag), \`scm/resourceState/context\` +
\`editor/title/context\` + \`explorer/context\` (compareFileWithCommit /
Branch / Tag), and the Command Palette (compareTwoCommits,
compareFileWithCommit, reopenLast). No new views, sidebars, activity-bar
icons, or webviews — \`src/menus.ts\` is the single source of truth and
\`scripts/sync-menus.mjs\` keeps \`package.json\` byte-identical with a
check mode for CI.

**Git layer (\`src/git/\`, zero \`vscode\` imports)** — \`GitRunner\`
shells out to \`git\` via \`child_process.spawn\` returning
\`Result<string, GitError>\`. \`GitRepo\` exposes \`log\`,
\`nameStatus\`, \`numstat\`, \`show\`, \`refs\`, \`revParse\`, and
\`currentBranch\` (the latter added in this branch via \`git branch
--show-current\` to drive the new exclude-current-branch behavior). All
porcelain parsing is NUL-delimited (\`-z\` everywhere) in
\`src/git/parsers.ts\` — no regex over structured git output. Multi-root
workspaces resolve via longest-prefix match in \`src/git/repoMatch.ts\`.

**UI (\`src/ui/\`)** — thin wrappers around
\`vscode.window.createQuickPick<T>()\` returning \`Result<T,
Cancelled>\`. \`FilePicker\` stays open after selection
(\`ignoreFocusOut: true\`) so a single comparison can drive many diffs.
\`RefPicker\` accepts an optional \`excludeBranchName\` that is filtered
out by the pure \`src/ui/format/refFilter.ts\` module — branch-only
exclusion, so a tag with the same name is kept.

**Compare flow (\`src/commands/flow.ts\`)** — \`pickRefAsSha\` now looks
up \`repo.currentBranch()\` and threads the name into \`pickRef\` so the
user's own branch is hidden from the picker (a current-branch lookup
failure logs and degrades silently rather than blocking the picker).
\`drillIntoFiles\` records the comparison via
\`MementoStore.setLastComparison\` then opens \`FilePicker\` to fan out
per-file \`vscode.diff\` invocations.

**Content provider (\`src/providers/DiffyContentProvider.ts\`)** —
implements \`TextDocumentContentProvider\` for scheme \`diffy://\`; URI
codec and parse errors live in pure \`src/ui/uri.ts\` (\`buildDiffyUri\`
/ \`parseDiffyUri\`, round-tripping unicode, spaces, \`#\`, \`?\`).

**Repo plumbing** — \`Makefile\` exposes the 7 standard targets
(\`build\`, \`test\`, \`lint\`, \`fmt\`, \`clean\`, \`ci\`, \`setup\`)
plus repo-specific \`package\`; \`test:coverage\` is fail-fast, runs
unit + E2E, enforces threshold from \`coverage-thresholds.json\`
(default 95). TS in strict mode with \`exactOptionalPropertyTypes\`;
ESLint + Prettier; pino logger writing to file + VS Code OutputChannel
via \`src/logger.ts\`. \`shipwright.json\` +
\`schemas/shipwright.schema.json\` define release metadata, validated by
\`scripts/validate-shipwright-manifest.mjs\`. CI runs
\`.github/workflows/ci.yml\`, release builds via
\`.github/workflows/release.yml\`.

**Test fixtures** — \`test-fixtures/repo-seed/seed.sh\` builds a
deterministic three-commit repo with a \`feature\` branch at commit 2
and a \`v0.1.0\` tag at commit 2, so the RefPicker E2E tests have a
non-current branch to pick after \`main\` is hidden.

**Brand assets** — primary icon set at 128/256/512/1024 PNG + WebP under
\`docs/assets/diffy-icon-primary-*\`; the root \`icon.png\` is
byte-identical to the 128 px export (verified by sha1 in this branch's
docs change). \`docs/design-system.md\` Logo Direction table pruned of
contact-sheet / brackets / panes / delta / gutter rows that referenced
files that no longer ship.

## How Do The Automated Tests Prove It Works?

\`make ci\` is green on this branch: 141 unit + 35 E2E tests pass,
coverage at **95.62 % statements / 87.34 % branches / 97.82 % functions
/ 95.62 % lines** — above the 95 threshold in
\`coverage-thresholds.json\`.

Spot-check coverage of the recent behavior added in this branch:

- **\`src/test/unit/refFilter.test.ts\`** (7 tests, 100 % file coverage)
— \`filterRefs\` returns refs untouched with no filters; \`type=branch\`
/ \`type=tag\` keep only the matching ref kind;
\`excludeBranchName='agentpmo'\` drops the branch but keeps the
same-named tag (this is the exact scenario the user reported);
branch-typed filter + exclude name returns the remaining branches;
\`excludeBranchName=undefined\` is a no-op (detached-HEAD case);
excluding a name never removes tags.
- **\`src/test/suite/commands.test.ts →
compareWithBranch(historyItem={id:sha1}) → branch-filtered RefPicker
hides current branch \`main\` → diff vs feature(=sha2)\`** — drives the
SCM-history command end-to-end via \`vscode.commands.executeCommand\`
and asserts the resulting \`vscode.TabInputTextDiff\` has
\`diffy://commit/${sha1}/\` on the left, \`diffy://commit/${sha2}/\` on
the right, and the human-readable label \`<short1> ↔ <short2> — …\`.
- **\`compareFileWithBranch(uri:a.txt) → branch RefPicker hides current
branch → diff opens for feature(=sha2) vs working copy\`** — same shape
against the editor-tab command; left is
\`diffy://commit/${sha2}/a.txt\`, right is the on-disk \`file://\` URI,
label is \`<short2> ↔ Working Copy — a.txt\`.
- **\`compareWith → SideB=pickRef → user picks the v0.1.0 tag → diff
against that ref\`** — confirms the picker order is now (alphabetical,
current-branch hidden) \`feature\`, \`v0.1.0\`.
- Parser suite (\`src/test/unit/parsers.test.ts\`, 41 tests) — every
\`A/M/D/R<NNN>/C<NNN>\` status, numstat binary marker (\`-\`/\`-\`),
empty input, malformed inputs → \`Err\`, unicode + quoted paths.
- URI round-trip (\`src/test/unit/uri.test.ts\`, 23 tests) — spaces,
unicode, \`#\`, \`?\`, deeply nested paths.
- Menu manifest (\`src/test/unit/menus.test.ts\`, 8 tests) —
\`package.json\` \`contributes.menus\` and \`contributes.commands\` are
byte-equal to what \`src/menus.ts\` would write; every \`COMMAND_ID\`
has a title; SCM history menu contains all five commit-level commands;
the proposed \`contribSourceControlHistoryItemMenu\` API is declared.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant