Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -158,5 +158,56 @@ describe("generateStarterIgnoreFile", () => {
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
expect(lines).toHaveLength(0);
});

it("treats anchored .gitignore patterns as covered by defaults", () => {
writeFileSync(join(testDir, ".gitignore"), "/node_modules\n/dist\n.env\n");
const content = generateStarterIgnoreFile(testDir);
// Extract the pattern lines under the .gitignore section (between its
// header and the next "# ---" section header).
const lines = content.split("\n");
const headerIdx = lines.findIndex((l) => l.includes("From .gitignore"));
// Guard: if the section were fully deduped, findIndex returns -1 and the
// slice below would silently scan from the top of the file (masking a
// regression). Assert the section is actually present before slicing.
expect(headerIdx).toBeGreaterThanOrEqual(0);
const nextSectionIdx = lines.findIndex((l, i) => i > headerIdx && l.startsWith("# ---"));
const sectionLines = lines.slice(headerIdx + 1, nextSectionIdx === -1 ? undefined : nextSectionIdx);
const patterns = sectionLines
.filter((l) => l.startsWith("# ") && !l.startsWith("# ---"))
.map((l) => l.slice(2));
// /node_modules and /dist are anchored forms of defaults node_modules/ and dist/.
expect(patterns).not.toContain("/node_modules"); // fails today: '# /node_modules' is present
expect(patterns).not.toContain("/dist"); // fails today: '# /dist' is present
expect(patterns).toContain(".env"); // still suggested
});

it("treats **/ and ./ anchor variants as covered by defaults", () => {
writeFileSync(
join(testDir, ".gitignore"),
"**/node_modules\n**/build/\n./dist/\n.env\n",
);
const content = generateStarterIgnoreFile(testDir);
const lines = content.split("\n");
const headerIdx = lines.findIndex((l) => l.includes("From .gitignore"));
expect(headerIdx).toBeGreaterThanOrEqual(0);
const nextSectionIdx = lines.findIndex((l, i) => i > headerIdx && l.startsWith("# ---"));
const sectionLines = lines.slice(headerIdx + 1, nextSectionIdx === -1 ? undefined : nextSectionIdx);
const patterns = sectionLines
.filter((l) => l.startsWith("# ") && !l.startsWith("# ---"))
.map((l) => l.slice(2));
// **/node_modules, **/build/, and ./dist/ are anchor variants of defaults.
expect(patterns).not.toContain("**/node_modules");
expect(patterns).not.toContain("**/build/");
expect(patterns).not.toContain("./dist/");
// A non-default pattern still survives.
expect(patterns).toContain(".env");
});

it("does not suggest .gitignore negation (!) patterns", () => {
writeFileSync(join(testDir, ".gitignore"), "build/\n!build/keep.txt\n.env\n");
const content = generateStarterIgnoreFile(testDir);
expect(content).not.toContain("!build/keep.txt"); // fails today: '# !build/keep.txt' is emitted
expect(content).toContain("# .env");
});
});
});
21 changes: 18 additions & 3 deletions understand-anything-plugin/packages/core/src/ignore-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,37 @@ const GENERIC_SUGGESTIONS = [

/**
* Parses a .gitignore file and returns active patterns (no comments, no blanks).
*
* Negation (`!`) lines are intentionally dropped: a `!pattern` force-INCLUDES a
* path and is only meaningful relative to the project's own .gitignore ignore
* stack, which understand-anything does not apply. Surfacing it under the
* "uncomment to exclude" suggestion section would invert its semantics, so it is
* omitted rather than presented as a candidate exclusion. The header still
* documents the general `!` mechanism for users who want to author one by hand.
*/
function parseGitignorePatterns(gitignorePath: string): string[] {
if (!existsSync(gitignorePath)) return [];
const content = readFileSync(gitignorePath, "utf-8");
return content
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0 && !line.startsWith("#"));
.filter(
(line) => line.length > 0 && !line.startsWith("#") && !line.startsWith("!"),
);
}

/**
* Returns true if a gitignore pattern is already covered by the hardcoded defaults.
* Normalizes trailing slashes for comparison.
* Normalizes leading anchors (root "/", "./", and globstar) and trailing slashes
* for comparison so that anchored forms like `/node_modules`, `./dist/`, and a
* leading globstar `node_modules` match the defaults `node_modules/`, `dist/`.
*/
function isCoveredByDefaults(pattern: string): boolean {
const normalize = (p: string) => p.replace(/\/+$/, "");
const normalize = (p: string) =>
p
.replace(/^\.?\/+/, "")
.replace(/^\*\*\/+/, "")
.replace(/\/+$/, "");
const normalized = normalize(pattern);
return DEFAULT_IGNORE_PATTERNS.some((d) => normalize(d) === normalized);
}
Expand Down
Loading