skip-files.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. #!/usr/bin/env bun
  2. /**
  3. * Skip files transform - handles files that should be completely skipped during merge
  4. *
  5. * These are files that exist in upstream but should NOT exist in Kilo fork.
  6. * Examples: README.*.md (translated READMEs), STATS.md, etc.
  7. *
  8. * During merge, these files will be:
  9. * - Removed if they were added from upstream
  10. * - Kept deleted if they don't exist in Kilo
  11. */
  12. import { $ } from "bun"
  13. import { info, success, warn, debug } from "../utils/logger"
  14. import { defaultConfig } from "../utils/config"
  15. export interface SkipResult {
  16. file: string
  17. action: "removed" | "skipped" | "not-found"
  18. dryRun: boolean
  19. }
  20. export interface SkipOptions {
  21. dryRun?: boolean
  22. verbose?: boolean
  23. patterns?: string[]
  24. }
  25. /**
  26. * Check if a file matches any skip patterns
  27. */
  28. export function shouldSkip(filePath: string, patterns: string[]): boolean {
  29. return patterns.some((pattern) => {
  30. // Exact match
  31. if (filePath === pattern) return true
  32. // Regex pattern (e.g., README\.[a-z]+\.md)
  33. if (pattern.startsWith("^") || pattern.includes("\\")) {
  34. const regex = new RegExp(pattern)
  35. return regex.test(filePath)
  36. }
  37. // Glob-style pattern
  38. if (pattern.includes("*")) {
  39. const regex = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$")
  40. return regex.test(filePath)
  41. }
  42. return false
  43. })
  44. }
  45. /**
  46. * Get list of files that were added/modified from upstream during merge
  47. */
  48. async function getUpstreamFiles(): Promise<string[]> {
  49. // Get files that are staged (after merge)
  50. const result = await $`git diff --cached --name-only`.quiet().nothrow()
  51. if (result.exitCode !== 0) return []
  52. return result.stdout
  53. .toString()
  54. .trim()
  55. .split("\n")
  56. .filter((f) => f.length > 0)
  57. }
  58. /**
  59. * Get list of unmerged (conflicted) files
  60. */
  61. async function getUnmergedFiles(): Promise<string[]> {
  62. const result = await $`git diff --name-only --diff-filter=U`.quiet().nothrow()
  63. if (result.exitCode !== 0) return []
  64. return result.stdout
  65. .toString()
  66. .trim()
  67. .split("\n")
  68. .filter((f) => f.length > 0)
  69. }
  70. /**
  71. * Check if a file exists in a specific git ref
  72. */
  73. async function fileExistsInRef(file: string, ref: string): Promise<boolean> {
  74. const result = await $`git cat-file -e ${ref}:${file}`.quiet().nothrow()
  75. return result.exitCode === 0
  76. }
  77. /**
  78. * Remove a file from the merge (git rm)
  79. */
  80. async function removeFile(file: string): Promise<boolean> {
  81. const result = await $`git rm -f ${file}`.quiet().nothrow()
  82. return result.exitCode === 0
  83. }
  84. /**
  85. * Skip files that shouldn't exist in Kilo fork
  86. *
  87. * This function handles files that:
  88. * 1. Match skip patterns (like README.*.md)
  89. * 2. Were added from upstream during merge
  90. * 3. Don't exist in Kilo's version (HEAD before merge)
  91. */
  92. export async function skipFiles(options: SkipOptions = {}): Promise<SkipResult[]> {
  93. const results: SkipResult[] = []
  94. const patterns = options.patterns || defaultConfig.skipFiles
  95. if (!patterns || patterns.length === 0) {
  96. info("No skip patterns configured")
  97. return results
  98. }
  99. // Get all files involved in the merge
  100. const stagedFiles = await getUpstreamFiles()
  101. const unmergedFiles = await getUnmergedFiles()
  102. const allFiles = [...new Set([...stagedFiles, ...unmergedFiles])]
  103. if (allFiles.length === 0) {
  104. info("No files to process")
  105. return results
  106. }
  107. debug(`Checking ${allFiles.length} files against ${patterns.length} skip patterns`)
  108. for (const file of allFiles) {
  109. if (!shouldSkip(file, patterns)) continue
  110. // Check if file existed in Kilo before merge (HEAD~1 or the merge base)
  111. const existedInKilo = await fileExistsInRef(file, "HEAD")
  112. if (existedInKilo) {
  113. debug(`Skipping ${file} - exists in Kilo, not removing`)
  114. results.push({ file, action: "skipped", dryRun: options.dryRun ?? false })
  115. continue
  116. }
  117. // File doesn't exist in Kilo - should be removed
  118. if (options.dryRun) {
  119. info(`[DRY-RUN] Would remove: ${file}`)
  120. results.push({ file, action: "removed", dryRun: true })
  121. } else {
  122. const removed = await removeFile(file)
  123. if (removed) {
  124. success(`Removed: ${file}`)
  125. results.push({ file, action: "removed", dryRun: false })
  126. } else {
  127. warn(`Failed to remove: ${file}`)
  128. results.push({ file, action: "not-found", dryRun: false })
  129. }
  130. }
  131. }
  132. return results
  133. }
  134. /**
  135. * Skip files from a specific list (used during conflict resolution)
  136. */
  137. export async function skipSpecificFiles(files: string[], options: SkipOptions = {}): Promise<SkipResult[]> {
  138. const results: SkipResult[] = []
  139. for (const file of files) {
  140. if (options.dryRun) {
  141. info(`[DRY-RUN] Would remove: ${file}`)
  142. results.push({ file, action: "removed", dryRun: true })
  143. } else {
  144. const removed = await removeFile(file)
  145. if (removed) {
  146. success(`Removed: ${file}`)
  147. results.push({ file, action: "removed", dryRun: false })
  148. } else {
  149. warn(`Failed to remove: ${file}`)
  150. results.push({ file, action: "not-found", dryRun: false })
  151. }
  152. }
  153. }
  154. return results
  155. }
  156. // CLI entry point
  157. if (import.meta.main) {
  158. const args = process.argv.slice(2)
  159. const dryRun = args.includes("--dry-run")
  160. const verbose = args.includes("--verbose")
  161. // Get specific files if provided
  162. const files = args.filter((a) => !a.startsWith("--"))
  163. if (dryRun) {
  164. info("Running in dry-run mode (no files will be modified)")
  165. }
  166. const results =
  167. files.length > 0 ? await skipSpecificFiles(files, { dryRun, verbose }) : await skipFiles({ dryRun, verbose })
  168. const removed = results.filter((r) => r.action === "removed")
  169. console.log()
  170. success(`Removed ${removed.length} files`)
  171. if (dryRun) {
  172. info("Run without --dry-run to apply changes")
  173. }
  174. }