Created
December 18, 2024 19:44
-
-
Save crazyrabbitLTC/7644f77d0014db9e6ebc451d0bd62797 to your computer and use it in GitHub Desktop.
LLM Script to rewrite your Github History
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { exec } from 'child_process'; | |
import { promisify } from 'util'; | |
import * as fs from 'fs/promises'; | |
import path from 'path'; | |
import { z } from 'zod'; | |
import OpenAI from 'openai'; | |
import { zodResponseFormat } from 'openai/helpers/zod'; | |
const execAsync = promisify(exec); | |
const CommitMessageSchema = z.object({ | |
type: z.enum(["feat", "fix", "docs", "style", "refactor", "test", "chore"]), | |
description: z.string(), | |
body: z.union([z.string(), z.null()]) | |
}).strict(); | |
interface Commit { | |
hash: string; | |
parentHash: string | null; | |
oldMessage: string; | |
diff: string; | |
} | |
interface CommitWithNewMessage extends Commit { | |
newMessage: string; | |
} | |
async function getGitRoot(): Promise<string> { | |
const { stdout, stderr } = await execAsync('git rev-parse --show-toplevel', { maxBuffer: 100 * 1024 * 1024 }); | |
if (stderr) { | |
throw new Error(stderr); | |
} | |
return stdout.trim(); | |
} | |
async function getAllCommits(): Promise<Commit[]> { | |
const { stdout: revList } = await execAsync('git rev-list --reverse HEAD', { maxBuffer: 100 * 1024 * 1024 }); | |
const hashes = revList.trim().split('\n'); | |
const commits: Commit[] = []; | |
let processed = 0; | |
for (const hash of hashes) { | |
processed++; | |
process.stdout.write(`\r💫 Reading commit details (${processed}/${hashes.length})`); | |
const { stdout: parentHash } = await execAsync(`git rev-parse ${hash}^@ 2>/dev/null || echo ""`, { maxBuffer: 100 * 1024 * 1024 }); | |
const { stdout: oldMessage } = await execAsync(`git log -1 --pretty=%B ${hash}`, { maxBuffer: 100 * 1024 * 1024 }); | |
const { stdout: diff } = await execAsync(`git show --pretty=format: ${hash}`, { maxBuffer: 100 * 1024 * 1024 }); | |
commits.push({ | |
hash, | |
parentHash: parentHash.trim() || null, | |
oldMessage: oldMessage.trim(), | |
diff: diff.trim() | |
}); | |
} | |
console.log('\n✅ Successfully retrieved all commit information'); | |
return commits; | |
} | |
async function generateCommitMessage(diff: string): Promise<string> { | |
const OPENAI_API_KEY = process.env.OPENAI_API_KEY; | |
if (!OPENAI_API_KEY) { | |
throw new Error('❌ OPENAI_API_KEY environment variable is required'); | |
} | |
const openai = new OpenAI({ apiKey: OPENAI_API_KEY }); | |
const MAX_DIFF_LENGTH = 50000; | |
const truncatedDiff = diff.length > MAX_DIFF_LENGTH ? diff.substring(0, MAX_DIFF_LENGTH) + "\n... [diff truncated]" : diff; | |
const messages = [ | |
{ | |
role: 'system', | |
content: `You are a helpful assistant that generates commit messages in conventional commit format. Return only valid JSON that adheres to the given schema.` | |
}, | |
{ | |
role: 'user', | |
content: `Given the following git diff, produce a commit message as a JSON object that matches this schema: | |
{ | |
"type": "one of [\"feat\", \"fix\", \"docs\", \"style\", \"refactor\", \"test\", \"chore\"]", | |
"description": "A short summary", | |
"body": "A longer body explaining what changed and why, or null if no additional info" | |
} | |
Return only the JSON object and nothing else. If no additional body is needed, use null for body. | |
Diff: | |
${truncatedDiff}` | |
} | |
]; | |
const maxAttempts = 5; | |
let attempt = 0; | |
let waitTime = 5000; // initial wait time (5 seconds) | |
while (attempt < maxAttempts) { | |
attempt++; | |
try { | |
const completion = await openai.beta.chat.completions.parse({ | |
model: "gpt-4o-2024-08-06", | |
messages, | |
response_format: zodResponseFormat(CommitMessageSchema, "commit_message"), | |
temperature: 0 | |
}); | |
const result = completion.choices[0].message; | |
if (result.refusal) { | |
throw new Error(`❌ Model refused: ${result.refusal}`); | |
} | |
if (!result.parsed) { | |
throw new Error('❌ Invalid response - no parsed result'); | |
} | |
const commitData = result.parsed; | |
// Construct the final commit message | |
let finalMessage = `${commitData.type}: ${commitData.description}`; | |
if (commitData.body && commitData.body.trim() !== "") { | |
finalMessage += `\n\n${commitData.body}`; | |
} | |
return finalMessage; | |
} catch (error: any) { | |
if (error.message && (error.message.includes('429') || error.message.includes('503'))) { | |
console.warn(`⚠️ Rate limit or server error on attempt ${attempt}. Retrying in ${waitTime / 1000} seconds...`); | |
await new Promise(resolve => setTimeout(resolve, waitTime)); | |
waitTime *= 2; | |
} else if (attempt < maxAttempts) { | |
console.warn(`⚠️ Attempt ${attempt} failed. Retrying in ${waitTime / 1000} seconds...`, error); | |
await new Promise(resolve => setTimeout(resolve, waitTime)); | |
waitTime *= 2; | |
} else { | |
throw error; | |
} | |
} | |
} | |
throw new Error('❌ Failed after multiple attempts'); | |
} | |
async function rewriteHistory(commits: CommitWithNewMessage[]): Promise<void> { | |
console.log('\n🔄 Starting history rewrite...'); | |
const timestamp = new Date().getTime(); | |
const newBranch = `rewritten-history-${timestamp}`; | |
console.log(`📌 Creating new branch: ${newBranch}`); | |
await execAsync(`git checkout --orphan ${newBranch}`, { maxBuffer: 100 * 1024 * 1024 }); | |
await execAsync('git reset', { maxBuffer: 100 * 1024 * 1024 }); | |
let lastCommitHash: string | null = null; | |
let processed = 0; | |
for (const commit of commits) { | |
processed++; | |
process.stdout.write(`\r📝 Rewriting commits (${processed}/${commits.length})`); | |
const { stdout: tree } = await execAsync(`git show ${commit.hash} --pretty=format:%T -s`, { maxBuffer: 100 * 1024 * 1024 }); | |
const newCommitCmd = [ | |
'git commit-tree', | |
tree.trim(), | |
lastCommitHash ? `-p ${lastCommitHash}` : '', | |
`-m "${commit.newMessage.replace(/"/g, '\\"')}"` | |
].filter(Boolean).join(' '); | |
const { stdout: newHash } = await execAsync(newCommitCmd, { maxBuffer: 100 * 1024 * 1024 }); | |
lastCommitHash = newHash.trim(); | |
} | |
if (lastCommitHash) { | |
console.log('\n🔀 Updating branch reference...'); | |
await execAsync(`git update-ref refs/heads/${newBranch} ${lastCommitHash}`, { maxBuffer: 100 * 1024 * 1024 }); | |
await execAsync(`git checkout ${newBranch}`, { maxBuffer: 100 * 1024 * 1024 }); | |
console.log(`✅ History has been rewritten in new branch: ${newBranch}`); | |
} else { | |
throw new Error('❌ No commits were processed'); | |
} | |
} | |
async function main() { | |
console.log('🚀 Starting git history rewrite process...\n'); | |
try { | |
const rootPath = await getGitRoot(); | |
const commits = await getAllCommits(); | |
console.log('\n🤖 Generating new commit messages using OpenAI...'); | |
const commitsWithNewMessages: CommitWithNewMessage[] = []; | |
let processedCount = 0; | |
for (const commit of commits) { | |
processedCount++; | |
process.stdout.write(`\r🤖 Generating commit message (${processedCount}/${commits.length})`); | |
// Insert a small delay to reduce rate limit issues | |
await new Promise(resolve => setTimeout(resolve, 2000)); | |
const newMessage = await generateCommitMessage(commit.diff); | |
console.log('\n----------------------------------------'); | |
console.log(`Old Commit Message (${commit.hash}):\n${commit.oldMessage}`); | |
console.log('\nNew Commit Message:\n' + newMessage); | |
console.log('----------------------------------------\n'); | |
commitsWithNewMessages.push({ | |
...commit, | |
newMessage | |
}); | |
} | |
console.log('\n✅ Successfully generated all new commit messages'); | |
await rewriteHistory(commitsWithNewMessages); | |
console.log('\n💾 Saving commit message mapping...'); | |
const mapping = commitsWithNewMessages.map(commit => ({ | |
oldHash: commit.hash, | |
oldMessage: commit.oldMessage, | |
newMessage: commit.newMessage | |
})); | |
const mappingPath = path.join(rootPath, 'commit-message-mapping.json'); | |
await fs.writeFile(mappingPath, JSON.stringify(mapping, null, 2)); | |
console.log('\n🎉 Success! Your git history has been rewritten'); | |
console.log(`📄 A mapping of old to new commits has been saved in: ${mappingPath}`); | |
console.log('\n⚠️ To review the changes:'); | |
console.log('1. Check the current branch for the new commit messages'); | |
console.log('2. Review the mapping file for a comparison with old messages'); | |
console.log('3. If you need to start over, run: git branch -D rewritten-history-*'); | |
} catch (error: any) { | |
console.error('\n❌ Error:', error.message); | |
console.log('\n💡 Tips:'); | |
console.log('- Ensure you have set OPENAI_API_KEY environment variable'); | |
console.log('- Check that you are in a git repository'); | |
console.log('- Make sure you have permissions to create branches'); | |
console.log('- Consider truncating large diffs'); | |
process.exit(1); | |
} | |
} | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment