affaan-m/ECC · error

git worktree list --porcelain failed: {stderr}

Error message

git worktree list --porcelain failed: {stderr}

What it means

Thrown by base_checkout_path at ecc2/src/worktree/mod.rs:1581 when `git -C <worktree.path> worktree list --porcelain` exits non-zero. base_checkout_path parses that porcelain output to find which checkout owns refs/heads/<base_branch> (the merge/rebase target). It is called by merge_into_base, rebase_onto_base, and branch_head_oid, so this error can surface from any of them. Failure means git itself refused to enumerate worktrees — usually because the worktree path is no longer registered or git metadata is corrupt.

Source

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

        .arg(repo_root)
        .args(["rev-parse", "--abbrev-ref", "HEAD"])
        .output()
        .context("Failed to get current branch")?;

    Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}

fn base_checkout_path(worktree: &WorktreeInfo) -> Result<PathBuf> {
    let output = Command::new("git")
        .arg("-C")
        .arg(&worktree.path)
        .args(["worktree", "list", "--porcelain"])
        .output()
        .context("Failed to resolve git worktree list")?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        anyhow::bail!("git worktree list --porcelain failed: {stderr}");
    }

    let target_branch = format!("refs/heads/{}", worktree.base_branch);
    let mut current_path: Option<PathBuf> = None;
    let mut current_branch: Option<String> = None;
    let mut fallback: Option<PathBuf> = None;

    for line in String::from_utf8_lossy(&output.stdout).lines() {
        if line.is_empty() {
            if let Some(path) = current_path.take() {
                if fallback.is_none() && path != worktree.path {
                    fallback = Some(path.clone());
                }
                if current_branch.as_deref() == Some(target_branch.as_str())
                    && path != worktree.path
                {
                    return Ok(path);
                }

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Check the worktree is still registered: `git -C <worktree_path> worktree list` — if missing, run `git -C <main_repo> worktree repair <worktree_path>` or recreate the worktree.
  2. If the worktree was deleted intentionally, prune and recreate: `git -C <main_repo> worktree prune && git -C <main_repo> worktree add <path> <branch>`.
  3. Verify the .git back-link inside the worktree is intact: `<worktree_path>/.git` should be a file pointing at `<main_repo>/.git/worktrees/<id>`.
  4. Upgrade git to ≥2.7 if porcelain output is unsupported.

Example fix

// before
let repo_root = base_checkout_path(&worktree)?;

// after: repair the worktree registration if porcelain fails
let repo_root = match base_checkout_path(&worktree) {
    Ok(p) => p,
    Err(e) if e.to_string().contains("worktree list --porcelain") => {
        Command::new("git").arg("-C").arg(&worktree.path)
            .args(["worktree", "repair"]).status()?;
        base_checkout_path(&worktree)?
    }
    Err(e) => return Err(e),
};
Defensive patterns

Strategy: validation

Validate before calling

fn worktree_registered(worktree_path: &Path) -> Result<bool> {
    let out = Command::new("git").arg("-C").arg(worktree_path)
        .args(["rev-parse", "--is-inside-worktree"]).output()?;
    Ok(out.status.success()
        && String::from_utf8_lossy(&out.stdout).trim() == "true")
}
if !worktree_registered(&worktree.path)? {
    Command::new("git").arg("-C").arg(&worktree.path)
        .args(["worktree", "repair"]).status()?;
}
let repo_root = base_checkout_path(&worktree)?;

Type guard

fn gitfile_intact(worktree_path: &Path) -> bool {
    let gitfile = worktree_path.join(".git");
    gitfile.is_file()  // linked worktrees use a .git file, not a directory
}

Try / catch

match base_checkout_path(&worktree) {
    Ok(p) => Ok(p),
    Err(e) if e.to_string().starts_with("git worktree list --porcelain failed") => {
        Command::new("git").arg("-C").arg(&worktree.path)
            .args(["worktree", "repair"]).status()?;
        base_checkout_path(&worktree)
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: The worktree was pruned (git worktree prune ran, or the user manually deleted it) leaving WorktreeInfo.path dangling; the .git file in the worktree points to a back-link that no longer exists; filesystem-level removal of <worktree_path>/.git; running inside a directory that looks like a worktree but is actually a standalone clone; git version too old to support `worktree list --porcelain`.

Common situations: Session restart after a reboot where another process pruned worktrees; dev manually rm -rf'd a worktree dir without `git worktree remove`; storage cleanup tools that scrub hidden .git files; very old git (<2.7) without porcelain format; the worktree lives on an unmounted or permission-restricted volume.

Related errors


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