Skip to content

Tutorial

Info

If in order to study things you prefer to analyse code by yourself, rather than go through detailed step-by-step tutorials like this one, then maybe Example Djig Application will help you better.

In this tutorial we are going to create an application with dynamic beans managed by djig.

The app will consist of three projects:

  • api
  • app
  • dynamic

It will be a web server with just one endpoint GET /hello returning a greeting text. The greeting text will be formed by a dynamic bean, sitting in the dynamic project. We'll be able to change the greeting without restarting the app.

When we launch the application for the first time, GET /hello will return Hello, world!.

After that we will change the code creating the message in the dynamic project, and then, without restarting the app, GET /hello will start to return Hi, world!.

In this tutorial, we'll be using Java, Gradle and GitLab.

Note

Djig can also be used with Maven, but for this tutorial we recommend Gradle.

For Gradle djig projects there is the org.taruts.workspace Gradle plugin, which simplifies working with multiple Git repository code bases. Also, there is the org.taruts.djig Gradle plugin, simplifying creation of a developer's personal local copies of dynamic projects.

In this tutorial we will cover working with those plugins.

Arranging Local Project Directories

First, create a directory djig-test at whatever place in your file system. This will be the parent directory for the directories of our three projects.

1
mkdir djig-test

The api project

This project will build the API JAR and publish it to the local Maven repository.

The app and dynamic projects will depend on the API JAR.

Creating the api Directory

1
2
cd djig-test
mkdir api

Configuring the Build

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
plugins {
    id 'java-library'
    id 'maven-publish'//(1)!
}

group = 'x'
version = '1.0-SNAPSHOT'

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    api 'org.taruts.djig:djig-dynamic-api:1.0.3'//(2)!
}

publishing {//(3)!
    publications {
        jar(MavenPublication) {
            from components.java
            versionMapping {
                usage('java-api') {
                    fromResolutionResult()
                }
                usage('java-runtime') {
                    fromResolutionResult()
                }
            }
        }
    }
    repositories {
        mavenLocal()
    }
}
  1. Adding the publish Gradle task
  2. Read about djig-dynamic-api in more detail here
  3. Configuring the maven-publish plugin so that gradle publish would publish API JAR in the local Maven repository
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
plugins {
    id("java-library")
    id("maven-publish")//(1)!
}

group = "x"
version = "1.0-SNAPSHOT"

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    api("org.taruts.djig:djig-dynamic-api:1.0.3")//(2)!
}

publishing {//(3)!
    publications {
        create<MavenPublication>("jar") {
            from(components["java"])
            versionMapping {
                usage("java-api") {
                    fromResolutionResult()
                }
                usage("java-runtime") {
                    fromResolutionResult()
                }
            }
        }
    }
    repositories {
        mavenLocal()
    }
}
  1. Adding the publish Gradle task
  2. Read about djig-dynamic-api in more detail here
  3. Configuring the maven-publish plugin so that gradle publish would publish API JAR in the local Maven repository
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>x</groupId>
    <artifactId>api</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>1.7</maven.compiler.source>
        <maven.compiler.target>1.7</maven.compiler.target>
    </properties>

    <dependencies>

        <dependency><!--(1)!-->
            <groupId>org.taruts.djig</groupId>
            <artifactId>djig-dynamic-api</artifactId>
            <version>1.0.3</version>
        </dependency>

    </dependencies>
</project>
  1. Read about djig-dynamic-api in more detail here

.gitignore

1
2
3
4
5
6
7
8
9
### Gradle ###
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/

### Your IDE stuff ###
... #(1)!
  1. Files, that the IDE creates to work with the project, that are not normally pushed
1
2
3
4
5
6
7
8
### Maven ###
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/

### Your IDE stuff ###
... #(1)!
  1. Files, that the IDE creates to work with the project, that are not normally pushed

The MessageProvider interface

Let's add a dynamic bean interface MessageProvider to the api project, with which we're going to build our API JAR.

The API JAR will be in the dependencies of both the app and dynamic projects.

dynamic will use the MessageProvider interface to define a dynamic bean, implementing this interface.

app will use the MessageProvider interface, in its beans (in its single controller), to define a dependency on the dynamic bean, defined in dynamic.

api/src/main/java/x/dynamic/api/MessageProvider.java
1
2
3
4
5
6
7
package x.dynamic.api;

import org.taruts.djig.dynamicApi.DynamicComponent;

