Skip to content

Commit ff7be1c

Browse files
feat(server): add gitignore support to SourceExclusions
- Add test setup/cleanup with temp workspace and gitignore file - Add new test cases for gitignore pattern exclusions - Update existing tests for cross-platform compatibility - Test various gitignore patterns (wildcards, directories, extensions)
1 parent f20c645 commit ff7be1c

File tree

2 files changed

+324
-29
lines changed

2 files changed

+324
-29
lines changed

shared/src/main/kotlin/org/javacs/kt/SourceExclusions.kt

+67-29
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@ package org.javacs.kt
22

33
import java.net.URI
44
import java.nio.file.FileSystems
5+
import java.nio.file.Files
56
import java.nio.file.Path
67
import org.javacs.kt.util.filePath
8+
import java.io.IOException
79

8-
// TODO: Read exclusions from gitignore/settings.json/... instead of
9-
// hardcoding them
1010
class SourceExclusions(
1111
private val workspaceRoots: Collection<Path>,
1212
scriptsConfig: ScriptsConfiguration,
1313
) {
14-
val excludedPatterns =
15-
(listOf(
14+
private val defaultPatterns =
15+
listOf(
1616
".git",
1717
".hg",
1818
".svn", // Version control systems
@@ -26,16 +26,52 @@ class SourceExclusions(
2626
"bin",
2727
"build",
2828
"node_modules",
29-
) +
30-
when {
31-
!scriptsConfig.enabled -> listOf("*.kts")
32-
!scriptsConfig.buildScriptsEnabled -> listOf("*.gradle.kts")
33-
else -> emptyList()
34-
})
29+
)
30+
31+
private val scriptPatterns =
32+
when {
33+
!scriptsConfig.enabled -> listOf("*.kts")
34+
!scriptsConfig.buildScriptsEnabled -> listOf("*.gradle.kts")
35+
else -> emptyList()
36+
}
3537

36-
private val exclusionMatchers = excludedPatterns
37-
.filter { !it.startsWith("!") }
38-
.map { FileSystems.getDefault().getPathMatcher("glob:$it") }
38+
private val gitignorePatterns: List<String> = readGitignorePatterns()
39+
40+
val excludedPatterns = defaultPatterns + scriptPatterns + gitignorePatterns
41+
42+
private val exclusionMatchers =
43+
excludedPatterns
44+
.filter { !it.startsWith("!") } // Skip negated patterns for now
45+
.map { FileSystems.getDefault().getPathMatcher("glob:$it") }
46+
47+
private fun readGitignorePatterns(): List<String> {
48+
return workspaceRoots
49+
.flatMap { root ->
50+
val gitignore = root.resolve(".gitignore")
51+
if (Files.exists(gitignore)) {
52+
try {
53+
Files.readAllLines(gitignore)
54+
.asSequence()
55+
.map { it.trim() }
56+
.filter { it.isNotEmpty() && !it.startsWith("#") }
57+
.map { it.removeSuffix("/") }
58+
.toList()
59+
.also { patterns ->
60+
LOG.debug("Read {} patterns from {}", patterns.size, gitignore)
61+
}
62+
} catch (e: IOException) {
63+
LOG.warn("Could not read .gitignore at $gitignore: ${e.message}")
64+
emptyList()
65+
} catch (e: SecurityException) {
66+
LOG.warn("Security error reading .gitignore at $gitignore: ${e.message}")
67+
emptyList()
68+
}
69+
} else {
70+
emptyList()
71+
}
72+
}
73+
.distinct() // Remove duplicates across workspace roots
74+
}
3975

4076
fun walkIncluded(): Sequence<Path> =
4177
workspaceRoots.asSequence().flatMap { root ->
@@ -49,25 +85,27 @@ class SourceExclusions(
4985
return false
5086
}
5187

52-
val relativePaths = workspaceRoots
53-
.mapNotNull { if (file.startsWith(it)) it.relativize(file) else null }
54-
.flatten()
88+
val relativePaths =
89+
workspaceRoots
90+
.mapNotNull { if (file.startsWith(it)) it.relativize(file) else null }
91+
.flatten()
5592

56-
val isIncluded = when {
57-
// Check if we're in a target directory
58-
relativePaths.contains(Path.of("target")) -> {
59-
val pathList = relativePaths.toList()
60-
val targetIndex = pathList.indexOf(Path.of("target"))
61-
// Allow only target directory itself or if next directory is generated-sources
62-
pathList.size <= targetIndex + 1 || pathList[targetIndex + 1] == Path.of("generated-sources")
93+
val isIncluded =
94+
when {
95+
// Check if we're in a target directory
96+
relativePaths.contains(Path.of("target")) -> {
97+
val pathList = relativePaths.toList()
98+
val targetIndex = pathList.indexOf(Path.of("target"))
99+
// Allow only target directory itself or if next directory is generated-sources
100+
pathList.size <= targetIndex + 1 ||
101+
pathList[targetIndex + 1] == Path.of("generated-sources")
102+
}
103+
// Check exclusion patterns
104+
exclusionMatchers.any { matcher -> relativePaths.any(matcher::matches) } -> false
105+
// Include paths outside target directory by default
106+
else -> true
63107
}
64-
// Check exclusion patterns
65-
exclusionMatchers.any { matcher -> relativePaths.any(matcher::matches) } -> false
66-
// Include paths outside target directory by default
67-
else -> true
68-
}
69108

70109
return isIncluded
71110
}
72111
}
73-
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
package org.javacs.kt
2+
3+
import java.io.File
4+
import java.nio.file.Files
5+
import java.nio.file.attribute.PosixFilePermissions
6+
import org.hamcrest.Matchers.equalTo
7+
import org.junit.After
8+
import org.junit.Assert.assertThat
9+
import org.junit.Before
10+
import org.junit.Test
11+
12+
class SourceExclusionsTest {
13+
private val workspaceRoot =
14+
File(System.getProperty("java.io.tmpdir"), "test-workspace").toPath()
15+
private lateinit var sourceExclusions: SourceExclusions
16+
17+
@Before
18+
fun setup() {
19+
Files.createDirectories(workspaceRoot)
20+
21+
val gitignore = workspaceRoot.resolve(".gitignore")
22+
Files.write(
23+
gitignore,
24+
listOf("*.log", "output/", "temp/", "# comment to ignore", "build-output/", "*.tmp"),
25+
)
26+
27+
sourceExclusions =
28+
SourceExclusions(
29+
workspaceRoots = listOf(workspaceRoot),
30+
scriptsConfig = ScriptsConfiguration(enabled = true, buildScriptsEnabled = true),
31+
)
32+
}
33+
34+
@After
35+
fun cleanup() {
36+
workspaceRoot.toFile().deleteRecursively()
37+
}
38+
39+
@Test
40+
fun `test path exclusions`() {
41+
assertThat(sourceExclusions.isPathIncluded(workspaceRoot.resolve(".git")), equalTo(false))
42+
assertThat(
43+
sourceExclusions.isPathIncluded(workspaceRoot.resolve("node_modules")),
44+
equalTo(false),
45+
)
46+
assertThat(sourceExclusions.isPathIncluded(workspaceRoot.resolve(".idea")), equalTo(false))
47+
48+
assertThat(
49+
sourceExclusions.isPathIncluded(
50+
workspaceRoot.resolve("src").resolve("main").resolve("kotlin")
51+
),
52+
equalTo(true),
53+
)
54+
assertThat(
55+
sourceExclusions.isPathIncluded(
56+
workspaceRoot.resolve("src").resolve("test").resolve("kotlin")
57+
),
58+
equalTo(true),
59+
)
60+
}
61+
62+
@Test
63+
fun `test gitignore patterns`() {
64+
assertThat(
65+
sourceExclusions.isPathIncluded(workspaceRoot.resolve("debug.log")),
66+
equalTo(false),
67+
)
68+
assertThat(
69+
sourceExclusions.isPathIncluded(workspaceRoot.resolve("output").resolve("file.txt")),
70+
equalTo(false),
71+
)
72+
assertThat(sourceExclusions.isPathIncluded(workspaceRoot.resolve("temp")), equalTo(false))
73+
assertThat(
74+
sourceExclusions.isPathIncluded(workspaceRoot.resolve("build-output")),
75+
equalTo(false),
76+
)
77+
assertThat(
78+
sourceExclusions.isPathIncluded(workspaceRoot.resolve("data.tmp")),
79+
equalTo(false),
80+
)
81+
82+
assertThat(
83+
sourceExclusions.isPathIncluded(workspaceRoot.resolve("src").resolve("main.kt")),
84+
equalTo(true),
85+
)
86+
assertThat(
87+
sourceExclusions.isPathIncluded(workspaceRoot.resolve("README.md")),
88+
equalTo(true),
89+
)
90+
}
91+
92+
@Test
93+
fun `test target directory handling`() {
94+
assertThat(sourceExclusions.isPathIncluded(workspaceRoot.resolve("target")), equalTo(true))
95+
assertThat(
96+
sourceExclusions.isPathIncluded(
97+
workspaceRoot.resolve("target").resolve("generated-sources")
98+
),
99+
equalTo(true),
100+
)
101+
assertThat(
102+
sourceExclusions.isPathIncluded(workspaceRoot.resolve("target").resolve("classes")),
103+
equalTo(false),
104+
)
105+
}
106+
107+
@Test
108+
fun `test URI inclusion`() {
109+
val includedUri =
110+
workspaceRoot
111+
.resolve("src")
112+
.resolve("main")
113+
.resolve("kotlin")
114+
.resolve("Example.kt")
115+
.toUri()
116+
val excludedUri = workspaceRoot.resolve(".git").resolve("config").toUri()
117+
val gitignoreExcludedUri = workspaceRoot.resolve("output").resolve("test.txt").toUri()
118+
119+
assertThat(sourceExclusions.isURIIncluded(includedUri), equalTo(true))
120+
assertThat(sourceExclusions.isURIIncluded(excludedUri), equalTo(false))
121+
assertThat(sourceExclusions.isURIIncluded(gitignoreExcludedUri), equalTo(false))
122+
}
123+
124+
@Test
125+
fun `test paths outside workspace root`() {
126+
val outsidePath =
127+
File(System.getProperty("java.io.tmpdir"), "outside-workspace")
128+
.toPath()
129+
.resolve("file.kt")
130+
assertThat(sourceExclusions.isPathIncluded(outsidePath), equalTo(false))
131+
}
132+
133+
@Test
134+
fun `test script handling based on configuration`() {
135+
val restrictedScriptsExclusions =
136+
SourceExclusions(
137+
workspaceRoots = listOf(workspaceRoot),
138+
scriptsConfig = ScriptsConfiguration(enabled = false),
139+
)
140+
141+
assertThat(
142+
restrictedScriptsExclusions.isPathIncluded(workspaceRoot.resolve("build.gradle.kts")),
143+
equalTo(false),
144+
)
145+
146+
assertThat(
147+
sourceExclusions.isPathIncluded(workspaceRoot.resolve("build.gradle.kts")),
148+
equalTo(true),
149+
)
150+
}
151+
152+
@Test
153+
fun `test gitignore handling with IO errors`() {
154+
val ioErrorWorkspace = workspaceRoot.resolve("io-error-workspace")
155+
Files.createDirectories(ioErrorWorkspace)
156+
157+
val problematicGitignore = ioErrorWorkspace.resolve(".gitignore")
158+
Files.write(problematicGitignore, listOf("test-pattern"))
159+
160+
try {
161+
// Make the file unreadable to simulate IO error
162+
Files.setPosixFilePermissions(
163+
problematicGitignore,
164+
PosixFilePermissions.fromString("--x------"),
165+
)
166+
167+
val exclusionsWithIOError =
168+
SourceExclusions(
169+
workspaceRoots = listOf(ioErrorWorkspace),
170+
scriptsConfig = ScriptsConfiguration(enabled = true, buildScriptsEnabled = true),
171+
)
172+
173+
assertThat(
174+
exclusionsWithIOError.isPathIncluded(ioErrorWorkspace.resolve(".git")),
175+
equalTo(false),
176+
)
177+
assertThat(
178+
exclusionsWithIOError.isPathIncluded(
179+
ioErrorWorkspace.resolve("src/main/kotlin/Test.kt")
180+
),
181+
equalTo(true),
182+
)
183+
} catch (e: UnsupportedOperationException) {
184+
// Skip test if POSIX permissions are not supported
185+
}
186+
}
187+
188+
@Test
189+
fun `test multiple gitignore files`() {
190+
val subdir = workspaceRoot.resolve("subproject")
191+
Files.createDirectories(subdir)
192+
Files.write(subdir.resolve(".gitignore"), listOf("subproject-specific.log", "local-temp/"))
193+
194+
val multiRootExclusions =
195+
SourceExclusions(
196+
workspaceRoots = listOf(workspaceRoot, subdir),
197+
scriptsConfig = ScriptsConfiguration(enabled = true, buildScriptsEnabled = true),
198+
)
199+
200+
assertThat(
201+
multiRootExclusions.isPathIncluded(workspaceRoot.resolve("debug.log")),
202+
equalTo(false),
203+
)
204+
assertThat(
205+
multiRootExclusions.isPathIncluded(subdir.resolve("subproject-specific.log")),
206+
equalTo(false),
207+
)
208+
assertThat(multiRootExclusions.isPathIncluded(subdir.resolve("local-temp")), equalTo(false))
209+
}
210+
211+
@Test
212+
fun `test empty gitignore handling`() {
213+
val emptyGitignoreWorkspace = workspaceRoot.resolve("empty-gitignore-workspace")
214+
Files.createDirectories(emptyGitignoreWorkspace)
215+
Files.write(emptyGitignoreWorkspace.resolve(".gitignore"), listOf<String>())
216+
217+
val exclusionsWithEmptyGitignore =
218+
SourceExclusions(
219+
workspaceRoots = listOf(emptyGitignoreWorkspace),
220+
scriptsConfig = ScriptsConfiguration(enabled = true, buildScriptsEnabled = true),
221+
)
222+
223+
assertThat(
224+
exclusionsWithEmptyGitignore.isPathIncluded(emptyGitignoreWorkspace.resolve(".git")),
225+
equalTo(false),
226+
)
227+
assertThat(
228+
exclusionsWithEmptyGitignore.isPathIncluded(
229+
emptyGitignoreWorkspace.resolve("src/main/kotlin/Test.kt")
230+
),
231+
equalTo(true),
232+
)
233+
}
234+
235+
@Test
236+
fun `test non-existent gitignore handling`() {
237+
val noGitignoreWorkspace = workspaceRoot.resolve("no-gitignore-workspace")
238+
Files.createDirectories(noGitignoreWorkspace)
239+
240+
val exclusionsWithoutGitignore =
241+
SourceExclusions(
242+
workspaceRoots = listOf(noGitignoreWorkspace),
243+
scriptsConfig = ScriptsConfiguration(enabled = true, buildScriptsEnabled = true),
244+
)
245+
246+
assertThat(
247+
exclusionsWithoutGitignore.isPathIncluded(noGitignoreWorkspace.resolve(".git")),
248+
equalTo(false),
249+
)
250+
assertThat(
251+
exclusionsWithoutGitignore.isPathIncluded(
252+
noGitignoreWorkspace.resolve("src/main/kotlin/Test.kt")
253+
),
254+
equalTo(true),
255+
)
256+
}
257+
}

0 commit comments

Comments
 (0)