Skip to content
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

Add task to generate pre commit hook that formats staged changes #270

Open
wants to merge 23 commits into
base: main
Choose a base branch
from

Conversation

gpopides
Copy link

🚀 Description

I have added 2 tasks. 1 is intented to be used by the user (ktfmtGeneratePreCommitHook) and 1 internal (ktfmtPreCommitHook).

ktfmtPreCommitHook Acts pretty much the same with the example in the readme. The pre commit hook will use this task to format staged kotlin files. I have added this task in order to avoid asking the user to add the pre commit example that is present in the readme.

To generate the file, the user needs to run gradlew ktfmtGeneratePreCommitHook

The hook will be generated if :

  1. There is a git directory in the root directory of the project
  2. There isnt an existing pre-commit hook in the hooks directory

📄 Motivation and Context

This PR tries to address this issue about generating a git pre commit hook for running ktformat on commit

🧪 How Has This Been Tested?

Integration tests + manual tests on a different project.

Tested on Arch linux with Kotlin 2.0.0-Beta4 and Gradle 8.6

📦 Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

✅ Checklist

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.

@cortinico
Copy link
Owner

Thanks for sending this over @gpopides
The unit tests are failing, can we make them green?

@gpopides
Copy link
Author

Thanks for sending this over @gpopides The unit tests are failing, can we make them green?

Sure i will take a look again tomorrow. Locally they are passing but i will make sure it works on CI too!

@cortinico
Copy link
Owner

CI is still red apparentyl

@gpopides
Copy link
Author

Indeed, the last failure was because of a detekt issue which i pushed a change to fix. This makes the macos and ubuntu builds pass but the windows one still fails. Looking at the artifact i see:

Execution failed for task ':ktfmtGenerateGitPreCommitHook'.
  > Cannot access input property 'source' of task ':ktfmtGenerateGitPreCommitHook'. Accessing unreadable inputs or outputs is not supported. Declare the task as untracked by using Task.doNotTrackState(). For more information, please refer to https://docs.gradle.org/8.6/userguide/incremental_build.html#sec:disable-state-tracking in the Gradle documentation.
   > Failed to create MD5 hash for file content.

Unfortunately i don't have a windows machine nor i am that familiar with it. Do you happen to have any insights on why this might happen?

Thanks

hooksDirectory.createDirectory()
}

val hookFile = File("${hooksDirectory}/pre-commit")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

