clone

an offline sliding-block puzzle game in gleam and lustre, with an endless ladder of puzzles ranked by search

release: fold the version stamp into the commit it belongs to

Dami committed 98a2116
The version is derived at ship time, so sw.js is always dirty the moment a
deploy finishes and the commit describing a change never contains the
version it shipped as. That left a trailing stamp commit after every
release -- not a change anyone made, just bookkeeping about the one above.

Amending rewrites history, so it happens only where that is safe: there is
a commit to amend, sw.js is the only dirty path, and HEAD is not already
published. Any of those failing leaves the stamp as a working change, which
is the previous behaviour and is fine.

Dirty paths come from `diff --name-only` and `ls-files --others` rather
than from `status --porcelain`. Porcelain leads with two status characters,
so an unstaged edit reads " M mockup/sw.js" -- and the git helper trims,
which ate the first of them and shifted every offset by one. The guard read
correctly and could never have fired.
commit
98a211636d86ab21331c23359edc4aa950e4cd2c
parent
38901091f20c004249333bd5631b42c6bbfd8949
author
Dami <dami.etoile@gmail.com>
1 file changed, +73 -6
Mmockup/tools/release.mjs79++++++++++++++++++++++++++++--
M mockup/tools/release.mjs +73 -6
@@ -53,19 +53,37 @@ const run = (cmd, args, o = {}) =>
5353 5454 const die = m => { console.error(`\n ${m}\n`); process.exit(1); };5555 56+/** git, but a non-zero exit is an answer rather than an exception. */57+const git = (...args) => {58+ try { return { ok: true, out: run('git', args, { stdio: ['ignore', 'pipe', 'ignore'] }).trim() }; }59+ catch { return { ok: false, out: '' }; }60+};61+62+/* Everything not committed, as bare repo-root-relative paths.63+ *64+ * Deliberately not parsed out of `status --porcelain`. That format leads with65+ * two status characters, so an unstaged edit reads ` M mockup/sw.js` -- and the66+ * helper above trims, which eats the first of them and silently shifts every67+ * subsequent offset by one. These two commands emit paths and nothing else, so68+ * there is no column to miscount. */69+const dirtyPaths = () => [70+ ...git('diff', '--name-only', 'HEAD').out.split('\n'),71+ ...git('ls-files', '--others', '--exclude-standard', '--full-name').out.split('\n'),72+].filter(Boolean);73+5674 if (NOTIFY && !body) die('A notification body is required. Pass one, or --no-notify.');5775 5876 /* ---------------------------------------------------------------- 0. guard */5977 /* Deploying work that is not committed makes the live site a thing no commit6078 * describes. Not fatal -- previewing is legitimate -- but it should never6179 * happen silently. */62-try {63- const dirty = run('git', ['status', '--porcelain']).trim();64- if (dirty) {80+if (git('rev-parse', '--git-dir').ok) {81+ const dirty = dirtyPaths();82+ if (dirty.length) {6583 console.log('\n ! working tree is dirty; the deploy will not match any commit:');66- for (const l of dirty.split('\n').slice(0, 10)) console.log(` ${l}`);84+ for (const f of dirty.slice(0, 10)) console.log(` ${f}`);6785 }68-} catch { /* not a repo: nothing to compare against */ }86+}6987 7088 /* ------------------------------------------------------------ 1. the checks */7189 /* Before the version moves, not after. A failed check after a bump leaves the
@@ -120,7 +138,56 @@ try {
120138 }121139 console.log(` ${res.url} seq ${res.seq} ${res.files} files, ${res.newBlobs} new`);122140 123-/* ----------------------------------------------------------- 4. the notify */141+/* ------------------------------------------------------------- 4. the stamp142+ *143+ * The version is derived at ship time, so sw.js is always dirty the moment a144+ * deploy finishes and the commit describing the change never contains the145+ * version it shipped as. Left alone that produces a trailing "stamp" commit146+ * after every release, which is noise: it is not a change anyone made, it is147+ * bookkeeping about the change above it.148+ *149+ * So it is folded into that commit instead. Amending rewrites history, which is150+ * only safe under conditions worth checking rather than assuming:151+ *152+ * - there is a commit to amend153+ * - sw.js is the *only* thing dirty, so nothing unrelated is swept in154+ * - the commit is not already published, or the rewrite would strand anyone155+ * who has it156+ *157+ * Any of those failing is not an error. It just means the stamp stays a working158+ * change for a human to deal with, which is the status quo and is fine.159+ */160+if (git('rev-parse', '--git-dir').ok) {161+ console.log('\n stamp');162+ const dirty = dirtyPaths();163+ // Compared against what git itself calls the file. It is a directory below164+ // the repo root, so the path reads `mockup/sw.js` and never plain `sw.js`.165+ const swRel = git('ls-files', '--full-name', 'sw.js').out;166+ const onlySw = dirty.length === 1 && swRel && dirty[0] === swRel;167+ const upstream = git('rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}');168+ const published = upstream.ok && git('merge-base', '--is-ancestor', 'HEAD', upstream.out).ok;169+170+ if (!dirty.length) {171+ console.log(' nothing to fold in; the shell was unchanged');172+ } else if (!git('rev-parse', '--verify', 'HEAD').ok) {173+ console.log(' no commit to amend; sw.js left as a working change');174+ } else if (!onlySw) {175+ console.log(' more than sw.js is dirty; left as a working change so');176+ console.log(' nothing unrelated is folded into the last commit');177+ } else if (published) {178+ console.log(` HEAD is already on ${upstream.out}; amending would rewrite`);179+ console.log(' published history, so sw.js is left as a working change');180+ } else {181+ git('add', 'sw.js');182+ if (git('commit', '--amend', '--no-edit').ok) {183+ console.log(` folded into ${git('log', '-1', '--format=%h %s').out}`);184+ } else {185+ console.log(' amend failed; sw.js left as a working change');186+ }187+ }188+}189+190+/* ----------------------------------------------------------- 5. the notify */124191 if (!NOTIFY) { console.log('\n --no-notify\n'); process.exit(0); }125192 126193 console.log('\n notify');