Skip to content

Commit e886bca

Browse files
feat: add IntelliJ-based code formatting and git hooks (#15535)
1 parent a9ac7d4 commit e886bca

File tree

3 files changed

+141
-6
lines changed

3 files changed

+141
-6
lines changed

build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStylePlugin.groovy

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ import org.gradle.api.plugins.quality.CheckstylePlugin
3232
import org.gradle.api.plugins.quality.CodeNarc
3333
import org.gradle.api.plugins.quality.CodeNarcExtension
3434
import org.gradle.api.plugins.quality.CodeNarcPlugin
35+
import org.gradle.api.tasks.Copy
36+
import org.gradle.process.ExecSpec
37+
import org.apache.tools.ant.taskdefs.condition.Os
3538

3639
@CompileStatic
3740
class GrailsCodeStylePlugin implements Plugin<Project> {
@@ -49,6 +52,7 @@ class GrailsCodeStylePlugin implements Plugin<Project> {
4952
void apply(Project project) {
5053
initExtension(project)
5154
configureCodeStyle(project)
55+
registerFormattingTasks(project)
5256
doNotApplyStylingToTests(project)
5357
}
5458

@@ -108,8 +112,10 @@ class GrailsCodeStylePlugin implements Plugin<Project> {
108112
}
109113

110114
private static void doNotApplyStylingToTests(Project project) {
111-
project.tasks.named('checkstyleTest') {
112-
it.enabled = false // Do not check test sources at this time
115+
if (project.tasks.names.contains('checkstyleTest')) {
116+
project.tasks.named('checkstyleTest') {
117+
it.enabled = false // Do not check test sources at this time
118+
}
113119
}
114120

115121
project.afterEvaluate {
@@ -191,4 +197,88 @@ class GrailsCodeStylePlugin implements Plugin<Project> {
191197
)
192198
}
193199
}
200+
201+
private static void registerFormattingTasks(Project project) {
202+
if (project == project.rootProject) {
203+
project.tasks.register('installGitHooks', Copy) {
204+
it.group = 'verification'
205+
it.description = 'Installs the git pre-commit hook for automatic code formatting'
206+
it.from(project.rootProject.layout.projectDirectory.file('etc/hooks/pre-commit'))
207+
it.into(project.rootProject.layout.projectDirectory.dir('.git/hooks'))
208+
it.fileMode = 0755
209+
}
210+
}
211+
212+
project.tasks.register('formatCode') {
213+
it.group = 'verification'
214+
it.description = 'Formats Java and Groovy source files using the IntelliJ command line formatter'
215+
216+
it.doLast {
217+
String ideaHome = (project.findProperty('idea.home') ?: System.getenv('IDEA_HOME')) as String
218+
String executable = Os.isFamily(Os.FAMILY_WINDOWS) ? 'format.bat' : 'format.sh'
219+
File formatExec = null
220+
221+
if (ideaHome) {
222+
formatExec = new File(ideaHome, "bin/$executable")
223+
} else {
224+
// Try common paths on macOS
225+
if (Os.isFamily(Os.FAMILY_MAC)) {
226+
def commonPaths = [
227+
"/Applications/IntelliJ IDEA.app/Contents/bin/$executable",
228+
"/Applications/IntelliJ IDEA CE.app/Contents/bin/$executable"
229+
]
230+
for (path in commonPaths) {
231+
File f = new File(path)
232+
if (f.exists()) {
233+
formatExec = f
234+
break
235+
}
236+
}
237+
}
238+
239+
if (formatExec == null && !Os.isFamily(Os.FAMILY_WINDOWS)) {
240+
// On Linux/Mac, try to find 'idea' in PATH
241+
try {
242+
def out = new ByteArrayOutputStream()
243+
project.exec { ExecSpec exec ->
244+
exec.commandLine 'which', 'idea'
245+
exec.standardOutput = out
246+
exec.ignoreExitValue = true
247+
}
248+
def path = out.toString().trim()
249+
if (path) {
250+
formatExec = new File(new File(path).parentFile, executable)
251+
}
252+
} catch (Exception ignored) { }
253+
}
254+
}
255+
256+
if (formatExec == null || !formatExec.exists()) {
257+
project.logger.error("IntelliJ formatter executable not found.")
258+
project.logger.error("Please set 'idea.home' property or IDEA_HOME environment variable to your IntelliJ installation directory.")
259+
project.logger.error("Example: ./gradlew formatCode -Pidea.home=/Applications/IntelliJ\\ IDEA.app/Contents")
260+
throw new RuntimeException("IntelliJ formatter executable not found.")
261+
}
262+
263+
def filesToFormat = project.findProperty('formatFiles')
264+
def settingsFile = project.rootProject.file('.idea/codeStyles/Project.xml')
265+
266+
if (!settingsFile.exists()) {
267+
throw new RuntimeException("IntelliJ code style settings not found at ${settingsFile.absolutePath}")
268+
}
269+
270+
project.exec { ExecSpec exec ->
271+
exec.commandLine formatExec.absolutePath
272+
exec.args '-s', settingsFile.absolutePath
273+
exec.args '-mask', '*.java,*.groovy'
274+
exec.args '-r'
275+
if (filesToFormat) {
276+
exec.args((filesToFormat.toString()).split(','))
277+
} else {
278+
exec.args project.projectDir.absolutePath
279+
}
280+
}
281+
}
282+
}
283+
}
194284
}

build.gradle

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
* limitations under the License.
1616
*/
1717

18+
plugins {
19+
id 'org.apache.grails.gradle.grails-code-style'
20+
}
21+
1822
import java.time.Instant
1923
import java.time.LocalDate
2024
import java.time.ZoneOffset
@@ -27,10 +31,10 @@ import org.apache.tools.ant.taskdefs.condition.Os
2731
ext {
2832
isReproducibleBuild = System.getenv("SOURCE_DATE_EPOCH") != null
2933
buildInstant = java.util.Optional.ofNullable(System.getenv("SOURCE_DATE_EPOCH"))
30-
.filter(s -> !s.isEmpty())
31-
.map(Long::parseLong)
32-
.map(Instant::ofEpochSecond)
33-
.orElseGet(Instant::now)
34+
.filter { s -> !s.isEmpty() }
35+
.map { s -> Long.parseLong(s as String) }
36+
.map { l -> Instant.ofEpochSecond(l as Long) }
37+
.orElseGet { Instant.now() }
3438
formattedBuildDate = System.getenv("SOURCE_DATE_EPOCH") ?
3539
DateTimeFormatter.ISO_INSTANT.format(buildInstant) :
3640
DateTimeFormatter.ISO_DATE.format(LocalDate.ofInstant(buildInstant as Instant, ZoneOffset.UTC))

etc/hooks/pre-commit

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/bin/bash
2+
#
3+
# Licensed to the Apache Software Foundation (ASF) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The ASF licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# https://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing,
14+
# software distributed under the License is distributed on an
15+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
# KIND, either express or implied. See the License for the
17+
# specific language governing permissions and limitations
18+
# under the License.
19+
#
20+
21+
set -e
22+
23+
# Get staged files that are Groovy or Java
24+
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(groovy|java)$' || true)
25+
26+
if [ -n "$STAGED_FILES" ]; then
27+
echo "Formatting staged Groovy/Java files using IntelliJ formatter..."
28+
29+
# Convert newline-separated list to comma-separated for Gradle property
30+
FILES_COMMAS=$(echo "$STAGED_FILES" | tr '\n' ',' | sed 's/,$//')
31+
32+
# Run the Gradle formatting task
33+
./gradlew :formatCode -PformatFiles="$FILES_COMMAS"
34+
35+
# Re-stage the files in case they were modified by the formatter
36+
for FILE in $STAGED_FILES; do
37+
if [ -f "$FILE" ]; then
38+
git add "$FILE"
39+
fi
40+
done
41+
fi

0 commit comments

Comments
 (0)