gitmark 0.0.78 → 0.0.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,8 +68,17 @@ The chain of addresses on Bitcoin mirrors the chain of commits in git. Anyone ca
68
68
  | `git mark info` | Show trail state, balance, addresses |
69
69
  | `git mark verify` | Verify all marks against Bitcoin |
70
70
  | `git mark update` | Update blocktrails.json from git notes |
71
+ | `git mark badge` | Print README badge markdown. Option: `--branch` |
71
72
  | `git mark --version` | Show version |
72
73
 
74
+ ## Badge
75
+
76
+ `git mark badge` prints markdown for a README badge, built from the `origin` remote, the current branch (or `--branch`) and `blocktrails.json`. For example, this is the badge for [melvincarvalho/delivery-day](https://github.com/melvincarvalho/delivery-day), whose trail is on txbt4:
77
+
78
+ [![gitmarks](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmelvincarvalho%2Fdelivery-day%2Fgh-pages%2Fblocktrails.json&query=%24.states.length&label=gitmarks&suffix=%20%C2%B7%20txbt4&color=f7931a)](https://mempool.guide/testnet4/tx/8129190799de150153bdae7d1092c4cd505874e842cfbf429ac452956025fa51)
79
+
80
+ It's a shields.io [dynamic JSON badge](https://shields.io/badges/dynamic-json-badge) that counts `states` in the pushed trail file, and links to the latest mark on the chain's explorer. The count updates whenever `blocktrails.json` is pushed, so it shows what the repo records. `git mark verify` checks it against the chain. For remotes not on GitHub, it prints `RAW_TRAIL_URL` and `TRAIL_LINK` placeholders to fill in.
81
+
73
82
  ## Trail File
74
83
 
75
84
  `blocktrails.json` in your repo root — committed, visible, verifiable:
package/bin/git-mark.js CHANGED
@@ -9,6 +9,7 @@
9
9
  * git mark info
10
10
  * git mark verify
11
11
  * git mark update
12
+ * git mark badge [--branch name]
12
13
  */
13
14
 
14
15
  import { secp256k1, schnorr } from '@noble/curves/secp256k1';
@@ -565,6 +566,48 @@ async function cmdVerify() {
565
566
  process.exit(ok ? 0 : 1);
566
567
  }
567
568
 
569
+ // --- Badge ---
570
+ // owner/repo from a GitHub remote URL (https, ssh or scp form), or null
571
+ function parseGithubRemote(url) {
572
+ const m = String(url || '').trim().match(/^(?:https?:\/\/|ssh:\/\/git@|git@)github\.com[:/]([^/]+)\/(.+?)(?:\.git)?\/?$/);
573
+ return m ? { owner: m[1], repo: m[2] } : null;
574
+ }
575
+
576
+ // the explorer's web page for a transaction: its API base without /api
577
+ function explorerTxUrl(chain, txid) {
578
+ const api = CHAINS[chain]?.explorer;
579
+ return api ? `${api.replace(/\/api$/, '')}/tx/${txid}` : null;
580
+ }
581
+
582
+ // Markdown for a shields.io dynamic JSON badge counting the marks in the pushed trail file.
583
+ // trailUrl: where the raw blocktrails.json is served; link: where the badge points.
584
+ function badgeMarkdown({ trailUrl, chain, link }) {
585
+ const q = new URLSearchParams({ url: trailUrl, query: '$.states.length', label: 'gitmarks', suffix: ` · ${chain}`, color: 'f7931a' });
586
+ const img = `https://img.shields.io/badge/dynamic/json?${q.toString().replace(/\+/g, '%20')}`;
587
+ return `[![gitmarks](${img})](${link})`;
588
+ }
589
+
590
+ function cmdBadge(args) {
591
+ const trail = loadFullTrail();
592
+ if (!trail) { console.error(`No ${TRAIL_FILE} found. Run: git mark init`); process.exit(1); }
593
+ const branchIdx = args.indexOf('--branch');
594
+ const quiet = (cmd) => { try { return execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch { return ''; } };
595
+ const branch = (branchIdx !== -1 ? args[branchIdx + 1] : null) || quiet('git symbolic-ref --short HEAD') || 'main';
596
+ const remote = quiet('git remote get-url origin');
597
+ const gh = parseGithubRemote(remote);
598
+
599
+ // the latest mark; before the first one, the badge links to the trail file instead
600
+ const lastTxid = trail.txo.length ? parseTxoUri(trail.txo[trail.txo.length - 1]).txid : null;
601
+ const trailUrl = gh
602
+ ? `https://raw.githubusercontent.com/${gh.owner}/${gh.repo}/${branch}/${TRAIL_FILE}`
603
+ : 'RAW_TRAIL_URL';
604
+ const link = (lastTxid && explorerTxUrl(trail.chain, lastTxid))
605
+ || (gh ? `https://github.com/${gh.owner}/${gh.repo}/blob/${branch}/${TRAIL_FILE}` : 'TRAIL_LINK');
606
+
607
+ console.log(badgeMarkdown({ trailUrl, chain: trail.chain, link }));
608
+ if (!gh) console.error(`\nOrigin is not on GitHub: replace RAW_TRAIL_URL with the URL-encoded address the pushed ${TRAIL_FILE} is served from, and TRAIL_LINK with where the badge should point.`);
609
+ }
610
+
568
611
  function cmdUpdate() {
569
612
  const trail = loadTrailFromNotes();
570
613
  if (!trail) { console.error(`No ${TRAIL_FILE} found. Run: git mark init`); process.exit(1); }
@@ -577,7 +620,8 @@ export {
577
620
  taggedHash, btScalar, deriveChainedPrivkey, deriveChainedPubkey,
578
621
  pubkeyToAddress, parseTxoUri, p2trScript, buildTransaction,
579
622
  TRAIL_FILE, PRIVATE_FILE, CHAINS, isDirty, loadTrailFromNotes, loadFullTrail,
580
- resolveVoucher, consumeVoucher
623
+ resolveVoucher, consumeVoucher,
624
+ parseGithubRemote, explorerTxUrl, badgeMarkdown
581
625
  };
582
626
 
583
627
  // --- CLI ---
@@ -596,6 +640,8 @@ if (isMain) {
596
640
  cmdVerify();
597
641
  } else if (cmd === 'update') {
598
642
  cmdUpdate();
643
+ } else if (cmd === 'badge') {
644
+ cmdBadge(args.slice(1));
599
645
  } else if (cmd === 'mark' || !cmd || (cmd && !cmd.startsWith('-'))) {
600
646
  if (!existsSync(TRAIL_FILE) && cmd !== 'mark') {
601
647
  console.log('Usage:');
@@ -604,6 +650,7 @@ if (isMain) {
604
650
  console.log(' git mark info # show trail state');
605
651
  console.log(' git mark verify # verify trail against Bitcoin');
606
652
  console.log(' git mark update # update blocktrails.json from git notes');
653
+ console.log(' git mark badge # print README badge markdown');
607
654
  } else {
608
655
  cmdMark(args.slice(cmd === 'mark' ? 1 : 0));
609
656
  }
@@ -613,5 +660,6 @@ if (isMain) {
613
660
  console.log(' git mark # anchor HEAD to Bitcoin');
614
661
  console.log(' git mark info # show trail state');
615
662
  console.log(' git mark verify # verify trail against Bitcoin');
663
+ console.log(' git mark badge # print README badge markdown');
616
664
  }
617
665
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitmark",
3
- "version": "0.0.78",
3
+ "version": "0.0.79",
4
4
  "type": "module",
5
5
  "description": "Anchor git commits to Bitcoin via blocktrails",
6
6
  "main": "bin/git-mark.js",
@@ -6,7 +6,8 @@ import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
6
6
  import {
7
7
  taggedHash, btScalar, deriveChainedPrivkey, deriveChainedPubkey,
8
8
  pubkeyToAddress, parseTxoUri, p2trScript, CHAINS, isDirty, loadFullTrail, loadTrailFromNotes,
9
- resolveVoucher, consumeVoucher, buildTransaction
9
+ resolveVoucher, consumeVoucher, buildTransaction,
10
+ parseGithubRemote, explorerTxUrl, badgeMarkdown
10
11
  } from '../bin/git-mark.js';
11
12
  import { unifiedSighash, parseTransaction, SCRIPT_TYPE_TAPROOT } from '../lib/unified-sighash.js';
12
13
  import { schnorr } from '@noble/curves/secp256k1';
@@ -438,3 +439,37 @@ describe('Signing', () => {
438
439
  assert.notDeepStrictEqual(plain.inputs[0].witness[0], unified.inputs[0].witness[0].slice(0, 64));
439
440
  });
440
441
  });
442
+
443
+ describe('Badge', () => {
444
+ it('reads owner and repo from GitHub remotes in every form', () => {
445
+ for (const url of [
446
+ 'https://github.com/melvincarvalho/delivery-day.git',
447
+ 'https://github.com/melvincarvalho/delivery-day',
448
+ 'git@github.com:melvincarvalho/delivery-day.git',
449
+ 'ssh://git@github.com/melvincarvalho/delivery-day.git',
450
+ ]) assert.deepStrictEqual(parseGithubRemote(url), { owner: 'melvincarvalho', repo: 'delivery-day' }, url);
451
+ });
452
+
453
+ it('returns null for remotes not on GitHub', () => {
454
+ assert.strictEqual(parseGithubRemote('https://gitlab.com/a/b.git'), null);
455
+ assert.strictEqual(parseGithubRemote(''), null);
456
+ });
457
+
458
+ it('links a transaction on the chain explorer', () => {
459
+ assert.strictEqual(explorerTxUrl('txbt4', 'ab'), 'https://mempool.guide/testnet4/tx/ab');
460
+ assert.strictEqual(explorerTxUrl('xbt', 'ab'), 'https://mempool.kilombino.com/tx/ab');
461
+ assert.strictEqual(explorerTxUrl('btc', 'ab'), 'https://mempool.space/tx/ab');
462
+ assert.strictEqual(explorerTxUrl('nope', 'ab'), null);
463
+ });
464
+
465
+ it('builds a shields.io dynamic JSON badge counting states', () => {
466
+ const md = badgeMarkdown({
467
+ trailUrl: 'https://raw.githubusercontent.com/melvincarvalho/delivery-day/gh-pages/blocktrails.json',
468
+ chain: 'txbt4',
469
+ link: 'https://mempool.guide/testnet4/tx/ab',
470
+ });
471
+ assert.strictEqual(md,
472
+ '[![gitmarks](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmelvincarvalho%2Fdelivery-day%2Fgh-pages%2Fblocktrails.json' +
473
+ '&query=%24.states.length&label=gitmarks&suffix=%20%C2%B7%20txbt4&color=f7931a)](https://mempool.guide/testnet4/tx/ab)');
474
+ });
475
+ });