Files
2026-02-05 13:31:47 -03:00

198 lines
5.5 KiB
TypeScript

#!/usr/bin/env bun
/**
* Analyze upstream changes without merging
*
* Generates a detailed conflict report for a target upstream version.
*
* Usage:
* bun run script/upstream/analyze.ts --version v1.1.49
* bun run script/upstream/analyze.ts --commit abc123
* bun run script/upstream/analyze.ts --version v1.1.49 --base-branch catrielmuller/kilo-opencode-v1.1.44
*/
import { $ } from "bun"
import * as git from "./utils/git"
import * as version from "./utils/version"
import * as report from "./utils/report"
import { defaultConfig, loadConfig } from "./utils/config"
import { header, info, success, warn, error, divider, list } from "./utils/logger"
interface AnalyzeOptions {
version?: string
commit?: string
output?: string
baseBranch?: string
}
function parseArgs(): AnalyzeOptions {
const args = process.argv.slice(2)
const options: AnalyzeOptions = {}
const versionIdx = args.indexOf("--version")
if (versionIdx !== -1 && args[versionIdx + 1]) {
options.version = args[versionIdx + 1]
}
const commitIdx = args.indexOf("--commit")
if (commitIdx !== -1 && args[commitIdx + 1]) {
options.commit = args[commitIdx + 1]
}
const outputIdx = args.indexOf("--output")
if (outputIdx !== -1 && args[outputIdx + 1]) {
options.output = args[outputIdx + 1]
}
const baseBranchIdx = args.indexOf("--base-branch")
if (baseBranchIdx !== -1 && args[baseBranchIdx + 1]) {
options.baseBranch = args[baseBranchIdx + 1]
}
return options
}
async function main() {
const options = parseArgs()
const config = loadConfig(options.baseBranch ? { baseBranch: options.baseBranch } : undefined)
header("Upstream Change Analysis")
// Check upstream remote
if (!(await git.hasUpstreamRemote())) {
error("No 'upstream' remote found. Please add it:")
info(" git remote add upstream git@github.com:anomalyco/opencode.git")
process.exit(1)
}
// Fetch upstream
info("Fetching upstream...")
await git.fetchUpstream()
// Determine target
let target: version.VersionInfo | null = null
if (options.commit) {
target = await version.getVersionForCommit(options.commit)
if (!target) {
target = { version: "unknown", tag: "unknown", commit: options.commit }
}
} else if (options.version) {
const versions = await version.getAvailableUpstreamVersions()
target = versions.find((v) => v.version === options.version || v.tag === options.version) || null
if (!target) {
error(`Version ${options.version} not found`)
info("Use 'bun run script/upstream/list-versions.ts' to see available versions")
process.exit(1)
}
} else {
target = await version.getLatestUpstreamVersion()
}
if (!target) {
error("Could not determine target version")
process.exit(1)
}
success(`Analyzing: ${target.tag} (${target.commit.slice(0, 8)})`)
divider()
// Analyze conflicts
info("Analyzing changes...")
// Use commit hash directly since we may not have the tag fetched locally
const conflicts = await report.analyzeConflicts(target.commit, config.baseBranch, config.keepOurs)
// Group by type
const byType = new Map<string, report.ConflictFile[]>()
for (const c of conflicts) {
const list = byType.get(c.type) || []
list.push(c)
byType.set(c.type, list)
}
// Group by recommendation
const byRec = new Map<string, report.ConflictFile[]>()
for (const c of conflicts) {
const list = byRec.get(c.recommendation) || []
list.push(c)
byRec.set(c.recommendation, list)
}
console.log()
success(`Total files changed: ${conflicts.length}`)
console.log()
// By type
info("Changes by type:")
for (const [type, files] of byType) {
console.log(` ${type.padEnd(12)} ${files.length}`)
}
console.log()
// By recommendation
info("Changes by recommendation:")
for (const [rec, files] of byRec) {
const label =
rec === "keep-ours"
? "Keep Kilo's"
: rec === "codemod"
? "Auto-transform"
: rec === "keep-theirs"
? "Take upstream"
: "Manual review"
console.log(` ${label.padEnd(16)} ${files.length}`)
}
// Show manual review files
const manual = byRec.get("manual") || []
if (manual.length > 0 && manual.length <= 20) {
console.log()
warn("Files requiring manual review:")
list(manual.map((f) => f.path))
}
// Generate report
const conflictReport: report.ConflictReport = {
timestamp: new Date().toISOString(),
upstreamVersion: target.version,
upstreamCommit: target.commit,
baseBranch: config.baseBranch,
mergeBranch: "",
totalConflicts: conflicts.length,
conflicts,
recommendations: [],
}
if (manual.length > 0) {
conflictReport.recommendations.push(`${manual.length} files require manual review`)
}
const codemodFiles = byRec.get("codemod") || []
if (codemodFiles.length > 0) {
conflictReport.recommendations.push(`${codemodFiles.length} files will be auto-transformed`)
}
const keepOursFiles = byRec.get("keep-ours") || []
if (keepOursFiles.length > 0) {
conflictReport.recommendations.push(`${keepOursFiles.length} files will keep Kilo's version`)
}
// Save report
const outputPath = options.output || `upstream-analysis-${target.version}.md`
await report.saveReport(conflictReport, outputPath)
divider()
success(`Report saved to ${outputPath}`)
console.log()
info("Next steps:")
info(" 1. Review the report")
info(" 2. Run merge with: bun run script/upstream/merge.ts --version " + target.tag)
}
main().catch((err) => {
error(`Error: ${err}`)
process.exit(1)
})