UNPKG

gmsc

Version:

Git Multi-repo Sync Command - Manage multiple git repositories in parallel

232 lines (197 loc) 6.59 kB
#!/usr/bin/env node import { Command } from 'commander'; import chalk from 'chalk'; import ora from 'ora'; import pLimit from 'p-limit'; import fs from 'fs/promises'; import path from 'path'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); const program = new Command(); const CONFIG_FILE = '.gms.json'; async function loadConfig() { try { const configPath = path.join(process.cwd(), CONFIG_FILE); const data = await fs.readFile(configPath, 'utf8'); return JSON.parse(data); } catch (error) { return null; } } async function saveConfig(config) { const configPath = path.join(process.cwd(), CONFIG_FILE); await fs.writeFile(configPath, JSON.stringify(config, null, 2)); } async function getSubmodules() { const config = await loadConfig(); if (!config) { console.error(chalk.red('Not a gms repository. Run "gms init" first.')); process.exit(1); } return config.submodules; } async function isGitRepo(dir) { try { const gitPath = path.join(dir, '.git'); const stats = await fs.stat(gitPath); // Handle both regular .git directories and .git files (submodules/worktrees) if (stats.isDirectory()) { return true; } else if (stats.isFile()) { // Check if it's a gitdir file const content = await fs.readFile(gitPath, 'utf8'); return content.startsWith('gitdir:'); } return false; } catch { return false; } } async function executeInRepo(repoPath, command, showOutput = false) { try { const { stdout, stderr } = await execAsync(command, { cwd: repoPath }); return { success: true, stdout, stderr, repoPath }; } catch (error) { return { success: false, error: error.message, repoPath }; } } async function executeInParallel(command, showOutput = false) { const submodules = await getSubmodules(); const limit = pLimit(5); // Limit to 5 concurrent operations const spinner = ora(`Running "${command}" in ${submodules.length} repositories...`).start(); const promises = submodules.map(submodule => limit(() => executeInRepo(submodule.path, command, showOutput)) ); const results = await Promise.all(promises); spinner.stop(); // Display results console.log(chalk.bold(`\nResults for: ${command}\n`)); results.forEach(result => { const repoName = path.basename(result.repoPath); if (result.success) { console.log(chalk.green(`✓ ${repoName}`)); if (showOutput && result.stdout) { console.log(chalk.gray(` ${result.stdout.trim().replace(/\n/g, '\n ')}`)); } } else { console.log(chalk.red(`✗ ${repoName}`)); console.log(chalk.red(` Error: ${result.error}`)); } }); const successCount = results.filter(r => r.success).length; console.log(chalk.bold(`\n${successCount}/${results.length} repositories succeeded`)); } program .name('gms') .description('Git Multi-repo Sync - Manage multiple git repositories in parallel') .version('1.0.0'); program .command('init') .description('Initialize gms in the current directory') .action(async () => { const spinner = ora('Initializing gms...').start(); try { // Check if already initialized const existingConfig = await loadConfig(); if (existingConfig) { spinner.fail('Already initialized'); process.exit(1); } // Find all subdirectories that are git repos const entries = await fs.readdir(process.cwd(), { withFileTypes: true }); const submodules = []; for (const entry of entries) { if (entry.isDirectory()) { const dirPath = path.join(process.cwd(), entry.name); if (await isGitRepo(dirPath)) { submodules.push({ name: entry.name, path: dirPath }); } } } if (submodules.length === 0) { spinner.fail('No git repositories found in subdirectories'); process.exit(1); } // Save config await saveConfig({ submodules }); spinner.succeed(`Initialized with ${submodules.length} repositories`); console.log(chalk.gray('\nTracking:')); submodules.forEach(sm => console.log(chalk.gray(` - ${sm.name}`))); } catch (error) { spinner.fail(`Failed to initialize: ${error.message}`); process.exit(1); } }); program .command('status') .description('Show git status for all repositories') .action(async () => { await executeInParallel('git status --short', true); }); program .command('branch') .description('Show current branch for all repositories') .action(async () => { await executeInParallel('git branch --show-current', true); }); program .command('checkout <branch>') .description('Checkout branch in all repositories') .action(async (branch) => { await executeInParallel(`git checkout ${branch}`, true); }); program .command('pull') .description('Pull in all repositories') .action(async () => { await executeInParallel('git pull', true); }); program .command('push') .description('Push in all repositories') .argument('[remote]', 'Remote name', 'origin') .argument('[branch]', 'Branch name') .action(async (remote, branch) => { const command = branch ? `git push ${remote} ${branch}` : `git push ${remote}`; await executeInParallel(command, true); }); program .command('fetch') .description('Fetch in all repositories') .action(async () => { await executeInParallel('git fetch', true); }); program .command('remote') .description('Show remotes for all repositories') .option('-v, --verbose', 'Show remote URLs') .action(async (options) => { const command = options.verbose ? 'git remote -v' : 'git remote'; await executeInParallel(command, true); }); program .command('exec <command>') .description('Execute any git command in all repositories') .action(async (command) => { await executeInParallel(`git ${command}`, true); }); program .command('list') .alias('ls') .description('List all tracked repositories') .action(async () => { const config = await loadConfig(); if (!config) { console.error(chalk.red('Not a gms repository. Run "gms init" first.')); process.exit(1); } console.log(chalk.bold('Tracked repositories:\n')); config.submodules.forEach(sm => { console.log(` ${chalk.green('•')} ${sm.name} ${chalk.gray(`(${sm.path})`)}`); }); }); program.parse();