Git Branching — Rebase on Shared Branch Lost 3 Hours
12 engineers got 'Your branch and origin/develop have diverged' after a force push.
20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.
- ✓Solid grasp of DevOps fundamentals
- ✓Comfortable with command-line tools
- ✓Basic Linux administration knowledge
git checkout -b feature/name— create and switch to a new branchgit merge feature/x— integrate branch changes into current branchgit rebase main— replay your commits on top of main's latest state--no-ff— force a merge commit even when fast-forward is possible
Think of a Git repository as a book being written by multiple authors. A branch is like each author working on their own photocopy of the latest chapter. They can write freely without interfering with each other. When they're done, they bring their pages back to the original book — that's a merge.
Rebase is different: instead of stapling your pages to the end, you take your pages and rewrite them as if you started writing after the latest page in the original book. The content is the same, but the page numbers change. If someone already wrote down your old page numbers, their references break.
Branching enables parallel development by isolating changes into independent lines of history. Each branch is a pointer to a commit — creating one costs almost nothing in Git because no files are copied.
The merge vs rebase decision affects how your repository's history reads during code review and debugging. Merge preserves the exact timeline of how work happened. Rebase creates a clean linear narrative. Both are used in production teams — the choice depends on your team's workflow and tooling.
Common misconceptions: that rebase is always better because it produces cleaner history, that fast-forward merges are always preferable, and that branches are expensive to create. Understanding the trade-offs prevents the force-push incidents and history corruption that plague teams adopting Git without training.
What Is a Git Branch — The Fundamentals
A Git branch is a lightweight, movable pointer to a specific commit. When you create a branch, Git writes a 41-byte file (40-character SHA + newline) — that's it. There is no copying of files or directories, no duplication of history.
This simplicity makes Git branching radically different from older VCS branching. In SVN, creating a branch copies the entire repository, taking seconds or minutes. In Git, it's instant.
How branching works: - Starting point: main points to commit M3. - git branch feature creates a pointer feature also pointing to M3. - git switch feature moves HEAD to the feature pointer. - New commits advance only the feature pointer. main stays at M3. - git merge feature creates a merge commit that joins both lines of history.
Merge vs Rebase: - git merge feature-x creates a merge commit joining the two branch histories. Preserves the exact timeline. - git rebase main rewrites your branch's commits as if they started from main's current tip. Linear history, but rewrites SHAs.
The critical rule: never rebase commits that have been pushed to a shared branch. Rebasing rewrites commit hashes, breaking every teammate's local references.
Why Rebasing a Shared Branch Is a Team-Wide Time Bomb
Git branching and merging is the mechanism for diverging and reunifying lines of development. A branch is a movable pointer to a commit; merging creates a new commit that joins two histories, while rebasing rewrites history by replaying commits onto a new base. The core mechanic is that merging preserves topology, rebasing linearizes it — at the cost of changing commit hashes.
In practice, merging is safe for shared branches because it never alters existing commits. Rebasing, however, replaces old commits with new ones (different SHA-1 hashes), so any collaborator who has pulled the pre-rebase branch now has a divergent history. Git will detect the conflict and force a merge or another rebase, creating duplicate commits and a tangled graph. This is not a theoretical problem — it's a deterministic failure mode.
Use rebase only on local or private branches before pushing. On shared branches, always merge (or use merge commits) to maintain a single source of truth. The cost of cleaning up a rebased shared branch scales linearly with the number of collaborators — three devs can lose half a day each untangling the mess.
Creating and Managing Branches
A Git branch is a pointer to a commit. Creating a branch does not copy files — it creates a 41-byte reference file in .git/refs/heads/. This is why branches are cheap: you can create hundreds without meaningful storage or performance cost.
The pointer moves forward automatically when you make new commits on the branch. When you switch branches (git checkout or git switch), Git updates your working directory to match the commit the branch points to.
Branch naming conventions matter for tooling. Most CI systems, PR platforms, and branch protection rules use pattern matching (feature/, release/, hotfix/*). Consistent naming enables automated workflows — branch protection rules, auto-deletion after merge, and CI trigger filters all depend on naming patterns.
- Creating a branch: one 41-byte file in .git/refs/heads/
- Switching branches: Git updates the working directory to match the target commit
- Deleting a branch: removes the pointer file. The commits remain until garbage-collected.
- Branch naming patterns enable CI/CD automation: feature/, release/, hotfix/*
git branch --no-merged main to find branches that haven't been integrated yet. Auto-delete merged branches to keep the remote clean.Merge vs Rebase
Merge and rebase both integrate changes from one branch into another. They produce different histories, and the choice affects code review, debugging, and collaboration.
Merge creates a merge commit — a special commit with two parents. It preserves the exact history of how work happened: when the branch was created, what commits were made on it, and when it was integrated. The history is a DAG (directed acyclic graph), not a straight line.
Rebase replays your commits on top of another branch. Each commit is cherry-picked onto the new base, creating new commits with new SHAs. The result is a linear history — no merge commits, no branching visible in git log. But the original commit SHAs are gone, which breaks any reference to them.
The golden rule: never rebase commits that have been pushed to a shared branch. Rebasing rewrites commit SHAs. Every teammate who pulled the old SHAs now has references to commits that no longer exist on the remote.
- Merge: original commits untouched, merge commit added. DAG history.
- Rebase: all commits replayed with new SHAs. Linear history.
- Squash merge: collapses feature commits into one. Simplest history, loses granularity.
- The golden rule: never rebase commits that others have pulled.
git log --first-parent main shows a clean integration timeline — each merge commit represents a feature. With rebase, all commits appear linearly, mixing feature work with bug fixes. This makes git bisect more powerful (finer granularity) but git log harder to read. Teams using rebase should enforce squash-before-rebase to keep the linear history readable. Teams using merge should use --no-ff to preserve branch grouping in the history.Resolving Merge Conflicts
Merge conflicts occur when both branches modify the same lines of a file, or when one branch deletes a file the other modifies. Git cannot automatically decide which version to keep — it marks the file with conflict markers and pauses the merge.
Conflict markers show three versions: the current branch's version (<<<<<<< HEAD), a separator (=======), and the incoming branch's version (>>>>>>> feature/branch). Your job is to edit the file to produce the correct result, removing the markers.
Resolving conflicts is not just about picking one version over the other. Often the correct resolution combines both changes — for example, if both branches added different imports to the same file, you need both imports in the resolved version.
After resolving all conflicts, stage the files and commit. Git creates the merge commit automatically if all conflicts are resolved.
- Conflict markers show three versions: yours, theirs, and the common ancestor
- Resolving often requires combining both changes, not picking one
- After resolving: always compile and test before committing the merge
- Frequent conflicts in the same files indicate the code needs refactoring into separate modules
git merge --abort to cancel. After resolving, always compile and test — conflict resolution is a common source of regression bugs.Fast-Forward vs 3-Way Merge
A fast-forward merge happens when the target branch's HEAD is a direct ancestor of the source branch. Git simply moves the target pointer forward — no merge commit is created. The history remains linear.
A 3-way merge happens when both branches have diverged — each has commits the other doesn't. Git creates a merge commit with two parents, combining the changes from both branches. This requires finding the common ancestor (merge base) and computing the diff from that point.
The --no-ff flag forces a merge commit even when fast-forward is possible. This is useful for traceability: git log --first-parent main shows only merge commits, giving a clean integration timeline. Without --no-ff, feature branch commits appear directly in main's history with no grouping.
- Fast-forward: no merge commit, linear history, pointer just moves forward
- 3-way merge: merge commit with two parents, DAG history, content combined
- --no-ff forces a merge commit for traceability even when fast-forward is possible
- git log --first-parent main shows only the integration timeline (merge commits)
git log --first-parent main that shows exactly when features were integrated. Teams that allow fast-forward get a linear history that's easier to read with git log but harder to trace which commits belong to which feature. For release management and changelog generation, --no-ff is superior because each merge commit represents a complete feature. For simple projects with few contributors, fast-forward is simpler and sufficient.--no-ff to force merge commits for traceability. git log --first-parent main shows only the integration timeline when using --no-ff merges.Branch Strategies: GitFlow, GitHub Flow, and Trunk-Based Development
Branch strategy defines how your team uses branches for development, releases, and hotfixes. The three dominant strategies are GitFlow, GitHub Flow, and trunk-based development. Each has different trade-offs for release cadence, code stability, and operational complexity.
GitFlow uses long-lived branches (main, develop) and short-lived branches (feature, release, hotfix). It's designed for scheduled releases with strict version management. The overhead is significant — multiple branch types, merge ordering rules, and release branch management.
GitHub Flow is simpler: one long-lived branch (main), short-lived feature branches, and deploy-from-main. Every merge strong CI/CD and feature flags for incomplete work.
Trunk-based development uses very short-lived branches (hours, not days) or direct commits to main. Feature flags control incomplete work. It maximizes integration frequency and minimizes merge conflicts but requires mature CI/CD and feature flag infrastructure.
- GitFlow: main + develop + feature/release/hotfix branches. Scheduled releases. High overhead.
- GitHub Flow: main + short-lived feature branches. Deploy on merge. Simple.
- Trunk-based: main + very short-lived branches (hours). Feature flags. Maximum integration frequency.
- Most teams at scale use trunk-based development with feature flags.
Pull Requests Are the Gate, Not the Goal
Stop treating pull requests like a rubber stamp.
The PR is where you enforce code quality, run automated checks, and fucking document why that hotfix exists. If your team merges to main without a PR because "it's just a small change", you're building a tech debt mountain.
Here's the hard truth: PRs exist to catch mistakes before they hit production. That means your CI/CD pipeline should be running full test suites, linting, and security scans on every PR before merge. If it doesn't, you're shipping with a blindfold on.
Senior devs don't approve PRs because the code compiles. They approve because the logic is sound, the tests cover edge cases, and the commit messages tell the story. If you can't justify every line in your PR, it's not ready.
Your main branch should be sacred. Every merge via PR is a conscious decision to move the needle forward. Treat it like one.
Release Branches: The Only Safety Net You'll Thank Me For
Feature branches are for development. Release branches are for sanity.
When you're sprinting toward a release, the last thing you need is someone merging a half-baked feature into main and breaking the entire deploy. Release branches isolate the exact code that goes to production, allowing hotfixes to land without dragging in unfinished work.
Here's the workflow: when you're ready to ship, cut a release branch from develop or the feature freeze point. Name it release/v1.2.3. All QA fixes and last-minute patches go here. Meanwhile, main keeps moving forward with the next iteration's work.
After release, the critical step seniors hammer home: merge the release branch back into both main and develop. Why? Because otherwise, the release fixes disappear into branch limbo. The next developer who branches from develop will be missing those patches, and you'll relive the same merge conflict hell.
Tags are for snapshots. Release branches are for survival.
Rebase on Shared Branch: 12 Engineers Lose 3 Hours of Work
git rebase main on the develop branch to linearize 8 merge commits.
2. This rewrote all 8 commit SHAs. The new commits had the same content but different hashes.
3. git push origin develop was rejected (non-fast-forward). The engineer used git push --force.
4. The remote develop now pointed to the rebased commits. The old 8 commits were orphaned on the remote.
5. Twelve engineers had the old commits in their local develop. Their next git pull showed divergence.
6. Three engineers had feature branches with parents pointing to old develop SHAs. Those parent references were now dangling — their branches were effectively orphaned from develop's history.
7. CI webhook payloads referenced the old SHA of the last pre-rebase commit, which no longer existed on the remote.git reset --hard <pre-rebase-sha> && git push origin develop --force-with-lease.
3. For the 12 engineers with diverged local develop: git fetch origin && git reset --hard origin/develop.
4. For the 3 engineers with orphaned feature branches: used git rebase --onto origin/develop <old-parent> <branch-tip> to replay their feature commits on top of the restored develop.
5. Re-triggered CI manually.
6. Added branch protection rules requiring PR reviews for develop — preventing direct pushes entirely.- Never rebase a branch that other people pull from. The golden rule exists because rewritten SHAs break every clone that pulled the old history.
- --force is never acceptable on shared branches. --force-with-lease would have been irrelevant here (nobody else pushed), but normalizing --force creates dangerous habits.
- Branch protection rules that prevent direct pushes to shared branches would have prevented this entirely.
- Feature branches with parent pointers to specific SHAs are fragile. Always rebase feature branches onto the latest develop before merging, rather than relying on fixed parent SHAs.
git fetch origingit reset --hard origin/<branch>| File | Command / Code | Purpose |
|---|---|---|
| 01_branch_basics.sh | git init branch-demo && cd branch-demo | What Is a Git Branch |
| io | git switch -c feature/payment-retry | Creating and Managing Branches |
| io | git checkout main | Merge vs Rebase |
| io | git merge feature/payment-retry | Resolving Merge Conflicts |
| io | git checkout main | Fast-Forward vs 3-Way Merge |
| io | git checkout develop | Branch Strategies |
| PR-GatePipeline.yml | name: PR-Gate | Pull Requests Are the Gate, Not the Goal |
| ReleaseBranchWorkflow.yml | git checkout develop | Release Branches |
Key takeaways
Interview Questions on This Topic
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.
That's Git. Mark it forged?
5 min read · try the examples if you haven't