mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-28 06:23:01 +00:00
Three verified-open defects, one family: sync silently destroying or diverging on content it should preserve. #2404 (P0) — 'ops' was hardcoded in PRUNE_DIR_NAMES (a v0.2.0-era carve-out), so any path with an ops segment was 'pruned-dir': committed ops/*.md never imported, and modified ops/* files hit the unsyncableModified delete loop (whose #1433 guard only spared 'metafile'), silently deleting put-created pages like the bundled daily-task-manager's canonical ops/tasks on every sync. Fix: remove 'ops' from the prune list (ordinary user content; the vendor/generated entries stay), and harden the delete loop to also skip 'pruned-dir' — a page under a pruned dir can only exist via a deliberate put_page. #2426 (P0) — write-through content stayed DB-only and was deleted by sync --full. All three compounding bugs fixed: 1. writePageThrough now best-effort commits the artifact (path-limited git commit) on durability-hardened repos, so the post-commit hook can push it; result carries committed?: boolean. 2. scripts/brain-commit-push.sh stages+commits BEFORE any pull — the old fetch+pull-rebase-first order aborted on any dirty tree, so the helper could never commit a MODIFIED page; brain_push's rebase-on-reject already handles an advanced remote. 3. The full-sync delete-reconcile partitions stale pages by git history (listEverCommittedPaths): never-committed source_paths are DB-only write-through — pages are KEPT and re-exported to the working tree instead of soft-deleted. Builds on the #2828 mass-delete valve (covers the below-valve cases). #2607 — the sync --full git ls-files fast path bypassed pruneDir, so a full pass imported (and resurrected soft-deleted) pages under dot-dirs and vendored trees that incremental sync excludes. Fix: isCollectibleForWalker applies the same segment-level pruneDir gate as classifySync, so full and incremental enumeration agree. One regression test per defect (all verified failing against master src): test/sync-ops-pages.serial.test.ts, test/write-through-commit.serial.test.ts, the #2426 helper-order test in test/brain-durability-hook.serial.test.ts, test/sync-reconcile-db-only.serial.test.ts, test/import-git-fastpath-prune.test.ts. Fixes #2404 Fixes #2426 Fixes #2607 Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
91 lines
4.1 KiB
TypeScript
91 lines
4.1 KiB
TypeScript
/**
|
|
* #2607 — the `sync --full` git fast path applies the same prune gate as
|
|
* incremental sync.
|
|
*
|
|
* Bug class: `collectSyncableFiles` on a git work tree takes the
|
|
* `git ls-files` fast path, which historically filtered ONLY by
|
|
* strategy/extension + .gitignore — no `pruneDir`, so `sync --full`
|
|
* imported (and resurrected previously-soft-deleted) pages under dot-dirs
|
|
* and vendored trees that incremental sync's `isSyncable` excludes. The two
|
|
* enumeration modes cycled content in and out depending on which ran last.
|
|
*
|
|
* Fix: `isCollectibleForWalker` (shared by the git fast path AND the FS-walk
|
|
* emit filter) now rejects any path with a segment `pruneDir` would block —
|
|
* the same segment rule `classifySync` applies on the incremental path.
|
|
*
|
|
* No PGLite needed: `collectSyncableFiles` is pure filesystem + git.
|
|
*/
|
|
|
|
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'fs';
|
|
import { execSync } from 'child_process';
|
|
import { tmpdir } from 'os';
|
|
import { join, relative } from 'path';
|
|
import { collectSyncableFiles } from '../src/commands/import.ts';
|
|
import { isSyncable } from '../src/core/sync.ts';
|
|
|
|
let repo: string;
|
|
|
|
function rel(files: string[]): string[] {
|
|
return files.map((f) => relative(repo, f));
|
|
}
|
|
|
|
beforeAll(() => {
|
|
repo = mkdtempSync(join(tmpdir(), 'gbrain-fastpath-'));
|
|
execSync('git init', { cwd: repo, stdio: 'pipe' });
|
|
execSync('git config user.email "t@t.t"', { cwd: repo, stdio: 'pipe' });
|
|
execSync('git config user.name "T"', { cwd: repo, stdio: 'pipe' });
|
|
|
|
// Ordinary content — must be collected.
|
|
mkdirSync(join(repo, 'notes'), { recursive: true });
|
|
writeFileSync(join(repo, 'notes/real.md'), '---\ntitle: Real\n---\nbody\n');
|
|
mkdirSync(join(repo, 'ops'), { recursive: true });
|
|
writeFileSync(join(repo, 'ops/tasks.md'), '---\ntitle: Tasks\n---\nbody\n');
|
|
|
|
// TRACKED files under excluded trees — `git ls-files` returns these, so
|
|
// only the prune gate keeps them out (this is the #2607 divergence).
|
|
mkdirSync(join(repo, '.obsidian'), { recursive: true });
|
|
writeFileSync(join(repo, '.obsidian/plugin-notes.md'), 'not a page\n');
|
|
mkdirSync(join(repo, 'vendor/pkg'), { recursive: true });
|
|
writeFileSync(join(repo, 'vendor/pkg/notes.md'), 'vendored\n');
|
|
mkdirSync(join(repo, 'node_modules/dep'), { recursive: true });
|
|
writeFileSync(join(repo, 'node_modules/dep/CHANGELOG.md'), 'dep changelog\n');
|
|
mkdirSync(join(repo, 'people/pedro.raw'), { recursive: true });
|
|
writeFileSync(join(repo, 'people/pedro.raw/source.md'), 'raw sidecar\n');
|
|
|
|
// Metafiles — excluded on both routes (pre-existing #345 behavior).
|
|
writeFileSync(join(repo, 'README.md'), '# repo\n');
|
|
writeFileSync(join(repo, 'notes/index.md'), '# index\n');
|
|
|
|
execSync('git add -A -f && git commit -m "fixture"', { cwd: repo, stdio: 'pipe' });
|
|
});
|
|
|
|
afterAll(() => {
|
|
if (repo) rmSync(repo, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('#2607 — git fast path excludes what incremental sync excludes', () => {
|
|
test('tracked files under pruned dirs are NOT collected', () => {
|
|
const files = rel(collectSyncableFiles(repo, { strategy: 'markdown' }));
|
|
expect(files).toContain('notes/real.md');
|
|
expect(files).toContain('ops/tasks.md'); // ordinary content (#2404)
|
|
expect(files).not.toContain('.obsidian/plugin-notes.md');
|
|
expect(files).not.toContain('vendor/pkg/notes.md');
|
|
expect(files).not.toContain('node_modules/dep/CHANGELOG.md');
|
|
expect(files).not.toContain('people/pedro.raw/source.md');
|
|
// Metafiles stay excluded too.
|
|
expect(files).not.toContain('README.md');
|
|
expect(files).not.toContain('notes/index.md');
|
|
});
|
|
|
|
test('full-sync enumeration agrees with incremental isSyncable for every collected file', () => {
|
|
// The single-source-of-truth contract: nothing the full path collects may
|
|
// be something the incremental path would refuse to sync.
|
|
const files = rel(collectSyncableFiles(repo, { strategy: 'markdown' }));
|
|
for (const f of files) {
|
|
expect({ path: f, syncable: isSyncable(f) }).toEqual({ path: f, syncable: true });
|
|
}
|
|
expect(files.length).toBeGreaterThan(0);
|
|
});
|
|
});
|