Git Merge, Rebase & Squash — Part 6: Team Workflows and the Decision Guide

On this page
Abstract
Establishing systematic version control workflows is critical for development teams aiming to maintain a coherent, recoverable, and readable repository history. While individual developers must master repository mechanics, shared guidelines prevent destructive collisions and preserve integration context. Platform pull requests strictly map to foundational operations: a standard merge preserves the complete commit graph, a squash condenses a branch into a single commit, and a rebase rebuilds commits upon a new base to synthesize a linear timeline with altered hashes. To safely arbitrate timelines, practitioners must differentiate a reset, which retroactively shifts branch pointers, from a revert, which generates a new commit to negate past changes on shared branches without losing data. Furthermore, teams enforce history policies by configuring explicit behaviors like fast-forward merges, utilizing cherry-pick for surgical code backports, and applying the reflog to rescue seemingly lost work. This article translates these distinct operational mechanics into comprehensive decision frameworks for standardizing repository management.

This is the finale of the series. The previous five parts explained the mechanics: the commit graph and every kind of merge, squash merge, rebase, interactive rebase, and how to handle conflicts and merge strategies. Part 6 turns all of that into decisions — which operation to reach for in each situation, how GitHub's Pull Request buttons map onto the concepts you already know, and how a team can agree on a workflow that keeps its history clean and recoverable.
After this part you will be able to pick the right tool for any everyday Git situation from a single decision table, run a safe feature-branch workflow from first commit to merged PR, tell reset apart from revert and rebase, revert a merge commit correctly, and rescue work you thought you had lost with the reflog.
GitHub's three Pull Request merge methods
When you merge a Pull Request on GitHub you are usually offered three buttons. Each maps directly onto a concept from earlier parts.
Create a merge commit
Keeps every commit from the branch and adds a merge commit on top. The full development history of the PR stays visible on the target branch.
Squash and merge
Combines the entire Pull Request into a single commit on the target branch — one PR becomes one commit.
Rebase and merge
Recreates the PR's commits on top of the target branch with no merge commit. Because the commits are rebuilt on a new base, their commit hashes on the target branch will change — GitHub always recreates the commits, even when a fast-forward would have been possible.
"Rebase and merge" gives you the individual commits and a linear history — but remember the hashes are new, exactly as with a local rebase.
How cherry-pick fits in
Cherry-pick is not a merge, but it belongs in the same mental toolbox.
git cherry-pick <commit-hash>
It means:
Copy the changes from one commit and create them as a new commit on the current branch.
Before, starting from:
A---B---C main
\
D---E feature
On main, run:
git cherry-pick E
After:
Notice the new commit is E', a fresh copy — not the original E. Use cherry-pick when you want to:
- take a single bug-fix commit to production,
- backport a fix to another release branch,
- move one commit without merging the whole branch.
Do not use cherry-pick to pull in an entire feature branch in place of a real merge without a good reason — doing so produces duplicated history.
Reset is not the same as Rebase
Reset
Reset moves the branch pointer.
git reset --soft HEAD~1
git reset --mixed HEAD~1
git reset --hard HEAD~1
Rebase
Rebase replays commits onto a new base.
git rebase main
Both can rewrite history, but their purposes differ: reset repositions where a branch points, while rebase rebuilds the commits themselves on top of a different starting point.
Revert is not the same as Reset
Revert
Revert creates a new commit that undoes the effect of an older commit.
git revert <commit-hash>
It suits shared branches because it does not delete history.
Reset
Reset moves the pointer backwards and can make commits disappear from the branch.
git reset --hard HEAD~1
It suits local history that has not been shared yet.
Reverting a merge commit
A merge commit has more than one parent, so you must tell Git which parent is the mainline.
git revert -m 1 <merge-commit-hash>
Think of a three-way merge, where M has two parents:
In general, -m 1 means "treat the first parent as the main branch, then undo the changes that came in from the branch that was merged." Always inspect the parents first:
git show <merge-commit-hash>
What git pull actually does
By default:
git pull
is conceptually equivalent to:
git fetch
git merge
which may create a merge commit.
Pull with rebase
git pull --rebase
is equivalent to:
git fetch
git rebase origin/main
Pull that never creates a merge commit
git pull --ff-only
It is better to configure explicit behaviour than to leave it to chance.
Make pull always rebase
git config --global pull.rebase true
Make pull fast-forward only
git config --global pull.ff only
Pick one according to your team's workflow.
A recommended feature-branch workflow
Step 1 — update main
git switch main
git pull --ff-only
Step 2 — create a feature branch
git switch -c feature/login
Step 3 — work and commit
git add .
git commit -m "feat: add login form"
git add .
git commit -m "test: add login validation tests"
Step 4 — update the base before opening a PR
git fetch origin
git rebase origin/main
Resolve conflicts if any arise:
git add .
git rebase --continue
Step 5 — push
If you have not pushed before:
git push -u origin feature/login
If you had already pushed before the rebase:
git push --force-with-lease
Step 6 — merge the PR
Choose according to your team policy: Squash and merge, Rebase and merge, or a merge commit.
How to choose for each situation
Use a normal merge when
- the branch is shared with several people,
- you want to preserve the real history,
- the feature has commits that are meaningful on their own,
- you want the feature branch's boundary to be visible,
- it is a release branch or an integration branch.
git merge --no-ff feature/login
Use rebase when
- you want to bring a feature branch up to date with main,
- the branch is your own,
- you want a linear history,
- you want to tidy the commits before opening a PR.
git switch feature/login
git rebase main
Use squash merge when
- the feature has commits like
fix,fix again,oops, - you want one PR to become one commit,
- main needs to stay easy to read,
- you do not need to keep the fine-grained development history.
git switch main
git merge --squash feature/login
git commit -m "feat: add login system"
Use fast-forward only when
- you want a linear history,
- the feature has already been rebased,
- you want to prevent accidental merge commits.
git merge --ff-only feature/login
Use cherry-pick when
- you need only a specific commit,
- you are taking a bug fix to several release branches,
- you are backporting a security fix.
git cherry-pick abc1234
Decision table
The whole series distilled into a single lookup: find your situation on the left, use the approach on the right.
| Situation | Recommended approach |
|---|---|
| Private branch needs to catch up with main | git rebase main |
| Shared branch worked on by several people | Merge |
| PR has messy commits | Squash merge |
| Keep every commit but keep history linear | Rebase and merge |
| You want the feature boundary to be visible | merge --no-ff |
| You need only a single bug-fix commit | Cherry-pick |
| Production branch must undo a change | Revert |
| The latest local commit is wrong | Amend or interactive rebase |
| Merge commits are not allowed | --ff-only |
A policy for small teams
For a small team, this shape works well:
main
└── protected, no force push
feature/*
└── the author may rebase
Pull Request
└── use Squash and merge
The workflow:
git switch feature/my-feature
git fetch origin
git rebase origin/main
git push --force-with-lease
Then use Squash and merge. The result on main is one clean commit per PR:
One PR equals one commit. This suits SaaS products, web applications, and any team that wants an easy-to-read history.
A policy for projects that need detailed history
When you must keep the full development record, use a branch model like:
main
develop
feature/*
release/*
hotfix/*
and integrate with:
git merge --no-ff feature/x
The result:
- feature boundaries are clear,
- individual commits are preserved,
- merge commits mark the integration points.
This suits large systems, multiple release lines, work that must be auditable, and cases where you need to trace exactly which steps a feature went through.
Reading the Git graph
To see the graph clearly:
git log --graph --oneline --decorate --all
Example output:
* a8f9210 (HEAD -> main) Merge feature/login
|\
| * 73ab110 Add validation
| * 19c4d20 Add login form
* | 10fc991 Update dependencies
|/
* 61ad820 Initial commit
A handy alias:
git config --global alias.graph "log --graph --oneline --decorate --all"
Then simply run:
git graph
Recovering from mistakes
Git can usually still rescue you through the reflog.
git reflog
Example:
a123456 HEAD@{0}: rebase finished
b234567 HEAD@{1}: rebase started
c345678 HEAD@{2}: commit: working version
Recover the branch:
git branch recovery c345678
or:
git reset --hard c345678
Before using reset --hard, make sure you do not need any uncommitted changes.
One sentence per command
- Merge — joins two histories together without rewriting the original commits.
- Rebase — rebuilds commits on a new base, producing a linear history.
- Squash — combines many commits into one.
- Cherry-pick — copies a chosen commit and recreates it on the current branch.
- Revert — creates a new commit that undoes the effect of an old one.
- Reset — moves the branch pointer backwards.
The most important rules to remember
Merge preserves history. Rebase rewrites history. Squash compresses history.
And:
You can rebase local/private history, but avoid rebasing shared, public history.
For a workflow that is both safe and clean:
git switch feature/my-feature
git fetch origin
git rebase origin/main
git push --force-with-lease
Then use Squash and merge once the PR has passed review.
Key takeaways
- GitHub's three PR buttons map onto concepts you already know: Create a merge commit (keep everything), Squash and merge (one PR, one commit), Rebase and merge (linear, but new hashes).
- Keep three operations distinct: reset moves a pointer, rebase replays commits on a new base, revert adds a new commit that undoes an old one — and revert is the safe choice on shared branches.
- Revert a merge commit with
git revert -m 1 <hash>, and always check the parents withgit showfirst. - Make
git pulldeterministic withpull.rebaseorpull.ff only, then use the decision table to choose merge vs rebase vs squash vs cherry-pick per situation. - Almost nothing is truly lost:
git reflogplus a recovery branch orreset --hardcan rescue work after a bad rebase or reset.