mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-21 05:52:35 +08:00
Merge remote-tracking branch 'origin/main' into mark/upstream-compat-base
This commit is contained in:
@@ -48,6 +48,7 @@ const EXCLUDE_PATTERNS = [
|
||||
/^https?:\/\/api\.openai\.com/,
|
||||
/^https?:\/\/api\.github\.com/,
|
||||
/^https?:\/\/api\.githubcopilot\.com/,
|
||||
/^https?:\/\/[^/]+\.openai\.azure\.com\/openai/, // kilocode_change
|
||||
/^https?:\/\/api\.cloudflare\.com/,
|
||||
/^https?:\/\/api\.releases\.hashicorp\.com/,
|
||||
/^https?:\/\/auth\.openai\.com/,
|
||||
|
||||
@@ -204,7 +204,7 @@ The only remaining conflicts are files with **actual code differences** - files
|
||||
Options:
|
||||
--version <version> Target upstream version (e.g., v1.1.49)
|
||||
--commit <hash> Target upstream commit hash
|
||||
--base-branch <name> Base branch to merge into (default: main)
|
||||
--base-branch <name> Base branch to merge into; use HEAD for current branch (default: main)
|
||||
--dry-run Preview changes without applying them
|
||||
--no-push Don't push branches to remote
|
||||
--no-worktrees Don't create reference worktrees
|
||||
@@ -305,6 +305,8 @@ Tighten the blast radius with `--review-limit 0` (only `markers-only` and `cosme
|
||||
|
||||
By default, upstream merges start from the `main` branch. However, you can use `--base-branch` to start from a different branch. This is useful for:
|
||||
|
||||
Passing `--base-branch HEAD` targets the currently checked-out branch without typing its full name.
|
||||
|
||||
### Incremental Merges
|
||||
|
||||
When working on multiple upstream versions, you can create a chain of merge PRs:
|
||||
|
||||
@@ -251,8 +251,7 @@ function detail(entry: Entry): string {
|
||||
|
||||
function describe(bucket: Bucket, count: number, dryRun: boolean): { label: string; action: string } {
|
||||
if (bucket === "markers-only") return { label: `markers-only (${count})`, action: dryRun ? "would reset" : "reset" }
|
||||
if (bucket === "cosmetic-only")
|
||||
return { label: `cosmetic-only (${count})`, action: dryRun ? "would reset" : "reset" }
|
||||
if (bucket === "cosmetic-only") return { label: `cosmetic-only (${count})`, action: dryRun ? "would reset" : "reset" }
|
||||
if (bucket === "small-diff") return { label: `small-diff (${count})`, action: dryRun ? "would reset" : "reset" }
|
||||
if (bucket === "large-diff") return { label: `large-diff (${count})`, action: "skipped" }
|
||||
if (bucket === "identical") return { label: `identical (${count})`, action: "nothing to do" }
|
||||
|
||||
@@ -10,15 +10,7 @@
|
||||
|
||||
import path from "node:path"
|
||||
import { error, header, info, success, warn } from "./utils/logger"
|
||||
import {
|
||||
annotate,
|
||||
annotates,
|
||||
changed,
|
||||
clean,
|
||||
fresh,
|
||||
ranges,
|
||||
supported,
|
||||
} from "./utils/markers"
|
||||
import { annotate, annotates, changed, clean, fresh, ranges, supported } from "./utils/markers"
|
||||
import { last, normalize, root, translate, upstream } from "./utils/upstream"
|
||||
|
||||
interface Args {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* Options:
|
||||
* --version <version> Target upstream version (e.g., v1.1.49)
|
||||
* --commit <hash> Target upstream commit hash
|
||||
* --base-branch <name> Base branch to merge into (default: main)
|
||||
* --base-branch <name> Base branch to merge into, or HEAD for current branch (default: main)
|
||||
* --dry-run Preview changes without applying them
|
||||
* --no-push Don't push branches to remote
|
||||
* --no-worktrees Don't create reference worktrees for manual resolution
|
||||
@@ -25,7 +25,7 @@ import * as logger from "./utils/logger"
|
||||
import * as version from "./utils/version"
|
||||
import * as report from "./utils/report"
|
||||
import * as worktree from "./utils/worktree"
|
||||
import { loadConfig } from "./utils/config"
|
||||
import { loadConfig, resolveBaseBranch } from "./utils/config"
|
||||
import { transformAll as transformPackageNames } from "./transforms/package-names"
|
||||
import { preserveAllVersions } from "./transforms/preserve-versions"
|
||||
import { keepOursFiles, resetToOurs } from "./transforms/keep-ours"
|
||||
@@ -219,7 +219,6 @@ async function main() {
|
||||
process.chdir((await $`git rev-parse --show-toplevel`.text()).trim())
|
||||
|
||||
const options = parseArgs()
|
||||
const config = loadConfig(options.baseBranch ? { baseBranch: options.baseBranch } : undefined)
|
||||
|
||||
if (options.verbose) {
|
||||
logger.setVerbose(true)
|
||||
@@ -248,6 +247,12 @@ async function main() {
|
||||
const currentBranch = await git.getCurrentBranch()
|
||||
logger.info(`Current branch: ${currentBranch}`)
|
||||
|
||||
const base = resolveBaseBranch(options.baseBranch, currentBranch)
|
||||
const config = loadConfig(base ? { baseBranch: base } : undefined)
|
||||
if (options.baseBranch === "HEAD") {
|
||||
logger.info(`Resolved --base-branch HEAD to current branch: ${config.baseBranch}`)
|
||||
}
|
||||
|
||||
// Enable git rerere so conflict resolutions are recorded and reused across merges
|
||||
if (!options.dryRun) {
|
||||
await git.ensureRerere()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/upstream-merge",
|
||||
"version": "7.2.42",
|
||||
"version": "7.2.44",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Scripts for automating upstream opencode merges into Kilo",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { resolveBaseBranch } from "./config"
|
||||
|
||||
test("resolves HEAD to the current branch", () => {
|
||||
expect(resolveBaseBranch("HEAD", "session/agent-123")).toBe("session/agent-123")
|
||||
})
|
||||
|
||||
test("keeps explicit base branch names", () => {
|
||||
expect(resolveBaseBranch("main", "session/agent-123")).toBe("main")
|
||||
expect(resolveBaseBranch(undefined, "session/agent-123")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("rejects HEAD when detached", () => {
|
||||
expect(() => resolveBaseBranch("HEAD", "HEAD")).toThrow("--base-branch HEAD requires a named branch")
|
||||
})
|
||||
@@ -231,3 +231,9 @@ export const defaultConfig: MergeConfig = {
|
||||
export function loadConfig(overrides?: Partial<MergeConfig>): MergeConfig {
|
||||
return { ...defaultConfig, ...overrides }
|
||||
}
|
||||
|
||||
export function resolveBaseBranch(base: string | undefined, current: string): string | undefined {
|
||||
if (base !== "HEAD") return base
|
||||
if (current === "HEAD") throw new Error("--base-branch HEAD requires a named branch, but git is in detached HEAD")
|
||||
return current
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
*/
|
||||
|
||||
import { $ } from "bun"
|
||||
import { rm } from "node:fs/promises"
|
||||
import { randomUUID } from "node:crypto"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
|
||||
export interface BranchInfo {
|
||||
current: string
|
||||
@@ -409,6 +413,11 @@ export async function ensureRerere(): Promise<void> {
|
||||
await $`git config rerere.autoupdate true`.quiet()
|
||||
}
|
||||
|
||||
async function reset(dir: string): Promise<void> {
|
||||
await $`git -C ${dir} reset -q --hard`.quiet().nothrow()
|
||||
await $`git -C ${dir} clean -fdx`.quiet().nothrow()
|
||||
}
|
||||
|
||||
/**
|
||||
* Train the rerere cache from past merge commits in the repo history.
|
||||
* Implements the same logic as git's contrib/rerere-train.sh:
|
||||
@@ -419,14 +428,14 @@ export async function ensureRerere(): Promise<void> {
|
||||
* Returns the number of resolutions learned.
|
||||
*/
|
||||
export async function trainRerere(grep: string): Promise<number> {
|
||||
// Save the current HEAD so we can restore it afterwards
|
||||
const headResult = await $`git symbolic-ref -q HEAD`.quiet().nothrow()
|
||||
const branch = headResult.exitCode === 0 ? headResult.stdout.toString().trim() : null
|
||||
const originalHead = branch ?? (await $`git rev-parse --verify HEAD`.text()).trim()
|
||||
const head = (await $`git rev-parse --verify HEAD`.text()).trim()
|
||||
const dir = join(tmpdir(), `kilo-rerere-train-${randomUUID()}`)
|
||||
|
||||
let learned = 0
|
||||
|
||||
try {
|
||||
await $`git worktree add --detach ${dir} ${head}`.quiet()
|
||||
|
||||
// Find all merge commits matching the grep pattern (merges have multiple parents)
|
||||
const revList = await $`git rev-list --parents --all --grep=${grep}`.quiet().nothrow()
|
||||
if (revList.exitCode !== 0 || !revList.stdout.toString().trim()) return 0
|
||||
@@ -443,45 +452,45 @@ export async function trainRerere(grep: string): Promise<number> {
|
||||
|
||||
const [commit, parent1, ...otherParents] = parts
|
||||
|
||||
await reset(dir)
|
||||
|
||||
// Checkout the first parent
|
||||
const coResult = await $`git checkout -q ${parent1}`.quiet().nothrow()
|
||||
const coResult = await $`git -C ${dir} checkout -q ${parent1}`.quiet().nothrow()
|
||||
if (coResult.exitCode !== 0) continue
|
||||
|
||||
// Attempt the merge - we expect it to fail with conflicts
|
||||
const mergeResult = await $`git merge --no-gpg-sign ${otherParents}`.quiet().nothrow()
|
||||
const mergeResult = await $`git -C ${dir} merge --no-gpg-sign ${otherParents}`.quiet().nothrow()
|
||||
if (mergeResult.exitCode === 0) {
|
||||
// Cleanly merged — no conflicts to learn from, reset and skip
|
||||
await $`git reset -q --hard`.quiet().nothrow()
|
||||
await reset(dir)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if rerere recorded a pre-image (MERGE_RR exists and is non-empty)
|
||||
const mergeRR = Bun.file(`${process.env.GIT_DIR || ".git"}/MERGE_RR`)
|
||||
const hasMergeRR = await mergeRR.exists().catch(() => false)
|
||||
const rr = await $`git -C ${dir} rev-parse --git-path MERGE_RR`.text()
|
||||
const hasMergeRR = await Bun.file(rr.trim())
|
||||
.exists()
|
||||
.catch(() => false)
|
||||
if (!hasMergeRR) {
|
||||
await $`git reset -q --hard`.quiet().nothrow()
|
||||
await reset(dir)
|
||||
continue
|
||||
}
|
||||
|
||||
// Record the conflict pre-image
|
||||
await $`git rerere`.quiet().nothrow()
|
||||
await $`git -C ${dir} rerere`.quiet().nothrow()
|
||||
|
||||
// Apply the actual resolution by checking out the merge commit's tree
|
||||
await $`git checkout -q ${commit} -- .`.quiet().nothrow()
|
||||
await $`git -C ${dir} checkout -q ${commit} -- .`.quiet().nothrow()
|
||||
|
||||
// Record the resolution post-image
|
||||
await $`git rerere`.quiet().nothrow()
|
||||
await $`git -C ${dir} rerere`.quiet().nothrow()
|
||||
|
||||
learned++
|
||||
await $`git reset -q --hard`.quiet().nothrow()
|
||||
await reset(dir)
|
||||
}
|
||||
} finally {
|
||||
// Always restore original branch
|
||||
if (branch) {
|
||||
await $`git checkout ${branch.replace("refs/heads/", "")}`.quiet().nothrow()
|
||||
} else {
|
||||
await $`git checkout ${originalHead}`.quiet().nothrow()
|
||||
}
|
||||
await $`git worktree remove --force ${dir}`.quiet().nothrow()
|
||||
await rm(dir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
return learned
|
||||
|
||||
Reference in New Issue
Block a user