public interface MessageProvider extends DynamicComponent/*(1)!*/ {
    String getMessage();
}
  1. Note that MessageProvider being a dynamic bean interface must inherit the DynamicComponent marker interface

Publishing the API JAR

While in the api directory let's run the following command:

1
gradle publish
1
mvn install

The dynamic project

Creating the dynamic Directory

1
2
cd djig-test
mkdir dynamic

Configuring the Build

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
plugins {
    id 'java'
}

group = 'x'
version = '1.0-SNAPSHOT'

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    compileOnly 'x:api:1.0-SNAPSHOT'//(1)!
}
  1. A dependency on the API JAR, that we've defined in the api project and published to the local Maven repository
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
plugins {
    id("java")
}

group = "x"
version = "1.0-SNAPSHOT"

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    compileOnly("x:api:1.0-SNAPSHOT")//(1)!
}
  1. A dependency on the API JAR, that we've defined in the api project and published to the local Maven repository
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>x</groupId>
    <artifactId>dynamic</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>1.7</maven.compiler.source>
        <maven.compiler.target>1.7</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>x</groupId>
            <artifactId>api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency> <!--(1)!-->
    </dependencies>
</project>
  1. A dependency on the API JAR, that we've defined in the api project and published to the local Maven repository

.gitignore

1
2
3
4
5
6
7
8
9
### Gradle ###
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/

### Your IDE stuff ###
... #(1)!
  1. Files, that the IDE creates to work with the project, that are not normally pushed
1
2
3
4
5
6
7
8
### Maven ###
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/

### Your IDE stuff ###
... #(1)!
  1. Files, that the IDE creates to work with the project, that are not normally pushed

The MessageProviderImpl dynamic bean

Let's go to the dynamic project and create a dynamic bean there, implementing the MessageProvider dynamic interface, which is from the api project and, consequently, from the API JAR.

dynamic/src/main/java/x/dynamic/MessageProviderImpl.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package x.dynamic;

import org.springframework.stereotype.Component;
import x.dynamic.api.MessageProvider;

@Component
public class MessageProviderImpl implements MessageProvider {

    @Override
    public String getMessage() {
        return "Hello, world!";
    }
}

Publishing the dynamic project to GitLab

Go to GitLab and create an EMPTY, PRIVATE repository dynamic.

Be sure not to select the checkbox adding a README.MD to the project, we need an absolutely empty repository.

Copy the HTTPS clone URL of the newly created repository to the clipboard.

Now go to the project local directory and run the following commands to push the dynamic project to GitLab.

1
2
3
4
5
git init -b main
git add .
git commit -m "Initial commit"
git remote add origin <HTTPS clone URL of the dynamic project>
git push -u origin main

The app Project

Creating the app Directory

1
2
cd djig-test
mkdir app

Creating the Working Directory

At runtime the app will clone dynamic projects into its working directory.

So, in order not to create problems in working with Git, the working directory should not be the root of the app directory itself.

As the working directory we will use the working-directory subdirectory of the app directory.

1
2
cd app
mkdir working-directory

Later we will add working-directory to the .gitignore.

Configuring the Build

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
plugins {
    id 'application'
    id 'org.springframework.boot' version '2.7.0'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
}

group = 'x'
version = '0.0.1-SNAPSHOT'
java.sourceCompatibility = JavaVersion.VERSION_17

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webflux'//(1)!
    implementation 'org.taruts.djig:djig-spring-boot-starter:1.0.3'//(2)!
    implementation 'x:api:1.0-SNAPSHOT'//(3)!
}
  1. Instead of spring-boot-starter-webflux you can as well use spring-boot-starter-web
  2. A starter adding djig to a project
  3. A dependency on the API JAR, that we've earlier defined in the api project and published to the local Maven repository
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
plugins {
    application
    id("org.springframework.boot") version "2.7.0"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
}

