├── .gitignore ├── README.md ├── build.gradle └── config.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .gradle 3 | .groovy 4 | .settings 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #multiproject-git-gradle 2 | 3 | ##Overview 4 | 5 | This is gradle script for multiple git-repository setup, configuration and build. It supports automated cloning/pulling git-repositories (from any repositories, supported by JGit) and typical gradle tasks ("build", "clean", etc) that can be 6 | run against some or all repositories. Repositories can be supplied with inter-repository dependencies, so that particular order of assembly/testing/etc is guaranteed. 7 | 8 | Many thanks to the creators of gradle-git plugin ( https://github.com/ajoberstar/gradle-git ) for their excellent work, 9 | which is used by multiproject-git-gradle. 10 | 11 | **Content of this document** 12 | 13 | * [Required files](#required-files) 14 | * [Command line syntax](#command-line-syntax) 15 | * [Supported tasks](#supported-tasks) 16 | * [build task](#build-task) 17 | * [buildApps task](#buildapps-task) 18 | * [buildExamples task](#buildexamples-task) 19 | * [clean task](#clean-task) 20 | * [gitBranchList task](#gitbranchlist-task) 21 | * [gitStatus task](#gitstatus-task) 22 | * [update task](#update-task) 23 | * [uploadArchives task](#uploadarchives-task) 24 | * [release task](#release-task) 25 | * [Configuration](#configuration) 26 | * [Specifying projects](#specifying-projects) 27 | * [Configuring inter-project dependencies](#configuring-inter-project-dependencies) 28 | * [Configuring git-repositories](#configuring-git-repositories) 29 | * [Configuring build property](#configuring-build-property) 30 | * [Configuring apps property](#configuring-apps-property) 31 | * [Configuring examples property](#configuring-examples-property) 32 | * [Configuring skipTests property](#configuring-skiptests-property) 33 | * [Automated Release Feature (ARF)](#automated-release-feature) 34 | * [ARF Requirements](#arf-requirements) 35 | * [How to start using ARF](#how-to-start-using-arf) 36 | * [ARF side effects](#arf-side-effects) 37 | * [ARF release versions](#arf-release-versions) 38 | * [ARF new versions](#arf-new-versions) 39 | * [DSL for custom version increments](#dsl-for-custom-version-increments) 40 | * [releaseNoPush property](#releasenopush-property) 41 | * [releaseNoCommit property](#releasenocommit-property) 42 | * [Publishing to maven repositories](#publishing-to-maven-repositories) 43 | * [Prerequisites](#prerequisites) 44 | * [Publishing to local maven repo](#publishing-to-local-maven-repo) 45 | * [Publishing to remote maven repo (artifactory)](#publishing-to-remote-maven-repo-artifactory) 46 | * [Copyright and License](#copyright-and-license) 47 | 48 | ##Required files 49 | 50 | To start using multiproject-git-gradle, you need two files: 51 | 52 | "build.gradle" - taken from this repository, this is gradle script. 53 | 54 | "config.gradle" - you write it yourself, according to your needs. You can use "config.gradle" from this repository 55 | as an example/starting-point for editing. See more information on this in chapter [Configuration](#configuration). 56 | 57 | ##Command line syntax 58 | 59 | Like with any other gradle script: 60 | 61 | ```shell 62 | gradle [some task names specified here] 63 | ``` 64 | 65 | See [Supported Tasks](#supported-tasks) for more information on concrete tasks and their semantics. 66 | 67 | It is possible to start gradle without task: 68 | 69 | ```shell 70 | gradle 71 | ``` 72 | 73 | then the default task [buildApps](#buildapps-task) is executed. 74 | 75 | ##Supported tasks 76 | 77 | ###build task 78 | 79 | build task allows to build multiple gradle projects (from different git-repositories) in an automated way. It does the following: 80 | 81 | Iterates all projects described in [configuration](#configuration), performs the following for each project: 82 | 83 | 1. Checks whether project exists in the file system. If not, the project is cloned from [git-repository](#configuring-git-repositories). 84 | 85 | 2. Checks whether the project has [build property](#configuring-build-property) and configures project-level "build" task. 86 | 87 | 3. Checks whether the project has [dependsOn](#configuring-inter-project-dependencies) property. if it does, the dependencies are built first. 88 | 89 | 4. The project itself is being built, according to [build property](#configuring-build-property). 90 | 91 | Note that "build" task does not depend on "update" task, but all "build" steps are performed strictly after 92 | corresponding "update" steps. That means: 93 | 94 | a) if you routinely run "gradle build", it will not pull the changes from git-sources, but only compile things for you. 95 | Only if some projects are missing, they will be cloned from git-sources. 96 | 97 | b) if you run "gradle update build", it is guaranteed, that every project is first updated (cloned or pulled from repository) 98 | and only then built. 99 | 100 | ###buildApps task 101 | 102 | buildApps is an optional task, allowing to build "apps" sub-projects of multiple gradle projects (from different git-repositories) in an automated way. 103 | It does the following: 104 | 105 | First it builds all projects, as described in [build task](#build-task). 106 | 107 | Then it iterates all projects described in [configuration](#configuration), performs the following for each project: 108 | 109 | 1. Checks whether the project has ["apps" property](#configuring-apps-property) and configures project-level "buildApps" task. 110 | 111 | 2. Tries to perform "gradle build" in [apps sub-folder](#configuring-apps-property) of the project folder. 112 | 113 | ###buildExamples task 114 | 115 | buildExamples is an optional task, allowing to build "examples" sub-projects of multiple gradle projects (from different git-repositories) in an automated way. 116 | It does the following: 117 | 118 | First it builds all projects, as described in [build task](#build-task). 119 | 120 | Then it iterates all projects described in [configuration](#configuration), performs the following for each project: 121 | 122 | 1. Checks whether the project has [examples](#configuring-examples-property) property. If it does not, the project is skipped (not built). 123 | 124 | 2. Tries to perform "gradle build" in [examples sub-folder](#configuring-examples-property) of the project folder. 125 | 126 | ###clean task 127 | 128 | clean task allows to clean multiple projects (from different git-repositories) in an automated way. 129 | 130 | ###gitBranchList task 131 | 132 | TODO: document this task! 133 | 134 | ###gitStatus task 135 | 136 | TODO: document this task! 137 | 138 | ###update task 139 | 140 | update task allows to clone/pull multiple projects (not necessarily gradle-projects) from git-sources in an automated way. It does the following: 141 | 142 | Iterates all projects described in [configuration](#configuration), checks each project, whether it exists, then: 143 | 144 | 1. If project does not exist, it is cloned from [git-repository](#configuring-git-repositories). 145 | 146 | 2. If project exists, it is pulled from [git-repository](#configuring-git-repositories). 147 | 148 | ###uploadArchives task 149 | 150 | TODO: document this task! 151 | 152 | ### release task 153 | 154 | This task performs full release process on multiple git projects. 155 | 156 | See more information at [Automated Release Feature (ARF)](#automated-release-feature). 157 | 158 | ##Configuration 159 | 160 | "build.gradle" (of multiproject-git-gradle) reads "config.gradle" file in order to "understand" project structure. 161 | "config.gradle" is being interpreted by gradle, therefore it should comply to gradle syntax. 162 | 163 | Absolutely minimal version of "config.gradle" (which is useless, but "well-formed" in a sence 164 | of multiproject-git-gradle) looks like this: 165 | 166 | ```groovy 167 | multiproject { 168 | } 169 | ``` 170 | 171 | ###Specifying projects 172 | 173 | The simplest usable configuration looks like this: 174 | 175 | ```groovy 176 | multiproject { 177 | project name: 'ProjectA' 178 | project name: 'ProjectB' 179 | } 180 | ``` 181 | 182 | where ProjectA and ProjectB must designate existing subfolders of the current folder. 183 | 184 | **Effect:** when you run "gradle build", multiproject-git-gradle will consequently build each project specified in multiproject configuration. 185 | 186 | ###Configuring inter-project dependencies 187 | 188 | You can specify inter-project dependencies the following way: 189 | 190 | ```groovy 191 | multiproject { 192 | project name: 'ProjectA' 193 | project name: 'ProjectB', dependsOn: 'ProjectA' 194 | project name: 'ProjectC', dependsOn: 'ProjectA' 195 | project name: 'ProjectD', dependsOn: [ 'ProjectB', 'ProjectC' ] 196 | } 197 | ``` 198 | 199 | Rules: 200 | 201 | * dependsOn can be specified as a string or an array of strings. 202 | * dependsOn refers to projects (not tasks). 203 | * dependsOn defines the order in which projects are updated (cloned/pulled from their git-repositories) and built. 204 | * dependsOn is transitive. In the example above, "ProjectD" directly depends on "ProjectB", "ProjectC" and, indirectly, 205 | on "ProjectA". 206 | 207 | ###Configuring git-repositories 208 | 209 | There are two ways to specify git-repositories: via gitBase property and via gitSource property. 210 | 211 | ####Configuring gitBase property 212 | 213 | gitBase specifies "base" URL, from where the project(s) come. It supports all protocols supported by JGit library 214 | (for example, "http", "https", "git", "ssh"). gitBase can be specified "globally", for project group or for individual projects: 215 | 216 | ```groovy 217 | multiproject { 218 | gitBase = 'https://github.com/someUser' 219 | project name: 'ProjectA' 220 | git gitBase: 'https://github.com/anotherUser', { 221 | project name: 'ProjectB' 222 | project name: 'ProjectC' 223 | } 224 | project name: 'ProjectD', gitBase: 'https://github.com/thirdUser' 225 | } 226 | ``` 227 | 228 | Rules: 229 | 230 | * Whenever a project has gitBase property, the script will use it for calculating git-repository URI the following way: 231 | URI = gitBase + "/" + name + ".git" 232 | 233 | * Whenever a project is enclosed by "git" group, defining gitBase property, the script will use it for calculating git-repository. 234 | 235 | * Whenever a project does not have gitBase property and is not enclosed by "git" group, the script will use "global" gitBase property for calculating git-repository. 236 | 237 | In the concrete example (above) "ProjectA" will be cloned/pulled from "https://github.com/someUser/ProjectA.git", 238 | "ProjectB" will be cloned/pulled from "https://github.com/anotherUser/ProjectB.git", "ProjectC" will be cloned/pulled from 239 | "https://github.com/anotherUser/ProjectC.git" and "ProjectD" will be cloned/pulled from "https://github.com/thirdUser/ProjectD.git". 240 | 241 | ####Configuring gitSource property 242 | 243 | gitSource property represents complete URI to git-repository and is not combined with anything. 244 | It supports all protocols supported by JGit library (for example, "http", "https", "git", "ssh"). 245 | 246 | ```groovy 247 | multiproject { 248 | gitBase = 'https://github.com/someUser' 249 | project name: 'ProjectA' 250 | project name: "ProjectB", gitSource: "https://anotherdomain.com/someDifferentProjectName.git" 251 | } 252 | ``` 253 | 254 | Rules: 255 | 256 | * Whenever a project has gitSource property, the script will use it as complete URI for cloning/pulling. 257 | gitBase property (either per-project, per-group or global) is ignored for such project. 258 | 259 | * gitSource can be specified only for individual projects, there is no global gitSource property. 260 | 261 | In the concrete example (above) "ProjectA" will be cloned/pulled from "https://github.com/someUser/ProjectA.git", 262 | "ProjectB" will be cloned/pulled from "https://anotherdomain.com/someDifferentProjectName.git". 263 | 264 | ####Configuring gitNameSeparator and gitNameSuffix 265 | 266 | By default, if a project does not have gitSource property, it's effective git repository URI is calculated 267 | by formula: URI = gitBase + "/" + name + ".git" 268 | You can fine-tune this formula by specifying gitNameSeparator and gitNameSuffix: either globally, per-group or per-project: 269 | 270 | ```groovy 271 | multiproject { 272 | gitBase = 'https://github.com/someUser' 273 | project name: 'ProjectA' 274 | git gitBase: 'git@someGitoliteRepository', gitNameSeparator: ':', gitNameSuffix: '', { 275 | project name: 'ProjectB' 276 | project name: 'ProjectC' 277 | } 278 | } 279 | ``` 280 | 281 | In the concrete example (above) "ProjectA" will be cloned/pulled from "https://github.com/someUser/ProjectA.git", 282 | "ProjectB" will be cloned/pulled from "git@someGitoliteRepository:ProjectB", "ProjectC" will be cloned/pulled from 283 | "git@someGitoliteRepository:ProjectC". 284 | 285 | ###Configuring build property 286 | 287 | By default, multiproject-git-gradle builds all projects specified in multiproject configuration. 288 | You can fine-tune what and how is built by using "build" property: 289 | 290 | ```groovy 291 | multiproject { 292 | project name: 'ProjectA' 293 | project name: 'ProjectB', build: false 294 | project name: 'ProjectC', build: 'subFolder' 295 | } 296 | ``` 297 | 298 | Rules: 299 | 300 | * Whenever a project does not have "build" property, gradle script will run "build" task against projectDir folder 301 | 302 | * Whenever a project has "build" property and it evaluates to false, gradle script will not build the given project 303 | 304 | * Whenever a project has "build" property and it evaluates to string, gradle script will run "build" task against the combined folder "$projectDir/$build". 305 | 306 | In the concrete example (above) "ProjectA" will be built in "ProjectA" subfolder, 307 | "ProjectB" will not be built, "ProjectC" will be built in "ProjectC/subFolder" subfolder. 308 | 309 | ###Configuring apps property 310 | 311 | If you specify "apps" property for a given project, multiproject-git-gradle defines additional task "buildApps", 312 | which can be called on command line as "gradle buildApps". 313 | 314 | ```groovy 315 | multiproject { 316 | project name: 'ProjectA', apps: true 317 | project name: 'ProjectB', apps: "applications" 318 | } 319 | ``` 320 | 321 | Rules: 322 | 323 | * Whenever a project has "apps" property and it evaluates to true, gradle script defines "buildApps" task, 324 | which will be run against the combined folder "$projectDir/apps". 325 | 326 | * Whenever a project has "apps" property and it evaluates to string, gradle script defines "buildApps" task, 327 | which will be run against the combined folder "$projectDir/$apps". 328 | 329 | * Otherwise "buildApps" task is not defined for the given project. 330 | 331 | ###Configuring examples property 332 | 333 | If you specify "examples" property for a given project, multiproject-git-gradle defines additional task "buildExamples", 334 | which can be called on command line as "gradle buildExamples". 335 | 336 | ```groovy 337 | multiproject { 338 | project name: 'ProjectA', examples: true 339 | project name: 'ProjectB', examples: "samples" 340 | } 341 | ``` 342 | 343 | Rules: 344 | 345 | * Whenever a project has "examples" property and it evaluates to true, gradle script defines "buildExamples" task, 346 | which will be run against the combined folder "$projectDir/examples". 347 | 348 | * Whenever a project has "examples" property and it evaluates to string, gradle script defines "buildExamples" task, 349 | which will be run against the combined folder "$projectDir/$examples". 350 | 351 | * Otherwise "buildExamples" task is not defined for the given project. 352 | 353 | ### Configuring skipTests property 354 | 355 | Boolean property skipTests allows to enable and disable unit-tests on all projects or individual projects: 356 | 357 | ```groovy 358 | multiproject { 359 | skipTests = true 360 | project name: 'ProjectA' 361 | project name: 'ProjectB', skipTests: false 362 | } 363 | ``` 364 | 365 | By default skipTests is globally set to false, i.e. unit-tests are enabled. 366 | 367 | ## Automated Release Feature 368 | 369 | Since version 1.0.12 multiproject-git-gradle implements Automated Release Feature (AFR), which allows to release the software from multiple git repositories. 370 | 371 | AFR is implemented as part of multiproject-git-gradle and accessible as a single task: "release". As with other tasks of multiproject-git-gradle, the file "config.gradle" defines upstream git repositories and inter-repository dependencies. 372 | 373 | ### ARF Requirements 374 | 375 | 1. All involved git repositories are required to have root "build.gradle", which includes [townsfolk/gradle-release](https://github.com/townsfolk/gradle-release) plugin. 376 | 377 | 2. All involved git repositories are required to have "gradle.properties" file, containing at least "version" property. 378 | 379 | 3. AFR uses and updates versions exlusively via "gradle.properties" file. It is expected that none of "build.gradle" files (either at the roots of git-repositories or in subprojects) contain any "hardcoded" dependency versions to the artifacts of another git repository. Any inter-repository dependencies should be managed via dependencies of kind: compile "group:artifact:${repoName}_version", where "${repoName}_version" is defined as property in "gradle.properties" and repoName is the name of existing git repository listed in "config.gradle". 380 | 381 | ### How to start using ARF 382 | 383 | 1. Add townsfolk/gradle-release plugin to root "build.gradle" of every git repository. 384 | 385 | 2. Add "gradle.properties" with at least "version" property to every git repository. 386 | 387 | 3. Convert all inter-repository dependencies to managed dependencies with versions in "gradle.properties" in the format "${repoName}_version=versionValue". 388 | 389 | ### ARF side effects 390 | 391 | 1. ARF performs all standard checks (implemented by townsfolk/gradle-release plugin) against all involved git repositories. If any check fails, the whole release process is cancelled and error message is shown. 392 | 393 | 2. ARF modifies "gradle.properties" files of all involved git repositories, replacing current versions with release versions. See section [ARF release versions](#arf-release-versions) for details. 394 | 395 | 3. ARF checks for the presence of snapshot dependencies in all involved git repositories. If some git repositories contain snapshot dependencies, the whole release process is cancelled and error message is shown. 396 | 397 | 4. ARF builds code in all involved git repositories. The inter-repository order of build is defined by "config.gradle". 398 | 399 | 5. ARF performs the following in each git repository: creates a commit, creates a release tag and pushes to the origin. Push can be disabled by [releaseNoPush property](#releasenopush-property). Commit can be disabled by [releaseNoCommit property](#releasenocommit-property). 400 | 401 | 6. ARF modifies "gradle.properties" files of all involved git repositories, replacing current versions with new versions. See section [ARF new versions](#arf-new-versions) for details. 402 | 403 | 7. ARF performs the following in each git repository: creates a commit and pushes to the origin. Push can be disabled by [releaseNoPush property](#releasenopush-property). Commit can be disabled by [releaseNoCommit property](#releasenocommit-property). 404 | 405 | 8. ARF restores the changed "gradle.properties" files to their original state in case of errors. 406 | 407 | 9. ARF can clone some or all repositories (mentioned in "config.gradle") from upstream locations, if they are not already present in the root directory. 408 | 409 | 10. If some repositories are already present in the root directory, automated release feature will perform git-pull on them. 410 | 411 | ### ARF release versions 412 | 413 | ARF uses the following algorithm for defining release version: 414 | 415 | It takes "version" property from the given project and matches it against regex: 416 | 417 | ```regex 418 | /(\d+)([^\d]*$)/ 419 | ``` 420 | 421 | if project version matches this pattern, the release version is calculated as: 422 | 423 | ```groovy 424 | m.replaceAll(m[0][1]) 425 | ``` 426 | 427 | where m is an object of class java.util.regex.Matcher (the result of regex match). 428 | 429 | Example: if original version is "1.0-SNAPSHOT", the calculated release version will be "1.0". 430 | 431 | ### ARF new versions 432 | 433 | ARF uses the following algorithm for defining new version: 434 | 435 | It takes "version" property from the given project and matches it against regex: 436 | 437 | ```regex 438 | /(\d+)([^\d]*$)/ 439 | ``` 440 | 441 | if project version matches this pattern, the new version is calculated as: 442 | 443 | ```groovy 444 | m.replaceAll("${ (m[0][1] as int) + 1 }${ m[0][2] }") } 445 | ``` 446 | 447 | where m is an object of class java.util.regex.Matcher (the result of regex match). 448 | 449 | Example: if original version is "1.0-SNAPSHOT", the calculated new version will be "1.1-SNAPSHOT". 450 | 451 | ### DSL for custom version increments 452 | 453 | You can define your own rules for release version and new version with the help of multiproject-git-gradle DSL: 454 | 455 | ```groovy 456 | multiproject { 457 | git baseDir: 'upstream_repos', { 458 | project name: 'project1' 459 | project name: 'project3' 460 | project name: 'project2', dependsOn: [ 'project1', 'project3' ], { 461 | releaseVersion(/(\d+)([^\d]*$)/, { m -> m.replaceAll("${ m[0][1] }-RELEASE") }) 462 | newVersion(/(\d+)([^\d]*$)/, { m -> m.replaceAll("${ m[0][1] }-UNSTABLE") }) 463 | } 464 | } 465 | } 466 | ``` 467 | 468 | in this example we define that project2 gets release versions that look like "X.Y-RELEASE" and new versions that look like "X.Y-UNSTABLE". 469 | 470 | releaseVersion and newVersion elements are actually functions, accepting two arguments: first one is regex, second one is closure. 471 | 472 | Closure is only invoked when the given regex is successfully matched against "version" property of "gradle.properties" file. Closure receives an object of class java.util.regex.Matcher (the result of regex match) as a parameter. Closure must return a string (or an object meaningfully convertible to string) containing version. 473 | 474 | It is possible to specify "global" releaseVersion and newVersion elements: 475 | 476 | ```groovy 477 | multiproject { 478 | releaseVersion(/(\d+)([^\d]*$)/, { m -> m.replaceAll("${ m[0][1] }-release") }) 479 | newVersion(/(\d+)([^\d]*$)/, { m -> m.replaceAll("${ m[0][1] }-unstable") }) 480 | git baseDir: 'upstream_repos', { 481 | project name: 'project1' 482 | project name: 'project3' 483 | project name: 'project2', dependsOn: [ 'project1', 'project3' ], { 484 | releaseVersion(/(\d+)([^\d]*$)/, { m -> m.replaceAll("${ m[0][1] }-RELEASE") }) 485 | newVersion(/(\d+)([^\d]*$)/, { m -> m.replaceAll("${ m[0][1] }-UNSTABLE") }) 486 | } 487 | } 488 | } 489 | ``` 490 | 491 | in this example we define that project1 and project3 get gets release versions that look like "X.Y-release" and new versions that look like "X.Y-unstable", while project2 gets release versions that look like "X.Y-RELEASE" and new versions that look like "X.Y-UNSTABLE". 492 | 493 | It is possible to specify multiple releaseVersion and newVersion elements in the same scope, each one with it's own regex and closure. 494 | 495 | If there are multiple releaseVersion elements with the same regex in the same scope, only the last one is used, the previous ones are ignored. 496 | 497 | If there are multiple newVersion elements with the same regex in the same scope, only the last one is used, the previous ones are ignored. 498 | 499 | ### releaseNoPush property 500 | 501 | releaseNoPush boolean property could be defined either in context of multiproject element or in context of project element: 502 | 503 | ```groovy 504 | multiproject { 505 | releaseNoPush = true 506 | git baseDir: 'upstream_repos', { 507 | project name: 'project1', releaseNoPush: true 508 | project name: 'project3' 509 | project name: 'project2', dependsOn: [ 'project1', 'project3' ] 510 | } 511 | } 512 | ``` 513 | 514 | when releaseNoPush=true, ARF commits release and new versions, but does not push anything upstream. 515 | 516 | ### releaseNoCommit property 517 | 518 | releaseNoCommit boolean property could be defined either in context of multiproject element or in context of project element: 519 | 520 | ```groovy 521 | multiproject { 522 | releaseNoCommit = true 523 | git baseDir: 'upstream_repos', { 524 | project name: 'project1', releaseNoCommit: true 525 | project name: 'project3' 526 | project name: 'project2', dependsOn: [ 'project1', 'project3' ] 527 | } 528 | } 529 | ``` 530 | 531 | when releaseNoCommit=true, ARF does not commit nor push release and new versions. 532 | 533 | ### Publishing to maven repositories 534 | 535 | Since version 1.0.25 multiproject-git-gradle supports publishing both to local maven repo and any remote maven repo (like artifactory). Below are the detailed instructions. 536 | 537 | #### Prerequisites 538 | 539 | - set of git repositories, each git repository contains one or more JVM-based projects 540 | 541 | - those projects, that are intended for publishing to local or remote maven repo, must have an instruction: 542 | 543 | ```groovy 544 | apply plugin: 'maven' 545 | ``` 546 | 547 | Note that applying maven plugin only at root project will not work in case of nested projects. You'll need to add `apply plugin: 'maven'` to every subproject that needs to be published. This can be automated via: 548 | 549 | ```groovy 550 | subprojects { 551 | apply plugin: 'maven' 552 | } 553 | ``` 554 | 555 | #### Publishing to local maven repo 556 | 557 | 1. In "config.gradle": add releaseDeployTasks property to either multiproject element or to project element: 558 | 559 | ```groovy 560 | multiproject { 561 | releaseDeployTasks = ['install'] 562 | git baseDir: 'upstream_repos', { 563 | project name: 'project1' 564 | project name: 'project3' 565 | project name: 'project2', dependsOn: [ 'project1', 'project3' ] 566 | } 567 | } 568 | ``` 569 | 570 | 2. Invoke on command line: `gradle release`. 571 | 572 | **Effect**: the release versions and new versions will be installed to local maven repo (~/.m2) after the commit to git. The exact sequence is: 573 | 574 | - prepare release version 575 | - commit release version to git 576 | - publish release version to local maven repo 577 | - prepare new version 578 | - commit new version to git 579 | - publish new version to local maven repo 580 | 581 | #### Publishing to remote maven repo (artifactory) 582 | 583 | 1. In "config.gradle": add releaseDeployTasks property to either multiproject element or to project element: 584 | 585 | ```groovy 586 | multiproject { 587 | releaseDeployTasks = ['uploadArchives'] 588 | git baseDir: 'upstream_repos', { 589 | project name: 'project1' 590 | project name: 'project3' 591 | project name: 'project2', dependsOn: [ 'project1', 'project3' ] 592 | } 593 | } 594 | ``` 595 | 596 | 2. Create new file "~/.gradle/init.d/deploy.gradle", insert code: 597 | 598 | ```groovy 599 | rootProject { 600 | ext { 601 | artifactoryReleases = [ url: 'protocol-host-and-port/artifactory/libs-release-local', user: 'UploadUser', password : 'UploadPassword' ] 602 | artifactorySnapshots = [ url: 'protocol-host-and-port/artifactory/libs-snapshot-local', user: 'UploadUser', password : 'UploadPassword' ] 603 | } 604 | } 605 | 606 | afterProject { proj -> 607 | 608 | if(proj.plugins.findPlugin('maven') && proj.uploadArchives instanceof org.gradle.api.tasks.Upload) { 609 | 610 | proj.configurations { 611 | deployerJars 612 | } 613 | 614 | proj.dependencies { 615 | deployerJars 'org.apache.maven.wagon:wagon-http:2.4' 616 | } 617 | 618 | proj.uploadArchives { 619 | repositories.mavenDeployer { 620 | configuration = proj.configurations.deployerJars 621 | if(proj.version.contains('-SNAPSHOT')) 622 | repository(url: rootProject.ext.artifactorySnapshots.url) { 623 | authentication(userName: rootProject.ext.artifactorySnapshots.user, password: rootProject.ext.artifactorySnapshots.password) 624 | } 625 | else 626 | repository(url: rootProject.ext.artifactoryReleases.url) { 627 | authentication(userName: rootProject.ext.artifactoryReleases.user, password: rootProject.ext.artifactoryReleases.password) 628 | } 629 | } 630 | } 631 | } 632 | } 633 | ``` 634 | 635 | where "protocol-host-and-port" should be replaced with concrete protocol, host and port of target maven repository, "UploadUser" should be replaced with an existing user and "UploadPassword" should be replaced with a valid password. 636 | 637 | 3. Invoke on command line: `gradle release`. 638 | 639 | **Effect**: the release versions and new versions will be deployed to remote maven repo after the commit to git. The exact sequence is: 640 | 641 | - prepare release version 642 | - commit release version to git 643 | - publish release version to remote maven repo 644 | - prepare new version 645 | - commit new version to git 646 | - publish new version to remote maven repo 647 | 648 | ##Copyright and License 649 | 650 | Copyright 2013 (c) Andrey Hihlovskiy 651 | 652 | All versions, present and past, of "multiproject-git-gradle" script are licensed under MIT license: 653 | 654 | * [MIT](http://opensource.org/licenses/MIT) 655 | 656 | You are encouraged to use it to whatever purpose and whichever way, all for free, provided that you retain copyright 657 | notice at the beginning of the script. 658 | 659 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | /** 2 | * multiproject-git-gradle/build.gradle 3 | * 4 | * Gradle script automating git-clone/pull and compilation of multiple 5 | * inter-connected projects. 6 | * 7 | * Written by Andrey Hihlovskiy (akhikhl AT gmail DOT com). 8 | * Licensed under the MIT (http://opensource.org/licenses/MIT). 9 | * 10 | * @author Andrey Hihlovskiy 11 | * @version 1.0.36 12 | * @requires gradle 1.11+, git 1.8+ 13 | * 14 | * Full documentation and sources at: 15 | * https://github.com/akhikhl/multiproject-git-gradle 16 | **/ 17 | 18 | buildscript { 19 | repositories { 20 | jcenter() 21 | mavenCentral() 22 | } 23 | dependencies { classpath 'org.ajoberstar:gradle-git:0.8.0' } 24 | } 25 | 26 | import org.ajoberstar.grgit.Grgit 27 | import java.util.regex.Matcher 28 | 29 | boolean isString(x) { 30 | x instanceof String || x instanceof GString 31 | } 32 | 33 | def isCollection(obj) { 34 | return [Collection, Object[]].any { it.isAssignableFrom(obj.getClass()) } 35 | } 36 | 37 | def toCollection(obj) { 38 | return isCollection(obj) ? obj : [ obj ] 39 | } 40 | 41 | class Project extends Expando { 42 | 43 | Project() { 44 | } 45 | 46 | Project(Map properties) { 47 | super(properties) 48 | } 49 | 50 | void newVersion(String regex, Closure closure) { 51 | if(newVersion == null) 52 | newVersion = [:] 53 | newVersion[regex] = closure 54 | } 55 | 56 | void releaseVersion(String regex, Closure closure) { 57 | if(releaseVersion == null) 58 | releaseVersion = [:] 59 | releaseVersion[regex] = closure 60 | } 61 | } 62 | 63 | class MultiProjectExtension { 64 | String rootFolder 65 | def projects = [:] 66 | String gitBase 67 | String gitNameSeparator = '/' 68 | String gitNameSuffix = '.git' 69 | def releaseVersion = [ /(\d+)([^\d]*$)/ : { m -> m.replaceAll(m[0][1]) } ] 70 | def newVersion = [ /(\d+)([^\d]*$)/ : { m -> m.replaceAll("${ (m[0][1] as int) + 1 }${ m[0][2] }") } ] 71 | Boolean releaseNoCommit 72 | Boolean releaseNoPush 73 | String branch = 'master' 74 | Boolean failOnSnapshotDependencies 75 | List deployTasks = [] 76 | Boolean skipTests 77 | 78 | def git(Map options = [:], Closure closure) { 79 | def savedGitBase = gitBase 80 | gitBase = options.baseUrl ?: options.baseDir 81 | def savedGitNameSeparator = gitNameSeparator 82 | gitNameSeparator = options.nameSeparator ?: options.gitNameSeparator ?: gitNameSeparator 83 | def savedGitNameSuffix = gitNameSuffix 84 | gitNameSuffix = options.nameSuffix ?: options.gitNameSuffix ?: gitNameSuffix 85 | closure.delegate = this 86 | closure.resolveStrategy = Closure.DELEGATE_FIRST 87 | closure() 88 | gitNameSuffix = savedGitNameSuffix 89 | gitNameSeparator = savedGitNameSeparator 90 | gitBase = savedGitBase 91 | } 92 | 93 | boolean isString(x) { 94 | x instanceof String || x instanceof GString 95 | } 96 | 97 | void newVersion(String regex, Closure closure) { 98 | newVersion[regex] = closure 99 | } 100 | 101 | def project(proj, Closure closure = null) { 102 | String projName = isString(proj) ? proj : proj.name 103 | if(projects.containsKey(projName)) 104 | throw new Exception("Duplicate project definition: $projName") 105 | if(isString(proj)) 106 | proj = new Project([ name: proj as String ]) 107 | else 108 | proj = new Project(proj) 109 | if(closure) { 110 | closure.delegate = proj 111 | closure.resolveStrategy = Closure.DELEGATE_FIRST 112 | closure() 113 | } 114 | if(!proj.gitBase && !proj.gitSource) 115 | proj.gitBase = gitBase 116 | if(!proj.gitNameSeparator && !proj.gitSource) 117 | proj.gitNameSeparator = gitNameSeparator 118 | if(!proj.gitNameSuffix && !proj.gitSource) 119 | proj.gitNameSuffix = gitNameSuffix 120 | if(!proj.releaseVersion) 121 | proj.releaseVersion = releaseVersion 122 | else if(releaseVersion instanceof Map && proj.releaseVersion instanceof Map) 123 | proj.releaseVersion = releaseVersion + proj.releaseVersion 124 | if(!proj.newVersion) 125 | proj.newVersion = newVersion 126 | else if(newVersion instanceof Map && proj.newVersion instanceof Map) 127 | proj.newVersion = newVersion + proj.newVersion 128 | if(proj.releaseNoCommit == null) 129 | proj.releaseNoCommit = releaseNoCommit 130 | if(proj.releaseNoPush == null) 131 | proj.releaseNoPush = releaseNoPush 132 | if(proj.branch == null) 133 | proj.branch = branch 134 | if(proj.failOnSnapshotDependencies == null) { 135 | proj.failOnSnapshotDependencies = failOnSnapshotDependencies 136 | if(proj.failOnSnapshotDependencies == null) 137 | proj.failOnSnapshotDependencies = true 138 | } 139 | if(!proj.deployTasks) 140 | proj.deployTasks = deployTasks 141 | else if(deployTasks instanceof List && proj.deployTasks instanceof List) 142 | proj.deployTasks = deployTasks + proj.deployTasks 143 | if(proj.skipTests == null) 144 | proj.skipTests = skipTests 145 | projects[projName] = proj 146 | } 147 | 148 | void releaseVersion(String regex, Closure closure) { 149 | releaseVersion[regex] = closure 150 | } 151 | } 152 | 153 | project.extensions.create 'multiproject', MultiProjectExtension 154 | 155 | apply from: 'config.gradle' 156 | 157 | if(project.ext.has('extraConfig')) { 158 | def extraConfigs = project.ext.extraConfig.split(',') 159 | for(def extraConfig in extraConfigs) { 160 | if(!extraConfig.endsWith('.gradle')) 161 | extraConfig += '.gradle' 162 | apply from: extraConfig 163 | } 164 | } 165 | 166 | def rootFolder = (project.multiproject.rootFolder ? new File(project.projectDir, project.multiproject.rootFolder).canonicalFile : project.projectDir) 167 | 168 | task('nuke', group: 'cleanup') { 169 | description = 'Deletes local git repositories listed in configuration.' 170 | } 171 | 172 | if(!tasks.findByName('clean')) 173 | task('clean', group: 'cleanup') { 174 | description = 'Cleans up compilation results and other temporary files.' 175 | } 176 | 177 | clean.mustRunAfter { update } 178 | 179 | task('noupdate', group: 'git') { 180 | description = 'Inhibits pulling or cloning git repositories from upstream.' 181 | doLast { 182 | tasks.each { 183 | if(it.name =~ /(.+)_maybeClone/ || it.name =~ /(.+)_update/) 184 | it.enabled = false 185 | } 186 | } 187 | } 188 | 189 | task('maybeClone', group: 'git') { 190 | description = 'Clones repositories from upstream, if they do not exist.' 191 | mustRunAfter nuke 192 | mustRunAfter noupdate 193 | } 194 | 195 | task('update', group: 'git') { 196 | description = 'Pulls every git repository from upstream.' 197 | dependsOn maybeClone 198 | mustRunAfter noupdate 199 | } 200 | 201 | task('gitBranchList', group: 'git') { 202 | description = 'Prints a list of git branches in all git repositories.' 203 | } 204 | 205 | task('gitCommit', group: 'git') { 206 | description = 'Commits changes (if any) in all git repositories.' 207 | } 208 | 209 | task('gitRevert', group: 'git') { 210 | description = 'Reverts all changes in work directories of all git repositories.' 211 | } 212 | 213 | task('gitPush', group: 'git') { 214 | description = 'Pushes current branches of all git repositories to upstream.' 215 | } 216 | 217 | task('gitStatus', group: 'git') { 218 | description = 'Shows status of changed files in all git repositories.' 219 | } 220 | 221 | task('build', group: 'build') { 222 | description = 'Builds projects in repositories.' 223 | dependsOn maybeClone 224 | mustRunAfter update 225 | } 226 | 227 | task('uploadArchives', group: 'deployment') { 228 | description = 'Uploads archives of all git repositories to remote maven repository.' 229 | dependsOn build 230 | } 231 | 232 | // automated release feature ++ 233 | 234 | task('releaseChecks', group: 'release') { 235 | description = 'Checks all git repositories before release.' 236 | dependsOn update 237 | } 238 | 239 | task('releaseCollectVersions', group: 'release') { 240 | description = 'Collects version information on all git repositories.' 241 | dependsOn releaseChecks 242 | } 243 | task('releaseResolveVersions', group: 'release') { 244 | description = 'Resolves version information on all git repositories.' 245 | dependsOn releaseCollectVersions 246 | } 247 | 248 | task('releasePrepareReleaseVersions', group: 'release') { 249 | description = 'Assigns release versions to all git repositories.' 250 | dependsOn releaseResolveVersions 251 | } 252 | 253 | task('releaseCheckSnapshotDependencies', group: 'release') { 254 | description = 'Checks all git repositories for snapshot dependencies.' 255 | dependsOn releasePrepareReleaseVersions 256 | } 257 | 258 | task('release', group: 'release') { 259 | description = 'Releases the software from all git repositories.' 260 | dependsOn releaseCheckSnapshotDependencies 261 | finalizedBy { afterRelease } 262 | } 263 | 264 | Map versionMap = [:] 265 | 266 | def getProjectKey = { String projName -> 267 | projName.replaceAll(/[^0-9A-Za-z]/, '_') 268 | } 269 | 270 | def getVersionInfo = { String projName -> 271 | versionMap[getProjectKey(projName)] 272 | } 273 | 274 | task ('afterRelease', group: 'release') { 275 | description = 'Prints report after release is finished.' 276 | doLast { 277 | project.logger.warn 'Release is finished. The following repositories/projects were released:' 278 | def pad = { String s, int length -> 279 | (s ?: '').padRight(length, ' ') 280 | } 281 | project.logger.warn '{}| {} | {} | {}', pad('Repository', 16), pad('Branch', 16), pad('Release version', 16), pad('New version', 16) 282 | versionMap.each { projKey, versionInfo -> 283 | project.logger.warn '{}| {} | {} | {}', pad(versionInfo.originalProjectName, 16), pad(versionInfo.branch, 16), pad(versionInfo.releaseVersion, 16), pad(versionInfo.newVersion, 16) 284 | } 285 | } 286 | } 287 | 288 | project.gradle.taskGraph.afterTask { Task task, TaskState state -> 289 | if (state.failure && task.name =~ /(.+)_release(.+)$/) { 290 | project.logger.error 'Release process failed, reverting uncommitted changes:' 291 | versionMap.each { projKey, versionInfo -> 292 | def grgit = Grgit.open(versionInfo.projFolder.absolutePath) 293 | try { 294 | def status = grgit.status() 295 | if(status.unstaged.added || status.unstaged.modified || status.unstaged.removed || 296 | status.staged.added || status.staged.modified || status.staged.removed) { 297 | project.logger.warn '{} : reverting changes', versionInfo.originalProjectName 298 | def cmd = grgit.repository.jgit.checkout() 299 | cmd.setAllPaths(true) 300 | cmd.call() 301 | } 302 | } finally { 303 | grgit.close() 304 | } 305 | } 306 | } 307 | } 308 | 309 | // automated release feature -- 310 | 311 | def clonedRepositories = [] 312 | 313 | // first pass, we create per-project tasks 314 | project.multiproject.projects.each { projName, proj -> 315 | 316 | File projFolder = new File(rootFolder, new File(projName).getName()) 317 | 318 | task("${projName}_nuke") { 319 | nuke.dependsOn it 320 | doLast { 321 | project.logger.warn 'Deleting directory {}', projFolder 322 | projFolder.deleteDir() 323 | } 324 | } 325 | 326 | task("${projName}_maybeClone") { 327 | maybeClone.dependsOn it 328 | mustRunAfter nuke 329 | mustRunAfter noupdate 330 | doLast { 331 | if(!projFolder.exists()) { 332 | def gitSource 333 | if(proj.gitSource) 334 | gitSource = proj.gitSource 335 | else { 336 | def gitName = proj.gitName ?: projName 337 | gitName += proj.gitNameSuffix 338 | def gitBase 339 | if(proj.gitBase) 340 | gitBase = proj.gitBase 341 | else if(project.multiproject.gitBase) 342 | gitBase = project.multiproject.gitBase 343 | else 344 | throw new GradleException("Don't know from where to clone project '$projName'. Please specify gitBase or gitSource.") 345 | gitSource = gitBase 346 | gitSource += proj.gitNameSeparator 347 | gitSource += gitName 348 | } 349 | if(new File(gitSource).exists()) 350 | gitSource = new File(gitSource).toURI().toString() 351 | def cloneOptions = [ dir: projFolder, uri: gitSource ] 352 | project.logger.warn '{} : cloning from {} to {}', projName, gitSource, projFolder 353 | if(proj.branch) { 354 | project.logger.warn '{} : checking out branch {}', projName, proj.branch 355 | cloneOptions.refToCheckout = proj.branch 356 | } 357 | Grgit.clone(cloneOptions).close() 358 | clonedRepositories.add(projName) 359 | } 360 | } 361 | } 362 | 363 | task("${projName}_update") { 364 | dependsOn maybeClone 365 | mustRunAfter noupdate 366 | update.dependsOn it 367 | doLast { 368 | if(!clonedRepositories.contains(projName)) { 369 | def grgit = Grgit.open(projFolder.absolutePath) 370 | try { 371 | if(proj.branch) { 372 | project.logger.warn '{} : checking out branch {}', projName, proj.branch 373 | if(grgit.branch.list().find { it.name == proj.branch }) 374 | grgit.checkout(branch: proj.branch) 375 | else { 376 | def cmd = grgit.repository.jgit.checkout() 377 | cmd.name = proj.branch 378 | cmd.createBranch = true 379 | cmd.upstreamMode = org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode.SET_UPSTREAM 380 | cmd.startPoint = "origin/${proj.branch}" 381 | cmd.call() 382 | } 383 | } 384 | project.logger.warn '{} : pulling from origin', projName 385 | grgit.pull() 386 | } finally { 387 | grgit.close() 388 | } 389 | } 390 | } 391 | } 392 | 393 | boolean shouldBuild = proj.build == null || proj.build 394 | boolean buildSelfDefined = proj.build == null || (proj.build instanceof Boolean && proj.build) 395 | def projBuildTask = null 396 | def projCleanTask = null 397 | if(shouldBuild) { 398 | projBuildTask = task("${projName}_build", type: GradleBuild) { 399 | description = "Builds the project $projName" 400 | dependsOn maybeClone 401 | mustRunAfter update 402 | build.dependsOn it 403 | if(buildSelfDefined) 404 | dir = projFolder 405 | else { 406 | proj.build = proj.build.toString() 407 | if(proj.build.endsWith('.gradle')) 408 | buildFile = "$projFolder/${proj.build}" 409 | else 410 | dir = "$projFolder/${proj.build}" 411 | } 412 | tasks = proj.buildTasks ?: [ 'build' ] + proj.deployTasks 413 | if(proj.skipTests) 414 | startParameter.excludedTaskNames = [ 'test' ] 415 | } 416 | projCleanTask = task("${projName}_clean", type: GradleBuild) { 417 | description = "Cleans the project $projName" 418 | dependsOn maybeClone 419 | mustRunAfter update 420 | clean.dependsOn it 421 | dir = new File(projFolder, isString(proj.build) ? proj.build : '.') 422 | tasks = [ 'clean' ] 423 | } 424 | } 425 | 426 | if(proj.contribs) { 427 | task("${projName}_installContribs", type: GradleBuild) { 428 | description = "Installs contributions in project $projName" 429 | dependsOn maybeClone 430 | mustRunAfter update 431 | projBuildTask.dependsOn it 432 | if(isString(proj.contribs)) { 433 | proj.contribs = proj.contribs as String 434 | if(proj.contribs.endsWith('.gradle')) 435 | buildFile = "$projFolder/${proj.build}" 436 | else 437 | dir = "$projFolder/${proj.contribs}" 438 | } 439 | else 440 | dir = "$projFolder/contribs" 441 | tasks = [ 'install' ] 442 | } 443 | task("${projName}_cleanContribs", type: GradleBuild) { 444 | description = "Cleans contributions in project $projName" 445 | dependsOn maybeClone 446 | mustRunAfter update 447 | dependsOn projCleanTask 448 | clean.dependsOn it 449 | if(isString(proj.contribs)) { 450 | proj.contribs = proj.contribs as String 451 | if(proj.contribs.endsWith('.gradle')) 452 | buildFile = "$projFolder/${proj.build}" 453 | else 454 | dir = "$projFolder/${proj.contribs}" 455 | } 456 | else 457 | dir = "$projFolder/contribs" 458 | tasks = [ 'clean' ] 459 | } 460 | } 461 | 462 | if(proj.examples) { 463 | if(!tasks.findByName('buildExamples')) 464 | task('buildExamples', group: 'build') { 465 | description = 'Builds examples in repositories.' 466 | dependsOn build 467 | } 468 | 469 | task("${projName}_buildExamples", type: GradleBuild) { 470 | description = "Builds examples in project $projName" 471 | dependsOn build 472 | buildExamples.dependsOn it 473 | dir = new File(projFolder, isString(proj.examples) ? proj.examples : 'examples') 474 | tasks = [ 'build' ] 475 | if(proj.skipTests) 476 | startParameter.excludedTaskNames = [ 'test' ] 477 | } 478 | 479 | task("${projName}_cleanExamples", type: GradleBuild) { 480 | description = "Cleans examples in project $projName" 481 | dependsOn maybeClone 482 | mustRunAfter update 483 | clean.dependsOn it 484 | if(projCleanTask) 485 | projCleanTask.dependsOn it 486 | dir = new File(projFolder, isString(proj.examples) ? proj.examples : 'examples') 487 | tasks = [ 'clean' ] 488 | } 489 | } 490 | 491 | if(proj.apps) { 492 | if(!tasks.findByName('buildApps')) 493 | task('buildApps', group: 'build') { 494 | description = 'Builds applications in repositories.' 495 | dependsOn build 496 | } 497 | 498 | task("${projName}_buildApps", type: GradleBuild) { 499 | description = "Builds applications in project $projName" 500 | dependsOn build 501 | buildApps.dependsOn it 502 | dir = new File(projFolder, isString(proj.apps) ? proj.apps : 'apps') 503 | tasks = [ 'build' ] 504 | if(proj.skipTests) 505 | startParameter.excludedTaskNames = [ 'test' ] 506 | } 507 | 508 | task("${projName}_cleanApps", type: GradleBuild) { 509 | description = "Cleans applications in project $projName" 510 | dependsOn maybeClone 511 | mustRunAfter update 512 | clean.dependsOn it 513 | if(projCleanTask) 514 | projCleanTask.dependsOn it 515 | dir = new File(projFolder, isString(proj.apps) ? proj.apps : 'apps') 516 | tasks = [ 'clean' ] 517 | } 518 | } 519 | 520 | if(proj.uploadArchives || buildSelfDefined) { 521 | def uploadArchivesTask = task("${projName}_uploadArchives", type: GradleBuild) { 522 | description = "Uploads archives of project $projName" 523 | dependsOn build 524 | uploadArchives.dependsOn it 525 | dir = new File(projFolder, isString(proj.build) ? proj.build : '.') 526 | tasks = [ 'uploadArchives' ] 527 | } 528 | if(proj.contribs) 529 | task("${projName}_uploadContribs", type: GradleBuild) { 530 | description = "Uploads contributions of project $projName" 531 | dependsOn maybeClone 532 | mustRunAfter update 533 | uploadArchivesTask.dependsOn it 534 | if(isString(proj.contribs)) { 535 | proj.contribs = proj.contribs as String 536 | if(proj.contribs.endsWith('.gradle')) 537 | buildFile = "$projFolder/${proj.build}" 538 | else 539 | dir = "$projFolder/${proj.contribs}" 540 | } 541 | else 542 | dir = "$projFolder/contribs" 543 | tasks = [ 'uploadArchives' ] 544 | } 545 | } 546 | 547 | task("${projName}_gitStatus") { 548 | description = "git status on ${projName}" 549 | dependsOn maybeClone 550 | mustRunAfter update 551 | gitStatus.dependsOn it 552 | doLast { 553 | def grgit = Grgit.open(projFolder.absolutePath) 554 | try { 555 | def status = grgit.status() 556 | def unstaged = status.unstaged 557 | if(unstaged.added || unstaged.modified || unstaged.removed) 558 | println ' unstaged:' 559 | for(def f in unstaged.added) 560 | println " added $f" 561 | for(def f in unstaged.modified) 562 | println " modified $f" 563 | for(def f in unstaged.removed) 564 | println " removed $f" 565 | def staged = status.staged 566 | if(staged.added || staged.modified || staged.removed) 567 | println ' staged:' 568 | for(def f in staged.added) 569 | println " added $f" 570 | for(def f in staged.modified) 571 | println " modified $f" 572 | for(def f in staged.removed) 573 | println " removed $f" 574 | } finally { 575 | grgit.close() 576 | } 577 | } 578 | } 579 | 580 | task("${projName}_gitBranchList") { 581 | description = "git branches on ${projName}" 582 | dependsOn maybeClone 583 | mustRunAfter update 584 | gitBranchList.dependsOn it 585 | doLast { 586 | def grgit = Grgit.open(projFolder.absolutePath) 587 | try { 588 | def branches = grgit.branch.list() 589 | for(def branch in branches) { 590 | print (branch == grgit.branch.current ? '* ' : ' ') 591 | println branch.name 592 | } 593 | } finally { 594 | grgit.close() 595 | } 596 | } 597 | } 598 | 599 | def gitCommitTask = task("${projName}_gitCommit") { 600 | description = "git commit on ${projName}" 601 | dependsOn maybeClone 602 | mustRunAfter update 603 | gitCommit.dependsOn it 604 | doLast { 605 | if(!project.ext.has('message')) 606 | throw new GradleException('Cannot commit: message is not specified') 607 | boolean amend = project.ext.has('amend') ? (project.ext.amend as boolean) : false 608 | def grgit = Grgit.open(projFolder.absolutePath) 609 | try { 610 | def status = grgit.status() 611 | if(status.unstaged.added || status.unstaged.modified || status.unstaged.removed || 612 | status.staged.added || status.staged.modified || status.staged.removed) { 613 | project.logger.warn '{} : committing changes', projName 614 | grgit.commit(all: true, message: project.ext.message, amend: amend) 615 | } 616 | } finally { 617 | grgit.close() 618 | } 619 | } 620 | } 621 | 622 | task("${projName}_gitRevert") { 623 | description = "git revert on ${projName}" 624 | dependsOn maybeClone 625 | mustRunAfter update 626 | gitRevert.dependsOn it 627 | doLast { 628 | def grgit = Grgit.open(projFolder.absolutePath) 629 | try { 630 | project.logger.warn '{} : reverting changes', projName 631 | def cmd = grgit.repository.jgit.checkout() 632 | cmd.setAllPaths(true) 633 | cmd.call() 634 | } finally { 635 | grgit.close() 636 | } 637 | } 638 | } 639 | 640 | task("${projName}_gitPush") { 641 | description = "git push on ${projName}" 642 | dependsOn maybeClone 643 | mustRunAfter update 644 | mustRunAfter gitCommitTask 645 | gitPush.dependsOn it 646 | doLast { 647 | def grgit = Grgit.open(projFolder.absolutePath) 648 | try { 649 | project.logger.warn '{} : pushing {} to origin', projName, proj.branch 650 | grgit.push() 651 | } finally { 652 | grgit.close() 653 | } 654 | } 655 | } 656 | 657 | if(shouldBuild) { 658 | task("${projName}_releaseChecks", type: GradleBuild) { 659 | description = "Do checks before release of ${projName}" 660 | dependsOn maybeClone 661 | mustRunAfter update 662 | releaseChecks.dependsOn it 663 | dir = projFolder 664 | tasks = [ 'checkCommitNeeded', 'checkUpdateNeeded' ] 665 | } 666 | 667 | task("${projName}_releaseCollectVersions") { 668 | description = "Collect versions before release of ${projName}" 669 | dependsOn releaseChecks 670 | releaseCollectVersions.dependsOn it 671 | doLast { 672 | File propertiesFile = new File(projFolder, 'gradle.properties') 673 | if(!propertiesFile.exists()) 674 | throw new GradleException("File ${propertiesFile} does not exist. Could not release.") 675 | Properties properties = new Properties() 676 | propertiesFile.withInputStream { properties.load(it) } 677 | if(!properties.version) 678 | throw new GradleException("'version' property is not defined in ${propertiesFile}. Could not release.") 679 | def releaseVersion 680 | if(proj.releaseVersion instanceof String) 681 | releaseVersion = proj.releaseVersion 682 | else if(proj.releaseVersion instanceof Map) 683 | for(def e in proj.releaseVersion) { 684 | def m = properties.version =~ e.key 685 | if(m) { 686 | releaseVersion = e.value(m) 687 | if(releaseVersion == null) 688 | releaseVersion = properties.version 689 | break 690 | } 691 | } 692 | def newVersion 693 | if(proj.newVersion instanceof String) 694 | newVersion = proj.newVersion 695 | else if(proj.newVersion instanceof Map) 696 | for(def e in proj.newVersion) { 697 | def m = properties.version =~ e.key 698 | if(m) { 699 | newVersion = e.value(m) 700 | if(newVersion == null) 701 | newVersion = properties.version 702 | break 703 | } 704 | } 705 | versionMap[getProjectKey(projName)] = [ originalProjectName: projName, projFolder: projFolder, branch: proj.branch, propertiesFile: propertiesFile, originalProperties: properties, releaseVersion: releaseVersion, newVersion: newVersion ] 706 | } 707 | } 708 | 709 | task("${projName}_releaseResolveVersions") { 710 | description = "Resolve versions before release of ${projName}" 711 | dependsOn releaseCollectVersions 712 | releaseResolveVersions.dependsOn it 713 | doLast { 714 | def thisVersion = getVersionInfo(projName) 715 | def releaseProperties = [:] + thisVersion.originalProperties 716 | releaseProperties.version = thisVersion.releaseVersion 717 | def newProperties = [:] + thisVersion.originalProperties 718 | newProperties.version = thisVersion.newVersion 719 | thisVersion.originalProperties.each { key, value -> 720 | def m = key =~ /(.+)_version$/ 721 | if(m) { 722 | def otherProjName = m[0][1] 723 | def otherProjVersion = getVersionInfo(otherProjName) 724 | if(otherProjVersion) { 725 | releaseProperties[key] = otherProjVersion.releaseVersion 726 | newProperties[key] = otherProjVersion.newVersion 727 | } else 728 | project.logger.warn 'File {} contains property {} that refers to a non-existing project {}.', thisVersion.propertiesFile, key, otherProjName 729 | } 730 | } 731 | thisVersion.releaseProperties = releaseProperties 732 | thisVersion.newProperties = newProperties 733 | } 734 | } 735 | 736 | task("${projName}_releasePrepareReleaseVersion") { 737 | description = "Prepare release version for ${projName}" 738 | dependsOn releaseResolveVersions 739 | releasePrepareReleaseVersions.dependsOn it 740 | doLast { 741 | def thisVersion = getVersionInfo(projName) 742 | project.logger.warn '{} : {} : setting release version', projName, thisVersion.releaseVersion 743 | def releaseProperties = new Properties() + thisVersion.releaseProperties 744 | thisVersion.propertiesFile.withOutputStream { releaseProperties.store(it, null) } 745 | } 746 | } 747 | 748 | task("${projName}_releaseCheckSnapshotDependencies", type: GradleBuild) { 749 | description = "Check for snapshot dependencies before release of ${projName}" 750 | dependsOn releasePrepareReleaseVersions 751 | releaseCheckSnapshotDependencies.dependsOn it 752 | dir = projFolder 753 | tasks = [ 'checkSnapshotDependencies' ] 754 | enabled = proj.failOnSnapshotDependencies as boolean 755 | } 756 | 757 | task("${projName}_releaseBuildReleaseVersion", type: GradleBuild) { 758 | description = "Builds release version of the project $projName" 759 | dependsOn releaseCheckSnapshotDependencies 760 | if(buildSelfDefined) 761 | dir = projFolder 762 | else { 763 | proj.build = proj.build.toString() 764 | if(proj.build.endsWith('.gradle')) 765 | buildFile = "$projFolder/${proj.build}" 766 | else 767 | dir = "$projFolder/${proj.build}" 768 | } 769 | tasks = (proj.buildTasks ?: [ 'build' ]) + proj.deployTasks 770 | if(proj.skipTests) 771 | startParameter.excludedTaskNames = [ 'test' ] 772 | } 773 | 774 | task("${projName}_releaseCommitReleaseVersion") { 775 | dependsOn "${projName}_releaseBuildReleaseVersion" 776 | doLast { 777 | def thisVersion = getVersionInfo(projName) 778 | def grgit = Grgit.open(projFolder.absolutePath) 779 | try { 780 | if(proj.releaseNoCommit) 781 | project.logger.warn '{} : {} : releaseNoCommit is set, will not be commited and/or pushed', projName, thisVersion.releaseVersion 782 | else { 783 | def status = grgit.status() 784 | if(status.unstaged.added || status.unstaged.modified || status.unstaged.removed || 785 | status.staged.added || status.staged.modified || status.staged.removed) { 786 | project.logger.warn '{} : {} : committing release version', projName, thisVersion.releaseVersion 787 | grgit.commit(all: true, message: "Release ${thisVersion.releaseVersion}", amend: false) 788 | } 789 | project.logger.warn '{} : {} : creating release tag', projName, thisVersion.releaseVersion 790 | grgit.tag.add(name: thisVersion.releaseVersion, message: "Release ${thisVersion.releaseVersion}", force: true) 791 | if(proj.releaseNoPush) 792 | project.logger.warn '{} : {} : releaseNoPush is set, will not be pushed to upstream repository', projName, thisVersion.releaseVersion 793 | else { 794 | project.logger.warn '{} : {} : pushing release version to upstream repository', projName, thisVersion.releaseVersion 795 | grgit.push() 796 | grgit.push(tags: true) 797 | } 798 | } 799 | } finally { 800 | grgit.close() 801 | } 802 | } 803 | } 804 | 805 | task("${projName}_releasePrepareNewVersion") { 806 | description = "Prepare new version for ${projName}" 807 | dependsOn "${projName}_releaseCommitReleaseVersion" 808 | doLast { 809 | def thisVersion = getVersionInfo(projName) 810 | project.logger.warn '{} : {} : setting new version', projName, thisVersion.newVersion 811 | def newProperties = new Properties() + thisVersion.newProperties 812 | thisVersion.propertiesFile.withOutputStream { newProperties.store(it, null) } 813 | } 814 | } 815 | 816 | task("${projName}_releaseBuildNewVersion", type: GradleBuild) { 817 | description = "Builds new version of the project $projName" 818 | dependsOn "${projName}_releasePrepareNewVersion" 819 | if(buildSelfDefined) 820 | dir = projFolder 821 | else { 822 | proj.build = proj.build.toString() 823 | if(proj.build.endsWith('.gradle')) 824 | buildFile = "$projFolder/${proj.build}" 825 | else 826 | dir = "$projFolder/${proj.build}" 827 | } 828 | tasks = (proj.buildTasks ?: [ 'build' ]) + proj.deployTasks 829 | if(proj.skipTests) 830 | startParameter.excludedTaskNames = [ 'test' ] 831 | } 832 | 833 | task("${projName}_releaseCommitNewVersion") { 834 | dependsOn "${projName}_releaseBuildNewVersion" 835 | release.dependsOn it 836 | doLast { 837 | def thisVersion = getVersionInfo(projName) 838 | def grgit = Grgit.open(projFolder.absolutePath) 839 | try { 840 | if(proj.releaseNoCommit) 841 | project.logger.warn '{} : {} : releaseNoCommit is set, will not be commited and/or pushed', projName, thisVersion.newVersion 842 | else { 843 | def status = grgit.status() 844 | if(status.unstaged.added || status.unstaged.modified || status.unstaged.removed || 845 | status.staged.added || status.staged.modified || status.staged.removed) { 846 | project.logger.warn '{} : {} : committing new version', projName, thisVersion.newVersion 847 | grgit.commit(all: true, message: "New version ${thisVersion.newVersion}", amend: false) 848 | if(proj.releaseNoPush) 849 | project.logger.warn '{} : {} : releaseNoPush is set, will not be pushed to upstream repository', projName, thisVersion.newVersion 850 | else { 851 | project.logger.warn '{} : {} : pushing new version to upstream repository', projName, thisVersion.newVersion 852 | grgit.push() 853 | } 854 | } 855 | } 856 | } finally { 857 | grgit.close() 858 | } 859 | } 860 | } 861 | } // shouldBuild 862 | 863 | } // first pass is over 864 | 865 | // second pass, now we handle inter-project dependencies 866 | project.multiproject.projects.each { projName, proj -> 867 | if(proj.dependsOn) { 868 | def thisProjUpdate = tasks.findByName("${projName}_update") 869 | def thisProjBuild = tasks.findByName("${projName}_build") 870 | def thisProjBuildExamples = tasks.findByName("${projName}_buildExamples") 871 | def thisProjBuildApps = tasks.findByName("${projName}_buildApps") 872 | def thisProjClean = tasks.findByName("${projName}_clean") 873 | def thisProjGitStatus = tasks.findByName("${projName}_gitStatus") 874 | def thisProjReleaseBuildReleaseVersion = tasks.findByName("${projName}_releaseBuildReleaseVersion") 875 | toCollection(proj.dependsOn).each { otherProj -> 876 | def otherProjUpdate = "${otherProj}_update" 877 | thisProjUpdate.dependsOn otherProjUpdate 878 | if(thisProjBuild) { 879 | def otherProjBuild = tasks.findByName("${otherProj}_build") 880 | if(otherProjBuild) 881 | thisProjBuild.dependsOn otherProjBuild 882 | else if (!file(otherProj).exists()) 883 | thisProjBuild.dependsOn otherProjUpdate 884 | } 885 | if(thisProjBuildExamples) { 886 | def otherProjBuild = tasks.findByName("${otherProj}_build") 887 | if(otherProjBuild) 888 | thisProjBuildExamples.dependsOn otherProjBuild 889 | else if (!file(otherProj).exists()) 890 | thisProjBuildExamples.dependsOn otherProjUpdate 891 | } 892 | if(thisProjBuildApps) { 893 | def otherProjBuild = tasks.findByName("${otherProj}_build") 894 | if(otherProjBuild) 895 | thisProjBuildApps.dependsOn otherProjBuild 896 | else if (!file(otherProj).exists()) 897 | thisProjBuildApps.dependsOn otherProjUpdate 898 | } 899 | if(thisProjClean) { 900 | def otherProjClean = tasks.findByName("${otherProj}_clean") 901 | if(otherProjClean) 902 | thisProjClean.dependsOn otherProjClean 903 | else if (!file(otherProj).exists()) 904 | thisProjClean.dependsOn otherProjUpdate 905 | } 906 | thisProjGitStatus.dependsOn "${otherProj}_gitStatus" 907 | thisProjReleaseBuildReleaseVersion.dependsOn "${otherProj}_releaseCommitNewVersion" 908 | } 909 | } 910 | } 911 | 912 | defaultTasks 'buildApps' 913 | 914 | -------------------------------------------------------------------------------- /config.gradle: -------------------------------------------------------------------------------- 1 | /* This is an example configuration, demonstrating multiproject-git-gradle at work. 2 | * To use this configuration, run from the command-line "gradle build". 3 | * See more information in "README.md". 4 | */ 5 | multiproject { 6 | rootFolder '../akhikhl_projects' 7 | git baseUrl: 'https://github.com/akhikhl', { 8 | project name: 'gradle-onejar' 9 | project name: 'xmltools', dependsOn: 'gradle-onejar' 10 | project name: 'gino', dependsOn: 'gradle-onejar' 11 | project name: 'gasmine', dependsOn: 'gino' 12 | } 13 | } 14 | --------------------------------------------------------------------------------