Files
auto-rebase/app.ts
2024-12-27 16:04:30 +01:00

253 lines
7.9 KiB
TypeScript

import { spawn } from "child_process";
import { ChildProcess, SpawnOptions } from "node:child_process";
import * as core from '@actions/core';
// Ignored branches
const IGNORED_BRANCHES: string[] = core.getInput('branchesToIgnore', { required: false }).split(",");
const mainBranch: string = `origin/${ core.getInput('mainBranch', { required: false }) }`;
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<string> => {
console.log(`Running git command: git ${ args.join(" ") }`);
return 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<string[]> => {
console.log("Fetching all branches...");
await runGitCommand(["fetch", "--all"]);
const branches: string = await runGitCommand(["branch", "-r"]);
return branches
.split("\n")
.map((branch) => branch.trim())
.filter((branch) => !IGNORED_BRANCHES.includes(branch.replace("origin/", "")) && branch !== "");
}
// Get the full commit history for a branch
const getCommitsForBranch = async (branch: string): Promise<Set<string>> => {
const commits = await runGitCommand(["rev-list", branch]);
return new Set(commits.split("\n").filter(Boolean));
};
const removeIgnoredBranches = (branchesWithDependencies: Record<string, any>) => {
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<Record<string, BranchWithDependencies>> => {
const commitHistories: Record<string, Set<string>> = {};
for (const branch of branches) {
commitHistories[branch] = await getCommitsForBranch(branch);
}
let finalBranches: Record<string, BranchWithDependencies> = {};
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<string, BranchWithDependencies>): 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.replace("origin/", "")
]);
if(action === Action.Rebase) {
await runGitCommand([
"rebase",
onBranch
]).catch(async error => {
console.error(error);
await runGitCommand([
"rebase",
"--abort"
])
});
} else if(action === Action.Reset) {
await runGitCommand([
"reset",
"--hard",
onBranch
]);
}
await runGitCommand([
"push",
"--force-with-lease"
]);
}
const setupAutoRebaseGit = async () => {
await runGitCommand([
"config",
"user.email",
"auto-rebase@skydust.fr"
]);
await runGitCommand([
"config",
"user.name",
"Auto Rebase"
]);
}
/**
* Main function to execute the workflow.
*/
const main = async (): Promise<void> => {
try {
console.log("Setup auto rebase")
await setupAutoRebaseGit();
// 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);
process.exit(1);
}
}
// Execute the script
main();