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.
Create a merge commit
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.
Use cases for merge commits
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
Drawbacks of merge commits
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.
Squash and merge
This combines all commits from the branch into a single one. The combined commit is then added “on top” of the commits from the main
branch.
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
…and merging
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.
For example, this commit in the Administrate repository is the result of a pull request containing multiple commits.
Uses cases for squash and merge
“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.
Drawbacks of squashing everything
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.
Rebase and merge
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
Consider this flightdeck pull request, for example. Its 3 commits (8d37163
, 139c284
, d5e8b32
) have all been preserved in the main branch after merging.
Use cases for rebase and merge
“Rebase and merge” serves you best when you want to keep each individual commit in the final history, but consider merge commits noise.
Drawbacks of rebase and merge
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.
Decide and stick to a strategy
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.