The GitHub user interface nudges you towards a certain workflow.
- Create a feature branch
- Open a pull request
- Have others review the pull request
- Make adjustments to address the review comments
- Merge: integrate the work from the feature branch into the main branch
Teams that choose this workflow may decide what happens when pressing the “Merge” button in the last step. Having used all of the currently available options on GitHub (and Gitlab, and Bitbucket) on different projects, I’d like to share arguments in favour of each one.
This one merges all of the commits into the base branch. GitHub lists alternative, manual instructions that shed some light on the underlying process:
Step 1: Clone the repository or update your local repository with the latest changes.
git pull origin main
Step 2: Switch to the base branch of the pull request.
git checkout main
Step 3: Merge the head branch into the base branch.
git merge name-of-the-feature-branch
Step 4: Push the changes.
git push -u origin main
A key difference is that using “Create a merge commit” always creates a merge commit. So the merge step is more like:
git merge --no-ff your-branch
Check this merge commit in the Rails repository for example. The commit message title is
Merge pull request #47902 from zzak/re-47365
and the message body
Add link to security guide for CSRF from JS token part
That description corresponds to the title of the associated pull request.
If you use
git merge --no-ff yourself, you are free to write the message as you wish.
Merge commits are effective when you want to keep the individual commits in the history, but show where development of a feature starts and ends.
Merge pull request #4 from redacted/playlists-pagination Only load 5 playlists at once Paginate user playlists Merge pull request #3 from redacted/automatic-deployment Document automatic deployments Automatically deploy to production on successful staging deployment Automatically deploy to staging on merge
This works best if you rebase the feature branch on the main branch just before merging. Otherwise, the history may not read as well depending on when the commits are authored and the branches merged:
Merge pull request #4 from redacted/playlists-pagination Merge pull request #3 from redacted/automatic-deployment Document automatic deployments Automatically deploy to production on successful staging deployment Only load 5 playlists at once Paginate user playlists Automatically deploy to staging on merge
Sometimes you read the git history in search of specific changes and
git blame doesn’t help (“What was the commit where we deleted this feature?”). You use something like
git log --oneline. All of the merge commits are noise in that case.
Merge commits irritate me particularly during code review. If the pull request author has been using
git merge (or GitHub’s “update branch” button on the pull request page) to regularly bring changes from the main branch back into theirs, I have a hard time understanding their train of thought. Please use
git rebase to keep on top of updates from the main branch. Keep you history tidy, even splitting commits if it will make review easier.
I have used merge commits successfully to denote blocks of changes like in the example above. In hindsight, tagging commits on release would have provided a similar benefit, assuming each pull request is deployed immediately after merging.
This combines all commits from the branch into a single one. The combined commit is then added “on top” of the commits from the
You could achieve the same manually by first doing an interactive rebase:
git fetch git checkout feature-branch git rebase -i origin/main
…indicating you want all commits to be squashed into the first one
pick 923899 First commit in the branch squash 19384 Other commit squash 10ab9 Last commit
…writing an appropriate combined commit message
git checkout main git merge feature-branch
(Alternatively, one could switch to the
main branch and cherry-pick the single, combined commit.)
The nice thing with GitHub is that it also automatically adds a reference to the pull request number at the end of the commit title.
Automate deployment (#3)
On the GitHub website,
#3 would be a link to the related pull request.
“Squash and merge” works well if you don’t care about the back and forth that lead to the final implementation shipped with the pull request.
If you do care about the individual commits, perhaps you would prefer combine and reorder commits regularly until your pull request is ready and then rebase and merge.
The commits in a pull request should be related to each other (otherwise why are you doing everything in a single pull request?). However, grouping everything in a single commit when merging might make it harder to debug later. The value of self-contained, well-named and well-described commits is that they capture your expertise in the moment. Have you been disappointed by looking for context on a line of code and finding a huge commit that makes no mention of the aspect you are researching? I have. I’ve also experienced the joy of finding an explanation in a commit message, even when that change is overall insignificant at the scale of the pull request where it was introduced.
Here is another example. Consider the advice about refactoring in preparation for adding new code:
For each desired change, make the change easy (warning: this may be hard), then make the easy change.
The refactoring is a safe change, because it does not modify the external behaviour of the system. The new feature that comes on top of that might be riskier. If you squash the commits, but later need to revert, you undo the risky change and the refactoring. If you want to keep some of the changes you now need to extract them from the single “squashed” commit.
The rebase and merge strategy is a single-button shortcut for a workflow like the following:
git fetch git checkout feature-branch git rebase origin/main git checkout main git merge feature-branch git push
The individual commits from the branch are preserved and the commits from the branch
feature-branch are added “on top” of the commits from the
main branch. The merge step is a “fast forward” step: no merge commit is necessary.
3rd commit on feature-branch 2nd commit on feature-branch 1st commit on feature-branch 2nd commit on main 1st commit on main
“Rebase and merge” serves you best when you want to keep each individual commit in the final history, but consider merge commits noise.
In opposition to the drawbacks of “squash and merge”, many small commits can distract from the bigger picture.
Do you use commits as checkpoints? Consider combining some of them to tell a story.
Do you have too many commits? Perhaps the changes should be split in multiple pull requests.
All three strategies we saw have trade-offs.
Your team may value a tidy git history, but it may also value the current state of the code rather than how it got there.
You may value small commits, or you may prefer all-encompassing commits that implement an entire feature at once.
What I know is that if you care about the git history one way or another, it’s best to keep a consistent merge strategy for everyone in the team. Otherwise, everyone is unhappy.