keep-ours.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. #!/usr/bin/env bun
  2. /**
  3. * Keep Kilo's version of specific files during merge
  4. *
  5. * This script handles files that should always keep Kilo's version
  6. * and not be overwritten by upstream changes.
  7. */
  8. import { $ } from "bun"
  9. import { info, success, warn, error, debug } from "../utils/logger"
  10. import { defaultConfig } from "../utils/config"
  11. import { checkoutOurs, stageFiles, getConflictedFiles } from "../utils/git"
  12. export interface KeepOursResult {
  13. file: string
  14. action: "kept" | "skipped" | "not-conflicted"
  15. dryRun: boolean
  16. }
  17. export interface KeepOursOptions {
  18. dryRun?: boolean
  19. verbose?: boolean
  20. files?: string[]
  21. }
  22. /**
  23. * Check if a file matches any keep-ours patterns
  24. */
  25. export function shouldKeepOurs(filePath: string, patterns: string[]): boolean {
  26. return patterns.some((pattern) => {
  27. // Exact match
  28. if (filePath === pattern) return true
  29. // Pattern match (simple glob)
  30. if (pattern.includes("*")) {
  31. const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$")
  32. return regex.test(filePath)
  33. }
  34. // Contains match
  35. return filePath.includes(pattern)
  36. })
  37. }
  38. /**
  39. * Keep Kilo's version of conflicted files
  40. */
  41. export async function keepOursFiles(options: KeepOursOptions = {}): Promise<KeepOursResult[]> {
  42. const results: KeepOursResult[] = []
  43. // Get list of conflicted files
  44. const conflicted = await getConflictedFiles()
  45. if (conflicted.length === 0) {
  46. info("No conflicted files found")
  47. return results
  48. }
  49. info(`Found ${conflicted.length} conflicted files`)
  50. const patterns = options.files || defaultConfig.keepOurs
  51. for (const file of conflicted) {
  52. if (shouldKeepOurs(file, patterns)) {
  53. if (options.dryRun) {
  54. info(`[DRY-RUN] Would keep ours: ${file}`)
  55. results.push({ file, action: "kept", dryRun: true })
  56. } else {
  57. try {
  58. await checkoutOurs([file])
  59. await stageFiles([file])
  60. success(`Kept ours: ${file}`)
  61. results.push({ file, action: "kept", dryRun: false })
  62. } catch (err) {
  63. error(`Failed to keep ours for ${file}: ${err}`)
  64. results.push({ file, action: "skipped", dryRun: false })
  65. }
  66. }
  67. } else {
  68. debug(`Skipping ${file} (not in keep-ours list)`)
  69. results.push({ file, action: "not-conflicted", dryRun: options.dryRun ?? false })
  70. }
  71. }
  72. return results
  73. }
  74. /**
  75. * Reset specific files to Kilo's version (even if not conflicted)
  76. */
  77. export async function resetToOurs(files: string[], options: KeepOursOptions = {}): Promise<KeepOursResult[]> {
  78. const results: KeepOursResult[] = []
  79. for (const file of files) {
  80. if (options.dryRun) {
  81. info(`[DRY-RUN] Would reset to ours: ${file}`)
  82. results.push({ file, action: "kept", dryRun: true })
  83. } else {
  84. try {
  85. // Get the file from HEAD (Kilo's version)
  86. await $`git checkout HEAD -- ${file}`
  87. success(`Reset to ours: ${file}`)
  88. results.push({ file, action: "kept", dryRun: false })
  89. } catch (err) {
  90. warn(`Could not reset ${file}: ${err}`)
  91. results.push({ file, action: "skipped", dryRun: false })
  92. }
  93. }
  94. }
  95. return results
  96. }
  97. // CLI entry point
  98. if (import.meta.main) {
  99. const args = process.argv.slice(2)
  100. const dryRun = args.includes("--dry-run")
  101. const verbose = args.includes("--verbose")
  102. // Get specific files if provided
  103. const files = args.filter((a) => !a.startsWith("--"))
  104. if (dryRun) {
  105. info("Running in dry-run mode (no files will be modified)")
  106. }
  107. const results =
  108. files.length > 0 ? await resetToOurs(files, { dryRun, verbose }) : await keepOursFiles({ dryRun, verbose })
  109. const kept = results.filter((r) => r.action === "kept")
  110. console.log()
  111. success(`Kept Kilo's version for ${kept.length} files`)
  112. if (dryRun) {
  113. info("Run without --dry-run to apply changes")
  114. }
  115. }