patchette

USAGE.md

Using Patchette🔗

Patchette snapshots your branches into a patches repo, as one folder of patch files per branch, and rebuilds those exact branches from the patches, commit for commit, in any clone of the upstream. You keep working in your own repo, with plain git; patchette only reads it, and writes the branches back when you ask.

Every session below is real: the test suite runs each one, and checks that patchette still prints exactly what's shown. They all start from the same place: a home dir with an upstream repo at ~/upstream/frob, and your clone of it at ~/src/frob, with main checked out, carrying two branches of your own work, topic/fancy and docs-polish.

Setting up🔗

The patches live in a repo of their own, beside your clone. Make one, then pair your clone with it, so patchette knows where to look:

~$ cd src/frob

~/src/frob$ git init -b main ../frob-patches
Initialized empty Git repository in ~/src/frob-patches/.git/

~/src/frob$ patchette pair ../frob-patches
paired: '~/src/frob' -> patches dir '../frob-patches'

~/src/frob$ patchette pair
paired: patches dir is '../frob-patches' (relative to '~/src/frob')

Pairing is the one thing patchette keeps in your repo: a single value, in .git/patchette, never pushed. Delete that file to un-pair.

The sessions in the rest of this guide each start as though you'd just done that (apart from the last two sections, which start unpaired, from ~):

cd ~/src/frob
git init -q -b main ../frob-patches
patchette pair ../frob-patches

Taking snapshots🔗

Work on your branches however you like; then export them, and commit the result in the patches repo. Export takes the upstream commit your branches are based on, then the branches:

~/src/frob$ patchette export origin/main topic/fancy docs-polish
export: wrote ~/src/frob-patches/topic¦fancy (8 patches)
export: wrote ~/src/frob-patches/docs-polish (2 patches)

~/src/frob$ ls ../frob-patches/docs-polish
STACK
mention-the-oiling-schedule.patch
polish-the-readme.patch

~/src/frob$ git -C ../frob-patches add .

~/src/frob$ git -C ../frob-patches commit -q -m "first snapshot"

Each branch becomes a folder: a STACK file saying where it's based and what's in it, and one patch file per commit.

~/src/frob-patches/docs-polish/STACK names the branch, the upstream commit it's based on, and its tip; then, after series, its patches, in order:

ref refs/heads/docs-polish
base 5b0130cfd2153bcdb4a6d7ec356e7d805018afc9
tip 2dfecf1d7ce26028ef2dcc7430b5b9bc9c8aa86a
series
polish-the-readme
mention-the-oiling-schedule

