mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
Cleanup old branches. (#19354)
This commit is contained in:
180
scripts/cleanup-branches.ts
Normal file
180
scripts/cleanup-branches.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import * as readline from 'node:readline/promises';
|
||||
import * as process from 'node:process';
|
||||
|
||||
function runCmd(cmd: string): string {
|
||||
return execSync(cmd, {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
}).trim();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
runCmd('gh --version');
|
||||
} catch {
|
||||
console.error(
|
||||
'Error: "gh" CLI is required but not installed or not working.',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
runCmd('git --version');
|
||||
} catch {
|
||||
console.error('Error: "git" is required.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Fetching remote branches from origin...');
|
||||
let allBranchesOutput = '';
|
||||
try {
|
||||
// Also fetch to ensure we have the latest commit dates
|
||||
console.log(
|
||||
'Running git fetch to ensure we have up-to-date commit dates and prune stale branches...',
|
||||
);
|
||||
runCmd('git fetch origin --prune');
|
||||
|
||||
// Get all branches with their commit dates
|
||||
allBranchesOutput = runCmd(
|
||||
"git for-each-ref --format='%(refname:lstrip=3) %(committerdate:unix)' refs/remotes/origin",
|
||||
);
|
||||
} catch {
|
||||
console.error('Failed to fetch branches from origin.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const THIRTY_DAYS_IN_SECONDS = 30 * 24 * 60 * 60;
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const remoteBranches: { name: string; lastCommitDate: number }[] =
|
||||
allBranchesOutput
|
||||
.split(/\r?\n/)
|
||||
.map((line) => {
|
||||
const parts = line.split(' ');
|
||||
if (parts.length < 2) return null;
|
||||
const date = parseInt(parts.pop() || '0', 10);
|
||||
const name = parts.join(' ');
|
||||
return { name, lastCommitDate: date };
|
||||
})
|
||||
.filter((b): b is { name: string; lastCommitDate: number } => b !== null);
|
||||
|
||||
console.log(`Found ${remoteBranches.length} branches on origin.`);
|
||||
|
||||
console.log('Fetching open PRs...');
|
||||
let openPrsJson = '[]';
|
||||
try {
|
||||
openPrsJson = runCmd(
|
||||
'gh pr list --state open --limit 5000 --json headRefName',
|
||||
);
|
||||
} catch {
|
||||
console.error('Failed to fetch open PRs.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const openPrs = JSON.parse(openPrsJson);
|
||||
const openPrBranches = new Set(
|
||||
openPrs.map((pr: { headRefName: string }) => pr.headRefName),
|
||||
);
|
||||
|
||||
const protectedPattern =
|
||||
/^(main|master|next|release[-/].*|hotfix[-/].*|v\d+.*|HEAD|gh-readonly-queue.*)$/;
|
||||
|
||||
const branchesToDelete = remoteBranches.filter((branch) => {
|
||||
if (protectedPattern.test(branch.name)) {
|
||||
return false;
|
||||
}
|
||||
if (openPrBranches.has(branch.name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ageInSeconds = now - branch.lastCommitDate;
|
||||
if (ageInSeconds < THIRTY_DAYS_IN_SECONDS) {
|
||||
return false; // Skip branches pushed to recently
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (branchesToDelete.length === 0) {
|
||||
console.log('No remote branches to delete.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
'\nThe following remote branches are NOT release branches, have NO active PR, and are OLDER than 30 days:',
|
||||
);
|
||||
console.log(
|
||||
'---------------------------------------------------------------------',
|
||||
);
|
||||
branchesToDelete.forEach((b) => console.log(` - ${b.name}`));
|
||||
console.log(
|
||||
'---------------------------------------------------------------------',
|
||||
);
|
||||
console.log(`Total to delete: ${branchesToDelete.length}`);
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const answer = await rl.question(
|
||||
`\nDo you want to delete these ${branchesToDelete.length} remote branches from origin? (y/N) `,
|
||||
);
|
||||
rl.close();
|
||||
|
||||
if (answer.toLowerCase() === 'y') {
|
||||
console.log('Deleting remote branches...');
|
||||
// Delete in batches to avoid hitting command line length limits
|
||||
const batchSize = 50;
|
||||
for (let i = 0; i < branchesToDelete.length; i += batchSize) {
|
||||
const batch = branchesToDelete.slice(i, i + batchSize).map((b) => b.name);
|
||||
const branchList = batch.join(' ');
|
||||
console.log(`Deleting remote batch ${Math.floor(i / batchSize) + 1}...`);
|
||||
try {
|
||||
execSync(`git push origin --delete ${branchList}`, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} catch {
|
||||
console.warn('Batch failed, trying to delete branches individually...');
|
||||
for (const branch of batch) {
|
||||
try {
|
||||
execSync(`git push origin --delete ${branch}`, {
|
||||
stdio: 'pipe',
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const error = err as { stderr?: Buffer; message?: string };
|
||||
const stderr = error.stderr?.toString() || '';
|
||||
if (!stderr.includes('remote ref does not exist')) {
|
||||
console.error(
|
||||
`Failed to delete branch "${branch}":`,
|
||||
stderr.trim() || error.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Cleaning up local tracking branches...');
|
||||
try {
|
||||
execSync('git remote prune origin', { stdio: 'inherit' });
|
||||
} catch {
|
||||
console.error('Failed to prune local tracking branches.');
|
||||
}
|
||||
console.log('Cleanup complete.');
|
||||
} else {
|
||||
console.log('Operation cancelled.');
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user