🌳
pt0/deployF/libF/gitF/gitDirtyPathsAI.mts
1import { execSync } from 'child_process'
4const parseGitStatusPorcelain = (stdout: string): string[] =>
5 stdout.split('\n').map(line => {
6 const match = line.match(/^.{2}\s(.+?)(?:\s->\s.+)?$/)
7 return match?.[1]
8 }).filter(Boolean) as string[]
10export const getUncommittedInPathSet = (pathsA: string[]): string[] => {
11 const stdout = execSync('git status --porcelain', { cwd: ptDir, encoding: 'utf8' })
12 if (!stdout.trim()) return []
13 const changedFiles = parseGitStatusPorcelain(stdout)
14 const pathsSet = new Set(pathsA)
15 return changedFiles.filter(f => pathsSet.has(f) || pathsA.some(p => f.startsWith(p + '/')))
18export const getGitignoredInPathSet = (pathsA: string[]): string[] => {
19 if (pathsA.length === 0) return []
20 // Batch to avoid pipe buffer limits; skip paths in submodules (git check-ignore exits 128 on them)
21 const batchSize = 1000
22 const ignoredA: string[] = []
23 for (let i = 0; i < pathsA.length; i += batchSize) {
24 const batch = pathsA.slice(i, i + batchSize)
25 const input = batch.join('\n')
26 try {
27 const stdout = execSync('git check-ignore --stdin', { cwd: ptDir, input, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: 10 * 1024 * 1024 }).trim()
28 if (stdout) ignoredA.push(...stdout.split('\n').filter(Boolean))
29 } catch (e: any) {
30 if (e.status === 1) continue // exit 1 = no paths ignored in this batch
31 if (e.status === 128) continue // exit 128 = fatal (e.g. submodule paths) — skip batch, filtered later by isExcludedPath
32 throw e
33 }
34 }
35 return ignoredA
38export const gitDiffQuietInPaths = (fromSha: string, toSha: string, pathsA: string[]): boolean => {
39 try {
40 execSync(
41 `git diff --quiet ${fromSha}..${toSha} -- ${pathsA.join(' ')}`,
42 { cwd: ptDir, stdio: 'pipe', maxBuffer: 10 * 1024 * 1024 },
43 )
44 return true // exit 0 = no diff
45 } catch {
46 return false // exit 1 = has diff
47 }