gitbutlerapp/gitbutler · error · anyhow::Error

commit '{spec}' is not part of the editable divergence

Error message

commit '{spec}' is not part of the editable divergence

What it means

Raised by `resolve_commit` inside `parse_integration_steps_script` when a commit spec prefix-matches zero commits in `allowed_commits` — the set built from the divergence's editable commits (local-only commits chained with `upstream_only` commits). Anything outside that divergence (merge-base ancestors, commits on other branches, typos) is rejected so the integration cannot pull in foreign history.

Source

Thrown at crates/but-workspace/src/branch/integrate_branch_upstream/parsing.rs:159

        .filter(|commit| {
            matches!(
                commit.target_relation,
                IntegrationDivergenceTargetRelation::NotIntegrated
            )
        })
        .map(|commit| commit.id)
        .chain(divergence.upstream_only.iter().map(|commit| commit.id))
        .collect()
}

fn resolve_commit(spec: &str, allowed_commits: &HashSet<gix::ObjectId>) -> Result<gix::ObjectId> {
    let matches = allowed_commits
        .iter()
        .copied()
        .filter(|commit_id| commit_id.to_string().starts_with(spec))
        .collect::<Vec<_>>();
    match matches.as_slice() {
        [] => bail!("commit '{spec}' is not part of the editable divergence"),
        [commit_id] => Ok(*commit_id),
        _ => bail!("commit prefix '{spec}' is ambiguous"),
    }
}

fn split_message_clause(line: &str) -> Result<(&str, Option<&str>)> {
    let Some((command_part, message_part)) = line.split_once('|') else {
        return Ok((line, None));
    };
    Ok((command_part.trim_end(), Some(message_part.trim_start())))
}

fn parse_message_clause(clause: &str) -> Result<String> {
    let value = clause
        .strip_prefix("message=")
        .context("expected `message=` after `|`")?;
    parse_quoted_string(value)
}

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Re-open/re-render the integration proposal so the script's commit list matches the current divergence, then re-apply edits.
  2. Copy commit prefixes only from the script itself or the divergence display shown alongside it.
  3. If a specific commit must be included, first bring it into the branch (so it appears in the local-only divergence), then restart integration.
Defensive patterns

Strategy: validation

Validate before calling

// Verify every spec in the script resolves against the current divergence before applying:
let allowed: HashSet<gix::ObjectId> = /* local_only + upstream_only commit ids from divergence */;
for (i, line) in script.lines().enumerate() {
    for spec in line.split_whitespace().skip(1) {
        let spec = spec.trim_start_matches('#');
        if !allowed.iter().any(|id| id.to_string().starts_with(spec)) {
            return Err(anyhow::anyhow!("line {}: commit {spec} not in divergence (stale script?)", i + 1));
        }
    }
}

Try / catch

if let Err(err) = parse_integration_steps_script(&script, &divergence) {
    if err.to_string().contains("not part of the editable divergence") {
        // prompt: divergence changed, re-render the proposal and re-apply edits
    }
}

Prevention

When it happens

Trigger: A script line references a SHA that is not one of the local-only or upstream-only divergence commits — e.g. copied from `git log` of the main branch, a full SHA from another stack, or a typo/hand-typed prefix. Also stale scripts: the divergence changed (branch updated, commits added) between rendering the script and parsing it back.

Common situations: User pastes a commit from elsewhere in the repo into the integration editor; the editor session is stale after the branch moved (script rendered before a fetch/update, parsed after); truncated SHAs that don't correspond to any allowed commit.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/72b2d380a7eef885. Report an issue: GitHub.