Git Rebase vs Merge — Shared Branch Rebase Cost 6 Dev Hours
Git Rebase vs Merge: After a force-pushed rebase on develop cost 6 devs 3 hours, learn the production disaster, recovery steps, and the golden rule to prevent it..
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
- ✓Solid grasp of DevOps fundamentals
- ✓Comfortable with command-line tools
- ✓Basic Linux administration knowledge
- Merge creates a merge commit with two parents — preserves parallel development history
- Rebase replays commits on top of new base — linear history with rewritten SHAs
- Interactive rebase (rebase -i): squash, reorder, reword commits before PR
- Golden Rule: never rebase a branch others have pulled
- Performance: linear history from rebase speeds up git bisect by ~30%
- Production insight: rebasing a shared branch forces teammates to hard-reset; one bad rebase costs hours
Git rebase and merge are two strategies for integrating changes from one branch into another, but they differ fundamentally in how they handle commit history. Merge creates a new 'merge commit' that ties together the divergent branch histories, preserving the exact timeline of when each commit was made.
This results in a non-linear history that accurately reflects parallel development. Rebase, by contrast, rewrites history: it takes the commits from your current branch and replays them on top of the target branch, creating entirely new commits with new hashes.
The result is a clean, linear history that looks as if all work was done sequentially on a single branch. This trade-off—accuracy of history vs. cleanliness of history—is the core decision point.
The problem arises when you rebase a branch that others have already pulled from or pushed to. Because rebase rewrites commit hashes, anyone who has the old version of the branch will have a divergent history that Git cannot reconcile without manual intervention.
This is the 'shared branch rebase cost' referenced in the title: a single rebase of a shared feature branch can force every developer who has that branch to delete their local copy, fetch the rewritten branch, and re-apply any uncommitted work. In production environments, this has caused teams to lose hours of work when developers unknowingly rebase branches that teammates are actively using.
In practice, merge is the safe default for shared branches like main, develop, or long-lived feature branches where multiple people collaborate. Rebase is best reserved for local, unshared branches—cleaning up your own commit history before pushing, or keeping a feature branch up-to-date with main via git pull --rebase when you are the sole contributor.
The --onto flag extends rebase's power by letting you transplant a range of commits onto a different base, useful for splitting or reordering work, but again only safe on private branches. The rule of thumb: if more than one person has touched the branch, use merge; if it's just you, rebase is fine.
Imagine you're writing a group essay. Merge is like stapling everyone's drafts together at the end — you can see every version and who wrote what, but the final document has a messy paper trail. Rebase is like retyping your section from scratch onto the latest shared draft — the result looks seamless, as if only one person wrote it the whole time. Both get you to a finished essay, but they leave very different paper trails behind.
Merge and rebase both integrate changes from one branch into another. They solve the same problem in fundamentally different ways. Merge creates a merge commit that records when two branches came together. Rebase rewrites your commits so they appear to have started from the tip of the target branch.
The choice shapes your project's Git history. Merge preserves the full context of parallel development — you can see exactly what state main was in when your feature was built. Rebase produces a clean precise and git log more readable. Neither is universally better.
Common misconceptions: that rebase is always cleaner (it rewrites SHAs, which breaks shared branches), that merge always creates noise (merge commits are meaningful integration markers), and that the choice is personal preference (it is a team decision that affects debugging and collaboration).
Rebase vs Merge — The Fundamentals
Both git merge and git rebase integrate changes from one branch into another, but they produce fundamentally different histories.
git merge creates a merge commit that joins two lines of development. The merge commit has two parents — the tip of your current branch and the tip of the branch you're merging. This preserves the exact chronology: 'I started feature X, then main moved forward, then I merged main into my feature.'
Pros: Preserves context ('when was this work done relative to other changes'). Cons: Can create a messy graph with many merge commits.
git rebase replays your commits on top of another branch's tip. Instead of creating a merge commit, Git computes diffs between each of your commits and applies them sequentially after the target branch's current tip. This produces a linear history.
Pros: Clean, linear history. No merge commits. Cons: Rewrites commit SHAs. Changes the timeline (your commits appear to happen after all target commits, even if they were written earlier).
When to use which: - Use merge (with --no-ff) for integrating completed features into shared branches (main, develop). - Use rebase for cleaning up local commits before pushing. - Never use rebase on branches that others pull from.
# Setup: create a base and divergent branches git init && echo 'Initial' > file.txt && git add . && git commit -m 'Initial' git checkout -b feature echo 'Feature work' >> file.txt && git add . && git commit -m 'Add feature' git checkout main && echo 'Main work' >> file.txt && git add . && git commit -m 'Update main' # MERGE: creates a merge commit git checkout feature git merge main --no-ff -m 'Merge main into feature' git log --oneline --graph --all echo '---' # RESET and try REBASE instead git reset --hard HEAD~1 git rebase main git log --oneline --graph --all
git merge --squash feature combines all feature commits into one, keeping main's history clean without rewriting SHAs. This gives you the best of both: linear main history and no rebase-on-shared-branch risk.Git Rebase vs Merge — The Divergent Commit Topology Problem
Git rebase rewrites commit history by replaying a branch's commits onto the tip of another branch, creating a linear sequence. Merge preserves the full divergence by creating a merge commit that ties two branch histories together. The core mechanic: rebase changes the parent of each replayed commit; merge does not.
Rebase produces a clean, linear history — every commit appears as if it were made sequentially on the target branch. This eliminates the topological complexity of merge commits, but at a cost: each replayed commit is a new object with a new hash. If the branch is shared, force-pushing a rebased branch invalidates every other developer's local copy, causing upstream rebase conflicts that require manual resolution.
Use rebase for local feature branches before merging into a shared integration branch. Use merge for integrating long-lived shared branches (e.g., release branches) where preserving the exact merge point and avoiding history rewrite outweighs linearity. In practice, teams that rebase shared branches lose hours to conflict resolution — one mis-timed rebase on a 5-person team can cost 6 dev-hours of unproductive sync work.
How Git Merge Works — and What It Leaves Behind
When you run git merge, Git finds the most recent common ancestor of the two branches (called the merge base), then combines the changes from both branches into a brand-new commit. This new commit has two parents — one from each branch — which is why it's called a merge commit. It's the only commit in your repo that points backwards at two different lines of work.
This means your history is a faithful record of reality. If you and a colleague worked in parallel for a week, the graph shows that. You can run git log --graph and literally see two lanes of traffic merging into one. That's incredibly useful when you're trying to understand why a change was made in the context of what else was happening at the time.
The downside is noise. On a busy team where a dozen feature branches merge into main every day, your history fills up with merge commits that add structural information but no actual code change. Tools like git bisect and git log --oneline become harder to read. Some teams are fine with this — it's an honest record. Others find it distracting. That tension is the whole reason rebase exists.
# io.thecodeforge — Merge Feature Branch # ───────────────────────────────────────────────────────────── # SCENARIO: You've been building a user-authentication feature. # A colleague just merged a critical security patch into main. # You need those changes in your feature branch before you can continue. # ───────────────────────────────────────────────────────────── # Step 1 — Check your current position git log --oneline --graph --all # Output before merge: # * f3a91bc (HEAD -> feature/user-auth) Add password hashing # * 9d44e02 Add login endpoint # | * c7b83af (main) SECURITY: sanitize SQL inputs in user queries # | * 4e21d09 Add rate limiting middleware # |/ # * 8b12cde Initial project scaffold # Step 2 — Switch to your feature branch (already on it, just confirming) git checkout feature/user-auth # Step 3 — Merge main INTO your feature branch # This brings the security patch into your work git merge main # Git opens your editor for a commit message, or auto-creates: # "Merge branch 'main' into feature/user-auth" # Step 4 — See what the history looks like now git log --oneline --graph --all
- Merge commit has two parents — one from each branch
- git log --graph shows the parallel lanes and the merge point
- History is a faithful record of what happened in parallel
- Merge commits add structural information but no code change — this is the noise trade-off
How Git Rebase Works — and Why It Rewrites History
Rebase does something more radical: it replays your commits one by one on top of a new base commit. The word 'rebase' is literal — you're changing the base commit that your branch started from. The result looks as if you had branched off at the very tip of the target branch and written all your commits from there.
Here's the key thing to internalise: your original commits are not moved. Git creates brand-new commits with the same changes but different parent hashes, timestamps, and therefore different SHA-1 identifiers. The old commits still exist temporarily, but with nothing pointing to them, they'll be cleaned up by Git's garbage collector. This is not a cosmetic rename — it is a genuine rewrite.
The payoff is a perfectly linear history. When a reviewer reads your branch after a rebase, they see a clean sequence of purposeful commits with no structural noise. git log looks like a well-written changelog. git bisect works with surgical precision because every commit in the chain is meaningful. This is why many teams require rebase before merging feature branches via pull requests — you get the readability benefits of a linear history in the shared record.
# io.thecodeforge — Rebase Feature Branch onto Main # ───────────────────────────────────────────────────────────── # Same scenario — feature branch needs the security patch from main. # This time we'll use rebase instead of merge. # ───────────────────────────────────────────────────────────── # Step 1 — Verify starting state (same branching point as before) git log --oneline --graph --all # * f3a91bc (HEAD -> feature/user-auth) Add password hashing # * 9d44e02 Add login endpoint # | * c7b83af (main) SECURITY: sanitize SQL inputs in user queries # | * 4e21d09 Add rate limiting middleware # |/ # * 8b12cde Initial project scaffold # Step 2 — Make sure your working tree is clean before rebasing git status # Should show: nothing to commit, working tree clean # Step 3 — Rebase the feature branch onto the tip of main # Git will replay 9d44e02 and f3a91bc on top of c7b83af git rebase main # Output during rebase: # Successfully rebased and updated refs/heads/feature/user-auth. # Step 4 — Look at the history now git log --oneline --graph --all # Step 5 — If you had already pushed the old commits, force-push the rebased ones # ONLY if nobody else has pulled your branch git push --force-with-lease origin feature/user-auth # --force-with-lease is safer than --force: it fails if the remote has # commits you don't know about (someone else pushed while you were rebasing).
Warning: Never Rebase Shared Branches
The Golden Rule of rebasing is non-negotiable: never rebase a branch that another developer has pulled from. Once a commit is shared — pushed to a remote or pulled by a teammate — its SHA must never change. Rebasing a shared branch rewrites all commits, creating diverging histories that force every collaborator to hard-reset. The most common casualty is the develop branch, where a well-intentioned cleanup costs hours of team coordination.
Visual Branch History: Merge vs Rebase
Seeing the difference between merge and rebase is easier with visual diagrams. Below is a Mermaid graph showing the same initial forked history, then the result of merging, and the result of rebasing. Notice how the merge result introduces a merge commit with two parents, while the rebase result appears as a linear sequence of commits.
Before: Forked History — main has commit A, then both main and feature branches diverge. main receives commit C, feature receives commit B.
After Merge — a new merge commit D joins the two branches. Git shows both lanes of work. This is truthful parallelism but adds structural nodes.
After Rebase — feature's commit B is replayed on top of main's commit C, becoming B' with a new SHA. The history looks as if the feature was developed entirely after main's latest commit. This is easier to read but loses the fact that the feature and main commits happened concurrently.
graph TD subgraph "Before: Forked History" A[main: commit A] --> B[feature: commit B] A --> C[main: commit C] end subgraph "After Merge" D[main: commit A] --> E[feature: commit B] D --> F[main: commit C] E --> G[merge commit D] F --> G end subgraph "After Rebase" H[main: commit A] --> I[main: commit C] I --> J["feature: commit B' (rewritten)"] end
Rebasing onto Specific Branches with --onto
The git rebase --onto flag gives you fine-grained control over where your branch is rebased. Without --onto, rebasing moves the entire branch onto the new base (e.g., git rebase main rebases all commits in the current branch that are not in main onto the tip of main). With --onto, you can rebase only a subset of commits onto a completely different base.
Common use case: you started a feature branch from the wrong point — say you branched from main when you should have branched from release/v2.0. Instead of cherry-picking each commit, you can run:
git rebase --onto release/v2.0 main feature/checkout
This takes all commits on feature/checkout that are not in main and replays them on top of release/v2.0. It's a precise surgical operation.
Another scenario: you have a chain of branches (feature-A depends on feature-B). If feature-B has already been merged and you want to rebase feature-A directly onto main, you can use --onto to skip the intermediate base.
# io.thecodeforge — Rebase onto Specific Branch Using --onto # ───────────────────────────────────────────────────────────── # SCENARIO: You accidentally branched feature/checkout from main, # but it should be based on release/v2.0 which has different configs. # ───────────────────────────────────────────────────────────── # Step 1 — Check the current state git log --oneline --graph --all # * e7c12d3 (feature/checkout) Add checkout validation # * a1f84b9 Add shipping cost calculation # | * d9e0a44 (release/v2.0) Update tax rates for EU # | * 4e21d09 Add EU-specific checkout fields # |/ # * c7b83af (main) SECURITY: sanitize SQL inputs # Step 2 — Rebase feature/checkout onto release/v2.0, skipping main # The three-argument form: git rebase --onto <new_base> <old_base> <branch> # old_base is main (the current base), new_base is release/v2.0 # This replays ONLY commits that are not in main (the feature commits) git rebase --onto release/v2.0 main feature/checkout # Step 3 — Verify the new base git log --oneline --graph --all # * f5c12e7 (feature/checkout) Add checkout validation # * b4f84c1 Add shipping cost calculation # * d9e0a44 (release/v2.0) Update tax rates for EU # * 4e21d09 Add EU-specific checkout fields # * c7b83af (main) SECURITY: sanitize SQL inputs
Three Real-World Workflows — Which Approach Fits Each
Knowing the mechanics is half the battle. Knowing which to reach for in a given situation is what separates a senior engineer from someone who just memorised the commands.
Workflow 1 — Keeping your feature branch up to date: Use rebase. While you're developing in isolation, nobody else is working off your branch. Rebasing onto main daily keeps your branch current without cluttering the future merge commit with a tangle of catch-up merges inside your PR. Your pull request shows exactly your work, nothing else.
Workflow 2 — Landing a feature into main via pull request: Both strategies are used in industry. Teams that value linear history use 'Squash and Rebase' in GitHub/GitLab so the entire feature lands as one clean commit. Teams that value commit granularity and want to see the feature's internal development use a regular merge commit. Know your team's convention before you hit the merge button.
Workflow 3 — Merging a long-lived shared branch (e.g. a release branch back into main): Always use merge, never rebase. Release branches are shared by the whole team. Rebasing would rewrite commits that everyone already has locally, causing a synchronisation disaster. A merge commit here is not noise — it's a meaningful event marker that says 'release 2.4.0 was integrated on this date'.
# io.thecodeforge — Interactive Rebase Cleanup Before PR # ───────────────────────────────────────────────────────────── # SCENARIO: You've been working on a checkout feature. # Your commit history is messy with 'WIP' and 'fix typo' commits. # Before raising a PR, you want to clean it up into meaningful commits. # ───────────────────────────────────────────────────────────── # Step 1 — See what you're working with git log --oneline # 8a3f21c Fix typo in promo code validator # 7d91b4e WIP saving progress before standup # 6c82e10 Add promo code validation logic # 5b44a01 Add cart total calculation # 4e30f99 (main) Set up checkout module # Step 2 — Start interactive rebase for the last 4 commits # The SHA here is the commit BEFORE the ones you want to edit git rebase -i 4e30f99 # Git opens your editor showing: # pick 5b44a01 Add cart total calculation # pick 6c82e10 Add promo code validation logic # pick 7d91b4e WIP saving progress before standup # pick 8a3f21c Fix typo in promo code validator # Step 3 — Edit the file to squash the WIP and typo fix into the promo commit # Change 'pick' to 'squash' (or 's') for commits you want to fold in: # pick 5b44a01 Add cart total calculation # pick 6c82e10 Add promo code validation logic # squash 7d91b4e WIP saving progress before standup # squash 8a3f21c Fix typo in promo code validator # Save and close. Git opens another editor for the combined commit message. # Write a clean message: "Add promo code validation with edge case handling" # Step 4 — Verify the clean result git log --oneline
- squash: fold a commit into the previous one — combines changes, merges messages
- fixup: fold a commit into the previous one — combines changes, discards message
- reorder: move commits up or down in the editor to change the sequence
- reword: change a commit message without changing the code
Resolving Conflicts: How Merge and Rebase Differ Under Pressure
Both commands can hit conflicts when the same lines of code were changed in both branches. But the experience of resolving those conflicts is very different — and this catches a lot of developers off guard the first time they rebase a branch with multiple commits.
With merge, you get exactly one conflict-resolution session. Git combines everything in one shot and stops if it can't. You fix the conflicts, run git add, then git merge --continue (or git commit), and you're done.
With rebase, conflicts can appear multiple times — once for each commit being replayed. If you have five commits and the second one conflicts, you'll resolve it and run git rebase --continue, then potentially hit another conflict at the fourth commit. Each resolution is isolated to the changes in that specific commit, which is actually more precise but also more repetitive. If you're not prepared for this, it feels like you've broken something when the conflict reappears.
The nuclear escape hatch is git rebase --abort, which returns your branch to exactly the state it was in before you started the rebase. There's no equivalent 'undo' once a merge commit is created — you'd need git revert or git reset, both of which are more involved.
# io.thecodeforge — Handle Rebase Conflict # ───────────────────────────────────────────────────────────── # SCENARIO: Rebasing a feature branch that modifies the same config # file that main also modified — a conflict is guaranteed. # ───────────────────────────────────────────────────────────── # Start the rebase git rebase main # Auto-merging config/database.yml # CONFLICT (content): Merge conflict in run "git rebase --continue". # Step 1 — Open the conflicted file and look at what Git shows you cat config/database.yml # <<<<<<< HEAD (the tip of main, your new base) # pool_size: 10 # timeout: 5000 # ======= # pool_size: 25 # timeout: 3000 # >>>>>>> 3c9a11f (Update database pool size for auth service) # Step 2 — Decide what the correct value is and edit the file manually # Edit config/database.yml to the correct final state # Remove ALL conflict markers (<<<<<<<, =======, >>>>>>>) # Step 3 — Stage the resolved file (do NOT git commit here) git add config/database.yml # Step 4 — Tell rebase to continue replaying the remaining commits git rebase --continue # Git may open your editor to confirm the commit message — save and close. # Step 5 — Confirm the rebase completed cleanly git log --oneline --graph
Recovery Procedure: Upstream Branch Was Rebasing
If a teammate rebased and force-pushed a shared branch (like develop), your local branch is now based on outdated SHAs. The typical symptom is Your branch and 'origin/develop' have diverged. The wrong instinct is to merge — that creates a duplicate set of commits and a confusing history. The correct recovery is a hard reset to the rebased remote, then cherry-pick any local commits you had made on top of the old branch.
Recovery Steps: 1. Do NOT merge. Merging creates duplicate commits from the old base and the new base. 2. Fetch the latest remote state: git fetch origin 3. Align your local branch with the rebased remote: git reset --hard origin/develop. 4. If you had local commits on top of the old develop (commits you haven't pushed or that are unique to your local), recover them by finding their SHAs in git reflog and cherry-picking them: git cherry-pick <sha>. Or, if you had a feature branch based on the old develop, you can rebase that feature branch directly onto the new origin/develop: git checkout feature/your-feature && git rebase origin/develop. 5. After recovery, communicate with your team to ensure everyone is synchronized. No one should push before all have done the hard reset.
Prevention: Branch protection rules that block force-pushes to develop and main. If a rebase of a shared branch is absolutely necessary, it must be announced with a clear timeline and a plan for everyone to reset.
# io.thecodeforge — Recover from Upstream Rebase of Shared Branch # ───────────────────────────────────────────────────────────── # SCENARIO: Your teammate rebased develop and force-pushed. # You have no local work on develop, just need to get back in sync. # ───────────────────────────────────────────────────────────── # Step 1 — First, do NOT merge. Check the divergence: git status # On branch develop # Your branch and 'origin/develop' have diverged. # Step 2 — Fetch the rebased remote state git fetch origin # Step 3 — Align your local branch with the rebased remote # WARNING: This discards any local commits on develop that are not in the remote. # If you have unpushed commits on develop, find them first via reflog. git reset --hard origin/develop # ───────────────────────────────────────────────────────────── # If you had local commits (e.g., on a feature branch based on old develop): # ───────────────────────────────────────────────────────────── # Step 1 — Checkout your feature branch git checkout feature/my-feature # Step 2 — Rebase directly onto the new origin/develop # This replays your feature commits on top of the rebuilt develop git rebase origin/develop # Step 3 — Force-push your feature branch (only if you're the only one on it) git push --force-with-lease origin feature/my-feature
git pull when your local branch has diverged from the remote, Git will by default do a merge (depending on config). This creates a merge commit that includes all the old commits from the shared branch PLUS all the new rebased commits — effectively duplicating all the work. The proper fix is always a hard reset or a rebase onto the new remote state.Decision Guide: When to Use Merge vs Rebase
The choice between merge and rebase is not about personal preference — it's about branch type, team workflow, and what kind of history your project needs.
Use Merge When: - Integrating a shared branch (main, develop, release/*, hotfix). These branches have multiple contributors. Rebasing them would rewrite everyone's history. - Landing a large feature that has many commits and you want to preserve the internal development context. The merge commit acts as a bookmark. - Bringing a long-lived branch up to date with its base. Merge creates a single commit that records when the synchronization happened. - Working on a branch that multiple people are actively pushing to. Merge is the only safe way to integrate.
Use Rebase When: - Updating your private feature branch with changes from main. Your branch hasn't been pushed or others haven't pulled from it. - Clean up your branch's commit history before opening a pull request (interactive rebase). - You want a linear, easy-to-read git log that simplifies tools like git bisect and git log --oneline. - You're applying a sequence of patches from another branch (with --onto).
Philosophical Debate: Some teams advocate for rebase-only workflows because they produce a clean history. Others argue merge commits are essential for understanding the project's timeline. In practice, most mature teams use both: merge for shared branches and rebase for feature branches, with interactive rebase for PR preparation. The key is to establish a team convention and stick to it.
# io.thecodeforge — Decision Flow: Merge or Rebase? # Quick personal algorithm: # 1. Is this branch shared with other developers? # YES → merge # NO → go to 2 # 2. Is this branch long-lived (more than a week)? # YES → merge (preserve context) # NO → go to 3 # 3. Do you need to clean up commits before a PR? # YES → interactive rebase then push # NO → rebase onto target for linear history or merge if team convention
Conflict Resolution Dynamics — Why Rebase Makes You Suffer in Sequence
Merge and rebase both hit conflicts when the same lines change in different branches. But how they expose those conflicts is where the pain differs. Merge resolves everything in one giant batch. You fix conflicts once, in the merge commit, and move on. Rebase replays each commit one at a time. If your feature branch has seven commits and three of them touch the same file, you fix the same conflict three times. That isn't a bug. It's a feature. Each replay applies your changes against the base as it existed at that moment in history, giving you a cleaner final diff. But you pay for that cleanliness with repetitive conflict resolution. When you're under pressure, that repetition can destroy your focus. Senior engineers know that rebase conflicts are a signal your branch has been alive too long. Either rebase more frequently, or keep the branch short-lived. If you're stuck fixing conflicts seven times, you already lost the productivity battle. Default to merge for long-running branches. Use rebase when you can afford surgical precision.
// io.thecodeforge — devops tutorial // Merge conflict — one fix, done git checkout feature/payment-v2 git merge main # CONFLICT in src/PaymentProcessor.java # Fix it once, stage, commit # Rebase conflict — same conflict, three times git checkout feature/payment-v2 git rebase main # First commit: conflict in PaymentProcessor.java # fix, git add, git rebase --continue # Second commit: same conflict again # fix, git add, git rebase --continue # Third commit: same conflict, different context # fix, git add, git rebase --continue
Historical Fidelity vs. Readability — Pick Your Poison
A merge commit preserves everything: exactly when each commit was created, by whom, and in what order. That fidelity is gold for audits and forensics. You can trace a bug back to the exact moment a feature was integrated and see every intermediate state. But that comes at a cost. Your log fills with merge commits, branch crossings, and timestamps that tell a true story but an ugly one. Rebase throws that fidelity away. It rewrites timestamps, changes commit hashes, and creates a linear history that reads like a clean changelog. Every commit is a logical step forward. No noise. No context about parallel work. That sounds great until you try to figure out why a commit broke production six weeks ago. The merge history shows you the merge point and the exact integration timeline. The rebase history shows you a tidy line that hides the collaboration story. Senior teams don't pick one over the other. They pick merge for the shared branch where compliance and traceability matter, and rebase for their personal branches where they just want to ship clean code.
// io.thecodeforge — devops tutorial # Merge history: truthful but noisy git log --oneline main # 9a3f2e7 Merge branch 'feature/pci-update' # 1b8c4a9 Add PCI validations # 6d0f3b2 Merge branch 'bugfix/cors-config' # 4e2f1a0 Fix CORS wildcard header # a1b2c3d Merge branch 'release/v2.1' # Rebase history: clean but context-free git log --oneline main # 9a3f2e7 Add PCI validations # 1b8c4a9 Fix CORS wildcard header # 6d0f3b2 Bump version to 2.1 # 4e2f1a0 Update deployment config # a1b2c3d Initial service scaffold
git log --graph --oneline --all on merge-based repos to see the true branch topology. On rebase-based repos, that graph is a straight line — easier to read, but harder to debug.Hybrid Workflow: How to Merge Upstream and Rebase Downstream
Stop treating merge and rebase like they're mutually exclusive religions. In production, you use both — just not on the same branch. The hybrid model works because it respects branch ownership. Your feature branch is your sandbox. Rebasing it against main keeps your history linear and your commits clean before review. But when you pull in changes from a shared integration branch like develop or staging, you merge. Why? Because that history is shared. Rebasing it would rewrite commits your teammates already have, triggering the exact pain we warned about earlier. The pattern is simple: merge into shared branches from upstream, rebase your local feature branches to stay current. This gives you narrative clarity on your feature branch and historical fidelity on the integration branch. You get readable commit trails where they matter — your PR — and stable history where it matters — the team's shared baseline.
// io.thecodeforge — devops tutorial # Hybrid strategy: merge upstream, rebase downstream workflow: integration_branch: develop feature_branch: feat/logging-overhaul # Step 1: Rebase your feature onto latest develop before PR steps: - command: git checkout feat/logging-overhaul - command: git rebase develop # Step 2: Merge (not rebase) feature into develop - command: git checkout develop - command: git merge --no-ff feat/logging-overhaul # Rationale: # Rebase on feature keeps history clean for review. # Merge on develop preserves branch context for all devs.
git stash push -m 'wip'. Rebase, then git stash pop. Keeps your local edits intact without committing half-baked code.Merge-Centric Patterns: When You Commit to the Mess
Some teams embrace merge commits. Not because they're clean — they're not. But because the merge commit itself carries context. When you merge a feature branch into main with --no-ff, you force a merge commit even if a fast-forward is possible. That commit message becomes a timestamped boundary: 'Feature X landed here.' If your CI/CD pipeline tags builds by commit SHA, that merge commit is your deployment marker. Every commit in the feature branch stays exactly as it was — no rewriting, no rebase conflicts. This matters when auditors, or your future self, need to trace blame back to the exact line of code as it existed at merge time. The cost is a cluttered history. You'll see diamond patterns in git log --graph. If your team values accountability over aesthetics, this is your pattern. Just don't pretend it's pretty. Admit you're trading readability for traceability.
// io.thecodeforge — devops tutorial # Merge-centric workflow with forced merge commits pipeline: branches: main: - step: build-and-test trigger: merge - step: deploy-to-production trigger: on-merge-commit # Force a merge commit even on fast-forward merge_strategy: --no-ff # Tag the merge commit for deploy tracking post_merge: - command: git tag deploy-$(date +%Y%m%d-%H%M) - command: git push origin --tags # Benefit: every deploy maps to a visible merge commit. # Cost: history shows explicit branch topology.
--no-ff if it also runs automated squashing. You'll double-create merge commits and lose the ability to bisect cleanly.Rebase-Oriented Patterns: Narrative Clarity for Code Review
You're writing a story for your reviewer. Not a diary of 'fixed typo', 'removed log', 'added log back'. Rebase lets you edit that story before anyone reads it. In a rebase-oriented workflow, your feature branch is private until you open a PR. You rebase daily to pull in upstream changes, then use interactive rebase to squash fixup commits and reorder logical steps. The result? A linear, focused commit history that tells a coherent narrative: 'implement auth middleware', 'add rate limiting to auth', 'wire up error handling'. Each commit compiles and passes tests alone — because you verified during the rebase. Reviewers read this and approve faster. No noise. No 'oops' commits. The tradeoff: you cannot rebase after the PR is open if other people have pulled your branch. So you rebase before push, and you never force-push to a shared ref. That discipline is the cost of clean history.
// io.thecodeforge — devops tutorial # Rebase-oriented workflow for clean PR history feature_branch: feat/payment-gateway procedure: # Daily: rebase onto latest main - command: git checkout feat/payment-gateway - command: git rebase main # Pre-PR: interactive rebase to squash fixups - command: git rebase -i HEAD~10 # Final: force-push only to your feature branch - command: git push --force-with-lease origin feat/payment-gateway goal: "Every commit compiles and is self-contained." # Note: --force-with-lease prevents overwriting # remote changes you haven't seen.
Strategic Integration Approaches — Solve for Head, Not for Tail
Merge and rebase are tactical tools. Strategic integration is about how your team absorbs changes over time. Merging preserves a true record of parallel work but creates merge commits. Rebasing linearizes history but forces every downstream developer to rebase after you. The strategic choice depends on release cadence and branch lifespan. Short-lived feature branches (under a day) benefit from rebase because conflict resolution happens once, during the rebase, not during a merge commit that contains unrelated changes. Long-running integration branches like 'develop' or 'main' should never be rebased — they are shared truth. The winning strategy: merge into shared branches from upstream, rebase onto shared branches for your private work. This hybrid approach gives you linear readable history on features without polluting the shared branch's commit graph. Teams that pick a single strategy for all cases pay a cost: too many merges creates noise; too many rebases creates coordination failures.
// io.thecodeforge — devops tutorial // Strategic integration: hybrid rule // Merge upstream (shared), rebase downstream (private) team: integration: upstream: merge-only # main, develop, release downstream: rebase-only # feature branches rule: "Merge flows down. Rebase flows up." workflow: feature-start: command: "git checkout -b feature/xyz main" feature-sync: command: "git rebase main" feature-finish: command: "git merge --no-ff feature/xyz"
Tooling Ecosystem Considerations — Your CI/CD Pipeline Decides for You
// io.thecodeforge — devops tutorial // Tooling alignment: pick one github: merge_type: "squash" # linear history, loses granularity gitlab: merge_type: "rebase" # linear history, preserves commits bitbucket: merge_type: "no-ff" # non-linear, full graph constraint: ci_rebuild_on_rebase: true # all SHAs change pr_approval_invalidated: true # re-review required
Rebase on develop Branch: 6 Developers Lose 3 Hours Coordinating Recovery
git rebase main on the develop branch to remove merge commit clutter.
2. The rebase rewrote all commit SHAs on develop.
3. They force-pushed: git push --force origin develop.
4. Six teammates had feature branches that were branched off the old develop (with old SHAs).
5. On their next git fetch, their local origin/develop now pointed to the rebased commits (new SHAs).
6. Their feature branches were still based on the old develop (old SHAs).
7. Git saw the branches as diverged — the old commits and new commits had different parents.
8. One developer tried to merge origin/develop into their feature branch, creating a merge commit with duplicate changes.
9. The CI pipeline built from develop and deployed the duplicate-logic code.git fetch origin && git reset --hard origin/develop to align with the rebased remote.
2. The developer who merged the diverged branches had to git reset --hard to the commit before the merge and re-branch from the rebased develop.
3. Team rule: never rebase develop, main, release/*, or any branch that others have branched from.
4. Added branch protection on GitHub to prevent force-pushes to develop and main.
5. Documented the Golden Rule in the team wiki with a link to this incident.- The Golden Rule is non-negotiable: never rebase a branch that another developer has pulled. The moment a commit is shared, its SHA must not change.
- Force-pushing a rebased shared branch costs every downstream developer time to recover. One bad rebase can cost the team hours.
- Branch protection rules on GitHub/GitLab prevent force-pushes to protected branches. Configure them for main, develop, and release branches.
- If you accidentally rebase a shared branch, announce it immediately. Coordinate the recovery before anyone merges the diverged state.
git fetch origin to get the rebased remote state.
4. Hard-reset: git reset --hard origin/main to align with the rebased remote.
5. If you had local commits on top of the old branch: cherry-pick them onto the new base: git cherry-pick <old-commit-hash>.git add <file> and git rebase --continue.
3. Do NOT run git commit during a rebase — use git rebase --continue only.
4. If too complex: git rebase --abort to return to pre-rebase state.git push --force is safe.
3. If others have pulled: do NOT force-push. Coordinate with them first.
4. Prevention: only rebase commits that have not been pushed yet.git reflog to find the commit hash before the rebase.
3. Cherry-pick the lost commit: git cherry-pick <hash>.
4. If the entire rebase went wrong: git reset --hard ORIG_HEAD to return to pre-rebase state.git commit instead of git rebase --continue during conflict resolution.
2. The extra commit is now in your rebased chain.
3. Fix: git rebase -i and squash the extra commit into the correct parent.
4. Prevention: always use git rebase --continue during rebase conflict resolution.git fetch origin (get the rebased remote state)git reset --hard origin/main (align with rebased remote)git status (see conflicted files)git add <file> && git rebase --continue (resolve and continue)git log --oneline origin/main..HEAD (see your rebased commits)git log --oneline HEAD..origin/main (see remote-only commits)git reflog | grep 'rebase' (find the rebase operation and pre-rebase state)git cherry-pick <hash> (recover the dropped commit)git rebase -i HEAD~N (open interactive rebase for recent commits)squash the extra commit into its parent (change 'pick' to 'squash')| Feature / Aspect | git merge | git rebase |
|---|---|---|
| History shape | Non-linear — shows parallel development as a graph | Linear — appears as a single straight chain of commits |
| Creates new commits | Yes — one merge commit with two parents | Yes — new copies of every replayed commit with new SHAs |
| Conflict resolution | One session for the entire merge | One session per replayed commit that causes a conflict |
| Safe on shared branches | Yes — always safe | No — never rebase branches others are working from |
| Best for | Integrating long-lived or shared branches | Updating private feature branches and cleaning up before PRs |
| Reversibility | Undo with git revert of the merge commit | Abort mid-process with git rebase --abort; hard to undo after |
| Commit SHAs preserved | Yes — existing commits unchanged | No — all replayed commits get new SHAs |
| Readability of git log | Can get noisy on active repos | Clean and easy to scan linearly |
| When to avoid | When you want a pristine linear history | On public/shared branches like main, develop, release/* |
| Interactive mode | Not available | git rebase -i for powerful history editing |
| File | Command / Code | Purpose |
|---|---|---|
| 01_rebase_vs_merge.sh | git init && echo 'Initial' > file.txt && git add . && git commit -m 'Initial' | Rebase vs Merge |
| io | git log --oneline --graph --all | How Git Merge Works |
| io | git log --oneline --graph --all | How Git Rebase Works |
| io | graph TD | Visual Branch History |
| io | git log --oneline --graph --all | Rebasing onto Specific Branches with --onto |
| io | git log --oneline | Three Real-World Workflows |
| io | git rebase main | Resolving Conflicts |
| io | git status | Recovery Procedure |
| ConflictScenarios.yml | git checkout feature/payment-v2 | Conflict Resolution Dynamics |
| HistoryTradeoffs.yml | git log --oneline main | Historical Fidelity vs. Readability |
| hybrid-workflow.yml | workflow: | Hybrid Workflow |
| merge-centric-pipeline.yml | pipeline: | Merge-Centric Patterns |
| rebase-oriented-pr.yml | feature_branch: feat/payment-gateway | Rebase-Oriented Patterns |
| Strategy.yml | team: | Strategic Integration Approaches |
| ToolingConstraint.yml | github: | Tooling Ecosystem Considerations |
Key takeaways
main or develop; reserve rebase for local, unshared branches.git bisect, but only when used on private branches.Common mistakes to avoid
2 patternsRebasing a shared branch to clean up history
Running git commit instead of git rebase --continue during conflict resolution
Interview Questions on This Topic
Explain the Golden Rule of rebasing and why it's important.
Frequently Asked Questions
Never rebase a branch that another developer has pulled from. Once a commit is shared, its SHA must never change because rebasing rewrites commit hashes, creating divergent histories that force every collaborator to hard-reset their local copy.
Use merge for shared branches like main, develop, or long-lived feature branches where multiple people collaborate. Merge preserves the full context of parallel development and avoids the cost of rewriting history that breaks other developers' local copies.
Yes. Rebase is best reserved for local, unshared branches—cleaning up your own commit history before pushing, or keeping a feature branch up-to-date with main via git pull --rebase when you are the sole contributor.
Every other developer who has that branch will have a divergent history that Git cannot reconcile without manual intervention. They must delete their local copy, fetch the rewritten branch, and re-apply any uncommitted work, which can cost hours of team coordination.
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
That's Git. Mark it forged?
12 min read · try the examples if you haven't