Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXI · RebasingExecution

Rebase execution and stoppoints — when rebase pauses and how to resume

Advanced⏱ ~22 mingit

What you'll learn

  • Identify the conditions that cause `git rebase` to pause mid-replay
  • Use `git rebase --continue` to resume after a conflict is resolved
  • Use `git rebase --abort` to abandon a rebase and return to the pre-rebase tip
  • Use `git rebase --skip` to drop the current commit and continue with the next
  • Use `git rebase --exec` to run a command between commits, for example to rerun tests at each step

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

A rebase is not a single atomic operation; it is a sequence of commits replayed one at a time, with explicit stop points between each step. Stop points are triggered by a conflict during the current cherry-pick, by an edit line in an interactive rebase, or by a non-zero exit from an exec command. At a stop point, the repository is in a paused state: the working tree may hold conflict markers, the index may hold staged and unstaged changes, and HEAD points at a temporary commit that is not the original tip. The three verbs --continue, --abort, and --skip are the only ways out of a paused state, and choosing the wrong one is the most common way to leave a rebase in an unrecoverable shape.

The three stop points

A non-interactive git rebase <upstream> pauses under three conditions:

  1. A cherry-pick in the replay sequence cannot apply cleanly. The diff from the original commit’s parent to the original commit cannot be merged into the current HEAD without conflicts. The working tree is left with conflict markers; HEAD points at the temporary commit produced by the partial cherry-pick.
  2. An interactive rebase reaches an edit line. The commit is replayed successfully, then the rebase stops so the engineer can amend it.
  3. An exec command exits non-zero. The rebase stops so the engineer can investigate the failed command.

The first is the most common; the second and third are interactive features. In all three cases, the repository state is the same modulo: HEAD is at a temporary commit, the index is in an in-progress state, and a .git/rebase-merge (or .git/rebase-apply on the legacy backend) directory holds the todo list, the original HEAD, and the current commit’s original OID.

Resuming with git rebase --continue

The most common resume verb. The engineer resolves any conflicts, stages the resolved files with git add, and runs git rebase --continue. Git commits the staged state with the original commit’s message (or with the message produced by an interactive reword or squash), then proceeds to the next commit in the replay sequence.

git rebase main
# Conflict during cherry-pick of 9f3c1d7
# Resolve the conflict in the working tree
git add terraform/modules/iam/main.tf
git rebase --continue
# Applying: fix(iam): correct trust policy arn
# Applying: feat(iam): add terraform module
# Successfully rebased and updated refs/heads/feature/iam-rotation.

--continue is a no-op if there are no staged changes; Git complains that the index is empty and refuses to continue. The fix is git add on the resolved files before --continue.

Abandoning with git rebase --abort

The “this went wrong, I want my old branch back” verb. --abort reads the original tip from .git/rebase-merge/head-name (or the legacy equivalent), resets HEAD to that tip, deletes the rebase state directory, and leaves the working tree and index in the state they were in before the rebase started.

git rebase --abort
# (no output on success; HEAD is now back at the original tip)

--abort is safe to run at any stop point and at any time during the replay. It is the verb to use when the rebase has produced more conflicts than expected, when the original commits are needed for a different operation, or when the engineer decides the rebase is not the right verb after all.

# Recovering from a bad interactive rebase: just abort and start over
git rebase -i main
# (in editor: typo, save with bad todo list)
# (rebase starts; produces wrong history)
git rebase --abort
git rebase -i main
# (correct todo list this time)

Skipping with git rebase --skip

The “this commit does not belong in the new history” verb. --skip drops the current commit from the replay sequence and proceeds to the next. The dropped commit is removed from the branch view but remains in the object store, unreachable, until garbage collection.

git rebase --skip
# Skipping: fix(iam): correct trust policy arn
# Applying: feat(iam): add terraform module
# Successfully rebased and updated refs/heads/feature/iam-rotation.

--skip is the right verb when the current commit’s diff is already present in the new base (a git rebase after a cherry-pick that has already moved the change to upstream) or when the engineer decides, mid-rebase, that the current commit should not be in the new history. It is not a verb for “the conflict is hard, let me skip it”: an unresolved conflict leaves the working tree in a conflicted state, and --skip discards the commit’s intent entirely.

flowchart LR
    subgraph BEFORE_S["before rebase (4 commits on feature)"]
        A1["F1 feat: policy"] --> B1["F2 fix: trust"]
        B1 --> C1["F3 feat: module"]
        C1 --> D1["F4 chore: bump"]
    end
    subgraph STATE["during rebase, paused at F2 (conflict)"]
        R1["R1 (replayed F1)"] --> X["F2 = current commit, conflict markers in tree"]
        X -.skip.-> SKIP["F2 dropped, R3 = replayed F3"]
        SKIP --> R4["R4 = replayed F4, new feature tip"]
    end

Conflict resolution during a rebase

