import { spawn } from "child_process"; import { ChildProcess, SpawnOptions } from "node:child_process"; // Ignored branches const IGNORED_BRANCHES = ["master", "main", "dev", "release"]; const mainBranch = "dev"; enum Action { Rebase = 0, Reset = 1 } interface RebaseAction { branch: string, onBranch: string, action: Action } interface BranchWithDependencies { rebaseBranch?: string, differenceWithRebase?: number, equalBranches?: string[], ignore?: boolean } /** * Helper function to run a Git command and capture stdout and stderr. */ const runGitCommand = (args: string[], options?: SpawnOptions): Promise => new Promise((resolve, reject) => { const git: ChildProcess = spawn("git", args, { stdio: "pipe", ...options }); let stdout: string = ""; let stderr: string = ""; git.stdout.on("data", (data) => stdout += data.toString()); git.stderr.on("data", (data) => stderr += data.toString()); // Handle completion git.on("close", (code) => (code === 0) ? resolve(stdout) : reject(new Error(`Git command failed with code ${code}: ${stderr}`))); }); /** * Fetch all remote branches. */ const fetchBranches = async (): Promise => { console.log("Fetching all branches..."); await runGitCommand(["fetch", "--all"]); const branches: string = await runGitCommand(["branch", "-r"]); return branches .split("\n") .map((branch) => branch.trim().replace("origin/", "")) .filter((branch) => !IGNORED_BRANCHES.includes(branch) && branch !== ""); } // Get the full commit history for a branch const getCommitsForBranch = async (branch: string): Promise> => { const commits = await runGitCommand(["rev-list", branch]); return new Set(commits.split("\n").filter(Boolean)); }; const removeIgnoredBranches = (branchesWithDependencies: Record) => { let withoutIgnoredBranches = {} for (const [branch, value] of Object.entries(branchesWithDependencies)) { if(!value.ignore) { withoutIgnoredBranches[branch] = value; } } return withoutIgnoredBranches; } // Build the dependency graph const buildRebaseDependencyGraph = async (branches: string[]): Promise> => { const commitHistories: Record> = {}; for (const branch of branches) { commitHistories[branch] = await getCommitsForBranch(branch); } let finalBranches: Record = {}; for (const branchA of branches) { for (const branchB of branches) { if(branchA !== branchB) { const infos = { superset: commitHistories[branchA].isSupersetOf(commitHistories[branchB]), difference: commitHistories[branchA].difference(commitHistories[branchB]) } if (infos.superset) { if(branchB === mainBranch) { // SUPERSET OF MAIN BRANCH, MEANING ALREADY REBASED finalBranches[branchA] = { ignore: true } } if (infos.difference.size === 0) { const prevBranches: string[] = finalBranches[branchA]?.equalBranches ?? []; finalBranches[branchA] = { rebaseBranch: mainBranch, ...finalBranches[branchA], equalBranches: [...prevBranches, branchB] }; } else { if (branchA !== mainBranch && (!finalBranches[branchA] || finalBranches[branchA].differenceWithRebase > infos.difference.size)) { finalBranches[branchA] = { ...finalBranches[branchA], rebaseBranch: branchB, differenceWithRebase: infos.difference.size }; } } } } } } // Set rebase for branches with no dependencies for (const branch of branches) { if(branch !== mainBranch) { finalBranches[branch] = finalBranches[branch] ?? { rebaseBranch: mainBranch, differenceWithRebase: 0 } } } return removeIgnoredBranches(finalBranches); }; const rebaseOrder = (branchesWithDependencies: Record): RebaseAction[] => { console.log("Order everything") // First choose the right actions let orderedActions: RebaseAction[] = [] for (const branch of Object.keys(branchesWithDependencies)) { const alreadyRebasedEqualBranch = orderedActions.find(action => branchesWithDependencies[branch].equalBranches?.some(otherBranch => otherBranch === action.branch)) if(alreadyRebasedEqualBranch) { orderedActions.push({ branch, onBranch: alreadyRebasedEqualBranch.branch, action: Action.Reset }) } else { orderedActions.push({ branch, onBranch: branchesWithDependencies[branch].rebaseBranch, action: Action.Rebase }); } } // Then order by differenceWithRebase and then add resets orderedActions = orderedActions.sort((a,b) => { const diffA = branchesWithDependencies[a.branch].differenceWithRebase; const diffB = branchesWithDependencies[b.branch].differenceWithRebase; return diffA - diffB; }) orderedActions = orderedActions.sort((a,b) => a.action - b.action) return orderedActions; } const rebaseBranch = async ({ branch, onBranch, action }: RebaseAction) => { console.log(`${ action === Action.Rebase ? "Rebasing" : "Resetting" } ${ branch } on ${ onBranch }`); await runGitCommand([ "checkout", branch ]); if(action === Action.Rebase) { await runGitCommand([ "rebase", onBranch ]); } else if(action === Action.Reset) { await runGitCommand([ "reset", "--hard", onBranch ]); } await runGitCommand([ "push", "--force-with-lease" ]); } /** * Main function to execute the workflow. */ const main = async (): Promise => { try { // Step 1: Fetch branches const branches: string[] = (await fetchBranches()); branches.push(mainBranch) console.log("Branches:", branches); // Step 2: Detect dependencies const dependencies = await buildRebaseDependencyGraph(branches); console.log("Dependencies:", dependencies); // Step 3: Determine rebase order const order = rebaseOrder(dependencies); console.log("Rebase order:", order); // Step 4: Rebase branches for (const rebaseAction of order) { await rebaseBranch(rebaseAction); } } catch (error) { console.error("Error during workflow execution:", error.message); } } // Execute the script main();