mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
feat: seed .understandignore with .gitignore patterns on first generation
Read the project's .gitignore at starter-file generation time and include non-default patterns as commented suggestions in .understandignore. Patterns already covered by hardcoded defaults are deduplicated (with trailing-slash normalization). This is a one-time inclusion — users can later remove patterns for files they want analyzed without the filter re-reading .gitignore. Also fixes the starter header listing bin/ as a built-in default when it is intentionally excluded from DEFAULT_IGNORE_PATTERNS (bin/ is used by Node/Ruby CLI launchers). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
bc311c80ad
commit
6ef1a21315
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { generateStarterIgnoreFile } from "../ignore-generator";
|
||||
import { mkdirSync, rmSync } from "node:fs";
|
||||
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
@@ -94,4 +94,69 @@ describe("generateStarterIgnoreFile", () => {
|
||||
expect(content).not.toContain("# .storybook/");
|
||||
expect(content).not.toContain("# fixtures/");
|
||||
});
|
||||
|
||||
describe(".gitignore integration", () => {
|
||||
it("includes .gitignore patterns not covered by defaults", () => {
|
||||
writeFileSync(join(testDir, ".gitignore"), ".env\nsecrets/\n*.pyc\n");
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("From .gitignore");
|
||||
expect(content).toContain("# .env");
|
||||
expect(content).toContain("# secrets/");
|
||||
expect(content).toContain("# *.pyc");
|
||||
});
|
||||
|
||||
it("excludes .gitignore patterns already in defaults", () => {
|
||||
writeFileSync(join(testDir, ".gitignore"), "node_modules/\ndist/\n.env\n");
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
// .env is not in defaults, should appear
|
||||
expect(content).toContain("# .env");
|
||||
// node_modules/ and dist/ are in defaults, should not appear in .gitignore section
|
||||
const gitignoreSection = content.split("From .gitignore")[1]?.split("---")[0] ?? "";
|
||||
expect(gitignoreSection).not.toContain("node_modules");
|
||||
expect(gitignoreSection).not.toContain("dist");
|
||||
});
|
||||
|
||||
it("skips .gitignore comments and blank lines", () => {
|
||||
writeFileSync(join(testDir, ".gitignore"), "# a comment\n\n.env\n \n");
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("# .env");
|
||||
// Should not include the original comment as a pattern
|
||||
const gitignoreSection = content.split("From .gitignore")[1]?.split("---")[0] ?? "";
|
||||
expect(gitignoreSection).not.toContain("a comment");
|
||||
});
|
||||
|
||||
it("handles .gitignore with trailing-slash normalization for defaults", () => {
|
||||
// "dist" without trailing slash should still match "dist/" default
|
||||
writeFileSync(join(testDir, ".gitignore"), "dist\ncoverage\n.env\n");
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("From .gitignore");
|
||||
// Extract lines between the .gitignore header and the next section header
|
||||
const lines = content.split("\n");
|
||||
const headerIdx = lines.findIndex((l) => l.includes("From .gitignore"));
|
||||
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));
|
||||
expect(patterns).toContain(".env");
|
||||
expect(patterns).not.toContain("dist");
|
||||
expect(patterns).not.toContain("coverage");
|
||||
});
|
||||
|
||||
it("omits .gitignore section when no .gitignore exists", () => {
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).not.toContain("From .gitignore");
|
||||
});
|
||||
|
||||
it("omits .gitignore section when all patterns are covered by defaults", () => {
|
||||
writeFileSync(join(testDir, ".gitignore"), "node_modules/\ndist/\n*.lock\n");
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).not.toContain("From .gitignore");
|
||||
});
|
||||
|
||||
it("all .gitignore suggestions are commented out", () => {
|
||||
writeFileSync(join(testDir, ".gitignore"), ".env\nsecrets/\n*.pyc\n");
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
||||
expect(lines).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { DEFAULT_IGNORE_PATTERNS } from "./ignore-filter.js";
|
||||
|
||||
const HEADER = `# .understandignore — patterns for files/dirs to exclude from analysis
|
||||
# Syntax: same as .gitignore (globs, # comments, ! negation, trailing / for dirs)
|
||||
@@ -7,7 +8,7 @@ const HEADER = `# .understandignore — patterns for files/dirs to exclude from
|
||||
# Use ! prefix to force-include something excluded by defaults.
|
||||
#
|
||||
# Built-in defaults (always excluded unless negated):
|
||||
# node_modules/, .git/, dist/, build/, bin/, obj/, *.lock, *.min.js, etc.
|
||||
# node_modules/, .git/, dist/, build/, obj/, *.lock, *.min.js, etc.
|
||||
#
|
||||
`;
|
||||
|
||||
@@ -30,13 +31,51 @@ const GENERIC_SUGGESTIONS = [
|
||||
"*.snap",
|
||||
];
|
||||
|
||||
/**
|
||||
* Parses a .gitignore file and returns active patterns (no comments, no blanks).
|
||||
*/
|
||||
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("#"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a gitignore pattern is already covered by the hardcoded defaults.
|
||||
* Normalizes trailing slashes for comparison.
|
||||
*/
|
||||
function isCoveredByDefaults(pattern: string): boolean {
|
||||
const normalize = (p: string) => p.replace(/\/+$/, "");
|
||||
const normalized = normalize(pattern);
|
||||
return DEFAULT_IGNORE_PATTERNS.some((d) => normalize(d) === normalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a starter .understandignore file content by scanning the project
|
||||
* for common directories. All suggestions are commented out.
|
||||
* for common directories and reading .gitignore patterns.
|
||||
* All suggestions are commented out — this is a one-time generation.
|
||||
*/
|
||||
export function generateStarterIgnoreFile(projectRoot: string): string {
|
||||
const sections: string[] = [HEADER];
|
||||
|
||||
// Section 1: patterns from .gitignore not already in defaults
|
||||
const gitignorePath = join(projectRoot, ".gitignore");
|
||||
const gitignorePatterns = parseGitignorePatterns(gitignorePath).filter(
|
||||
(p) => !isCoveredByDefaults(p),
|
||||
);
|
||||
|
||||
if (gitignorePatterns.length > 0) {
|
||||
sections.push("# --- From .gitignore (uncomment to exclude) ---\n");
|
||||
for (const pattern of gitignorePatterns) {
|
||||
sections.push(`# ${pattern}`);
|
||||
}
|
||||
sections.push("");
|
||||
}
|
||||
|
||||
// Section 2: detected directories
|
||||
const detected: string[] = [];
|
||||
for (const { dir, pattern } of DETECTABLE_DIRS) {
|
||||
if (existsSync(join(projectRoot, dir))) {
|
||||
@@ -52,6 +91,7 @@ export function generateStarterIgnoreFile(projectRoot: string): string {
|
||||
sections.push("");
|
||||
}
|
||||
|
||||
// Section 3: generic test patterns
|
||||
sections.push("# --- Test file patterns (uncomment to exclude) ---\n");
|
||||
for (const pattern of GENERIC_SUGGESTIONS) {
|
||||
sections.push(`# ${pattern}`);
|
||||
|
||||
Reference in New Issue
Block a user