Generally speaking, we have a common workflow when creating a PR:
- Create a branch, such as feature
- Submit the PR
- Have someone review it
- Make changes based on the review comments
- Merge the PR into the main branch
However, the feature branch may have multiple commits, so how to merge it into the main branch becomes a problem. GitHub provides three options:
Create a merge commit#
Create a "merge commit" that connects the history of two branches
We can simulate this process locally. First, create three commits in the main branch. You can see the commit history using git log --oneline --graph
:
* b8472e4 (HEAD -> main) add c.txt
* 2db7159 add b.txt
* b5b52d8 add a.txt
Then, create two commits in the new branch git checkout -b feature
:
* 9770b82 (HEAD -> feature) add e.txt
* 4e65cc4 add d.txt
* b8472e4 (main) add c.txt
* 2db7159 add b.txt
* b5b52d8 add a.txt
Switch back to the main branch and merge using the command:
git checkout main
git merge --no-ff feature
View the records again:
* 7da24fd (HEAD -> main) Merge branch 'feature' into main
|\
| * 9770b82 (feature) add e.txt
| * 4e65cc4 add d.txt
|/
* b8472e4 add c.txt
* 2db7159 add b.txt
* b5b52d8 add a.txt
As you can see, GitHub uses the default option git merge --no-ff
behind the scenes to create a merge commit that connects the history of the two branches.
The advantage of this method is that it preserves all the commit history and individual time points, but the disadvantage is that it can become messy with multiple or long-term branches.
Squash and merge#
Replace all commits in the branch with a single commit before merging
Let's simulate it locally again. First, git reset --hard b8472e4
to go back before the merge:
* b8472e4 (HEAD -> main) add c.txt
* 2db7159 add b.txt
* b5b52d8 add a.txt
Use git merge --squash feature
to squash the commits, then write an appropriate commit message.
Finally, the commit history in the main branch is as follows:
* 52c548d (HEAD -> main) add d.txt and e.txt
* b8472e4 add c.txt
* 2db7159 add b.txt
* b5b52d8 add a.txt
The advantage of this method is that the commit history in the main branch is clear and linear, with one commit solving one feature. However, the disadvantage is that it loses the context information of the merge and individual commits, and it is even impossible to know whether this branch was merged or not.
Another method of Squash#
Use the interactive rebase command for more detailed operations:
git checkout feature
git rebase -i main
Choose squash to merge the subsequent commits into the previous commit:
pick 4e65cc4 add d.txt
squash 9770b82 add e.txt
Write an appropriate commit message. At this point, the commit history in the feature branch becomes:
* f37e60f (HEAD -> feature) add d.txt and e.txt
* b8472e4 (main) add c.txt
* 2db7159 add b.txt
* b5b52d8 add a.txt
Finally, go back to the main branch and merge (of course, you can also use cherry-pick to merge individual or combined commits):
git checkout main
git merge feature
Bonus#
How to recover from these operations that mess up the commit history?
git reflog
# You will see all the git operations you have done, find the previous reset step that messed up and reset to it
git reset HEAD@{index}
Rebase and merge#
All commits will be added and merged to the "top" of the main branch
No image, but you can understand from the description that the final result in the main branch should be:
Restore the branch status and then perform the following operations:
git checkout feature
git rebase main
git checkout main
git merge feature
After completion, the commit history in the main branch is as follows:
* 9770b82 (HEAD -> main, feature) add e.txt
* 4e65cc4 add d.txt
* b8472e4 add c.txt
* 2db7159 add b.txt
* b5b52d8 add a.txt
The advantage is that the commit history in the main branch is clear and linear, without unnecessary merge commits, and the commit history of other branches will be directly saved in the main branch records.
The disadvantage is similar to squash merge, and more small commits may affect the attention to the big picture.
Recommendation#
All choices are trade-offs, and you should consider these key factors:
- Team preference
- Commit history: complete or clear?
- Smaller commits or complete feature commits
- Long-term running branches or not
- ...
I have only one recommendation: make more commits, write more PRs, don't pile up code, smaller code snippets mean easier code review, simpler unit testing, and code that adheres to the single responsibility principle.