Skip to content
zit A squash merge is a cherry-pick
Login

A squash merge is a cherry-pick

zit 0.8.0's import-git records a squash-merged pull request as what it is in Fossil's terms: trunk took the branch's changes without merging its history, which is a cherry-pick. This note records the rule, where the pairing comes from, and what it never does.

The picture it fixes

GitHub's squash merge writes one new commit on the default branch with a single parent. The pull request's own commits never become its ancestors. Imported, the branch therefore ends in mid-air: nothing joins its last check-in to trunk, and on a busy mirror every pull request draws the same dangling line. A merge commit joins the two lines through its second parent. A squash leaves nothing to join them.

Fossil has the relation already. A Q +HASH card says a check-in took the changes of HASH, and Fossil's timeline draws it as a dashed line from HASH to the check-in. Forges that draw Fossil timelines draw it too.

Where the pair comes from

GitHub keeps refs/pull/N/head for every pull request forever, after its branch is deleted too, pointing at the head that was merged. Its squash commit's subject ends in (#N) by default. Together they name the pair exactly, with no guessing from message text. The rule:

When all of these hold, the commit's check-in gets Q + and the check-in of the pull request's head. When any fails, the commit is written exactly as before.

Only those heads are taken from the pull refs. The export leaves refs/pull/ out, then names each paired head, so a pull request that was never merged, or was merged by fast-forward or a merge commit, is never imported from its ref (since 0.8.1; 0.8.0 exported every pull ref). The paired pull request's commits are imported on a branch named pull/N, closed at the head the squash took. One whose branch still exists is imported from that branch, as before, and keeps its name.

The head has to be in the import for the card to name it. A squash and its pull request share no ancestry, so Git's default export order does not promise the head comes first. When the source holds pull refs, the export therefore runs in date order, which puts the head first. A source without pull refs keeps the default order, and so its exact bytes.

What it never does

zit never rewrites a check-in. A squash already in a store keeps no Q card. Only commits imported from now on get one, and a fresh import links every squash whose pull request head the source holds. A mirror needs the pull refs in its source clone, for example with a fetch refspec of +refs/pull/*/head:refs/pull/*/head. GitLab's refs/merge-requests/N/head is not read.