Skip to content

Instantly share code, notes, and snippets.

@crazyrabbitLTC
Created December 18, 2024 19:44
Show Gist options
  • Save crazyrabbitLTC/7644f77d0014db9e6ebc451d0bd62797 to your computer and use it in GitHub Desktop.
Save crazyrabbitLTC/7644f77d0014db9e6ebc451d0bd62797 to your computer and use it in GitHub Desktop.
LLM Script to rewrite your Github History
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