mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
* fix: reduce worktree deletion branch cleanup overhead Refresh only the remote base branches needed to prove a branch merged, with automatic maintenance disabled. Skip merge simulation when no common ancestor is available and preserve the branch when its remote base cannot be refreshed. * fix: preserve fetch mappings and live branch cleanup fallbacks * fix: spawn macOS worktree deletion commands off the main thread
124 lines
3.9 KiB
TypeScript
124 lines
3.9 KiB
TypeScript
import type { GitBranchCleanupExec } from './git-branch-cleanup'
|
|
import { GIT_FETCH_SKIP_AUTO_MAINTENANCE_CONFIG_ARGS } from './git-fetch-auto-maintenance'
|
|
import { isMissingRemoteRefGitError } from './git-missing-remote-ref'
|
|
|
|
function matchRefPattern(pattern: string, ref: string): string | null {
|
|
const star = pattern.indexOf('*')
|
|
if (star === -1) {
|
|
return pattern === ref ? '' : null
|
|
}
|
|
if (star !== pattern.lastIndexOf('*')) {
|
|
return null
|
|
}
|
|
const prefix = pattern.slice(0, star)
|
|
const suffix = pattern.slice(star + 1)
|
|
return ref.startsWith(prefix) &&
|
|
ref.endsWith(suffix) &&
|
|
ref.length >= prefix.length + suffix.length
|
|
? ref.slice(prefix.length, ref.length - suffix.length)
|
|
: null
|
|
}
|
|
|
|
function sourceRefForTarget(refspecs: string[], target: string): string | undefined {
|
|
if (target.includes('*')) {
|
|
return undefined
|
|
}
|
|
const sources = new Set<string>()
|
|
for (const refspec of refspecs) {
|
|
const parts = refspec.replace(/^\+/, '').split(':')
|
|
const [source, destination] = parts
|
|
if (
|
|
parts.length !== 2 ||
|
|
!source?.startsWith('refs/') ||
|
|
!destination ||
|
|
source.includes('*') !== destination.includes('*')
|
|
) {
|
|
continue
|
|
}
|
|
const capture = matchRefPattern(destination, target)
|
|
if (capture !== null) {
|
|
const resolved = source.replace('*', capture)
|
|
if (!resolved.includes('*')) {
|
|
sources.add(resolved)
|
|
}
|
|
}
|
|
}
|
|
const source = [...sources][0]
|
|
if (sources.size !== 1 || !source) {
|
|
return undefined
|
|
}
|
|
// Explicit refspecs bypass negative fetch mappings, so preserve their exclusions here.
|
|
return refspecs.some(
|
|
(refspec) => refspec.startsWith('^') && matchRefPattern(refspec.slice(1), source) !== null
|
|
)
|
|
? undefined
|
|
: source
|
|
}
|
|
|
|
export async function refreshBranchCleanupTargetRefs(
|
|
runGit: GitBranchCleanupExec,
|
|
targetRefs: readonly string[]
|
|
): Promise<string[]> {
|
|
let remotes: string[]
|
|
try {
|
|
remotes = (await runGit(['remote'])).stdout
|
|
.split(/\r?\n/)
|
|
.map((line) => line.trim())
|
|
.filter((remote) => remote && !remote.startsWith('-'))
|
|
.sort((left, right) => right.length - left.length)
|
|
} catch {
|
|
return []
|
|
}
|
|
const targetsByRemote = new Map<string, Set<string>>()
|
|
for (const target of targetRefs) {
|
|
const remote = remotes.find((name) => target.startsWith(`refs/remotes/${name}/`))
|
|
if (remote) {
|
|
const targets = targetsByRemote.get(remote) ?? new Set<string>()
|
|
targets.add(target)
|
|
targetsByRemote.set(remote, targets)
|
|
}
|
|
}
|
|
const refreshed: string[] = []
|
|
for (const [remote, targets] of targetsByRemote) {
|
|
let refspecs: string[]
|
|
try {
|
|
refspecs = (await runGit(['config', '--get-all', `remote.${remote}.fetch`])).stdout
|
|
.split(/\r?\n/)
|
|
.map((line) => line.trim())
|
|
.filter(Boolean)
|
|
} catch {
|
|
continue
|
|
}
|
|
const mappings = [...targets].flatMap((target) => {
|
|
const source = sourceRefForTarget(refspecs, target)
|
|
return source ? [{ target, refspec: `+${source}:${target}` }] : []
|
|
})
|
|
if (mappings.length === 0) {
|
|
continue
|
|
}
|
|
const fetch = async (batch: typeof mappings): Promise<void> => {
|
|
// Deletion must not fetch every feature branch or launch disk-heavy auto-maintenance.
|
|
await runGit([
|
|
...GIT_FETCH_SKIP_AUTO_MAINTENANCE_CONFIG_ARGS,
|
|
'fetch',
|
|
'--no-tags',
|
|
remote,
|
|
...batch.map((mapping) => mapping.refspec)
|
|
])
|
|
refreshed.push(...batch.map((mapping) => mapping.target))
|
|
}
|
|
try {
|
|
await fetch(mappings)
|
|
} catch (error) {
|
|
if (mappings.length > 1 && isMissingRemoteRefGitError(error)) {
|
|
// A deleted saved base must not prevent the live default branch from being refreshed.
|
|
for (const mapping of mappings) {
|
|
await fetch([mapping]).catch(() => {})
|
|
}
|
|
}
|
|
// A stale remote-tracking ref cannot prove the remote still retains these changes.
|
|
}
|
|
}
|
|
return refreshed
|
|
}
|