Parchemin Academy

Version Control Mastery with Git: Branching, Rebasing, and Conflict Resolution

Restructure Commit History Using Interactive Rebase and Cherry-Pick

30 min readLesson 1 of 30

🎬 The Scenario

It's 3:45 PM on a Thursday and your pull request for the new payment-retry feature has just been reviewed by a senior engineer at a fintech startup. The feedback is blunt: "This looks like a debugging diary, not a feature. I count 8 commits — three of them say 'WIP', one says 'fix typo again', and the actual logic is buried in commit six. I'm not merging this. Clean it up." The release window is tomorrow morning. You have the code right, but the history is a mess — and at this company, a clean git log is not optional. It's how the team audits what changed and why, six months from now, when something breaks in prod.


The Problem This Solves

When you work alone on a feature branch, your commit history is a working journal: WIP, trying this, reverted back, actually fixed. That's fine while you're building. It becomes a liability the moment it lands in main. A messy history means git bisect takes twice as long to find regressions, git blame leads to useless commit messages, and code reviewers have to reconstruct what changed and why by reading diffs instead of reading intent.

The deeper problem is that Git's default merge workflow preserves every one of those intermediate commits verbatim, plus adds a merge commit on top. For long-running feature branches, this turns git log --oneline into an archaeological dig. Interactive rebase gives you the ability to rewrite your branch's commits — squash noise into signal, reword vague messages into precise ones, and reorder commits so the history reads like a coherent explanation of the change, not a stream of consciousness. git cherry-pick solves the complementary problem: a critical hotfix commit exists on one branch and needs to be applied to another, without merging everything else that came with it.


Concept Foundation

Git's commit history is a linked list: every commit points to its parent. When you rebase a branch, you are replaying your commits on top of a different base commit — like lifting a stack of changes off one foundation and re-placing them on another. Interactive rebase (git rebase -i) does this one commit at a time and hands you a text-based editor where you decide what to do with each commit before it is replayed: pick (keep it), squash (fold it into the commit above), fixup (squash silently, discard message), reword (keep the change, edit the message), drop (remove it entirely), or reorder (by rearranging lines).

The critical mental model: rebase rewrites history. Each replayed commit gets a new SHA, even if the code change is identical. This is safe on your own private feature branch because no one else has built on top of those commits. It is dangerous on shared branches because anyone who has already pulled those commits will have a divergent history — this is the cardinal rule of rebasing: never rebase commits that have been pushed to a branch others are working on.

git cherry-pick takes a specific commit SHA from anywhere in the repository and replays just that one commit's diff onto your current branch's HEAD. It does not bring the full branch history with it. This is exactly what you need when a hotfix was committed to main but must also land on a release/1.4 branch that diverged three weeks ago. Cherry-pick creates a new commit with a new SHA but the same code change and commit message.

This lesson maps to the GitHub Actions certification indirectly (clean history is a prerequisite for meaningful CI audit trails), and directly to the Git fundamentals expected in interviews for any DevOps or platform engineering role. The concepts here — rewriting history, linear vs. non-linear graphs, SHA immutability — are standard senior-level screening questions.


Teaching Example

# -- TEACHING EXAMPLE: simplified for clarity --

# Step 1: See what you're dealing with
git log --oneline feature/payment-retry
# Output (example):
# a1b2c3d fix typo again
# e4f5g6h WIP - testing edge case
# i7j8k9l actually added retry logic
# m0n1o2p WIP
# q3r4s5t added tests (some failing)
# u6v7w8x refactored helper method
# y9z0a1b initial scaffold for payment retry
# b2c3d4e baseline before feature (this is where main diverged)

# Step 2: Start interactive rebase against the commit BEFORE your work began.
# HEAD~7 means "go back 7 commits from HEAD" — covering all 7 feature commits.
# We use HEAD~7 instead of the SHA to avoid hardcoding.
git rebase -i HEAD~7

# Step 3: The editor opens with this content (oldest commit at the top):
# pick y9z0a1b initial scaffold for payment retry
# pick u6v7w8x refactored helper method
# pick q3r4s5t added tests (some failing)
# pick m0n1o2p WIP
# pick i7j8k9l actually added retry logic
# pick e4f5g6h WIP - testing edge case
# pick a1b2c3d fix typo again

# Edit the file to look like this:
# pick y9z0a1b initial scaffold for payment retry
# squash u6v7w8x refactored helper method        # fold into scaffold — same logical unit
# reword q3r4s5t added tests (some failing)      # keep the tests, fix the message
# fixup m0n1o2p WIP                              # silently absorbed into tests commit
# squash i7j8k9l actually added retry logic      # consolidate all logic into one commit
# fixup e4f5g6h WIP - testing edge case          # discard this message, keep the change
# fixup a1b2c3d fix typo again                   # typo fixes don't deserve a commit

# Save and close the editor. Git will pause for squash/reword steps to let you
# write the final commit message. Write messages like:
#   "feat(payments): scaffold retry module with helper extraction"
#   "test(payments): add retry logic unit tests for edge cases"
#   "feat(payments): implement exponential backoff retry with max attempts"

