affaan-m/ECC · error · anyhow::Error
git reset failed for {path}: {stderr}
Error message
git reset failed for {path}: {stderr} What it means
unstage_path runs `git -C <worktree.path> reset HEAD -- <path>`. On a non-zero exit git's stderr is re-thrown. The most common cause is an unborn branch (no HEAD commit yet) where `HEAD` is not a valid ref to reset against, or a path that was never in the index.
Source
Thrown at ecc2/src/worktree/mod.rs:320
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git add failed for {path}: {stderr}");
}
}
pub fn unstage_path(worktree: &WorktreeInfo, path: &str) -> Result<()> {
let output = Command::new("git")
.arg("-C")
.arg(&worktree.path)
.args(["reset", "HEAD", "--"])
.arg(path)
.output()
.with_context(|| format!("Failed to unstage {}", path))?;
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git reset failed for {path}: {stderr}");
}
}
pub fn reset_path(worktree: &WorktreeInfo, entry: &GitStatusEntry) -> Result<()> {
if entry.untracked {
let target = worktree.path.join(&entry.path);
if !target.exists() {
return Ok(());
}
let metadata = fs::symlink_metadata(&target)
.with_context(|| format!("Failed to inspect untracked path {}", target.display()))?;
if metadata.is_dir() {
fs::remove_dir_all(&target)
.with_context(|| format!("Failed to remove {}", target.display()))?;
} else {
fs::remove_file(&target)
.with_context(|| format!("Failed to remove {}", target.display()))?;
}View on GitHub (pinned to 01e15490f0)
Solutions
- On unborn branches use `git rm --cached -- <path>` semantics instead of `reset HEAD` (requires a code path that detects the empty-HEAD case).
- Refresh `git_status_entries` first and only call unstage_path for entries where `entry.staged == true`.
- Remove a stale `.git/index.lock`.
- Confirm HEAD resolves (`git -C <path> rev-parse --verify HEAD`) before calling.
Example fix
// before
unstage_path(&worktree, &entry.path)?;
// after
if !entry.staged {
return Ok(()); // nothing to unstage
}
unstage_path(&worktree, &entry.path)?; Defensive patterns
Strategy: validation
Validate before calling
fn can_unstage(worktree: &WorktreeInfo, path: &str) -> anyhow::Result<bool> {
let entries = git_status_entries(worktree)?;
let Some(entry) = entries.iter().find(|e| e.path == path) else {
return Ok(false);
};
if !entry.staged {
return Ok(false); // nothing staged to unstage
}
// On unborn branches `reset HEAD` fails; detect that.
let has_head = std::process::Command::new("git")
.arg("-C").arg(&worktree.path)
.args(["rev-parse", "--verify", "HEAD^{commit}"])
.output()?.status.success();
Ok(has_head)
}
if !can_unstage(&worktree, &path)? {
return Ok(()); // no-op
}
unstage_path(&worktree, &path)?; Try / catch
match unstage_path(&worktree, &path) {
Ok(()) => { /* refresh */ }
Err(e) => {
let m = format!("{e:#}");
if m.contains("unknown revision") || m.contains("HEAD") {
// unborn branch: fall back to `git rm --cached`
} else {
return Err(e);
}
}
} Prevention
- Only offer the unstage affordance for entries where `entry.staged == true`.
- Detect unborn branches in the UI and switch to `git rm --cached` semantics.
- Disable the unstage button immediately after it fires to prevent double-dispatch.
When it happens
Trigger: Calling unstage_path on a brand-new repository with zero commits (HEAD does not resolve); unstaging a path that was never staged; unstaging after the index was already reset by another process; `.git/index.lock` contention.
Common situations: Fresh worktree on an empty base branch where the first commit has not been made; UI double-firing an unstage action; race between two UI sessions editing the same worktree's index.
Related errors
- git add failed for {path}: {stderr}
- git restore failed for {}: {stderr}
- git worktree add failed: {stderr}
- git status failed: {stderr}
- cannot reset a staged hunk while the file also has unstaged
AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13).
Data as JSON: /api/errors/05b96d23a270d04f.
Report an issue: GitHub.