From ecbfb0e66f34597e1d2aacb8de6e253ca530d82b Mon Sep 17 00:00:00 2001 From: hey-cube Date: Wed, 5 Nov 2025 18:50:03 +0900 Subject: [PATCH 1/4] Fix pre-commit hook path handling for projects in subdirectories When the Gradle project root differs from the Git repository root (e.g., Gradle is in a subdirectory like 'project/submodule/'), the pre-commit hooks generated by ktlint-gradle fail to properly format files because of path mismatches. This change fixes the issue by: - Using `git diff --relative=$gradleRootDirPrefix` to output paths relative to the Gradle project root - Adding proper file path prefix handling in the git add command when checking if files exist and staging them This ensures that when git outputs paths like "src/main/Foo.kt" (relative to project/submodule/), the hook correctly references them as "project/submodule/src/main/Foo.kt" when checking file existence and staging. Fixes #374 --- .../kotlin/org/jlleitschuh/gradle/ktlint/GitHook.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/GitHook.kt b/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/GitHook.kt index 4b4368b1e..c0e2918ed 100644 --- a/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/GitHook.kt +++ b/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/GitHook.kt @@ -44,16 +44,18 @@ private fun generateGitCommand( ): String = if (gradleRootDirPrefix.isEmpty()) { "git --no-pager diff --name-status --no-color --cached" } else { - "git --no-pager diff --name-status --no-color --cached -- $gradleRootDirPrefix/" + "git --no-pager diff --name-status --no-color --cached --relative=$gradleRootDirPrefix -- $gradleRootDirPrefix/" } private fun postCheck( - shouldUpdateCommit: Boolean + shouldUpdateCommit: Boolean, + gradleRootDirPrefix: String ): String = if (shouldUpdateCommit) { + val filePrefix = if (gradleRootDirPrefix.isNotEmpty()) "$gradleRootDirPrefix/" else "" """ echo "${'$'}CHANGED_FILES" | while read -r file; do - if [ -f ${'$'}file ]; then - git add ${'$'}file + if [ -f $filePrefix${'$'}file ]; then + git add $filePrefix${'$'}file fi done """ @@ -91,7 +93,7 @@ internal fun generateGitHook( gradle_command_exit_code=${'$'}? echo "Completed ktlint run." - ${postCheck(shouldUpdateCommit)} + ${postCheck(shouldUpdateCommit, gradleRootDirPrefix)} if [ -s ${'$'}diff ]; then git apply --ignore-whitespace ${'$'}diff From cfc1aa1abf06ec69c618f002c86b6d6d698e67e7 Mon Sep 17 00:00:00 2001 From: hey-cube Date: Wed, 5 Nov 2025 19:43:19 +0900 Subject: [PATCH 2/4] Add unit tests for subdirectory pre-commit hook path handling Added four new test cases to verify the fix for pre-commit hook path handling when Gradle projects are in subdirectories: 1. checkHookUsesRelativeFlagInSubdirectory: Verifies that check hooks use --relative flag when Gradle is in a subdirectory 2. formatHookUsesRelativeFlagInSubdirectory: Verifies that format hooks use --relative flag and correctly prefix file paths in git add commands 3. checkHookDoesNotUseRelativeFlagAtGitRoot: Verifies that check hooks do not use --relative flag when Gradle is at git root 4. formatHookDoesNotUseRelativeFlagAtGitRoot: Verifies that format hooks do not use --relative flag or path prefixes when Gradle is at git root --- .../gradle/ktlint/GitHookTasksTest.kt | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/GitHookTasksTest.kt b/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/GitHookTasksTest.kt index dd158274d..98c79f859 100644 --- a/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/GitHookTasksTest.kt +++ b/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/GitHookTasksTest.kt @@ -153,6 +153,75 @@ class GitHookTasksTest : AbstractPluginTest() { } } + @DisplayName("Check hook should use --relative flag when Gradle project is in subdirectory") + @CommonTest + fun checkHookUsesRelativeFlagInSubdirectory(gradleVersion: GradleVersion) { + val gradleRoot = projectRoot.resolve("project/submodule/").also { it.mkdirs() } + val gitDir = projectRoot.initGit() + + project(gradleVersion, projectPath = gradleRoot) { + build(":$INSTALL_GIT_HOOK_CHECK_TASK") { + assertThat(task(":$INSTALL_GIT_HOOK_CHECK_TASK")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + val hookText = gitDir.preCommitGitHook().readText() + assertThat(hookText).contains("--relative=project/submodule") + assertThat(hookText).contains("-- project/submodule/") + } + } + } + + @DisplayName("Format hook should use --relative flag and correct file paths when Gradle project is in subdirectory") + @CommonTest + fun formatHookUsesRelativeFlagInSubdirectory(gradleVersion: GradleVersion) { + val gradleRoot = projectRoot.resolve("project/submodule/").also { it.mkdirs() } + val gitDir = projectRoot.initGit() + + project(gradleVersion, projectPath = gradleRoot) { + build(":$INSTALL_GIT_HOOK_FORMAT_TASK") { + assertThat(task(":$INSTALL_GIT_HOOK_FORMAT_TASK")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + val hookText = gitDir.preCommitGitHook().readText() + // Should use --relative flag for git diff + assertThat(hookText).contains("--relative=project/submodule") + assertThat(hookText).contains("-- project/submodule/") + // Should prefix file paths in git add command + assertThat(hookText).contains("if [ -f project/submodule/\$file ]; then") + assertThat(hookText).contains("git add project/submodule/\$file") + } + } + } + + @DisplayName("Check hook should not use --relative flag when Gradle project is at git root") + @CommonTest + fun checkHookDoesNotUseRelativeFlagAtGitRoot(gradleVersion: GradleVersion) { + project(gradleVersion) { + val gitDir = projectPath.initGit() + + build(":$INSTALL_GIT_HOOK_CHECK_TASK") { + assertThat(task(":$INSTALL_GIT_HOOK_CHECK_TASK")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + val hookText = gitDir.preCommitGitHook().readText() + // Should NOT use --relative flag when at git root + assertThat(hookText).doesNotContain("--relative=") + } + } + } + + @DisplayName("Format hook should not use --relative flag when Gradle project is at git root") + @CommonTest + fun formatHookDoesNotUseRelativeFlagAtGitRoot(gradleVersion: GradleVersion) { + project(gradleVersion) { + val gitDir = projectPath.initGit() + + build(":$INSTALL_GIT_HOOK_FORMAT_TASK") { + assertThat(task(":$INSTALL_GIT_HOOK_FORMAT_TASK")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + val hookText = gitDir.preCommitGitHook().readText() + // Should NOT use --relative flag when at git root + assertThat(hookText).doesNotContain("--relative=") + // Should NOT prefix file paths + assertThat(hookText).contains("if [ -f \$file ]; then") + assertThat(hookText).doesNotContain("if [ -f /\$file ]; then") + } + } + } + @DisplayName("Check hook should not include files into git commit") @CommonTest fun checkHookShouldNotIncludeFilesIntoGitCommit(gradleVersion: GradleVersion) { From 225f9e1460161e65ff4f7351dda5a75b404b06d1 Mon Sep 17 00:00:00 2001 From: hey-cube Date: Tue, 11 Nov 2025 14:28:10 +0900 Subject: [PATCH 3/4] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1dcb234c..3beb92aea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +- fix [#374](https://github.com/JLLeitschuh/ktlint-gradle/issues/374): fix pre-commit hook path handling for projects in subdirectories [#996](https://github.com/JLLeitschuh/ktlint-gradle/pull/996) + ## [14.0.1] - 2025-11-10 - Update build to work with gradle 9.1 and Java 25 [#962](https://github.com/JLLeitschuh/ktlint-gradle/pull/962) From 4efc1a180c61b6d5a71aa3c97f5919691f36a27c Mon Sep 17 00:00:00 2001 From: hey-cube Date: Tue, 11 Nov 2025 14:49:15 +0900 Subject: [PATCH 4/4] Fix path separator for Windows in git hook generation The gradleRootDirPrefix was using platform-specific path separators (backslash on Windows), but git hook scripts always require forward slashes. This caused tests to fail on Windows. Changed to always use forward slashes by replacing File.separator with "/" when constructing gradleRootDirPrefix. Fixes test failures on windows-latest CI runners. --- plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/GitHook.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/GitHook.kt b/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/GitHook.kt index c0e2918ed..d10fb48fa 100644 --- a/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/GitHook.kt +++ b/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/GitHook.kt @@ -190,7 +190,7 @@ open class KtlintInstallGitHookTask @Inject constructor( gitHookFile.createNewFile() gitHookFile.setExecutable(true) } - val gradleRootDirPrefix = File(rootDirectory.get()).relativeTo(repo.workTree).path + val gradleRootDirPrefix = File(rootDirectory.get()).relativeTo(repo.workTree).path.replace(File.separator, "/") if (gitHookFile.length() == 0L) { gitHookFile.writeText(