Skip to content

feat(pull): rebase local splice changes onto the upstream with --rebase - #208

Draft
roschaefer wants to merge 10 commits into
feat/track-upstream-branchfrom
feat/pull-rebase
Draft

roschaefer wants to merge 10 commits into
feat/track-upstream-branchfrom
feat/pull-rebase

Conversation

@roschaefer

@roschaefer roschaefer commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #207. Taking a fork's rebased patch branch into a monorepo whose protected main only takes squash-merged pull requests, as when dogfooding the scrut splice.

  • git splice pull --rebase / merge --rebase (feat: rebase local splice changes onto upstream for linear pushes #55): like git pull --rebase, takes the upstream branch in on top of the newest commit whose folder has exactly the synced commit's files, and rebases the commits since onto that pull. The next push sends the local changes linearly on top of the upstream branch, also after the upstream rebased it (the old patches stay dropped). It rewrites the monorepo branch, so it refuses when a remote-tracking branch has any of those commits. The refusal after an upstream rewrite now names it.
  • --branch <name>: names the upstream branch for one pull or merge, without writing .splice, so a pull request's branch can pull the patch branch main follows.
  • A squash merge that mixes a local change into a pull of a rewritten upstream branch can't be rebuilt without publishing the dropped commits again, and nothing can rewrite it on a protected main. status and push refuse and name the way out by hand: commit the folder's changes on top of the upstream branch, push them there, pull. That pull records the new commit with the folder's files, so the rebuild starts there; no new rebuild rule. A pull request that only pulls squash-merges fine.

Scenario 06-rewritten-history/04-patch-branch-rebased now has two patches (one taken upstream, one rebased), drawings that name the monorepo's and upstream's branches (check-drawing reads a branch name in a box on a monorepo commit as a monorepo branch), pull --rebase in redo-on-top, and the squash merge with its way out in 02-squash-merged.

Defaults taken for #55's open questions, to confirm:

  • Published commits: refuse when a remote-tracking branch has any commit --rebase would rewrite.
  • Monorepo merges in the rewritten range: flattened, as git rebase does by default.
  • Several splices: one after another; a conflict stops the rest.
  • Flag, not command: pull --rebase and merge --rebase; --bump and --rebase don't go together.

Refs #55, #169

With a protected main, a pull of the fork's rebased patch branch reaches
main as a squash merge, together with a local change. The squash commit
records the rebased branch as the synced commit, but its folder isn't
that branch's, so the rebuild joins it with the history before it, still
built on the dropped patch, and the next push publishes that patch
again. A pull request's own branch can't pull the patch branch either:
it syncs with the upstream branch named like it.

The scenario shows both, to decide on them before git splice pull
--rebase (#55) builds on the result.
@coderabbitai

coderabbitai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3ebf23ae-cf71-405c-95c0-39efc1ce35e0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Only the end state was drawn, which didn't show where the dropped patch
comes back: after the pull and on the pull request's branch, R is still
linear on top of the rebased branch; only the squash merge joins the old
history in. The steps now run in the document, each followed by a
checked drawing, and one unchecked drawing shows the proposed rebuild.
A drawing that shows a pull request's branch next to main is hard to
follow without the branches' names, but a branch name in a box only
named an upstream branch. In a box on a monorepo commit, it now names
the monorepo's branch, even where the upstream has one named alike.
With a single patch that upstream takes, the rebased patch branch ends
up on upstream main, so the drawings showed T = patches = main. A real
fork's patch branch (the scrut fork's, say) keeps patches upstream
hasn't taken: one patch drops out, the other gets a new commit on top
of main, and main and patches differ. The drawings name the monorepo's
and the upstream's branches, so the pull request steps are easier to
follow, and the squash-merged page proposes --branch for pulling the
patch branch on a pull request's branch.
…anch

A pull request's branch syncs with the upstream branch named like it.
When a protected main follows a fork's patch branch, the upstream has
no branch for the pull request, so its pull finds nothing, and taking a
rebased patch branch had to happen on a local main first. --branch
names the branch for that one command, without writing it to .splice
(default-branch belongs to the upstream, not to one monorepo branch),
and only for the splices the command selected: the ones below keep
syncing with their own.
…ritten upstream

A squash merge of a pull request that pulled a rebased patch branch and
changed the folder records the new synced commit, but its folder isn't
that commit's. The rebuild joined it with the history before it, still
built on the commits the rebase dropped, so the next push published
them again. On a protected main, nothing can rewrite that commit, and
which upstream commits should carry its changes is the user's call. So
push, status and the other commands that show the rebuild refuse, and
say the way out: commit the folder's changes on top of the upstream branch
by hand, push them there, and pull. That pull records the branch with
the folder's files, so the rebuild starts there again. merge and pull
therefore still rebuild past the refusal: they only compare with it.
With several upstreams, a pull from the original after the fork moves
the synced commit from the fork's patch to the original's release, and
the patch is no ancestor of the release. That's no rewrite: the fork
still has the patch, so joining both sides is right, as the
several-upstreams scenario shows. Like merge, the rebuild only takes it
for a rewrite when no fetched upstream branch has the earlier commit.
A pull that merges a diverged upstream makes the next push send a merge
commit, which upstreams with linear history don't take (#55). After the
upstream rebased its branch, e.g. a fork's patch branch onto a release,
the pull even refused: the local changes, rebuilt on the dropped
commits, would bring them back. Redoing the changes by hand on top took
five commands.

pull --rebase and merge --rebase do it like git pull --rebase: they
take the upstream branch in on top of the newest commit whose folder
has exactly the synced commit's files, which merges without a conflict
from there, and rebase the commits since onto that pull. The rebuild
then starts at the pulled commit and sends the local changes linearly
on top. It rewrites the monorepo branch, so it refuses when a
remote-tracking branch has any of those commits; a pull request's branch
is where it belongs, together with --branch. The refusal after an
upstream rewrite now names it. A rebase in progress, like a cherry-pick,
stops the next pull, before the detached HEAD would hide why.
…, fix by hand

The squash-merged page now takes the release on the pull request's
branch with --branch, shows status and push refusing the squash merge
that mixed a local change into it, and the way out by hand: commit the
folder's changes on top of the patch branch upstream, push, and pull.
The rebuild then starts at that commit, without a new rebuild rule.
The box above q covered the vendor/a/ label of the lane it sits in.
@roschaefer

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-19T13:19:06.527443Z 5d4ec7a Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5d4ec7af1d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/rebuild.sh
if [[ -z "${REBUILD_JOINS_REWRITTEN:-}" && -n "$previous" ]] &&
git cat-file -e "$previous^{commit}" 2>/dev/null &&
! git merge-base --is-ancestor "$previous" "$synced" &&
[[ -z "$(git for-each-ref --count=1 --contains "$previous" refs/splices/)" ]]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Scope the rewrite exception to this splice's upstreams

The refs/splices/ query searches fetched branches for every splice, so a different splice whose upstream happens to retain previous suppresses this safety check. In that case rebuild_splice joins the rewritten-away history and succeeds, allowing the next push to republish precisely the commits this guard is intended to reject. Limit the search to the upstream keys configured for path, as on_no_upstream_branch does.

Useful? React with 👍 / 👎.

Comment thread lib/merge.sh
Comment on lines +193 to +195
if ! git -c advice.mergeConflict=false rebase -q --onto "$pull" "$base"; then
log_err "$path: conflict while rebasing onto the $verb -- resolve it, then 'git rebase --continue' (or 'git rebase --abort' to give up)"
return 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Distinguish pre-rebase failures from conflicts

When the index or worktree contains tracked changes, this git rebase invocation exits before creating any rebase state, but every nonzero exit is reported as a conflict with instructions to run git rebase --continue. The same misdiagnosis can occur when a pre-rebase hook rejects the operation. Git's --autostash documentation identifies that option as what permits rebasing a dirty worktree; since it is not used here, detect preflight failures and tell the user to commit or stash instead of directing them to a nonexistent rebase.

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant