Last active
September 6, 2024 16:13
-
-
Save sellout/d05c5750e9a24161e85694d6b546c14d to your computer and use it in GitHub Desktop.
An implementation of the merge strategy I’ve tried to use manually
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
## FIXME: This hasn’t been tested yet – it’s just a sketch. | |
## `git best-merge` implements the ideal merge strategy. | |
## | |
## This will fast-forward _only_ when the branch contains a single commit with | |
## no conflicts. In all other cases, it will create a merge commit. | |
## | |
## __NB__: Regardless of any flexibility provided by `git merge`, the last | |
## argument _must_ be a commit to merge, not an option. | |
## | |
## __NB__: If you explicitly supply a `--*ff` option, it will be used instead of | |
## the one selected by this script. | |
## | |
## # Rationale | |
## | |
## This is preferred over the default `--ff` because it can be used to maintain | |
## branch rules at _every_ commit on a branch. | |
## | |
## For example, say you have the following history | |
## | |
## A ─ B ← main | |
## └─ C ─ D ← my-branch | |
## | |
## `git merge` will result in | |
## | |
## A ─ B ─ C ─ D ← main | |
## | |
## This has at least two problems. | |
## | |
## The first is conceptual – commits on a branch generally have something that | |
## ties them together. When commits are fast-forwarded, that grouping is lost, | |
## and each commit becomes isolated. Keeping the grouping of a branch shows that | |
## multiple steps were made to a common goal. | |
## | |
## The second problem is technical. It is common when working on a branch to | |
## have commits that _intentionally_ fail CI. In this case, say `C` adds | |
## intentionally failing tests to show that `D` indeed fixes them.[^1] | |
## | |
## The problem now is that a `git bisect` at some point may land on `C`, and | |
## fail its check because of those temporarily-failing tests. And because of the | |
## conceptual problem, it’s not easy to see that this commit should be paired | |
## with the one after it, and moving to that commit would be a better bisect | |
## point. | |
## | |
## Instead, we want to use `git merge --no-ff`, which results in | |
## | |
## A ─ B ─────┌─ E ← main | |
## └─ C ─ D ← my-branch | |
## | |
## where `E` is the merge commit. Now, `git bisect --first-parent`[^2] can never | |
## land on commit `C`, but only on the merge commit. | |
## | |
## Consequently, `git merge --no-ff` is the best merge strategy available in | |
## `git` proper. But it can introduce merge commits that aren’t helpful. Merging | |
## a branch that has only a single fast-forwardable commit gains no benefit from | |
## the merge commit. So this identifies those commits and merges them in the | |
## default `--ff` manner. | |
## | |
## [^1]: This is an important pattern to ensure that new tests weren’t | |
## succeeding before the change that intended to fix them. | |
## | |
## [^2]: `--first-parent` should be enabled by default. It allows bisect to only | |
## explore commits “on” the branch, not commits that have been merged | |
## from some other branch. | |
branch="${*: -1}" | |
commit_count=$(git rev-list --count "${branch}" ^HEAD) | |
if (( commit_count <= 1 )); then | |
## This is the default, but it’s helpful to be explicit. | |
handler="--ff" | |
else | |
handler="--no-ff" | |
fi | |
git merge "${handler}" "${@}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment