Files
orca/src/main/git/worktree-include-file.ts
T
OrcaWinandOrcaWin 4687e90a16 [comment audit] Continue main-process comment cleanup (#32946)
* [comment audit] Shorten daemon, hook, and emulator comments [skip ci]

* [comment audit] Shorten VM, conversation, SQLite, and theme-import comments [skip ci]

* [comment audit] Tighten Git execution and source-control comment contracts [skip ci]

* [comment audit] Clarify repository probes and maintenance comments [skip ci]

* [comment audit] Shorten status, diff-cache, and checkout comments [skip ci]

* [comment audit] Clarify worktree lifecycle and WSL routing comments [skip ci]

* [comment audit] Shorten forge identity and review-refresh comments [skip ci]

* [comment audit] Tighten hosted-review client comment contracts [skip ci]

* [comment audit] Clarify GitLab compatibility and review comments [skip ci]

* [comment audit] Tighten hook ownership and watchdog rationale [skip ci]

* [comment audit] Condense host and integration compatibility comments [skip ci]

* [comment audit] Clarify IPC authority and remote-routing rationale [skip ci]

* [comment audit] Finalize file-import and containment comment cleanup

* [comment audit] Fix stale Jira type imports blocking full CI

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-23 16:27:37 -07:00

121 lines
4.0 KiB
TypeScript

import { lstat, readFile } from 'node:fs/promises'
import { isAbsolute, join } from 'node:path'
import { checkIgnoredPaths } from './check-ignored-paths'
import type { GitRuntimeOptions } from './git-runtime-options'
import { mapWithConcurrency } from '../../shared/map-with-concurrency'
/** Cross-tool convention for copying selected gitignored paths into new worktrees (#7549). */
export const WORKTREE_INCLUDE_FILE = '.worktreeinclude'
const WORKTREE_INCLUDE_MAX_FILE_BYTES = 256 * 1024
const WORKTREE_INCLUDE_MAX_ENTRIES = 1000
// Bound stat fan-out without serializing independent paths.
const WORKTREE_INCLUDE_PATH_STAT_CONCURRENCY = 8
/** Entries are literal paths anchored to the repo root, without implicit depth matching. */
export function parseWorktreeIncludeFile(content: string): string[] {
const seen = new Set<string>()
const entries: string[] = []
for (const rawLine of content.split(/\r?\n/)) {
const line = rawLine.trim()
if (!line || line.startsWith('#')) {
continue
}
const normalized = line.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '')
if (!normalized || seen.has(normalized)) {
continue
}
seen.add(normalized)
entries.push(normalized)
}
return entries
}
function isUnsupportedPattern(entry: string): boolean {
return entry.startsWith('!') || entry.includes('*') || entry.includes('?')
}
function isSafeIncludePath(relativePath: string): boolean {
if (!relativePath || isAbsolute(relativePath)) {
return false
}
const segments = relativePath.split('/')
return !segments.includes('..') && !segments.includes('') && segments[0] !== '.git'
}
async function readWorktreeIncludeFile(repoPath: string): Promise<string | null> {
const includePath = join(repoPath, WORKTREE_INCLUDE_FILE)
try {
const stats = await lstat(includePath)
if (!stats.isFile() || stats.size > WORKTREE_INCLUDE_MAX_FILE_BYTES) {
return null
}
return await readFile(includePath, 'utf8')
} catch {
return null
}
}
/** Copy only existing, gitignored paths; failures return [] so inclusion cannot block creation. */
export async function resolveWorktreeIncludePaths(
repoPath: string,
options: GitRuntimeOptions = {}
): Promise<string[]> {
try {
const content = await readWorktreeIncludeFile(repoPath)
if (content === null) {
return []
}
const candidates: string[] = []
for (const entry of parseWorktreeIncludeFile(content)) {
if (candidates.length >= WORKTREE_INCLUDE_MAX_ENTRIES) {
console.warn(
`[worktree-include] ${WORKTREE_INCLUDE_FILE} lists more than ${WORKTREE_INCLUDE_MAX_ENTRIES} entries; ignoring the rest`
)
break
}
if (isUnsupportedPattern(entry)) {
console.warn(
`[worktree-include] Skipping unsupported ${WORKTREE_INCLUDE_FILE} pattern "${entry}" (only literal files and directories are supported)`
)
continue
}
if (!isSafeIncludePath(entry)) {
console.warn(`[worktree-include] Skipping unsafe ${WORKTREE_INCLUDE_FILE} path "${entry}"`)
continue
}
candidates.push(entry)
}
if (candidates.length === 0) {
return []
}
const existence = await mapWithConcurrency(
candidates,
WORKTREE_INCLUDE_PATH_STAT_CONCURRENCY,
async (relativePath) => {
try {
await lstat(join(repoPath, relativePath))
return relativePath
} catch {
return null
}
}
)
const existing = existence.filter(
(relativePath): relativePath is string => relativePath !== null
)
if (existing.length === 0) {
return []
}
// Copying unignored paths would introduce spurious worktree diffs.
const ignored = new Set(await checkIgnoredPaths(repoPath, existing, options))
return existing.filter((relativePath) => ignored.has(relativePath)).sort()
} catch (error) {
console.warn(`[worktree-include] Failed to resolve ${WORKTREE_INCLUDE_FILE} paths:`, error)
return []
}
}