mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 04:46:43 +08:00
feat: enable git rerere
This commit is contained in:
@@ -141,6 +141,22 @@ async function main() {
|
||||
const currentBranch = await git.getCurrentBranch()
|
||||
logger.info(`Current branch: ${currentBranch}`)
|
||||
|
||||
// Enable git rerere so conflict resolutions are recorded and reused across merges
|
||||
if (!options.dryRun) {
|
||||
await git.ensureRerere()
|
||||
logger.info("git rerere enabled (resolutions will be recorded and reused automatically)")
|
||||
|
||||
// Train rerere from past upstream merge commits so the cache is populated
|
||||
// even on a fresh clone. This replays past merges to learn their resolutions.
|
||||
logger.info("Training rerere cache from past merge history...")
|
||||
const learned = await git.trainRerere("merge: upstream\\|Resolve merge conflict")
|
||||
if (learned > 0) {
|
||||
logger.success(`Learned ${learned} conflict resolution(s) from history`)
|
||||
} else {
|
||||
logger.info("No new resolutions to learn from history (cache already up to date)")
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Fetch upstream
|
||||
logger.step(2, 8, "Fetching upstream...")
|
||||
|
||||
@@ -385,6 +401,14 @@ async function main() {
|
||||
logger.info("Conflicted files:")
|
||||
logger.list(mergeResult.conflicts)
|
||||
|
||||
// Check if git rerere already auto-resolved any conflicts from recorded history.
|
||||
// rerere.autoupdate stages them automatically; we just log how many were handled.
|
||||
const rerereResolved = await git.getRerereResolved()
|
||||
if (rerereResolved.length > 0) {
|
||||
logger.success(`git rerere auto-resolved ${rerereResolved.length} conflict(s) from recorded history:`)
|
||||
logger.list(rerereResolved)
|
||||
}
|
||||
|
||||
// Since we applied all branding transforms pre-merge, remaining conflicts should be minimal.
|
||||
// These are likely files with kilocode_change markers or actual logic differences.
|
||||
|
||||
|
||||
@@ -236,3 +236,105 @@ export async function oursHasKilocodeChanges(file: string): Promise<boolean> {
|
||||
if (result.exitCode !== 0) return false
|
||||
return result.stdout.toString().includes("kilocode_change")
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable git rerere (REuse REcorded REsolution) in the local repo config.
|
||||
* Also enables autoupdate so resolved files are automatically staged.
|
||||
*/
|
||||
export async function ensureRerere(): Promise<void> {
|
||||
await $`git config rerere.enabled true`.quiet()
|
||||
await $`git config rerere.autoupdate true`.quiet()
|
||||
}
|
||||
|
||||
/**
|
||||
* Train the rerere cache from past merge commits in the repo history.
|
||||
* Implements the same logic as git's contrib/rerere-train.sh:
|
||||
* For each merge commit in the range, replay the merge to let rerere
|
||||
* record the pre-image, then check out the resolved tree so rerere
|
||||
* records the post-image (the resolution).
|
||||
*
|
||||
* 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()
|
||||
|
||||
let learned = 0
|
||||
|
||||
try {
|
||||
// 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
|
||||
|
||||
const lines = revList.stdout
|
||||
.toString()
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter((l) => l.trim())
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/)
|
||||
if (parts.length < 3) continue // skip non-merges (need commit + at least 2 parents)
|
||||
|
||||
const [commit, parent1, ...otherParents] = parts
|
||||
|
||||
// Checkout the first parent
|
||||
const coResult = await $`git 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()
|
||||
if (mergeResult.exitCode === 0) {
|
||||
// Cleanly merged — no conflicts to learn from, reset and skip
|
||||
await $`git reset -q --hard`.quiet().nothrow()
|
||||
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)
|
||||
if (!hasMergeRR) {
|
||||
await $`git reset -q --hard`.quiet().nothrow()
|
||||
continue
|
||||
}
|
||||
|
||||
// Record the conflict pre-image
|
||||
await $`git rerere`.quiet().nothrow()
|
||||
|
||||
// Apply the actual resolution by checking out the merge commit's tree
|
||||
await $`git checkout -q ${commit} -- .`.quiet().nothrow()
|
||||
|
||||
// Record the resolution post-image
|
||||
await $`git rerere`.quiet().nothrow()
|
||||
|
||||
learned++
|
||||
await $`git reset -q --hard`.quiet().nothrow()
|
||||
}
|
||||
} finally {
|
||||
// Always restore original branch
|
||||
if (branch) {
|
||||
await $`git checkout ${branch.replace("refs/heads/", "")}`.quiet().nothrow()
|
||||
} else {
|
||||
await $`git checkout ${originalHead}`.quiet().nothrow()
|
||||
}
|
||||
}
|
||||
|
||||
return learned
|
||||
}
|
||||
|
||||
/**
|
||||
* Return files that git rerere has already auto-resolved.
|
||||
* These files no longer have conflict markers but haven't been staged yet
|
||||
* (unless rerere.autoupdate is true, in which case they're already staged).
|
||||
*/
|
||||
export async function getRerereResolved(): Promise<string[]> {
|
||||
const result = await $`git rerere status`.quiet().nothrow()
|
||||
if (result.exitCode !== 0 || !result.stdout.toString().trim()) return []
|
||||
return result.stdout
|
||||
.toString()
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter((f) => f.length > 0)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user