Made it work
This commit was merged in pull request #1.
This commit is contained in:
218
app.ts
218
app.ts
@@ -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
20012
dist/app.js
vendored
File diff suppressed because one or more lines are too long
8
dist/package.json
vendored
8
dist/package.json
vendored
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user