Skip to content

Commit

Permalink
ci: add post-commit hook with license header checks and linting (#124)
Browse files Browse the repository at this point in the history
Add post-commit hook for automatically checking license headers / running detekt.

Add script for updating / adding copyright headers.

Add gradle task 'setupMusikus' for installing commit hooks as well as dedicated tasks for checking/fixing license headers.
  • Loading branch information
mipro98 authored Nov 13, 2024
1 parent c80fbec commit ea89f17
Show file tree
Hide file tree
Showing 8 changed files with 375 additions and 12 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
.externalNativeBuild
.cxx
local.properties
musikus.properties
122 changes: 111 additions & 11 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
@file:Suppress("UnstableApiUsage")

import io.gitlab.arturbosch.detekt.Detekt
import java.util.Properties
import java.util.Scanner

val properties = Properties()
file("$rootDir/build.properties").inputStream().use { properties.load(it) }
val importedVersionCode = properties["versionCode"] as String
val importedVersionName = properties["versionName"] as String
val commitHash = properties["commitHash"] as String
val buildProperties = Properties()
file("$rootDir/build.properties").inputStream().use { buildProperties.load(it) }
val importedVersionCode = buildProperties["versionCode"] as String
val importedVersionName = buildProperties["versionName"] as String
val commitHash = buildProperties["commitHash"] as String

val reportsPath = "$projectDir/build/reports"

Expand Down Expand Up @@ -43,6 +45,7 @@ android {
}

signingConfigs {
@Suppress("TooGenericExceptionCaught")
try {
create("release") {
storeFile = file(System.getenv("SIGNING_KEY_STORE_PATH"))
Expand Down Expand Up @@ -112,20 +115,55 @@ room {
schemaDirectory("$projectDir/schemas")
}

object DetektSettings {
const val VERSION = "1.23.6"
const val CONFIG_FILE = "config/detekt.yml" // Relative to the project root
const val BUILD_UPON_DEFAULT_CONFIG = true
const val REPORT_PATH = "lint" // Relative to the reports path
}

detekt {
// Version of detekt that will be used. When unspecified the latest detekt
// version found will be used. Override to stay on the same version.
toolVersion = "1.23.6"
toolVersion = DetektSettings.VERSION

// Point to your custom config defining rules to run, overwriting default behavior
config.setFrom("$projectDir/config/detekt.yml")
config.setFrom("$projectDir/${DetektSettings.CONFIG_FILE}")

// Applies the config files on top of detekt's default config file. `false` by default.
buildUponDefaultConfig = true
buildUponDefaultConfig = DetektSettings.BUILD_UPON_DEFAULT_CONFIG

// Specify the base path for file paths in the formatted reports.
// If not set, all file paths reported will be absolute file path.
basePath = "$reportsPath/lint"
basePath = "$reportsPath/${DetektSettings.REPORT_PATH}"
}

tasks.register<Detekt>("detektOnFiles") {
description = "Runs detekt on the changed Kotlin files."
version = DetektSettings.VERSION
setSource(files("/"))
config.setFrom("$projectDir/${DetektSettings.CONFIG_FILE}")
buildUponDefaultConfig = true
basePath = "$reportsPath/${DetektSettings.REPORT_PATH}"

doFirst {
// Step 1: Get the list of changed Kotlin files
val changedKotlinFiles =
System.getenv("CHANGED_FILES")?.split(":")?.filter {
it.endsWith(".kt") || it.endsWith(".kts")
} ?: emptyList()

// Step 2: Check if there are any Kotlin files changed
if (changedKotlinFiles.isEmpty()) {
println("No Kotlin files changed in the last commit, skipping Detekt")
include("build.gradle.kts") // Include the build file to avoid a no-source error
}

// Step 3: Include the changed Kotlin files in the detekt task
changedKotlinFiles.forEach {
include(it.removePrefix("app/"))
}
}
}

tasks.withType<Test> {
Expand All @@ -148,6 +186,68 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach
}
}

tasks.register("setupMusikus") {
group = "setup"
description = "Sets up the Musikus project with pre-commit hooks, copyright header, and IDE settings."

doFirst {
gradle.startParameter.consoleOutput = ConsoleOutput.Plain
}

doLast {
println("Setting up Musikus project...")

// Step 1: Execute the existing bash script to install the pre-commit hook
if (System.getProperty("os.name").lowercase().contains("win")) {
exec {
workingDir = file("$rootDir/tools/hooks")
commandLine("cmd", "/c", "setup_hooks.bat")
}
} else {
exec {
commandLine("bash", "$rootDir/tools/hooks/setup_hooks.sh")
}
}
println("Pre-commit hook installed.\n")

// Step 2: Query and store a name for the copyright header
val scanner = Scanner(System.`in`)
print("Enter your name for the copyright header: ")
System.out.flush() // Needed to ensure the prompt is displayed
val name = scanner.nextLine()
require(!name.isNullOrBlank()) { "Name must not be empty." }
val propertiesFile = file("$rootDir/musikus.properties")
propertiesFile.writeText("copyrightName=$name")
println("Name stored for copyright header: $name\n")
}
}

tasks.register("checkLicense") {
group = "verification"
description = "Checks if all files most in the HEAD commit have the correct license header."

doLast {
// Execute python script to check license headers
exec {
workingDir = file("$rootDir/tools")
commandLine("python", "check_license_headers.py")
}
}
}

tasks.register("fixLicense") {
group = "verification"
description = "Fixes the license header in all staged files."

doLast {
// Execute python script to update license headers
exec {
workingDir = file("$rootDir/tools")
commandLine("python", "fix_license_headers.py")
}
}
}

dependencies {
detektPlugins(libs.detekt.formatting)

Expand All @@ -170,7 +270,7 @@ dependencies {
implementation(libs.androidx.compose.animation.core)
implementation(libs.androidx.compose.animation.graphics)

//Foundation
// Foundation
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.foundation.layout)

Expand Down Expand Up @@ -206,7 +306,7 @@ dependencies {
// Immutable Lists
implementation(libs.kotlinx.collections.immutable)

//Dagger - Hilt
// Dagger - Hilt
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
Expand Down
4 changes: 3 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
android.nonTransitiveRClass=false
android.nonFinalResIds=false
android.nonFinalResIds=false
# Necessary for input prompts to be properly displayed in the console
org.gradle.console=plain
101 changes: 101 additions & 0 deletions tools/check_license_headers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
Copyright (c) 2024 Matthias Emde
"""

import os
import re
import subprocess
import sys
from pathlib import Path
import datetime

# ANSI escape codes for colors
RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RESET = '\033[0m'

# Get the project path
file_path = os.path.abspath(__file__)
project_path = os.path.dirname(file_path)
while '.git' not in os.listdir(project_path):
project_path = os.path.dirname(project_path)

# Get the current year
current_year = str(datetime.datetime.now().year)

# Define the license header patterns
xml_license_pattern = re.compile(r'<!--\n'
r' This Source Code Form is subject to the terms of the Mozilla Public\n'
r' License, v. 2.0. If a copy of the MPL was not distributed with this\n'
r' file, You can obtain one at https://mozilla.org/MPL/2.0/.\n'
r'\n'
r' Copyright \(c\) (' + current_year + r'|\d{4}-' + current_year + r') ([A-Za-z]+ [A-Za-z]+)(, [A-Za-z]+ [A-Za-z]+)*\n'
r'-->\n')

kt_license_pattern = re.compile(r'/\*\n'
r' \* This Source Code Form is subject to the terms of the Mozilla Public\n'
r' \* License, v. 2.0. If a copy of the MPL was not distributed with this\n'
r' \* file, You can obtain one at https://mozilla.org/MPL/2.0/.\n'
r' \*\n'
r' \* Copyright \(c\) (' + current_year + r'|\d{4}-' + current_year + r') ([A-Za-z]+ [A-Za-z]+)(, [A-Za-z]+ [A-Za-z]+)*\n'
r'('
r' \*\n'
r' \* Parts of this software are licensed under the MIT license\n'
r' \*\n'
r' \* Copyright \(c\) \d{4}, Javier Carbone, author ([A-Za-z]+ [A-Za-z]+)\n'
r'( \* Additions and modifications, author ([A-Za-z]+ [A-Za-z]+)\n)?'
r')?'
r' \*/\n'
r'\n')


# Function to fetch the files that were just commited to git
def get_committed_files():
result = subprocess.run(['git', 'diff', '--name-only', 'origin/main', 'HEAD'], capture_output=True, text=True)
files = result.stdout.splitlines()

return [f'{project_path}/{f}' for f in files]


# Function to check the license header in a file
def check_license_header(file_path):
try:
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read(500) # Read the first 500 characters
if file_path.suffix == '.xml':
if not xml_license_pattern.search(content):
return False
elif file_path.suffix == '.kt':
if not kt_license_pattern.search(content):
return False
return True
except Exception as e:
print(f"{RED}Error reading {file_path}: {e}{RESET}")
return False


def main():
all_files_have_correct_license_headers = True

# Get the staged files to check for license headers
files_to_check = get_committed_files()

# Check each file
for file_path in files_to_check:
path = Path(file_path)
if path.suffix in {'.xml', '.kt'} and not check_license_header(path):
print(f"{RED}License header missing or incorrect in {file_path}{RESET}")
all_files_have_correct_license_headers = False

if not all_files_have_correct_license_headers:
print(f"\n{YELLOW}Some files have missing or incorrect license headers. :(\nFix them manually or using the fixLicense gradle task and amend your commit.{RESET}")
else:
print(f"\n{GREEN}All files have correct license headers. :){RESET}")

if __name__ == "__main__":
main()
Loading

0 comments on commit ea89f17

Please sign in to comment.