#16234

`but branch update` on a branch level with its remote fails with "Integration steps cannot be empty" — and cannot refresh a worktree left behind by a plain-git fast-forward

Opendenkhausopened 25d ago

What happens

but branch update <branch> fails with Error: Integration steps cannot be empty (exit 1) whenever the integration plan would be empty, and there is no way to make a second run a clean no-op. Three states reach it:

  1. A branch that is level with its remote. Run any successful but branch update and then run it again — the second run fails.
  2. A branch whose local ref is already at the remote tip while gitbutler/workspace (HEAD) is older — for example after a plain git fetch origin <branch>:<branch> fast-forwards the local ref. The command fails the same way, so nothing repairs the state: files that exist in the branch ref stay missing from the worktree. Making a commit to change the state is not possible either:
    • but commit → Error: Head stack has no ref
    • but commit -b <branch> … → Error: A branch named '<branch>' exists but is not applied The only command we found that repairs the worktree is but apply <branch>, which is not obvious from the failure.
  3. Retrying after a successful update (same as 1).

Reproduce (stock nightly, no token needed)

$ git init /tmp/repro && cd /tmp/repro
$ git commit --allow-empty -m base
$ but setup                      # local remote, target origin/main
$ but branch new A
$ echo hi > f.txt
$ but commit -m 'add f.txt'      # one applied stack, commits everything uncommitted
$ but push A
$ but branch update A
Error: Integration steps cannot be empty            # exit 1

For state 2, advance the remote (any commit that adds a file), then git fetch origin A:A and run but branch update A again — same error, and the file that the branch ref contains is still absent on disk.

Mechanism

The pull-rebase plan is pick <upstream commits> chained with the non-integrated local commits. Both lists are empty exactly when the planner sees no divergence between refs/heads/<branch> and its tracking ref, and integrate_branch_with_steps then bails early (crates/but-workspace/src/branch/integrate_branch_upstream/mod.rs). That bail is a fine low-level invariant, but "nothing to integrate" is a legitimate outcome for the caller: it means the branch already contains everything its upstream has.

Suggested behavior

  • An empty plan reports a no-op (<branch> is already up to date with <upstream>.) instead of failing, and does not claim an update happened.
  • If the branch tip is not part of HEAD in that state, the workspace is refreshed for it, so the reported "up to date" is true of the worktree as well.

We have a fix

Fixed in our fork (denkhaus/gitbutler), happy to open a PR against upstream:

  • empty plan is handled at the but-api seam (apply_branch_integration_with_perm), so CLI, desktop, Lite and N-API all see the no-op instead of the error;
  • the no-op path re-applies the branch when its tip is missing from HEAD (the same machinery but apply uses), records an oplog snapshot because that is a real mutation, and stays read-only for dry runs.

Both are covered by sandbox tests (a level-branch scenario and a plain-git-fast-forward scenario). Commits: 829cde8704 and 2fe14c92d7 on our master. If the response here is positive we will follow up with the PR.

Verified against but 0.5.2216 (nightly build 3273, 2026-10-05) and against current master. Happy to attach the full scripted reproduction (single driver, throwaway repos) if useful.

Activity

1
denkhaus25d ago

Related: #16235 (the merge-commit counterpart — same session, same command, different mechanism).