mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
fix(shared): match nested gitignore semantics for unanchored patterns and escaped !
This commit is contained in:
committed by
Peter Steinberger
parent
c2e0ce7b68
commit
45395c2fd1
@@ -0,0 +1,97 @@
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import ignore from "ignore";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { addIgnoreRules } from "./ignore-rules.js";
|
||||
|
||||
function writeIgnoreTree(root: string, rules: string, dir = root) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(join(dir, ".gitignore"), rules);
|
||||
}
|
||||
|
||||
function buildMatcher(root: string, dir: string) {
|
||||
const ig = ignore();
|
||||
addIgnoreRules(ig, join(root, dir), root);
|
||||
return ig;
|
||||
}
|
||||
|
||||
describe("addIgnoreRules", () => {
|
||||
it("ignores nested slash-free patterns at any depth", () => {
|
||||
const root = join(tmpdir(), "ignore-rules-test-1");
|
||||
writeIgnoreTree(root, "*.log\n", join(root, "sub"));
|
||||
writeFileSync(join(root, "sub", "y.log"), "");
|
||||
mkdirSync(join(root, "sub", "deep"), { recursive: true });
|
||||
writeFileSync(join(root, "sub", "deep", "x.log"), "");
|
||||
|
||||
const ig = buildMatcher(root, "sub");
|
||||
|
||||
expect(ig.ignores("sub/y.log")).toBe(true);
|
||||
expect(ig.ignores("sub/deep/x.log")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps anchored patterns relative to the ignore file directory", () => {
|
||||
const root = join(tmpdir(), "ignore-rules-test-2");
|
||||
writeIgnoreTree(root, "dir/*.log\n", join(root, "sub"));
|
||||
mkdirSync(join(root, "sub", "dir"), { recursive: true });
|
||||
writeFileSync(join(root, "sub", "dir", "x.log"), "");
|
||||
mkdirSync(join(root, "sub", "other"), { recursive: true });
|
||||
writeFileSync(join(root, "sub", "other", "x.log"), "");
|
||||
|
||||
const ig = buildMatcher(root, "sub");
|
||||
|
||||
expect(ig.ignores("sub/dir/x.log")).toBe(true);
|
||||
expect(ig.ignores("sub/other/x.log")).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps leading-slash patterns relative to the ignore file directory", () => {
|
||||
const root = join(tmpdir(), "ignore-rules-test-3");
|
||||
writeIgnoreTree(root, "/dir/*.log\n", join(root, "sub"));
|
||||
mkdirSync(join(root, "sub", "dir"), { recursive: true });
|
||||
writeFileSync(join(root, "sub", "dir", "x.log"), "");
|
||||
|
||||
const ig = buildMatcher(root, "sub");
|
||||
|
||||
expect(ig.ignores("sub/dir/x.log")).toBe(true);
|
||||
});
|
||||
|
||||
it("treats trailing-slash directory patterns as unanchored", () => {
|
||||
const root = join(tmpdir(), "ignore-rules-test-4");
|
||||
writeIgnoreTree(root, "node_modules/\n", join(root, "sub"));
|
||||
mkdirSync(join(root, "sub", "node_modules"), { recursive: true });
|
||||
mkdirSync(join(root, "sub", "deep", "node_modules"), { recursive: true });
|
||||
|
||||
const ig = buildMatcher(root, "sub");
|
||||
|
||||
expect(ig.ignores("sub/node_modules/file.js")).toBe(true);
|
||||
expect(ig.ignores("sub/deep/node_modules/file.js")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not corrupt escaped ! patterns at the root", () => {
|
||||
const root = join(tmpdir(), "ignore-rules-test-5");
|
||||
writeIgnoreTree(root, "*.txt\n\\!keep.txt\n");
|
||||
writeFileSync(join(root, "keep.txt"), "");
|
||||
writeFileSync(join(root, "!keep.txt"), "");
|
||||
writeFileSync(join(root, "drop.txt"), "");
|
||||
|
||||
const ig = buildMatcher(root, "");
|
||||
|
||||
expect(ig.ignores("keep.txt")).toBe(true);
|
||||
expect(ig.ignores("!keep.txt")).toBe(true);
|
||||
expect(ig.ignores("drop.txt")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not corrupt escaped ! patterns in nested directories", () => {
|
||||
const root = join(tmpdir(), "ignore-rules-test-6");
|
||||
writeIgnoreTree(root, "*.txt\n\\!keep.txt\n", join(root, "sub"));
|
||||
writeFileSync(join(root, "sub", "keep.txt"), "");
|
||||
writeFileSync(join(root, "sub", "!keep.txt"), "");
|
||||
writeFileSync(join(root, "sub", "drop.txt"), "");
|
||||
|
||||
const ig = buildMatcher(root, "sub");
|
||||
|
||||
expect(ig.ignores("sub/keep.txt")).toBe(true);
|
||||
expect(ig.ignores("sub/!keep.txt")).toBe(true);
|
||||
expect(ig.ignores("sub/drop.txt")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -48,14 +48,27 @@ function prefixIgnorePattern(line: string, prefix: string): string | null {
|
||||
if (pattern.startsWith("!")) {
|
||||
negated = true;
|
||||
pattern = pattern.slice(1);
|
||||
} else if (pattern.startsWith("\\!")) {
|
||||
pattern = pattern.slice(1);
|
||||
}
|
||||
// Keep escaped "!" intact; the ignore library handles "\!" as a literal filename.
|
||||
|
||||
let anchored = false;
|
||||
if (pattern.startsWith("/")) {
|
||||
anchored = true;
|
||||
pattern = pattern.slice(1);
|
||||
}
|
||||
|
||||
const prefixed = prefix ? `${prefix}${pattern}` : pattern;
|
||||
const slashIndex = pattern.indexOf("/");
|
||||
if (slashIndex !== -1 && slashIndex !== pattern.length - 1) {
|
||||
anchored = true;
|
||||
}
|
||||
|
||||
// A pattern without a middle/beginning slash matches at any depth below the
|
||||
// ignore file's directory; prefix with **/ so the ignore library agrees.
|
||||
const needsDepthGlob = prefix && !anchored;
|
||||
const prefixed = prefix
|
||||
? needsDepthGlob
|
||||
? `${prefix}**/${pattern}`
|
||||
: `${prefix}${pattern}`
|
||||
: pattern;
|
||||
return negated ? `!${prefixed}` : prefixed;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user