diff --git a/gitnexus/src/core/git-staleness.ts b/gitnexus/src/core/git-staleness.ts index 603198c8a..919732e39 100644 --- a/gitnexus/src/core/git-staleness.ts +++ b/gitnexus/src/core/git-staleness.ts @@ -123,11 +123,15 @@ export async function checkCwdMatch(cwd: string): Promise { const cwdHead = getCurrentCommit(cwdGitRoot) || undefined; const drift = commitsAheadOfIndexed(cwdGitRoot, sibling.lastCommit); - let hint: string; + // Same commit on both clones → still report match=sibling-by-remote + // (the relationship is real and useful to callers like list_repos / + // future tooling) but leave `hint` unset: there's nothing to warn + // about, and `maybeWarnSiblingDrift` already short-circuits this + // case independently. Surfacing a no-op hint would force callers + // to second-guess whether they need to display it. + let hint: string | undefined; if (cwdHead && cwdHead === sibling.lastCommit) { - hint = - `ℹ️ Indexed clone "${sibling.name}" lives at ${sibling.path}; ` + - `your cwd (${cwdGitRoot}) is a different clone of the same repo at the same commit — results apply.`; + hint = undefined; } else if (drift && drift > 0) { hint = `⚠️ Index for "${sibling.name}" was built at ${sibling.path}; ` + diff --git a/gitnexus/src/storage/git.ts b/gitnexus/src/storage/git.ts index 5dc0f5d7b..6ad362c22 100644 --- a/gitnexus/src/storage/git.ts +++ b/gitnexus/src/storage/git.ts @@ -62,6 +62,12 @@ export const getRemoteUrl = (repoPath: string): string | undefined => { // Lower-case the host segment of `scheme://[user@]host[:port]/...` // and the host segment of `git@host:owner/repo` SCP form. + // SSH user-segment regex deliberately accepts the common + // `git@`/`-_@` cases. Less common usernames (e.g. with + // dots) fall through to the URL-form branch — they will simply + // not get host-case normalisation, which is acceptable: the raw + // `git config` output is still a valid fingerprint, just slightly + // less collapsible across host casings. const sshMatch = normalised.match(/^(git@|[a-zA-Z0-9_-]+@)([^:/]+)(:.+)$/); if (sshMatch) { normalised = `${sshMatch[1]}${sshMatch[2].toLowerCase()}${sshMatch[3]}`; diff --git a/gitnexus/test/unit/sibling-clone-drift.test.ts b/gitnexus/test/unit/sibling-clone-drift.test.ts index a5c71f7c2..b858b3440 100644 --- a/gitnexus/test/unit/sibling-clone-drift.test.ts +++ b/gitnexus/test/unit/sibling-clone-drift.test.ts @@ -36,11 +36,6 @@ const initRepoWithCommit = (dir: string, remoteUrl?: string): string => { return execSync('git rev-parse HEAD', { cwd: dir }).toString().trim(); }; -const advanceCommit = (dir: string): string => { - execSync('git commit --allow-empty -q -m advance', { cwd: dir }); - return execSync('git rev-parse HEAD', { cwd: dir }).toString().trim(); -}; - describe('registry persists remoteUrl', () => { let tmpHome: Awaited>; let tmpRepo: Awaited>; @@ -178,19 +173,22 @@ describe('checkCwdMatch', () => { } }); - it('detects sibling-by-remote at the same commit (no drift hint)', async () => { + it('detects sibling-by-remote when sibling HEAD differs from indexed commit', async () => { const indexed = await createTempDir('cwd-indexed-'); const sibling = await createTempDir('cwd-sibling-'); try { const remote = 'https://example.com/foo/bar'; const indexedHead = initRepoWithCommit(indexed.dbPath, remote); - // Sibling is a separate `git init` with the same remote URL and - // an identical-message empty commit — that's enough for the - // remote-URL-based fingerprint to match. Commits differ; we use - // the same lastCommit for the registered side anyway because - // the warning logic only cares about the *registered* commit - // vs the cwd HEAD. - initRepoWithCommit(sibling.dbPath, remote); + // Sibling is a separate `git init` with the same remote URL — + // that's enough for the remote-URL-based fingerprint to match. + // Use a distinct commit message so the sibling's SHA cannot + // coincidentally collide with the indexed one even when both + // commits land in the same second. + execSync('git init -q', { cwd: sibling.dbPath }); + execSync('git config user.email test@example.com', { cwd: sibling.dbPath }); + execSync('git config user.name test', { cwd: sibling.dbPath }); + execSync('git commit --allow-empty -q -m sibling-distinct', { cwd: sibling.dbPath }); + execSync(`git remote add origin ${remote}`, { cwd: sibling.dbPath }); await registerRepo(indexed.dbPath, { repoPath: indexed.dbPath, @@ -247,13 +245,15 @@ describe('checkCwdMatch', () => { // Use a fabricated indexed commit that doesn't exist in the // sibling clone — git rev-list will fail and `drift` is left // undefined. The hint must still flag this as a stale-or-divergent - // sibling clone. - const fakeIndexedCommit = '0000000000000000000000000000000000000000'; + // sibling clone. Named to make test intent obvious; not git's + // all-zero "null" OID, which has special semantics in some git + // commands. + const FAKE_INDEXED_COMMIT = 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; initRepoWithCommit(sibling.dbPath, remote); await registerRepo(indexed.dbPath, { repoPath: indexed.dbPath, - lastCommit: fakeIndexedCommit, + lastCommit: FAKE_INDEXED_COMMIT, indexedAt: new Date().toISOString(), remoteUrl: remote, }); @@ -261,11 +261,45 @@ describe('checkCwdMatch', () => { const m = await checkCwdMatch(sibling.dbPath); expect(m.match).toBe('sibling-by-remote'); expect(m.cwdHead).toBeTruthy(); - expect(m.cwdHead).not.toBe(fakeIndexedCommit); + expect(m.cwdHead).not.toBe(FAKE_INDEXED_COMMIT); expect(m.hint).toMatch(/sibling clone/); } finally { await indexed.cleanup(); await sibling.cleanup(); } }); + + it('omits hint when sibling cwd HEAD matches the indexed commit (no drift)', async () => { + // Same-commit sibling: the relationship is real (and surfaces in + // `match: 'sibling-by-remote'`) but there is nothing to warn + // about. `LocalBackend.maybeWarnSiblingDrift` short-circuits in + // exactly this case, so confirming `hint` is unset here pins the + // contract those two pieces of code rely on. + const indexed = await createTempDir('cwd-same-indexed-'); + const sibling = await createTempDir('cwd-same-sibling-'); + try { + const remote = 'https://example.com/foo/bar'; + initRepoWithCommit(indexed.dbPath, remote); + const siblingHead = initRepoWithCommit(sibling.dbPath, remote); + + // Register the indexed entry with the SIBLING's HEAD as + // `lastCommit`. That is the on-disk reality when both clones + // happen to be at the same commit hash — e.g. immediately + // after both fast-forwarded to the same `main`. + await registerRepo(indexed.dbPath, { + repoPath: indexed.dbPath, + lastCommit: siblingHead, + indexedAt: new Date().toISOString(), + remoteUrl: remote, + }); + + const m = await checkCwdMatch(sibling.dbPath); + expect(m.match).toBe('sibling-by-remote'); + expect(m.cwdHead).toBe(siblingHead); + expect(m.hint).toBeUndefined(); + } finally { + await indexed.cleanup(); + await sibling.cleanup(); + } + }); });