Coverage / src/commitbox.ts 95.6%
86 of 90 lines run, 13 of 13 functions, 39 of 42 branches · run #26 on main · run missed
| 1 | 0× | // The commit box's logic (spec §5.1), kept free of the vscode module so it |
| 2 | 0× | // can be tested: signing state from `oxy identity show`, the placeholder |
| 3 | 0× | // that shows it, message history, and conflict marker detection. |
| 4 | 1× | |
| 5 | export interface SigningInfo { | |
| 6 | /** `Name <email>`, or empty without an identity. */ | |
| 7 | who: string; | |
| 8 | /** Whether the secret key is in the key directory. */ | |
| 9 | secret: boolean; | |
| 10 | /** `change`: every commit is signed; `seal`: changes are sealed at push. */ | |
| 11 | mode: "change" | "seal"; | |
| 12 | } | |
| 13 | 1× | |
| 14 | 0× | /** Parse `oxy --plain identity show`; `null` when no identity is configured. */ |
| 15 | 4× | export function parseIdentityShow(text: string): SigningInfo | null { |
| 16 | 1× | const head = text.match(/^#\s*identity\s+(.+)$/m); |
| 17 | 1× | if (!head) { |
| 18 | 1× | return null; |
| 19 | 1× | } |
| 20 | 1× | const secretLine = text.match(/^\s*secret\s+(.+)$/m)?.[1] ?? ""; |
| 21 | 1× | const signingLine = text.match(/^\s*signing\s+(.+)$/m)?.[1] ?? ""; |
| 22 | 1× | return { |
| 23 | 1× | who: head[1].trim(), |
| 24 | 1× | secret: /available/.test(secretLine), |
| 25 | 1× | mode: /core\.sign\s*=\s*change/.test(signingLine) ? "change" : "seal", |
| 26 | 1× | }; |
| 27 | 3× | } |
| 28 | 3× | |
| 29 | 3× | /** A short name for the placeholder: the name without the address. */ |
| 30 | 3× | function shortName(who: string): string { |
| 31 | 3× | return who.replace(/\s*<[^>]*>\s*$/, "").trim() || who; |
| 32 | 5× | } |
| 33 | 5× | |
| 34 | 5× | /** How the next commit will be authenticated, for the commit box. */ |
| 35 | 5× | export function signingLabel(info: SigningInfo | null | undefined): string { |
| 36 | 5× | if (info === undefined) { |
| 37 | 1× | return ""; |
| 38 | 1× | } |
| 39 | 5× | if (info === null) { |
| 40 | 1× | return "⚠ unsigned: no identity (oxy identity create)"; |
| 41 | 1× | } |
| 42 | 5× | if (!info.secret) { |
| 43 | 1× | return `⚠ unsigned: the key for ${shortName(info.who)} is missing`; |
| 44 | 1× | } |
| 45 | 5× | return info.mode === "change" ? `✔ signed as ${shortName(info.who)}` : `◈ sealed at push as ${shortName(info.who)}`; |
| 46 | 5× | } |
| 47 | 1× | |
| 48 | 1× | /** The commit box placeholder for the current mode. */ |
| 49 | 1× | export function placeholder(opts: { amend: boolean; resolution: boolean; signing: string }): string { |
| 50 | 3× | const what = opts.amend ? "Amend the last change: new message, or empty to keep it" : opts.resolution ? "Describe the resolution" : "Message"; |
| 51 | 3× | const key = opts.amend ? "Ctrl+Enter to amend" : "Ctrl+Enter to commit"; |
| 52 | 3× | return `${what} (${key})${opts.signing ? ` · ${opts.signing}` : ""}`; |
| 53 | 3× | } |
| 54 | 1× | |
| 55 | 1× | /** Recent commit messages, newest first, without duplicates. */ |
| 56 | 1× | export class MessageHistory { |
| 57 | 1× | static readonly LIMIT = 30; |
| 58 | 1× | private items: string[]; |
| 59 | 1× | |
| 60 | 1× | constructor(items: readonly string[] = []) { |
| 61 | 1× | this.items = items.filter((m) => m.trim()).slice(0, MessageHistory.LIMIT); |
| 62 | 1× | } |
| 63 | 1× | |
| 64 | 1× | add(message: string): void { |
| 65 | 43× | const m = message.trim(); |
| 66 | 43× | if (!m) { |
| 67 | 1× | return; |
| 68 | 1× | } |
| 69 | 43× | this.items = [m, ...this.items.filter((x) => x !== m)].slice(0, MessageHistory.LIMIT); |
| 70 | 43× | } |
| 71 | 1× | |
| 72 | 1× | get entries(): readonly string[] { |
| 73 | 3× | return this.items; |
| 74 | 3× | } |
| 75 | 3× | } |
| 76 | 3× | |
| 77 | 3× | /** |
| 78 | 3× | * Line numbers (1-based) that open or close a conflict: Oxygen's markers |
| 79 | 3× | * (`<<<<<<< oxy conflict …`) and Git's alike, since both begin with seven |
| 80 | 3× | * angle brackets. |
| 81 | 3× | */ |
| 82 | 3× | export function conflictMarkerLines(text: string): number[] { |
| 83 | 3× | const out: number[] = []; |
| 84 | 3× | text.split("\n").forEach((l, i) => { |
| 85 | 18× | if (/^(<{7}|>{7})( |\r?$)/.test(l)) { |
| 86 | 4× | out.push(i + 1); |
| 87 | 4× | } |
| 88 | 3× | }); |
| 89 | 3× | return out; |
| 90 | 3× | } |