feat: add fast-forward repo sync #41

Merged
andres merged 5 commits from build/23-repo-sync into main 2026-08-31 18:42:40 +00:00
3 changed files with 48 additions and 9 deletions
Showing only changes of commit ea6c1a4fe9 - Show all commits

View file

@ -467,8 +467,6 @@ repo
if (!config || !config.url || !config.token) {
throw new Error('Not authenticated. Run: stoke auth login');
}
if (!options.branch) throw new Error('--branch is required');
const base = config.url.replace(/\/+$/, '');
const forgeUrl = `${base}/${encodeURIComponent(options.owner)}/${encodeURIComponent(options.repo)}.git`;
const result = syncRepository({
@ -477,7 +475,11 @@ repo
branch: options.branch,
env: gitAuthEnv({ ...config, url: base }),
});
console.log(`${result.branch} ${result.oldSha}..${result.newSha}`);
if (result.changed) {
console.log(`${result.branch} ${result.oldSha}..${result.newSha}`);
} else {
console.log(`${result.branch} is up to date at ${result.newSha}`);
}
} catch (err) {
console.error(`Repository sync failed: ${err.message}`);
process.exit(1);

View file

@ -19,6 +19,15 @@ function syncRepository({ forgeUrl, upstreamUrl, branch, env }) {
try {
runGit(['init', '--bare', directory], { cwd: directory, env });
if (!branch) {
const symbolicHead = runGit(['ls-remote', '--symref', forgeUrl, 'HEAD'], {
cwd: directory,
env,
}).stdout;
const match = symbolicHead.match(/^ref:\s+refs\/heads\/(.+)\s+HEAD$/m);
if (!match) throw new Error('Could not resolve the forge repository default branch');
branch = match[1];
}
runGit(['fetch', '--no-tags', forgeUrl, `refs/heads/${branch}:${forgeRef}`], {
cwd: directory,
env,
@ -45,7 +54,7 @@ function syncRepository({ forgeUrl, upstreamUrl, branch, env }) {
env,
});
}
return { branch, oldSha, newSha };
return { branch, oldSha, newSha, changed: oldSha !== newSha };
} finally {
fs.rmSync(directory, { recursive: true, force: true });
}

View file

@ -55,17 +55,18 @@ function fixture() {
};
}
function runSync(fx, extra = []) {
return spawnSync(process.execPath, [
function runSync(fx, extra = [], { branch = 'main' } = {}) {
const args = [
CLI,
'repo',
'sync',
'-o', 'o',
'-r', 'r',
'--from', `file://${fx.upstreamRepo}`,
'--branch', 'main',
...extra,
], {
];
if (branch) args.push('--branch', branch);
args.push(...extra);
return spawnSync(process.execPath, args, {
encoding: 'utf8',
env: { ...process.env, STOKE_CONFIG_FILE: fx.config },
});
@ -83,3 +84,30 @@ test('repo sync fast-forwards an undiverged forge branch', () => {
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();
}
});