~/src/frob-patches/docs-polish/polish-the-readme.patch is one commit: its parents (BASE is the STACK's base), then the rest of its headers exactly as git stored them; its message, between ---- lines; and its diff:

parents BASE
author Ada Downstream <ada@downstream.example> 1700000660 +0000
committer Ada Downstream <ada@downstream.example> 1700000660 +0000

----
> Polish the readme
----

diff --git a/README.md b/README.md
index a330cd687e7fdede54bbe0aca5df47c06d2e8b64..c195933372242cdbb3c014665ebcff2bb3fb4449 100644
--- a/README.md
+++ b/README.md
@@ -3,3 +3,5 @@
 Upstream docs.
 
 More upstream docs.
+
+Downstream notes: be kind to the machinery.

Both formats are specified in full in FORMAT.md.

The payoff comes when your patches change. Here, a commit on docs-polish gets reworked; export again, and the patches repo shows what changed about the change:

~/src/frob$ patchette export origin/main docs-polish
export: wrote ~/src/frob-patches/docs-polish (2 patches)

~/src/frob$ git -C ../frob-patches add .

~/src/frob$ git -C ../frob-patches commit -q -m "first snapshot"

~/src/frob$ git switch -q docs-polish

~/src/frob$ sed -i 's/weekly/daily/' README.md

~/src/frob$ git commit -q -a --amend --no-edit

~/src/frob$ patchette export origin/main docs-polish
export: wrote ~/src/frob-patches/docs-polish (2 patches)

~/src/frob$ git -C ../frob-patches diff
diff --git a/docs-polish/STACK b/docs-polish/STACK
index c56f84d..99ac5cd 100644
--- a/docs-polish/STACK
+++ b/docs-polish/STACK
@@ -1,6 +1,6 @@
 ref refs/heads/docs-polish
 base 5b0130cfd2153bcdb4a6d7ec356e7d805018afc9
-tip 2dfecf1d7ce26028ef2dcc7430b5b9bc9c8aa86a
+tip 88f146ae1801f5fd8d443d6eb5904f09b88927c6
 series
 polish-the-readme
 mention-the-oiling-schedule
diff --git a/docs-polish/mention-the-oiling-schedule.patch b/docs-polish/mention-the-oiling-schedule.patch
index 8b0dda3..1f6c371 100644
--- a/docs-polish/mention-the-oiling-schedule.patch
+++ b/docs-polish/mention-the-oiling-schedule.patch
@@ -1,17 +1,17 @@
 parents polish-the-readme
 author Ada Downstream <ada@downstream.example> 1700000720 +0000
-committer Ada Downstream <ada@downstream.example> 1700000720 +0000
+committer Ada Downstream <ada@downstream.example> 1700086400 +0000
 
 ----
 > Mention the oiling schedule
 ----
 
 diff --git a/README.md b/README.md
-index c195933372242cdbb3c014665ebcff2bb3fb4449..073a43988cb91f0fe493964670f6d52517082346 100644
+index c195933372242cdbb3c014665ebcff2bb3fb4449..61f9c849c8c9962d481e8ce3874abcc47a640eba 100644
 --- a/README.md
 +++ b/README.md
 @@ -5,3 +5,4 @@ Upstream docs.
  More upstream docs.
  
  Downstream notes: be kind to the machinery.
-+Oil the gears weekly.
++Oil the gears daily.

Commit that with a message saying why it changed, and the history of your patches tells its own story.

Checking a snapshot🔗

verify rebuilds each branch from its patches, off to the side, and checks it comes out exactly as it was recorded. It never touches your branches. Name branches to check just those:

~/src/frob$ patchette export origin/main topic/fancy docs-polish
export: wrote ~/src/frob-patches/topic¦fancy (8 patches)
export: wrote ~/src/frob-patches/docs-polish (2 patches)

~/src/frob$ patchette verify
verify: ok: refs/heads/docs-polish reproduces recorded tip 2dfecf1d7ce26028ef2dcc7430b5b9bc9c8aa86a
verify: ok: refs/heads/topic/fancy reproduces recorded tip cc261dc9f0b878a78e2b80aeb60de397205f61ad

~/src/frob$ patchette verify topic/fancy
verify: ok: refs/heads/topic/fancy reproduces recorded tip cc261dc9f0b878a78e2b80aeb60de397205f61ad

Rebuilding branches in a fresh clone🔗

The patches repo is enough to bring your branches back, exactly, in a clone that never had them. Pair the new clone, and import:

~/src/frob$ patchette export origin/main topic/fancy docs-polish
export: wrote ~/src/frob-patches/topic¦fancy (8 patches)
export: wrote ~/src/frob-patches/docs-polish (2 patches)

~/src/frob$ cd ..

~/src$ git clone -q ~/upstream/frob frob2

~/src$ cd frob2

~/src/frob2$ patchette pair ../frob-patches
paired: '~/src/frob2' -> patches dir '../frob-patches'

~/src/frob2$ patchette import
import: created refs/heads/docs-polish at 2dfecf1d7ce26028ef2dcc7430b5b9bc9c8aa86a
import: created refs/heads/topic/fancy at cc261dc9f0b878a78e2b80aeb60de397205f61ad

~/src/frob2$ git rev-parse topic/fancy
cc261dc9f0b878a78e2b80aeb60de397205f61ad

~/src/frob2$ git -C ../frob rev-parse topic/fancy
cc261dc9f0b878a78e2b80aeb60de397205f61ad

Editing patches by hand🔗

Patch files are plain text, and fine to edit directly. Then rebuild the branch from them. import won't replace a branch that already exists unless you say -f, and even then, not the branch you have checked out: switch away first.

~/src/frob$ patchette export origin/main topic/fancy
export: wrote ~/src/frob-patches/topic¦fancy (8 patches)

~/src/frob$ sed -i 's/^> The hum is load-bearing.$/> The hum is load-bearing, and tuned./' ../frob-patches/topic¦fancy/teach-gizmo-to-hum.patch

~/src/frob$ patchette import topic/fancy
patchette: 'refs/heads/topic/fancy' already exists in '~/src/frob'; refusing to touch it (-f updates it instead)
[exit status: 1]

~/src/frob$ git switch -q topic/fancy

~/src/frob$ patchette import -f topic/fancy
patchette: 'refs/heads/topic/fancy' is checked out in '~/src/frob'; refusing to move it under that checkout, even with -f
[exit status: 1]

~/src/frob$ git switch -q main

~/src/frob$ patchette import -f topic/fancy
import: updated refs/heads/topic/fancy to 57d05b16d9ad81fd92af19d210f0884dc9745e01 (previous tip cc261dc9f0b878a78e2b80aeb60de397205f61ad remains in its reflog)
import: note: replayed tip differs from recorded tip cc261dc9f0b878a78e2b80aeb60de397205f61ad (patches edited since the snapshot?)

~/src/frob$ patchette verify topic/fancy
verify: MISMATCH: refs/heads/topic/fancy replays to 57d05b16d9ad81fd92af19d210f0884dc9745e01, but records tip cc261dc9f0b878a78e2b80aeb60de397205f61ad
verify:   either the snapshot was unfaithful, or the patches were edited since (patches repo is ahead of its snapshot)
[exit status: 1]

~/src/frob$ patchette export origin/main topic/fancy
export: wrote ~/src/frob-patches/topic¦fancy (8 patches)

~/src/frob$ patchette verify topic/fancy
verify: ok: refs/heads/topic/fancy reproduces recorded tip 57d05b16d9ad81fd92af19d210f0884dc9745e01

verify fails right after the edit: the patches no longer match the snapshot they were taken as. Exporting the rebuilt branch takes a new snapshot of it, and they agree again.

Without pairing🔗

Pairing is a convenience. From anywhere, --subject names your repo and --patches the patches repo; each one, given, wins over the pairing.

~$ git init -q -b main src/frob-patches

~$ patchette export --subject=src/frob --patches=src/frob-patches origin/main docs-polish
export: wrote ~/src/frob-patches/docs-polish (2 patches)

~$ patchette verify --subject=src/frob --patches=src/frob-patches
verify: ok: refs/heads/docs-polish reproduces recorded tip 2dfecf1d7ce26028ef2dcc7430b5b9bc9c8aa86a

When something's wrong🔗

~$ cd src/frob

~/src/frob$ patchette verify
patchette: --subject/--patches not given, and '~/src/frob' is not paired; run 'patchette pair <patches-dir>' there, or pass the flags
[exit status: 1]

~/src/frob$ cd /tmp

/tmp$ patchette verify --subject="$HOME/src/frob"
patchette: --subject/--patches not given, and the cwd is not inside a git repository
[exit status: 1]

/tmp$ cd ~/src/frob

~/src/frob$ mkdir ../frob-patches

~/src/frob$ patchette pair ../frob-patches
paired: '~/src/frob' -> patches dir '../frob-patches'

~/src/frob$ patchette export origin/main docs-polish
export: wrote ~/src/frob-patches/docs-polish (2 patches)

~/src/frob$ patchette verify docs-polish --patches=../frob-patches
patchette: no branch folder for '--patches=../frob-patches' in '~/src/frob-patches'
[exit status: 1]

~/src/frob$ patchette verify --patches=
verify: ok: refs/heads/docs-polish reproduces recorded tip 2dfecf1d7ce26028ef2dcc7430b5b9bc9c8aa86a

~/src/frob$ patchette export
patchette: usage: patchette export [--sigil=quote|--sigil=tab] [--subject=<repo>] [--patches=<dir>] <base-rev> <branch>...
[exit status: 1]

docs/userguide/USAGE.md · 11.1 KiB · d231dd1498