From 419ddea52a3a5d2d27578df0528d46d6c55fb039 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 14:06:37 +0000 Subject: [PATCH 1/6] chore(upstream): implement compatibility base tracking for merges Update the upstream merge script to identify and link to the previous compatibility commit. This ensures that pre-merge transformations maintain a proper commit history by using multiple parents when a prior compatibility base is found, rather than creating disconnected commits. - Add `findLatestCompatCommit` to locate previous compatibility commits - Implement `commitTree` and `updateBranch` for low-level git manipulation - Update `merge.ts` to integrate compatibility base detection and multi-parent commits --- script/upstream/merge.ts | 19 ++++++++++++- script/upstream/utils/git.ts | 55 ++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/script/upstream/merge.ts b/script/upstream/merge.ts index 01affc03caf..967ea1d9c78 100644 --- a/script/upstream/merge.ts +++ b/script/upstream/merge.ts @@ -429,6 +429,15 @@ async function main() { await git.createBranch(opencodeBranch) logger.info(`Created opencode branch: ${opencodeBranch}`) + const prior = await git.findLatestCompatCommit(config.baseBranch, targetVersion.commit) + if (prior) { + logger.info( + `Found previous compatibility base: ${prior.message} (${prior.commit.slice(0, 8)}) from upstream ${prior.upstream.slice(0, 8)}`, + ) + } else { + logger.warn("No previous compatibility base found; merge base will remain pristine upstream") + } + // Step 6: Apply ALL transformations to opencode branch (pre-merge) // This reduces conflicts by transforming upstream code to Kilo conventions BEFORE merging logger.step(6, 8, "Applying transformations to opencode branch (pre-merge)...") @@ -524,7 +533,15 @@ async function main() { // Commit all transformations await git.stageAll() - await git.commit(`refactor: kilo compat for ${targetVersion.tag}`) + const compatMessage = `refactor: kilo compat for ${targetVersion.tag}` + if (prior) { + const tree = await git.writeTree() + const commit = await git.commitTree(tree, compatMessage, [targetVersion.commit, prior.commit]) + await git.updateBranch(opencodeBranch, commit) + await git.checkout(opencodeBranch) + } else { + await git.commit(compatMessage) + } logger.success("Committed pre-merge transformations") // Step 7: Merge into Kilo branch diff --git a/script/upstream/utils/git.ts b/script/upstream/utils/git.ts index a781276e983..d30cb2b99ea 100644 --- a/script/upstream/utils/git.ts +++ b/script/upstream/utils/git.ts @@ -15,6 +15,12 @@ export interface RemoteInfo { url: string } +export interface CompatBase { + commit: string + upstream: string + message: string +} + export async function getCurrentBranch(): Promise { const result = await $`git rev-parse --abbrev-ref HEAD`.text() return result.trim() @@ -177,6 +183,55 @@ export async function getCommitHash(ref: string): Promise { return result.trim() } +export async function getCommitParents(ref: string): Promise { + const result = await $`git show --no-patch --format=%P ${ref}`.text() + return result + .trim() + .split(/\s+/) + .filter((parent) => parent.length > 0) +} + +export async function writeTree(): Promise { + const result = await $`git write-tree`.text() + return result.trim() +} + +export async function commitTree(tree: string, message: string, parents: string[]): Promise { + const args = parents.flatMap((parent) => ["-p", parent]) + const result = await $`git commit-tree ${tree} ${args} -m ${message}`.text() + return result.trim() +} + +export async function updateBranch(name: string, commit: string): Promise { + await $`git update-ref refs/heads/${name} ${commit}` +} + +export async function findLatestCompatCommit(base: string, target: string): Promise { + const grep = "^refactor: kilo compat for " + const result = await $`git log --format=%H%x00%s --grep=${grep} ${base}`.quiet().nothrow() + if (result.exitCode !== 0) { + throw new Error(`Failed to search compatibility commits: ${result.stderr.toString()}`) + } + + const lines = result.stdout + .toString() + .trim() + .split("\n") + .filter((line) => line.length > 0) + + for (const line of lines) { + const [commit, message = ""] = line.split("\0") + if (!commit) continue + + const parents = await getCommitParents(commit) + for (const parent of parents) { + if (await isAncestor(parent, target)) return { commit, upstream: parent, message } + } + } + + return null +} + /** * Check if `commit` is an ancestor of `ref` (i.e. reachable from `ref`). * Uses `git merge-base --is-ancestor`, which exits 0 for yes, 1 for no. From 4b1c1930c2fd07a4dec556f138217e4953685594 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 15:54:19 +0000 Subject: [PATCH 2/6] fix: preserve upstream compat merge bases --- script/upstream/merge.ts | 2 +- script/upstream/utils/git.test.ts | 55 +++++++++++++++++++++++++++++++ script/upstream/utils/git.ts | 5 ++- 3 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 script/upstream/utils/git.test.ts diff --git a/script/upstream/merge.ts b/script/upstream/merge.ts index 967ea1d9c78..db3245c1c1d 100644 --- a/script/upstream/merge.ts +++ b/script/upstream/merge.ts @@ -536,7 +536,7 @@ async function main() { const compatMessage = `refactor: kilo compat for ${targetVersion.tag}` if (prior) { const tree = await git.writeTree() - const commit = await git.commitTree(tree, compatMessage, [targetVersion.commit, prior.commit]) + const commit = await git.createMergeCommit(tree, compatMessage, targetVersion.commit, prior.commit) await git.updateBranch(opencodeBranch, commit) await git.checkout(opencodeBranch) } else { diff --git a/script/upstream/utils/git.test.ts b/script/upstream/utils/git.test.ts new file mode 100644 index 00000000000..5636241a2f7 --- /dev/null +++ b/script/upstream/utils/git.test.ts @@ -0,0 +1,55 @@ +import { afterEach, beforeEach, expect, test } from "bun:test" +import { mkdtemp, rm } from "node:fs/promises" +import { join } from "node:path" +import { tmpdir } from "node:os" +import { $ } from "bun" +import { createMergeCommit, findLatestCompatCommit, getCommitHash, updateBranch, writeTree } from "./git" + +const cwd = process.cwd() +let dir = "" + +async function commit(message: string) { + await $`git add -A`.quiet() + await $`git -c user.name=Test -c user.email=test@example.com commit -m ${message}`.quiet() + return getCommitHash("HEAD") +} + +beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "kilo-upstream-git-")) + process.chdir(dir) + await $`git init -b upstream`.quiet() + await $`git config user.name Test`.quiet() + await $`git config user.email test@example.com`.quiet() +}) + +afterEach(async () => { + process.chdir(cwd) + await rm(dir, { recursive: true, force: true }) +}) + +test("finds previous compatibility commit for transformed merge base", async () => { + await Bun.write("brand.txt", "opencode A\n") + const old = await commit("release: v1.0.0") + + await Bun.write("brand.txt", "opencode B\n") + const target = await commit("release: v1.0.1") + + await $`git checkout -b main ${old}`.quiet() + await Bun.write("brand.txt", "kilo A\n") + const prior = await commit("refactor: kilo compat for v1.0.0") + + const found = await findLatestCompatCommit("main", target) + expect(found?.commit).toBe(prior) + expect(found?.upstream).toBe(old) + + await $`git checkout ${target}`.quiet() + await $`git checkout -b opencode-v1.0.1`.quiet() + await Bun.write("brand.txt", "kilo B\n") + await $`git add -A`.quiet() + const tree = await writeTree() + const next = await createMergeCommit(tree, "refactor: kilo compat for v1.0.1", target, prior) + await updateBranch("opencode-v1.0.1", next) + + const base = (await $`git merge-base main opencode-v1.0.1`.text()).trim() + expect(base).toBe(prior) +}) diff --git a/script/upstream/utils/git.ts b/script/upstream/utils/git.ts index d30cb2b99ea..4e9ccea8197 100644 --- a/script/upstream/utils/git.ts +++ b/script/upstream/utils/git.ts @@ -196,9 +196,8 @@ export async function writeTree(): Promise { return result.trim() } -export async function commitTree(tree: string, message: string, parents: string[]): Promise { - const args = parents.flatMap((parent) => ["-p", parent]) - const result = await $`git commit-tree ${tree} ${args} -m ${message}`.text() +export async function createMergeCommit(tree: string, message: string, first: string, second: string): Promise { + const result = await $`git commit-tree ${tree} -p ${first} -p ${second} -m ${message}`.text() return result.trim() } From 4b22bfbc57c2ac1a3911a910ec59e1d045e6e540 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 07:57:01 +0000 Subject: [PATCH 3/6] fix: keep compat branches linear --- script/upstream/merge.ts | 2 +- script/upstream/utils/git.test.ts | 7 ++++--- script/upstream/utils/git.ts | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/script/upstream/merge.ts b/script/upstream/merge.ts index db3245c1c1d..e4ac18f7441 100644 --- a/script/upstream/merge.ts +++ b/script/upstream/merge.ts @@ -536,7 +536,7 @@ async function main() { const compatMessage = `refactor: kilo compat for ${targetVersion.tag}` if (prior) { const tree = await git.writeTree() - const commit = await git.createMergeCommit(tree, compatMessage, targetVersion.commit, prior.commit) + const commit = await git.createCommit(tree, compatMessage, prior.commit) await git.updateBranch(opencodeBranch, commit) await git.checkout(opencodeBranch) } else { diff --git a/script/upstream/utils/git.test.ts b/script/upstream/utils/git.test.ts index 5636241a2f7..999510bb42b 100644 --- a/script/upstream/utils/git.test.ts +++ b/script/upstream/utils/git.test.ts @@ -3,7 +3,7 @@ import { mkdtemp, rm } from "node:fs/promises" import { join } from "node:path" import { tmpdir } from "node:os" import { $ } from "bun" -import { createMergeCommit, findLatestCompatCommit, getCommitHash, updateBranch, writeTree } from "./git" +import { createCommit, findLatestCompatCommit, getCommitHash, getCommitParents, updateBranch, writeTree } from "./git" const cwd = process.cwd() let dir = "" @@ -27,7 +27,7 @@ afterEach(async () => { await rm(dir, { recursive: true, force: true }) }) -test("finds previous compatibility commit for transformed merge base", async () => { +test("finds previous compatibility commit for transformed base", async () => { await Bun.write("brand.txt", "opencode A\n") const old = await commit("release: v1.0.0") @@ -47,9 +47,10 @@ test("finds previous compatibility commit for transformed merge base", async () await Bun.write("brand.txt", "kilo B\n") await $`git add -A`.quiet() const tree = await writeTree() - const next = await createMergeCommit(tree, "refactor: kilo compat for v1.0.1", target, prior) + const next = await createCommit(tree, "refactor: kilo compat for v1.0.1", prior) await updateBranch("opencode-v1.0.1", next) const base = (await $`git merge-base main opencode-v1.0.1`.text()).trim() expect(base).toBe(prior) + expect(await getCommitParents(next)).toEqual([prior]) }) diff --git a/script/upstream/utils/git.ts b/script/upstream/utils/git.ts index 4e9ccea8197..b25212de02a 100644 --- a/script/upstream/utils/git.ts +++ b/script/upstream/utils/git.ts @@ -196,8 +196,8 @@ export async function writeTree(): Promise { return result.trim() } -export async function createMergeCommit(tree: string, message: string, first: string, second: string): Promise { - const result = await $`git commit-tree ${tree} -p ${first} -p ${second} -m ${message}`.text() +export async function createCommit(tree: string, message: string, parent: string): Promise { + const result = await $`git commit-tree ${tree} -p ${parent} -m ${message}`.text() return result.trim() } From fe3724605fd10da25f2a0c624049eea373b4b284 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 08:00:21 +0000 Subject: [PATCH 4/6] fix: resolve compat bases from version tags --- script/upstream/utils/git.test.ts | 2 ++ script/upstream/utils/git.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/script/upstream/utils/git.test.ts b/script/upstream/utils/git.test.ts index 999510bb42b..f841d74bf59 100644 --- a/script/upstream/utils/git.test.ts +++ b/script/upstream/utils/git.test.ts @@ -33,8 +33,10 @@ test("finds previous compatibility commit for transformed base", async () => { await Bun.write("brand.txt", "opencode B\n") const target = await commit("release: v1.0.1") + await $`git tag v1.0.1 ${target}`.quiet() await $`git checkout -b main ${old}`.quiet() + await $`git tag v1.0.0 ${old}`.quiet() await Bun.write("brand.txt", "kilo A\n") const prior = await commit("refactor: kilo compat for v1.0.0") diff --git a/script/upstream/utils/git.ts b/script/upstream/utils/git.ts index b25212de02a..d8730d2d378 100644 --- a/script/upstream/utils/git.ts +++ b/script/upstream/utils/git.ts @@ -205,6 +205,19 @@ export async function updateBranch(name: string, commit: string): Promise await $`git update-ref refs/heads/${name} ${commit}` } +async function compatUpstream(message: string): Promise { + const prefix = "refactor: kilo compat for " + if (!message.startsWith(prefix)) return null + + const tag = message.slice(prefix.length).trim().split(/\s+/)[0] + if (!tag) return null + + const ref = `${tag}^{commit}` + const result = await $`git rev-parse ${ref}`.quiet().nothrow() + if (result.exitCode !== 0) return null + return result.stdout.toString().trim() +} + export async function findLatestCompatCommit(base: string, target: string): Promise { const grep = "^refactor: kilo compat for " const result = await $`git log --format=%H%x00%s --grep=${grep} ${base}`.quiet().nothrow() @@ -222,6 +235,9 @@ export async function findLatestCompatCommit(base: string, target: string): Prom const [commit, message = ""] = line.split("\0") if (!commit) continue + const upstream = await compatUpstream(message) + if (upstream && (await isAncestor(upstream, target))) return { commit, upstream, message } + const parents = await getCommitParents(commit) for (const parent of parents) { if (await isAncestor(parent, target)) return { commit, upstream: parent, message } From 5c14ec2bed7048a24071de856976cbda4362ddc9 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 08:10:36 +0000 Subject: [PATCH 5/6] fix: preserve upstream ancestry on kilo merge --- script/upstream/merge.ts | 4 ++++ script/upstream/utils/git.test.ts | 26 ++++++++++++++++++++++++-- script/upstream/utils/git.ts | 10 ++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/script/upstream/merge.ts b/script/upstream/merge.ts index e4ac18f7441..77f3faa4713 100644 --- a/script/upstream/merge.ts +++ b/script/upstream/merge.ts @@ -548,6 +548,10 @@ async function main() { logger.step(7, 8, "Merging into Kilo branch...") await git.checkout(kiloBranch) + if (prior) { + const linked = await git.recordAncestor(targetVersion.commit, `merge: record upstream ${targetVersion.tag}`) + if (linked) logger.info(`Recorded upstream ${targetVersion.tag} as Kilo branch ancestry`) + } const mergeResult = await git.merge(opencodeBranch) if (!mergeResult.success) { diff --git a/script/upstream/utils/git.test.ts b/script/upstream/utils/git.test.ts index f841d74bf59..7d1f5e3c904 100644 --- a/script/upstream/utils/git.test.ts +++ b/script/upstream/utils/git.test.ts @@ -3,7 +3,16 @@ import { mkdtemp, rm } from "node:fs/promises" import { join } from "node:path" import { tmpdir } from "node:os" import { $ } from "bun" -import { createCommit, findLatestCompatCommit, getCommitHash, getCommitParents, updateBranch, writeTree } from "./git" +import { + createCommit, + findLatestCompatCommit, + getCommitHash, + getCommitParents, + isAncestor, + recordAncestor, + updateBranch, + writeTree, +} from "./git" const cwd = process.cwd() let dir = "" @@ -51,8 +60,21 @@ test("finds previous compatibility commit for transformed base", async () => { const tree = await writeTree() const next = await createCommit(tree, "refactor: kilo compat for v1.0.1", prior) await updateBranch("opencode-v1.0.1", next) - const base = (await $`git merge-base main opencode-v1.0.1`.text()).trim() expect(base).toBe(prior) expect(await getCommitParents(next)).toEqual([prior]) + + await $`git checkout main`.quiet() + expect(await recordAncestor(target, "merge: record upstream v1.0.1")).toBe(true) + const link = await getCommitHash("HEAD") + expect(await getCommitParents(link)).toEqual([prior, target]) + expect(await isAncestor(target, link)).toBe(true) + + const linked = (await $`git merge-base main opencode-v1.0.1`.text()).trim() + expect(linked).toBe(prior) + + await $`git merge opencode-v1.0.1`.quiet() + const head = await getCommitHash("HEAD") + expect(await getCommitParents(head)).toEqual([link, next]) + expect(await isAncestor(target, head)).toBe(true) }) diff --git a/script/upstream/utils/git.ts b/script/upstream/utils/git.ts index d8730d2d378..35588124e00 100644 --- a/script/upstream/utils/git.ts +++ b/script/upstream/utils/git.ts @@ -205,6 +205,16 @@ export async function updateBranch(name: string, commit: string): Promise await $`git update-ref refs/heads/${name} ${commit}` } +export async function recordAncestor(ref: string, message: string): Promise { + if (await isAncestor(ref, "HEAD")) return false + + const result = await $`git merge -s ours --no-ff ${ref} -m ${message}`.nothrow() + if (result.exitCode !== 0) { + throw new Error(`Failed to record ancestor ${ref}: ${result.stderr.toString()}`) + } + return true +} + async function compatUpstream(message: string): Promise { const prefix = "refactor: kilo compat for " if (!message.startsWith(prefix)) return null From 70fde2221e3cca5624c0a728b956483b3f2e345a Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 14:34:13 +0000 Subject: [PATCH 6/6] refactor(upstream): improve compatibility commit lookup using semver Update `findLatestCompatCommit` to use semantic versioning when identifying the latest compatibility commit. This ensures the correct commit is selected even when upstream tags diverge or are not strictly linear. - Add semver parsing and comparison utilities - Implement `targetSemver` to resolve versions from tags or commit messages - Filter and sort compatibility candidates by version relative to the target version - Add test case for diverging upstream tags --- script/upstream/utils/git.test.ts | 30 +++++++++++++ script/upstream/utils/git.ts | 70 +++++++++++++++++++++++++++++-- 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/script/upstream/utils/git.test.ts b/script/upstream/utils/git.test.ts index 7d1f5e3c904..b51dd35ea4a 100644 --- a/script/upstream/utils/git.test.ts +++ b/script/upstream/utils/git.test.ts @@ -78,3 +78,33 @@ test("finds previous compatibility commit for transformed base", async () => { expect(await getCommitParents(head)).toEqual([link, next]) expect(await isAncestor(target, head)).toBe(true) }) + +test("finds previous compatibility commit when upstream tags diverge", async () => { + await Bun.write("brand.txt", "opencode 1.4.9\n") + const old = await commit("release: v1.4.9") + await $`git tag v1.4.9 ${old}`.quiet() + + await $`git checkout -b release-30 ${old}`.quiet() + await Bun.write("brand.txt", "opencode 1.14.30\n") + const side = await commit("release: v1.14.30") + await $`git tag v1.14.30 ${side}`.quiet() + + await $`git checkout -b release-31 ${old}`.quiet() + await Bun.write("brand.txt", "opencode 1.14.31\n") + const target = await commit("release: v1.14.31") + await $`git tag v1.14.31 ${target}`.quiet() + + await $`git checkout -b main ${old}`.quiet() + await Bun.write("brand.txt", "kilo 1.4.9\n") + const ancient = await commit("refactor: kilo compat for v1.4.9") + await Bun.write("brand.txt", "kilo 1.14.30\n") + const prior = await commit("refactor: kilo compat for v1.14.30") + + expect(await isAncestor(side, target)).toBe(false) + expect(await isAncestor(old, target)).toBe(true) + + const found = await findLatestCompatCommit("main", target) + expect(found?.commit).toBe(prior) + expect(found?.upstream).toBe(side) + expect(found?.commit).not.toBe(ancient) +}) diff --git a/script/upstream/utils/git.ts b/script/upstream/utils/git.ts index 69d965cd553..e23823ed9a4 100644 --- a/script/upstream/utils/git.ts +++ b/script/upstream/utils/git.ts @@ -21,6 +21,12 @@ export interface CompatBase { message: string } +type Semver = readonly [number, number, number] + +interface Candidate extends CompatBase { + version: Semver +} + export async function getCurrentBranch(): Promise { const result = await $`git rev-parse --abbrev-ref HEAD`.text() return result.trim() @@ -216,10 +222,7 @@ export async function recordAncestor(ref: string, message: string): Promise { - const prefix = "refactor: kilo compat for " - if (!message.startsWith(prefix)) return null - - const tag = message.slice(prefix.length).trim().split(/\s+/)[0] + const tag = compatTag(message) if (!tag) return null const ref = `${tag}^{commit}` @@ -228,6 +231,55 @@ async function compatUpstream(message: string): Promise { return result.stdout.toString().trim() } +function compatTag(message: string): string | null { + const prefix = "refactor: kilo compat for " + if (!message.startsWith(prefix)) return null + return message.slice(prefix.length).trim().split(/\s+/)[0] ?? null +} + +function parseSemver(value: string | undefined): Semver | null { + const match = value?.match(/^v?(\d+)\.(\d+)\.(\d+)$/) + if (!match) return null + const major = Number.parseInt(match[1] ?? "0", 10) + const minor = Number.parseInt(match[2] ?? "0", 10) + const patch = Number.parseInt(match[3] ?? "0", 10) + return [major, minor, patch] +} + +function compareSemver(a: Semver, b: Semver): number { + for (const idx of [0, 1, 2] as const) { + if (a[idx] < b[idx]) return -1 + if (a[idx] > b[idx]) return 1 + } + return 0 +} + +function exists(value: T | null): value is T { + return value !== null +} + +async function targetSemver(ref: string): Promise { + const tags = await getTagsForCommit(ref) + const tag = tags.find((item) => parseSemver(item) !== null) + const parsed = parseSemver(tag) + if (parsed) return parsed + + const message = await getCommitMessage(ref) + const match = message.match(/\bv?\d+\.\d+\.\d+\b/) + return parseSemver(match?.[0]) +} + +async function candidate(line: string): Promise { + const [commit, message = ""] = line.split("\0") + if (!commit) return null + + const version = parseSemver(compatTag(message) ?? undefined) + if (!version) return null + + const upstream = (await compatUpstream(message)) ?? (await getCommitParents(commit))[0] ?? commit + return { commit, upstream, message, version } +} + export async function findLatestCompatCommit(base: string, target: string): Promise { const grep = "^refactor: kilo compat for " const result = await $`git log --format=%H%x00%s --grep=${grep} ${base}`.quiet().nothrow() @@ -241,6 +293,16 @@ export async function findLatestCompatCommit(base: string, target: string): Prom .split("\n") .filter((line) => line.length > 0) + const targetVersion = await targetSemver(target) + if (targetVersion) { + const candidates = (await Promise.all(lines.map(candidate))) + .filter(exists) + .filter((item) => compareSemver(item.version, targetVersion) < 0) + .sort((a, b) => compareSemver(b.version, a.version)) + const latest = candidates[0] + if (latest) return { commit: latest.commit, upstream: latest.upstream, message: latest.message } + } + for (const line of lines) { const [commit, message = ""] = line.split("\0") if (!commit) continue