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

  1. On unborn branches use `git rm --cached -- <path>` semantics instead of `reset HEAD` (requires a code path that detects the empty-HEAD case).
  2. Refresh `git_status_entries` first and only call unstage_path for entries where `entry.staged == true`.
  3. Remove a stale `.git/index.lock`.
  4. 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

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


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/05b96d23a270d04f. Report an issue: GitHub.