mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-14 19:41:16 +00:00
cli: add configurable ignore rules and deployment artifacts
IgnoreRules (src/apps/cli/serviceModules/IgnoreRules.ts): - Reads .livesync/ignore for user-defined glob patterns - Applies gitignore matchBase semantics: patterns without / get **/ prefix, patterns ending with / get ** appended for directory contents - Supports `import: .gitignore` directive to merge gitignore patterns - Rejects negation patterns with a warning (not fully supportable) - Integrated into both daemon and mirror commands via isTargetFile handler Wiring: - IgnoreRules loaded before LiveSyncBaseCore construction so beginWatch() receives rules when it fires during onLoad/onFirstInitialise - Passed through initialiseServiceModulesCLI -> StorageEventManagerCLI -> CLIStorageEventManagerAdapter -> CLIWatchAdapter Deployment: - src/apps/cli/deploy/livesync-cli.service - systemd unit template - src/apps/cli/deploy/install.sh - user/system install script Testing: - src/apps/cli/test/test-daemon-linux.sh - e2e tests for ignore rules - src/apps/cli/serviceModules/IgnoreRules.unit.spec.ts - 15 unit tests - src/apps/cli/commands/daemonCommand.unit.spec.ts - 7 unit tests
This commit is contained in:
129
src/apps/cli/serviceModules/IgnoreRules.ts
Normal file
129
src/apps/cli/serviceModules/IgnoreRules.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import * as fs from "fs/promises";
|
||||
import * as path from "path";
|
||||
|
||||
import { minimatch } from "minimatch";
|
||||
|
||||
/**
|
||||
* Loads and evaluates ignore rules from `.livesync/ignore` inside the vault.
|
||||
*
|
||||
* File format:
|
||||
* - Lines starting with `#` are comments.
|
||||
* - Blank lines are ignored.
|
||||
* - `import: .gitignore` (exactly) — merges patterns from the vault's `.gitignore`.
|
||||
* - All other lines are minimatch glob patterns relative to the vault root.
|
||||
*
|
||||
* Negation patterns (lines starting with `!`) are not supported. Loading a
|
||||
* ruleset containing them throws an error — use separate include/exclude files
|
||||
* instead.
|
||||
*
|
||||
* Missing files (`.livesync/ignore` or `.gitignore`) are silently skipped.
|
||||
*/
|
||||
export class IgnoreRules {
|
||||
private patterns: string[] = [];
|
||||
|
||||
constructor(private vaultPath: string) {}
|
||||
|
||||
/**
|
||||
* Reads `.livesync/ignore` (and optionally `.gitignore`) and populates the
|
||||
* pattern list. Safe to call multiple times — each call replaces the
|
||||
* previous state. Does not throw if files are absent.
|
||||
*
|
||||
* @throws if any pattern line begins with `!` (negation is unsupported).
|
||||
*/
|
||||
async load(): Promise<void> {
|
||||
this.patterns = [];
|
||||
const ignorePath = path.join(this.vaultPath, ".livesync", "ignore");
|
||||
let rawLines: string[];
|
||||
try {
|
||||
const content = await fs.readFile(ignorePath, "utf-8");
|
||||
rawLines = content.split(/\r?\n/);
|
||||
} catch {
|
||||
// File absent or unreadable — treat as empty ruleset.
|
||||
return;
|
||||
}
|
||||
|
||||
for (const line of rawLines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
// NOTE: Only the exact string "import: .gitignore" is recognised.
|
||||
// Any future generalisation of this directive must validate that
|
||||
// the resolved path stays within the vault directory.
|
||||
if (trimmed === "import: .gitignore") {
|
||||
await this._importGitignore();
|
||||
continue;
|
||||
}
|
||||
if (trimmed.startsWith("import:")) {
|
||||
console.error(`[IgnoreRules] Warning: unrecognised directive '${trimmed}' — only 'import: .gitignore' is supported`);
|
||||
continue;
|
||||
}
|
||||
this._addPattern(trimmed);
|
||||
}
|
||||
if (this.patterns.length > 0) {
|
||||
console.error(`[IgnoreRules] Loaded ${this.patterns.length} ignore patterns`);
|
||||
}
|
||||
}
|
||||
|
||||
// Normalises a single gitignore-style pattern:
|
||||
// - Patterns ending with `/` (directory patterns like `build/`) are
|
||||
// converted to `build/**` so they match all files inside that directory.
|
||||
// - Patterns without a `/` are prefixed with `**/` to give them matchBase
|
||||
// semantics (e.g. `*.tmp` → `**/*.tmp`), matching the basename in any
|
||||
// subdirectory as gitignore does.
|
||||
// - Patterns that already contain a `/` (but don't end with one) are
|
||||
// path-specific and used as-is.
|
||||
private _normalisePattern(pattern: string): string {
|
||||
if (pattern.endsWith("/")) {
|
||||
return "**/" + pattern + "**";
|
||||
} else if (!pattern.includes("/")) {
|
||||
return "**/" + pattern;
|
||||
}
|
||||
return pattern;
|
||||
}
|
||||
|
||||
private async _importGitignore(): Promise<void> {
|
||||
const gitignorePath = path.join(this.vaultPath, ".gitignore");
|
||||
let content: string;
|
||||
try {
|
||||
content = await fs.readFile(gitignorePath, "utf-8");
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
this._parseLines(content);
|
||||
}
|
||||
|
||||
private _parseLines(content: string): void {
|
||||
for (const line of content.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||
this._addPattern(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
private _addPattern(raw: string): void {
|
||||
if (raw.startsWith("!")) {
|
||||
throw new Error(
|
||||
`[IgnoreRules] Negation pattern '${raw}' is not supported. ` +
|
||||
`Remove it from .livesync/ignore or use a separate include/exclude file.`
|
||||
);
|
||||
}
|
||||
this.patterns.push(this._normalisePattern(raw));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the given vault-relative path matches any loaded
|
||||
* ignore pattern.
|
||||
*
|
||||
* @param relativePath - Path relative to the vault root, using forward
|
||||
* slashes or the OS separator.
|
||||
*/
|
||||
shouldIgnore(relativePath: string): boolean {
|
||||
if (this.patterns.length === 0) {
|
||||
return false;
|
||||
}
|
||||
// Normalise to forward slashes for minimatch.
|
||||
const normalised = relativePath.replace(/\\/g, "/");
|
||||
return this.patterns.some((p) => minimatch(normalised, p, { dot: true }));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user