Skip to content

Instantly share code, notes, and snippets.

@sickmz
Created October 9, 2025 17:19
Show Gist options
  • Save sickmz/082fedd725f9a20816810e8d95d7aa0f to your computer and use it in GitHub Desktop.
Save sickmz/082fedd725f9a20816810e8d95d7aa0f to your computer and use it in GitHub Desktop.
git rewrite history
#!/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