When a PR is opened on GitHub, there are three branches/commit references involved:
Branch/Commit Ref | Description |
---|---|
base | The branch into which changes will be merged |
head | The PR branch which contains changes to be merged into base |
merge-base | The most recent common ancestor commit of base and head |
In all cases, the goal of the PR is to incorporate all changes from the commit range merge-base..head into base. Let's call the new commit (generated as a result of the merge) base' (which we pronounce as base prime).
Let's consider an example where commit C is base:HEAD and commit D is head:HEAD.
-A---B---C (base)
\
`-D---E (head)
In this case, the merge-base is commit A and the goal is to merge changes from D and E on top of commit C. To do this, one of three strategies is used:
-
Merge
In this case, a merge commit F is created with parents C and E.
-A---B---C---F (base') \ / `-D---E-' (head)
-
Squash-Merge
In this case, commits D and E are squashed into a single commit G (represented by the branch squash) and then a merge commit F is created with parents C and G.
-A---B---C---F (base') |\ / | `-G---*-' (squash) \ `-D---E (head)
-
Rebase
In this case, commits D and E get mapped to commits D' and E' on top of C (since they may be modified during rebasing).
-A---B---C---D'--E' (base') \ `-D---E (head)
To avoid merge conflicts, a very common situation is that PRs are created where the merge-base is the base commit, i.e., we have a situation like:
-A---B---C (base)
\
`-D---E (head)
In this common case, there is actually no need to create a merge commit since all of the desired changes are already encapsulated in commit E. However, what GitHub actually does is the following:
-
Merge
In this case, a useless merge commit F is create with parents C and E whose contents are equal to E.
-A---B---C---*---*---F (base') \ / `-D---E-' (head)
This is because, in this case, GitHub merges with git's
--no-ff
option. -
Squash-Merge
In this case, commits D and E are squashed into a single commit G and then base branch is fast-forwarded to commit G.
-A---B---C---G (base', squash) \ `-D---E (head)
-
Rebase
In this case, base branch is fast-forwarded to the head branch so both branches become identical:
-A---B---C---D---E (head, base')
Note: In this special case, we have the property that the contents of base' and head are identical.
Suppose we have the following case:
-A---B---C (base)
\
`-D---E (head)
If GitHub cannot apply one of its strategies due to merge conflicts, it does one of the following:
- gives up (it does this when conflicts are detected during a rebase or if detected conflicts are too complex);
- presents the user with a merge-conflict editor.
If the user chooses to use the merge-conflict editor, once the conflicts are resolved, the resolved conflicts will be merged into the head branch, i.e., we will have:
-A---B---C (base)
\ \
`-D---E-`-F (head')
This is done, as far as I can tell, because, ultimately, the head branch is the one which must pass reviewer/CI validation, so all changes must become part of the head branch for testing. If this new commit head' passes CI validation and obtains the necessary approvals, then it can be merged into the base branch, using either the merge or squash-merge strategy strategies depicted above (as GitHub doesn't perform conflict resolution for rebases).
- https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges
- https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/merging-a-pull-request
- https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/about-merge-conflicts
- https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts