Skip to content

[patch] Unstage with reset so it works before the first commit - #143

Merged
matt-edmondson merged 1 commit into
mainfrom
fix/unstage-on-unborn-branch
Sep 29, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
fix/unstage-on-unborn-branch

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Unstage(path) now works in a repository that has no commits yet. Before this, it failed every time there: ExecuteAsync threw GitCommandException ("fatal: could not resolve HEAD") and TryExecuteAsync returned Success = false.

Cause

On git 2.23 and later, GitRestoreBuilder built git restore --staged -- <path>. With no --source, restore --staged reads the index back from HEAD, and on an unborn branch HEAD doesn't resolve.

Fix

GitRestoreBuilder now always builds git reset -q -- <path>, the option the triage recommended:

  • For a committed path, it copies the entry from HEAD into the index, which is what restore --staged did.
  • On an unborn branch, it removes the entry, which leaves the file untracked.
  • It behaves the same on every supported git. That makes the version probe (ProbeVersionAsync, the ExecuteAsync/TryExecuteAsync overrides) and the reset HEAD fallback unnecessary, so they're removed.
  • -q stops reset printing its "Unstaged changes after reset" list.
  • The bare -- separator stays, so a dash-leading path is still safe; checked against git 2.43 with a file named -dash.

The public surface is unchanged: IGitRestoreBuilder and GitRepository.Unstage. The builder class keeps its name because it's internal.

Tests

New integration tests against real git (they ran here, none skipped):

  • UnstageBeforeTheFirstCommitLeavesTheFileUntrackedAsync is the issue's acceptance case: Add().All() then Unstage on an unborn branch. Neither call throws, and the file comes back untracked.
  • UnstageAfterACommitRestoresTheIndexAndKeepsTheWorkingTreeAsync guards the ordinary case. The staged change leaves the index and stays in the working tree.

Unit tests:

  • GitRestoreBuilderTests now pins the reset -q -- <path> vector, the bare --, and a single invocation with no version probe.
  • Two tests are removed: FallsBackToResetOnAGitOlderThanRestoreAsync and TreatsExactlyTwoTwentyThreeAsSupportedAsync. The branch they tested no longer exists.

Verification:

  • Verified by reversal: the unborn-branch test fails on main with the issue's exact error, git exited with code 128: fatal: could not resolve HEAD, and passes with the fix.
  • The full suite passes: 727/727.

docs/superpowers/specs/2026-09-24-patch-verbs-design.md still describes the restore-plus-fallback design. I left it alone because it's a dated design record, not current documentation.

Fixes #122

🤖 Generated with Claude Code

https://claude.ai/code/session_01PNmrp6FP3tBU47owLsiovc


Generated by Claude Code

Unstage() built `git restore --staged -- <path>` on git 2.23 and later.
With no --source, restore reads the index back from HEAD, so in a
repository with no commits yet it exited 128 with "could not resolve
HEAD", and ExecuteAsync threw. That is exactly when a user is most likely
to have staged too much.

GitRestoreBuilder now always builds `git reset -q -- <path>`. It unstages
a committed path and leaves a never-committed one untracked, on every
supported git, so the version probe and the reset HEAD fallback go away.

Fixes #122

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PNmrp6FP3tBU47owLsiovc
@sonarqubecloud

Copy link
Copy Markdown

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.

Unstage() always fails before the first commit: git restore --staged dies with "could not resolve HEAD" on an unborn branch

2 participants