Created
October 9, 2025 17:19
-
-
Save sickmz/082fedd725f9a20816810e8d95d7aa0f to your computer and use it in GitHub Desktop.
git rewrite 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
| #!/usr/bin/env node | |
| import { spawnSync } from "child_process"; | |
| import * as readline from "readline"; | |
| interface CommitInfo { | |
| hash: string; | |
| date: Date; | |
| } | |
| const rl = readline.createInterface({ | |
| input: process.stdin, | |
| output: process.stdout, | |
| }); | |
| function prompt(question: string): Promise<string> { | |
| return new Promise((resolve) => { | |
| rl.question(question, resolve); | |
| }); | |
| } | |
| function exec(command: string, args: string[] = []): string { | |
| const result = spawnSync(command, args, { | |
| encoding: "utf-8", | |
| shell: false, | |
| }); | |
| if (result.status !== 0) { | |
| const errorDetails = | |
| result.stderr || result.error?.message || "Unknown error"; | |
| throw new Error( | |
| `Command failed: ${command} ${args.join(" ")}\n${errorDetails}` | |
| ); | |
| } | |
| return result.stdout.trim(); | |
| } | |
| function checkGitRepo(): void { | |
| try { | |
| exec("git", ["rev-parse", "--git-dir"]); | |
| } catch { | |
| console.error("Not a git repository"); | |
| process.exit(1); | |
| } | |
| } | |
| function getCommits(count?: number): string[] { | |
| const args = count | |
| ? ["log", `--pretty=format:%H`, `-${count}`] | |
| : ["log", `--pretty=format:%H`]; | |
| const output = exec("git", args); | |
| if (!output) { | |
| return []; | |
| } | |
| return output.split("\n").reverse(); | |
| } | |
| function randomTime( | |
| startHour: number, | |
| endHour: number | |
| ): { hour: number; minute: number; second: number } { | |
| return { | |
| hour: Math.floor(Math.random() * (endHour - startHour + 1)) + startHour, | |
| minute: Math.floor(Math.random() * 60), | |
| second: Math.floor(Math.random() * 60), | |
| }; | |
| } | |
| function generateCommitDates( | |
| commits: string[], | |
| startDate: Date, | |
| days: number, | |
| startHour: number = 20, | |
| endHour: number = 23 | |
| ): CommitInfo[] { | |
| const commitsPerDay = commits.length / days; | |
| return commits.map((hash, index) => { | |
| const dayOffset = Math.floor(index / commitsPerDay); | |
| const { hour, minute, second } = randomTime(startHour, endHour); | |
| const date = new Date(startDate); | |
| date.setDate(date.getDate() + dayOffset); | |
| date.setHours(hour, minute, second, 0); | |
| return { hash, date }; | |
| }); | |
| } | |
| async function rewriteHistory(commitInfos: CommitInfo[]): Promise<void> { | |
| console.log("\nCreating backup branch..."); | |
| try { | |
| exec("git", ["branch", "backup-before-rewrite"]); | |
| } catch (e: any) { | |
| console.warn("Could not create backup branch. It might already exist."); | |
| } | |
| console.log("Rewriting commit history...\n"); | |
| const filterScriptLines = commitInfos.map(({ hash, date }) => { | |
| const dateStr = date.toISOString().substring(0, 19); | |
| return `"${hash}:${dateStr}"`; | |
| }); | |
| const filterScript = filterScriptLines.join(" "); | |
| const envFilter = `COMMIT_DATES=(${filterScript}); for mapping in "\${COMMIT_DATES[@]}"; do commit_hash="\${mapping%%:*}"; new_date="\${mapping#*:}"; if [[ "\$GIT_COMMIT" == "\${commit_hash}"* ]]; then export GIT_AUTHOR_DATE="\$new_date"; export GIT_COMMITTER_DATE="\$new_date"; fi; done`; | |
| try { | |
| const oldestCommit = commitInfos[0].hash; | |
| let revisionArgs: string[]; | |
| try { | |
| const parentCommit = exec("git", ["rev-parse", `${oldestCommit}^`]); | |
| revisionArgs = [`${parentCommit}..HEAD`]; | |
| } catch (e) { | |
| console.log( | |
| "Oldest commit is the root commit. Rewriting all history on current branch." | |
| ); | |
| revisionArgs = ["HEAD"]; | |
| } | |
| const filterBranchArgs = [ | |
| "filter-branch", | |
| "-f", | |
| "--env-filter", | |
| envFilter, | |
| ...revisionArgs, | |
| ]; | |
| exec("git", filterBranchArgs); | |
| console.log("\nRewrite complete!\n"); | |
| console.log("Summary:"); | |
| commitInfos.slice(0, 5).forEach(({ hash, date }, i) => { | |
| const dateStr = date.toLocaleString("it-IT", { | |
| year: "numeric", | |
| month: "2-digit", | |
| day: "2-digit", | |
| hour: "2-digit", | |
| minute: "2-digit", | |
| }); | |
| console.log(` [${i + 1}] ${hash.substring(0, 7)} → ${dateStr}`); | |
| }); | |
| if (commitInfos.length > 5) { | |
| console.log(` ... and ${commitInfos.length - 5} more`); | |
| } | |
| console.log("\nBackup: git checkout backup-before-rewrite"); | |
| console.log( | |
| 'Verify: git log --pretty=format:"%h %ad %s" --date=format:"%Y-%m-%d %H:%M"' | |
| ); | |
| } catch (error: any) { | |
| console.error("\nError during rewrite:", error.message); | |
| console.log("\nAttempting to restore..."); | |
| try { | |
| exec("git", ["reset", "--hard", "backup-before-rewrite"]); | |
| console.log("Restore successful."); | |
| } catch (restoreError) { | |
| console.error( | |
| "Restore failed. Please check your repository state manually." | |
| ); | |
| } | |
| process.exit(1); | |
| } | |
| } | |
| async function main() { | |
| console.log("Git Commit Timestamp Rewriter"); | |
| console.log("=".repeat(50)); | |
| checkGitRepo(); | |
| const countInput = await prompt("\nHow many commits? (Enter for all): "); | |
| const count = countInput ? parseInt(countInput) : undefined; | |
| const dateInput = await prompt("Start date (YYYY-MM-DD, Enter for today): "); | |
| const startDate = dateInput | |
| ? new Date(dateInput + "T00:00:00") | |
| : new Date(new Date().setHours(0, 0, 0, 0)); | |
| const daysInput = await prompt("Spread over how many days? (default 20): "); | |
| const days = daysInput ? parseInt(daysInput) : 20; | |
| const commits = getCommits(count); | |
| if (commits.length === 0) { | |
| console.error("No commits found"); | |
| rl.close(); | |
| process.exit(1); | |
| } | |
| console.log(`\nFound ${commits.length} commits`); | |
| console.log(`Start: ${startDate.toLocaleDateString("it-IT")}`); | |
| const endDate = new Date(startDate); | |
| endDate.setDate(endDate.getDate() + days); | |
| console.log(`End: ${endDate.toLocaleDateString("it-IT")}`); | |
| console.log(`Time: 20:00 - 23:59`); | |
| const confirm = await prompt("\nRewrite git history? (yes/no): "); | |
| if (confirm.toLowerCase() !== "yes") { | |
| console.log("Aborted."); | |
| rl.close(); | |
| process.exit(0); | |
| } | |
| const commitInfos = generateCommitDates(commits, startDate, days); | |
| await rewriteHistory(commitInfos); | |
| rl.close(); | |
| } | |
| main().catch((error) => { | |
| console.error("Fatal error:", error.message); | |
| rl.close(); | |
| process.exit(1); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment