Skill update round trip / roundtrip (false, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, latest) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
139 lines
4.1 KiB
TypeScript
139 lines
4.1 KiB
TypeScript
import { getStatus } from '../git/status'
|
|
import { gitExecFileAsync } from '../git/runner'
|
|
import type { IGitProvider } from '../providers/types'
|
|
import type { GitStatusResult } from '../../shared/git-status-types'
|
|
import type { Repo } from '../../shared/repo-types'
|
|
import type { Worktree } from '../../shared/worktree/types'
|
|
import type { WorkspaceCleanupBlocker } from '../../shared/workspace-cleanup'
|
|
import {
|
|
WORKSPACE_CLEANUP_GIT_READ_TIMEOUT_MS,
|
|
WorkspaceCleanupScanCancelledError,
|
|
withWorkspaceCleanupTimeout
|
|
} from './workspace-cleanup-scan-primitives'
|
|
import { getWorktreeSharedLinkPaths } from '../git/worktree-shared-directories'
|
|
|
|
export type WorkspaceCleanupGitEvidence = {
|
|
clean: boolean | null
|
|
upstreamAhead: number | null
|
|
upstreamBehind: number | null
|
|
checkedAt: number | null
|
|
blockers: WorkspaceCleanupBlocker[]
|
|
}
|
|
|
|
export function createEmptyWorkspaceCleanupGitEvidence(): WorkspaceCleanupGitEvidence {
|
|
return {
|
|
clean: null,
|
|
upstreamAhead: null,
|
|
upstreamBehind: null,
|
|
checkedAt: null,
|
|
blockers: []
|
|
}
|
|
}
|
|
|
|
export async function readWorkspaceCleanupGitEvidence(
|
|
worktree: Worktree,
|
|
repo: Repo,
|
|
provider: IGitProvider | null,
|
|
signal?: AbortSignal
|
|
): Promise<WorkspaceCleanupGitEvidence> {
|
|
const blockers: WorkspaceCleanupBlocker[] = []
|
|
let status: GitStatusResult
|
|
const checkedAt = Date.now()
|
|
const sharedLinkPaths = repo.connectionId ? [] : getWorktreeSharedLinkPaths(repo)
|
|
|
|
try {
|
|
status = await withWorkspaceCleanupTimeout(
|
|
(signal) =>
|
|
repo.connectionId
|
|
? provider!.getStatus(worktree.path, { includeLineStats: false, signal })
|
|
: getStatus(worktree.path, {
|
|
includeLineStats: false,
|
|
signal,
|
|
...(sharedLinkPaths.length > 0 ? { sharedLinkPaths } : {})
|
|
}),
|
|
WORKSPACE_CLEANUP_GIT_READ_TIMEOUT_MS,
|
|
'Timed out reading git status.',
|
|
signal
|
|
)
|
|
} catch (error) {
|
|
if (error instanceof WorkspaceCleanupScanCancelledError) {
|
|
throw error
|
|
}
|
|
return {
|
|
...createEmptyWorkspaceCleanupGitEvidence(),
|
|
blockers: ['git-status-error']
|
|
}
|
|
}
|
|
|
|
if (status.upstreamStatus === undefined) {
|
|
return {
|
|
...createEmptyWorkspaceCleanupGitEvidence(),
|
|
blockers: ['git-status-error']
|
|
}
|
|
}
|
|
|
|
const clean = status.entries.length === 0
|
|
if (!clean) {
|
|
blockers.push('dirty-files')
|
|
}
|
|
|
|
const upstreamAhead = status.upstreamStatus.hasUpstream ? status.upstreamStatus.ahead : null
|
|
const upstreamBehind = status.upstreamStatus.hasUpstream ? status.upstreamStatus.behind : null
|
|
if (upstreamAhead !== null && upstreamAhead > 0) {
|
|
blockers.push('unpushed-commits')
|
|
}
|
|
if (clean && upstreamAhead === null) {
|
|
const unpushedCommitCount = await readUnpushedCommitCount(worktree, repo, provider, signal)
|
|
if (unpushedCommitCount === null) {
|
|
blockers.push('unknown-base')
|
|
} else if (unpushedCommitCount > 0) {
|
|
blockers.push('unpushed-commits')
|
|
}
|
|
}
|
|
|
|
return {
|
|
clean,
|
|
upstreamAhead,
|
|
upstreamBehind,
|
|
checkedAt,
|
|
blockers: uniqueWorkspaceCleanupGitBlockers(blockers)
|
|
}
|
|
}
|
|
|
|
async function readUnpushedCommitCount(
|
|
worktree: Worktree,
|
|
repo: Repo,
|
|
provider: IGitProvider | null,
|
|
signal?: AbortSignal
|
|
): Promise<number | null> {
|
|
try {
|
|
const result = await withWorkspaceCleanupTimeout(
|
|
(signal) =>
|
|
repo.connectionId
|
|
? provider!.exec(['rev-list', '--count', 'HEAD', '--not', '--remotes'], worktree.path, {
|
|
signal
|
|
})
|
|
: gitExecFileAsync(['rev-list', '--count', 'HEAD', '--not', '--remotes'], {
|
|
cwd: worktree.path,
|
|
signal
|
|
}),
|
|
WORKSPACE_CLEANUP_GIT_READ_TIMEOUT_MS,
|
|
'Timed out checking unpushed commits.',
|
|
signal
|
|
)
|
|
const count = Number.parseInt(result.stdout.trim(), 10)
|
|
return Number.isFinite(count) ? count : null
|
|
} catch (error) {
|
|
if (error instanceof WorkspaceCleanupScanCancelledError) {
|
|
throw error
|
|
}
|
|
return null
|
|
}
|
|
}
|
|
|
|
function uniqueWorkspaceCleanupGitBlockers(
|
|
blockers: WorkspaceCleanupBlocker[]
|
|
): WorkspaceCleanupBlocker[] {
|
|
return [...new Set(blockers)]
|
|
}
|