| 1 | import { runGitBuffer } from '../exec/run-git.js'; |
| 2 | |
| 3 | export interface SignedCommit |
| 4 | /** The commit object with its signature header removed, as git signs it. */ |
| 5 | payload: string; |
| 6 | /** Armored signature taken from the `gpgsig` header. */ |
| 7 | signature: string; |
| 8 | } |
| 9 | |
| 10 | const PGP_SIGNATURE_HEADER = ' |
| 11 | |
| 12 | /** Commits without an OpenPGP signature (SSH ones included) are absent from the map. One `cat-file --batch` answers a whole page: `git log` cannot hand back the signature header. */ |
| 13 | export async function readSignedCommits |
| 14 | gitDir, |
| 15 | env, |
| 16 | shas, |
| 17 | }: { |
| 18 | gitDir: string |
| 19 | env?: Record |
| 20 | shas: string |
| 21 | }): Promise<Map |
| 22 | const found = |
| 23 | if (shas.length |
| 24 | |
| 25 | const raw = |
| 26 | args: ['cat-file', |
| 27 | gitDir, |
| 28 | env, |
| 29 | input: Buffer.from |
| 30 | }); |
| 31 | |
| 32 | // `<sha> <type> <size>\n<size bytes>\n`, repeated. The size is the only safe boundary: a commit message can hold anything, newlines included. |
| 33 | let offset = |
| 34 | while (offset < |
| 35 | const newline = |
| 36 | if (newline === |
| 37 | |
| 38 | const [sha, |
| 39 | .subarray(offset, |
| 40 | .toString('utf8') |
| 41 | .split(' ') |
| 42 | // A sha git could not read is reported as `<sha> missing`, with no body. |
| 43 | if (!size) |
| 44 | offset = newline |
| 45 | continue; |
| 46 | } |
| 47 | |
| 48 | const start = |
| 49 | const end = |
| 50 | if (type === |
| 51 | const signed = |
| 52 | if (signed) found |
| 53 | } |
| 54 | offset = end |
| 55 | } |
| 56 | |
| 57 | return found; |
| 58 | } |
| 59 | |
| 60 | /** git signs the commit object as it would read without the `gpgsig` header, so exactly that header and its continuation lines come out - every other byte is payload. */ |
| 61 | export function splitSignature(commit |
| 62 | const lines = |
| 63 | const payload: |
| 64 | const signature: |
| 65 | let inSignature = |
| 66 | |
| 67 | for (const [index |
| 68 | if (inSignature) { |
| 69 | // Headers end at the blank line before the message; a continuation line of the signature is the only other line that starts with a space. |
| 70 | if (line.startsWith |
| 71 | signature.push(line |
| 72 | continue; |
| 73 | } |
| 74 | inSignature = false; |
| 75 | } |
| 76 | |
| 77 | // Only a header can carry the signature, and headers stop at the first blank line - a message body quoting `gpgsig ` must not be mistaken for it. |
| 78 | if ( |
| 79 | signature.length === 0 |
| 80 | line.startsWith('gpgsig |
| 81 | lines.slice(0, |
| 82 | ) { |
| 83 | signature.push(line |
| 84 | inSignature = true; |
| 85 | continue; |
| 86 | } |
| 87 | |
| 88 | payload.push(line |
| 89 | } |
| 90 | |
| 91 | if (signature.length |
| 92 | |
| 93 | const armored = |
| 94 | if (!armored.startsWith |
| 95 | |
| 96 | return { payload: |
| 97 | } |
| 98 | |
| 99 | export type CommitSigner |
| 100 | |
| 101 | /** Puts `signature` into `payload` as its `gpgsig` header, the inverse of {@ |
| 102 | export function insertSignature(payload |
| 103 | const headersEnd = |
| 104 | if (headersEnd === |
| 105 | throw new Error( |
| 106 | } |
| 107 | const header = |
| 108 | .trimEnd() |
| 109 | .split('\n') |
| 110 | .map((line, |
| 111 | .join('\n') |
| 112 | return `${payload. |
| 113 | } |