Git & GitHub · lesson 7 of 12
Also part of: DevOps
PR Workflow: Rebasing, Squash Merge, and Merge Commits
Sync a feature branch with rebase instead of merge, then compare squash-merge and --no-ff merge-commit history side by side with real git log output.
By SkillByExample · Updated
The mechanics from earlier lessons — branch, merge, conflict, rebase — are exactly what a pull-request workflow is built from. This lesson uses two real merge strategies on two real branches so the resulting history can be compared directly.
1) Sync a PR branch onto main with rebase
$ # ...create feature/mathlib-docs, commit "Document multiply()"...
$ # ...meanwhile, main gains "Add mathlib.mod"...
$ git checkout feature/mathlib-docs
$ git rebase main
Successfully rebased and updated refs/heads/feature/mathlib-docs.
$ git log --oneline --graph -3
* f89e27e Document multiply()
* c3f2b20 Add mathlib.mod
* 0b276a2 Add mathlib.divide
The branch’s one commit now sits directly on top of main’s latest tip, with a brand
new hash (f89e27e) — same conflict-resolution mechanics as an interactive rebase,
just against main instead of squashing your own commits.
2) Squash-merge collapses the branch into one commit
$ git checkout main
$ git merge --squash feature/mathlib-docs
Updating c3f2b20..f89e27e
Fast-forward
Squash commit -- not updating HEAD
mathlib.mjs | 1 +
1 file changed, 1 insertion(+)
$ git commit -m "Document multiply() (#42)"
$ git log --oneline --graph -4
* c8217ef Document multiply() (#42)
* c3f2b20 Add mathlib.mod
* 0b276a2 Add mathlib.divide
* f70cf08 Refactor add for readability
--squash stages the branch’s combined diff but deliberately doesn’t commit — that’s
what “not updating HEAD” means. One git commit later, main’s history has exactly
one new, linear commit. feature/mathlib-docs itself and its original commit
(f89e27e) still exist, just no longer referenced from main.
3) A —no-ff merge keeps the branch’s commits, visibly
$ # ...on a second branch, feature/mathlib-readme, one commit "Add README"...
$ git checkout main
$ git merge --no-ff feature/mathlib-readme -m "Merge feature/mathlib-readme"
$ git log --oneline --graph -3
* 16e3bf5 Merge feature/mathlib-readme
|\
| * 3efe520 Add README
|/
* c8217ef Document multiply() (#42)
--no-ff forces a real merge commit (two parents) even though a fast-forward was
possible — the opposite choice from squash. 3efe520 stays visible in main’s log,
reachable through the merge commit, which is the tradeoff: full branch history in
exchange for a less linear graph.
Practice
-
Run
git log --oneline(no--graph) after both merges above and compare it togit log --oneline --graph. Notice the squashed commit and the merge commit both appear as single lines either way —--graphis what reveals the branch structure. -
Repeat the squash-merge steps but this time use
git merge --squashon a branch with three commits. Confirm all three collapse into the one commit you make — not three separate ones.Solution
git merge --squashalways stages the entire combined diff between the branches’ common ancestor and the branch tip, regardless of how many commits produced it. The commit count on the source branch never affects the result: it’s always one commit. -
After a
--no-ffmerge, rungit log --first-parent mainto see onlymain’s own commit sequence with each merge collapsed to one line — the view most PR-based teams actually read day to day. -
Force a real conflict during a rebase (not a merge) by editing the same line on both
mainand a feature branch, thengit rebase main. Resolve it withgit add <file>followed bygit rebase --continue— note the command name differs from a merge conflict’sgit commit. -
After rebasing a branch that was already pushed, run
git push(without--force) and read the rejection. Then usegit push --force-with-leaseand explain why--force-with-leaseis safer than plain--forcefor a shared branch.