Last active
April 6, 2026 02:27
-
-
Save pgaskin/9733ad30d8588f95a9043f75de13b2f7 to your computer and use it in GitHub Desktop.
This is just an experiment. Do not use it. Use https://github.com/pgaskin/push-signed-commits.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| async function createCommitOnBranch(octokit, input) { | |
| const result = await octokit.graphql('mutation($input:CreateCommitOnBranchInput!){createCommitOnBranch(input:$input){commit{oid}}}', {input}) | |
| return result.createCommitOnBranch.commit.oid | |
| } | |
| async function createCommitOnBranchInput(repo, target, commit, overrideParent = undefined) { | |
| const sha = trimSuffix(await git(false, 'rev-parse', '--verify', commit), '\n') // will fail not a valid commit rev | |
| const parents = overrideParent ?? trimSuffix(await git(false, 'rev-parse', sha + '^@'), '\n').split('\n') | |
| const subject = trimSuffix(await git(false, 'show', '--no-patch', '--format=%s', sha), '\n') | |
| const body = trimSuffix(await git(false, 'show', '--no-patch', '--format=%b', sha), '\n') | |
| if (parents.length == 0) throw Error(`cannot create new branches via the api`) | |
| if (parents.length != 1) throw Error(`cannot push merge commits via the api`) | |
| const input = { | |
| branch: { | |
| repositoryNameWithOwner: repo, | |
| branchName: target, | |
| }, | |
| expectedHeadOid: parents[0], | |
| message: { | |
| headline: subject, | |
| body: body, | |
| }, | |
| fileChanges: { | |
| additions: [], | |
| deletions: [], | |
| }, | |
| } | |
| const diff = (await git(false, 'diff-tree', '-z', '-r', '--name-status', parents[0], sha)).split('\0') | |
| while (diff.length && diff[0] != '') { | |
| const status = diff.shift() // see git diff.h DIFF_STATUS_* | |
| const name = diff.shift() | |
| switch (status) { | |
| case 'A': // added | |
| case 'M': // modified | |
| case 'T': // type changed | |
| const type = trimSuffix(await git(false, 'ls-tree', '-z', '--format=%(objecttype)', sha, name), '\0') | |
| switch (type) { | |
| case 'blob': // file or symbolic link (based on the mode) | |
| break | |
| case 'commit': // submodule | |
| throw Error(`cannot create commit via the api for submodule`) | |
| case 'tree': // dir (but should never happen since diff-tree lists files, not dirs) | |
| throw Error(`wtf: diff-tree gave us a directory`) | |
| default: | |
| throw Error(`cannot create commit via the api for unknown object type ${type}`) | |
| } | |
| const mode = trimSuffix(await git(false, 'ls-tree', '-z', '--format=%(objectmode)', sha, name), '\0') // this will always be a single blob | |
| switch (mode) { | |
| case '100644': // regular file | |
| break | |
| case '100755': // executable file | |
| throw Error(`cannot create commit via the api for executable file ${name}`) | |
| case '120000': // symbolic link | |
| throw Error(`cannot create commit via the api for symbolic link ${name}`) | |
| default: // should never happen since git doesn't store other modes | |
| throw Error(`cannot create commit via the api for file ${name} with unknown mode ${mode}`) | |
| } | |
| const raw = await git(true, 'cat-file', '-p', sha+':'+name) // this shouldn't fail since ls-tree worked | |
| input.fileChanges.additions.push({ | |
| path: name, | |
| contents: raw.toString('base64'), | |
| }) | |
| break | |
| case 'D': // deleted | |
| input.fileChanges.deletions.push({ | |
| path: name, | |
| }) | |
| break | |
| case 'R': // renamed | |
| case 'C': // copied | |
| throw Error(`wtf: got diff status ${status} for ${name} even though we didn't ask for it`) // diff-tree, unlike diff, shouldn't do it even if the config specifies it | |
| case 'X': // unknown (should never happen) | |
| case 'U': // unmerged (should never happen since we're reading from the index) | |
| case 'B': // filter broken (should never happen since we're reading from the index) | |
| throw Error(`cannot create commit via the api for file ${name} with diff status ${status}`) | |
| default: | |
| throw Error(`cannot create commit via the api for file ${name} with unknown diff status ${status}`) | |
| } | |
| } | |
| return input | |
| } | |
| function git(raw, ...args) { | |
| return new Promise((resolve, reject) => { | |
| const child = require('node:child_process').spawn('git', args) | |
| const stdout = [] | |
| const stderr = [] | |
| child.stdout.on('data', (chunk) => stdout.push(chunk)) | |
| child.stderr.on('data', (chunk) => stderr.push(chunk)) | |
| child.on('error', err => reject(err)) | |
| child.on('close', (code, signal) => { | |
| if (code) reject(new Error(`git ${JSON.stringify(args)}: exit status ${code} (stderr: ${Buffer.concat(stderr).toString('utf-8')})`)) | |
| else if (signal) reject(new Error(`git ${JSON.stringify(args)}: killed with ${signal} (stderr: ${Buffer.concat(stderr).toString('utf-8')})`)) | |
| else resolve(raw ? Buffer.concat(stdout) : Buffer.concat(stdout).toString('utf-8')) | |
| }) | |
| }) | |
| } | |
| function trimSuffix(str, suffix) { | |
| if (str.endsWith(suffix)) { | |
| return str.slice(0, -suffix.length) | |
| } | |
| return str | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment