SPB Git

spb/spbgit Public MIT

SPB Git — the platform hosting itself

JavaScript 73.9% CSS 11.7% Nunjucks 11.6% Shell 2.7%
3.9 KB · 98 lines javascript
Raw Blame History
1/**2 * ─────────────────────────────────────────────3 *  SPB Git — Personal Git Platform4 * ─────────────────────────────────────────────5 *  Author  : Simon-Pierre Boucher6 *  Contact : contact@spboucher.ai7 *  File    : cli/commands/sync.mjs8 *  Purpose : `spbgit sync` — the killer command: commit+rebase+push all9 *  License : MIT © Simon-Pierre Boucher10 * ─────────────────────────────────────────────11 */1213import { basename } from 'node:path';14import pc from 'picocolors';15import { requireCliConfig } from '../lib/config.mjs';16import { workspaceRepos, git, repoStatus } from '../lib/git.mjs';17import { printTable, mapLimit } from '../lib/ui.mjs';18import { EXIT } from '../lib/api.mjs';1920function defaultMessage() {21  const now = new Date();22  const pad = (n) => String(n).padStart(2, '0');23  return `chore: sync ${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;24}2526/**27 * Sync one repository. Never force-pushes.28 * @returns {Promise<{repo: string, state: 'synced'|'pushed'|'conflict'|'error', pushedCount: number, note: string}>}29 */30async function syncRepo(dir, message, token) {31  const repo = basename(dir);32  const before = await repoStatus(dir);3334  if (before.dirty > 0) {35    await git(dir, ['add', '-A']);36    const commit = await git(dir, ['commit', '-m', message]);37    if (!commit.ok) {38      return { repo, state: 'error', pushedCount: 0, note: commit.stderr.trim().split('\n').pop() ?? 'commit failed' };39    }40  }4142  if (before.hasUpstream) {43    const pull = await git(dir, ['pull', '--rebase'], { token });44    if (!pull.ok) {45      await git(dir, ['rebase', '--abort']);46      return {47        repo,48        state: 'conflict',49        pushedCount: 0,50        note: 'rebase conflict — resolve manually: cd ' + dir + ' && git pull --rebase',51      };52    }53  }5455  const after = await repoStatus(dir);56  const toPush = after.hasUpstream ? after.ahead : 1;57  if (after.hasUpstream && after.ahead === 0) {58    return { repo, state: 'synced', pushedCount: 0, note: '' };59  }60  const pushArgs = after.hasUpstream ? ['push'] : ['push', '-u', 'origin', after.branch];61  const push = await git(dir, pushArgs, { token });62  if (!push.ok) {63    return { repo, state: 'error', pushedCount: 0, note: push.stderr.trim().split('\n').pop() ?? 'push failed' };64  }65  return { repo, state: 'pushed', pushedCount: toPush, note: '' };66}6768export function registerSync(program) {69  program70    .command('sync')71    .description('for every workspace repo: add-all, commit, pull --rebase, push')72    .option('-m, --message <msg>', 'commit message', defaultMessage())73    .action(async (opts) => {74      const config = requireCliConfig();75      const dirs = workspaceRepos(config.workspace);76      if (dirs.length === 0) {77        console.log(pc.dim(`Workspace ${config.workspace} has no repositories. Try: spbgit clone --all`));78        return;79      }80      console.log(pc.dim(`Syncing ${dirs.length} repositories…\n`));81      const results = await mapLimit(dirs, 4, (dir) => syncRepo(dir, opts.message, config.token));8283      const rows = [[pc.bold('REPO'), pc.bold('RESULT'), pc.bold('NOTE')]];84      for (const r of results) {85        const cell =86          r.state === 'synced' ? pc.green('✓ synced')87          : r.state === 'pushed' ? pc.cyan(`↑ pushed ${r.pushedCount}`)88          : r.state === 'conflict' ? pc.red('✗ conflict')89          : pc.red('✗ error');90        rows.push([r.repo, cell, pc.dim(r.note)]);91      }92      console.log('');93      printTable(rows);94      const bad = results.filter((r) => r.state === 'conflict' || r.state === 'error');95      if (bad.length > 0) process.exit(EXIT.PARTIAL);96    });97}98