Skip to content

Commit d3e599a

Browse files
authored
Merge pull request #7 from fbosch/fix/handle-include-exclude-changes
fix(rules): Detect include/exclude pattern changes
2 parents 24e4a38 + 63aaf2f commit d3e599a

6 files changed

Lines changed: 152 additions & 7 deletions

File tree

src/lock.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface DocsCacheLockSource {
88
bytes: number;
99
fileCount: number;
1010
manifestSha256: string;
11+
rulesSha256?: string;
1112
updatedAt: string;
1213
}
1314

@@ -79,6 +80,10 @@ export const validateLock = (input: unknown): DocsCacheLock => {
7980
value.manifestSha256,
8081
`sources.${key}.manifestSha256`,
8182
),
83+
rulesSha256:
84+
value.rulesSha256 === undefined
85+
? undefined
86+
: assertString(value.rulesSha256, `sources.${key}.rulesSha256`),
8287
updatedAt: assertString(value.updatedAt, `sources.${key}.updatedAt`),
8388
};
8489
}

src/sync.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createHash } from "node:crypto";
12
import { access, mkdir, readFile } from "node:fs/promises";
23
import path from "node:path";
34
import pc from "picocolors";
@@ -42,10 +43,12 @@ type SyncResult = {
4243
ref: string;
4344
resolvedCommit: string;
4445
lockCommit: string | null;
46+
lockRulesSha256?: string;
4547
status: "up-to-date" | "changed" | "missing";
4648
bytes?: number;
4749
fileCount?: number;
4850
manifestSha256?: string;
51+
rulesSha256?: string;
4952
};
5053

