affaan-m/ECC · warning · anyhow::Error

cannot reset a staged hunk while the file also has unstaged

Error message

cannot reset a staged hunk while the file also has unstaged changes; unstage it first

What it means

reset_hunk on a Staged hunk additionally requires that the file has no unstaged changes (`!entry.unstaged`). The reverse-with-`--index` apply would otherwise desynchronize the index from a working tree that has independent unstaged edits, leaving the repo in a confusing half-reset state. The library refuses rather than risk silent corruption.

Source

Thrown at ecc2/src/worktree/mod.rs:441

    )
}

pub fn reset_hunk(
    worktree: &WorktreeInfo,
    entry: &GitStatusEntry,
    hunk: &GitPatchHunk,
) -> Result<()> {
    if entry.untracked {
        anyhow::bail!("cannot reset hunks for untracked files");
    }

    match hunk.section {
        GitPatchSectionKind::Unstaged => {
            git_apply_patch(&worktree.path, &["-R"], &hunk.patch, "reset selected hunk")
        }
        GitPatchSectionKind::Staged => {
            if entry.unstaged {
                anyhow::bail!(
                    "cannot reset a staged hunk while the file also has unstaged changes; unstage it first"
                );
            }
            git_apply_patch(
                &worktree.path,
                &["-R", "--index"],
                &hunk.patch,
                "reset selected staged hunk",
            )
        }
    }
}

pub fn commit_staged(worktree: &WorktreeInfo, message: &str) -> Result<String> {
    let message = message.trim();
    if message.is_empty() {
        anyhow::bail!("commit message cannot be empty");
    }

View on GitHub (pinned to 01e15490f0)

Solutions

  1. First call unstage_path (or unstage_hunk for the staged hunk) to clear the staged portion, then re-derive the entry and reset.
  2. Inspect `entry.staged` and `entry.unstaged` before reset_hunk and surface a 'unstage first' prompt instead of calling through.
  3. After unstaging, refresh `git_status_entries` so the entry's flags reflect the new state.

Example fix

// before
reset_hunk(&worktree, &entry, &hunk)?;

// after
if entry.unstaged && hunk.section == GitPatchSectionKind::Staged {
    unstage_path(&worktree, &entry.path)?;
    let fresh = git_status_entries(&worktree)?.into_iter()
        .find(|e| e.path == entry.path).unwrap();
    // re-derive hunks, then reset
} else {
    reset_hunk(&worktree, &entry, &hunk)?;
}
Defensive patterns

Strategy: validation

Validate before calling

use crate::worktree::{GitPatchSectionKind, GitStatusEntry, unstage_path};

fn can_reset_staged_hunk(entry: &GitStatusEntry) -> bool {
    // Safe only when the file has no parallel unstaged edits.
    entry.staged && !entry.unstaged
}

if hunk.section == GitPatchSectionKind::Staged && !can_reset_staged_hunk(&entry) {
    // must unstage first to avoid index/worktree desync
    unstage_path(&worktree, &entry.path)?;
    // refresh entry + hunks, then retry reset on the now-unstaged hunk
} else {
    reset_hunk(&worktree, &entry, &hunk)?;
}

Type guard

fn staged_only(entry: &GitStatusEntry) -> bool {
    entry.staged && !entry.unstaged
}

Try / catch

match reset_hunk(&worktree, &entry, &hunk) {
    Ok(()) => { /* refresh */ }
    Err(e) if format!("{e}").contains("unstaged changes") => {
        unstage_path(&worktree, &entry.path)?;
        // refresh and retry
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling reset_hunk on a Staged hunk for a file whose GitStatusEntry reports both `staged == true` and `unstaged == true` (the XY porcelain status has both an index and a worktree change). The file was partially staged and then edited further.

Common situations: User staged a hunk, kept editing the same file, then asked to reset the staged hunk; mixed staged/unstaged state for one path that the UI collapses into a single entry.

Related errors


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