#15127·gitbutler

Commits already contained in the new base stay applied as "(no changes)", and can still be handed to me as conflicts

Author: duc-cloudthinkerCreated Aug 1, 2026Updated Sep 10, 2026
Labelsbug

Version

App 0.22.0 / but CLI 0.21.1

Operating System

macOS (Apple Silicon)

Distribution Method

dmg (Mac OS - Apple Silicon)

Describe the issue

When one of my branches gets merged upstream and the workspace base moves forward, GitButler keeps that branch applied and rewrites its commits into zero-diff entries labelled (no changes). It never works out that the work is already in the base, so nothing gets dropped and nothing gets unapplied.

I hit this several times a week. Two reasons it bothers me more than a cosmetic label would:

1. Dead lanes pile up and I can't tell them apart from live ones.

After a few merges the workspace is full of lanes whose every commit says (no changes). The label is per commit, so there is no lane-level signal that says "this whole thing already landed". I end up checking against the target branch by hand to work out which lanes I can throw away. I know but clean exists, but nothing in the UI or in but status points me at it, and I only found it by reading but --help.

2. A (no changes) commit can also come back conflicted.

This is the part that actually cost me time today. but status (names redacted):

┊╭┄en [my/fix/some-branch]
┊╭┄┄(upstream: on origin/my/fix/some-branch)
┊●   bea6272505 fix(mod): follow-up fix landed upstream after the merge
┊-
┊◐   zno fix(mod): second commit of my branch (no changes)
┊◐   ltt fix(mod): first commit of my branch (no changes) {conflicted}

ltt has no changes and is conflicted at the same time. but resolve ltt gave me:

Conflicted files remaining:
  ✗ src/mod/utils.py
      conflicts at lines 195-372
  ✗ tests/test_utils.py
      conflicts at lines 1-260

Roughly 430 lines of markers across two files. I read all of it. Every hunk was my own branch on both sides, because the new base already contained that branch (it was merged) plus a follow-up fix on top of it. The correct resolution was "take the base side everywhere", and after but resolve finish the commit was empty, exactly as (no changes) had already said before I started.

So the conflict resolution was pure waste. GitButler knew the commit had no changes, printed that on the same line as {conflicted}, and still asked me to hand-merge it.

One guess at the cause, take it or leave it: my remote is GitLab, not GitHub. If integration detection leans on forge PR state, it may simply have no signal here and fall back to keeping everything applied. Patch-level detection (is this commit's diff already contained in the new base?) would work regardless of forge.

How to reproduce

  1. Create a branch in the workspace, commit to it, push it, open a PR/MR.
  2. Get it merged into the target branch upstream. Mine was a real merge commit, not a squash.
  3. Land one more commit upstream that touches the same lines (in my case a follow-up fix on top of the merge).
  4. Update the workspace base.
  5. but status: the branch is still applied, its commits read (no changes), and at least one of them is also {conflicted}.

Expected behavior

  1. A commit whose patch is already contained in the new base should be dropped during the base update, and a branch whose commits all drop should be unapplied (or at minimum marked integrated at the lane level in but status and in the GUI, with the cleanup action offered right there).
  2. Failing that, a commit with no changes should never be handed to me as a conflict to resolve. If GitButler can already tell the commit contributes nothing, it can resolve it to empty by itself instead of printing 430 lines of markers where both sides are identical.

Additional context

Private repo on a GitLab remote, several applied lanes. Branch names, commit subjects and file paths above are redacted; happy to answer follow-up questions about the shape of the state.