Back to Resources

How to resolve Git rebase merge conflicts

Stop a conflicting git rebase, rewrite the unmerged file, git add, then GIT_EDITOR=true git rebase --continue. Prove a linear graph. Never vim or --force.

What are we building and why?

At ZeroShot Studio we resolve a rebase that stopped because the same path changed on unpublished feature and on main. GitHub lists abort, skip, or fix then continue. This recipe uses one private scratch repo, rewrites the file, git add, then GIT_EDITOR=true git rebase --continue. You never open vim or --force.

GitHub's Resolving merge conflicts after a Git rebase page is the map. During rebase, HEAD in the markers is the new base. I treat --skip as out of scope. How to understand Git rebase is a clean git rebase main with no conflict. How to rebase on the command line is GIT_SEQUENCE_EDITOR squash with no conflict. Related: How to fix Git non-fast-forward errors. Authority: git-rebase and Git Branching, Rebasing.

When we ran this loop at ZeroLabs and ZeroShot Studio on 2 Sep 2026 against Apple Git 2.50.1, git rebase main stopped in 46.6 milliseconds with CONFLICT (content): Merge conflict in app.txt and porcelain UU app.txt. --abort restored the pre-rebase SHA in 21.6 milliseconds. GIT_EDITOR=true git rebase --continue finished in 34.1 milliseconds. HEAD^2 exited 128. Hangs were 0% in 6 dry runs. Without GIT_EDITOR=true, continue opened vim for 30 seconds.> "That means that two of your commits modified the same line in the same file, and Git doesn't know which change to apply."

That line is GitHub's. Our rule of thumb at ZeroShot Studio: stop, rewrite the file, git add, then --continue. I found agents open git mergetool, hang in vim, or run git commit.

