feat(dd): add visual diff family (hug dd s/u/w), productize git-dd#166
Conversation
…sual diff
WHY: The original `dd` gitconfig alias had a critical semantic bug: bare
`git difftool --dir-diff` (no ref) compares worktree-vs-index = UNSTAGED
changes only. Users who ran `hug dd` expecting "all my changes" silently lost
their staged hunks from the visual diff. Additionally, the alias had no help
text, no tests, no completion, no TTY guard, and no no-changes guard — an
undocumented, untested, unfixable API.
This commit productizes `dd` into a first-class `git-dd` script that fixes
the combined-diff correctness bug by design, and adds all the guards the
alias lacked. Design reviewed by /autoplan (CEO + Eng + DX, dual-voice
Codex + Claude, 7/7 unanimous on the critical bug).
WHAT:
- NEW git-config/bin/git-dd — thin gateway script; sources hug-git-difftool;
declares _hug_category + _hug_keywords for hug help discovery; show_help
with full subcommand table, CAPTURING OUTPUT note, SEE ALSO cross-refs.
- NEW git-config/lib/hug-git-difftool — all visual-diff logic:
dd_check_tty : refuses to launch a blocking GUI on non-TTY stdout
dd_preflight : validates diff.tool / difftool.<x>.cmd; friendly error
run_visual_diff : strict-mode-safe wrapper (set +e / set -e around call)
dd_staged : --cached mode (index vs HEAD)
dd_unstaged : bare mode (worktree vs index)
dd_working : HEAD mode (worktree vs HEAD — net all-uncommitted)
dd_ref : arbitrary ref / range forwarding
dd_dispatch : arg parser; subcommand or ref; normalized -- path args
- MOD git-config/lib/hug-git-diff — adds diff_has_working_changes() beside
the existing diff_has_staged_changes / diff_has_unstaged_changes pair.
- NEW tests/unit/test_dd.bats — 20 tests covering all acceptance criteria.
HOW — key design decisions (each deliberate, each had an alternative):
1. dd w = git difftool --dir-diff HEAD (the CRITICAL invariant):
Do NOT simplify this to bare `git difftool --dir-diff` — that is
worktree-vs-index (unstaged only). HEAD gives net worktree-vs-HEAD =
all uncommitted changes. A dir-diff cannot show a split staged+unstaged
view in one tool invocation, so dd w is documented as "net" (same as
`git commit -a` would produce), distinct from text sw's split view.
2. Strict-mode safety via set +e / set -e wrapper in run_visual_diff:
`git difftool` exits 1 when the user closes the tool early or the tool
binary is missing — both normal, expected events. A naked call under
`set -euo pipefail` would abort the entire hug command silently. The
wrapper captures dt_exit, treats 0 and 1 as clean, propagates >=2.
3. TTY guard checks stdout (fd 1), bypassed by HUG_TEST_MODE=true:
The project's stdout/stderr discipline flags visual commands as TTY-only.
HUG_TEST_MODE allows BATS tests to bypass the TTY guard to exercise the
difftool-preflight and no-changes guards independently — without it, every
test would exit at the TTY check before reaching anything interesting.
4. Difftool preflight reads BOTH diff.tool AND difftool.<x>.cmd:
A user may configure an inline cmd entry without a diff.tool pointer.
Checking only diff.tool would give a false "unconfigured" error. Both
checks use `git config --get-regexp` so they honor the same config scopes
(local -> global -> system) as git itself.
5. Fake-difftool harness (option A) + PATH git shim (option B):
Option A (diff.tool fake + difftool.fake.cmd logging to ARGV_LOG) verifies
end-to-end plumbing but can only assert "tool was/was not invoked".
Option B (thin git shim on PATH that logs argv for difftool calls, execs
real git for everything else) asserts EXACT argument vectors — critical for
the regression test that HEAD is passed (not --cached, not nothing).
WHY resolve real git BEFORE prepending the shim: once the shim is on PATH,
`command -v git` returns the shim itself, causing infinite recursion. The
absolute path is baked into the shim at creation time.
6. GIT_CONFIG_GLOBAL=/dev/null in the unconfigured-difftool test:
The developer running the tests likely has kitty (or another difftool)
in their ~/.gitconfig. Without this guard, the "unconfigured" test would
silently pass the preflight and try to launch kitty, getting an unrelated
/dev/tty error instead of the expected failure. This is a recurring trap
in tests that check for absent config — always isolate config scope.
IMPACT:
- hug dd w now includes staged changes (the core correctness fix)
- hug dd s / hug dd u give staged-only / unstaged-only visual views
- Friendly errors replace silent vimdiff fallback and blocking pipeline hijack
- 20 new unit tests; 0 regressions in 142 status/staging tests
- hug help dd and hug help /visual now work; command is discoverable
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
) WHY: After `hug dd` was productized with s/u/w subcommands and guards, the only remaining gap was the interactive file picker. Every text-diff sibling (hug ss / hug su / hug sw) exposes a trailing bare `--` trigger to let users cherry-pick files from a gum-powered list. Without parity, `hug dd --` did nothing useful (it treated `--` as a pathspec separator with no paths, silently launching the full working diff). This left the "I want to visually diff just a handful of files" use-case unaddressed. WHAT: Trailing bare `--` now triggers an interactive file picker for all dd modes (bare, s, u, w). After selection, exactly ONE difftool invocation is launched with all chosen files as pathspecs. New library function `dd_pick_and_diff(mode)` in `hug-git-difftool`: - Builds select_files_with_status flags per mode (mirrors _diff_cmd_setup) - Collects selections via mapfile + select_files_with_status - Cancels cleanly (exit 0 + "No files selected or cancelled") on empty pick - Issues exactly ONE run_visual_diff call with all selected files after `--` Updated `dd_dispatch` in the same library: - Detects trailing bare `--` BEFORE the main parse loop (same pre-loop check as _diff_cmd_setup in hug-git-diff — strip last arg, set picker_mode flag) - After the parse loop, if picker_mode: resolves mode name → dd_pick_and_diff - Ref/range mode with `--` is rejected with a clear error (no sensible scope) Three new BATS tests in `tests/unit/test_dd.bats` (total now 23): - #10a: `hug dd --` → gum-mock selects 2 files → 1 difftool call with HEAD - #10b: `hug dd s --` → gum-mock selects 1 staged file → 1 call with --cached - #11: `hug dd --` → gum-mock cancels (exit 1) → message, 0 difftool calls HOW: Key design decisions and their rationale: BATCHED CALL (never a per-file loop): git difftool --dir-diff is designed to open ONE tool window showing all changed files side-by-side in two temp directories. A per-file loop would produce N sequential blocking windows — the opposite of the dir-diff UX. The invariant: N selected files → exactly 1 tool window. Enforced in dd_pick_and_diff by building the full file list then calling run_visual_diff once with all files as positional args after `--`. select_mode MIRRORS _diff_cmd_setup (hug-git-diff): The text-diff picker uses "--staged", "--unstaged", "--staged --unstaged" for the three modes. We use the same vocabulary with select_files_with_status so both families feel consistent. The mapping is explicit in dd_pick_and_diff: staged → --staged unstaged → --unstaged working → --staged --unstaged (net view = both categories) TRAILING `--` DETECTION IS PRE-LOOP: Checking ${!#} == "--" before the parse loop (and stripping it with set -- "${@:1:$(($#-1))}") is exactly how _diff_cmd_setup does it. This avoids the complexity of detecting it mid-loop after args are consumed. PITFALL: do NOT detect trailing `--` by letting it fall through the loop — the loop already treats `--` as a pathspec separator (consuming remaining args), so by the time you'd check "did we get any pathspecs?", the `--` is gone and the empty-pathspecs heuristic is ambiguous. NO CHANGE TO hug-git-difftool's LOAD CONTRACT: hug-common already sources hug-select-files (which provides select_files_with_status). Since git-dd sources hug-common first, the function is available without adding another `for f in ...` entry to git-dd. Adding it explicitly would be misleading (implies it's not already loaded). PICKER NOT AVAILABLE FOR REF/RANGE MODE: `hug dd HEAD~3 --` is rejected with a clear error. A ref already scopes the diff completely; the picker lists "current working-tree changes" which are unrelated to an arbitrary ref. If we silently ignored `--` for refs, the user would get the full ref diff and wonder why the picker didn't open. IMPACT: - `hug dd --`, `hug dd s --`, `hug dd u --`, `hug dd w --` all open the gum file picker, narrowing the visual diff to the selected subset. - Consistent with the established hug `--` convention from text-diff siblings. - One tool window regardless of selection count — honours dir-diff semantics. - All 20 existing tests still pass; 3 new tests cover the picker paths. - make sanitize clean (shellcheck + shfmt + ruff + mypy all pass). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ejection test, polish docs and tests
WHY: A code review of the hug-git-difftool library identified six concrete
improvement areas: two logic-clarity bugs (I1, I2) and four polish items
(M1–M4). None changed runtime semantics, but together they make the code
teach its intent instead of hiding it.
WHAT: Six targeted fixes across three files:
I1 — Clean up no-changes guards in dd_staged / dd_unstaged
───────────────────────────────────────────────────────────
Both functions used a convoluted double-negation:
if ! git diff --quiet; then true; else info "No changes"; return 0; fi
The dummy `true` branch existed only to satisfy the structure of an
inverted condition — no meaningful work happened there. The inverted form
forced readers to mentally negate "exits 0 when quiet" twice before
understanding the intent.
Replaced with the canonical early-return guard:
if git diff --quiet; then info "No changes."; return 0; fi
run_visual_diff ...
WHY non-inverted form is strictly better:
- `git diff --quiet` exits 0 = no differences = "nothing to show" — the
condition maps directly to "nothing to do", which reads naturally.
- Early-return is the industry-standard guard pattern: the guard is the
entire `if` block; the happy path follows unconditionally.
- The dummy `true` was a code smell that signalled "this branch does
nothing" — a maintenance trap that invites accidental insertion of
logic into the wrong branch.
- dd_working already used this clean pattern (via diff_has_working_changes);
dd_staged and dd_unstaged are now consistent with it.
I2 — Add missing rejection test for `hug dd <ref> --`
──────────────────────────────────────────────────────
The rejection path at dd_dispatch:444-448 (error when picker is combined
with a ref/range argument) had NO test. The code path existed and worked,
but was invisible to the regression suite.
Added:
@test "hug dd <ref> --: interactive picker with a ref/range is rejected"
Uses create_test_repo_with_history (to have HEAD~1) and
configure_fake_difftool (consistent with other Category 1 tests). Does NOT
set up the git shim — the command is expected to fail before difftool is
ever called, so the shim would only add noise.
WHY this gap was worth closing: the rejection logic is a UX invariant ("a
ref scopes itself — there is no current-changes pool to pick from"). If
someone accidentally removed or refactored the guard, no test would catch
the regression.
M1 — Add diff_has_working_changes to hug-git-diff header comment
─────────────────────────────────────────────────────────────────
The Functions: list in the file header was the source of truth for
"what does this module export?" diff_has_working_changes was added as
the third change-detection helper but never added to the list, making
the header incomplete and misleading (readers would think only staged and
unstaged detection existed).
M2 — Document unreachable `return 1` lines after error() calls
──────────────────────────────────────────────────────────────
error() in hug-output calls exit — the return 1 lines on the next line
are therefore dead code. Removing them is the ideal fix but shellcheck
uses them for fallthrough analysis in case/if blocks and may warn without
them (shellcheck is already clean with them present).
Decision: keep the lines, add a comment:
# error() calls exit — unreachable; kept for shellcheck's fallthrough analysis
This documents the intent (would remove if shellcheck allowed it) without
introducing a new shellcheck warning. make sanitize confirmed clean before
and after.
M3 — Remove dead outer `source` and fix misleading comment in test_dd.bats
────────────────────────────────────────────────────────────────────────────
The diff_has_working_changes BATS test body contained:
source "$PROJECT_ROOT/git-config/lib/hug-common" # dead — no effect on subshell
This line ran in the BATS test body process. It had zero effect on the
`run bash -c "source ... && diff_has_working_changes"` subshell below,
because `run` spawns a fresh subprocess that inherits nothing sourced in
the parent. The outer source was not wrong (it doesn't break anything)
but was a teaching trap — future readers might think it was necessary.
Replaced with an explanatory comment that captures the key lesson:
- WHY hug-common (not hug-git-diff directly): hug-common is the umbrella
loader; sourcing it gives diff_has_working_changes all transitive deps.
- NOTE: this source is for the human reader — the subshell sources for
itself. The outer source has no effect on the subshell.
M4 — Strengthen picker multi-select test to assert actual filenames
───────────────────────────────────────────────────────────────────
The existing test for `dd --` (picker selects multiple files) verified:
- Exactly ONE difftool invocation
- HEAD is present in the log
- --no-prompt is present
- -- separator is present
What it did NOT verify: whether the selected FILE NAMES were actually
forwarded as pathspecs after --. The count + flag assertions only prove
the plumbing was called once with the right flags; they say nothing about
the data payload.
Added two assertions after the existing ones:
assert_shim_logged "staged.txt"
assert_shim_logged "README.md"
Why these specific files: the fixture create_repo_with_staged_and_unstaged
creates staged.txt (staged) and README.md (unstaged). In working mode,
select_files_with_status lists staged files first (via --staged flag in the
loop), then unstaged — so index 0 = staged.txt, index 1 = README.md. The
HUG_TEST_GUM_SELECTION_INDICES="0,1" mock selects both by position, and
the assertions close the loop by verifying the filenames arrived in the
shim log as pathspecs.
HOW:
- All edits are surgically minimal: no surrounding logic was restructured.
- The I1 fix changes condition polarity and removes the dummy `true` branch
only; the run_visual_diff call and "$@" forwarding are untouched.
- The I2 test follows the exact pattern of the other Category 1 tests
(arrange with fixture helper, configure_fake_difftool, run git-dd, assert).
- M3 and M4 are additive-only changes to the test file (no test logic removed).
- make sanitize (shellcheck + ruff + mypy) and make test-unit TEST_FILE=test_dd.bats
were both clean after each change.
IMPACT:
- Test count: 23 → 24 (new I2 rejection test is now a permanent regression guard).
- No behaviour change: all guards produce the same runtime outcomes; only
their source-code expression improved.
- Future maintainers reading dd_staged / dd_unstaged will immediately see
the early-return pattern, understand git diff --quiet's exit-code semantics,
and not be confused by a dummy `true` branch.
- The M3 comment is a standalone lesson in BATS subshell isolation — one of
the most common BATS pitfalls (thinking outer `source` affects `run`).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-diff family
WHY: A new visual-diff family (`hug dd`, `hug dd s`, `hug dd u`, `hug dd w`) was
added as GUI/difftool counterparts to the existing text-diff commands. Without
bidirectional help cross-references, users who already know `hug ss` would never
discover that `hug dd s` can open the same staged diff side-by-side in their
configured difftool — and vice versa. SEE ALSO sections are the canonical
discovery mechanism in this codebase; adding a line there is the minimum viable
discoverability change.
WHAT: One line added to the SEE ALSO block of each of four commands:
git-sw → "hug dd w : Visual working-tree diff (difftool)"
git-ss → "hug dd s : Visual staged diff (difftool)"
git-su → "hug dd u : Visual unstaged diff (difftool)"
git-shp → "hug dd : Visual directory diff (difftool)"
git-dd's own SEE ALSO already lists ss/su/sw/shp — these four lines complete
the bidirectional link graph without touching git-dd.
HOW: Each new line mirrors its file's existing column alignment: 4-space indent,
command padded to 13 chars before the colon (e.g. "hug dd w :"). This keeps
the block visually scannable at a glance — a maintainer adding a future entry
should count to 13 chars or align visually against existing lines.
ALIGNMENT NOTE FOR FUTURE CONTRIBUTORS:
All SEE ALSO entries in this codebase follow the pattern:
hug <cmd> : <one-line description>
where the text up to (not including) the colon is 13 characters wide.
"hug dd w" is 8 chars → pad with 5 spaces; "hug ss" is 6 chars → pad with 7.
Breaking this alignment makes the block harder to read — always measure.
IMPACT: Users discover the visual-diff siblings from text-diff help and vice
versa. No behavior changes; pure documentation. The coverage check confirmed
all 15 test-plan cases are already exercised by the existing 24 tests in
tests/unit/test_dd.bats — no new tests needed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 85d848ee70
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # Check 2: any difftool.<name>.cmd entry | ||
| if git config --get-regexp 'difftool\..+\.cmd' > /dev/null 2>&1; then | ||
| return 0 |
There was a problem hiding this comment.
Require a selected diff.tool for custom difftool commands
When a user has only difftool.<name>.cmd configured, this preflight returns success even though run_visual_diff later invokes plain git difftool without --tool=<name>. I checked git difftool -h, which exposes selecting a custom tool via --tool <tool> or diff.tool; with only difftool.fake.cmd set, Git still reports diff.tool is not configured and falls through to its default/prompt path. That breaks the advertised no-fallthrough friendly error for users who define a custom command but have not selected it with diff.tool.
Useful? React with 👍 / 👎.
| # Guards run before argument parsing so they always fire regardless of mode. | ||
| dd_check_tty | ||
| dd_preflight |
There was a problem hiding this comment.
Parse help before TTY and difftool guards
Because these guards run before the argument parser handles -h|--help, hug dd --help can fail instead of showing setup instructions whenever stdout is captured/piped or the user has not configured a difftool yet. This is especially likely for first-time users trying to learn how to configure the new command, and help should not require an interactive terminal, a Git repo, or a working difftool configuration.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Already fixed in this PR (commit cc2eebd, fix(dd): show --help/-h before the TTY and difftool guards): a pre-scan for -h/--help now runs at the very top of dd_dispatch, ahead of both dd_check_tty and dd_preflight. It stops at the first --, so a file literally named --help after a path separator is still treated as a pathspec.
The two help tests were also strengthened to run with HUG_TEST_MODE="" and GIT_CONFIG_GLOBAL=/dev/null, so they exercise the real non-TTY, no-difftool path — previously they masked this exact issue by bypassing both guards.
WHY: `hug dd --help` (and `-h`) failed in any non-interactive context — `hug dd --help | less`, redirected output, or CI — with "requires an interactive terminal (TTY)" and exit 1, instead of printing help. Help is documentation; it must never depend on a TTY or on a difftool being configured. WHAT: dd_dispatch ran dd_check_tty + dd_preflight BEFORE the arg loop that handled -h/--help, so the guards fired first. Added a small -h/--help pre-scan at the very top of dd_dispatch, ahead of both guards. The scan stops at the first `--`, so a file literally named `--help` after `--` is still treated as a pathspec, not a help request. Removed the now-dead -h/--help case from the main parse loop. HOW THE TESTS HID IT (the lesson): The help tests passed only because test_helper exports HUG_TEST_MODE=true (bypassing the TTY guard) AND the dev's global git config happened to have a difftool (satisfying preflight). Neither holds in clean CI. The two help tests now run with HUG_TEST_MODE="" and GIT_CONFIG_GLOBAL=/dev/null, so they exercise the real non-TTY, no-difftool path and would catch this regression. Lesson: a test that sets a bypass env can silently mask the very codepath it appears to cover. IMPACT: `hug dd --help` now works everywhere — pipes, redirects, CI, no difftool. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WHY: The dd feature shipped with help + SEE ALSO but not the user-facing reference the project's "adding a command" workflow requires (README, command-map, docs/commands). And dd's subtlest point — that `dd w` is a NET worktree-vs-HEAD view, not the two-section split that text `sw` shows — needed one clear, authoritative explanation so a user who sees `dd w` "miss" a staged-then-reverted change understands why. WHAT: - docs/commands/status-staging.md: new "Visual diff: hug dd" section — the single source of truth, with the three-trees model (HEAD -> index -> worktree) and a worked cancellation example. - git-config/bin/git-dd: a concise "NET vs SPLIT VIEW" note in help that points to that section. - README.md + command-map.md: one-line dd entries for discoverability. - design doc: a pointer to the user walkthrough (it records the decision; the user-facing explanation lives in status-staging.md). HOW: Single-source-of-truth per docs/DOCS_ORGANIZATION.md: the worked example exists once (status-staging.md); help carries the must-know plus a pointer; README/command-map/design-doc link, never duplicate — so the explanation cannot drift across surfaces. IMPACT: Completes the add-command doc checklist for `dd`, and gives a reader one authoritative place explaining why `dd w` is net-not-split and which command (`dd s` + `dd u`, or `sw`) gives the staged/unstaged split. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WHY: `hug version` / `hug --version` printed a description but no version number, so users and scripts had no way to tell which build of Hug they were running. The repo had a 1.0.0 tag but no VERSION file to anchor the current state. WHAT: - Adds a `VERSION` file at the repo root, set to `1.1.0-dev` (the in-progress next minor; the [Unreleased] CHANGELOG accumulates toward it). - Wires the dispatcher's `--version | version)` branch to print `Version: <contents of $HUG_HOME/VERSION>` after the existing description. - Adds two test_dispatcher cases asserting `hug version` and `hug --version` report the VERSION file's contents. HOW: The version branch reads `$HUG_HOME/VERSION`. HUG_HOME is frozen and exported at the top of bin/hug (before any -C/-S cd), so the lookup is CWD-independent. A missing VERSION file degrades gracefully (the `[[ -f ]]` guard skips the line). The tests invoke `"$PROJECT_ROOT/bin/hug"` explicitly rather than `run hug`: test_helper puts only git-config/bin (the git-* command scripts) on PATH, NOT bin/ (the dispatcher), so `run hug` would exercise the globally-installed dispatcher instead of this checkout's. Asserting against `$(cat "$HUG_HOME/VERSION")` keeps the test in sync with the file, so a future version bump can't silently break it. IMPACT: `hug version` now identifies the build. The VERSION file becomes the single source of truth a release process (or a script) can read. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Unreleased Adds two entries to CHANGELOG.md `## [Unreleased]` → `### Added`, following the repo convention of a per-feature changelog line: - `hug dd` visual side-by-side diff family (dd s/u/w + bare + ref), the difftool counterpart to ss/su/sw, with the net-vs-split caveat for `dd w` and a pointer to docs/commands/status-staging.md. - `hug version` now reporting a version number from the new VERSION file. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WHY: Hug SCM has shipped continuously to main without a single tagged release or version number — users had no way to answer "which Hug do I have?" and there were no release notes to read. The recent `hug dd` visual-diff family (#166) added a VERSION file (1.1.0-dev) and a `hug version` command, which finally made a real release both possible and worth doing. This commit turns the accumulated, never-released work into v1.1.0: the first point on the project's version timeline and the baseline every future bump compares against. WHAT: - VERSION: 1.1.0-dev → 1.1.0 (finalize the in-development marker). - CHANGELOG: promote the unreleased work into a single, dated `## [1.1.0] - 2026-06-04` section and open a fresh empty `## [Unreleased]`. Consolidation only — no entry content was rewritten or dropped: * merged a duplicated `### Added` header (two adjacent blocks → one); * folded a malformed second `## [Unreleased] - 2025-01-13` (the older Makefile-canonical-targets batch) into [1.1.0] as a dated `### Changed` subsection — with no prior release, every "unreleased" pile belongs to this first one; * dropped a now-stale "(currently `1.1.0-dev`)" parenthetical so the CHANGELOG and the VERSION file no longer contradict each other. HOW: Surgical header edits, not a rewrite. The ~110 lines of Makefile migration tables were left physically in place rather than reproduced — the safe way to restructure a CHANGELOG, because reordering big blocks by hand is exactly how content gets silently lost. Every bullet was verified present after the edits (header counts + per-feature grep probes). IMPACT: `hug version` now reports a real number (1.1.0) that maps to a dated, quotable release-notes section and a `v1.1.0` git tag / GitHub release. Semver note: the CHANGELOG lists "breaking changes" (Makefile target renames, worktree-indicator and stdout/stderr output-format shifts), but those are contributor- and script-author-facing, not breaks in the core CLI contract — and as the *first* tag there is no prior release to break compatibility with, so a 1.1.0 minor baseline is correct rather than 2.0.0. Known doc debt deferred out of this release (pre-existing, not introduced here): CLAUDE.md still claims "21 modular library functions" (actual: 35 lib files), and git-config/lib/README.md documents a curated ~9-module subset (both hug-git-diff and the new hug-git-difftool are absent). Cleaning those is a focused docs pass, not release-cut scope. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
✅ Add a visual (difftool) diff family⚠️ The old "combined" visual idea used ⚠️ Bare
hug dd s|u|w(staged/unstaged/working) mirroring the text commandsss/su/sw, plushug dd <ref|range>and barehug dd. Productizes the old bareddgitconfig alias into a real, testedgit-dd.git difftool --dir-diffwith no ref = unstaged-only, which silently hides staged changes;dd wand bareddnow useHEAD.ddsemantics change (was unstaged-only) and your shellbaseddalias must be removed so the script isn't shadowed. Backstory: https://github.com/elifarley/hug-scm/blob/main/docs/plans/2026-06-04-visual-diff-flag-design.mdUsage
hug dd sgit difftool --dir-diff --no-prompt --cachedhug dd ugit difftool --dir-diff --no-prompthug dd w/ baregit difftool --dir-diff --no-prompt HEADdd wis net worktree-vs-HEAD, so it intentionally differs from textsw(which renders two sections): a hunk staged then reverted cancels out in the net view. Documented; the split view stays insw.🔁 Post-merge
ddalias from your shellbase.gitconfig. Git resolves aliases before an externalgit-<cmd>, so the alias would shadow the newgit-ddscript.Follow-ups
shp/lp/lc/lcr(deferred in the design doc).tests/unit/test_dispatcher.bats:334(hug init, no args) isn't worktree-safe: passes onmain, fails only when the suite runs from a linked worktree. Pre-existing, unrelated to this PR.Review story, must-fixes, testing
Reframed via
/autoplan(CEO + Eng + DX, dual-voice Codex + Claude). The original draft added a-d/--visualflag tosw/ss/su; review caught a critical bug (the "combined" view would silently drop staged changes) plus a partial-convention smell (the flag would live on only 3 of ~8 diff-producing commands). Reframed into a productizedddfamily. Implemented test-first via subagent-driven development (implementer → spec review → code-quality review → final whole-feature review).Must-fixes baked in:
diff.tool/difftool.*.cmdis configured (never falls through to git's vimdiff/prompt).--no-prompt(a blocking GUI won't hijack a pipe).set -euo pipefailsafety around difftool exit codes (user-cancel / missing tool doesn't crash the command).--path scoping (no-- --doubling, no flag leaking in as a pathspec).Structure follows the repo convention (thin
git-config/bin/git-dd, logic in newgit-config/lib/hug-git-difftool;diff_has_working_changesadded beside its siblings inhug-git-diff).Tests: 24 BATS cases in
tests/unit/test_dd.batsusing a fake difftool that records argv, so dispatch is verifiable without a GUI (thedd w → HEADcase is the regression guard for the combined-diff bug).test_status_stagingstays 142/142 (no regression from the shared-lib addition).make sanitizeclean.