diff --git a/app.ts b/app.ts index e18d0ad..ad4c06c 100644 --- a/app.ts +++ b/app.ts @@ -1,15 +1,21 @@ -import * as core from "@actions/core"; import { spawn } from "child_process"; -import fs from "fs/promises"; -import path from "path"; -import {ChildProcess, SpawnOptions} from "node:child_process"; - -const myToken = core.getInput("branches"); +import { ChildProcess, SpawnOptions } from "node:child_process"; // Ignored branches const IGNORED_BRANCHES = ["master", "main", "dev", "release"]; -type BranchDependencies = Record; +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. @@ -44,63 +50,140 @@ const fetchBranches = async (): Promise => { .filter((branch) => !IGNORED_BRANCHES.includes(branch) && branch !== ""); } -/** - * Detect dependencies between branches. - */ -const detectDependencies = async (branches: string[]): Promise => { - console.log("Detecting dependencies..."); - const dependencies: BranchDependencies = {}; +// 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) { - dependencies[branch] = null; // Default: no dependency + commitHistories[branch] = await getCommitsForBranch(branch); + } - for (const otherBranch of branches) { - if (branch !== otherBranch) { - const base = await runGitCommand(["merge-base", `origin/${branch}`, `origin/${otherBranch}`]); - const isAncestor = await runGitCommand(["merge-base", "--is-ancestor", base.trim(), `origin/${branch}`]).catch(() => false); - if (isAncestor) { - dependencies[branch] = otherBranch; - break; + 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 + }; + } + } } } } } - return dependencies; + + // 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; } -/** - * Perform a topological sort to determine the rebase order. - */ -const determineRebaseOrder = (dependencies: BranchDependencies) => { - console.log("Determining rebase order..."); - const visited = new Set(); - const order = []; +const rebaseOrder = (branchesWithDependencies: any): RebaseAction[] => { + console.log("Order everything") - const visit = (branch) => { - if (visited.has(branch)) return; - visited.add(branch); - if (dependencies[branch]) visit(dependencies[branch]); - order.push(branch); - }; + // 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; - Object.keys(dependencies).forEach((branch) => visit(branch)); - return order; } -/** - * Rebase a branch onto its base branch. - */ -const rebaseBranch = async (branch: string, baseBranch = "master"): Promise => { - console.log(`Rebasing ${branch} onto ${baseBranch}...`); - try { - await runGitCommand(["checkout", branch]); - await runGitCommand(["fetch", "origin", baseBranch]); - await runGitCommand(["rebase", `origin/${baseBranch}`]); - console.log(`Rebase successful for ${branch}. Pushing...`); - await runGitCommand(["push", "--force-with-lease"]); - } catch (error) { - console.error(`Rebase failed for ${branch}: ${error.message}`); - await runGitCommand(["rebase", "--abort"]); +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" + ]); } } @@ -110,24 +193,20 @@ const rebaseBranch = async (branch: string, baseBranch = "master"): Promise => { try { // Step 1: Fetch branches - const branches: string[] = await fetchBranches(); - console.log(branches) - await fs.writeFile(BRANCHES_FILE, branches.join("\n"), "utf-8"); + const branches: string[] = (await fetchBranches()); + branches.push("origin/main") console.log("Branches:", branches); + const dependencies = await buildRebaseDependencyGraph(branches); // Step 2: Detect dependencies - const dependencies = await detectDependencies(branches); - await fs.writeFile(DEPENDENCIES_FILE, JSON.stringify(dependencies, null, 2), "utf-8"); - console.log("Dependencies:", dependencies); + console.log("Dependencies:", dependencies); // Step 3: Determine rebase order - const order = determineRebaseOrder(dependencies); + const order = rebaseOrder(dependencies); console.log("Rebase order:", order); - // Step 4: Rebase branches - for (const branch of order) { - const baseBranch = dependencies[branch] || "master"; - await rebaseBranch(branch, baseBranch); + for (const rebaseAction of order) { + await rebaseBranch(rebaseAction); } } catch (error) { console.error("Error during workflow execution:", error.message); diff --git a/package.json b/package.json index 0f99aa0..a6f888e 100644 --- a/package.json +++ b/package.json @@ -18,5 +18,6 @@ "esbuild": "^0.24.0", "tsx": "^4.19.2", "typescript": "^5.7.2" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" }