-
Notifications
You must be signed in to change notification settings - Fork 2.6k
fix(gradle): use .gitignore to determine dependentTasksOutputFiles #33213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
fix(gradle): use .gitignore to determine dependentTasksOutputFiles #33213
Conversation
✅ Deploy Preview for nx-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
View your CI Pipeline Execution ↗ for commit 924cce4
☁️ Nx Cloud last updated this comment at |
9b1a189 to
9b5dc17
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nx Cloud is proposing a fix for your failed CI:
These changes apply automated code formatting to the Gradle project graph Kotlin files using ktfmt. We've reformatted the code to follow consistent style guidelines including proper indentation, line breaks, and import ordering, which will improve code readability and maintainability across the codebase.
We verified this fix by re-running gradle-project-graph:build.
Suggested Fix changes
diff --git a/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/GitIgnoreClassifier.kt b/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/GitIgnoreClassifier.kt
index d19bdcdcb3..a7b04a13ed 100644
--- a/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/GitIgnoreClassifier.kt
+++ b/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/GitIgnoreClassifier.kt
@@ -1,99 +1,100 @@
package dev.nx.gradle.utils
-import org.eclipse.jgit.ignore.FastIgnoreRule
import java.io.File
+import org.eclipse.jgit.ignore.FastIgnoreRule
/**
- * Determines if files match gitignore patterns
- * Provides heuristic for parameter classification: ignored files are likely outputs, tracked files are likely inputs
+ * Determines if files match gitignore patterns Provides heuristic for parameter classification:
+ * ignored files are likely outputs, tracked files are likely inputs
*/
-class GitIgnoreClassifier(
- private val workspaceRoot: File
-) {
- private val ignoreRules: MutableList<FastIgnoreRule> = mutableListOf()
+class GitIgnoreClassifier(private val workspaceRoot: File) {
+ private val ignoreRules: MutableList<FastIgnoreRule> = mutableListOf()
- init {
- loadIgnoreRules()
+ init {
+ loadIgnoreRules()
+ }
+
+ private fun loadIgnoreRules() {
+ try {
+ val gitIgnoreFile = File(workspaceRoot, ".gitignore")
+ if (gitIgnoreFile.exists()) {
+ gitIgnoreFile.readLines().forEach { line ->
+ val trimmed = line.trim()
+ if (trimmed.isNotEmpty() && !trimmed.startsWith("#")) {
+ try {
+ val rule = FastIgnoreRule(trimmed)
+ ignoreRules.add(rule)
+ } catch (e: Exception) {
+ // Skip invalid rules silently
+ }
+ }
+ }
+ }
+ } catch (e: Exception) {
+ // If we can't load gitignore rules, continue without them
}
+ }
- private fun loadIgnoreRules() {
+ private fun isPartOfWorkspace(path: File): Boolean {
+ // Use canonicalPath to resolve symlinks and normalize paths
+ val workspaceRootPath =
try {
- val gitIgnoreFile = File(workspaceRoot, ".gitignore")
- if (gitIgnoreFile.exists()) {
- gitIgnoreFile.readLines().forEach { line ->
- val trimmed = line.trim()
- if (trimmed.isNotEmpty() && !trimmed.startsWith("#")) {
- try {
- val rule = FastIgnoreRule(trimmed)
- ignoreRules.add(rule)
- } catch (e: Exception) {
- // Skip invalid rules silently
- }
- }
- }
- }
+ workspaceRoot.canonicalPath
} catch (e: Exception) {
- // If we can't load gitignore rules, continue without them
+ workspaceRoot.absolutePath
}
- }
- private fun isPartOfWorkspace(path: File): Boolean {
- // Use canonicalPath to resolve symlinks and normalize paths
- val workspaceRootPath = try {
- workspaceRoot.canonicalPath
- } catch (e: Exception) {
- workspaceRoot.absolutePath
- }
+ val filePath =
+ try {
+ path.canonicalPath
+ } catch (e: Exception) {
+ path.absolutePath
+ }
- val filePath = try {
- path.canonicalPath
- } catch (e: Exception) {
- path.absolutePath
- }
+ // Ensure the file path starts with the workspace root and is followed by a separator
+ // or is exactly the workspace root (which we exclude)
+ if (filePath == workspaceRootPath) {
+ return false
+ }
- // Ensure the file path starts with the workspace root and is followed by a separator
- // or is exactly the workspace root (which we exclude)
- if (filePath == workspaceRootPath) {
- return false
- }
+ return filePath.startsWith(workspaceRootPath + File.separator)
+ }
- return filePath.startsWith(workspaceRootPath + File.separator)
+ /**
+ * Determines if a file path should be ignored according to gitignore rules Works for both
+ * existing and non-existent paths by using pattern matching
+ */
+ fun isIgnored(path: File): Boolean {
+ if (ignoreRules.isEmpty()) {
+ return false
}
- /**
- * Determines if a file path should be ignored according to gitignore rules
- * Works for both existing and non-existent paths by using pattern matching
- */
- fun isIgnored(path: File): Boolean {
- if (ignoreRules.isEmpty()) {
- return false
- }
-
- if (!isPartOfWorkspace(path)) {
- return false
- }
+ if (!isPartOfWorkspace(path)) {
+ return false
+ }
- val relativePath = try {
+ val relativePath =
+ try {
path.relativeTo(workspaceRoot).path
} catch (e: IllegalArgumentException) {
return false
}
- return try {
- // Check path against all ignore rules
- var isIgnored = false
-
- for (rule in ignoreRules) {
- val isDirectory = path.isDirectory || relativePath.endsWith("/")
- if (rule.isMatch(relativePath, isDirectory)) {
- // FastIgnoreRule.getResult() returns true if should be ignored
- isIgnored = rule.result
- }
- }
+ return try {
+ // Check path against all ignore rules
+ var isIgnored = false
- isIgnored
- } catch (e: Exception) {
- false
+ for (rule in ignoreRules) {
+ val isDirectory = path.isDirectory || relativePath.endsWith("/")
+ if (rule.isMatch(relativePath, isDirectory)) {
+ // FastIgnoreRule.getResult() returns true if should be ignored
+ isIgnored = rule.result
}
+ }
+
+ isIgnored
+ } catch (e: Exception) {
+ false
}
+ }
}
diff --git a/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/TaskUtils.kt b/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/TaskUtils.kt
index 0dd6cc225f..62a6d0b77a 100644
--- a/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/TaskUtils.kt
+++ b/packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/TaskUtils.kt
@@ -88,7 +88,9 @@ fun getGradlewCommand(): String {
/**
* Cache the gitignore classifier per workspace root to avoid recreating it for every task
- * TODO(lourw): refactor this out. The structure of this plugin should be refactored to use dependency injection rather than maintaining a cached instance
+ *
+ * TODO(lourw): refactor this out. The structure of this plugin should be refactored to use
+ * dependency injection rather than maintaining a cached instance
*/
private val gitignoreClassifierCache = mutableMapOf<String, GitIgnoreClassifier>()
@@ -112,9 +114,10 @@ fun getInputsForTask(
val inputs = mutableListOf<Any>()
val externalDependencies = mutableListOf<String>()
- val classifier = gitignoreClassifierCache.getOrPut(workspaceRoot) {
- GitIgnoreClassifier(File(workspaceRoot))
- }
+ val classifier =
+ gitignoreClassifierCache.getOrPut(workspaceRoot) {
+ GitIgnoreClassifier(File(workspaceRoot))
+ }
// Collect outputs from dependent tasks
val tasksToProcess = dependsOnTasks ?: getDependsOnTask(task)
@@ -135,7 +138,8 @@ fun getInputsForTask(
// File is outside workspace - treat as external dependency
relativePath == null -> {
try {
- val externalDep = getExternalDepFromInputFile(inputFile.path, externalNodes, task.logger)
+ val externalDep =
+ getExternalDepFromInputFile(inputFile.path, externalNodes, task.logger)
externalDep?.let { externalDependencies.add(it) }
} catch (e: Exception) {
task.logger.info("Error resolving external dependency for ${inputFile.path}: $e")
@@ -168,16 +172,12 @@ fun getInputsForTask(
}
}
-/**
- * Checks if a file is within the workspace.
- */
+/** Checks if a file is within the workspace. */
private fun isFileInWorkspace(file: File, workspaceRoot: String): Boolean {
return file.path.startsWith(workspaceRoot + File.separator)
}
-/**
- * Converts a file to a relative path. If it's a directory, returns a glob pattern.
- */
+/** Converts a file to a relative path. If it's a directory, returns a glob pattern. */
private fun toRelativePathOrGlob(file: File, workspaceRoot: String): String {
val relativePath = file.path.substring(workspaceRoot.length + 1)
val isFile = file.name.contains('.') || (file.exists() && file.isFile)
diff --git a/packages/gradle/project-graph/src/test/kotlin/dev/nx/gradle/utils/GitIgnoreClassifierTest.kt b/packages/gradle/project-graph/src/test/kotlin/dev/nx/gradle/utils/GitIgnoreClassifierTest.kt
index 14e4820f62..0e6fb7b80e 100644
--- a/packages/gradle/project-graph/src/test/kotlin/dev/nx/gradle/utils/GitIgnoreClassifierTest.kt
+++ b/packages/gradle/project-graph/src/test/kotlin/dev/nx/gradle/utils/GitIgnoreClassifierTest.kt
@@ -21,11 +21,11 @@ class GitIgnoreClassifierTest {
val gitignore = File(tempDir, ".gitignore")
gitignore.writeText(
"""
- # Comments should be ignored
- node_modules
- *.log
- dist
- build
+ # Comments should be ignored
+ node_modules
+ *.log
+ dist
+ build
"""
.trimIndent())
@@ -54,9 +54,9 @@ class GitIgnoreClassifierTest {
val gitignore = File(tempDir, ".gitignore")
gitignore.writeText(
"""
- build
- .gradle
- out
+ build
+ .gradle
+ out
"""
.trimIndent())
@@ -83,10 +83,10 @@ class GitIgnoreClassifierTest {
val gitignore = File(tempDir, ".gitignore")
gitignore.writeText(
"""
- *.class
- *.jar
- *.log
- **/*.tmp
+ *.class
+ *.jar
+ *.log
+ **/*.tmp
"""
.trimIndent())
@@ -113,8 +113,8 @@ class GitIgnoreClassifierTest {
val gitignore = File(tempDir, ".gitignore")
gitignore.writeText(
"""
- /build/
- /dist/
+ /build/
+ /dist/
"""
.trimIndent())
@@ -136,8 +136,8 @@ class GitIgnoreClassifierTest {
val gitignore = File(tempDir, ".gitignore")
gitignore.writeText(
"""
- *.log
- !important.log
+ *.log
+ !important.log
"""
.trimIndent())
@@ -168,22 +168,22 @@ class GitIgnoreClassifierTest {
val gitignore = File(tempDir, ".gitignore")
gitignore.writeText(
"""
- # Build outputs
- build
- .gradle
- dist
- out
- target
-
- # IDE
- .idea
- .vscode
-
- # Logs
- *.log
-
- # OS
- .DS_Store
+ # Build outputs
+ build
+ .gradle
+ dist
+ out
+ target
+
+ # IDE
+ .idea
+ .vscode
+
+ # Logs
+ *.log
+
+ # OS
+ .DS_Store
"""
.trimIndent())
@@ -256,10 +256,10 @@ class GitIgnoreClassifierTest {
val gitignore = File(tempDir, ".gitignore")
gitignore.writeText(
"""
- # This is a comment
- # Another comment
+ # This is a comment
+ # Another comment
- # Yet another comment
+ # Yet another comment
"""
.trimIndent())
diff --git a/packages/gradle/project-graph/src/test/kotlin/dev/nx/gradle/utils/ProcessTaskUtilsTest.kt b/packages/gradle/project-graph/src/test/kotlin/dev/nx/gradle/utils/ProcessTaskUtilsTest.kt
index 0290d325e0..d42e23e6ea 100644
--- a/packages/gradle/project-graph/src/test/kotlin/dev/nx/gradle/utils/ProcessTaskUtilsTest.kt
+++ b/packages/gradle/project-graph/src/test/kotlin/dev/nx/gradle/utils/ProcessTaskUtilsTest.kt
@@ -3,7 +3,6 @@ package dev.nx.gradle.utils
import dev.nx.gradle.data.Dependency
import dev.nx.gradle.data.ExternalNode
import org.gradle.api.Project
-import org.gradle.internal.serialize.codecs.core.NodeOwner
import org.gradle.testfixtures.ProjectBuilder
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
@@ -122,7 +121,8 @@ class ProcessTaskUtilsTest {
projectRoot = project.projectDir.path
val gitIgnore = java.io.File(workspaceRoot, ".gitignore")
- // Any inputs of tasks that are found in ignored files are considered dependent task output files
+ // Any inputs of tasks that are found in ignored files are considered dependent task output
+ // files
gitIgnore.writeText("dist")
}
@@ -148,7 +148,7 @@ class ProcessTaskUtilsTest {
// Should contain dependentTasksOutputFiles for the output
assertTrue(
- result!!.any { it is Map<*, *> && it["dependentTasksOutputFiles"] == "dist/output.jar" })
+ result!!.any { it is Map<*, *> && it["dependentTasksOutputFiles"] == "dist/output.jar" })
// Should contain the non-conflicting input file
assertTrue(result.any { it == "{projectRoot}/src/main.kt" })
@@ -175,13 +175,13 @@ class ProcessTaskUtilsTest {
// File should get exact path
assertTrue(
- result!!.any { it is Map<*, *> && it["dependentTasksOutputFiles"] == "dist/app.jar" })
+ result!!.any { it is Map<*, *> && it["dependentTasksOutputFiles"] == "dist/app.jar" })
// Directory should get glob pattern
assertTrue(
- result.any {
- it is Map<*, *> && (it["dependentTasksOutputFiles"] as String).endsWith("/**/*")
- })
+ result.any {
+ it is Map<*, *> && (it["dependentTasksOutputFiles"] as String).endsWith("/**/*")
+ })
}
@Test
@@ -204,11 +204,12 @@ class ProcessTaskUtilsTest {
// Test with pre-computed dependsOnTasks
val resultWithPreComputed =
- getInputsForTask(preComputedDependsOn, mainTask, projectRoot, workspaceRoot, mutableMapOf())
+ getInputsForTask(
+ preComputedDependsOn, mainTask, projectRoot, workspaceRoot, mutableMapOf())
// Test without pre-computed (should compute internally)
val resultWithoutPreComputed =
- getInputsForTask(null, mainTask, projectRoot, workspaceRoot, mutableMapOf())
+ getInputsForTask(null, mainTask, projectRoot, workspaceRoot, mutableMapOf())
// Both results should be identical
assertNotNull(resultWithPreComputed)
@@ -217,13 +218,13 @@ class ProcessTaskUtilsTest {
// Should contain dependentTasksOutputFiles for the dependent task output
assertTrue(
- resultWithPreComputed.any {
- it is Map<*, *> && it["dependentTasksOutputFiles"] == "dist/output.jar"
- })
+ resultWithPreComputed.any {
+ it is Map<*, *> && it["dependentTasksOutputFiles"] == "dist/output.jar"
+ })
assertTrue(
- resultWithoutPreComputed.any {
- it is Map<*, *> && it["dependentTasksOutputFiles"] == "dist/output.jar"
- })
+ resultWithoutPreComputed.any {
+ it is Map<*, *> && it["dependentTasksOutputFiles"] == "dist/output.jar"
+ })
// Should contain the input file
assertTrue(resultWithPreComputed.any { it == "{projectRoot}/src/main.kt" })
@@ -284,9 +285,9 @@ class ProcessTaskUtilsTest {
val dependentTask3 = project.tasks.register("dependentTask3").get()
val multipleOutputs =
- listOf(
- java.io.File("$workspaceRoot/reports/test.xml"),
- java.io.File("$workspaceRoot/reports/coverage"))
+ listOf(
+ java.io.File("$workspaceRoot/reports/test.xml"),
+ java.io.File("$workspaceRoot/reports/coverage"))
dependentTask3.outputs.files(multipleOutputs)
// Create main task that depends on all three
@@ -295,37 +296,38 @@ class ProcessTaskUtilsTest {
// Add some input files
val inputFiles =
- listOf(
- java.io.File("$workspaceRoot/src/main.kt"),
- java.io.File("$workspaceRoot/config/app.properties"))
+ listOf(
+ java.io.File("$workspaceRoot/src/main.kt"),
+ java.io.File("$workspaceRoot/config/app.properties"))
mainTask.inputs.files(inputFiles)
// Get dependsOnTasks once and reuse
val dependsOnTasks = getDependsOnTask(mainTask)
val result =
- getInputsForTask(dependsOnTasks, mainTask, projectRoot, workspaceRoot, mutableMapOf())
+ getInputsForTask(dependsOnTasks, mainTask, projectRoot, workspaceRoot, mutableMapOf())
assertNotNull(result)
// Should contain dependentTasksOutputFiles for file output (exact path)
assertTrue(
- result!!.any { it is Map<*, *> && it["dependentTasksOutputFiles"] == "dist/app.jar" })
+ result!!.any { it is Map<*, *> && it["dependentTasksOutputFiles"] == "dist/app.jar" })
// Should contain dependentTasksOutputFiles for directory output (with /**/* pattern)
assertTrue(
- result.any {
- it is Map<*, *> && (it["dependentTasksOutputFiles"] as String) == "build/classes/**/*"
- })
+ result.any {
+ it is Map<*, *> && (it["dependentTasksOutputFiles"] as String) == "build/classes/**/*"
+ })
// Should contain dependentTasksOutputFiles for test report file
assertTrue(
- result.any { it is Map<*, *> && it["dependentTasksOutputFiles"] == "reports/test.xml" })
+ result.any { it is Map<*, *> && it["dependentTasksOutputFiles"] == "reports/test.xml" })
// Should contain dependentTasksOutputFiles for coverage directory (with /**/* pattern)
assertTrue(
- result.any {
- it is Map<*, *> && (it["dependentTasksOutputFiles"] as String) == "reports/coverage/**/*"
- })
+ result.any {
+ it is Map<*, *> &&
+ (it["dependentTasksOutputFiles"] as String) == "reports/coverage/**/*"
+ })
// Should contain regular input files
assertTrue(result.any { it == "{projectRoot}/src/main.kt" })
@@ -334,7 +336,7 @@ class ProcessTaskUtilsTest {
// Verify we have the expected number of dependentTasksOutputFiles entries (4 outputs from 3
// tasks)
val dependentTasksOutputFilesCount =
- result.count { it is Map<*, *> && it.containsKey("dependentTasksOutputFiles") }
+ result.count { it is Map<*, *> && it.containsKey("dependentTasksOutputFiles") }
assertEquals(4, dependentTasksOutputFilesCount)
// Verify we have the expected number of regular input files (2)
@@ -351,24 +353,24 @@ class ProcessTaskUtilsTest {
// Create .gitignore file
val gitignore = java.io.File(project.rootDir, ".gitignore")
gitignore.writeText(
- """
- build
- .gradle
- *.log
- dist
- """
- .trimIndent())
+ """
+ build
+ .gradle
+ *.log
+ dist
+ """
+ .trimIndent())
val mainTask = project.tasks.register("mainTask").get()
// Add inputs with mixed types
val sourceFile = java.io.File("$workspaceRoot/src/main.kt") // Not ignored - should be input
- val buildFile =
- java.io.File("$workspaceRoot/build/classes/Main.class") // Ignored - should be
+ val buildFile = java.io.File("$workspaceRoot/build/classes/Main.class") // Ignored - should be
// dependentTasksOutputFiles
- val logFile = java.io.File("$workspaceRoot/app.log") // Ignored - should be dependentTasksOutputFiles
+ val logFile =
+ java.io.File("$workspaceRoot/app.log") // Ignored - should be dependentTasksOutputFiles
val configFile =
- java.io.File("$workspaceRoot/config/app.properties") // Not ignored - should be input
+ java.io.File("$workspaceRoot/config/app.properties") // Not ignored - should be input
mainTask.inputs.files(sourceFile, buildFile, logFile, configFile)
@@ -384,13 +386,15 @@ class ProcessTaskUtilsTest {
// Build file should be dependentTasksOutputFiles (matches gitignore)
assertTrue(
- result.any {
- it is Map<*, *> && (it["dependentTasksOutputFiles"] as String).contains("build")
- })
+ result.any {
+ it is Map<*, *> && (it["dependentTasksOutputFiles"] as String).contains("build")
+ })
// Log file should be dependentTasksOutputFiles (matches gitignore)
assertTrue(
- result.any { it is Map<*, *> && (it["dependentTasksOutputFiles"] as String).contains("log") })
+ result.any {
+ it is Map<*, *> && (it["dependentTasksOutputFiles"] as String).contains("log")
+ })
}
@Test
@@ -402,18 +406,18 @@ class ProcessTaskUtilsTest {
// Create .gitignore with common patterns
val gitignore = java.io.File(project.rootDir, ".gitignore")
gitignore.writeText(
- """
- target
- dist
- """
- .trimIndent())
+ """
+ target
+ dist
+ """
+ .trimIndent())
val mainTask = project.tasks.register("mainTask").get()
// Add inputs
val javaSource = java.io.File("$workspaceRoot/src/Main.java") // Not ignored
val compiledClass =
- java.io.File("$workspaceRoot/dist/production/Main.class") // Ignored (*.class pattern)
+ java.io.File("$workspaceRoot/dist/production/Main.class") // Ignored (*.class pattern)
val jarTarget = java.io.File("$workspaceRoot/dist/app.jar") // Ignored (target)
mainTask.inputs.files(javaSource, compiledClass, jarTarget)
@@ -425,14 +429,14 @@ class ProcessTaskUtilsTest {
assertTrue(result!!.any { it == "{projectRoot}/src/Main.java" })
assertTrue(
- result.any {
- it is Map<*, *> && (it["dependentTasksOutputFiles"] as String).contains("Main.class")
- })
+ result.any {
+ it is Map<*, *> && (it["dependentTasksOutputFiles"] as String).contains("Main.class")
+ })
assertTrue(
- result.any {
- it is Map<*, *> && (it["dependentTasksOutputFiles"] as String).contains("dist")
- })
+ result.any {
+ it is Map<*, *> && (it["dependentTasksOutputFiles"] as String).contains("dist")
+ })
}
}
@@ -488,6 +492,4 @@ class ProcessTaskUtilsTest {
it is Map<*, *> && (it["dependentTasksOutputFiles"] as String) == "build/classes/**/*"
})
}
-
-
}
Or Apply changes locally with:
npx nx-cloud apply-locally GUMs-ozn7
Apply fix locally with your editor ↗ View interactive diff ↗
🎓 To learn more about Self Healing CI, please visit nx.dev
Current Behavior
Currently we set dependentTasksOutputFiles based on the location of input files. Only files that are part of the build directory are included in that classification of inputs. Rather, dependent tasks output files should also include inputs that are gitignored.
Expected Behavior
Matched the gitignore classifier that maven uses within the Gradle code such that we can match input paths against gitignored files and directories. Also refactored the input setting function within the gradle plugin to be easier to read by untangling how the inputs are set.
Related Issue(s)
Fixes NXC-3156