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:
- The source holds
refs/pull/N/head. - The commit is on the default branch's first-parent chain and has one parent. A merge already joins its branch through its second parent.
- Its subject ends in
(#N), with the pull request's number. - That head is not already the commit's ancestor. After a merge or a fast-forward it is, and there is nothing to pick.
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.