Skip to content

Instantly share code, notes, and snippets.

@sellout
Last active September 6, 2024 16:13
Show Gist options
  • Save sellout/d05c5750e9a24161e85694d6b546c14d to your computer and use it in GitHub Desktop.
Save sellout/d05c5750e9a24161e85694d6b546c14d to your computer and use it in GitHub Desktop.
An implementation of the merge strategy I’ve tried to use manually
#!/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