# Step 4: Verify the result
git log --oneline feature/payment-retry
# a1a1a1a feat(payments): implement exponential backoff retry with max attempts
# b2b2b2b test(payments): add retry logic unit tests for edge cases
# c3c3c3c feat(payments): scaffold retry module with helper extraction

# Step 5: Cherry-pick the hotfix commit from main onto release/1.4
# Your security team pushed a hotfix (SHA d4e5f6g) to main that patches a
# payment validation bypass. release/1.4 also needs it, without getting
# everything else that landed on main after the branch diverged.
git checkout release/1.4
git cherry-pick d4e5f6g
# Git replays just that commit's diff onto release/1.4's HEAD.
# Verify it's there:
git log --oneline -5 release/1.4

The git rebase -i HEAD~7 command opens your $EDITOR (usually vim or nano) with a todo list of your last 7 commits, oldest first. The order matters: Git will process this list top-to-bottom. Each line starts with a command (pick, squash, etc.) followed by the SHA and message. You edit the commands, save, and Git executes them in sequence. When it hits a squash or reword, it pauses and asks you to write the final commit message — this is your chance to write the clean, reviewable message you should have written the first time.

The fixup command is squash's quieter sibling. It folds the commit's code changes into the one above it but silently discards the commit message, which means Git doesn't pause to ask you for a new message. Use it for noise commits — typo fixes, stray WIPs — where there is genuinely nothing useful to say.

For cherry-pick: git cherry-pick d4e5f6g computes the diff that commit introduced relative to its parent and applies that diff to your current HEAD. If the code context has diverged enough, Git will flag a conflict — you resolve it exactly the same way as a merge conflict, then run git cherry-pick --continue.


Production Version

# -- PRODUCTION VERSION: how this looks in a real codebase --

# DIFFERENCE FROM TEACHING EXAMPLE: We add safety checks, a dry-run inspection
# step, and enforce a force-push convention — because in production you will
# almost certainly need to update the remote after rewriting history.

# Step 1: Always fetch latest before rebasing so you rebase onto current main,
# not a stale local copy. Stale base = unnecessary conflicts.
git fetch origin

# Step 2: Check for any remote tracking differences BEFORE you start.
# If this shows commits, someone pushed to your branch — coordinate before rebasing.
git log origin/feature/payment-retry..feature/payment-retry --oneline

# Step 3: Create a backup ref BEFORE touching history.
# If the rebase goes wrong, you can reset to this. Drop it only after you've
# verified the result and the PR is merged.
git branch feature/payment-retry-backup

# Step 4: Rebase interactively against origin/main, not HEAD~N.
# Using origin/main as the base is safer than HEAD~N — it rebases only the commits
# that aren't in main yet, regardless of how many that is.
git rebase -i origin/main

# Step 5: If a conflict occurs during rebase, Git will pause and tell you:
# "CONFLICT (content): Merge conflict in src/payments/retry.js"
# Resolve the conflict in your editor, then:
git add src/payments/retry.js       # stage the resolved file
git rebase --continue               # tell Git to proceed to the next commit

# If you decide you've made a mistake mid-rebase and want to abort entirely:
git rebase --abort                  # returns branch to pre-rebase state

# Step 6: Force-push with lease — safer than `--force`.
# --force-with-lease checks that the remote ref matches what you last fetched.
# If a teammate pushed to the branch since your last fetch, this FAILS instead
# of silently overwriting their work. Always use this, never bare --force.
git push --force-with-lease origin feature/payment-retry

# Step 7: Cherry-pick the hotfix with -x flag for traceability.
# -x appends "(cherry picked from commit <SHA>)" to the commit message,
# so reviewers on release/1.4 can trace the patch back to its origin on main.
git checkout release/1.4
git fetch origin release/1.4
git cherry-pick -x d4e5f6g

# Step 8: If cherry-pick produces a conflict (common when branches have diverged):
# Git outputs: "CONFLICT (content): Merge conflict in src/payments/validator.js"
git status                           # confirm which files need resolution
# Edit conflicted files, then:
git add src/payments/validator.js
git cherry-pick --continue

# Step 9: Push the updated release branch (no force needed — cherry-pick adds
# a new commit, it doesn't rewrite existing history on the release branch).
git push origin release/1.4

# Step 10: After the PR is merged and verified, clean up the backup.
git branch -d feature/payment-retry-backup

Common Mistakes & How to Fix Them

Mistake 1: Rebasing a shared branch and force-pushing with --force

  • Symptom: A teammate runs git pull origin feature/payment-retry and sees: error: Your local changes would be overwritten by merge or a diverged history requiring git pull --rebase. Their unpushed commits on the same branch are now orphaned.
  • Why it happens: --force overwrites the remote ref unconditionally, destroying any commits pushed after your last fetch.
  • Fix: Always use --force-with-lease. Better yet, only rebase branches that only you are working on. If others are on the branch, use a merge squash (git merge --squash) instead — it preserves the remote history and adds one clean commit.

Mistake 2: Wrong commit count in HEAD~N

  • Symptom: The rebase todo list includes commits from main that predate your feature, or it doesn't include your earliest feature commit.
  • Why it happens: You miscounted how many commits back to go.
  • Fix: Use git rebase -i origin/main instead of HEAD~N. This automatically scopes the interactive rebase to only the commits that are not yet in main, regardless of count.

Mistake 3: Accidentally dropping a commit with drop or by deleting the line

  • Symptom: After rebase, code that was working is gone. git log confirms the commit is not there.
  • Why it happens: In the rebase todo editor, deleting a line or using drop removes that commit's changes entirely.
  • Fix: Recover using git reflog. Run git reflog, find the SHA before the rebase started (look for the rebase (start) entry), and git reset --hard <that-SHA> to return to where you were.

Mistake 4: Cherry-picking without -x and losing traceability

  • Symptom: Three months later, someone sees a commit on release/1.4 with the message "fix payment validation bypass" and has no idea if it was also applied to main or where it came from.
  • Why it happens: Without -x, the cherry-picked commit has no record of its origin.
  • Fix: Always use git cherry-pick -x <SHA> on release/hotfix branches. The appended (cherry picked from commit ...) note is low-cost and saves real forensic time.

Mistake 5: Rewriting history after a PR review has started

  • Symptom: GitHub shows "force-pushed" in the PR timeline and all reviewer comments are now orphaned from the lines they referenced, because the SHAs changed.
  • Why it happens: Every rebased commit gets a new SHA, and GitHub anchors comments to SHAs.
  • Fix: Finish interactive cleanup before you open the PR for review. If review has already started, use git commit --amend only for the last commit, or wait until after merge to clean up (using --squash merge strategy in GitHub settings).

Interview Questions

Question 1 — Conceptual: "What is the difference between git merge and git rebase, and when would you choose one over the other?"

Model Answer: git merge combines two branches by creating a new merge commit that has two parents — it preserves the full branching and merging history exactly as it happened. git rebase replays your branch's commits on top of the target branch, producing a linear history with no merge commit. Choose merge when you want an honest record of parallel development (common on main or long-lived shared branches). Choose rebase when you want a clean, readable, linear history on a private feature branch before it's reviewed and merged. The rule of thumb: rebase before the PR, merge after it. Never rebase commits others have built on — it resets their history and causes divergence.


Question 2 — Scenario-Based: "A critical security patch was committed directly to main as commit abc1234. You have three release branches — release/2.1, release/2.2, and release/2.3 — all of which need the patch, but none of them should get any other commits from main. How do you do this safely?"

Model Answer: This is a textbook use case for git cherry-pick. Check out each release branch in turn, run git cherry-pick -x abc1234, resolve any conflicts, and push. The -x flag appends the origin SHA to each resulting commit message, so any future audit trail can confirm the patch was applied and trace it back to its source. Before cherry-picking, I'd run git log release/2.1..main -- <affected-file> to confirm there are no conflicting changes on those release branches that would make the diff apply incorrectly. If conflicts do arise, I'd resolve them carefully and document why in the commit message. I would not merge main into the release branches because that would pull in unrelated changes — exactly what the release branches are designed to isolate.

(Maps to: GitHub Advanced Security exam concepts, general Git proficiency expected in CKA pre-requisite knowledge, and AWS DevOps Engineer Professional domain: "Configuration Management and Infrastructure as Code" — where clean, auditable Git history is a prerequisite for safe automated deployments.)


What You Can Now Do

You can now take a messy, WIP-riddled feature branch and use git rebase -i to squash noise commits, reword vague messages, and produce a clean, reviewable history that your team will actually thank you for — and you can use git cherry-pick -x to surgically apply a single hotfix commit to a release branch without dragging along unrelated changes. You understand when interactive rebase is appropriate (private branches, before review) and when it's dangerous (shared branches, after push without --force-with-lease). In the next lesson, you will extend this to resolving multi-file merge conflicts when two feature branches modify the same code, using a structured three-way merge workflow that works under deadline pressure.


Key Terms

  • Interactive Rebase (git rebase -i): A Git command that opens an editor allowing you to reorder, squash, reword, or drop individual commits before replaying them onto a new base commit.
  • Squash: A rebase operation that folds a commit's changes into the commit immediately above it, prompting you to write a single combined commit message.
  • Fixup: Like squash, but silently discards the folded commit's message without prompting — used for noise commits with nothing meaningful to say.
  • Cherry-Pick (git cherry-pick): A Git command that applies the diff introduced by a specific commit onto the current branch's HEAD, creating a new commit with a new SHA.
  • Force-with-lease (--force-with-lease): A safer alternative to git push --force that fails if the remote ref has changed since your last fetch, preventing you from overwriting a teammate's pushed commits.
  • Reflog: Git's internal journal of every movement of HEAD and branch pointers, used to recover commits that appear lost after a rebase, reset, or accidental drop.
  • Linear History: A commit graph with no merge commits, where every commit has exactly one parent — the result of a rebase-before-merge workflow, preferred for auditability in CI/CD pipelines.
  • SHA (Secure Hash Algorithm): The unique 40-character identifier Git generates for every commit; rebasing always produces new SHAs even for identical code changes, which is why it rewrites history.