Skip to content

Commit 586fdf9

Browse files
committed
fix(server): decouple tree visibility from gitignore
1 parent 161ee2e commit 586fdf9

5 files changed

Lines changed: 67 additions & 26 deletions

File tree

packages/server/src/__tests__/file-commands.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,28 @@ describe("File Commands", () => {
119119
expect(files).toHaveLength(0);
120120
});
121121

122+
it("keeps .gitignore filtering for search results", async () => {
123+
await writeFile(join(testDir, ".gitignore"), "ignored-note.md\n");
124+
await writeFile(join(testDir, "ignored-note.md"), "hidden from search\n");
125+
126+
const result = await dispatch(
127+
{
128+
kind: "command",
129+
id: "file-search-3",
130+
op: "file.search",
131+
args: {
132+
workspaceId,
133+
query: "ignored",
134+
},
135+
},
136+
ctx
137+
);
138+
139+
expect(result.ok).toBe(true);
140+
const files = (result.data as { files: Array<{ path: string }> }).files;
141+
expect(files).toHaveLength(0);
142+
});
143+
122144
it("emits fs.dirty after file writes", async () => {
123145
const result = await dispatch(
124146
{

packages/server/src/__tests__/fs/gitignore.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import { mkdir, rm, writeFile } from "fs/promises";
66
import { tmpdir } from "os";
77
import { join } from "path";
88
import { afterEach, beforeEach, describe, expect, it } from "vitest";
9-
import { createGitignoreFilter, createWatcherIgnoreFilter } from "../../fs/gitignore.js";
9+
import {
10+
createGitignoreFilter,
11+
createTreeVisibilityFilter,
12+
createWatcherIgnoreFilter,
13+
} from "../../fs/gitignore.js";
1014

1115
describe("createGitignoreFilter", () => {
1216
let testDir: string;
@@ -86,6 +90,18 @@ describe("createGitignoreFilter", () => {
8690
});
8791
});
8892

93+
describe("createTreeVisibilityFilter", () => {
94+
it("hides only .git entries from the directory tree", () => {
95+
const filter = createTreeVisibilityFilter();
96+
97+
expect(filter(".git")).toBe(false);
98+
expect(filter(".gitignore")).toBe(true);
99+
expect(filter(".env")).toBe(true);
100+
expect(filter("node_modules")).toBe(true);
101+
expect(filter("src")).toBe(true);
102+
});
103+
});
104+
89105
describe("createWatcherIgnoreFilter", () => {
90106
let testDir: string;
91107

packages/server/src/__tests__/fs/tree.test.ts

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -91,25 +91,30 @@ describe("readTree", () => {
9191
expect(result.children[3].name).toBe("b-file.txt");
9292
});
9393

94-
it("should skip hidden files", async () => {
94+
it("should show hidden files except .git", async () => {
9595
await writeFile(join(testDir, ".hidden"), "hidden");
96+
await writeFile(join(testDir, ".gitignore"), "*.log\n");
97+
await mkdirAsync(join(testDir, ".git"));
9698
await writeFile(join(testDir, "visible.txt"), "visible");
9799

98100
const result = await readTree(testDir);
99101

100-
expect(result.children).toHaveLength(1);
101-
expect(result.children[0].name).toBe("visible.txt");
102+
expect(result.children.some((n) => n.name === ".hidden")).toBe(true);
103+
expect(result.children.some((n) => n.name === ".gitignore")).toBe(true);
104+
expect(result.children.some((n) => n.name === ".git")).toBe(false);
105+
expect(result.children.some((n) => n.name === "visible.txt")).toBe(true);
102106
});
103107

104-
it("should skip node_modules and .git", async () => {
108+
it("should show node_modules but skip .git", async () => {
105109
await mkdirAsync(join(testDir, "node_modules"));
106110
await mkdirAsync(join(testDir, ".git"));
107111
await writeFile(join(testDir, "file.txt"), "content");
108112

109113
const result = await readTree(testDir);
110114

111-
expect(result.children).toHaveLength(1);
112-
expect(result.children[0].name).toBe("file.txt");
115+
expect(result.children.some((n) => n.name === "node_modules")).toBe(true);
116+
expect(result.children.some((n) => n.name === ".git")).toBe(false);
117+
expect(result.children.some((n) => n.name === "file.txt")).toBe(true);
113118
});
114119

115120
it("should use relative paths", async () => {
@@ -122,7 +127,7 @@ describe("readTree", () => {
122127
// Subdir children are undefined (lazy loading)
123128
});
124129

125-
it("should respect .gitignore rules", async () => {
130+
it("should not hide .gitignore-matched files from the tree", async () => {
126131
await writeFile(join(testDir, ".gitignore"), "*.log\ndist/");
127132
await writeFile(join(testDir, "app.log"), "log content");
128133
await writeFile(join(testDir, "app.txt"), "text content");
@@ -131,23 +136,9 @@ describe("readTree", () => {
131136

132137
const result = await readTree(testDir);
133138

134-
expect(result.children.some((n) => n.name === "app.log")).toBe(false);
135-
expect(result.children.some((n) => n.name === "dist")).toBe(false);
139+
expect(result.children.some((n) => n.name === "app.log")).toBe(true);
140+
expect(result.children.some((n) => n.name === "dist")).toBe(true);
136141
expect(result.children.some((n) => n.name === "app.txt")).toBe(true);
137142
expect(result.children.some((n) => n.name === "src")).toBe(true);
138143
});
139-
140-
it("should show dotfiles when .gitignore does not ignore them", async () => {
141-
await writeFile(join(testDir, ".gitignore"), "*.log");
142-
await writeFile(join(testDir, ".env"), "secret");
143-
await writeFile(join(testDir, ".hidden-config"), "hidden");
144-
await writeFile(join(testDir, "visible.txt"), "visible");
145-
146-
const result = await readTree(testDir);
147-
148-
expect(result.children.some((n) => n.name === ".env")).toBe(true);
149-
expect(result.children.some((n) => n.name === ".gitignore")).toBe(true);
150-
expect(result.children.some((n) => n.name === ".hidden-config")).toBe(true);
151-
expect(result.children.some((n) => n.name === "visible.txt")).toBe(true);
152-
});
153144
});

packages/server/src/fs/gitignore.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ function isDefaultTreeIgnored(name: string): boolean {
2525
return name.startsWith(".") || name === "node_modules" || name === ".git";
2626
}
2727

28+
function isTreeHidden(name: string): boolean {
29+
return name === ".git";
30+
}
31+
2832
function isAlwaysTreeIgnored(name: string): boolean {
2933
return name === "node_modules" || name === ".git";
3034
}
@@ -68,6 +72,14 @@ export function createGitignoreFilter(
6872
};
6973
}
7074

75+
/**
76+
* Creates a filter for directory tree visibility.
77+
* Returns false if the entry should be hidden from the tree, true otherwise.
78+
*/
79+
export function createTreeVisibilityFilter(): (name: string) => boolean {
80+
return (name: string) => !isTreeHidden(name);
81+
}
82+
7183
/**
7284
* Creates a filter for the file watcher (chokidar).
7385
* Returns a function suitable for chokidar's `ignored` option.

packages/server/src/fs/tree.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import type { FileNode } from "@coder-studio/core";
77
import { readdir, stat } from "fs/promises";
88
import { join, relative } from "path";
9-
import { createGitignoreFilter } from "./gitignore.js";
9+
import { createGitignoreFilter, createTreeVisibilityFilter } from "./gitignore.js";
1010

1111
export interface ReadTreeResult {
1212
path: string;
@@ -24,7 +24,7 @@ export interface ReadTreeResult {
2424
*/
2525
export async function readTree(rootPath: string, subdir?: string): Promise<ReadTreeResult> {
2626
const targetPath = subdir ? join(rootPath, subdir) : rootPath;
27-
const filter = createGitignoreFilter(rootPath, targetPath);
27+
const filter = createTreeVisibilityFilter();
2828

2929
const entries = await readdir(targetPath, { withFileTypes: true });
3030
const nodes: FileNode[] = [];

0 commit comments

Comments
 (0)