| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| import { exec } from 'child_process'; |
| import { promisify } from 'util'; |
| import { createLogger, isValidRemoteName } from '@automaker/utils'; |
|
|
| |
| |
| const execAsync = promisify(exec); |
|
|
| const pathSeparator = process.platform === 'win32' ? ';' : ':'; |
| const _additionalPaths: string[] = []; |
| if (process.platform === 'win32') { |
| if (process.env.LOCALAPPDATA) |
| _additionalPaths.push(`${process.env.LOCALAPPDATA}\\Programs\\Git\\cmd`); |
| if (process.env.PROGRAMFILES) _additionalPaths.push(`${process.env.PROGRAMFILES}\\Git\\cmd`); |
| if (process.env['ProgramFiles(x86)']) |
| _additionalPaths.push(`${process.env['ProgramFiles(x86)']}\\Git\\cmd`); |
| } else { |
| _additionalPaths.push( |
| '/opt/homebrew/bin', |
| '/usr/local/bin', |
| '/home/linuxbrew/.linuxbrew/bin', |
| `${process.env.HOME}/.local/bin` |
| ); |
| } |
| const execEnv = { |
| ...process.env, |
| PATH: [process.env.PATH, ..._additionalPaths.filter(Boolean)].filter(Boolean).join(pathSeparator), |
| }; |
|
|
| const logger = createLogger('PRService'); |
|
|
| export interface ParsedRemote { |
| owner: string; |
| repo: string; |
| } |
|
|
| export interface PrTargetResult { |
| repoUrl: string | null; |
| targetRepo: string | null; |
| pushOwner: string | null; |
| upstreamRepo: string | null; |
| originOwner: string | null; |
| parsedRemotes: Map<string, ParsedRemote>; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function resolvePrTarget({ |
| worktreePath, |
| pushRemote, |
| targetRemote, |
| }: { |
| worktreePath: string; |
| pushRemote: string; |
| targetRemote?: string; |
| }): Promise<PrTargetResult> { |
| |
| |
| if (!isValidRemoteName(pushRemote)) { |
| throw new Error(`Invalid push remote name: "${pushRemote}"`); |
| } |
| if (targetRemote !== undefined && !isValidRemoteName(targetRemote)) { |
| throw new Error(`Invalid target remote name: "${targetRemote}"`); |
| } |
|
|
| let repoUrl: string | null = null; |
| let upstreamRepo: string | null = null; |
| let originOwner: string | null = null; |
| const parsedRemotes: Map<string, ParsedRemote> = new Map(); |
|
|
| try { |
| const { stdout: remotes } = await execAsync('git remote -v', { |
| cwd: worktreePath, |
| env: execEnv, |
| }); |
|
|
| |
| const lines = remotes.split(/\r?\n/); |
| for (const line of lines) { |
| |
| |
| |
| |
| let match = line.match( |
| /^([a-zA-Z0-9._-]+)\s+.*[:/]([^/]+)\/([^/\s]+?)(?:\.git)?\s+\(fetch\)/ |
| ); |
| if (!match) { |
| |
| match = line.match( |
| /^([a-zA-Z0-9._-]+)\s+git@[^:]+:([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/ |
| ); |
| } |
| if (!match) { |
| |
| match = line.match( |
| /^([a-zA-Z0-9._-]+)\s+https?:\/\/[^/]+\/([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/ |
| ); |
| } |
|
|
| if (match) { |
| const [, remoteName, owner, repo] = match; |
| parsedRemotes.set(remoteName, { owner, repo }); |
| if (remoteName === 'upstream') { |
| upstreamRepo = `${owner}/${repo}`; |
| repoUrl = `https://github.com/${owner}/${repo}`; |
| } else if (remoteName === 'origin') { |
| originOwner = owner; |
| if (!repoUrl) { |
| repoUrl = `https://github.com/${owner}/${repo}`; |
| } |
| } |
| } |
| } |
| } catch (err) { |
| |
| logger.debug('Failed to parse git remotes', { worktreePath, error: err }); |
| } |
|
|
| |
| |
| |
| |
| if (targetRemote && parsedRemotes.size === 0) { |
| throw new Error( |
| `targetRemote "${targetRemote}" was specified but no remotes could be parsed from the repository. ` + |
| `Ensure the repository has at least one configured remote (parsedRemotes is empty).` |
| ); |
| } |
|
|
| |
| |
| |
| |
| if (targetRemote && parsedRemotes.size > 0 && !parsedRemotes.has(targetRemote)) { |
| throw new Error(`targetRemote "${targetRemote}" not found in repository remotes`); |
| } |
|
|
| |
| |
| let targetRepo: string | null = null; |
| let pushOwner: string | null = null; |
| if (targetRemote && parsedRemotes.size > 0) { |
| const targetInfo = parsedRemotes.get(targetRemote); |
| const pushInfo = parsedRemotes.get(pushRemote); |
|
|
| |
| |
| |
| if (!pushInfo) { |
| logger.warn('Push remote not found in parsed remotes', { |
| pushRemote, |
| targetRemote, |
| availableRemotes: [...parsedRemotes.keys()], |
| }); |
| throw new Error(`Push remote "${pushRemote}" not found in repository remotes`); |
| } |
|
|
| if (targetInfo) { |
| targetRepo = `${targetInfo.owner}/${targetInfo.repo}`; |
| repoUrl = `https://github.com/${targetInfo.owner}/${targetInfo.repo}`; |
| } |
| pushOwner = pushInfo.owner; |
|
|
| |
| |
| if (targetRemote !== pushRemote && targetInfo) { |
| upstreamRepo = targetRepo; |
| originOwner = pushOwner; |
| } else if (targetInfo) { |
| |
| upstreamRepo = null; |
| originOwner = targetInfo.owner; |
| repoUrl = `https://github.com/${targetInfo.owner}/${targetInfo.repo}`; |
| } |
| } |
|
|
| |
| if (!repoUrl) { |
| try { |
| const { stdout: originUrl } = await execAsync('git config --get remote.origin.url', { |
| cwd: worktreePath, |
| env: execEnv, |
| }); |
| const url = originUrl.trim(); |
|
|
| |
| |
| const match = url.match(/[:/]([^/]+)\/([^/\s]+?)(?:\.git)?$/); |
| if (match) { |
| const [, owner, repo] = match; |
| originOwner = owner; |
| repoUrl = `https://github.com/${owner}/${repo}`; |
| } |
| } catch { |
| |
| } |
| } |
|
|
| return { |
| repoUrl, |
| targetRepo, |
| pushOwner, |
| upstreamRepo, |
| originOwner, |
| parsedRemotes, |
| }; |
| } |
|
|