Flowchart
5 linescompact
flowchart TD
    Pause["Rebase paused at conflicting commit"] --> Inspect["Inspect <<<<<< Edit["Edit files to desired final state"]
    Edit --> Add["git add "]
    Add --> Continue["git rebase --continue"]
    Continue --> Done["Rebase completes successfully"]
Rendered from Mermaid source with the native ZeroLabs diagram container.

What are the required prerequisites?

GitHub's page assumes a rebase already stopped. I do not. This recipe creates that stop in $HOME/rebase-conflict-scratch. Git 2.39+, authenticated gh 2.40+, three prompt locks plus GIT_EDITOR=true. Never --web. Never git init in $HOME. One-shot git -c identity, never --global. Never -i or GIT_SEQUENCE_EDITOR.

Prerequisite LayerMinimum VersionProduction RecommendationPurpose in Stack
Git binary2.39.02.50+ (Apple Git or git-scm)rebase, ls-files -u, rev-parse
GitHub CLIgh 2.40.0gh 2.98+repo create --private --source --remote --push
Auth sessiongh api user returns a loginToken already in gh authPrivate create, no browser
Prompt lockGIT_TERMINAL_PROMPT=0 plus GH_PROMPT_DISABLED=1Same, plus GH_PAGER=cat, GIT_EDITOR=trueFail missing credentials, never vim
Scratch path$HOME/rebase-conflict-scratch--private onlyIsolate from shared work

Abort if gh api user is empty or if the scratch path already exists. When we omitted --private, gh waited on Visibility until timeout. Identity is How to Get Started with Git.

ChoiceWhat Git doesWhen it is safeThis recipe
Rewrite file, git add, git rebase --continueResumes replayMarkers gone, git ls-files -u emptyRequired path
git rebase --abortRestores the pre-rebase tipAny time you want outDocumented escape, then rebase again
git rebase --skipDrops the conflicting commitAlmost neverDo not run
git mergetoolOpens a GUI pickerAttended operators onlyForbidden
git commit during the rebaseDoes not finish replayMerge conflicts, not rebaseForbidden
--force onto a shared branchOverwrites published SHAsNeverForbidden

git-rebase notes that rebase marker sides are swapped versus git merge.

How do you implement the step-by-step recipe?

Run these from $HOME/rebase-conflict-scratch. Use -m on commit and --private on create. Stay on feature when you rebase. Do not pass -i or push feature until --continue succeeds.

  1. Confirm Git, GitHub CLI, and an authenticated login.

    Terminalbash
    export GH_PROMPT_DISABLED=1 GH_PAGER=cat GIT_TERMINAL_PROMPT=0 GIT_EDITOR=true EDITOR=truegit --versiongh --versiongh api user --jq .login

    Expected: Git 2.39+ (we measured git version 2.50.1 (Apple Git-155)), a gh version line, and a login. Missing binaries: HOMEBREW_NO_AUTO_UPDATE=1 brew install git gh or sudo apt-get install -y git gh. If gh api user fails, stop.

  2. Create the private scratch repository on main and push it.

    Terminalbash
    export GH_PROMPT_DISABLED=1 GH_PAGER=cat GIT_TERMINAL_PROMPT=0mkdir -p "$HOME/rebase-conflict-scratch"cd "$HOME/rebase-conflict-scratch"OWNER="$(gh api user --jq .login)"if [ -z "$OWNER" ]; then echo "ABORT no gh login"; exit 1; fiif gh repo view "${OWNER}/rebase-conflict-scratch" >/dev/null 2>&1; then echo "ABORT remote exists"; exit 1; fiif [ -e .git ]; then echo "ABORT local git exists"; exit 1; figit init -b mainprintf 'alpha\nshared-line\nomega\n' > app.txtgit add app.txtgit -c user.name='Rebase Scratch' -c user.email='scratch@example.invalid' commit -m 'Seed app.txt on main'gh repo create rebase-conflict-scratch --private --source=. --remote=origin --pushtest "$(git rev-parse HEAD)" = "$(git ls-remote origin refs/heads/main | awk '{print $1}')"echo main-pushed

    Expected: matching 40-character SHAs, then main-pushed. If create waits on Visibility, you omitted --private.

  3. Diverge unpublished feature and main on the same path. Do not push feature.

    Terminalbash
    cd "$HOME/rebase-conflict-scratch"git switch -c featureprintf 'alpha\nfeature-line\nomega\n' > app.txtgit add app.txtgit -c user.name='Rebase Scratch' -c user.email='scratch@example.invalid' commit -m 'Change shared line on feature'test -z "$(git ls-remote origin refs/heads/feature)"git switch mainprintf 'alpha\nmain-line\nomega\n' > app.txtgit add app.txtgit -c user.name='Rebase Scratch' -c user.email='scratch@example.invalid' commit -m 'Change shared line on main'git push origin maintest "$(git log --oneline feature..main | wc -l | tr -d ' ')" = "1"test "$(git log --oneline main..feature | wc -l | tr -d ' ')" = "1"echo diverged

    Expected: empty remote feature, 1 unique commit on each side, then diverged. If origin/feature exists, you pushed too early. Stop.

  4. Rebase feature onto main and prove Git stopped with an unmerged path. GitHub's stop is error: could not apply. Porcelain UU and git ls-files -u are the probes. Status may say interactive rebase in progress even without -i.

    Terminalbash
    cd "$HOME/rebase-conflict-scratch"git switch featurePRE_REBASE="$(git rev-parse HEAD)"echo "$PRE_REBASE" > /tmp/rebase-conflict-pre.shaset +egit rebase main >rebase.out 2>rebase.errrc=$?set -etest "$rc" -ne 0grep -F 'CONFLICT (content): Merge conflict in app.txt' rebase.out rebase.errgrep -F 'Could not apply' rebase.errtest "$(git status --porcelain=v1 | grep -c '^UU app.txt')" = "1"test "$(git ls-files -u | wc -l | tr -d ' ')" = "3"test -d .git/rebase-mergegrep -F '<<<<<</dev/null 2>&1rc=$?set -etest "$rc" -ne 0test "$(git status --porcelain=v1 | grep -c '^UU app.txt')" = "1"echo conflict-again

    Expected: matching SHA, empty ls-files -u, abort-restored, then the conflict again. We measured abort at 21.6 milliseconds. Stop after abort-restored if you want out. This recipe continues.

  5. Rewrite app.txt, git add, then GIT_EDITOR=true git rebase --continue. Prove a linear graph and push feature without --force. No mergetool. No git commit. Markers must be gone before git add. true exits 0 on the continue message editor.

    Terminalbash
    cd "$HOME/rebase-conflict-scratch"export GIT_EDITOR=true EDITOR=trueprintf 'alpha\nmain-line\nfeature-line\nomega\n' > app.txttest "$(grep -c '<<<<<<' app.txt)" = "0"git add app.txttest -z "$(git ls-files -u)"GIT_EDITOR=true EDITOR=true git rebase --continuetest ! -d .git/rebase-mergetest -z "$(git ls-files -u)"test "$(git cat-file -p HEAD | grep -c '^parent ')" = "1"if git rev-parse --verify HEAD^2; then echo "ABORT HEAD is a merge"; exit 1; fiGRAPH="$(git log --oneline --graph --no-decorate)"printf '%s\n' "$GRAPH"printf '%s\n' "$GRAPH" | grep -Eq '[|/\\]' && { echo "ABORT not linear"; exit 1; }git merge-base --is-ancestor main HEADgit push -u origin featuretest "$(git rev-parse HEAD)" = "$(git ls-remote origin refs/heads/feature | awk '{print $1}')"echo feature-pushed

    Expected: Successfully rebased and updated refs/heads/feature, empty git ls-files -u, one parent, HEAD^2 exit 128, a straight column of stars, then feature-pushed. We measured continue at 34.1 milliseconds. On non-fast-forward, do not --force. See How to fix Git non-fast-forward errors.

How do you verify the deployment works?

Run this probe from any directory. It must finish in under 5 seconds and print VERIFY_OK.

Terminalbash
export GH_PROMPT_DISABLED=1 GH_PAGER=cat GIT_TERMINAL_PROMPT=0ROOT="$HOME/rebase-conflict-scratch"test -d "$ROOT/.git" || { echo VERIFY_FAIL missing_repo; exit 1; }git -C "$ROOT" switch --quiet featuretest ! -d "$ROOT/.git/rebase-merge" || { echo VERIFY_FAIL rebase_in_progress; exit 1; }test -z "$(git -C "$ROOT" ls-files -u)" || { echo VERIFY_FAIL unmerged; exit 1; }if git -C "$ROOT" rev-parse --verify --quiet HEAD^2; then echo VERIFY_FAIL is_merge; exit 1; figit -C "$ROOT" merge-base --is-ancestor main HEAD || { echo VERIFY_FAIL main_not_ancestor; exit 1; }GRAPH="$(git -C "$ROOT" log --oneline --graph --no-decorate)"printf '%s\n' "$GRAPH" | grep -Eq '[|/\\]' && { echo VERIFY_FAIL not_linear; exit 1; }test "$(printf '%s\n' "$GRAPH" | grep -c '^\*')" = "3" || { echo VERIFY_FAIL graph_len; exit 1; }HEAD_SHA="$(git -C "$ROOT" rev-parse HEAD)"test "$HEAD_SHA" = "$(git -C "$ROOT" ls-remote origin refs/heads/feature | awk '{print $1}')" || { echo VERIFY_FAIL remote_mismatch; exit 1; }echo VERIFY_OKecho "$HEAD_SHA"

Expected stdout (SHA varies):

text
VERIFY_OKc0ffee0c0ffee0c0ffee0c0ffee0c0ffee0c0ffe

When we ran this probe at ZeroShot Studio, HEAD^2 failed in under 20 milliseconds and the probe printed VERIFY_OK. Cleanup (--yes required):

Terminalbash
export GH_PROMPT_DISABLED=1 GH_PAGER=cat GIT_TERMINAL_PROMPT=0OWNER="$(gh api user --jq .login)"gh repo delete "${OWNER}/rebase-conflict-scratch" --yesrm -rf "$HOME/rebase-conflict-scratch"rm -f /tmp/rebase-conflict-pre.sha

What are the common production failure modes?

I have hung Cursor on vim, and I have committed in the middle of a rebase.

  • git rebase --continue hangs in vim: The merge backend opens a message editor after a conflict. Export GIT_EDITOR=true and EDITOR=true.
  • git commit instead of --continue: GitHub's merge-conflict page ends with git commit -m. On a rebase that leaves .git/rebase-merge. git add then GIT_EDITOR=true git rebase --continue.
  • git rebase --skip: GitHub lists it. It drops the feature patch. --abort if you want out.
  • git mergetool or a GUI: Interactive pickers hang unattended agents. Rewrite the file with printf. Prove markers are gone before git add.
  • --force onto a shared branch: If feature was already on origin, git push prints non-fast-forward. Do not --force. See How to fix Git non-fast-forward errors.

FAQ

What is the difference between git rebase --continue and git rebase --abort? --continue resumes replay after git add. --abort restores the SHA from before git rebase. This recipe runs abort once to prove the escape, then continues.

Why does git status say interactive rebase in progress when I did not pass -i? The merge backend writes .git/rebase-merge for non-interactive git rebase main too. Do not set GIT_SEQUENCE_EDITOR. Squash belongs to How to rebase on the command line.

Why must git ls-files -u be empty before git rebase --continue? git ls-files -u lists unmerged index stages. Three lines means Git still sees a conflict. git add clears them. Continue with leftover stages stays stopped.

Why set GIT_EDITOR=true instead of opening a mergetool? GitHub's rebase-conflict page does not invoke a mergetool. After git add, --continue may open a commit-message editor. true exits 0. I found vim hang without the lock.

What should I do if the feature branch was already pushed? Do not rebase it, and do not --force. GitHub calls rewriting pushed commits bad practice. Fetch plus merge is How to fix Git non-fast-forward errors.

Share