Forking a module
A distribution can fix a module it ships without waiting for upstream: fork the module, ship the fix, offer it upstream, and go back to upstream once it has the fix — or keep the fork.
pnpm dist fork id https://github.com/acme/id.git # check it out from your fork
cd apps/id && git switch -c fix-login # make the change on a branch
git commit -am "Fix the login redirect"
pnpm dist contribute id # push it, open a pull request upstream
git add .gitmodules apps/id && git commit # ship it meanwhile
pnpm dist sync # later: has upstream taken it?
pnpm dist unfork id # go back to the release that has it
What a fork is
The module checked out from a repository you can push to, with the one it came
from recorded beside it in .gitmodules:
[submodule "apps/id"]
path = apps/id
url = https://github.com/acme/id.git
upstream = https://github.com/fairgarden/id.git
Git ignores upstream; fg-dist reads it. Every clone gets the fork as
origin, and fg-dist adds upstream as a remote of the checkout whenever it
needs it. The fork itself has to exist first — on GitHub, the Fork button — and
fork refuses a url it cannot read.
Pins outlive pull requests
Everything the distribution pins in the fork is pushed to a branch named after
the distribution — acme-core, for @acme/core. fork puts the pinned commit
there, contribute moves it up to what it offered, and bump pushes its
merges there. The pull request has a branch of its own, and GitHub deletes that
once it is merged; a pin that lived only there would stop being fetchable, and
every build of that commit with it.
sync reports any module pinned at a commit its remote does
not have, fork or not: that is a build host failing to clone it.
Keeping up
A fork takes releases from upstream. sync fetches upstream's tags as well as
the fork's, so its upgrades are upstream's versions, and
bump takes them the same way as for any module — except
that, where the fork has work of its own, the release is merged in and pushed:
apps/id v1.2.0 -> v1.3.0 (fork: merged, pushed to acme-core)
A merge rather than a rebase, so every commit an older distribution pinned stays in the fork. A merge that conflicts stops, and says how to do it by hand. Afterwards the fork's version is the release's, which is what an extension's floor compares.
Going back
sync says when upstream has everything the fork adds:
apps/id v1.2.0+ -> v1.2.1 (patch) fork of github.com/fairgarden/id, all in upstream v1.2.1
"Everything" is checked by merging the fork into the release without touching anything: when nothing is left over, nothing would be lost — however upstream took it, merged, rebased or squashed.
unfork then checks the module out from upstream again, at
the oldest release from the pinned one on that has it all. It refuses while
upstream is missing something, unless you name what to go back to and pass
--force. The fork repository is left as it is.
Or keep the fork. Nothing requires going back: a fork with changes upstream will
not take is a fork you maintain, and bump keeps it current.
In the changelog
Each step is noted in the distribution's changelog: the
fork, each change shipped ahead of a release with a link to its pull request, each upstream
release taken, and going back. The module's own changelog wants a line from
you for the pull request — upstream's check asks for it — and contribute
prints it with the link filled in.
In an extension
A distribution that ships a fork publishes
the fork's repository and its upstream, and inherit gives
an extension the same fork. An extension can fork any module it inherited, too.
The floor compares versions, so a fork keeps up with a parent that moves ahead
by taking upstream's releases with bump.