GitHub: disable squash & merge on specific branches
Or how to associate different merging strategies to different branches
August 15, 2022
Or how to associate different merging strategies to different branches
August 15, 2022
Note: this is a mirror of the blog post originally published on Hookdeck blog!
At Hookdeck, we use a branching flow somewhat similar to the GitLab flow,
where we have staging
as our main branch, and regularly merge
staging
into main
to deploy to production.
We like to squash & merge feature branches into staging
, but we
want to create a merge commit when merging staging
into main
.
The problem is: GitHub always defaults to the last used merge
strategy, meaning that after you squash a feature branch into
staging
, youâre guaranteed to have squash & merge selected by default
when you want to merge staging
to main
.
In practice, this leads to accidentally squashing staging
into main
,
losing meaningful history, plus causing weird diff issues in the
subsequent PRs.
We realized we werenât the only ones with this issue, and decided to build an extension to mitigate it in the meantime GitHub addresses it natively.
The result: GitHub Contextual Merge Strategy.
We didnât publish this extension on the Chrome and Firefox store, but nevertheless you can easily clone it and install it using the developer mode to load unpacked extensions.
By default, it preselects create a merge commit when the source
branch is any of main
, master
, staging
and preview
, and
otherwise selects squash & merge.
Youâll only need to tweak the URL host and prefix you want it to run on in
background.js
(by default we only run it on the GitHub Hookdeck organization).
Also feel free to tweak the script.js
file accordingly to your own branching conventions and needs!
Hereâs the nitty gritty details of the implementation. The first idea was very basic:
Run this in your browser console on a GitHub PR page, and it will work like a charm.
But when we tried to put that in a content script as part of an
extension, it broke down: GitHub loads asynchronously the block at the
bottom where we get the different merge options! So running at
document_end
is not enough to be able to preselect a merging strategy,
as the merging strategies element is not loaded yet.
To work around that, we used a MutationObserver
in order to watch the parent element for any change to its child tree.
This allows us to get a callback when the merge options are loaded into
the DOM, and effectively select the one appropriate to the current
branch being merged!
But soon after, we realized this wasnât enough. The extension seemed to
be working sometimes, but not all the time. After further
investigation, it turned out that GitHub uses single-page navigation
with the History.pushState
API.
The symptoms: if you open a PR page directly, the extension works, but if you open any other page on GitHub and then browse your way to a PR page, it wonât!
To my dismay, thereâs no web API allowing us to subscribe to the pushState
events.
A common solutions is to proxy
the history.pushState
function, but this isnât easily done in an
extension since we donât have a shared global object with the page being
manipulated (unsafeWindow
doesnât seem to be a thing anymore for
security reasons).
An alternative is to use the extensions API webNavigation.onHistoryStateUpdated
,
which was music to my ears until I realized that it only works from a background script and not a content script!
Fair enough, I refactored the extension to use a background script in order to hook into the navigation events and trigger the script accordingly.
I didnât like the idea of running a background script but it seems that
browsers optimized them in a way that they only run when triggered by
specific events, here onHistoryStateUpdated
, only on github.com
and
when the path starts with /hookdeck
! This means the extension overhead
is very limited and thatâs a good thing.
After this last change, the extension was finally doing its job reliably. Weâve been using it for a few weeks now without any accidental squash!
If you also need to default the merge strategy differently on specific branches, go check out the GitHub repo and adapt it to your organization!
I hope GitHub will soon support such a setting natively and make this extension obsolete (so we donât have to maintain it đŹ), but in the meantime, I hope you enjoy it!