mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 08:02:02 +00:00
feat: add WSL support for repos on WSL filesystems (#375)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
beb62ff580
commit
f6ec69c969
+12
-24
@@ -2,7 +2,7 @@
|
||||
concurrency acquire/release pattern and error handling consistent across operations. */
|
||||
import type { PRInfo, PRMergeableState, PRCheckDetail, PRComment } from '../../shared/types'
|
||||
import { getPRConflictSummary } from './conflict-summary'
|
||||
import { execFileAsync, acquire, release, getOwnerRepo } from './gh-utils'
|
||||
import { execFileAsync, ghExecFileAsync, acquire, release, getOwnerRepo } from './gh-utils'
|
||||
export { _resetOwnerRepoCache } from './gh-utils'
|
||||
export { getIssue, listIssues } from './issues'
|
||||
import {
|
||||
@@ -88,8 +88,7 @@ export async function getPRForBranch(repoPath: string, branch: string): Promise<
|
||||
} | null = null
|
||||
|
||||
if (ownerRepo) {
|
||||
const { stdout } = await execFileAsync(
|
||||
'gh',
|
||||
const { stdout } = await ghExecFileAsync(
|
||||
[
|
||||
'pr',
|
||||
'list',
|
||||
@@ -104,16 +103,12 @@ export async function getPRForBranch(repoPath: string, branch: string): Promise<
|
||||
'--json',
|
||||
'number,title,state,url,statusCheckRollup,updatedAt,isDraft,mergeable,baseRefName,headRefName,baseRefOid,headRefOid'
|
||||
],
|
||||
{
|
||||
cwd: repoPath,
|
||||
encoding: 'utf-8'
|
||||
}
|
||||
{ cwd: repoPath }
|
||||
)
|
||||
const list = JSON.parse(stdout) as NonNullable<typeof data>[]
|
||||
data = list[0] ?? null
|
||||
} else {
|
||||
const { stdout } = await execFileAsync(
|
||||
'gh',
|
||||
const { stdout } = await ghExecFileAsync(
|
||||
[
|
||||
'pr',
|
||||
'view',
|
||||
@@ -121,10 +116,7 @@ export async function getPRForBranch(repoPath: string, branch: string): Promise<
|
||||
'--json',
|
||||
'number,title,state,url,statusCheckRollup,updatedAt,isDraft,mergeable,baseRefName,headRefName,baseRefOid,headRefOid'
|
||||
],
|
||||
{
|
||||
cwd: repoPath,
|
||||
encoding: 'utf-8'
|
||||
}
|
||||
{ cwd: repoPath }
|
||||
)
|
||||
data = JSON.parse(stdout)
|
||||
}
|
||||
@@ -175,14 +167,13 @@ export async function getPRChecks(
|
||||
// user explicitly clicks refresh we must skip it so gh fetches fresh data.
|
||||
const cacheArgs = options?.noCache ? [] : ['--cache', '60s']
|
||||
try {
|
||||
const { stdout } = await execFileAsync(
|
||||
'gh',
|
||||
const { stdout } = await ghExecFileAsync(
|
||||
[
|
||||
'api',
|
||||
...cacheArgs,
|
||||
`repos/${ownerRepo.owner}/${ownerRepo.repo}/commits/${encodeURIComponent(headSha)}/check-runs?per_page=100`
|
||||
],
|
||||
{ cwd: repoPath, encoding: 'utf-8' }
|
||||
{ cwd: repoPath }
|
||||
)
|
||||
const data = JSON.parse(stdout) as {
|
||||
check_runs: {
|
||||
@@ -207,10 +198,9 @@ export async function getPRChecks(
|
||||
}
|
||||
}
|
||||
// Fallback: no branch provided or non-GitHub remote
|
||||
const { stdout } = await execFileAsync(
|
||||
'gh',
|
||||
const { stdout } = await ghExecFileAsync(
|
||||
['pr', 'checks', String(prNumber), '--json', 'name,state,link'],
|
||||
{ cwd: repoPath, encoding: 'utf-8' }
|
||||
{ cwd: repoPath }
|
||||
)
|
||||
const data = JSON.parse(stdout) as { name: string; state: string; link: string }[]
|
||||
return data.map((d) => ({
|
||||
@@ -490,9 +480,8 @@ export async function mergePR(
|
||||
// Don't use --delete-branch: it tries to delete the local branch which
|
||||
// fails when the user's worktree is checked out on it. Branch cleanup
|
||||
// is handled by worktree deletion (local) and GitHub's auto-delete setting (remote).
|
||||
await execFileAsync('gh', ['pr', 'merge', String(prNumber), `--${method}`], {
|
||||
await ghExecFileAsync(['pr', 'merge', String(prNumber), `--${method}`], {
|
||||
cwd: repoPath,
|
||||
encoding: 'utf-8',
|
||||
env: { ...process.env, GH_PROMPT_DISABLED: '1' }
|
||||
})
|
||||
return { ok: true }
|
||||
@@ -515,9 +504,8 @@ export async function updatePRTitle(
|
||||
): Promise<boolean> {
|
||||
await acquire()
|
||||
try {
|
||||
await execFileAsync('gh', ['pr', 'edit', String(prNumber), '--title', title], {
|
||||
cwd: repoPath,
|
||||
encoding: 'utf-8'
|
||||
await ghExecFileAsync(['pr', 'edit', String(prNumber), '--title', title], {
|
||||
cwd: repoPath
|
||||
})
|
||||
return true
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { execFile } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import type { PRConflictSummary } from '../../shared/types'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
import { gitExecFileAsync } from '../git/runner'
|
||||
|
||||
export async function getPRConflictSummary(
|
||||
repoPath: string,
|
||||
@@ -47,9 +44,8 @@ async function resolveLatestBaseOid(
|
||||
try {
|
||||
// Why: cap the fetch at 10 s so slow or unreachable remotes don't block
|
||||
// the conflict-summary derivation indefinitely.
|
||||
await execFileAsync('git', ['fetch', '--quiet', remoteName, baseRefName], {
|
||||
await gitExecFileAsync(['fetch', '--quiet', remoteName, baseRefName], {
|
||||
cwd: repoPath,
|
||||
encoding: 'utf-8',
|
||||
timeout: 10_000
|
||||
})
|
||||
} catch {
|
||||
@@ -60,9 +56,8 @@ async function resolveLatestBaseOid(
|
||||
|
||||
for (const ref of [`refs/remotes/${remoteName}/${baseRefName}`, `${remoteName}/${baseRefName}`]) {
|
||||
try {
|
||||
const { stdout } = await execFileAsync('git', ['rev-parse', '--verify', ref], {
|
||||
cwd: repoPath,
|
||||
encoding: 'utf-8'
|
||||
const { stdout } = await gitExecFileAsync(['rev-parse', '--verify', ref], {
|
||||
cwd: repoPath
|
||||
})
|
||||
const oid = stdout.trim()
|
||||
if (oid) {
|
||||
@@ -81,17 +76,15 @@ async function resolveMergeBase(
|
||||
headOid: string,
|
||||
baseOid: string
|
||||
): Promise<string> {
|
||||
const { stdout } = await execFileAsync('git', ['merge-base', headOid, baseOid], {
|
||||
cwd: repoPath,
|
||||
encoding: 'utf-8'
|
||||
const { stdout } = await gitExecFileAsync(['merge-base', headOid, baseOid], {
|
||||
cwd: repoPath
|
||||
})
|
||||
return stdout.trim()
|
||||
}
|
||||
|
||||
async function countCommits(repoPath: string, range: string): Promise<number> {
|
||||
const { stdout } = await execFileAsync('git', ['rev-list', '--count', range], {
|
||||
cwd: repoPath,
|
||||
encoding: 'utf-8'
|
||||
const { stdout } = await gitExecFileAsync(['rev-list', '--count', range], {
|
||||
cwd: repoPath
|
||||
})
|
||||
return Number.parseInt(stdout.trim(), 10) || 0
|
||||
}
|
||||
@@ -104,8 +97,7 @@ async function loadConflictingFiles(
|
||||
): Promise<string[]> {
|
||||
let stdout = ''
|
||||
try {
|
||||
const result = await execFileAsync(
|
||||
'git',
|
||||
const result = await gitExecFileAsync(
|
||||
[
|
||||
'merge-tree',
|
||||
'--write-tree',
|
||||
@@ -117,10 +109,7 @@ async function loadConflictingFiles(
|
||||
headOid,
|
||||
baseOid
|
||||
],
|
||||
{
|
||||
cwd: repoPath,
|
||||
encoding: 'utf-8'
|
||||
}
|
||||
{ cwd: repoPath }
|
||||
)
|
||||
stdout = result.stdout
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { execFile } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import { gitExecFileAsync, ghExecFileAsync } from '../git/runner'
|
||||
|
||||
// Why: legacy generic execFile wrapper — only used by callers that don't need
|
||||
// WSL-aware routing (e.g. non-repo-scoped gh commands). Repo-scoped callers
|
||||
// should use ghExecFileAsync or gitExecFileAsync from the runner instead.
|
||||
export const execFileAsync = promisify(execFile)
|
||||
export { ghExecFileAsync, gitExecFileAsync }
|
||||
|
||||
// Concurrency limiter - max 4 parallel gh processes
|
||||
const MAX_CONCURRENT = 4
|
||||
@@ -44,9 +49,8 @@ export async function getOwnerRepo(
|
||||
return ownerRepoCache.get(repoPath)!
|
||||
}
|
||||
try {
|
||||
const { stdout } = await execFileAsync('git', ['remote', 'get-url', 'origin'], {
|
||||
cwd: repoPath,
|
||||
encoding: 'utf-8'
|
||||
const { stdout } = await gitExecFileAsync(['remote', 'get-url', 'origin'], {
|
||||
cwd: repoPath
|
||||
})
|
||||
const match = stdout.trim().match(/github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/)
|
||||
if (match) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IssueInfo } from '../../shared/types'
|
||||
import { mapIssueInfo } from './mappers'
|
||||
import { execFileAsync, acquire, release, getOwnerRepo } from './gh-utils'
|
||||
import { ghExecFileAsync, acquire, release, getOwnerRepo } from './gh-utils'
|
||||
|
||||
/**
|
||||
* Get a single issue by number.
|
||||
@@ -11,24 +11,22 @@ export async function getIssue(repoPath: string, issueNumber: number): Promise<I
|
||||
await acquire()
|
||||
try {
|
||||
if (ownerRepo) {
|
||||
const { stdout } = await execFileAsync(
|
||||
'gh',
|
||||
const { stdout } = await ghExecFileAsync(
|
||||
[
|
||||
'api',
|
||||
'--cache',
|
||||
'300s',
|
||||
`repos/${ownerRepo.owner}/${ownerRepo.repo}/issues/${issueNumber}`
|
||||
],
|
||||
{ cwd: repoPath, encoding: 'utf-8' }
|
||||
{ cwd: repoPath }
|
||||
)
|
||||
const data = JSON.parse(stdout)
|
||||
return mapIssueInfo(data)
|
||||
}
|
||||
// Fallback for non-GitHub remotes
|
||||
const { stdout } = await execFileAsync(
|
||||
'gh',
|
||||
const { stdout } = await ghExecFileAsync(
|
||||
['issue', 'view', String(issueNumber), '--json', 'number,title,state,url,labels'],
|
||||
{ cwd: repoPath, encoding: 'utf-8' }
|
||||
{ cwd: repoPath }
|
||||
)
|
||||
const data = JSON.parse(stdout)
|
||||
return mapIssueInfo(data)
|
||||
@@ -48,24 +46,22 @@ export async function listIssues(repoPath: string, limit = 20): Promise<IssueInf
|
||||
await acquire()
|
||||
try {
|
||||
if (ownerRepo) {
|
||||
const { stdout } = await execFileAsync(
|
||||
'gh',
|
||||
const { stdout } = await ghExecFileAsync(
|
||||
[
|
||||
'api',
|
||||
'--cache',
|
||||
'120s',
|
||||
`repos/${ownerRepo.owner}/${ownerRepo.repo}/issues?per_page=${limit}&state=open&sort=updated&direction=desc`
|
||||
],
|
||||
{ cwd: repoPath, encoding: 'utf-8' }
|
||||
{ cwd: repoPath }
|
||||
)
|
||||
const data = JSON.parse(stdout) as unknown[]
|
||||
return data.map((d) => mapIssueInfo(d as Parameters<typeof mapIssueInfo>[0]))
|
||||
}
|
||||
// Fallback for non-GitHub remotes
|
||||
const { stdout } = await execFileAsync(
|
||||
'gh',
|
||||
const { stdout } = await ghExecFileAsync(
|
||||
['issue', 'list', '--json', 'number,title,state,url,labels', '--limit', String(limit)],
|
||||
{ cwd: repoPath, encoding: 'utf-8' }
|
||||
{ cwd: repoPath }
|
||||
)
|
||||
const data = JSON.parse(stdout) as unknown[]
|
||||
return data.map((d) => mapIssueInfo(d as Parameters<typeof mapIssueInfo>[0]))
|
||||
|
||||
Reference in New Issue
Block a user