Download scripts/cleanup-branches.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 5.28 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/scripts/cleanup-branches.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/scripts/cleanup-branches.ts
-
curl -L -o cleanup-branches.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/scripts/cleanup-branches.ts
5.28 kB
| /** | |
| * @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); | |
| }); | |