Made it work #1

Merged
Skydust merged 1 commits from f/made into main 2024-12-27 13:26:32 +00:00
4 changed files with 289 additions and 19955 deletions

218
app.ts
View File

@@ -1,15 +1,28 @@
import * as core from "@actions/core";
import { spawn } from "child_process"; import { spawn } from "child_process";
import fs from "fs/promises"; import { ChildProcess, SpawnOptions } from "node:child_process";
import path from "path";
import {ChildProcess, SpawnOptions} from "node:child_process";
const myToken = core.getInput("branches");
// Ignored branches // Ignored branches
const IGNORED_BRANCHES = ["master", "main", "dev", "release"]; const IGNORED_BRANCHES = ["master", "main", "dev", "release"];
type BranchDependencies = Record<string, null | string>; 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. * Helper function to run a Git command and capture stdout and stderr.
@@ -37,71 +50,149 @@ const fetchBranches = async (): Promise<string[]> => {
await runGitCommand(["fetch", "--all"]); await runGitCommand(["fetch", "--all"]);
const branches: string = await runGitCommand(["branch", "-r"]); const branches: string = await runGitCommand(["branch", "-r"]);
console.log(branches)
return branches return branches
.split("\n") .split("\n")
.map((branch) => branch.trim().replace("origin/", "")) .map((branch) => branch.trim().replace("origin/", ""))
.filter((branch) => !IGNORED_BRANCHES.includes(branch) && branch !== ""); .filter((branch) => !IGNORED_BRANCHES.includes(branch) && branch !== "");
} }
/**
* Detect dependencies between branches.
*/
const detectDependencies = async (branches: string[]): Promise<BranchDependencies> => {
console.log("Detecting dependencies...");
const dependencies: BranchDependencies = {};
// 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) { for (const branch of branches) {
dependencies[branch] = null; // Default: no dependency commitHistories[branch] = await getCommitsForBranch(branch);
}
for (const otherBranch of branches) { let finalBranches: Record<string, BranchWithDependencies> = {};
if (branch !== otherBranch) { for (const branchA of branches) {
const base = await runGitCommand(["merge-base", `origin/${branch}`, `origin/${otherBranch}`]); for (const branchB of branches) {
const isAncestor = await runGitCommand(["merge-base", "--is-ancestor", base.trim(), `origin/${branch}`]).catch(() => false); if(branchA !== branchB) {
if (isAncestor) { const infos = {
dependencies[branch] = otherBranch; superset: commitHistories[branchA].isSupersetOf(commitHistories[branchB]),
break; 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 ?? [];
return dependencies; finalBranches[branchA] = {
} rebaseBranch: mainBranch,
...finalBranches[branchA],
/** equalBranches: [...prevBranches, branchB]
* 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 visit = (branch) => {
if (visited.has(branch)) return;
visited.add(branch);
if (dependencies[branch]) visit(dependencies[branch]);
order.push(branch);
}; };
} 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;
Object.keys(dependencies).forEach((branch) => visit(branch));
return order;
} }
/** const rebaseBranch = async ({
* Rebase a branch onto its base branch. branch,
*/ onBranch,
const rebaseBranch = async (branch: string, baseBranch = "master"): Promise<void> => { action
console.log(`Rebasing ${branch} onto ${baseBranch}...`); }: RebaseAction) => {
try { console.log(`${ action === Action.Rebase ? "Rebasing" : "Resetting" } ${ branch } on ${ onBranch }`);
await runGitCommand(["checkout", branch]);
await runGitCommand(["fetch", "origin", baseBranch]); await runGitCommand([
await runGitCommand(["rebase", `origin/${baseBranch}`]); "checkout",
console.log(`Rebase successful for ${branch}. Pushing...`); branch
await runGitCommand(["push", "--force-with-lease"]); ]);
} catch (error) {
console.error(`Rebase failed for ${branch}: ${error.message}`); if(action === Action.Rebase) {
await runGitCommand(["rebase", "--abort"]); await runGitCommand([
"rebase",
onBranch
]);
} else if(action === Action.Reset) {
await runGitCommand([
"reset",
"--hard",
onBranch
]);
} }
await runGitCommand([
"push",
"--force-with-lease"
]);
} }
/** /**
@@ -110,24 +201,21 @@ const rebaseBranch = async (branch: string, baseBranch = "master"): Promise<void
const main = async (): Promise<void> => { const main = async (): Promise<void> => {
try { try {
// Step 1: Fetch branches // Step 1: Fetch branches
const branches: string[] = await fetchBranches(); const branches: string[] = (await fetchBranches());
console.log(branches) branches.push(mainBranch)
await fs.writeFile(BRANCHES_FILE, branches.join("\n"), "utf-8");
console.log("Branches:", branches); console.log("Branches:", branches);
// Step 2: Detect dependencies // Step 2: Detect dependencies
const dependencies = await detectDependencies(branches); const dependencies = await buildRebaseDependencyGraph(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 // Step 3: Determine rebase order
const order = determineRebaseOrder(dependencies); const order = rebaseOrder(dependencies);
console.log("Rebase order:", order); console.log("Rebase order:", order);
// Step 4: Rebase branches // Step 4: Rebase branches
for (const branch of order) { for (const rebaseAction of order) {
const baseBranch = dependencies[branch] || "master"; await rebaseBranch(rebaseAction);
await rebaseBranch(branch, baseBranch);
} }
} catch (error) { } catch (error) {
console.error("Error during workflow execution:", error.message); console.error("Error during workflow execution:", error.message);

20012
dist/app.js vendored

File diff suppressed because one or more lines are too long

8
dist/package.json vendored
View File

@@ -4,7 +4,8 @@
"description": "", "description": "",
"main": "app.js", "main": "app.js",
"scripts": { "scripts": {
"build": "cp package.json dist/ && esbuild --outdir=dist --bundle --platform=node --allow-overwrite ./app.ts" "build": "cp package.json dist/ && esbuild --outdir=dist --bundle --platform=node --allow-overwrite ./app.ts",
"dev": "tsx app.ts"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
@@ -12,9 +13,10 @@
"@actions/core": "^1.11.1" "@actions/core": "^1.11.1"
}, },
"devDependencies": { "devDependencies": {
"@actions/core": "^1.11.1",
"@types/node": "^22.10.0", "@types/node": "^22.10.0",
"esbuild": "^0.24.0", "esbuild": "^0.24.0",
"tsx": "^4.19.2",
"typescript": "^5.7.2" "typescript": "^5.7.2"
} },
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
} }

View File

@@ -13,10 +13,10 @@
"@actions/core": "^1.11.1" "@actions/core": "^1.11.1"
}, },
"devDependencies": { "devDependencies": {
"@actions/core": "^1.11.1",
"@types/node": "^22.10.0", "@types/node": "^22.10.0",
"esbuild": "^0.24.0", "esbuild": "^0.24.0",
"tsx": "^4.19.2", "tsx": "^4.19.2",
"typescript": "^5.7.2" "typescript": "^5.7.2"
} },
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
} }