Skip to main content

Don't mix the strategies

In the same state, feature is rebased in the monorepo, and then the PR, which was never rebased, is pulled. That mixes the two strategies: the next push sends the old commits back to the PR, next to the rebased ones.

Output​

rebase-setup.bash builds this state:

$ build_scenario scenario_feature_branch_behind_default_branch

The monorepo, a with two children, p on main and b on feature, which is H; upstream, k, which is U(H), with two children, m on main and b prime, which is T, R and feature

The rebase copies b onto p as c:

$ git rebase -q main

push refuses: upstream has a commit this branch lacks, b':

$ git splice push vendor/a
!! vendor/a: upstream has commits this branch lacks
!! Failed: vendor/a
[1]

Out of habit, you pull before pushing again. Neither pull is the way here, but only one of them stops you:

  • A plain pull keeps every change, as git pull does, so it goes through. What it does to the PR is what this page is about.
  • pull --rebase would take b''s files and drop m: b' lacks it, and never had it, since main brought it in, not feature. That would lose a change, which git pull --rebase keeps, so it refuses.

The refusal first, since a pull changes the state it needs:

$ git splice pull --rebase vendor/a
ok vendor/a fetched
!! vendor/a: 'feature' lacks the synced commit 48aa825, and its reflog here doesn't show it ever had it -- --rebase would drop that commit's changes
!! Failed: vendor/a
[1]

It states only what it knows. It can't tell this feature from one that upstream rebased before this clone fetched it, e.g. in a fresh clone, so it suggests no command; here, the rebase strategies are the ways out.

The plain pull merges b', the copy of b from before the rebase, into feature as the pull x. No change is lost, and lock.txt stays:

$ git splice pull -m "x: pull the PR" vendor/a
ok vendor/a fetched
ok vendor/a: pulled 1041672
$ cat vendor/a/lock.txt
m: bump dep

But the push then sends both copies of the feature change to the PR:

$ git splice push vendor/a
ok vendor/a: pushed 7680df9 to feature
$ git -C "$UPSTREAM" log --graph --format=%s feature
* x: pull the PR
|\
| * b: feature change
* | b: feature change
* | m: bump dep
|/
* k: seed

The monorepo, a, p on main, c and the pull x on feature, with H at x; upstream, k with two children, m on main and b prime, which is U(H); c prime on top of m; x prime merges c prime and b prime, and is T, R and feature

The PR lists b: feature change twice, and the merge between them. A squash merge of the PR hides that; a rebase merge puts both copies on main.

If it happened​

Before anyone else builds on the PR, drop the pull. --keep refuses if that would lose uncommitted changes:

$ git reset -q --keep HEAD~1

The PR's merge x' now has exactly the folder's files, so to git-splice the splice is up to date, and even push --force has nothing to push:

$ git splice push --force vendor/a
ok vendor/a: nothing to push

It doesn't replace a history whose files are the same: that's how an upstream that rewrote its history, e.g. to remove a secret, stays rewritten. Name the commit to push instead, the rebuild c', with --rebuilt-commit:

$ r="$(git splice rebuild vendor/a)"
$ git splice push --force --rebuilt-commit "$r" vendor/a
ok vendor/a: pushed 125a49b to feature

The PR ends up as rebase via monorepo leaves it:

$ git -C "$UPSTREAM" log --graph --format=%s feature
* b: feature change
* m: bump dep
* k: seed

The monorepo, a, p on main and c on feature, with H at c; upstream, k, m on main, which is U(H), and c prime on top of m, which is T, R and feature