diff --git a/gitnexus/src/core/shared-store-analyze.ts b/gitnexus/src/core/shared-store-analyze.ts index ed33888ac..a831ccab0 100644 --- a/gitnexus/src/core/shared-store-analyze.ts +++ b/gitnexus/src/core/shared-store-analyze.ts @@ -20,7 +20,12 @@ import { existsSync, constants as fsConstants } from 'fs'; import fs from 'fs/promises'; import path from 'path'; import { acquireIndexLock, requireExclusiveIndexLock } from '../storage/index-lock.js'; -import { commitDistanceToHead, getRemoteUrl, isWorkingTreePristine } from '../storage/git.js'; +import { + commitDistanceToHead, + getRemoteUrl, + hasGitDir, + isWorkingTreePristine, +} from '../storage/git.js'; import { canonicalizePath, findRegistryEntryByRepoPath, @@ -534,12 +539,21 @@ const siblingCloneStore = ( * `--share-with`, the one its registry entry already points into, or a * sibling clone's store (#3352). `--share-with` requires the normalized remote * URL to match the member it names (R3); `analyze --no-share` records an - * opt-out that stops automatic joining. + * opt-out that stops automatic joining. Only tree roots participate, as in + * `resolveSharedStore`: `getRemoteUrl` answers from any subdirectory, so + * without this gate `analyze --skip-git /pkg` would join (#3374). */ export const resolveOptedInStore = async ( repoPath: string, shareWith: string | undefined, ): Promise => { + if (!hasGitDir(repoPath)) { + if (!shareWith) return undefined; + throw new Error( + `--share-with: "${repoPath}" is not the root of a git checkout. ` + + 'Only a clone root can share an index store.', + ); + } const entries = await readRegistry(); if (shareWith) { let target; diff --git a/gitnexus/test/integration/shared-store-clone-optin.test.ts b/gitnexus/test/integration/shared-store-clone-optin.test.ts index 150510b61..97b045ce8 100644 --- a/gitnexus/test/integration/shared-store-clone-optin.test.ts +++ b/gitnexus/test/integration/shared-store-clone-optin.test.ts @@ -309,4 +309,26 @@ describe('shared store founder key for concurrent sibling clones (#3374)', () => const joiner = await cloneAndRegister('aaa-joiner'); expect((await resolveOptedInStore(joiner, undefined))?.key).toBe(existing); }); + + // #3374 S8 — `getRemoteUrl` answers from any subdirectory, so only the + // tree-root gate keeps `analyze --skip-git /pkg` out of the store. + it('a subdirectory of a clone with a registered sibling neither joins nor founds a store', async () => { + await cloneAndRegister('member', cloneStoreKey(path.join(root, 'zz-founder'))); + const clone = await cloneAndRegister('other'); + const subdir = path.join(clone, 'pkg'); + await fs.mkdir(subdir); + + expect(await resolveOptedInStore(subdir, undefined)).toBeUndefined(); + expect(await resolveOptedInStore(clone, undefined)).toBeDefined(); + }); + + it('--share-with refuses a subdirectory of a clone', async () => { + const member = await cloneAndRegister('member', cloneStoreKey(path.join(root, 'zz-founder'))); + const subdir = path.join(await cloneAndRegister('other'), 'pkg'); + await fs.mkdir(subdir); + + await expect(resolveOptedInStore(subdir, member)).rejects.toThrow( + /--share-with: .* is not the root of a git checkout/, + ); + }); });