group = "x"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_17

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-webflux")//(1)!
    implementation("org.taruts.djig:djig-spring-boot-starter:1.0.3")//(2)!
    implementation("x:api:1.0-SNAPSHOT")//(3)!
}
  1. Instead of spring-boot-starter-webflux you can as well use spring-boot-starter-web
  2. A starter adding djig to a project
  3. A dependency on the API JAR, that we've earlier defined in the api project and published to the local Maven repository
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>2.7.0</version>
        <relativePath/>
    </parent>

    <groupId>x</groupId>
    <artifactId>app</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>1.7</maven.compiler.source>
        <maven.compiler.target>1.7</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency><!--(1)!-->

        <dependency>
            <groupId>org.taruts.djig</groupId>
            <artifactId>djig-spring-boot-starter</artifactId>
            <version>1.0.3</version><!--(2)!-->
        </dependency>

        <dependency>
            <groupId>x</groupId>
            <artifactId>api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency><!--(3)!-->

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  1. Instead of spring-boot-starter-webflux you can as well use spring-boot-starter-web
  2. A starter adding djig to a project
  3. A dependency on the API JAR, that we've earlier defined in the api project and published to the local Maven repository

.gitignore

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
### Gradle ###
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/

### Your IDE stuff ###
... #(1)!

working-directory #(2)!
  1. Files, that the IDE creates to work with the project, that are not normally pushed
  2. The directory that all dynamic projects will be cloned to at runtime
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
### Maven ###
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/

### Your IDE stuff ###
... #(1)!

working-directory #(2)!
  1. Files, that the IDE creates to work with the project, that are not normally pushed
  2. The directory that all dynamic projects will be cloned to at runtime

application.properties

Now let's reference the dynamic project, we've just published to GitLab, in the Spring Boot configuration properties of the app project.

Thus app will know, that the dynamic project dynamic exists and where it is.

src/main/resources/application.properties
1
2
3
djig.dynamic-projects.1.url=<HTTPS clone URL of the dynamic project>
djig.dynamic-projects.1.dynamic-interface-package=x.dynamic.api
djig.dynamic-projects.1.branch=main

Webhook Configuration

As any Spring Boot application by default, ours will listen on port 8080 of all network interfaces of the computer.

It might be that none of them is accessible from the public internet. If so then the app won't be able to receive callback requests from GitLab directly.

If this is you case - don't worry, you'll be able to see the app in action, simulating receiving callback requests by manually opening the corresponding URL in the browser.

But, if there is a network interface with a public IP on your computer, or there is a port forwarding from NAT configured on your router, or there is a reversed proxy accessible form outside, forwarding HTTP requests to your interface, then we will see the fully automated dynamic code redeployments.

If so - use the properties djig.hook.host, djig.hook.port and djig.hook.protocol in src/main/resources/application.properties to specify the corresponding parts of the URL that webhook requests should target.

Info

If your interface is acessible from outside indirectly, as we've described above, then those properties must point to the NAT or reverse proxy, not to the interface of the app.

More about indirect webhook callback requests see here.

config / application.properties

We will keep our GitLab credentials in a separate file. This is a good practice in respect to security, as this file can be specified in the .gitignore to prevent sending it outside via git push.

1
2
djig.dynamic-projects.1.username=<your username>
djig.dynamic-projects.1.password=<your password>

Also, instead of username and password you can specify your Personal Access Token (PAT):

1
djig.dynamic-projects.1.username=<your Personal Access Token>

The AppApplication Spring Boot Application Class

Let's add to app a standard Spring Boot application class.

app/src/main/java/x/app/AppApplication.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package x.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AppApplication {

    public static void main(String[] args) {
        SpringApplication.run(AppApplication.class, args);
    }
}

The HelloController Controller

Let's add to app a controller, serving the GET /hello endpoint.

app/src/main/java/x/app/HelloController.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package x.app;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import x.dynamic.api.MessageProvider;

@RestController
@RequestMapping("hello")
public class HelloController {

    @Autowired
    private MessageProvider messageProvider;

    @GetMapping
    public String hello() {
        return messageProvider.getMessage();
    }
}

Running the app

Warning

Make sure you run the app with working-directory as its working directory!

Changing the Dynamic Code at runtime

Go to the dynamic project, to the x.dynamic.MessageProviderImpl class and replace the Hello, world! with Hi, world!:

dynamic/src/main/java/x/dynamic/MessageProviderImpl.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package x.dynamic;

import org.springframework.stereotype.Component;
import x.dynamic.api.MessageProvider;

@Component
public class MessageProviderImpl implements MessageProvider {

    @Override
    public String getMessage() {
        return "Hi, world!";
    }
}

Now push the changes.

If we've correctly configured the djig.hook.* properties, then the app will receive a callback request from GitLab at URL http://localhost:8080/refresh/generic/1.

If we didn't configure the djig.hook.* properties, then we can imitate the callback request by manually opening this URL in the browser: http://localhost:8080/refresh/generic/1.

