mirror of
https://github.com/stablyai/orca.git
synced 2026-10-14 16:02:57 +00:00
* WIP: Changes before auto-review fixes Co-authored-by: Orca <help@stably.ai> * WIP: Changes before auto-review fixes Co-authored-by: Orca <help@stably.ai> * fix(worktree): preserve user push.autoSetupRemote, include path in warn - Probe push.autoSetupRemote with `git config --get` before writing so a deliberate user value at any scope (local/global/system) is preserved. - Include worktree path in the warn log for failed config writes. - Add test pinning the preserve-existing-value behavior. - Remove stray 00-review-context.md committed during review tooling. Co-authored-by: Orca <help@stably.ai> * WIP: Changes before auto-review fixes Co-authored-by: Orca <help@stably.ai> * fix(worktree): narrow config --get error handling, tighten test asserts Treat only exit code 1 from `git config --get push.autoSetupRemote` as "key unset". Other read failures (corrupt config, locked file, parse error) now re-throw to the outer warn handler instead of being silently treated as unset and overwriting whatever value the user actually has. Also: add test for the non-unset read-error path; convert the "preserves existing value" test from `.some()` predicates to a full-array `toEqual` matching sibling-test style; explicitly mock `config --get` (with code: 1) in the sparse-failure rollback test so it exercises the intended branch instead of the helper's empty- stdout fallthrough; document in the design notes that addSparseWorktree's rollback intentionally does not unset push.autoSetupRemote. Co-authored-by: Orca <help@stably.ai> * test(worktree): pin --get-empty-stdout and worktree-add-fail invariants Why: addWorktree's post-create config probe has two ordering invariants worth pinning so a future refactor can't silently regress them: (1) `git config --get` succeeding with empty stdout still counts as "already set" so we don't overwrite an explicit empty value, and (2) the entire config block is skipped when `worktree add` itself rejects. Co-authored-by: Orca <help@stably.ai> * WIP: Changes before auto-review fixes Co-authored-by: Orca <help@stably.ai> * docs(worktree): cross-ref local↔SSH addWorktree, clarify SSH-host git version, add empty-stdout parity test JSDoc on local addWorktree now flags the push.autoSetupRemote side effect; both paths cross-reference each other so the next change keeps them in lockstep. Relay comment clarifies that the git version that matters is the SSH host's, not the client's. Adds the missing empty-stdout-as-already-set parity test on the relay side. Co-authored-by: Orca <help@stably.ai> * chore: remove 00-review-context.md from PR Stray file from local review workflow; should not ship in this PR. Co-authored-by: Orca <help@stably.ai> * chore: remove worktree-ssh-no-track-parity.md from PR Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
134 lines
5.3 KiB
TypeScript
134 lines
5.3 KiB
TypeScript
/**
|
|
* Worktree management and commit operations for the relay git handler.
|
|
*
|
|
* Why: extracted from git-handler-ops.ts to keep all relay files under
|
|
* the oxlint max-lines (300) limit.
|
|
*/
|
|
import * as path from 'path'
|
|
import type { GitExec } from './git-handler-ops'
|
|
|
|
// ─── Worktree management ─────────────────────────────────────────────
|
|
|
|
export async function addWorktreeOp(git: GitExec, params: Record<string, unknown>): Promise<void> {
|
|
const repoPath = params.repoPath as string
|
|
const branchName = params.branchName as string
|
|
const targetDir = params.targetDir as string
|
|
const base = params.base as string | undefined
|
|
|
|
// Why: a branchName starting with '-' would be interpreted as a git flag,
|
|
// potentially changing the command's semantics (e.g. "--detach").
|
|
if (branchName.startsWith('-') || (base && base.startsWith('-'))) {
|
|
throw new Error('Branch name and base ref must not start with "-"')
|
|
}
|
|
|
|
// Why: --no-track + push.autoSetupRemote=true mirrors the local
|
|
// addWorktree path (src/main/git/worktree.ts). Keeping the SSH path in
|
|
// sync prevents a transport-only divergence where "Orca creates a
|
|
// worktree" produces a different `git status` / `git push` UX based on
|
|
// whether the repo is local or SSH-mounted. See full design rationale
|
|
// (state machine, common-dir scope, old-git fallback) in the comments
|
|
// around src/main/git/worktree.ts addWorktree — those invariants apply
|
|
// identically here.
|
|
const args = ['worktree', 'add', '--no-track', '-b', branchName, targetDir]
|
|
if (base) {
|
|
args.push(base)
|
|
}
|
|
|
|
await git(args, repoPath)
|
|
|
|
// Why: best-effort write so a deliberate user value (any scope) is
|
|
// preserved and a real read failure is not silently overwritten. Final
|
|
// catch is warn-only — if the remote host's git is <2.37 the value is
|
|
// ignored at push time and the user falls back to `git push -u` once.
|
|
// (Note: it is the SSH host's git that matters here, not the client's.)
|
|
// Mirrors local addWorktree exactly.
|
|
try {
|
|
let alreadySet = false
|
|
try {
|
|
await git(['config', '--get', 'push.autoSetupRemote'], targetDir)
|
|
alreadySet = true
|
|
} catch (readError) {
|
|
// Why: `git config --get` exits 1 only when the key is unset at every
|
|
// scope. Any other code is a real read failure (corrupt config,
|
|
// locked file) — surface it via the outer catch instead of falling
|
|
// through to overwrite the user's actual value.
|
|
const code = (readError as { code?: unknown })?.code
|
|
if (code !== 1) {
|
|
throw readError
|
|
}
|
|
}
|
|
if (!alreadySet) {
|
|
await git(['config', '--local', 'push.autoSetupRemote', 'true'], targetDir)
|
|
}
|
|
} catch (error) {
|
|
console.warn(`relay addWorktree: failed to set push.autoSetupRemote for ${targetDir}`, error)
|
|
}
|
|
}
|
|
|
|
export async function removeWorktreeOp(
|
|
git: GitExec,
|
|
params: Record<string, unknown>
|
|
): Promise<void> {
|
|
const worktreePath = params.worktreePath as string
|
|
const force = params.force as boolean | undefined
|
|
|
|
let repoPath = worktreePath
|
|
try {
|
|
const { stdout } = await git(['rev-parse', '--git-common-dir'], worktreePath)
|
|
const commonDir = stdout.trim()
|
|
if (commonDir && commonDir !== '.git') {
|
|
repoPath = path.resolve(worktreePath, commonDir, '..')
|
|
}
|
|
} catch {
|
|
// fall through with worktreePath as repo
|
|
}
|
|
|
|
const args = ['worktree', 'remove']
|
|
if (force) {
|
|
args.push('--force')
|
|
}
|
|
args.push(worktreePath)
|
|
await git(args, repoPath)
|
|
await git(['worktree', 'prune'], repoPath)
|
|
}
|
|
|
|
// ─── Commit ──────────────────────────────────────────────────────────
|
|
|
|
export async function commitChangesRelay(
|
|
git: GitExec,
|
|
worktreePath: string,
|
|
message: string
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
// Why: defense-in-depth. The IPC handler at src/main/ipc/filesystem.ts validates
|
|
// the message, but a relay caller (future automation, or an SSH client connecting
|
|
// to the relay directly) could bypass that path. Reject empty/whitespace messages
|
|
// here so we surface a clear error instead of git's opaque failure.
|
|
if (typeof message !== 'string' || message.trim().length === 0) {
|
|
return { success: false, error: 'Commit message is required' }
|
|
}
|
|
|
|
try {
|
|
await git(['commit', '-m', message], worktreePath)
|
|
return { success: true }
|
|
} catch (error) {
|
|
// Why: surface whichever channel carries the useful message. Pre-commit/GPG
|
|
// hook failures write to stderr; "nothing to commit, working tree clean"
|
|
// writes to stdout. Try stderr first, fall back to stdout, then error.message.
|
|
// Mirrors commitChanges in src/main/git/status.ts — keep the two paths in sync.
|
|
const readStringField = (field: string): string | null => {
|
|
if (typeof error === 'object' && error && field in error) {
|
|
const v = (error as Record<string, unknown>)[field]
|
|
if (typeof v === 'string' && v.length > 0) {
|
|
return v
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
const errorMessage =
|
|
readStringField('stderr') ??
|
|
readStringField('stdout') ??
|
|
(error instanceof Error ? error.message : 'Commit failed')
|
|
return { success: false, error: errorMessage }
|
|
}
|
|
}
|