diff --git a/script/upstream/utils/git.ts b/script/upstream/utils/git.ts index a781276e983..a04f513e5e5 100644 --- a/script/upstream/utils/git.ts +++ b/script/upstream/utils/git.ts @@ -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 { await $`git config rerere.autoupdate true`.quiet() } +async function reset(dir: string): Promise { + 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 { * Returns the number of resolutions learned. */ export async function trainRerere(grep: string): Promise { - // 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 { 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