Regardless the way the callback request has come, dynamic code redeployment process will start.

After some seconds refresh the tab with http://localhost:8080/hello. You will see Hi, world!.

Success

This tells us we've successfuly change the dynamic code without stopping the app!

It was Hello, world!, and now it's Hi, world!.

Using Static Beans In Dynamic Beans

Let's now change the architecture a bit.

Let's now have the punctuation mark at the end of the greeting be formed by the main part of the application.

The dynamic bean will still be responsible for building the greeting, but it will delegate the choice of the punctuation mark to a bean in the static part of the application.

Here's the plan:

  • Go to api and add an interface for a new bean in app that will provide the punctuation mark
  • In dynamic change the dynamic bean MessageProviderImpl so that it would delegate the decision about the punctuation mark to the new bean in app
  • Add a bean implementing the new interface to app

Adding the New Interface to api

Let's add a new interface x.dynamic.api.PunctuationMarkProvider to api:

api/src/main/java/x/dynamic/api/PunctuationMarkProvider.java
1
2
3
4
5
package x.dynamic.api;

public interface PunctuationMarkProvider {
    String getPunctuationMark();
}

Note that this interface does NOT extend org.taruts.djig.dynamicApi.DynamicComponent, because it is NOT a dynamic bean interface, it is for a STATIC bean.

Now open the terminal, go to the api directory and run gradle publish. The new API JAR will be published to the local Maven repository.

Changing the Dynamic Bean in dynamic

Go to the dynamic project.

Info

We've just published the API JAR for the second time with the same artifact version. If we use an IDE, we might need to synchronize the IDE dependencies with those of the Gradle build. E.g. in Intellij IDEA 2022.3.2 this is done with Gradle tool window / right mouse button on the dynamic tree node / context menu element Reload Gradle project or Refresh Gradle dependencies. You might need to run both, one after another.

Let's change x.dynamic.MessageProviderImpl

dynamic/src/main/java/x/dynamic/MessageProviderImpl.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package x.dynamic;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import x.dynamic.api.MessageProvider;
import x.dynamic.api.PunctuationMarkProvider;

@Component
public class MessageProviderImpl implements MessageProvider {

    @Autowired
    private PunctuationMarkProvider markProvider;//(1)!

    @Override
    public String getMessage() {
        return "Hi, world" + markProvider.getPunctuationMark();//(2)!
    }
}
  1. Adding a dependency on the static bean via the PunctuationMarkProvider interface from the API JAR
  2. Delegate the decision about the punctuation mark at the end of the greeting to the static bean

Commit and push the changes to GitLab.

Add the implementation to app

Go back to app.

Stop the application if it's still running.

Again, if we're using an IDE, synchronize the dependencies with the Gradle build.

In app create a new class x.app.PunctuationMarkProviderImpl implementing the previously created interface in API JAR:

app/src/main/java/x/app/PunctuationMarkProviderImpl.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package x.app;

import org.springframework.stereotype.Component;
import x.dynamic.api.PunctuationMarkProvider;

@Component
public class PunctuationMarkProviderImpl implements PunctuationMarkProvider {

    @Override
    public String getPunctuationMark() {
        return "!!!!!!!!!!";
    }
}

Testing the Changes

Now rebuild the app and start it all over again.

Refresh the tab with http://localhost:8080/hello, we should now see Hi, world!!!!!!!!!!.

Success

The exclamation marks are in place, we can see that the dynamic bean uses the dependency on the bean from the main part of the app!

Adding a Dynamic REST Endpoint

Note

You can read more about dynamic endpoints in Dynamic Endpoints.

Let's add a new REST endpoint to our application, without stopping it.

Start the application if it isn't running.

Adding a Controller to dynamic

Go to dynamic and add the following controller:

dynamic/src/main/java/x/dynamic/DynamicController.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package x.dynamic;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("some/path")//(1)!
public class DynamicController {

    @GetMapping
    String get() {
        return "Some text";
    }
}
  1. Because this is a controller from a dynamic project, the app will add the /dynamic-project/<project name>/ prefix to the path some/text. In our case, as the project name is 1, the prefix will be /dynamic-project/1/, and the full URL will be http://localhost:8080/dynamic-project/1/some/path.

Push the changes.

After some seconds open this in the browser: http://localhost:8080/dynamic-project/1/some/path.

You'll get some text in response.

Success

We've successfully added the REST endpoint http://localhost:8080/dynamic-project/1/some/path to our application without stopping the app!

