From dab21834b716fcf64819af38626c59510bb7c862 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Mon, 17 Jun 2019 19:00:21 +0200 Subject: [PATCH] Add "installGitPreCommitHook" sub command. (#487) Move old one (`--install-git-pre-commit-hook`) option to be a ktlint subcommand - new syntax: `ktlint installGitPreCommitHook`. Old one `--install-git-pre-commit-hook` is still working, but deprecated. Signed-off-by: Yahor Berdnikau --- README.md | 2 +- .../main/kotlin/com/pinterest/ktlint/Main.kt | 74 ++++++---------- .../pinterest/ktlint/internal/ByteArrayExt.kt | 9 ++ .../ktlint/internal/CommandLineExt.kt | 20 +++++ .../internal/GitPreCommitHookSubCommand.kt | 84 +++++++++++++++++++ 5 files changed, 138 insertions(+), 51 deletions(-) create mode 100644 ktlint/src/main/kotlin/com/pinterest/ktlint/internal/ByteArrayExt.kt create mode 100644 ktlint/src/main/kotlin/com/pinterest/ktlint/internal/CommandLineExt.kt create mode 100644 ktlint/src/main/kotlin/com/pinterest/ktlint/internal/GitPreCommitHookSubCommand.kt diff --git a/README.md b/README.md index b8c7afcfd7..ac3d87f809 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ $ ktlint --reporter=plain --reporter=checkstyle,output=ktlint-report-in-checksty # install git hook to automatically check files for style violations on commit # use --install-git-pre-push-hook if you wish to run ktlint on push instead -$ ktlint --install-git-pre-commit-hook +$ ktlint installGitPreCommitHook ``` > on Windows you'll have to use `java -jar ktlint ...`. diff --git a/ktlint/src/main/kotlin/com/pinterest/ktlint/Main.kt b/ktlint/src/main/kotlin/com/pinterest/ktlint/Main.kt index 388535696f..f2d59a0d40 100644 --- a/ktlint/src/main/kotlin/com/pinterest/ktlint/Main.kt +++ b/ktlint/src/main/kotlin/com/pinterest/ktlint/Main.kt @@ -11,19 +11,20 @@ import com.pinterest.ktlint.core.RuleExecutionException import com.pinterest.ktlint.core.RuleSet import com.pinterest.ktlint.core.RuleSetProvider import com.pinterest.ktlint.internal.EditorConfig +import com.pinterest.ktlint.internal.GitPreCommitHookSubCommand import com.pinterest.ktlint.internal.IntellijIDEAIntegration import com.pinterest.ktlint.internal.KtlintVersionProvider import com.pinterest.ktlint.internal.MavenDependencyResolver +import com.pinterest.ktlint.internal.hex +import com.pinterest.ktlint.internal.printHelpOrVersionUsage import com.pinterest.ktlint.test.DumpAST import java.io.File import java.io.FileNotFoundException import java.io.IOException import java.io.PrintStream -import java.math.BigInteger import java.net.URLDecoder import java.nio.file.Path import java.nio.file.Paths -import java.security.MessageDigest import java.util.ArrayList import java.util.Arrays import java.util.LinkedHashMap @@ -55,11 +56,25 @@ import picocli.CommandLine.Parameters fun main(args: Array) { val ktlintCommand = KtlintCommandLine() val commandLine = CommandLine(ktlintCommand) - commandLine.parseArgs(*args) - when { - commandLine.isUsageHelpRequested -> commandLine.usage(System.out, CommandLine.Help.Ansi.OFF) - commandLine.isVersionHelpRequested -> commandLine.printVersionHelp(System.out, CommandLine.Help.Ansi.OFF) - else -> ktlintCommand.run() + .addSubcommand(GitPreCommitHookSubCommand.COMMAND_NAME, GitPreCommitHookSubCommand()) + val parseResult = commandLine.parseArgs(*args) + + commandLine.printHelpOrVersionUsage() + + if (parseResult.hasSubcommand()) { + handleSubCommand(commandLine, parseResult) + } else { + ktlintCommand.run() + } +} + +fun handleSubCommand( + commandLine: CommandLine, + parseResult: CommandLine.ParseResult +) { + when (val subCommand = parseResult.subcommand().commandSpec().userObject()) { + is GitPreCommitHookSubCommand -> subCommand.run() + else -> commandLine.usage(System.out, CommandLine.Help.Ansi.OFF) } } @@ -104,7 +119,7 @@ class KtlintCommandLine { names = ["--android", "-a"], description = ["Turn on Android Kotlin Style Guide compatibility"] ) - private var android: Boolean = false + var android: Boolean = false // todo: make it a command in 1.0.0 (it's too late now as we might interfere with valid "lint" patterns) @Option( @@ -139,12 +154,6 @@ class KtlintCommandLine { ) private var format: Boolean = false - @Option( - names = ["--install-git-pre-commit-hook"], - description = ["Install git hook to automatically check files for style violations on commit"] - ) - private var installGitPreCommitHook: Boolean = false - @Option( names = ["--install-git-pre-push-hook"], description = ["Install git hook to automatically check files for style violations before push"] @@ -248,12 +257,6 @@ class KtlintCommandLine { private fun File.location() = if (relative) this.toRelativeString(File(workDir)) else this.path fun run() { - if (installGitPreCommitHook) { - installGitPreCommitHook() - if (!apply) { - exitProcess(0) - } - } if (installGitPrePushHook) { installGitPrePushHook() if (!apply) { @@ -539,33 +542,6 @@ class KtlintCommandLine { .asSequence() .map(Path::toFile) - private fun installGitPreCommitHook() { - if (!File(".git").isDirectory) { - System.err.println( - ".git directory not found. " + - "Are you sure you are inside project root directory?" - ) - exitProcess(1) - } - val hooksDir = File(".git", "hooks") - hooksDir.mkdirsOrFail() - val preCommitHookFile = File(hooksDir, "pre-commit") - val expectedPreCommitHook = - ClassLoader.getSystemClassLoader() - .getResourceAsStream("ktlint-git-pre-commit-hook${if (android) "-android" else ""}.sh").readBytes() - // backup existing hook (if any) - val actualPreCommitHook = try { preCommitHookFile.readBytes() } catch (e: FileNotFoundException) { null } - if (actualPreCommitHook != null && !actualPreCommitHook.isEmpty() && !Arrays.equals(actualPreCommitHook, expectedPreCommitHook)) { - val backupFile = File(hooksDir, "pre-commit.ktlint-backup." + hex(actualPreCommitHook)) - System.err.println(".git/hooks/pre-commit -> $backupFile") - preCommitHookFile.copyTo(backupFile, overwrite = true) - } - // > .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit - preCommitHookFile.writeBytes(expectedPreCommitHook) - preCommitHookFile.setExecutable(true) - System.err.println(".git/hooks/pre-commit installed") - } - private fun installGitPrePushHook() { if (!File(".git").isDirectory) { System.err.println( @@ -583,7 +559,7 @@ class KtlintCommandLine { // backup existing hook (if any) val actualPrePushHook = try { prePushHookFile.readBytes() } catch (e: FileNotFoundException) { null } if (actualPrePushHook != null && !actualPrePushHook.isEmpty() && !Arrays.equals(actualPrePushHook, expectedPrePushHook)) { - val backupFile = File(hooksDir, "pre-push.ktlint-backup." + hex(actualPrePushHook)) + val backupFile = File(hooksDir, "pre-push.ktlint-backup." + actualPrePushHook.hex) System.err.println(".git/hooks/pre-push -> $backupFile") prePushHookFile.copyTo(backupFile, overwrite = true) } @@ -629,8 +605,6 @@ class KtlintCommandLine { System.err.println("(if you experience any issues please report them at https://github.com/pinterest/ktlint)") } - private fun hex(input: ByteArray) = BigInteger(MessageDigest.getInstance("SHA-256").digest(input)).toString(16) - // a complete solution would be to implement https://www.gnu.org/software/bash/manual/html_node/Tilde-Expansion.html // this implementation takes care only of the most commonly used case (~/) private fun expandTilde(path: String) = path.replaceFirst(Regex("^~"), System.getProperty("user.home")) diff --git a/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/ByteArrayExt.kt b/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/ByteArrayExt.kt new file mode 100644 index 0000000000..20c8cd1e9b --- /dev/null +++ b/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/ByteArrayExt.kt @@ -0,0 +1,9 @@ +package com.pinterest.ktlint.internal + +import java.math.BigInteger +import java.security.MessageDigest + +/** + * Generate hex string for given [ByteArray] content. + */ +internal val ByteArray.hex get() = BigInteger(MessageDigest.getInstance("SHA-256").digest(this)).toString(16) diff --git a/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/CommandLineExt.kt b/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/CommandLineExt.kt new file mode 100644 index 0000000000..9f56696d5a --- /dev/null +++ b/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/CommandLineExt.kt @@ -0,0 +1,20 @@ +package com.pinterest.ktlint.internal + +import kotlin.system.exitProcess +import picocli.CommandLine + +/** + * Check if user requested either help or version options, if yes - print it + * and exit process with [exitCode] exit code. + */ +internal fun CommandLine.printHelpOrVersionUsage( + exitCode: Int = 0 +) { + if (isUsageHelpRequested) { + usage(System.out, CommandLine.Help.Ansi.OFF) + exitProcess(exitCode) + } else if (isVersionHelpRequested) { + printVersionHelp(System.out, CommandLine.Help.Ansi.OFF) + exitProcess(exitCode) + } +} diff --git a/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/GitPreCommitHookSubCommand.kt b/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/GitPreCommitHookSubCommand.kt new file mode 100644 index 0000000000..e0a5b11216 --- /dev/null +++ b/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/GitPreCommitHookSubCommand.kt @@ -0,0 +1,84 @@ +package com.pinterest.ktlint.internal + +import com.pinterest.ktlint.KtlintCommandLine +import java.io.File +import kotlin.system.exitProcess +import picocli.CommandLine + +@CommandLine.Command( + description = [ + "Install git hook to automatically check files for style violations on commit", + "Usage of \"--install-git-pre-commit-hook\" command line option is deprecated!" + ], + aliases = ["--install-git-pre-commit-hook"], + mixinStandardHelpOptions = true, + versionProvider = KtlintVersionProvider::class +) +class GitPreCommitHookSubCommand : Runnable { + @CommandLine.ParentCommand + private lateinit var ktlintCommand: KtlintCommandLine + + @CommandLine.Spec + private lateinit var commandSpec: CommandLine.Model.CommandSpec + + override fun run() { + commandSpec.commandLine().printHelpOrVersionUsage() + + val gitHooksDir = resolveGitHooksDir() + val preCommitHookFile = gitHooksDir.resolve("pre-commit") + val preCommitHook = loadGitPreCommitHookTemplate() + + if (preCommitHookFile.exists()) { + backupExistingPreCommitHook(gitHooksDir, preCommitHookFile, preCommitHook) + } + + // > .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit + preCommitHookFile.writeBytes(preCommitHook) + preCommitHookFile.setExecutable(true) + println(".git/hooks/pre-commit installed") + } + + private fun resolveGitHooksDir(): File { + val gitDir = File(".git") + if (!gitDir.isDirectory) { + System.err.println( + ".git directory not found. Are you sure you are inside project root directory?" + ) + exitProcess(1) + } + + val hooksDir = gitDir.resolve("hooks") + if (!hooksDir.exists() && !hooksDir.mkdir()) { + System.err.println("Failed to create .git/hooks folder") + exitProcess(1) + } + + return hooksDir + } + + private fun loadGitPreCommitHookTemplate(): ByteArray = ClassLoader + .getSystemClassLoader() + .getResourceAsStream( + "ktlint-git-pre-commit-hook${if (ktlintCommand.android) "-android" else ""}.sh" + ).use { it.readBytes() } + + private fun backupExistingPreCommitHook( + hooksDir: File, + preCommitHookFile: File, + expectedPreCommitHook: ByteArray + ) { + // backup existing hook (if any) + val actualPreCommitHook = preCommitHookFile.readBytes() + if (actualPreCommitHook.isNotEmpty() && + !actualPreCommitHook.contentEquals(expectedPreCommitHook) + ) { + val backupFile = hooksDir.resolve("pre-commit.ktlint-backup.${actualPreCommitHook.hex}") + println(".git/hooks/pre-commit -> $backupFile") + preCommitHookFile.copyTo(backupFile, overwrite = true) + } + } + + companion object { + const val COMMAND_NAME = "installGitPreCommitHook" + } +}