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>
| M | mockup/ | 79 | ++++++++++++++++++++++++++++-- |
@@ -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');