Using the workspace Gradle Plugin to Automate Work with Multiple Git Repositories

At the moment our code base has three repositories:

  • api
  • dynamic
  • app

If we had one more dynamic project, we'd have 4 or 5.

4 - if the second dynamic project used the API JAR from our api project, 5 - if the second dynamic project used a separate API JAR with its sources in a separate Git repository.

In other words, if you work with djig you may have to deal with many Git repositories.

To simplify dealing with many Git repositories in djig we recommend using the org.taruts.workspace Gradle plugin.

This allows to rid new developers, starting to work on the product, of the necessity to clone a lot of Repositories manually.

Instead, they would need to do the following:

  • Clone just one repository (workspace)
  • Set up their GitLab or GitHub credentials
  • Run gradle cloneAll

Of course, if there are just three repositories, you can't say for sure this simplifies much, but if the number is five or more, the benefits of using the workspace Gradle plugin get more noticeable.

So, let's add the workspace Gradle plugin to our codebase.

Putting the app and api Projects on GitLab

So far only the dynamic project was on GitLab.

Now we're going to add app and api as well.

Go to GitLab and add EMPTY, PRIVATE projects api and app there. Be sure they are ABSOLUTELY EMPTY, without README.MD or something else.

Now we're going to init local Git repositories in the api and app directories, and commit and push the projects to GitLab.

For the api Project

1
2
3
4
5
6
cd api
git init --initial-branch=main
git remote add origin <HTTPS clone URL of the api project>
git add .
git commit -m "Initial commit"
git push -u origin main

For the app Project

1
2
3
4
5
6
cd app
git init --initial-branch=main
git remote add origin <HTTPS clone URL of the app project>
git add .
git commit -m "Initial commit"
git push -u origin main

Creating the workspace repository

The workspace repository, as we wrote above, is the only repository, that a new developer will clone manually. Other repositories (api, dynamic and app) will be cloned automatically.

Creating the workspace Local Directory

1
2
cd djig-test
mkdir workspace

Configuring the Build

1
2
3
plugins {
    id 'org.taruts.workspace' version '1.0.2'
}
1
2
3
plugins {
    id("org.taruts.workspace") version "1.0.2"
}

.gitignore

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
### Gradle ###
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/

### workspace Gradle plugin ###
projects #(1)!

### Your IDE stuff ###
... #(2)!
  1. The parent directory, in which gradle cloneAll will clone repositories
  2. Files, that the IDE creates to work with the project, that are not normally pushed

Setting up GitLab credentials

There are a lot of ways to specify Git repository hosting credentials for workspace. We'll be using only one of them, which is having the credentials in ~/.gradle/gradle.properties:

~/.gradle/gradle.properties
1
2
org.taruts.workspace.gitlab-com.username=<your GitLab username>
org.taruts.workspace.gitlab-com.password=<your GitHub password>

Or, if you prefer to use Personal Access Token (PAT) then specify it as username:

~/.gradle/gradle.properties
1
org.taruts.workspace.gitlab-com.username=<Your PAT>

Adding the workspace Project on GitLab

Log in at GitLab and a new EMPTY, PRIVATE repository workspace in the same project group with api, dynamic and app.

Be sure not to add README.MD, the repository must be COMPLETELY EMPTY.

Copy the HTTPS clone URL.

Pushing workspace to GitLab

1
2
3
4
5
6
cd workspace
git init --initial-branch=main
git remote add origin <HTTPS clone URL of the workspace project>
git add .
git commit -m "Initial commit"
git push -u origin main

Automatic Cloning of all other Projects

Run the following:

1
2
cd workspace
gradle cloneAll

In the projects directory there appeared api, app and dynamic.

Success

We have successfully applied the workspace Gradle plugin to enable automatic cloning of a group of repositories.

Warning

The list of repositories to clone is built after adding the workspace Gradle plugin to the build (actually, when any task is launched afterwards).

If, after the list of repositories is created, you add another repository to the GitLab project group where the workspace project sits, the list will stay the same, and cloneAll won't clone the new repository.

For example, if by mistake you run cloneAll before all the repositories are on GitLab, and you add them afterwards, consequent cloneAll launches won't clone them.

To make cloneAll see the new repositories, run gradle refreshProjectsList.

After that, in a separate launch of gradle execute gradle cloneAll (running them together in gradle refreshProjectsList cloneAll won't have the desired effect).