Merge branch 'Kilo-Org:main' into fix/openai-compat-reasoning-params

This commit is contained in:
VastCosmic
2026-05-06 18:11:44 +08:00
committed by GitHub
5 changed files with 61 additions and 24 deletions
+3 -1
View File
@@ -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:
+8 -3
View File
@@ -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()
+15
View File
@@ -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")
})
+6
View File
@@ -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
}
+29 -20
View File
@@ -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
@@ -267,6 +271,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:
@@ -277,14 +286,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
@@ -301,45 +310,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