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