Introduce Diffy v0.1.0: context-menu git diffing for VS Code#1
Merged
Conversation
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.
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
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)
---------
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
🤖 Generated with Claude Code