mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 16:03:17 +00:00
* chore(lint): upgrade oxlint to 1.71 and enable 7 new rules Upgrade oxlint 1.67.0 -> 1.71.0 (1.72 was blocked by the repo's 3-day minimum-release-age supply-chain guard; nothing here needs it). The bump is a no-op on the existing config. Enable 3 error rules (backlog autofixed to zero in this commit) and 4 warn rules (surface signal without gating CI): error (autofixed, behavior-preserving): - unicorn/prefer-node-protocol (~1531 sites: bare builtin -> node:) - typescript/no-import-type-side-effects (~36: all-inline-type -> import type) - unicorn/no-array-reverse (19: copy-then-reverse -> toReversed) warn (real signal, current fires are test-only/correct): - unicorn/no-array-fill-with-reference-type (aliasing footgun guard) - typescript/no-unsafe-function-type (bans bare Function type) - unicorn/prefer-array-flat-map (map().flat() -> flatMap()) - unicorn/prefer-regexp-test (.match() in bool ctx -> .test()) mobile/.oxlintrc.json extends root, so it inherits all 7; the autofix ran from root and covered mobile/ too. Verification (all green): oxlint 0 errors (root+mobile+aux configs), oxfmt clean, typecheck (node+cli+web), vitest 22795 passed / 0 failed, builds (electron-vite + web + cli) succeed. node: rewrites confirmed to skip embedded SSH/CLI string payloads (AST-only); all toReversed sites verified to operate on fresh copies or write-once locals. * chore(lint): bump mobile oxlint to 1.71 so inherited rules parse mobile/ is a standalone pnpm project pinning its own oxlint@1.67, which lacks unicorn/no-array-fill-with-reference-type (needs >=1.70). Since mobile/.oxlintrc.json extends the root config, mobile CI's 'cd mobile && oxlint' failed to parse the new rule. Bump mobile to match root (1.71). Verified in mobile/: oxlint 0 errors, oxfmt --check clean, tsc --noEmit pass, vitest 978 passed / 0 failed. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
240 lines
7.6 KiB
TypeScript
240 lines
7.6 KiB
TypeScript
import { type posix, win32 } from 'node:path'
|
|
|
|
type PathOps = typeof posix
|
|
export type StatPath = (path: string) => Promise<unknown>
|
|
export type ReadPath = (path: string) => Promise<unknown>
|
|
|
|
export async function gitFileProvesOrphanedWorktreeDirectory(args: {
|
|
gitFilePath: string
|
|
worktreePath: string
|
|
repoPath: string
|
|
pathOps: PathOps
|
|
statPath: StatPath
|
|
readPath: ReadPath
|
|
}): Promise<boolean> {
|
|
try {
|
|
const gitEntry = await args.statPath(args.gitFilePath)
|
|
if (!isGitFileStat(gitEntry)) {
|
|
return false
|
|
}
|
|
const gitFileContents = readFileResultToText(await args.readPath(args.gitFilePath))
|
|
return gitFileContents
|
|
? gitFilePointsAtRepoWorktree(
|
|
gitFileContents,
|
|
args.worktreePath,
|
|
args.repoPath,
|
|
args.pathOps,
|
|
args.statPath,
|
|
args.readPath
|
|
)
|
|
: false
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
function isGitFileStat(stat: unknown): boolean {
|
|
const fileStat =
|
|
stat && typeof stat === 'object' ? (stat as { isFile?: () => boolean; type?: unknown }) : null
|
|
return !!fileStat && (fileStat.isFile?.() === true || fileStat.type === 'file')
|
|
}
|
|
|
|
function readFileResultToText(result: unknown): string | null {
|
|
if (typeof result === 'string') {
|
|
return result
|
|
}
|
|
if (Buffer.isBuffer(result)) {
|
|
return result.toString('utf8')
|
|
}
|
|
if (result instanceof Uint8Array) {
|
|
return Buffer.from(result).toString('utf8')
|
|
}
|
|
if (!result || typeof result !== 'object') {
|
|
return null
|
|
}
|
|
const remoteRead = result as { content?: unknown; isBinary?: unknown }
|
|
if (remoteRead.isBinary === true || typeof remoteRead.content !== 'string') {
|
|
return null
|
|
}
|
|
return remoteRead.content
|
|
}
|
|
|
|
function resolveGitdirPath(gitdirPath: string, basePath: string, pathOps: PathOps): string {
|
|
return pathOps.isAbsolute(gitdirPath)
|
|
? pathOps.resolve(gitdirPath)
|
|
: pathOps.resolve(basePath, gitdirPath)
|
|
}
|
|
|
|
function areResolvedPathsEqual(leftPath: string, rightPath: string, pathOps: PathOps): boolean {
|
|
const left = pathOps.normalize(pathOps.resolve(leftPath))
|
|
const right = pathOps.normalize(pathOps.resolve(rightPath))
|
|
return pathOps === win32 ? left.toLowerCase() === right.toLowerCase() : left === right
|
|
}
|
|
|
|
async function resolveRepoWorktreesPath(
|
|
repoPath: string,
|
|
pathOps: PathOps,
|
|
statPath: StatPath,
|
|
readPath: ReadPath
|
|
): Promise<string | null> {
|
|
const repoGitPath = pathOps.join(repoPath, '.git')
|
|
try {
|
|
const repoGitEntry = await statPath(repoGitPath)
|
|
if (!isGitFileStat(repoGitEntry)) {
|
|
return pathOps.resolve(repoPath, '.git', 'worktrees')
|
|
}
|
|
const repoGitContents = readFileResultToText(await readPath(repoGitPath))
|
|
const repoGitdirPath = repoGitContents ? parseGitdirPath(repoGitContents) : null
|
|
if (!repoGitdirPath) {
|
|
return null
|
|
}
|
|
const resolvedRepoGitdirPath = resolveGitdirPath(repoGitdirPath, repoPath, pathOps)
|
|
const parentPath = pathOps.dirname(resolvedRepoGitdirPath)
|
|
if (
|
|
pathOps.basename(parentPath) === 'worktrees' &&
|
|
(await repoGitdirIsLinkedWorktreeAdminEntry(
|
|
resolvedRepoGitdirPath,
|
|
repoPath,
|
|
pathOps,
|
|
readPath
|
|
))
|
|
) {
|
|
return parentPath
|
|
}
|
|
return pathOps.join(resolvedRepoGitdirPath, 'worktrees')
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
async function repoGitdirIsLinkedWorktreeAdminEntry(
|
|
resolvedRepoGitdirPath: string,
|
|
repoPath: string,
|
|
pathOps: PathOps,
|
|
readPath: ReadPath
|
|
): Promise<boolean> {
|
|
const adminGitdirPath = pathOps.join(resolvedRepoGitdirPath, 'gitdir')
|
|
try {
|
|
const adminGitdirContents = readFileResultToText(await readPath(adminGitdirPath))
|
|
const repoGitPath = adminGitdirContents ? parseFirstLinePath(adminGitdirContents) : null
|
|
if (!repoGitPath) {
|
|
return false
|
|
}
|
|
const resolvedRepoGitPath = resolveGitdirPath(repoGitPath, resolvedRepoGitdirPath, pathOps)
|
|
const resolvedExpectedGitPath = pathOps.resolve(repoPath, '.git')
|
|
// Why: a separate git dir can live under a directory named `worktrees`;
|
|
// only the admin backlink proves the repo path is itself a linked worktree.
|
|
return areResolvedPathsEqual(resolvedRepoGitPath, resolvedExpectedGitPath, pathOps)
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
async function gitFilePointsAtRepoWorktree(
|
|
contents: string,
|
|
worktreePath: string,
|
|
repoPath: string,
|
|
pathOps: PathOps,
|
|
statPath: StatPath,
|
|
readPath: ReadPath
|
|
): Promise<boolean> {
|
|
const gitdirPath = parseGitdirPath(contents)
|
|
if (!gitdirPath) {
|
|
return false
|
|
}
|
|
const resolvedGitdirPath = resolveGitdirPath(gitdirPath, worktreePath, pathOps)
|
|
const repoWorktreesPath = await resolveRepoWorktreesPath(repoPath, pathOps, statPath, readPath)
|
|
if (!repoWorktreesPath || !containsPath(repoWorktreesPath, resolvedGitdirPath, pathOps)) {
|
|
return false
|
|
}
|
|
const relativeGitdirPath = pathOps.relative(repoWorktreesPath, resolvedGitdirPath)
|
|
if (relativeGitdirPath === '' || relativeGitdirPath.includes(pathOps.sep)) {
|
|
return false
|
|
}
|
|
return adminGitdirPointsAtCandidate(resolvedGitdirPath, worktreePath, pathOps, statPath, readPath)
|
|
}
|
|
|
|
async function adminGitdirPointsAtCandidate(
|
|
resolvedGitdirPath: string,
|
|
worktreePath: string,
|
|
pathOps: PathOps,
|
|
statPath: StatPath,
|
|
readPath: ReadPath
|
|
): Promise<boolean> {
|
|
const adminGitdirPath = pathOps.join(resolvedGitdirPath, 'gitdir')
|
|
try {
|
|
const adminGitdirContents = readFileResultToText(await readPath(adminGitdirPath))
|
|
const candidateGitPath = adminGitdirContents ? parseFirstLinePath(adminGitdirContents) : null
|
|
if (!candidateGitPath) {
|
|
return false
|
|
}
|
|
const resolvedCandidateGitPath = resolveGitdirPath(
|
|
candidateGitPath,
|
|
resolvedGitdirPath,
|
|
pathOps
|
|
)
|
|
const resolvedExpectedGitPath = pathOps.resolve(worktreePath, '.git')
|
|
// Why: copied .git files can target another worktree's admin entry; only
|
|
// Git's back-reference proves that entry still belongs to this candidate.
|
|
return areResolvedPathsEqual(resolvedCandidateGitPath, resolvedExpectedGitPath, pathOps)
|
|
} catch (error) {
|
|
if (!isMissingPathError(error)) {
|
|
return false
|
|
}
|
|
try {
|
|
await statPath(resolvedGitdirPath)
|
|
return false
|
|
} catch (statError) {
|
|
return isMissingPathError(statError)
|
|
}
|
|
}
|
|
}
|
|
|
|
function containsPath(parentPath: string, childPath: string, pathOps: PathOps): boolean {
|
|
const relativePath = pathOps.relative(parentPath, childPath)
|
|
// Why: `..name` is a valid child name; only `..` and `../...` escape.
|
|
return (
|
|
relativePath === '' ||
|
|
(!!relativePath &&
|
|
relativePath !== '..' &&
|
|
!relativePath.startsWith(`..${pathOps.sep}`) &&
|
|
!pathOps.isAbsolute(relativePath))
|
|
)
|
|
}
|
|
|
|
function parseGitdirPath(contents: string): string | null {
|
|
const firstLine = contents.split(/\r?\n/, 1)[0]?.trim()
|
|
if (!firstLine) {
|
|
return null
|
|
}
|
|
const match = /^gitdir:\s*(.+)$/i.exec(firstLine)
|
|
const gitdirPath = match?.[1]?.trim()
|
|
return gitdirPath || null
|
|
}
|
|
|
|
function parseFirstLinePath(contents: string): string | null {
|
|
const firstLine = contents.split(/\r?\n/, 1)[0]?.trim()
|
|
return firstLine || null
|
|
}
|
|
|
|
function isMissingPathError(error: unknown): boolean {
|
|
const code =
|
|
error && typeof error === 'object' && 'code' in error
|
|
? String((error as NodeJS.ErrnoException).code)
|
|
: undefined
|
|
if (code === 'ENOENT' || code === 'ENOTDIR') {
|
|
return true
|
|
}
|
|
const message =
|
|
error instanceof Error
|
|
? error.message
|
|
: error && typeof error === 'object' && 'message' in error
|
|
? String((error as { message: unknown }).message)
|
|
: typeof error === 'string'
|
|
? error
|
|
: ''
|
|
return /\b(ENOENT|ENOTDIR)\b|no such file or directory|cannot find (?:the )?(?:file|path)|(?:file|path) not found/i.test(
|
|
message
|
|
)
|
|
}
|