gitbutlerapp/gitbutler · error

The source of changes cannot have a conflicted 'after' side.

Error message

The source of changes cannot have a conflicted 'after' side.

What it means

ChangesSource::Commit resolves the 'after' side of a diff to the commit's tree. If that commit is conflicted (contains unresolved merge state), the resulting diff is not well-defined, so the accessor refuses rather than diffing against a conflicted tree.

Solutions

  1. Resolve the conflict and re-commit the commit before using it as a change source
  2. Use ChangesSource::Tree { after_id } with a clean tree instead
  3. Filter out conflicted commits when selecting change sources programmatically

Example fix

// before
let source = ChangesSource::Commit { id: conflicted_id };
create_tree_without_diff(repo, source, specs)?;
// after
if !commit.is_conflicted() {
    create_tree_without_diff(repo, source, specs)?;
} else { resolve_first()?; }
Defensive patterns

Strategy: type-guard

Validate before calling

let commit = but_core::Commit::from_id(id.attach(&repo))?;
if commit.is_conflicted() { return Err(ConflictNeedsResolution); }

Type guard

fn clean_commit_source(id: gix::ObjectId, repo: &gix::Repository) -> Option<ChangesSource> {
    let c = but_core::Commit::from_id(id.attach(repo)).ok()?;
    (!c.is_conflicted()).then_some(ChangesSource::Commit { id })
}

Try / catch

match result {
    Err(e) if e.to_string().contains("conflicted 'after' side") => {
        // resolve conflict or switch to a clean tree source
    }
    other => other?,
}

Prevention

When it happens

Trigger: Using a conflicted commit as a ChangesSource::Commit in create_tree_without_diff / uncommit paths, e.g. a commit produced by a merge with conflict markers or tracked conflict state.

Common situations: Workspaces with unresolved conflicts from merges or failed squashes; automation enumerating workspace commits without filtering out conflicted ones.

Understand the failure class

Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.

Related errors


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

Appendix: source

Thrown at crates/but-workspace/src/tree_manipulation/create_tree_without_diff.rs:47

            ChangesSource::Commit { id } => {
                let commit = repository.find_commit(*id)?;
                if let Some(parent_id) = commit.parent_ids().next() {
                    let parent = but_core::Commit::from_id(parent_id)?;
                    Ok(parent.tree_id_or_auto_resolution()?.object()?.into_tree())
                } else {
                    Ok(repository.empty_tree())
                }
            }
            ChangesSource::Tree { before_id, .. } => Ok(repository.find_tree(*before_id)?),
        }
    }

    fn after<'a>(&self, repository: &'a gix::Repository) -> anyhow::Result<gix::Tree<'a>> {
        match self {
            ChangesSource::Commit { id } => {
                let commit = but_core::Commit::from_id(id.attach(repository))?;
                if commit.is_conflicted() {
                    anyhow::bail!("The source of changes cannot have a conflicted 'after' side.");
                }
                Ok(repository.find_tree(commit.tree)?)
            }
            ChangesSource::Tree { after_id, .. } => Ok(repository.find_tree(*after_id)?),
        }
    }
}

/// Discard the given `changes` in either the work tree or an arbitrary commit or tree. If a change could not be matched with an
/// actual worktree change, for instance due to a race, that's not an error, instead it will be returned in the result Vec, along
/// with all hunks that couldn't be matched.
///
/// The returned Vec is typically empty, meaning that all `changes` could be discarded.
///
/// `context_lines` is the amount of context lines we should assume when obtaining hunks of worktree changes to match against
/// the ones we have specified in the hunks contained within `changes`.
///
/// Discarding a change is really more of an 'undo' of a change as it will restore the previous state to the desired extent - Git

View on GitHub (pinned to 58e5313667)