5154
const formatBytes = (value: number) => {
@@ -79,6 +82,29 @@ const hasDocs = async (cacheDir: string, sourceId: string) => {
7982
return await exists(path.join(sourceDir, MANIFEST_FILENAME));
8083
};
8184

85+
const normalizePatterns = (patterns?: string[]) => {
86+
if (!patterns || patterns.length === 0) {
87+
return [];
88+
}
89+
const normalized = patterns
90+
.map((pattern) => pattern.trim())
91+
.filter((pattern) => pattern.length > 0);
92+
return Array.from(new Set(normalized)).sort();
93+
};
94+
95+
const computeRulesHash = (params: {
96+
include: string[];
97+
exclude?: string[];
98+
}) => {
99+
const payload = {
100+
include: normalizePatterns(params.include),
101+
exclude: normalizePatterns(params.exclude),
102+
};
103+
const hash = createHash("sha256");
104+
hash.update(JSON.stringify(payload));
105+
return hash.digest("hex");
106+
};
107+
82108
export const getSyncPlan = async (
83109
options: SyncOptions,
84110
deps: SyncDeps = {},
@@ -108,6 +134,9 @@ export const getSyncPlan = async (
108134
const results: SyncResult[] = await Promise.all(
109135
filteredSources.map(async (source) => {
110136
const lockEntry = lockData?.sources?.[source.id];
137+
const include = source.include ?? defaults.include;
138+
const exclude = source.exclude;
139+
const rulesSha256 = computeRulesHash({ include, exclude });
111140
if (options.offline) {
112141
const docsPresent = await hasDocs(resolvedCacheDir, source.id);
113142
return {
@@ -116,10 +145,12 @@ export const getSyncPlan = async (
116145
ref: lockEntry?.ref ?? source.ref ?? defaults.ref,
117146
resolvedCommit: lockEntry?.resolvedCommit ?? "offline",
118147
lockCommit: lockEntry?.resolvedCommit ?? null,
148+
lockRulesSha256: lockEntry?.rulesSha256,
119149
status: lockEntry && docsPresent ? "up-to-date" : "missing",
120150
bytes: lockEntry?.bytes,
121151
fileCount: lockEntry?.fileCount,
122152
manifestSha256: lockEntry?.manifestSha256,
153+
rulesSha256,
123154
};
124155
}
125156
const resolved = await resolveCommit({
@@ -128,7 +159,9 @@ export const getSyncPlan = async (
128159
allowHosts: defaults.allowHosts,
129160
timeoutMs: options.timeoutMs,
130161
});
131-
const upToDate = lockEntry?.resolvedCommit === resolved.resolvedCommit;
162+
const upToDate =
163+
lockEntry?.resolvedCommit === resolved.resolvedCommit &&
164+
lockEntry?.rulesSha256 === rulesSha256;
132165
const status = lockEntry
133166
? upToDate
134167
? "up-to-date"
@@ -140,10 +173,12 @@ export const getSyncPlan = async (
140173
ref: resolved.ref,
141174
resolvedCommit: resolved.resolvedCommit,
142175
lockCommit: lockEntry?.resolvedCommit ?? null,
176+
lockRulesSha256: lockEntry?.rulesSha256,
143177
status,
144178
bytes: lockEntry?.bytes,
145179
fileCount: lockEntry?.fileCount,
146180
manifestSha256: lockEntry?.manifestSha256,
181+
rulesSha256,
147182
};
148183
}),
149184
);
@@ -199,6 +234,7 @@ const buildLock = async (
199234
fileCount: result.fileCount ?? prior?.fileCount ?? 0,
200235
manifestSha256:
201236
result.manifestSha256 ?? prior?.manifestSha256 ?? result.resolvedCommit,
237+
rulesSha256: result.rulesSha256 ?? prior?.rulesSha256,
202238
updatedAt: now,
203239
};
204240
}
@@ -467,6 +503,10 @@ export const printSyncPlan = (
467503
for (const result of plan.results) {
468504
const shortResolved = ui.hash(result.resolvedCommit);
469505
const shortLock = ui.hash(result.lockCommit);
506+
const rulesChanged =
507+
Boolean(result.lockRulesSha256) &&
508+
Boolean(result.rulesSha256) &&
509+
result.lockRulesSha256 !== result.rulesSha256;
470510

471511
if (result.status === "up-to-date") {
472512
ui.item(
@@ -477,6 +517,14 @@ export const printSyncPlan = (
477517
continue;
478518
}
479519
if (result.status === "changed") {
520+
if (result.lockCommit === result.resolvedCommit && rulesChanged) {
521+
ui.item(
522+
symbols.warn,
523+
result.id,
524+
`${pc.dim("rules changed")} ${pc.gray(shortResolved)}`,
525+
);
526+
continue;
527+
}
480528
ui.item(
481529
symbols.warn,
482530
result.id,

tests/fixtures/docs.lock

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
"vitest": {
77
"repo": "https://github.com/vitest-dev/vitest.git",
88
"ref": "main",
9-
"resolvedCommit": "0123456789abcdef0123456789abcdef01234567",
10-
"bytes": 123456,
11-
"fileCount": 512,
12-
"manifestSha256": "abcd",
13-
"updatedAt": "2026-01-30T12:00:00+01:00"
9+
"resolvedCommit": "0123456789abcdef0123456789abcdef01234567",
10+
"bytes": 123456,
11+
"fileCount": 512,
12+
"manifestSha256": "abcd",
13+
"rulesSha256": "efgh",
14+
"updatedAt": "2026-01-30T12:00:00+01:00"
1415
}
1516
}
1617
}

tests/lock.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ test("lock fixture is valid", async (t) => {
2727
const lock = module.validateLock(parsed);
2828
assert.equal(lock.version, 1);
2929
assert.ok(lock.sources.vitest);
30+
assert.equal(lock.sources.vitest.rulesSha256, "efgh");
3031
});
3132

3233
test("writeLock produces readable JSON", async (t) => {

tests/sync-include-exclude.test.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,81 @@ test("exclude overrides include on overlap", async () => {
131131
const docsRoot = path.join(cacheDir, "local");
132132
assert.equal(await exists(path.join(docsRoot, "docs", "guide.md")), false);
133133
});
134+
135+
test("sync re-materializes when include rules change", async () => {
136+
const tmpRoot = path.join(
137+
tmpdir(),
138+
`docs-cache-rules-${Date.now().toString(36)}`,
139+
);
140+
const cacheDir = path.join(tmpRoot, ".docs");
141+
const repoDir = path.join(tmpRoot, "repo");
142+
const configPath = path.join(tmpRoot, "docs.config.json");
143+
144+
await mkdir(path.join(repoDir, "docs"), { recursive: true });
145+
await writeFile(path.join(repoDir, "README.md"), "readme", "utf8");
146+
await writeFile(path.join(repoDir, "docs", "guide.md"), "guide", "utf8");
147+
148+
const baseConfig = {
149+
$schema:
150+
"https://raw.githubusercontent.com/fbosch/docs-cache/main/docs.config.schema.json",
151+
sources: [
152+
{
153+
id: "local",
154+
repo: "https://example.com/repo.git",
155+
include: ["docs/**"],
156+
},
157+
],
158+
};
159+
await writeFile(
160+
configPath,
161+
`${JSON.stringify(baseConfig, null, 2)}\n`,
162+
"utf8",
163+
);
164+
165+
const syncOptions = {
166+
configPath,
167+
cacheDirOverride: cacheDir,
168+
json: false,
169+
lockOnly: false,
170+
offline: false,
171+
failOnMiss: false,
172+
};
173+
const deps = {
174+
resolveRemoteCommit: async () => ({
175+
repo: "https://example.com/repo.git",
176+
ref: "HEAD",
177+
resolvedCommit: "abc123",
178+
}),
179+
fetchSource: async () => ({
180+
repoDir,
181+
cleanup: async () => undefined,
182+
}),
183+
};
184+
185+
await runSync(syncOptions, deps);
186+
187+
const docsRoot = path.join(cacheDir, "local");
188+
assert.equal(await exists(path.join(docsRoot, "README.md")), false);
189+
assert.equal(await exists(path.join(docsRoot, "docs", "guide.md")), true);
190+
191+
const updatedConfig = {
192+
...baseConfig,
193+
sources: [
194+
{
195+
id: "local",
196+
repo: "https://example.com/repo.git",
197+
include: ["README.md"],
198+
},
199+
],
200+
};
201+
await writeFile(
202+
configPath,
203+
`${JSON.stringify(updatedConfig, null, 2)}\n`,
204+
"utf8",
205+
);
206+
207+
await runSync(syncOptions, deps);
208+
209+
assert.equal(await exists(path.join(docsRoot, "README.md")), true);
210+
assert.equal(await exists(path.join(docsRoot, "docs", "guide.md")), false);
211+
});

tests/sync-output.test.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ test("printSyncPlan outputs summary and short hashes", () => {
2828
lockCommit: "cccccccccccccccccccccccccccccccccccccccc",
2929
status: "changed",
3030
},
31+
{
32+
id: "gamma",
33+
repo: "https://example.com/gamma.git",
34+
ref: "main",
35+
resolvedCommit: "dddddddddddddddddddddddddddddddddddddddd",
36+
lockCommit: "dddddddddddddddddddddddddddddddddddddddd",
37+
lockRulesSha256: "111111",
38+
rulesSha256: "222222",
39+
status: "changed",
40+
},
3141
],
3242
};
3343

@@ -44,9 +54,11 @@ test("printSyncPlan outputs summary and short hashes", () => {
4454
process.stdout.write = originalWrite;
4555
}
4656

47-
assert.ok(output.includes("2 sources"));
57+
assert.ok(output.includes("3 sources"));
4858
assert.ok(output.includes("alpha"));
4959
assert.ok(output.includes("beta"));
60+
assert.ok(output.includes("gamma"));
5061
assert.ok(output.includes("aaaaaaa"));
5162
assert.ok(output.includes("bbbbbbb"));
63+
assert.ok(output.includes("rules changed"));
5264
});

0 commit comments

Comments
 (0)