gitbutlerapp/gitbutler · error

Cannot compute diff specs for stacks

Error message

Cannot compute diff specs for stacks

What it means

Unsupported-kind guard in the diff-spec builder's push_changes_from_id: a stack CliId was passed, but diff specs can only be computed for concrete change sources (commits, uncommitted areas, hunks), not whole stacks. The caller must expand or narrow the selection to supported ID kinds.

Solutions

  1. Pass a specific commit, hunk, or file id within the stack instead of the stack id
  2. List the stack's commits first (e.g. `but log <stack>`) and choose a concrete id
  3. Use a command designed for stack-level operations if a whole-stack action is intended

Example fix

// before
but <op> my-stack    # stack id -> error
// after
but <op> abc123f     # a commit id inside the stack
Defensive patterns

Strategy: validation

Validate before calling

if matches!(id, CliId::Stack{..}) {
    eprintln!("stack ids cannot build diff specs; pick a commit inside the stack");
    return Ok(());
}

Type guard

fn is_stack_id(id: &CliId) -> bool { matches!(id, CliId::Stack{..}) }

Try / catch

if let Err(e) = cmd() { if e.to_string().contains("for stacks") { /* fall back to per-commit ids */ } else { return Err(e); } }

Prevention

When it happens

Trigger: Passing a `CliId::Stack` id (e.g. a stack name shown in `but status`) to a command whose id handling goes through `push_changes_from_id`.

Common situations: Using the stack name from `but status` output where a commit, hunk, or file id is expected; automation that iterates stacks and feeds stack ids into per-change commands.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18). Data as JSON: /api/errors/facb08ba2d01e0d1. Report an issue: GitHub.

Appendix: source

Thrown at crates/but/src/utils/diff_specs.rs:93

                anyhow::bail!("{}", anonymous_segment_error(&segment.id))
            }
            CliId::Commit {
                commit:
                    CommitId {
                        commit_id,
                        change_id: _,
                    },
                id: _,
            } => self.push_changes_from_commit(*commit_id),
            CliId::Uncommitted { id: _ } => self.push_changes_from_uncommitted_area(),
            // A worktree's uncommitted area is expanded into its files during
            // resolution, so the builder only ever sees hunks that already come from
            // its own repo. The reference holds no changes at all.
            CliId::Worktree { name, .. } | CliId::WorktreeUncommitted { name, .. } => {
                anyhow::bail!("Cannot compute diff specs for worktree `{name}`")
            }
            CliId::Stack { .. } => {
                anyhow::bail!("Cannot compute diff specs for stacks")
            }
            CliId::CommittedHunk(_) => {
                anyhow::bail!("Cannot compute diff specs for committed hunks")
            }
        }
    }

    pub fn push_changes_from_uncommitted(
        &mut self,
        uncommitted: &UncommittedHunkOrFile,
    ) -> anyhow::Result<()> {
        // Specs are built against one checkout's repository, so a hunk from
        // another would silently address the wrong files. Operations that support
        // worktree sources validate the selection and construct the builder via
        // [`Self::for_change_source`]; the rest read the main worktree and refuse
        // worktree hunks here.
        if uncommitted.source != self.source {
            if let Some(name) = uncommitted.source.worktree_name() {

View on GitHub (pinned to 58e5313667)