import { spawn } from "child_process"; import { ChildProcess, SpawnOptions } from "node:child_process"; // Ignored branches const IGNORED_BRANCHES = ["master", "main", "dev", "release"]; const mainBranch = "main"; enum Action { Rebase, Reset } interface RebaseAction { branch: string, onBranch: string, action: Action } /** * 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"]); console.log(branches) 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)); }; // 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 (infos.difference.size === 0) { const prevBranches = finalBranches[branchA]?.equalBranches ?? []; finalBranches[branchA] = { rebaseBranch: mainBranch, ...finalBranches[branchA], equalBranches: [...prevBranches, branchB] // IF diff 0 > to reset --hard }; } else { if (!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) { finalBranches[branch] = finalBranches[branch] ?? { rebaseBranch: mainBranch, differenceWithRebase: 0 } } return removeMainBranch(finalBranches); }; const removeMainBranch = (branchesWithDependencies: Record) => { let withoutMainBranch = {} for (const [branch, value] of Object.entries(branchesWithDependencies)) { if(branch !== mainBranch) { withoutMainBranch[branch] = value; } } return withoutMainBranch; } const rebaseOrder = (branchesWithDependencies: any): 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) => { await runGitCommand([ "checkout", branch ]); if(action === Action.Rebase) { await runGitCommand([ "rebase", onBranch ]); await runGitCommand([ "push", "--force-with-lease" ]); } else { 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("origin/main") console.log("Branches:", branches); const dependencies = await buildRebaseDependencyGraph(branches); // Step 2: Detect dependencies 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();