fix(qmd): skip git-ignored paths in repository collections
The daemon passes `git ls-files --others --ignored --directory` as escaped ignore globs to addCollection. QMD's YAML write-through drops `ignore` and index.yml wins on every re-sync, so the list is also written into index.yml and re-synced whenever it changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
abface2293
commit
691ad6a69f
4 files changed
+102
-9
No files matched your search
@@ -5,7 +5,7 @@ import { execFileSync } from 'node:child_process';
|
||||
import { mkdtempSync, rmSync, utimesSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { createServer, gitFingerprint, keepModelsLoaded } from '../../shared/qmd/qmd-daemon.mjs';
|
||||
import { createServer, gitFingerprint, ignoreGlobs, keepModelsLoaded } from '../../shared/qmd/qmd-daemon.mjs';
|
||||
import { collectionName } from '../../shared/qmd/qmd-lib.mjs';
|
||||
|
||||
const defaultHits = () => [{ displayPath: `${collectionName('/r')}/src/a.ts`, body: 'x\nretry here', bestChunkPos: 2, bestChunk: 'retry here', score: 0.91 }];
|
||||
@@ -30,7 +30,8 @@ async function start(t, overrides = {}, { updateGate, hits } = {}) {
|
||||
const daemon = createServer({
|
||||
store, token: 't0k', idleMs: 1000, now: () => clock, onIdle: () => idle.push(clock),
|
||||
extractSnippet: () => ({ line: 2, snippet: 'retry here' }),
|
||||
git: () => 'fingerprint-1', excludeCollection: async () => {}, recordCollection: () => {}, ...overrides,
|
||||
git: () => 'fingerprint-1', excludeCollection: async () => {}, recordCollection: () => {},
|
||||
ignored: () => [], persistIgnore: async () => {}, ...overrides,
|
||||
});
|
||||
await new Promise((done) => daemon.server.listen(0, '127.0.0.1', done));
|
||||
t.after(() => { daemon.server.closeAllConnections(); daemon.server.close(); });
|
||||
@@ -174,3 +175,29 @@ test('keepModelsLoaded disables QMD unloading models after 5 idle minutes', () =
|
||||
keepModelsLoaded({ internal: { llm } });
|
||||
assert.deepEqual(llm, { inactivityTimeoutMs: 0, disposeModelsOnInactivity: false });
|
||||
});
|
||||
|
||||
test('ignoreGlobs turns git ignored paths into escaped QMD ignore globs', () => {
|
||||
const escape = (path) => path.replace(/[()[\]]/g, '\\$&');
|
||||
assert.deepEqual(ignoreGlobs('dist/\0a b.ts\0out (1)/\0x[1].md\0', escape), ['dist/**', 'a b.ts', 'out \\(1\\)/**', 'x\\[1\\].md']);
|
||||
assert.deepEqual(ignoreGlobs('', escape), []);
|
||||
});
|
||||
|
||||
test('git ignored paths reach addCollection and are persisted only when they change', async (t) => {
|
||||
let ignore = ['dist/**'];
|
||||
let fingerprint = 'one';
|
||||
const persisted = [];
|
||||
const s = await start(t, { ignored: () => ignore, git: () => fingerprint, persistIgnore: async (name, list) => persisted.push([name, list]) });
|
||||
await s.call('/search', { repo: '/r', query: 'q' });
|
||||
assert.deepEqual(s.store.calls.find((c) => c[0] === 'add')[2].ignore, ['dist/**']);
|
||||
assert.deepEqual(persisted, [[collectionName('/r'), ['dist/**']]]);
|
||||
const order = s.store.calls.map((c) => c[0]);
|
||||
assert.ok(order.indexOf('add') < order.indexOf('update'));
|
||||
fingerprint = 'two';
|
||||
await s.call('/search', { repo: '/r', query: 'q' });
|
||||
assert.equal(persisted.length, 1, 'an unchanged list is not written again');
|
||||
ignore = ['dist/**', 'tmp/**'];
|
||||
fingerprint = 'three';
|
||||
await s.call('/search', { repo: '/r', query: 'q' });
|
||||
assert.deepEqual(persisted[1], [collectionName('/r'), ['dist/**', 'tmp/**']]);
|
||||
assert.equal(s.store.calls.filter((c) => c[0] === 'update').length, 3);
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import * as YAML from './fixtures/yaml-shim.mjs';
|
||||
import { collectionName, deviceEnv, idleMinutes, qmdConfigPath, writePrivateJson, writeTextAtomic, readJson, runQmdCli, MODELS } from '../../shared/qmd/qmd-lib.mjs';
|
||||
import { setModels, unsetModels, previousModels, restoreModels } from '../../shared/qmd/qmd-config.mjs';
|
||||
import { setModels, unsetModels, previousModels, restoreModels, setCollectionIgnore } from '../../shared/qmd/qmd-config.mjs';
|
||||
|
||||
test('collection name is stable, prefixed, and path-safe', () => {
|
||||
const a = collectionName('/tmp/My Repo/ü');
|
||||
@@ -102,3 +102,14 @@ test('writeTextAtomic replaces the file through a temp file in the same director
|
||||
assert.equal(readFileSync(file, 'utf8'), 'b: 2\n');
|
||||
assert.deepEqual(readdirSync(dir), ['index.yml']);
|
||||
});
|
||||
|
||||
test('setCollectionIgnore writes the ignore list of one collection and keeps the rest', () => {
|
||||
const original = '# mine\ncollections:\n notes:\n path: /n # keep\n repo-aaaaaaaaaaaa:\n path: /r\n includeByDefault: false\n';
|
||||
const withIgnore = setCollectionIgnore(original, YAML, 'repo-aaaaaaaaaaaa', ['dist/**', 'a.ts']);
|
||||
assert.match(withIgnore, /# mine/);
|
||||
assert.match(withIgnore, /path: \/n # keep/);
|
||||
assert.deepEqual(YAML.default.parse(withIgnore).collections['repo-aaaaaaaaaaaa'], { path: '/r', includeByDefault: false, ignore: ['dist/**', 'a.ts'] });
|
||||
const cleared = setCollectionIgnore(withIgnore, YAML, 'repo-aaaaaaaaaaaa', []);
|
||||
assert.deepEqual(YAML.default.parse(cleared).collections['repo-aaaaaaaaaaaa'], { path: '/r', includeByDefault: false });
|
||||
assert.equal(setCollectionIgnore(original, YAML, 'repo-bbbbbbbbbbbb', ['x']), null, 'an unknown collection is not created');
|
||||
});
|
||||
Reference in new issue
Block a user