Last active
April 10, 2026 19:20
-
-
Save Mintonne/fe6e7931b95b59b773e12848f005b698 to your computer and use it in GitHub Desktop.
Script to find all commits that touched a file and mark them as 'edit' for git rebase - https://github.com/Mintonne/find-commits
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
| 'use strict'; | |
| var os = require('os'); | |
| var path = require('path'); | |
| var child_process = require('child_process'); | |
| var fs = require('fs'); | |
| // find-commits - https://github.com/Mintonne/find-commits | |
| function toUnixPath(winPath) { | |
| return winPath.replace(/^([A-Z]):/i, (_, letter) => `/${letter.toLowerCase()}`).replace(/\\/g, "/"); | |
| } | |
| function getCommitsForPath(filePath, baseCommit) { | |
| const range = baseCommit ? `${baseCommit}..HEAD` : ""; | |
| const command = range ? `git log ${range} --oneline --format="%H %s" -- ":(glob)${filePath}"` : `git log --oneline --format="%H %s" -- ":(glob)${filePath}"`; | |
| const output = child_process.execSync(command, { encoding: "utf-8" }).trim(); | |
| if (!output) { | |
| return []; | |
| } | |
| return output.split("\n").map((line) => { | |
| const match = line.match(/^([a-f0-9]+)\s+(.+)$/); | |
| if (!match) { | |
| throw new Error(`Failed to parse log line: ${line}`); | |
| } | |
| return { | |
| hash: match[1], | |
| shortHash: match[1].substring(0, 7), | |
| message: match[2] | |
| }; | |
| }); | |
| } | |
| function getAllCommitsForRebase(baseCommit) { | |
| const range = baseCommit ? `${baseCommit}..HEAD` : ""; | |
| const command = range ? `git log ${range} --oneline --format="%H %s" --reverse` : `git log --oneline --format="%H %s" --reverse`; | |
| const output = child_process.execSync(command, { encoding: "utf-8" }).trim(); | |
| if (!output) { | |
| return []; | |
| } | |
| return output.split("\n").map((line) => { | |
| const match = line.match(/^([a-f0-9]+)\s+(.+)$/); | |
| if (!match) { | |
| throw new Error(`Failed to parse log line: ${line}`); | |
| } | |
| return { | |
| hash: match[1], | |
| shortHash: match[1].substring(0, 7), | |
| message: match[2] | |
| }; | |
| }); | |
| } | |
| function generateRebaseTodo(allCommits, editCommitHashes) { | |
| const lines = []; | |
| for (const commit of allCommits) { | |
| const action = editCommitHashes.has(commit.hash) ? "edit" : "pick"; | |
| lines.push(`${action} ${commit.hash} ${commit.message}`); | |
| } | |
| return lines.join("\n"); | |
| } | |
| function showHelp() { | |
| console.log("Usage: node path/to/find-commits.js <file-path> [base-commit] [-d]"); | |
| console.log(""); | |
| console.log("Arguments:"); | |
| console.log(" file-path - Path to the file (relative to repo root)"); | |
| console.log(" base-commit - Optional: base commit to start from (default: root)"); | |
| console.log(" -d, --preserve-date - Optional: preserve original dates (--committer-date-is-author-date)"); | |
| console.log(" -h, --help - Show this help message"); | |
| console.log(""); | |
| console.log("Examples:"); | |
| console.log(" node path/to/find-commits.js src/utils/validation/index.ts"); | |
| console.log(" node path/to/find-commits.js src/api/client.ts feature/multi-sync -d"); | |
| } | |
| function main() { | |
| const args = process.argv.slice(2); | |
| const helpFlags = ["-h", "--help"]; | |
| const preserveDateFlags = ["-d", "--preserve-date"]; | |
| const flags = [...helpFlags, ...preserveDateFlags]; | |
| const hasFlag = (flagGroup) => flagGroup.some((flag) => args.includes(flag)); | |
| if (args.length < 1 || hasFlag(helpFlags)) { | |
| showHelp(); | |
| process.exit(args.length < 1 ? 1 : 0); | |
| } | |
| const positionalArgs = args.filter((arg) => !flags.includes(arg)); | |
| if (positionalArgs.length < 1) { | |
| console.log("Error: No file path provided."); | |
| console.log(""); | |
| showHelp(); | |
| process.exit(1); | |
| } | |
| const filePath = positionalArgs[0]; | |
| const baseCommit = positionalArgs[1]; | |
| const preserveDate = hasFlag(preserveDateFlags); | |
| console.log(`Finding commits that touched: ${filePath}`); | |
| console.log(`Preserve dates: ${preserveDate}`); | |
| if (baseCommit) { | |
| console.log(`Base commit: ${baseCommit}`); | |
| } | |
| console.log(""); | |
| const fileCommits = getCommitsForPath(filePath, baseCommit); | |
| if (fileCommits.length === 0) { | |
| console.log("No commits found that touched this file."); | |
| process.exit(0); | |
| } | |
| console.log(`Found ${fileCommits.length} commit(s) that touched the file:`); | |
| fileCommits.forEach((c) => { | |
| console.log(` ${c.hash} ${c.message}`); | |
| }); | |
| console.log(""); | |
| console.log("Preparing rebase..."); | |
| const allCommits = getAllCommitsForRebase(baseCommit); | |
| const editHashes = new Set(fileCommits.map((c) => c.hash)); | |
| const rebaseTodo = generateRebaseTodo(allCommits, editHashes); | |
| const tempDir = os.tmpdir(); | |
| const outputPath = path.resolve(tempDir, "rebase-todo.txt"); | |
| const scriptPath = path.resolve(tempDir, "apply-rebase.sh"); | |
| fs.writeFileSync(outputPath, rebaseTodo + "\n"); | |
| const unixOutputPath = toUnixPath(outputPath); | |
| const rebaseFlag = preserveDate ? "--committer-date-is-author-date" : ""; | |
| const scriptContent = `#!/bin/bash | |
| # Auto-generated rebase script | |
| GIT_SEQUENCE_EDITOR="cp '${unixOutputPath}'" git rebase -i ${rebaseFlag} ${baseCommit || "--root"} | |
| `; | |
| fs.writeFileSync(scriptPath, scriptContent); | |
| try { | |
| fs.chmodSync(scriptPath, 493); | |
| } catch { | |
| } | |
| console.log(`Generated files in temp directory:`); | |
| console.log(` Todo: ${outputPath}`); | |
| console.log(` Script: ${scriptPath}`); | |
| console.log(""); | |
| console.log("To apply this rebase, run:"); | |
| const rebaseFlagDisplay = preserveDate ? " --committer-date-is-author-date" : ""; | |
| console.log(` GIT_SEQUENCE_EDITOR="cp '${unixOutputPath}'" git rebase -i${rebaseFlagDisplay} ${baseCommit || "--root"}`); | |
| } | |
| main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment