Skip to content

Instantly share code, notes, and snippets.

@pgaskin
Last active April 6, 2026 02:27
Show Gist options
  • Select an option

  • Save pgaskin/9733ad30d8588f95a9043f75de13b2f7 to your computer and use it in GitHub Desktop.

Select an option

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.
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