diff --git a/src/commands/init.ts b/src/commands/init.ts index 9f082a46..8e08fffd 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -81,12 +81,30 @@ export function ensureGitignoreEntries(cwd: string, entries: string[]): void { /** * Configure git to push notes automatically with regular pushes. - * Adds refs/notes/* to existing push refspecs, preserving any user-configured refspecs. + * Adds refs/notes/* only when custom push refspecs already exist. + * This avoids overriding git's default push behavior in repos that + * rely on implicit push semantics (no remote.origin.push configured). * Idempotent - safe to call multiple times. */ -export function configureNotesPush(cwd: string): void { +export type NotesPushConfigStatus = + | 'configured' + | 'already-configured' + | 'skipped-no-custom-push' + | 'skipped-no-origin'; + +export function configureNotesPush(cwd: string): { status: NotesPushConfigStatus } { let existingRefspecs: string[] = []; + try { + execFileSync('git', ['remote', 'get-url', 'origin'], { + cwd, + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + }); + } catch { + return { status: 'skipped-no-origin' }; + } + try { // Get existing push refspecs const existing = execFileSync('git', ['config', '--local', '--get-all', 'remote.origin.push'], { @@ -101,18 +119,16 @@ export function configureNotesPush(cwd: string): void { // Already has notes configured - nothing to do if (existingRefspecs.some((ref) => ref.includes('refs/notes'))) { - return; + return { status: 'already-configured' }; } } catch { - // Config doesn't exist yet, proceed to set it + // Config doesn't exist yet } - // If no existing refspecs, add heads first + // Without explicit push refspecs, adding one would override default + // git push behavior (often current/upstream branch). if (existingRefspecs.length === 0) { - execFileSync('git', ['config', '--local', 'remote.origin.push', '+refs/heads/*:refs/heads/*'], { - cwd, - stdio: ['pipe', 'pipe', 'pipe'], - }); + return { status: 'skipped-no-custom-push' }; } // Add notes refspec (preserves existing refspecs) @@ -120,6 +136,8 @@ export function configureNotesPush(cwd: string): void { cwd, stdio: ['pipe', 'pipe', 'pipe'], }); + + return { status: 'configured' }; } /** @@ -295,8 +313,17 @@ export async function initCommand(options: IInitCommandOptions, logger?: ILogger } // Configure git to push notes automatically with regular pushes - configureNotesPush(cwd); - console.log('✓ Configured git to push notes with commits'); + const notesPush = configureNotesPush(cwd); + if (notesPush.status === 'configured') { + console.log('✓ Configured git to push notes with commits'); + } else if (notesPush.status === 'already-configured') { + console.log('✓ Notes push already configured (skipped)'); + } else if (notesPush.status === 'skipped-no-custom-push') { + console.log('i Skipped notes push auto-config to preserve default git push behavior'); + console.log(' Use `git-mem sync --push` to publish notes, or add a custom remote.origin.push refspec.'); + } else { + console.log('i No origin remote found; skipped notes push configuration'); + } } // ── MCP config (skip if already exists) ──────────────────────── diff --git a/tests/unit/commands/init.test.ts b/tests/unit/commands/init.test.ts index fa95a114..ead3d690 100644 --- a/tests/unit/commands/init.test.ts +++ b/tests/unit/commands/init.test.ts @@ -7,16 +7,36 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { mkdtempSync, writeFileSync, readFileSync } from 'fs'; +import { mkdtempSync, writeFileSync, readFileSync, mkdirSync } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; import { rmSync } from 'fs'; +import { execFileSync } from 'child_process'; import { ensureGitignoreEntries, + configureNotesPush, readEnvApiKey, ensureEnvPlaceholder, } from '../../../src/commands/init'; +function git(args: string[], cwd: string): string { + return execFileSync('git', args, { + cwd, + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + }).trim(); +} + +function getPushRefspecs(cwd: string): string[] { + try { + const output = git(['config', '--local', '--get-all', 'remote.origin.push'], cwd); + if (!output) return []; + return output.split('\n').filter(Boolean); + } catch { + return []; + } +} + // ── ensureGitignoreEntries ─────────────────────────────────────────── describe('ensureGitignoreEntries', () => { @@ -271,3 +291,82 @@ describe('ensureEnvPlaceholder', () => { } }); }); + +// ── configureNotesPush ──────────────────────────────────────────────── + +describe('configureNotesPush', () => { + it('should skip when origin remote is not configured', () => { + const dir = mkdtempSync(join(tmpdir(), 'git-mem-notes-push-')); + try { + git(['init'], dir); + const result = configureNotesPush(dir); + assert.equal(result.status, 'skipped-no-origin'); + assert.deepEqual(getPushRefspecs(dir), []); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it('should skip when no custom remote.origin.push refspec exists', () => { + const base = mkdtempSync(join(tmpdir(), 'git-mem-notes-push-')); + const dir = join(base, 'repo'); + const remote = join(base, 'remote.git'); + try { + mkdirSync(dir, { recursive: true }); + git(['init'], dir); + git(['init', '--bare', remote], base); + git(['remote', 'add', 'origin', remote], dir); + + const result = configureNotesPush(dir); + assert.equal(result.status, 'skipped-no-custom-push'); + assert.deepEqual(getPushRefspecs(dir), []); + } finally { + rmSync(base, { recursive: true, force: true }); + } + }); + + it('should append notes refspec when custom push refspec exists', () => { + const base = mkdtempSync(join(tmpdir(), 'git-mem-notes-push-')); + const dir = join(base, 'repo'); + const remote = join(base, 'remote.git'); + try { + mkdirSync(dir, { recursive: true }); + git(['init'], dir); + git(['init', '--bare', remote], base); + git(['remote', 'add', 'origin', remote], dir); + git(['config', '--local', 'remote.origin.push', 'refs/heads/main:refs/heads/main'], dir); + + const result = configureNotesPush(dir); + assert.equal(result.status, 'configured'); + + const refspecs = getPushRefspecs(dir); + assert.ok(refspecs.includes('refs/heads/main:refs/heads/main')); + assert.ok(refspecs.includes('+refs/notes/*:refs/notes/*')); + } finally { + rmSync(base, { recursive: true, force: true }); + } + }); + + it('should be idempotent when notes refspec already exists', () => { + const base = mkdtempSync(join(tmpdir(), 'git-mem-notes-push-')); + const dir = join(base, 'repo'); + const remote = join(base, 'remote.git'); + try { + mkdirSync(dir, { recursive: true }); + git(['init'], dir); + git(['init', '--bare', remote], base); + git(['remote', 'add', 'origin', remote], dir); + git(['config', '--local', '--add', 'remote.origin.push', 'refs/heads/main:refs/heads/main'], dir); + git(['config', '--local', '--add', 'remote.origin.push', '+refs/notes/*:refs/notes/*'], dir); + + const result = configureNotesPush(dir); + assert.equal(result.status, 'already-configured'); + + const refspecs = getPushRefspecs(dir); + const notesRefs = refspecs.filter((ref) => ref === '+refs/notes/*:refs/notes/*'); + assert.equal(notesRefs.length, 1); + } finally { + rmSync(base, { recursive: true, force: true }); + } + }); +});