This plugin adds support for including Git repositories as source dependencies in Gradle builds, making multi-repository development much easier to deal with!
This plugin has been successfully tested on Gradle 7.1.1, 7.2, 7.3, 8.11. Earlier releases are not supported. The following Gradle versions are known to be broken with this plugin: 6.0.x, 6.1.x.
Rationale
Multi-repository development, while providing architectural advantages (reduced scope of libraries, faster development cycles, …), are often painful for developers, especially when the number of modules being involved grows.
For example, to develop a single feature, a developer may have to work on more than one repository at a time: a core
module, living in repository core
, and a library
module depending on core
, living in a repository library
.
The problem is that the development of a particular feature may require changes to both core
and library
.
In that case, developers, in the Java ecosystem, typically rely on publishing snapshots in their local Maven repository: make a change in core
, publish a snapshot, then make changes to library
, publish a snapshot, and repeat the loop.
There are several problems with this approach:
-
it requires switching between projects, publishing intermediate artifacts to the local filesystem
-
makes it difficult to collaborate with others as they also need to checkout several projects
-
it requires integrating
mavenLocal()
as a repository for your local builds, which is considered a bad practice (because it introduces non-reproducibility and makes the builds brittle) -
for CI, it requires publishing to a snapshot repository for downstream builds to pick up the changes, meaning that you will often have to merge work-in-progress just to be able to test changes
-
it simply doesn’t work if the library that you want to work with is not a first level dependency
To improve the situation, Gradle users can use composite builds to avoid publishing to a local repository.
This makes the development cycle much faster already, by avoiding the publishToMavenLocal
dance.
However, there are some limitations with composite builds:
-
the included builds must be available locally, in a directory
-
it makes it hard, or even impossible, to setup on CI servers, unless you create an "integrating" project which hardcodes checkouts
-
it forces to change the configuration of the build to use composites
In addition, there’s a process problem with developing in a multi-repository environment: if the feature requires changes to multiple modules, in order to be able to integrate the changes, in particular on CI, you have to publish either snapshots or pre-releases. The problem is that this is not necessarily acceptable: for example you might want to develop a feature in a branch of each repository, and only merge once the full feature is ready.
Gradle also provides experimental support for source dependencies, but there are addressing a different problem. In particular, source dependencies are a replacement for regular dependencies: they require to change the dependency notation in builds with "source dependencies", including the branches. What we want to do, instead, is to keep our build files untouched, and substitute binary dependencies with sources.
This plugin provides a solution to this problem by allowing to include (in the sense of Gradle included builds) Git repositories, and specifying what branches/tags should be used.
If you’re looking for a synthetic view of pros and cons of each solution, please refer to this section in the docs.
Configuration
Warning
|
This plugin is a settings plugin which must be applied to your settings.gradle(.kts) file, not to a project build.gradle(.kts) file.
|
plugins {
id 'me.champeau.includegit' version '0.2.0'
}
import me.champeau.gradle.igp.gitRepositories
plugins {
id("me.champeau.includegit") version "0.2.0"
}
Declaring included Git repositories
The plugin defines a gitRepositories
extension which is used to declare included Git repositories.
For example, say that you want to include jdoctor
which is hosted at https://github.com/melix/jdoctor/
, then you should write:
gitRepositories {
include('jdoctor') {
uri = 'git@github.com:melix/jdoctor.git'
// optional, set what branch to use
branch = 'feature1'
// you can also use a tag
tag = 'v1.0'
}
}
gitRepositories {
include("jdoctor") {
uri.set("git@github.com:melix/jdoctor.git")
// optional, set what branch to use
branch.set("feature1")
// you can also use a tag
tag.set("v1.0")
}
}
By default, the plugin will clone the included Git repositories in the checkouts
directory of the project.
If the repository is already cloned, the plugin will automatically perform an update every 24 hours.
Alternatively, you can force it to update by adding -Drefresh.git.repositories
to your Gradle command line.
Using local copies instead of cloning
Declaring included Git repositories will automatically make the plugin clone the remote repositories.
This makes it very convenient to use on CI, since you will now be able to have branches which use other working branches from other repositories.
However, it is likely that you already have local clones that you are already modifying and that you’d like to use for development.
In this case, you can set the local.git.XXXX
Gradle property, where XXXX
is the included repository name, in your gradle.properties
file, to point to your local copy.
It is recommended to use the gradle.properties
file located in your user home directory in this case:
local.git.jdoctor=/home/me/development/jdoctor
Note
|
if the property is found, the branch or tag configuration will be ignored.
|
Automatic local copies
Alternatively, you may have one or more directory with your checked out projects.
In this case, the plugin provides a convenience which is going to automatically map directories to Git repository names.
For this, you need to set the auto.include.git.dirs
to the list of directories to scan.
For example, say that you have:
/home/me
└── development
├── gradle
│ ├── foo-gradle-plugin
│ └── gradle-core
└── micronaut
├── micronaut-core
└── micronaut-data
Then you can set this in your gradle.properties
file:
auto.include.git.dirs=/home/me/development/gradle,/home/me/development/micronaut
The plugin will automatically scan the gradle
and micronaut
directories, and map the foo-gradle-plugin
, gradle-core
, micronaut-core
and micronaut-data
directories to potential included Git repositories.
If a build is including a repository named micronaut-core
, then it will automatically pick it from the micronaut-core
directory.
This mechanism makes it extremely convenient to work with complex codebases with multiple Git repositories.
Configuring the included build
By default, the root directory of the cloned repository will be automatically included.
You can tweak the configuration of the included build by calling the includeBuild
method of the gitRepositories
extension:
gitRepositories {
include('jdoctor') {
uri = 'git@github.com:melix/jdoctor.git'
includeBuild {
name = 'other-name'
}
}
}
gitRepositories {
include("jdoctor") {
uri.set("git@github.com:melix/jdoctor.git")
includeBuild {
name = "other-name"
}
}
}
Including sub-directories instead of the root
In some cases, the root directory of the cloned project may not be the directory you want to include, or you may want to include several sub-directories as separate included builds.
For this purpose, you can use the includeBuild
statement which works exactly like Gradle’s includeBuild
, except that the root directory is the checked out directory:
gitRepositories {
include('jdoctor') {
uri = 'git@github.com:melix/jdoctor.git'
// This will include the "build-logic" directory of the repository
// instead of the whole project
includeBuild 'build-logic'
}
}
gitRepositories {
include("jdoctor") {
uri.set("git@github.com:melix/jdoctor.git")
// This will include the "build-logic" directory of the repository
// instead of the whole project
includeBuild("build-logic")
}
}
Note
|
You may use several includeBuild statements from a single repository.
|
Authentication
The plugin supports 3 different authentication mechanisms:
-
basic authentication (username + password)
-
ssh with public key
-
ssh with password
Authentication can be configured per repository:
gitRepositories {
include('myrepo') {
// ...
authentication {
basic {
username = '...'
password = '...'
}
// or
sshWithPublicKey()
// or
sshWithPublicKey {
privateKey = file("/path/to/private/key")
}
// or
sshWithPassword {
password = '...'
}
}
}
}
gitRepositories {
include("myrepo") {
// ...
authentication {
basic {
username.set("...")
password.set("...")
}
// or
sshWithPublicKey()
// or
sshWithPublicKey {
privateKey.set(file("/path/to/private/key"))
}
// or
sshWithPassword {
password.set("...")
}
}
}
}
It is also possible to configure a default authentication mechanism, which will be used when authentication isn’t configured specifically on a repository:
gitRepositories {
defaultAuthentication {
sshWithPublicKey()
}
}
gitRepositories {
defaultAuthentication {
sshWithPublicKey()
}
}
Configuring checkout directories
The plugin supports 2 different ways to configure the checkout directory:
-
either by configuring the root directory where all repositories are going to be checked out (by default,
checkouts
)
gitRepositories {
checkoutsDirectory.set(file('.'))
}
gitRepositories {
checkoutsDirectory.set(file("."))
}
-
or by configuring a checkout directory per included repository
gitRepositories {
include('myrepo') {
// ...
checkoutDirectory = file('lib')
}
}
gitRepositories {
include("myrepo") {
// ...
checkoutDirectory.set(file("lib"))
}
}
Performing actions before the build is included
It is possible to perform actions right after a project has been cloned and before it is included. This can be useful, for example, if the project needs to be analyzed in order to properly configure the included builds.
The action will always be called, even if sources are already available locally. It is not recommended to mutate sources as part of this callback, since this will likely break updating the sources later.
gitRepositories {
include('my-project') {
// ...
codeReady { event ->
println("Project cloned in ${event.checkoutDirectory}")
}
}
}
gitRepositories {
include("my-project") {
// ...
codeReady {
println("Project cloned in ${checkoutDirectory}")
}
}
}
Comparison of solutions
This table summarizes some of the pros and cons of each solution, so that you can make a sound decision.
Snapshots | Included builds | Source dependencies | This plugin | |
---|---|---|---|---|
No |
Yes |
No |
Yes |
|
No |
Yes |
No |
Yes |
|
No |
No |
No |
Yes |
|
No |
No |
Yes |
Yes |
|
No |
No |
No |
Yes |
|
No |
No |
Yes |
Yes |
|
Yes |
No |
No |
No |
|
Yes |
Depends on builds |
Depends on builds |
Depends on builds |
|
No |
Manual |
Depends on dependencies |
Yes |
Here’s a description of the different columns. This comparison is made for the multi-repository setup. It doesn’t mean that it would be the same, say, for a Gradle composite build living in a single repository:
-
Works for transitive dependencies: a build defines "direct" dependencies, which are typically used directly in source code, but often what you need to test is a transitive dependency. This column indicates if the solution makes it possible to substitute a transitive dependency with sources, transparently
-
Transparent to build scripts: some solutions, typically
SNAPSHOTS
, require changes to build scripts because you need to introducemavenLocal
, put a particular version, or introduce a first level dependency so that the changes are visible. Other solutions like this plugin only require applying the plugin, but leave your dependency declarations untouched. -
Works consistently on CI and local: does the technical solution works consistently locally and on CI? Snapshots are the typical example of things which are hard to reason about because the local Maven repo may contain different dependencies than the remote snapshot repository. It also requires sync’ing and refreshing dependencies. Other solutions like composite builds work well for local development, but break as soon as you push on CI because the local repositories wouldn’t be available.
-
Handles cloning/checkout: does the solution handle checking out (or cloning in Git terminology) the dependency for you? Will it make the dependency visible as sources in your IDE?
-
Avoids publishing to artifact repository: Snapshots typically require publishing artifacts to a binary repository, or local file system, for other builds to "see" the changes. Some solutions like included builds do not, since they handle the dependency using sources instead.
-
Supports multiple branches: Snapshots work well, except when you need to integrate changes from different branches: either you have to publish different artifacts with different coordinates or versions to be able to test them in downstream projects, or you have to merge changes and push a snapshot. On the contrary, source dependencies handle branches gracefully because they don’t require any publication to a binary repository.
-
Works cross build tools: Snapshots can be consumed from different build tools, typically both Maven and Gradle. Source dependencies, included builds and this plugin require all participating builds to use Gradle and therefore are not suitable if you have a mix of build tools.
-
Supports same build tool, different versions: Snapshots are binary dependencies so the build tool which was used doesn’t matter. Included builds and source dependencies will use the version of the build tool which includes the other builds as the "driver". If there are incompatibilities between versions of the main build and the included ones, builds may fail.
-
Continous upstream testing: Does the solution make it possible to continuously test upstream dependencies? Typically, without changing your build scripts, it would be nice if you could test that the project is compatible with the latest
master
branch of a dependency. This plugin makes it quite simple to implement, while included builds require some manual setup. Snapshots won’t help.
Known limitations
The plugin won’t work for plugin substitutions (e.g includeBuild
in the pluginManagement
section).