scrum4me-mcp/src/git/push.ts
janpeter visser fbfaf905c8 feat: add pushBranchForJob helper (src/git/push.ts)
Runs git push -u origin <branch> in the worktree. Detects no-changes
(HEAD = origin/main) before pushing. Classifies push failures into
no-credentials, conflict, or unknown via stderr pattern matching.
5 unit tests covering all paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 11:57:14 +02:00

53 lines
1.7 KiB
TypeScript

import { execFile } from 'node:child_process'
import { promisify } from 'node:util'
const exec = promisify(execFile)
type PushSuccess = { pushed: true; remoteRef: string }
type PushFailure = {
pushed: false
reason: 'no-credentials' | 'conflict' | 'no-changes' | 'unknown'
stderr: string
}
export type PushResult = PushSuccess | PushFailure
export async function pushBranchForJob(opts: {
worktreePath: string
branchName: string
}): Promise<PushResult> {
const { worktreePath, branchName } = opts
// Detect no new commits vs origin/main
let headSha: string
let baseSha: string
try {
const [headResult, baseResult] = await Promise.all([
exec('git', ['rev-parse', 'HEAD'], { cwd: worktreePath }),
exec('git', ['rev-parse', 'origin/main'], { cwd: worktreePath }),
])
headSha = headResult.stdout.trim()
baseSha = baseResult.stdout.trim()
} catch (err) {
return { pushed: false, reason: 'unknown', stderr: (err as Error).message }
}
if (headSha === baseSha) {
return { pushed: false, reason: 'no-changes', stderr: '' }
}
// Push
try {
await exec('git', ['push', '-u', 'origin', branchName], { cwd: worktreePath })
return { pushed: true, remoteRef: `refs/heads/${branchName}` }
} catch (err) {
const stderr = (err as { stderr?: string }).stderr ?? (err as Error).message ?? ''
if (/Authentication failed|could not read Username/i.test(stderr)) {
return { pushed: false, reason: 'no-credentials', stderr }
}
if (/non-fast-forward|already exists on the remote|rejected/i.test(stderr)) {
return { pushed: false, reason: 'conflict', stderr }
}
return { pushed: false, reason: 'unknown', stderr }
}
}