Comment on lines 124 to 128
project.tasks.register("ktfmtPreCommitHook", KtfmtFormatTask::class.java) {
it.source = project.fileTree(project.projectDir)
it.doNotTrackState("Task does not require gradle cache")
it.include("**/*.kt")
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels wrong as you're basically recreating a format task that will reformat everything no?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea behind this is that is based on the example in the readme which mentions that we can create a task to be used as pre commit. Here i tried to handle the case where the user has not added the specific task. I can remove it and check if the task (mentioned in the readme) is defined and use that. Do you think that's a better approach?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see. So first you can just use ktfmtFormat. We don't need an extra task.
The problem is that it won't work well for all the source set tasks.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for clarification. Just a note, trying to use ktfmtFormat does not accept specific files --include-only. What am i missing?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that it won't work well for all the source set tasks.

Yeah that's what I meant here.
Say you're applying ktfmt to a KMP project, you will have those tasks:

ktfmtFormatKmpAndroidTest - Run Ktfmt formatter validation for sourceSet 'kmp androidTest' on project 'example-kmp'
ktfmtFormatKmpAndroidTestDebug - Run Ktfmt formatter validation for sourceSet 'kmp androidTestDebug' on project 'example-kmp'
ktfmtFormatKmpAndroidTestRelease - Run Ktfmt formatter validation for sourceSet 'kmp androidTestRelease' on project 'example-kmp'
ktfmtFormatKmpCommonMain - Run Ktfmt formatter validation for sourceSet 'kmp commonMain' on project 'example-kmp'
ktfmtFormatKmpCommonTest - Run Ktfmt formatter validation for sourceSet 'kmp commonTest' on project 'example-kmp'
ktfmtFormatKmpDebug - Run Ktfmt formatter validation for sourceSet 'kmp debug' on project 'example-kmp'
ktfmtFormatKmpJvmMain - Run Ktfmt formatter validation for sourceSet 'kmp jvmMain' on project 'example-kmp'
ktfmtFormatKmpJvmTest - Run Ktfmt formatter validation for sourceSet 'kmp jvmTest' on project 'example-kmp'
ktfmtFormatKmpMain - Run Ktfmt formatter validation for sourceSet 'kmp main' on project 'example-kmp'
ktfmtFormatKmpRelease - Run Ktfmt formatter validation for sourceSet 'kmp release' on project 'example-kmp'
ktfmtFormatKmpTest - Run Ktfmt formatter validation for sourceSet 'kmp test' on project 'example-kmp'
ktfmtFormatKmpTestDebug - Run Ktfmt formatter validation for sourceSet 'kmp testDebug' on project 'example-kmp'
ktfmtFormatKmpTestFixtures - Run Ktfmt formatter validation for sourceSet 'kmp testFixtures' on project 'example-kmp'
ktfmtFormatKmpTestFixturesDebug - Run Ktfmt formatter validation for sourceSet 'kmp testFixturesDebug' on project 'example-kmp'
ktfmtFormatKmpTestFixturesRelease - Run Ktfmt formatter validation for sourceSet 'kmp testFixturesRelease' on project 'example-kmp'
ktfmtFormatKmpTestRelease - Run Ktfmt formatter validation for sourceSet 'kmp testRelease' on project 'example-kmp'

The pre-commit hook will have to invoke those tasks and pass the --include-only flags to those

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alternative would be to use a Gradle Property, that you can set inside the commit source code as env variable to pass the --include-only values

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean setting for example ktfmtFormatKmpMain,ktfmtFormatKmpTestFixtures as the env variable and passing the include files to these ?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope. I mean that we can instruct each task to read a Gradle property called ktfmt.gradle.includeOnly. And then you'll be able to set an env variable called ORG_GRADLE_PROJECT_ktfmt.gradle.includeOnly=... to specify the files to include.

More on this here:
https://docs.gradle.org/current/userguide/project_properties.html#sec:project_properties

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay now its more clear to me. Just to verify, that will require changing createFormatTask to check this variable first and use it and if not present, use the existing defaultIncludes.

Which means that if we set this variable as you have said, we can use just ktfmtFormat

Is that correct?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah exactly

import org.gradle.workers.WorkerExecutor

/** ktfmt-gradle Git hook task. Creates a git hook that runs ktfmt on git commit */
abstract class CreateGitPreCommitHookTask : SourceTask() {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
abstract class CreateGitPreCommitHookTask : SourceTask() {
abstract class CreateGitPreCommitHookTask : DefaultTask() {

@gpopides This is the cause of the Windows failure. This is not a Source Task as you're not populating the source property. Windows is failing while attepting to MD5 the source field which is null here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for the feedback, i will change that.

Copy link
Owner

@cortinico cortinico left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the amazing work so far @gpopides

@gpopides
Copy link
Author

I have added the include option as you suggested and have rebased your newest changes. Is there anything else that i have missed before going forward with this PR?

Comment on lines +46 to +50
* **Kotlin 1.4+**. In order to reformat Kotlin 1.4 code, you need run on **Gradle to 6.8+** (This is due to Gradle 6.7
embedding Kotlin 1.3.x - See [#12660](https://github.com/gradle/gradle/issues/12660)).

* **Android**. `ktfmt-gradle` relies on features from **Android Gradle Plugin 4.1+**. So make sure you bump AGP before applying this plugin.
* **Android**. `ktfmt-gradle` relies on features from **Android Gradle Plugin 4.1+**. So make sure you bump AGP before
applying this plugin.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you amend this and other unrelated changes to the README?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, its because running the formatter.

@@ -132,10 +135,13 @@ tasks.register<KtfmtFormatTask>("ktfmtPrecommit") {
}
```

You can then invoke the task with `--include-only` and a comma separated list of relative path of files:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we remove this sentence, then we would have to remove the entire section above

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are probably right, my only thought is that you can still use a custom task without generating the hook. So i believe i should bring it back instead

Comment on lines 114 to 119
// check if specific files are specified to be included. If not, use the default includes
val includes =
(project.findProperty("ktfmt.gradle.includeOnly") as String?)
?.split(",")
?.filter { file -> file.isNotEmpty() }
?.takeIf { files -> files.isNotEmpty() } ?: KtfmtPlugin.defaultIncludes
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought I left a comment here but I apparently I haven't (couldn't find it anymore).

My question is: have you tried this? As setIncludes below expects a glob, and not a list of files. If you see:

val defaultIncludes = listOf("**/*.kt", "**/*.kts")

so I'm unsure how it ends up working if you pass a list of files.


// check if specific files are specified to be included. If not, use the default includes
val includes =
(project.findProperty("ktfmt.gradle.includeOnly") as String?)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're not handling the interaction between this Gradle property AND the use also specifying a --include-only.

I think instead of passing the setIncludes here, we should read the Gradle Preperty inside KtfmtBaseTask. So the logic would be:

  1. If you specified a --include-only flag, we honor it.
  2. If not, if you specified the "ktfmt.gradle.includeOnly" property, we honor it
  3. Otherwise we just process the default includes

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, makes sense. I will move it to the task while keeping the same significance order

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants