17 |
18 | true
19 | true
20 | false
21 |
22 |
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Christoffer Mouritzen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Changed Projects Task Plugin
2 | A Gradle plugin to run a user defined task on changed projects (modules) and their dependent projects (modules) based on git changes.
3 | This is based on either the HEAD commit, a specific commit id or even a commit range.
4 |
5 | ## Installation
6 | Recommended way is to apply the plugin to the root `build.gradle` in the `plugins` block
7 | ```groovy
8 | plugins {
9 | id 'io.github.crimix.changed-projects-task' version 'VERSION'
10 | }
11 | ```
12 | and then configure the plugin using the following block still in the root `build.gradle`
13 | ```groovy
14 | changedProjectsTask {
15 | taskToRun = "test" //One of the main use cases of the plugin
16 | alwaysRunProject = [
17 | ":some-other-project"
18 | ]
19 | affectsAllRegex = [
20 | ~'build.gradle$' //Changes to the root build.gradle affects all projects
21 | ]
22 | ignoredRegex = [
23 | ~'^.*([.]css|[.]html)$' //Ignore changes to front-end files
24 | ]
25 | }
26 | ```
27 |
28 | ## Configuration
29 | As seen above, there are a few different configuration options available
30 |
31 | | **Option** | **Explanation** |
32 | |-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
33 | | `debugLogging` | Is default false and can be left out. If true will print details during plugin configuration and execution. |
34 | | `taskToRun` | A name of a task to run on changed projects. |
35 | | `alwaysRunProject` | A set of string for project paths starting with `:` that will be run always when there is a not ignored changed file. |
36 | | `neverRunProject` | A set of string for project paths starting with `:` that will never be run, even it is changed or `affectsAllRegex` has been evaluated to true. |
37 | | `affectsAllRegex` | A set of regexes that if any file matches will cause the `taskToRun` to be executed for all projects. |
38 | | `ignoredRegex` | A set of regexes for files that are ignored when evaluating if any project has changed. |
39 | | `changedProjectsMode` | A string that denotes which mode the plugin is running in, either `ONLY_DIRECTLY` or `INCLUDE_DEPENDENTS`.
`INCLUDE_DEPENDENTS` is the default and causes the `taskToRun` to be executed for project that are changed and projects that depends on those changed.
`ONLY_DIRECTLY` causes the `taskToRun` to only be executed for projects that are changed and only those. |
40 |
41 | ## Usage
42 | To use the added `runTaskForChangedProjects` from this plugin you need to run it with a few parameters.
43 | The minimum required is `-PchangedProjectsTask.run` or `-PchangedProjectsTask.runCommandLine` which enables the plugin to run (See the differance below).
44 | Depending on usage, it might also be a good idea to run it with `--continue` such that all dependent tasks are run, instead of fail-fast behaviour.
45 | Then there are four other optional parameters `-PchangedProjectsTask.taskToRun`, `-PchangedProjectsTask.commit`, `-PchangedProjectsTask.prevCommit` and `-PchangedProjectsTask.compareMode`.
46 |
47 | - `-PchangedProjectsTask.run` informs the plugin to do its work, analysing changes files and which module it belongs to. It will then create a `dependsOn` relation between its own task and all modules `taskToRun`.
48 | To stop the tasks from running it guards it using an `onlyIf`, this `onlyIf` is only put on the `taskToRun` and not other dependices of that task.
49 |
50 |
51 | - `-PchangedProjectsTask.runCommandLine` informs the plugin to do its work, analysing changes files and which module it belongs to. Instead of using `dependsOn`, this one will invoke the `taskToRun` on the default commandline of the system.
52 | This means it would be just like if you manually called the task for the specific module changed. To supply commandline arguments (including `-D` and `-P` arguments), use `-PchangedProjectsTask.commandLineArgs`
53 |
54 |
55 | - `-PchangedProjectsTask.taskToRun` lets you configure the task to run on demand. If it is provided it takes priority over the task configured in the above-mentioned table.
56 |
57 |
58 | - `-PchangedProjectsTask.commit` is to configure which ref to use in the git diff.
59 | - If this is specified with `-PchangedProjectsTask.prevCommit` it creates a range to use in diff.
60 | By calling the following `git diff --name-only prevCommit~ commit`.
61 | - If it is specified with `-PchangedProjectsTask.prevCommit`, it uses the following command instead
62 | `git diff --name-only commit~ commit`
63 |
64 |
65 | - `-PchangedProjectsTask.prevCommit` is to configure which previous ref to use in the git diff.
66 | This cannot be used without also using `-PchangedProjectsTask.commit`
67 |
68 |
69 | - `-PchangedProjectsTask.compareMode` is used to change which mode it uses to compare.
70 | The following modes are available
71 | - `commit` (Default, the `-PchangedProjectsTask.commit` and `-PchangedProjectsTask.prevCommit` options are the commit ids and makes use of `~`)
72 | - `branch` (`-PchangedProjectsTask.commit` and `-PchangedProjectsTask.prevCommit` are now branch names and will be used like the following `git diff --name-only prev curr`, where `curr` is `-PchangedProjectsTask.commit`)
73 | - `branchTwoDotted` (`-PchangedProjectsTask.commit` and `-PchangedProjectsTask.prevCommit` are branch names and will be used like the following `git diff --name-only prev..curr`)
74 | - `branchThreeDotted` (`-PchangedProjectsTask.commit` and `-PchangedProjectsTask.prevCommit` are branch names and will be used like the following `git diff --name-only prev..curr`)
75 |
76 | If either `-PchangedProjectsTask.commit` and `-PchangedProjectsTask.prevCommit` is not specified when running the `runTaskForChangedProjects` command,
77 | then that option simply defaults to `HEAD` if it is allowed to by the logic, otherwise an error is thrown.
78 |
79 | The following table illustrates the allowed and available options and how the resulting diff command looks
80 |
81 | | **Mode** | **Current** | **Previous** | **Git diff command** |
82 | |-------------------|-------------|--------------|------------------------------------|
83 | | commit | | | `git diff --name-only HEAD~ HEAD` |
84 | | commit | curr | | `git diff --name-only curr~ curr` |
85 | | commit | curr | prev | `git diff --name-only prev~ curr` |
86 | | branch | curr | prev | `git diff --name-only prev curr` |
87 | | branch | | prev | `git diff --name-only prev HEAD` |
88 | | branchTwoDotted | curr | prev | `git diff --name-only prev..curr` |
89 | | branchTwoDotted | | prev | `git diff --name-only prev..` |
90 | | branchThreeDotted | curr | prev | `git diff --name-only prev...curr` |
91 | | branchThreeDotted | | prev | `git diff --name-only prev...` |
92 |
93 | ## Example for evaluating the plugin
94 | This is a basic example you can use to evaluate the plugin on your project, apply the following to your own root `build.gradle`.
95 |
96 | ```groovy
97 | plugins {
98 | id 'io.github.crimix.changed-projects-task' version 'VERSION'
99 | }
100 |
101 | allprojects {
102 | task print {
103 | doLast {
104 | println ">> " + project.path
105 | }
106 | }
107 | }
108 |
109 | changedProjectsTask {
110 | taskToRun = "print"
111 | }
112 | ```
113 | Then run the following Gradle command line
114 | `runTaskForChangedProjects -PchangedProjectsTask.run`
115 |
116 | This example will print the path of all the projects that is affected by some change and write `Task x:print SKIPPED` for those not affected.
117 | You can use this to test how the plugin works and also set up the configuration of the plugin using real-world changes in your project.
118 | This way to can skip running time-consuming task like test when you are just configuring the plugin.
119 |
120 | ## Local Development
121 | To develop and test locally with your own projects there are a few changes needed to be made.
122 | 1. First add the following to `settings.gradle` file in the root project
123 | ```groovy
124 | pluginManagement {
125 | repositories {
126 | mavenLocal()
127 | gradlePluginPortal()
128 | }
129 | }
130 | buildscript {
131 | configurations.all {
132 | resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
133 | }
134 | }
135 | ```
136 | 2. Change the version inside of `build.gradle` in ChangedProjectsTaskPlugin to be `x.y-SNAPSHOT`
137 | 3. Change the plugin version used in your project to be `x.y-SNAPSHOT`
138 | 4. Do a refresh / sync of Gradle
139 |
140 | ## Local debugging
141 | To debug the plugin on your project, first checkout the repo and set breakpoints.
142 | Then you start the project task as the following in your project
143 | `./gradlew runTaskForChangedProjects -PchangedProjectsTask.run -Dorg.gradle.debug=true --no-daemon`
144 |
145 | Then the Gradle task will wait for you to connect a debugger, this can be done using the `GradleRemoteDebug` run configuration from this repo.
146 |
147 | ## FAQ
148 | ### Dependent task execution stops on first failed
149 | Normally Gradle uses a fail-fast approach except for the test task. This means that if a dependent task fails the build stops.
150 | Depending on the use case it can be preferable, but if this plugin is used to skip unit tests, the wanted behavior will probably to execute all test tasks.
151 |
152 | The way to get the wanted behaviour is to run the task as the following
153 | ```
154 | --continue runTaskForChangedProjects -PchangedProjectsTask.run
155 | ```
156 |
157 | This caused Gradle to execute all tasks even if the fail and still report the build as failed when it is done.
158 | This way it is possible to run all dependent tasks and get all unit test results to present to the user.
159 |
160 | ### Task '.run' not found in root project
161 | If you encounter any issue running the commands with the `-PchangedProjectsTask.run` parameter, it might be because you are using
162 | PowerShell on Windows which needs double quotes around such parameters.
163 |
164 | ## Why did I make this
165 | I have for at least a month been looking for a plugin or way to do this in Gradle.
166 | I have found a few interesting articles and plugins/code snippets, but none that worked out-of-the-box or suited my needs.
167 |
168 | Thus, I began to write it using plain Groovy and Gradle, but when I was nearly done I stopped and thought for a moment,
169 | because all the embedded filtering and logic could be removed and put into a configuration instead, such that others could use it.
170 | Leading to this plugin being created
171 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java'
3 | id 'java-gradle-plugin'
4 | id "io.freefair.lombok" version "6.4.1"
5 | id 'net.saliman.properties' version '1.5.2'
6 |
7 | // Publishing publicly
8 | id "com.gradle.plugin-publish" version "0.20.0"
9 |
10 | // Publishing to Artifactory
11 | id 'maven-publish'
12 | }
13 |
14 | group 'io.github.crimix'
15 | version '1.8'
16 |
17 | java {
18 | sourceCompatibility = JavaVersion.VERSION_11
19 | targetCompatibility = JavaVersion.VERSION_11
20 | }
21 |
22 | repositories {
23 | mavenCentral()
24 | }
25 |
26 | dependencies {
27 | implementation 'org.apache.commons:commons-exec:1.3'
28 | testImplementation(platform('org.junit:junit-bom:5.8.2'))
29 | testImplementation('org.junit.jupiter:junit-jupiter:5.9.0')
30 | testImplementation('org.junit.jupiter:junit-jupiter-params:5.9.0')
31 | testImplementation("org.assertj:assertj-core:3.23.1")
32 | }
33 |
34 | test {
35 | useJUnitPlatform()
36 | testLogging {
37 | events "passed", "skipped", "failed"
38 | }
39 | }
40 |
41 | gradlePlugin {
42 | plugins {
43 | changedProjectsPlugin {
44 | id = 'io.github.crimix.changed-projects-task'
45 | implementationClass = 'io.github.crimix.changedprojectstask.ChangedProjectsPlugin'
46 | }
47 | }
48 | }
49 |
50 | pluginBundle {
51 | website = 'https://github.com/Crimix/ChangedProjectsTaskPlugin'
52 | vcsUrl = 'https://github.com/Crimix/ChangedProjectsTaskPlugin'
53 |
54 | plugins {
55 | changedProjectsPlugin {
56 | // id is captured from java-gradle-plugin configuration
57 | displayName = 'Changed Projects Task Plugin'
58 | description = 'A Gradle plugin to run a user defined task on changed projects (modules) and their dependent projects (modules)'
59 | tags = ['git', 'project dependencies', 'task', 'changed projects', 'changed modules']
60 | }
61 | }
62 | }
63 |
64 | publishToMavenLocal.dependsOn(validatePlugins)
65 | publishPlugins.dependsOn(validatePlugins)
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MSYS* | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'changed-projects-task'
2 |
3 |
--------------------------------------------------------------------------------
/src/main/java/io/github/crimix/changedprojectstask/ChangedProjectsPlugin.java:
--------------------------------------------------------------------------------
1 | package io.github.crimix.changedprojectstask;
2 |
3 | import io.github.crimix.changedprojectstask.configuration.ChangedProjectsConfiguration;
4 | import io.github.crimix.changedprojectstask.extensions.Extensions;
5 | import io.github.crimix.changedprojectstask.task.ChangedProjectsTask;
6 | import lombok.experimental.ExtensionMethod;
7 | import org.gradle.api.Plugin;
8 | import org.gradle.api.Project;
9 | import org.gradle.api.Task;
10 |
11 | @ExtensionMethod(Extensions.class)
12 | public class ChangedProjectsPlugin implements Plugin {
13 |
14 | @Override
15 | public void apply(Project project) {
16 | if (!project.isRootProject()) {
17 | throw new IllegalArgumentException(String.format("Must be applied to root project %s, but was found on %s instead.", project.getRootProject(), project.getName()));
18 | }
19 | ChangedProjectsConfiguration extension = project.getExtensions().create("changedProjectsTask", ChangedProjectsConfiguration.class);
20 | Task task = project.getTasks().register("runTaskForChangedProjects").get();
21 | if (project.hasBothRunCommands()) {
22 | throw new IllegalArgumentException("You may either use run or runCommandLine, not both");
23 | }
24 | if (project.hasBeenEnabled()) {
25 | ChangedProjectsTask.configureAndRun(project, task, extension);
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/io/github/crimix/changedprojectstask/configuration/ChangedProjectsChoice.java:
--------------------------------------------------------------------------------
1 | package io.github.crimix.changedprojectstask.configuration;
2 |
3 | /**
4 | * The two different modes that the plugin can be used
5 | */
6 | public enum ChangedProjectsChoice {
7 | /**
8 | * Only execute task on those modules that are directly affected
9 | */
10 | ONLY_DIRECTLY,
11 |
12 | /**
13 | * Execute task on all modules in the dependency tree that are affected
14 | */
15 | INCLUDE_DEPENDENTS
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/io/github/crimix/changedprojectstask/configuration/ChangedProjectsConfiguration.java:
--------------------------------------------------------------------------------
1 | package io.github.crimix.changedprojectstask.configuration;
2 |
3 | import org.gradle.api.provider.Property;
4 | import org.gradle.api.provider.SetProperty;
5 |
6 | import java.util.regex.Pattern;
7 |
8 | /**
9 | * The configuration that the user of the plugin can change to affect the behavior of the plugin
10 | */
11 | public interface ChangedProjectsConfiguration {
12 |
13 | /**
14 | * If the plugin should log stuff like changed files and identified project dependencies.
15 | * This is mostly used to debug if the plugin does not works as expected on a project.
16 | * @return whether the plugin to do debug logging
17 | */
18 | Property getDebugLogging();
19 |
20 | /**
21 | * The task to run on the changed projects and those affected by the change (If chosen).
22 | * @return the task name
23 | */
24 | Property getTaskToRun();
25 |
26 | /**
27 | * The projects to execute the task on when there is a not ignored change.
28 | * @return a list of project paths
29 | */
30 | SetProperty getAlwaysRunProject();
31 |
32 | /**
33 | * The projects to never execute the task on even when it has changed.
34 | * @return a list of project paths
35 | */
36 | SetProperty getNeverRunProject();
37 |
38 | /**
39 | * The list of regexes that filters the not ignored changes to see if any change has been marked to affect all projects
40 | * and thus needs to run the task on the root project instead.
41 | * @return a list of compiled patterns
42 | */
43 | SetProperty getAffectsAllRegex();
44 |
45 | /**
46 | * The list of regexes that filters the changes.
47 | * THis can be used to as an example ignored specific directories or file extensions.
48 | * @return a list of compiled patterns
49 | */
50 | SetProperty getIgnoredRegex();
51 |
52 | /**
53 | * The mode in which the plugin should work.
54 | * Either {@link ChangedProjectsChoice#ONLY_DIRECTLY} which means the task is only run for projects affected by changes files directly
55 | * or {@link ChangedProjectsChoice#INCLUDE_DEPENDENTS} which means the task is run on directly changed projects and projects dependent on those projects
56 | * Defaults to {@link ChangedProjectsChoice#INCLUDE_DEPENDENTS}
57 | * @return which mode the plugin is in
58 | */
59 | Property getChangedProjectsMode();
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/src/main/java/io/github/crimix/changedprojectstask/extensions/Extensions.java:
--------------------------------------------------------------------------------
1 | package io.github.crimix.changedprojectstask.extensions;
2 |
3 | import io.github.crimix.changedprojectstask.configuration.ChangedProjectsChoice;
4 | import io.github.crimix.changedprojectstask.configuration.ChangedProjectsConfiguration;
5 | import io.github.crimix.changedprojectstask.utils.GitDiffMode;
6 | import lombok.SneakyThrows;
7 | import org.gradle.api.Project;
8 | import org.gradle.api.logging.Logger;
9 |
10 | import java.io.File;
11 | import java.io.IOException;
12 | import java.nio.file.Path;
13 | import java.util.Collections;
14 | import java.util.Optional;
15 | import java.util.Set;
16 |
17 | import static io.github.crimix.changedprojectstask.utils.Properties.*;
18 |
19 | /**
20 | * Class the contains the Lombok extension methods
21 | */
22 | public class Extensions {
23 |
24 | /**
25 | * Returns whether the project is the root project.
26 | * @return true if the project is the root project
27 | */
28 | public static boolean isRootProject(Project project) {
29 | return project.equals(project.getRootProject());
30 | }
31 |
32 | /**
33 | * Gets the name of the project's directory
34 | * @return the name of the project's directory
35 | */
36 | public static String getProjectDirName(Project project) {
37 | return project.getProjectDir().getName();
38 | }
39 |
40 | /**
41 | * Returns whether the plugin's task is allowed to run and configure.
42 | * @return true if the plugin's task is allowed to run and configure
43 | */
44 | public static boolean hasBeenEnabled(Project project) {
45 | return project.getRootProject().hasProperty(ENABLE) || project.getRootProject().hasProperty(ENABLE_COMMANDLINE);
46 | }
47 |
48 | /**
49 | * Returns whether the plugin has been told to run using both task and commandline
50 | * @return true if the plugin has been told to run using both task and commandline
51 | */
52 | public static boolean hasBothRunCommands(Project project) {
53 | return project.getRootProject().hasProperty(ENABLE) && project.getRootProject().hasProperty(ENABLE_COMMANDLINE);
54 | }
55 |
56 |
57 | /**
58 | * Gets the task to run, this is either the override from CLI arugment of the default configured task.
59 | * @return task to run
60 | */
61 | public static String getTaskToRun(Project project, ChangedProjectsConfiguration configuration) {
62 | return Optional.of(project)
63 | .map(Project::getRootProject)
64 | .map(p -> p.findProperty(TASK_TO_RUN))
65 | .map(String.class::cast)
66 | .orElseGet(() -> configuration.getTaskToRun().getOrNull());
67 | }
68 |
69 | /**
70 | * Gets the configured commit id
71 | * @return either an optional with the commit id or an empty optional if it has not been configured
72 | */
73 | public static Optional getCommitId(Project project) {
74 | return Optional.of(project)
75 | .map(Project::getRootProject)
76 | .map(p -> p.findProperty(CURRENT_COMMIT))
77 | .map(String.class::cast);
78 | }
79 |
80 |
81 | /**
82 | * Returns if the task to runs should be invoked using the commandline instead of using the task onlyIf approach.
83 | * @return true if the task should be invoked using the commandline
84 | */
85 | public static boolean shouldUseCommandLine(Project project) {
86 | return project.getRootProject().hasProperty(ENABLE_COMMANDLINE);
87 | }
88 |
89 | /**
90 | * Gets the commandline arguments specified for use when invoking the task to run using the commandline.
91 | * @return the commandline arguments as a string
92 | */
93 | public static String getCommandLineArgs(Project project) {
94 | return Optional.of(project)
95 | .map(Project::getRootProject)
96 | .map(p -> p.findProperty(COMMANDLINE_ARGS))
97 | .map(String.class::cast)
98 | .orElse("");
99 | }
100 |
101 | /**
102 | * Gets the configured previous commit id
103 | * @return either an optional with the previous commit id or an empty optional if it has not been configured
104 | */
105 | public static Optional getPreviousCommitId(Project project) {
106 | return Optional.of(project)
107 | .map(Project::getRootProject)
108 | .map(p -> p.findProperty(PREVIOUS_COMMIT))
109 | .map(String.class::cast);
110 | }
111 |
112 | /**
113 | * Gets the configured git commit compare mode if specified.
114 | * Defaults to {@link GitDiffMode#COMMIT} if none specified.
115 | * @return the configured git compare mode or {@link GitDiffMode#COMMIT}
116 | */
117 | public static GitDiffMode getCommitCompareMode(Project project) {
118 | return Optional.of(project)
119 | .map(Project::getRootProject)
120 | .map(p -> p.findProperty(COMMIT_MODE))
121 | .map(String.class::cast)
122 | .map(GitDiffMode::getMode)
123 | .orElse(GitDiffMode.COMMIT);
124 | }
125 |
126 | /**
127 | * Finds the git root for the project.
128 | * @return a file that represents the git root of the project.
129 | */
130 | public static File getGitRootDir(Project project) {
131 | File currentDir = project.getRootProject().getProjectDir();
132 |
133 | //Keep going until we either hit a .git dir or the root of the file system on either Windows or Linux
134 | while (currentDir != null && !currentDir.getPath().equals("/")) {
135 | if (new File(String.format("%s/.git", currentDir.getPath())).exists()) {
136 | return currentDir;
137 | }
138 | currentDir = currentDir.getParentFile();
139 | }
140 |
141 | return null;
142 | }
143 |
144 | /**
145 | * Gets the canonical path to the project.
146 | * @return the canonical path to the project.
147 | */
148 | @SneakyThrows(IOException.class)
149 | public static Path getCanonicalProjectPath(Project project) {
150 | return project.getProjectDir().getCanonicalFile().toPath();
151 | }
152 |
153 | /**
154 | * Gets the canonical path's string length to the project.
155 | * @return the canonical path's string length to the project.
156 | */
157 | @SneakyThrows(IOException.class)
158 | public static int getCanonicalProjectPathStringLength(Project project) {
159 | return project.getProjectDir().getCanonicalFile().toPath().toString().length();
160 | }
161 |
162 | /**
163 | * Gets the canonical path to the file.
164 | * @return the canonical path to the file.
165 | */
166 | @SneakyThrows(IOException.class)
167 | public static Path getCanonicalFilePath(File file) {
168 | return file.getCanonicalFile().toPath();
169 | }
170 |
171 | /**
172 | * Runs validation on the configuration.
173 | */
174 | public static void validate(ChangedProjectsConfiguration configuration, Project root) {
175 | String taskToRun = getTaskToRun(root, configuration);
176 | if (taskToRun == null || taskToRun.isEmpty()) {
177 | throw new IllegalArgumentException("changedProjectsTask: taskToRun is required");
178 | } else if (taskToRun.startsWith(":")) {
179 | throw new IllegalArgumentException("changedProjectsTask: taskToRun should not start with :");
180 | }
181 | Set projectsAlwaysRun = configuration.getAlwaysRunProject().getOrElse(Collections.emptySet());
182 | for (String project : projectsAlwaysRun) {
183 | if (!project.startsWith(":")) {
184 | throw new IllegalArgumentException(String.format("changedProjectsTask: alwaysRunProject %s must start with :", project));
185 | }
186 | }
187 |
188 | configuration.getAffectsAllRegex().getOrElse(Collections.emptySet()); //Gradle will throw if the type does not match
189 | configuration.getIgnoredRegex().getOrElse(Collections.emptySet()); //Gradle will throw if the type does not match
190 | String mode = configuration.getChangedProjectsMode().getOrElse(ChangedProjectsChoice.INCLUDE_DEPENDENTS.name());
191 | try {
192 | ChangedProjectsChoice.valueOf(mode);
193 | } catch (IllegalArgumentException ignored) {
194 | throw new IllegalArgumentException(String.format("changedProjectsTask: ChangedProjectsMode must be either %s or %s ", ChangedProjectsChoice.ONLY_DIRECTLY.name(), ChangedProjectsChoice.INCLUDE_DEPENDENTS.name()));
195 | }
196 | }
197 |
198 | /**
199 | * Gets the plugin's configured mode
200 | * @return the mode the plugin is configured to use
201 | */
202 | public static ChangedProjectsChoice getPluginMode(ChangedProjectsConfiguration configuration) {
203 | return ChangedProjectsChoice.valueOf(configuration.getChangedProjectsMode().getOrElse(ChangedProjectsChoice.INCLUDE_DEPENDENTS.name()));
204 | }
205 |
206 | /**
207 | * Prints the configuration.
208 | * @param logger the logger to print the configuration to.
209 | */
210 | public static void print(ChangedProjectsConfiguration configuration, Project project, Logger logger) {
211 | if (shouldLog(configuration)) {
212 | logger.lifecycle("Printing configuration");
213 | logger.lifecycle("Task to run {}", getTaskToRun(project, configuration));
214 | logger.lifecycle("Always run project {}", configuration.getAlwaysRunProject().getOrElse(Collections.emptySet()));
215 | logger.lifecycle("Never run project {}", configuration.getNeverRunProject().getOrElse(Collections.emptySet()));
216 | logger.lifecycle("Affects all regex {}", configuration.getAffectsAllRegex().getOrElse(Collections.emptySet()));
217 | logger.lifecycle("Ignored regex {}", configuration.getIgnoredRegex().getOrElse(Collections.emptySet()));
218 | logger.lifecycle("Mode {}", getPluginMode(configuration));
219 | logger.lifecycle("");
220 | }
221 | }
222 |
223 | /**
224 | * Returns whether the plugin should log debug information to the Gradle log
225 | * @return true if the plugin should debug log
226 | */
227 | public static boolean shouldLog(ChangedProjectsConfiguration configuration) {
228 | return configuration.getDebugLogging().getOrElse(false);
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/src/main/java/io/github/crimix/changedprojectstask/providers/ChangedFilesProvider.java:
--------------------------------------------------------------------------------
1 | package io.github.crimix.changedprojectstask.providers;
2 |
3 | import io.github.crimix.changedprojectstask.configuration.ChangedProjectsConfiguration;
4 | import io.github.crimix.changedprojectstask.extensions.Extensions;
5 | import io.github.crimix.changedprojectstask.utils.CollectingOutputStream;
6 | import lombok.SneakyThrows;
7 | import lombok.experimental.ExtensionMethod;
8 | import org.apache.commons.exec.CommandLine;
9 | import org.apache.commons.exec.DefaultExecutor;
10 | import org.apache.commons.exec.PumpStreamHandler;
11 | import org.gradle.api.Project;
12 | import org.gradle.api.logging.Logger;
13 |
14 | import java.io.File;
15 | import java.util.Collections;
16 | import java.util.List;
17 | import java.util.function.Predicate;
18 | import java.util.regex.Pattern;
19 | import java.util.stream.Collectors;
20 |
21 | @ExtensionMethod(Extensions.class)
22 | public class ChangedFilesProvider {
23 |
24 | private final Project project;
25 | private final ChangedProjectsConfiguration extension;
26 | private final GitCommandProvider gitCommandProvider;
27 | private final List filteredChanges;
28 | private final boolean affectsAllProjects;
29 |
30 | public ChangedFilesProvider(Project project, ChangedProjectsConfiguration extension) {
31 | this.project = project;
32 | this.extension = extension;
33 | this.gitCommandProvider = new GitCommandProvider(project);
34 | List gitFilteredChanges = initFilteredChanges();
35 | this.filteredChanges = initFilteredChangedFiles(gitFilteredChanges);
36 | this.affectsAllProjects = initAffectsAllProjects(gitFilteredChanges);
37 | }
38 |
39 | @SneakyThrows
40 | private List initFilteredChanges() {
41 | File gitRoot = project.getGitRootDir();
42 | if (gitRoot == null) {
43 | throw new IllegalStateException("The project does not have a git root");
44 | }
45 |
46 | CollectingOutputStream stdout = new CollectingOutputStream();
47 | CollectingOutputStream stderr = new CollectingOutputStream();
48 | //We use Apache Commons Exec because we do not want to re-invent the wheel as ProcessBuilder hangs if the output or error buffer is full
49 | DefaultExecutor exec = new DefaultExecutor();
50 | exec.setStreamHandler(new PumpStreamHandler(stdout, stderr));
51 | exec.setWorkingDirectory(gitRoot);
52 | exec.execute(CommandLine.parse(gitCommandProvider.getGitDiffCommand()));
53 |
54 | if (stderr.isNotEmpty()) {
55 | if (containsErrors(stderr)) {
56 | throw new IllegalStateException(String.format("Failed to run git diff because of \n%s", stderr));
57 | } else {
58 | if (project.getLogger().isWarnEnabled()) {
59 | project.getLogger().warn(stderr.toString());
60 | }
61 | }
62 | }
63 |
64 | if (stdout.isEmpty()) {
65 | throw new IllegalStateException("Git diff returned no results this must be a mistake");
66 | }
67 |
68 | //Create a single predicate from the ignored regexes such that we can use a simple filter
69 | Predicate filter = extension.getIgnoredRegex()
70 | .getOrElse(Collections.emptySet())
71 | .stream()
72 | .map(Pattern::asMatchPredicate)
73 | .reduce(Predicate::or)
74 | .orElse(x -> false);
75 |
76 | //Filter and return the list
77 | return stdout.getLines().stream()
78 | .filter(Predicate.not(filter))
79 | .collect(Collectors.toList());
80 | }
81 |
82 | private boolean containsErrors(CollectingOutputStream stderr) {
83 | return stderr.getLines().stream().anyMatch(line -> line.startsWith("error:"));
84 | }
85 |
86 | private boolean initAffectsAllProjects(List gitFilteredChanges) {
87 | //Create a single predicate from the affects all projects regexes such that we can use a simple filter
88 | Predicate filter = extension.getAffectsAllRegex()
89 | .getOrElse(Collections.emptySet())
90 | .stream()
91 | .map(Pattern::asMatchPredicate)
92 | .reduce(Predicate::or)
93 | .orElse(x -> false);
94 |
95 | return gitFilteredChanges.stream()
96 | .anyMatch(filter);
97 | }
98 |
99 | private List initFilteredChangedFiles(List gitFilteredChanges) {
100 | File gitRoot = project.getGitRootDir();
101 | if (gitRoot == null) {
102 | throw new IllegalStateException("The project does not have a git root");
103 | }
104 |
105 | return gitFilteredChanges.stream()
106 | .map(s -> new File(gitRoot, s))
107 | .collect(Collectors.toList());
108 | }
109 |
110 | /**
111 | * Gets the filtered changed files
112 | * @return the filtered changed files
113 | */
114 | public List getChangedFiles() {
115 | return filteredChanges;
116 | }
117 |
118 | /**
119 | * Returns whether all projects are affected by the changes specified by the plugin configuration
120 | * @return true if all projects are affected
121 | */
122 | public boolean isAllProjectsAffected() {
123 | return affectsAllProjects;
124 | }
125 |
126 | /**
127 | * Prints debug information if it has been enabled
128 | * @param logger the logger to print information to
129 | */
130 | public void printDebug(Logger logger) {
131 | if (extension.shouldLog()) {
132 | logger.lifecycle("Git diff command uses {}", gitCommandProvider.getGitDiffCommand());
133 | logger.lifecycle("All projects affected? {}", isAllProjectsAffected());
134 | logger.lifecycle("Changed files:");
135 | getChangedFiles()
136 | .forEach(file -> logger.lifecycle(file.toString()));
137 | logger.lifecycle("");
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/main/java/io/github/crimix/changedprojectstask/providers/GitCommandProvider.java:
--------------------------------------------------------------------------------
1 | package io.github.crimix.changedprojectstask.providers;
2 |
3 | import io.github.crimix.changedprojectstask.extensions.Extensions;
4 | import io.github.crimix.changedprojectstask.utils.GitDiffMode;
5 | import lombok.experimental.ExtensionMethod;
6 | import org.gradle.api.Project;
7 | import org.gradle.internal.impldep.org.jetbrains.annotations.VisibleForTesting;
8 |
9 | import java.util.Optional;
10 |
11 | import static io.github.crimix.changedprojectstask.utils.Properties.CURRENT_COMMIT;
12 | import static io.github.crimix.changedprojectstask.utils.Properties.PREVIOUS_COMMIT;
13 |
14 | /**
15 | * This class is responsible for creating the git diff command based on the users command line choices when running the task.
16 | */
17 | @ExtensionMethod(Extensions.class)
18 | public class GitCommandProvider {
19 |
20 | // The default if no commit ids have been specified
21 | private static final String HEAD = "HEAD";
22 | private static final String BASE_DIFF_COMMAND = "git diff --name-only";
23 |
24 | private final Project project;
25 |
26 | public GitCommandProvider(Project project) {
27 | this.project = project;
28 | }
29 |
30 | /**
31 | * Constructs the git diff command that should be used to find the changed files.
32 | * @return the git diff command
33 | */
34 | public String getGitDiffCommand() {
35 | GitDiffMode mode = project.getCommitCompareMode();
36 | Optional currentCommitId = project.getCommitId();
37 | Optional previousCommitId = project.getPreviousCommitId();
38 |
39 | return evaluate(mode, currentCommitId, previousCommitId);
40 | }
41 |
42 | /**
43 | * Method created such that we can write test for it
44 | * @param mode the mode
45 | * @param currentCommitId the current commit ref if present
46 | * @param previousCommitId the previous commit ref if present
47 | * @return the git diff command
48 | */
49 | @VisibleForTesting
50 | public String evaluate(GitDiffMode mode, Optional currentCommitId, Optional previousCommitId) {
51 | switch (mode) {
52 | case COMMIT:
53 | return getCommitDiff(currentCommitId, previousCommitId);
54 | case BRANCH:
55 | return getBranchDiff(currentCommitId, previousCommitId);
56 | case BRANCH_TWO_DOT:
57 | return getBranchTwoDotDiff(currentCommitId, previousCommitId);
58 | case BRANCH_THREE_DOT:
59 | return getBranchThreeDotDiff(currentCommitId, previousCommitId);
60 | default:
61 | throw new UnsupportedOperationException(String.format("GitCommitMode %s is not supported", mode.name()));
62 | }
63 | }
64 |
65 | private String getCommitDiff(Optional currentCommitId, Optional previousCommitId) {
66 | //If only currentCommitId has been specified then we assume that it is the diff of that specific commit
67 | if (currentCommitId.isPresent() && previousCommitId.isPresent()) {
68 | return String.format("%s %s~ %s", BASE_DIFF_COMMAND, previousCommitId.get(), currentCommitId.get());
69 | } else if (currentCommitId.isPresent()) {
70 | return String.format("%s %s~ %s", BASE_DIFF_COMMAND, currentCommitId.get(), currentCommitId.get());
71 | } else if (previousCommitId.isPresent()) {
72 | throw new IllegalStateException(String.format("[%s] When using %s then %s must also be specified", GitDiffMode.COMMIT.name(), PREVIOUS_COMMIT, CURRENT_COMMIT));
73 | } else {
74 | return String.format("%s %s~ %s", BASE_DIFF_COMMAND, HEAD, HEAD);
75 | }
76 | }
77 |
78 | private String getBranchDiff(Optional currentCommitId, Optional previousCommitId) {
79 | if (currentCommitId.isPresent() && previousCommitId.isPresent()) {
80 | return String.format("%s %s %s", BASE_DIFF_COMMAND, previousCommitId.get(), currentCommitId.get());
81 | } else if (previousCommitId.isPresent()) {
82 | return String.format("%s %s %s", BASE_DIFF_COMMAND, previousCommitId.get(), HEAD);
83 | } else {
84 | throw new IllegalStateException(String.format("[%s] %s must always be specified", GitDiffMode.BRANCH.name(), PREVIOUS_COMMIT));
85 | }
86 | }
87 |
88 | private String getBranchTwoDotDiff(Optional currentCommitId, Optional previousCommitId) {
89 | if (currentCommitId.isPresent() && previousCommitId.isPresent()) {
90 | return String.format("%s %s..%s", BASE_DIFF_COMMAND, previousCommitId.get(), currentCommitId.get());
91 | } else if (previousCommitId.isPresent()) {
92 | return String.format("%s %s..", BASE_DIFF_COMMAND, previousCommitId.get());
93 | } else {
94 | throw new IllegalStateException(String.format("[%s] %s must always be specified", GitDiffMode.BRANCH_TWO_DOT.name(), PREVIOUS_COMMIT));
95 | }
96 | }
97 |
98 | private String getBranchThreeDotDiff(Optional currentCommitId, Optional previousCommitId) {
99 | if (currentCommitId.isPresent() && previousCommitId.isPresent()) {
100 | return String.format("%s %s...%s", BASE_DIFF_COMMAND, previousCommitId.get(), currentCommitId.get());
101 | } else if (previousCommitId.isPresent()) {
102 | return String.format("%s %s...", BASE_DIFF_COMMAND, previousCommitId.get());
103 | } else {
104 | throw new IllegalStateException(String.format("[%s] %s must always be specified", GitDiffMode.BRANCH_THREE_DOT.name(), PREVIOUS_COMMIT));
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/main/java/io/github/crimix/changedprojectstask/providers/ProjectDependencyProvider.java:
--------------------------------------------------------------------------------
1 | package io.github.crimix.changedprojectstask.providers;
2 |
3 | import io.github.crimix.changedprojectstask.configuration.ChangedProjectsConfiguration;
4 | import io.github.crimix.changedprojectstask.extensions.Extensions;
5 | import io.github.crimix.changedprojectstask.utils.Pair;
6 | import lombok.experimental.ExtensionMethod;
7 | import org.gradle.api.Project;
8 | import org.gradle.api.artifacts.Configuration;
9 | import org.gradle.api.artifacts.ProjectDependency;
10 | import org.gradle.api.logging.Logger;
11 |
12 | import java.io.File;
13 | import java.nio.file.Path;
14 | import java.util.Collection;
15 | import java.util.Collections;
16 | import java.util.Comparator;
17 | import java.util.HashSet;
18 | import java.util.Map;
19 | import java.util.Set;
20 | import java.util.function.Predicate;
21 | import java.util.function.Supplier;
22 | import java.util.stream.Collectors;
23 |
24 | @ExtensionMethod(Extensions.class)
25 | public class ProjectDependencyProvider {
26 |
27 | private final Project project;
28 | private final ChangedProjectsConfiguration extension;
29 | private final Map> projectDependentsMap;
30 |
31 | public ProjectDependencyProvider(Project project, ChangedProjectsConfiguration extension) {
32 | this.project = project;
33 | this.extension = extension;
34 | this.projectDependentsMap = initProjectDependents();
35 | }
36 |
37 | private Map> initProjectDependents() {
38 | //We create a lookup map of projects and the projects that depends on that project once
39 | //This is to speed up the evaluating dependent changed projects
40 | //The key of the map is a project that is a direct dependency for the value set
41 | return project.getSubprojects().stream()
42 | .map(this::getProjectDependencies)
43 | .flatMap(Collection::stream)
44 | .collect(Collectors.groupingBy(Pair::getKey, Collectors.mapping(Pair::getValue, Collectors.toSet())));
45 | }
46 |
47 | private Set> getProjectDependencies(Project subproject) {
48 | //We use a pair, because we want the project that is a dependency together with the project it is a dependency for
49 | return subproject.getConfigurations().stream()
50 | .map(Configuration::getDependencies)
51 | .map(dependencySet -> dependencySet.withType(ProjectDependency.class))
52 | .flatMap(Set::stream)
53 | .map(ProjectDependency::getDependencyProject)
54 | .map(p -> new Pair<>(p, subproject))
55 | .collect(Collectors.toSet());
56 | }
57 |
58 | public Project getChangedProject(File file) {
59 | Path filePath = file.getCanonicalFilePath();
60 | if (!filePath.startsWith(project.getRootProject().getCanonicalProjectPath())) {
61 | return null; //We return null here as there is no need to try and find which project it belongs to
62 | }
63 |
64 | //We find all projects which paths match the start of the files' path.
65 | //Then we take the project which has the most overlap with the beginning of the file path
66 | //Else we just use the root project as a fallback
67 | Project result = project.getAllprojects().stream()
68 | .filter(doesFilePathStartWithProjectDirPath(file))
69 | .max(Comparator.comparingInt(Extensions::getCanonicalProjectPathStringLength))
70 | .orElseGet(getFallback(file));
71 |
72 | if (extension.shouldLog()) {
73 | project.getLogger().lifecycle("File {} belongs to {}", file, result);
74 | }
75 |
76 | return result;
77 | }
78 |
79 | private Predicate doesFilePathStartWithProjectDirPath(File file) {
80 | return subproject -> {
81 | Path subprojectPath = subproject.getCanonicalProjectPath();
82 | Path filePath = file.getCanonicalFilePath();
83 | return filePath.startsWith(subprojectPath);
84 | };
85 | }
86 |
87 | private Supplier getFallback(File file) {
88 | return () -> {
89 | if (extension.shouldLog()) {
90 | project.getLogger().lifecycle("USING FALLBACK for file {}", file);
91 | }
92 | return project.getRootProject();
93 | };
94 | }
95 |
96 | public Set getAffectedDependentProjects(Set directlyChangedProjects) {
97 | //We use this to have a way to break our of recursion when we have already seen that project once
98 | //This makes it possible to avoid infinite recursion and also speeds up the process
99 | Set alreadyVisitedProjects = new HashSet<>();
100 |
101 | return directlyChangedProjects.stream()
102 | .map(p -> getDependentProjects(p, alreadyVisitedProjects))
103 | .flatMap(Collection::stream)
104 | .collect(Collectors.toSet());
105 | }
106 |
107 | private Set getDependentProjects(Project project, Set alreadyVisitedProjects) {
108 | //If we have already visited the project, we can just return empty as we have evaluated dependent projects in another call
109 | if (alreadyVisitedProjects.contains(project)) {
110 | return Collections.emptySet();
111 | }
112 | Set result = new HashSet<>(projectDependentsMap.getOrDefault(project, Collections.emptySet()));
113 | alreadyVisitedProjects.add(project);
114 |
115 | //Continue down the chain until no more new affected projects are found
116 | Set dependents = result.stream()
117 | .map(p -> getDependentProjects(p, alreadyVisitedProjects))
118 | .flatMap(Collection::stream)
119 | .collect(Collectors.toSet());
120 |
121 | result.addAll(dependents);
122 | return result;
123 | }
124 |
125 | public void printDebug(Logger logger) {
126 | if (extension.shouldLog()) {
127 | logger.lifecycle("Printing project dependents map");
128 | projectDependentsMap.forEach((key, value) -> logger.lifecycle("Project: {} is a direct dependent for the following {}", key, value));
129 | logger.lifecycle("");
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/main/java/io/github/crimix/changedprojectstask/task/ChangedProjectsTask.java:
--------------------------------------------------------------------------------
1 | package io.github.crimix.changedprojectstask.task;
2 |
3 | import io.github.crimix.changedprojectstask.configuration.ChangedProjectsChoice;
4 | import io.github.crimix.changedprojectstask.configuration.ChangedProjectsConfiguration;
5 | import io.github.crimix.changedprojectstask.extensions.Extensions;
6 | import io.github.crimix.changedprojectstask.providers.ChangedFilesProvider;
7 | import io.github.crimix.changedprojectstask.providers.ProjectDependencyProvider;
8 | import io.github.crimix.changedprojectstask.utils.LoggingOutputStream;
9 | import lombok.SneakyThrows;
10 | import lombok.experimental.ExtensionMethod;
11 | import org.apache.commons.exec.CommandLine;
12 | import org.apache.commons.exec.DefaultExecutor;
13 | import org.apache.commons.exec.PumpStreamHandler;
14 | import org.gradle.api.Project;
15 | import org.gradle.api.Task;
16 | import org.gradle.api.logging.Logger;
17 |
18 | import java.util.Collections;
19 | import java.util.HashSet;
20 | import java.util.Objects;
21 | import java.util.Set;
22 | import java.util.stream.Collectors;
23 | import java.util.stream.Stream;
24 |
25 | @ExtensionMethod(Extensions.class)
26 | public class ChangedProjectsTask {
27 |
28 | private final Project project;
29 | private final Task task;
30 | private final ChangedProjectsConfiguration extension;
31 |
32 | private boolean affectsAll = false;
33 | private Set affectedProjects = new HashSet<>();
34 | private Set alwaysRunProjects = new HashSet<>();
35 | private Set neverRunProjects = new HashSet<>();
36 |
37 | private ChangedProjectsTask(Project project, Task task, ChangedProjectsConfiguration extension) {
38 | this.project = project;
39 | this.task = task;
40 | this.extension = extension;
41 | }
42 |
43 | public static void configureAndRun(Project project, Task task, ChangedProjectsConfiguration extension) {
44 | ChangedProjectsTask changedProjectsTask = new ChangedProjectsTask(project, task, extension);
45 | if (!project.shouldUseCommandLine()){
46 | changedProjectsTask.configureBeforeEvaluate();
47 | }
48 | project.getGradle().projectsEvaluated(g -> changedProjectsTask.afterEvaluate());
49 |
50 | }
51 |
52 | private void configureBeforeEvaluate() {
53 | for (Project project : project.getAllprojects()) {
54 | configureProject(project);
55 | }
56 | }
57 |
58 | private void afterEvaluate() {
59 | configureAfterAllEvaluate();
60 | if (project.shouldUseCommandLine()) {
61 | commandLineRunProjects();
62 | }
63 | }
64 |
65 | private void commandLineRunProjects() {
66 | for (Project project : project.getAllprojects()) {
67 | if (shouldProjectRun(project)) {
68 | runCommandLineOnProject(project);
69 | }
70 | }
71 | }
72 |
73 | private void configureProject(Project project) {
74 | project.afterEvaluate(p -> {
75 | String path = getPathToTask(p);
76 | task.dependsOn(path);
77 | Task otherTask = p.getTasks().findByPath(path);
78 | if (otherTask != null) {
79 | otherTask.onlyIf(t -> shouldProjectRun(p));
80 | //configureTaskDependenciesRecursively(otherTask, t -> shouldProjectRun(p));
81 | }
82 | });
83 | }
84 |
85 | /*
86 | private void configureTaskDependenciesRecursively(Task task, Spec super Task> var1){
87 | for(Task dependsOn : task.getTaskDependencies().getDependencies(task)) {
88 | dependsOn.onlyIf(var1);
89 | configureTaskDependenciesRecursively(dependsOn, var1);
90 | }
91 | }
92 | */
93 |
94 | private boolean shouldProjectRun(Project p) {
95 | return !neverRunProjects.contains(p) && (affectsAll || affectedProjects.contains(p) || alwaysRunProjects.contains(p));
96 | }
97 |
98 | private void configureAfterAllEvaluate() {
99 | extension.validate(project);
100 | if (hasBeenEnabled()) {
101 | extension.print(project, getLogger());
102 | Project project = getRootProject();
103 | ChangedFilesProvider changedFilesProvider = new ChangedFilesProvider(project, extension);
104 | changedFilesProvider.printDebug(getLogger());
105 |
106 | if (changedFilesProvider.getChangedFiles().isEmpty() && !changedFilesProvider.isAllProjectsAffected()) {
107 | return; //If there are no changes, and we are not forced to run all projects, just skip the rest of the configuration
108 | }
109 |
110 | configureAlwaysAndNeverRun(project);
111 |
112 | // If we have already determined that we should run all, then no need to spend more time on finding the specific projects
113 | if (changedFilesProvider.isAllProjectsAffected()) {
114 | affectsAll = true;
115 | } else {
116 | ProjectDependencyProvider projectDependencyProvider = new ProjectDependencyProvider(project, extension);
117 | projectDependencyProvider.printDebug(getLogger());
118 |
119 | Set directlyAffectedProjects = evaluateDirectAffectedProjects(changedFilesProvider, projectDependencyProvider);
120 |
121 | if (extension.shouldLog()) {
122 | getLogger().lifecycle("Directly affected projects: {}", directlyAffectedProjects);
123 | }
124 |
125 | Set dependentAffectedProjects = new HashSet<>();
126 | if (ChangedProjectsChoice.INCLUDE_DEPENDENTS == extension.getPluginMode()) {
127 | dependentAffectedProjects.addAll(projectDependencyProvider.getAffectedDependentProjects(directlyAffectedProjects));
128 | if (extension.shouldLog()) {
129 | getLogger().lifecycle("Dependent affected Projects: {}", dependentAffectedProjects);
130 | }
131 | }
132 |
133 | affectedProjects = Stream.concat(directlyAffectedProjects.stream(), dependentAffectedProjects.stream())
134 | .collect(Collectors.toSet());
135 | }
136 | }
137 | }
138 |
139 | @SneakyThrows
140 | private void runCommandLineOnProject(Project affected) {
141 | String commandLine = String.format("%s %s %s", getGradleWrapper(), getPathToTask(affected), project.getCommandLineArgs());
142 | if (extension.shouldLog()) {
143 | getLogger().lifecycle("Running {}", commandLine);
144 | }
145 | LoggingOutputStream stdout = new LoggingOutputStream(project.getLogger()::lifecycle);
146 | LoggingOutputStream stderr = new LoggingOutputStream(project.getLogger()::error);
147 | //We use Apache Commons Exec because we do not want to re-invent the wheel as ProcessBuilder hangs if the output or error buffer is full
148 | DefaultExecutor exec = new DefaultExecutor();
149 | exec.setStreamHandler(new PumpStreamHandler(stdout, stderr));
150 | exec.setWorkingDirectory(project.getRootProject().getProjectDir());
151 | int exitValue = exec.execute(CommandLine.parse(commandLine));
152 |
153 | if (exitValue != 0) {
154 | throw new IllegalStateException("Executing command failed");
155 | }
156 | }
157 |
158 | private String getGradleWrapper() {
159 | if (System.getProperty("os.name").startsWith("Windows")) {
160 | return "gradlew.bat";
161 | } else {
162 | return "./gradlew";
163 | }
164 | }
165 |
166 | private Set evaluateDirectAffectedProjects(ChangedFilesProvider changedFilesProvider, ProjectDependencyProvider projectDependencyProvider) {
167 | return changedFilesProvider.getChangedFiles().stream()
168 | .map(projectDependencyProvider::getChangedProject)
169 | .filter(Objects::nonNull)
170 | .collect(Collectors.toSet());
171 | }
172 |
173 | private void configureAlwaysAndNeverRun(Project project) {
174 | Set alwaysRunPath = extension.getAlwaysRunProject().getOrElse(Collections.emptySet());
175 | alwaysRunProjects = project.getAllprojects().stream()
176 | .filter(p -> alwaysRunPath.contains(p.getPath()))
177 | .collect(Collectors.toSet());
178 | if (extension.shouldLog()) {
179 | getLogger().lifecycle("Always run projects: {}", alwaysRunProjects);
180 | }
181 |
182 | Set neverRunPath = extension.getNeverRunProject().getOrElse(Collections.emptySet());
183 | neverRunProjects = project.getAllprojects().stream()
184 | .filter(p -> neverRunPath.contains(p.getPath()))
185 | .collect(Collectors.toSet());
186 | if (extension.shouldLog()) {
187 | getLogger().lifecycle("Never run projects: {}", neverRunProjects);
188 | }
189 | }
190 |
191 | private Project getRootProject() {
192 | return project.getRootProject();
193 | }
194 |
195 | private boolean hasBeenEnabled() {
196 | return project.hasBeenEnabled();
197 | }
198 |
199 | private Logger getLogger() {
200 | return project.getLogger();
201 | }
202 |
203 | private String getPathToTask(Project project) {
204 | String taskToRun = project.getTaskToRun(extension);
205 | if (project.isRootProject()) {
206 | return String.format(":%s", taskToRun);
207 | } else {
208 | return String.format("%s:%s", project.getPath(), taskToRun);
209 | }
210 | }
211 |
212 | }
213 |
--------------------------------------------------------------------------------
/src/main/java/io/github/crimix/changedprojectstask/utils/CollectingOutputStream.java:
--------------------------------------------------------------------------------
1 | package io.github.crimix.changedprojectstask.utils;
2 |
3 | import org.apache.commons.exec.LogOutputStream;
4 |
5 | import java.util.LinkedList;
6 | import java.util.List;
7 |
8 | public class CollectingOutputStream extends LogOutputStream {
9 |
10 | private final List lines = new LinkedList<>();
11 |
12 | @Override
13 | protected void processLine(String line, int level) {
14 | lines.add(line);
15 | }
16 |
17 | /**
18 | * Gets the lines collected by the output stream.
19 | * @return the lines collected by the output stream.
20 | */
21 | public List getLines() {
22 | return lines;
23 | }
24 |
25 | /**
26 | * Returns whether this stream collected any lines.
27 | * @return true if the output stream collected lines from the output.
28 | */
29 | public boolean isNotEmpty() {
30 | return !isEmpty();
31 | }
32 |
33 | /**
34 | * Returns whether this stream did not collect any lines.
35 | * @return true if the output stream did not collect any lines from the output.
36 | */
37 | public boolean isEmpty() {
38 | return lines.isEmpty();
39 | }
40 |
41 | @Override
42 | public String toString() {
43 | return String.join("\n", lines);
44 | }
45 | }
--------------------------------------------------------------------------------
/src/main/java/io/github/crimix/changedprojectstask/utils/GitDiffMode.java:
--------------------------------------------------------------------------------
1 | package io.github.crimix.changedprojectstask.utils;
2 |
3 | import java.util.Arrays;
4 | import java.util.stream.Collectors;
5 |
6 | /**
7 | * The available modes to use when trying to get the git diff
8 | */
9 | public enum GitDiffMode {
10 | COMMIT("commit"),
11 | BRANCH("branch"),
12 | BRANCH_TWO_DOT("branchTwoDotted"),
13 | BRANCH_THREE_DOT("branchThreeDotted");
14 |
15 | private final String commandOption;
16 |
17 | GitDiffMode(String commandOption) {
18 | this.commandOption = commandOption;
19 | }
20 |
21 | /**
22 | * Gets the command line optional name of the mode.
23 | * @return the command line optional name of the mode
24 | */
25 | public String getCommandOption() {
26 | return commandOption;
27 | }
28 |
29 | /**
30 | * Gets the mode from the command line option or throws an exception if the command line option does not match a mode.
31 | * @param commandOption the command line option
32 | * @return the mode corresponding to the command line option
33 | */
34 | public static GitDiffMode getMode(String commandOption) {
35 | return Arrays.stream(GitDiffMode.values())
36 | .filter(e -> e.getCommandOption().equals(commandOption))
37 | .findFirst()
38 | .orElseThrow(() -> new IllegalStateException(String.format("Unknown compare mode %s available [%s]", commandOption, GitDiffMode.getAvailableOptions())));
39 | }
40 |
41 | /**
42 | * Gets the available command line options as a string
43 | * @return the available command line options as a string
44 | */
45 | private static String getAvailableOptions() {
46 | return Arrays.stream(GitDiffMode.values())
47 | .map(GitDiffMode::getCommandOption)
48 | .sorted()
49 | .collect(Collectors.joining(", "));
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/java/io/github/crimix/changedprojectstask/utils/LoggingOutputStream.java:
--------------------------------------------------------------------------------
1 | package io.github.crimix.changedprojectstask.utils;
2 |
3 | import org.apache.commons.exec.LogOutputStream;
4 |
5 | import java.util.function.Consumer;
6 |
7 | public class LoggingOutputStream extends LogOutputStream {
8 |
9 | private final Consumer logFunc;
10 |
11 | public LoggingOutputStream(Consumer logFunc) {
12 | this.logFunc = logFunc;
13 | }
14 |
15 | @Override
16 | protected void processLine(String line, int level) {
17 | logFunc.accept(line);
18 | }
19 |
20 | }
--------------------------------------------------------------------------------
/src/main/java/io/github/crimix/changedprojectstask/utils/Pair.java:
--------------------------------------------------------------------------------
1 | package io.github.crimix.changedprojectstask.utils;
2 |
3 | import lombok.Data;
4 |
5 | /**
6 | * Simple pair class as java does not have a built-in one
7 | * The getters are auto-generated by Lombok
8 | */
9 | @Data
10 | public class Pair {
11 | private final K key;
12 | private final V value;
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/io/github/crimix/changedprojectstask/utils/Properties.java:
--------------------------------------------------------------------------------
1 | package io.github.crimix.changedprojectstask.utils;
2 |
3 | /**
4 | * Configurable properies that can be used in gralde using the -P prefix
5 | * Like -PchangedProjectsTask.enable
6 | */
7 | public class Properties {
8 | private static final String PREFIX = "changedProjectsTask.";
9 |
10 | public static final String ENABLE = PREFIX + "run";
11 | public static final String ENABLE_COMMANDLINE = PREFIX + "runCommandLine";
12 | public static final String CURRENT_COMMIT = PREFIX + "commit";
13 | public static final String PREVIOUS_COMMIT = PREFIX + "prevCommit";
14 | public static final String COMMIT_MODE = PREFIX + "compareMode";
15 | public static final String TASK_TO_RUN = PREFIX + "taskToRun";
16 | public static final String COMMANDLINE_ARGS = PREFIX + "commandLineArgs";
17 | }
18 |
--------------------------------------------------------------------------------
/src/test/java/io/github/crimix/changedprojectstask/providers/GitCommandProviderTest.java:
--------------------------------------------------------------------------------
1 | package io.github.crimix.changedprojectstask.providers;
2 |
3 | import io.github.crimix.changedprojectstask.utils.GitDiffMode;
4 | import org.assertj.core.api.Assertions;
5 | import org.gradle.api.Project;
6 | import org.gradle.testfixtures.ProjectBuilder;
7 | import org.junit.jupiter.params.ParameterizedTest;
8 | import org.junit.jupiter.params.provider.Arguments;
9 | import org.junit.jupiter.params.provider.MethodSource;
10 |
11 | import java.util.Optional;
12 | import java.util.stream.Stream;
13 |
14 | public class GitCommandProviderTest {
15 |
16 | private static final String CURR = "curr";
17 | private static final String PREV = "prev";
18 | private static final String NOT_SET = null;
19 |
20 | public static Stream provideStringsForIsBlank() {
21 | return Stream.of(
22 | Arguments.of(NOT_SET, NOT_SET, GitDiffMode.COMMIT, false, "git diff --name-only HEAD~ HEAD"),
23 | Arguments.of(CURR, NOT_SET, GitDiffMode.COMMIT, false, "git diff --name-only curr~ curr"),
24 | Arguments.of(CURR, PREV, GitDiffMode.COMMIT, false, "git diff --name-only prev~ curr"),
25 | Arguments.of(NOT_SET, PREV, GitDiffMode.COMMIT, true, "[COMMIT] When using changedProjectsTask.prevCommit then changedProjectsTask.commit must also be specified"),
26 |
27 | Arguments.of(NOT_SET, NOT_SET, GitDiffMode.BRANCH, true, "[BRANCH] changedProjectsTask.prevCommit must always be specified"),
28 | Arguments.of(CURR, NOT_SET, GitDiffMode.BRANCH, true, "[BRANCH] changedProjectsTask.prevCommit must always be specified"),
29 | Arguments.of(CURR, PREV, GitDiffMode.BRANCH, false, "git diff --name-only prev curr"),
30 | Arguments.of(NOT_SET, PREV, GitDiffMode.BRANCH, false, "git diff --name-only prev HEAD"),
31 |
32 | Arguments.of(NOT_SET, NOT_SET, GitDiffMode.BRANCH_TWO_DOT, true, "[BRANCH_TWO_DOT] changedProjectsTask.prevCommit must always be specified"),
33 | Arguments.of(CURR, NOT_SET, GitDiffMode.BRANCH_TWO_DOT, true, "[BRANCH_TWO_DOT] changedProjectsTask.prevCommit must always be specified"),
34 | Arguments.of(CURR, PREV, GitDiffMode.BRANCH_TWO_DOT, false, "git diff --name-only prev..curr"),
35 | Arguments.of(NOT_SET, PREV, GitDiffMode.BRANCH_TWO_DOT, false, "git diff --name-only prev.."),
36 |
37 | Arguments.of(NOT_SET, NOT_SET, GitDiffMode.BRANCH_THREE_DOT, true, "[BRANCH_THREE_DOT] changedProjectsTask.prevCommit must always be specified"),
38 | Arguments.of(CURR, NOT_SET, GitDiffMode.BRANCH_THREE_DOT, true, "[BRANCH_THREE_DOT] changedProjectsTask.prevCommit must always be specified"),
39 | Arguments.of(CURR, PREV, GitDiffMode.BRANCH_THREE_DOT, false, "git diff --name-only prev...curr"),
40 | Arguments.of(NOT_SET, PREV, GitDiffMode.BRANCH_THREE_DOT, false, "git diff --name-only prev...")
41 | );
42 | }
43 |
44 | @ParameterizedTest
45 | @MethodSource("provideStringsForIsBlank")
46 | public void test(String current, String previous, GitDiffMode mode, boolean exception, String expected) {
47 | Project project = ProjectBuilder.builder()
48 | .withName("root")
49 | .build();
50 |
51 | GitCommandProvider provider = new GitCommandProvider(project);
52 | if (!exception) {
53 | String actual = provider.evaluate(mode, Optional.ofNullable(current), Optional.ofNullable(previous));
54 | Assertions.assertThat(actual)
55 | .isEqualTo(expected);
56 | } else {
57 | Assertions.assertThatExceptionOfType(IllegalStateException.class)
58 | .isThrownBy(() -> provider.evaluate(mode, Optional.ofNullable(current), Optional.ofNullable(previous)))
59 | .withMessage(expected);
60 | }
61 |
62 | }
63 | }
--------------------------------------------------------------------------------