fix(sync): skip git pull when repo has no origin remote

`gbrain sync` ran `git pull` unconditionally and printed scary stderr
on every cycle for brains that have no `origin` remote (local-only
workflows, single-machine setups, brains initialized via `gbrain init
--pglite` against an arbitrary directory). The pull failed harmlessly
but the noise was confusing and made operators think sync was broken.

`hasOriginRemote()` probes `git remote get-url origin` with stdio
ignored; on failure (`no such remote`), skip the pull, print a single
informational line, and proceed with the local working tree.

Cherry-picked from PR #1119.

Co-Authored-By: hnshah <hnshah@users.noreply.github.com>
This commit is contained in:
Garry Tan
2026-05-18 13:33:16 -07:00
co-authored by hnshah
parent b9d8258ef7
commit 4192a4a365
2 changed files with 39 additions and 1 deletions
+19 -1
View File
@@ -230,6 +230,19 @@ function git(repoPath: string, args: string[], configs: string[] = []): string {
}).trim();
}
function hasOriginRemote(repoPath: string): boolean {
try {
execFileSync('git', buildGitInvocation(repoPath, ['remote', 'get-url', 'origin']), {
encoding: 'utf-8',
timeout: 30000,
stdio: ['ignore', 'ignore', 'ignore'],
});
return true;
} catch {
return false;
}
}
function isDetachedHead(repoPath: string): boolean {
try {
git(repoPath, ['symbolic-ref', '--quiet', 'HEAD']);
@@ -450,7 +463,12 @@ async function performSyncInner(engine: BrainEngine, opts: SyncOpts): Promise<Sy
// hardening that cloneRepo applies. Route through pullRepo from
// git-remote.ts so the flag set is consistent across initial clone and
// ongoing pulls — single source of truth for the defensive flags.
if (!opts.noPull && !detachedHead) {
const originRemotePresent = !opts.noPull && !detachedHead ? hasOriginRemote(repoPath) : false;
if (!opts.noPull && !detachedHead && !originRemotePresent) {
console.error(`No origin remote on ${repoPath}; skipping git pull. Syncing from local working tree.`);
}
if (!opts.noPull && !detachedHead && originRemotePresent) {
const _t0 = Date.now();
console.error(`[gbrain phase] sync.git_pull start`);
try {
+20
View File
@@ -347,6 +347,26 @@ describe('performSync dry-run never writes', () => {
expect(await engine.getConfig('sync.repo_path')).toBeNull();
});
test('first sync without origin skips git pull noise and uses local working tree', async () => {
const { performSync } = await import('../src/commands/sync.ts');
const messages: string[] = [];
const originalError = console.error;
console.error = (...args: unknown[]) => { messages.push(args.map(String).join(' ')); };
try {
const result = await performSync(engine, {
repoPath,
noEmbed: true,
});
expect(result.status).toBe('first_sync');
} finally {
console.error = originalError;
}
expect(messages.some(m => m.includes('No origin remote') && m.includes('skipping git pull'))).toBe(true);
expect(messages.some(m => m.includes('sync.git_pull start'))).toBe(false);
expect(messages.some(m => m.includes('git pull failed'))).toBe(false);
});
test('incremental dry-run does NOT write to DB or advance the bookmark', async () => {
const { performSync } = await import('../src/commands/sync.ts');
// First do a real sync to seed the bookmark.