const { test } = require('node:test'); const assert = require('node:assert/strict'); const { execFileSync, spawnSync } = require('node:child_process'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const CLI = path.join(__dirname, '..', 'src', 'cli.js'); const TOKEN = 'stoke-secret-token-for-sync-tests'; const BASIC_CREDENTIAL = Buffer.from(`tester:${TOKEN}`).toString('base64'); const REAL_GIT = execFileSync('which', ['git'], { encoding: 'utf8' }).trim(); function git(args, cwd) { return execFileSync('git', args, { cwd, encoding: 'utf8' }).trim(); } function commit(directory, message, contents) { fs.writeFileSync(path.join(directory, 'content.txt'), `${contents}\n`); git(['add', 'content.txt'], directory); git(['-c', 'user.name=Tester', '-c', 'user.email=tester@example.com', 'commit', '-m', message], directory); return git(['rev-parse', 'HEAD'], directory); } function fixture() { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-sync-test-')); const forgeRoot = path.join(root, 'forge'); const forgeRepo = path.join(forgeRoot, 'o', 'r.git'); const seed = path.join(root, 'seed'); const upstreamWork = path.join(root, 'upstream-work'); const upstreamRepo = path.join(root, 'upstream.git'); fs.mkdirSync(path.dirname(forgeRepo), { recursive: true }); git(['init', '-b', 'main', seed], root); const oldSha = commit(seed, 'initial', 'initial'); git(['clone', '--bare', seed, forgeRepo], root); git(['clone', seed, upstreamWork], root); const newSha = commit(upstreamWork, 'upstream advance', 'advanced'); git(['clone', '--bare', upstreamWork, upstreamRepo], root); const config = path.join(root, 'config.json'); fs.writeFileSync(config, JSON.stringify({ url: `file://${forgeRoot}`, token: TOKEN, login: 'tester', })); return { root, forgeRepo, upstreamWork, upstreamRepo, config, oldSha, newSha, cleanup() { fs.rmSync(root, { recursive: true, force: true }); }, }; } function refSha(repository, ref) { const result = spawnSync('git', ['rev-parse', '--verify', ref], { cwd: repository, encoding: 'utf8', }); return result.status === 0 ? result.stdout.trim() : null; } function installGitWrapper(fx, body) { const wrapperDirectory = path.join(fx.root, 'bin'); const wrapper = path.join(wrapperDirectory, 'git'); fs.mkdirSync(wrapperDirectory); fs.writeFileSync(wrapper, `#!/bin/sh\n${body}\nexec "${REAL_GIT}" "$@"\n`); fs.chmodSync(wrapper, 0o755); return { PATH: `${wrapperDirectory}:${process.env.PATH}` }; } function runSync(fx, extra = [], { branch = 'main', env = {} } = {}) { const args = [ CLI, 'repo', 'sync', '-o', 'o', '-r', 'r', '--from', `file://${fx.upstreamRepo}`, ]; if (branch) args.push('--branch', branch); args.push(...extra); return spawnSync(process.execPath, args, { encoding: 'utf8', env: { ...process.env, STOKE_CONFIG_FILE: fx.config, ...env }, }); } test('repo sync fast-forwards an undiverged forge branch', () => { const fx = fixture(); try { const result = runSync(fx); assert.equal(result.status, 0, result.stderr); assert.equal(git(['rev-parse', 'refs/heads/main'], fx.forgeRepo), fx.newSha); assert.match(result.stdout, new RegExp(`main ${fx.oldSha}\\.\\.${fx.newSha}`)); } finally { fx.cleanup(); } }); test('repo sync resolves an omitted branch from the forge symbolic HEAD', () => { const fx = fixture(); try { const result = runSync(fx, [], { branch: null }); assert.equal(result.status, 0, result.stderr); assert.equal(git(['rev-parse', 'refs/heads/main'], fx.forgeRepo), fx.newSha); assert.match(result.stdout, new RegExp(`main ${fx.oldSha}\\.\\.${fx.newSha}`)); } finally { fx.cleanup(); } }); test('repo sync reports an already-current branch as a no-op', () => { const fx = fixture(); try { assert.equal(runSync(fx).status, 0); const result = runSync(fx); assert.equal(result.status, 0, result.stderr); assert.equal(git(['rev-parse', 'refs/heads/main'], fx.forgeRepo), fx.newSha); assert.match(result.stdout, new RegExp(`main is up to date at ${fx.newSha}`)); } finally { fx.cleanup(); } }); test('repo sync refuses a diverged forge branch without changing it', () => { const fx = fixture(); try { const forgeWork = path.join(fx.root, 'forge-work'); git(['clone', fx.forgeRepo, forgeWork], fx.root); const forgeSha = commit(forgeWork, 'forge-only change', 'forge-only'); git(['push', 'origin', 'main'], forgeWork); const result = runSync(fx); assert.equal(result.status, 1); assert.match(result.stderr, new RegExp(forgeSha)); assert.match(result.stderr, new RegExp(fx.newSha)); assert.match(result.stderr, /Diverged trees are out of scope/); assert.equal(git(['rev-parse', 'refs/heads/main'], fx.forgeRepo), forgeSha); } finally { fx.cleanup(); } }); test('repo sync --tags creates new tags but skips a moved upstream tag', () => { const fx = fixture(); try { git(['update-ref', 'refs/tags/stable', fx.oldSha], fx.forgeRepo); git(['update-ref', 'refs/tags/moved', fx.oldSha], fx.forgeRepo); git(['update-ref', 'refs/tags/stable', fx.oldSha], fx.upstreamRepo); git(['update-ref', 'refs/tags/moved', fx.newSha], fx.upstreamRepo); git(['update-ref', 'refs/tags/new-tag', fx.newSha], fx.upstreamRepo); const result = runSync(fx, ['--tags']); assert.equal(result.status, 1); assert.equal(git(['rev-parse', 'refs/heads/main'], fx.forgeRepo), fx.newSha); assert.equal(refSha(fx.forgeRepo, 'refs/tags/stable'), fx.oldSha); assert.equal(refSha(fx.forgeRepo, 'refs/tags/moved'), fx.oldSha); assert.equal(refSha(fx.forgeRepo, 'refs/tags/new-tag'), fx.newSha); assert.match(result.stderr, new RegExp(`moved.*${fx.oldSha}.*${fx.newSha}`)); } finally { fx.cleanup(); } }); test('repo sync --dry-run reports branch and tag moves without writing', () => { const fx = fixture(); try { git(['update-ref', 'refs/tags/new-tag', fx.newSha], fx.upstreamRepo); const result = runSync(fx, ['--tags', '--dry-run']); assert.equal(result.status, 0, result.stderr); assert.equal(git(['rev-parse', 'refs/heads/main'], fx.forgeRepo), fx.oldSha); assert.equal(refSha(fx.forgeRepo, 'refs/tags/new-tag'), null); assert.match(result.stdout, new RegExp(`main ${fx.oldSha}\\.\\.${fx.newSha}`)); assert.match(result.stdout, new RegExp(`new-tag .*${fx.newSha}`)); } finally { fx.cleanup(); } }); function runSourceTagRace({ dryRun }) { const fx = fixture(); git(['update-ref', 'refs/tags/race-tag', fx.oldSha], fx.upstreamRepo); const env = installGitWrapper(fx, ` case "$*" in *"refs/tags/race-tag:refs/stoke/upstream-tags/race-tag"*) "${REAL_GIT}" --git-dir="$STOKE_TEST_UPSTREAM_REPO" update-ref refs/tags/race-tag "$STOKE_TEST_NEW_SHA" ;; esac`); Object.assign(env, { STOKE_TEST_UPSTREAM_REPO: fx.upstreamRepo, STOKE_TEST_NEW_SHA: fx.newSha, }); const options = ['--tags']; if (dryRun) options.push('--dry-run'); return { fx, result: runSync(fx, options, { env }) }; } test('repo sync reports the fetched tag object when the source tag moves', () => { const { fx, result } = runSourceTagRace({ dryRun: false }); try { assert.equal(result.status, 0, result.stderr); assert.equal(refSha(fx.forgeRepo, 'refs/tags/race-tag'), fx.newSha); const tagLine = result.stdout.split('\n').find((line) => line.startsWith('tag race-tag')); assert.match(tagLine, new RegExp(fx.newSha)); assert.ok(!tagLine.includes(fx.oldSha)); } finally { fx.cleanup(); } }); test('repo sync --dry-run reports the fetched tag object when the source tag moves', () => { const { fx, result } = runSourceTagRace({ dryRun: true }); try { assert.equal(result.status, 0, result.stderr); assert.equal(refSha(fx.forgeRepo, 'refs/tags/race-tag'), null); const tagLine = result.stdout.split('\n').find((line) => line.startsWith('tag race-tag')); assert.match(tagLine, new RegExp(fx.newSha)); assert.ok(!tagLine.includes(fx.oldSha)); } finally { fx.cleanup(); } }); test('repo sync reclassifies a destination tag created during the push as moved', () => { const fx = fixture(); try { git(['update-ref', 'refs/tags/race-tag', fx.newSha], fx.upstreamRepo); const env = installGitWrapper(fx, ` case "$*" in *"refs/stoke/upstream-tags/race-tag:refs/tags/race-tag"*) "${REAL_GIT}" --git-dir="$STOKE_TEST_FORGE_REPO" update-ref refs/tags/race-tag "$STOKE_TEST_OLD_SHA" ;; esac`); Object.assign(env, { STOKE_TEST_FORGE_REPO: fx.forgeRepo, STOKE_TEST_OLD_SHA: fx.oldSha, }); const result = runSync(fx, ['--tags'], { env }); assert.equal(result.status, 1); assert.equal(git(['rev-parse', 'refs/heads/main'], fx.forgeRepo), fx.newSha); assert.equal(refSha(fx.forgeRepo, 'refs/tags/race-tag'), fx.oldSha); assert.match(result.stderr, new RegExp(`race-tag.*${fx.oldSha}.*${fx.newSha}`)); } finally { fx.cleanup(); } }); test('repo sync reports a destination branch that diverges during the push', () => { const fx = fixture(); try { const forgeWork = path.join(fx.root, 'forge-race-work'); git(['clone', fx.forgeRepo, forgeWork], fx.root); const racingSha = commit(forgeWork, 'racing forge change', 'racing-forge'); git(['push', 'origin', 'HEAD:refs/race/forge-only'], forgeWork); const env = installGitWrapper(fx, ` case "$*" in *"refs/stoke/upstream-branch:refs/heads/main"*) "${REAL_GIT}" --git-dir="$STOKE_TEST_FORGE_REPO" update-ref refs/heads/main "$STOKE_TEST_RACING_SHA" ;; esac`); Object.assign(env, { STOKE_TEST_FORGE_REPO: fx.forgeRepo, STOKE_TEST_RACING_SHA: racingSha, }); const result = runSync(fx, [], { env }); assert.equal(result.status, 1); assert.equal(git(['rev-parse', 'refs/heads/main'], fx.forgeRepo), racingSha); assert.match(result.stderr, new RegExp(racingSha)); assert.match(result.stderr, new RegExp(fx.newSha)); assert.match(result.stderr, /Diverged trees are out of scope/); } finally { fx.cleanup(); } }); test('repo sync keeps the token out of Git argv, output, remotes, and config', () => { const fx = fixture(); try { const argvLog = path.join(fx.root, 'git-argv.log'); const localConfigLog = path.join(fx.root, 'git-local-config.log'); const env = installGitWrapper(fx, ` printf '%s\\n' "$@" >> "$STOKE_TEST_GIT_ARGV" if [ -f "$PWD/config" ]; then sed -n '1,240p' "$PWD/config" >> "$STOKE_TEST_LOCAL_CONFIG" "${REAL_GIT}" config --local --get-regexp '^remote\\..*\\.url$' >> "$STOKE_TEST_LOCAL_CONFIG" 2>/dev/null || true fi`); Object.assign(env, { STOKE_TEST_GIT_ARGV: argvLog, STOKE_TEST_LOCAL_CONFIG: localConfigLog, }); const result = runSync(fx, [], { env }); assert.equal(result.status, 0, result.stderr); for (const text of [ result.stdout, result.stderr, fs.readFileSync(argvLog, 'utf8'), fs.readFileSync(localConfigLog, 'utf8'), fs.readFileSync(path.join(fx.forgeRepo, 'config'), 'utf8'), fs.readFileSync(path.join(fx.upstreamRepo, 'config'), 'utf8'), ]) { assert.ok(!text.includes(TOKEN), 'token leaked from the environment-only auth path'); assert.ok(!text.includes(BASIC_CREDENTIAL), 'encoded credential leaked from the environment-only auth path'); } } finally { fx.cleanup(); } });