A conflict during a rebase is identical to a conflict during a cherry-pick (because that is what a rebase is, internally). The working tree contains conflict markers; git status lists the conflicted files; git diff shows the unresolved hunks.

git rebase main
# CONFLICT (content): Merge conflict in terraform/modules/iam/main.tf
# error: could not apply 9f3c1d7... fix(iam): correct trust policy arn
# Resolve all conflicts manually, mark them as resolved with
# "git add/rm <conflicted_files>", then run "git rebase --continue".
# If you prefer to skip this patch, run "git rebase --skip" instead.
# To check out the original branch and stop rebasing, run "git rebase --abort".

The resolution procedure:

# Inspect the conflict
git status
git diff

# Edit the file to the desired state
$EDITOR terraform/modules/iam/main.tf

# Stage the resolution
git add terraform/modules/iam/main.tf

# Resume the rebase
git rebase --continue

The git add is what signals to Git that the conflict is resolved. Without the git add, --continue complains that the index has no changes for the current commit.

The rebase state directory

The .git/rebase-merge directory (or .git/rebase-apply on the legacy backend) holds the state of an in-progress rebase:

  • head-name — the original branch ref, used by --abort to restore HEAD.
  • onto — the upstream tip onto which commits are being replayed.
  • orig-head — the original branch tip before the rebase started.
  • interactive — empty file indicating an interactive rebase.
  • current-commit, current-oid, done — the replay state.

--abort reads head-name, resets HEAD to it, and deletes the directory. --continue reads the current commit, commits the staged state, and rewrites the directory with the next commit. --skip rewrites the directory to mark the current commit as done and proceeds to the next. Manual editing of the directory is not supported; the verbs are the only supported interface.

git rebase --exec and git rebase -x

--exec (or -x) runs a shell command between commits during a rebase. The command runs after each replayed commit; if it exits non-zero, the rebase stops. This is the canonical pattern for “rerun the tests at every commit in this branch”:

git rebase -i main --exec "make test"
# pick 8a3f9d2 feat(iam): add role assumption policy
# exec make test
# pick 9f3c1d7 fix(iam): correct trust policy arn
# exec make test
# ...

If make test exits non-zero on the second commit, the rebase stops with that commit’s state in the working tree. The engineer can fix the test failure, git rebase --continue, and the rebase proceeds to the third commit. The end result is a branch in which every commit is verified to pass make test against the new base.

--exec is the operational tool for “every commit in this branch must build and pass tests” — the kind of property a CI pipeline enforces at the branch level but a rebase cannot enforce on its own.

Production discipline

  1. Resolve to --continue whenever the diff matters. If the current commit’s changes are intended to land in the new history, resolve the conflict and --continue. --skip is for “this diff is already in the new base” only.
  2. Use --abort to back out cleanly. When a rebase is not going to plan, --abort returns the branch to its pre-rebase tip and leaves the engineer free to try a different operation (merge, rebase onto a different upstream, or no integration at all).
  3. Use --exec to verify intermediate commits. A rebase that passes replay but breaks tests at commit N is worse than a rebase that stops at commit N with a test failure. The --exec "make test" pattern catches the first kind before it ships.
  4. Never edit the rebase state directory by hand. The .git/rebase-merge directory is a private state file; the verbs are the only supported interface. Manual edits can leave the rebase in a state that no verb can recover from, requiring a --hard reset from the reflog.

Cross-course references

  • CI/CD Pipeline Patterns - Part V (MergeQueues) uses git rebase --exec in the merge queue to verify that each commit in the rebased branch builds and passes tests before the merge into the trunk is allowed.
  • GitOps with Argo CD - Part VI (MergeStrategies) maps --abort onto the rollback story: an Argo CD sync that aborts mid-reconcile leaves the cluster in the pre-sync state, the GitOps equivalent of --abort.
  • Terraform for Production Sysadmins - Part XI (PRWorkflows) uses git rebase --exec "terraform validate" to verify that every commit in a plan branch parses cleanly before the merge into main.

Quiz

Knowledge check · 4 questions

  1. Q1. What is the correct verb to use when a rebase has produced a conflict and the engineer has resolved the conflict in the working tree?

  2. Q2. `git rebase --skip` is not a safe verb to use when a conflict is hard to resolve and the engineer wants to move on to the next commit.

  3. Q3. What does `git rebase --exec 'make test'` do, and why is it useful for verifying that a rebase did not break intermediate commits?

  4. Q4. Decide the correct sequence of verbs to use during a rebase that produces a conflict and then a test failure, and justify each choice.

    An engineer runs `git rebase -i main --exec "make test"` on a feature branch. The rebase pauses at the second commit with a conflict in `terraform/modules/iam/main.tf`. The engineer resolves the conflict, runs `make test` manually, and the test passes. The engineer then runs `git rebase --continue`, but the rebase pauses again at the third commit because `make test` (run by `--exec`) exits non-zero.

Passing score: 75%. Answers are checked in this browser.