feat: upstream script wip

This commit is contained in:
Catriel Müller
2026-02-03 13:44:17 -03:00
parent b79517acb8
commit bd73c02c27
5 changed files with 91 additions and 40 deletions
+2 -5
View File
@@ -92,11 +92,8 @@ async function main() {
// Analyze conflicts
info("Analyzing changes...")
const conflicts = await report.analyzeConflicts(
`upstream/${target.tag}`,
defaultConfig.baseBranch,
defaultConfig.keepOurs,
)
// Use commit hash directly since we may not have the tag fetched locally
const conflicts = await report.analyzeConflicts(target.commit, defaultConfig.baseBranch, defaultConfig.keepOurs)
// Group by type
const byType = new Map<string, report.ConflictFile[]>()
+33
View File
@@ -0,0 +1,33 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "@kilocode/upstream-merge",
"dependencies": {
"ts-morph": "^24.0.0",
},
},
},
"packages": {
"@ts-morph/common": ["@ts-morph/common@0.25.0", "", { "dependencies": { "minimatch": "^9.0.4", "path-browserify": "^1.0.1", "tinyglobby": "^0.2.9" } }, "sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"ts-morph": ["ts-morph@24.0.0", "", { "dependencies": { "@ts-morph/common": "~0.25.0", "code-block-writer": "^13.0.3" } }, "sha512-2OAOg/Ob5yx9Et7ZX4CvTCc0UFoZHwLEJ+dpDPSUi5TgwwlTlX47w+iFRrEwzUZwYACjq83cgjS/Da50Ga37uw=="],
}
}
+11 -4
View File
@@ -55,7 +55,10 @@ export async function hasUpstreamRemote(): Promise<boolean> {
}
export async function fetchUpstream(): Promise<void> {
await $`git fetch upstream`
const result = await $`git fetch upstream`.quiet().nothrow()
if (result.exitCode !== 0) {
throw new Error(`Failed to fetch upstream: ${result.stderr.toString()}`)
}
}
export async function checkout(ref: string): Promise<void> {
@@ -161,11 +164,15 @@ export async function getAllTags(): Promise<string[]> {
}
export async function getUpstreamTags(): Promise<string[]> {
await fetchUpstream()
const result = await $`git ls-remote --tags upstream`.text()
const result = await $`git ls-remote --tags upstream`.quiet().nothrow()
if (result.exitCode !== 0) {
throw new Error(`Failed to list upstream tags: ${result.stderr.toString()}`)
}
const output = result.stdout.toString()
const tags: string[] = []
for (const line of result.trim().split("\n")) {
for (const line of output.trim().split("\n")) {
const match = line.match(/refs\/tags\/([^\^]+)$/)
if (match && match[1]) tags.push(match[1])
}
+10 -3
View File
@@ -99,13 +99,20 @@ export function getRecommendation(
* Analyze potential conflicts before merge
*/
export async function analyzeConflicts(
upstreamBranch: string,
upstreamRef: string,
baseBranch: string,
keepOurs: string[],
): Promise<ConflictFile[]> {
// Get list of files that differ between branches
const result = await $`git diff --name-only ${baseBranch}...${upstreamBranch}`.text()
const files = result
// Use quiet to suppress output and nothrow to handle errors
const result = await $`git diff --name-only ${baseBranch}...${upstreamRef}`.quiet().nothrow()
if (result.exitCode !== 0) {
throw new Error(`Failed to analyze conflicts: ${result.stderr.toString()}`)
}
const files = result.stdout
.toString()
.trim()
.split("\n")
.filter((f) => f.length > 0)
+35 -28
View File
@@ -14,10 +14,20 @@ export interface VersionInfo {
/**
* Parse version from a tag string (e.g., "v1.1.49" -> "1.1.49")
* Only matches stable versions (not dev/preview tags like v0.0.0-202507310417)
*/
export function parseVersion(tag: string): string | null {
const match = tag.match(/^v?(\d+\.\d+\.\d+.*)$/)
return match?.[1] ?? null
export function parseVersion(tag: string, includePrerelease = false): string | null {
// Match stable versions like v1.1.49 or 1.1.49
const stableMatch = tag.match(/^v?(\d+\.\d+\.\d+)$/)
if (stableMatch) return stableMatch[1] ?? null
// Optionally match prerelease versions
if (includePrerelease) {
const prereleaseMatch = tag.match(/^v?(\d+\.\d+\.\d+-.+)$/)
if (prereleaseMatch) return prereleaseMatch[1] ?? null
}
return null
}
/**
@@ -42,30 +52,11 @@ export function compareVersions(a: string, b: string): number {
* Get the latest upstream version from tags
*/
export async function getLatestUpstreamVersion(): Promise<VersionInfo | null> {
const tags = await getUpstreamTags()
const versions: Array<{ tag: string; version: string }> = []
for (const tag of tags) {
const version = parseVersion(tag)
if (version) versions.push({ tag, version })
}
const versions = await getAvailableUpstreamVersions()
if (versions.length === 0) return null
// Sort by version descending
versions.sort((a, b) => compareVersions(b.version, a.version))
const latest = versions[0]
if (!latest) return null
// Get commit for this tag
const commit = await $`git rev-list -n 1 upstream/${latest.tag}`.text().then((t) => t.trim())
return {
version: latest.version,
tag: latest.tag,
commit,
}
return versions[0] ?? null
}
/**
@@ -99,13 +90,27 @@ export async function getVersionForCommit(commit: string): Promise<VersionInfo |
* Get available upstream versions (sorted newest first)
*/
export async function getAvailableUpstreamVersions(): Promise<VersionInfo[]> {
const tags = await getUpstreamTags()
// Get tags with their commits directly from ls-remote
const result = await $`git ls-remote --tags upstream`.quiet().nothrow()
if (result.exitCode !== 0) {
throw new Error(`Failed to list upstream tags: ${result.stderr.toString()}`)
}
const output = result.stdout.toString()
const versions: VersionInfo[] = []
for (const tag of tags) {
for (const line of output.trim().split("\n")) {
// Match lines like: abc123... refs/tags/v1.1.49
// Skip annotated tag references (those ending with ^{})
const match = line.match(/^([a-f0-9]+)\s+refs\/tags\/([^\^]+)$/)
if (!match) continue
const commit = match[1]
const tag = match[2]
if (!commit || !tag) continue
const version = parseVersion(tag)
if (version) {
const commit = await $`git rev-list -n 1 upstream/${tag}`.text().then((t) => t.trim())
versions.push({ version, tag, commit })
}
}
@@ -120,6 +125,8 @@ export async function getAvailableUpstreamVersions(): Promise<VersionInfo[]> {
* Get current Kilo version from package.json
*/
export async function getCurrentKiloVersion(): Promise<string> {
const pkg = await Bun.file("packages/opencode/package.json").json()
// Resolve path relative to repo root (script is in script/upstream/)
const path = new URL("../../../packages/opencode/package.json", import.meta.url).pathname
const pkg = await Bun.file(path).json()
return pkg.version
}