Home | Send Feedback

Git with JGit

Published: 4. August 2019  •  java

JGit developed and maintained by the Eclipse Foundation is a pure Java implementation of Git and does not depend on a native library or the Git command-line tool.

The library was originally developed for the Eclipse IDE, where it's the main driver of the Git integration. But the library does not depend on the Eclipse IDE and is hosted on the Maven Central repository. It's therefore easy to add JGit to any Java application.

In this blog post, we are going to look at a few JGit examples. I'm not covering every command, just a few everyday Git tasks.

If you are looking for Java examples that are not covered here, check out the following repository:
https://github.com/centic9/jgit-cookbook
In this repository, you find JGit code examples and snippets for every possible Git use case.

In the last section of this blog post, I show you how to create a backup application for your GitHub repositories.

Installation

In a Maven managed project, you add JGit with the following dependency:

      <groupId>org.eclipse.jgit</groupId>
      <artifactId>org.eclipse.jgit</artifactId>
      <version>6.8.0.202311291450-r</version>
    </dependency>
    <dependency>

pom.xml

The JGit library consists of two parts. It contains classes that implement the low-level part of Git, and on top of this implementation, the library provides a high-level API called Porcelain. The Porcelain API is modeled after the Git command-line tool. If you know the command line command for a specific task, it can be easily translated into JGit code.

The main entry point to the Porcelain API is the class org.eclipse.jgit.api.Git

This class offers methods to construct command classes. Each Porcelain command is represented by one command class. A command class has setters for all the arguments and options. Each command class provides a call() method to execute the command. Methods of command classes are chainable.

Init

The command git init creates an empty Git repository. This is the first command you run when you want to put a new project under Git control.

To initialize a new Git repository with JGit, you call the static init() method. The method returns an instance of Git. Make sure that you close a Git instance with the close() method. The Git class implements AutoCloseable so you can wrap the init() call in an automatic resource management block.

    Path repoPath = Paths.get("./my_project");

    try (Git git = Git.init().setDirectory(repoPath.toFile()).call()) {

    }

Init.java

setDirectory() tells the method where to create the new Git repository. init() creates the directory if it does not exist yet. You may call init() on an existing Git repository, it does not delete the history.

As mentioned before, all methods from Git return a command class. In case of init() this is the InitCommand class. Instead of method chaining, you assign the command class to a variable and then call methods one after the other.

    InitCommand init = Git.init();
    init.setDirectory(repoPath.toFile());
    try (Git git = init.call()) {

    }

Init.java

It does not matter which style you use. There is no difference in functionality. Most examples in this blog post use the chained methods style.

Open repository

If you want to work with an existing Git repository, you open it with the static open() method.

    try (Git git = Git.open(repoPath.toFile())) {

    }

Init.java

As the argument, you must pass the file path to the Git repository. This command directly returns an instance of Git.

Add, remove and commit

With a Git instance in hand, we can now run Git commands.

Everyday tasks when working with Git are adding files to the index, removing files from the index, and create commits.

The code in the following example creates two files, adds them to the index, and creates a commit. You see two methods add() and commit() in action.

      Files.writeString(repoPath.resolve("file1.md"), "Hello World 1");
      git.add().addFilepattern("file1.md").call();
      git.commit().setMessage("create file1").setAuthor("author", "author@email.com")
          .call();

      Files.writeString(repoPath.resolve("file2.md"), "Hello World 2");
      git.add().addFilepattern(".").call();
      git.commit().setMessage("create file2").setAuthor("author", "author@email.com")
          .call();

AddCommit.java

add() adds one or multiple files to the index. You specify the files you want to add with the addFilepattern() method. In the first example, we add one file with the name file1.md to the index. In the second example, we specify a directory (. = current directory). The directory is relative to the path you pass to the open() method. When you specify a directory, add() scans the provided directory and all subdirectories recursively for untracked, not ignored files and for changed tracked files and adds them to the index.
Notice that addFilepattern() does not yet support fileglobs (e.g. *.md).

commit() creates a new commit with the current contents of the index and the given message and author.


In the following example, we change a tracked file. In this case, we may omit the add() command and instead call setAll(true) on the commit command. This is equivalent to the -a option on the command line (git commit -a ...) and tells commit to stage files that have been modified and deleted automatically. This only affects tracked files. New files always need to be added with add() first.

      Files.writeString(repoPath.resolve("file1.md"), "Hello Earth 1");
      git.commit().setAll(true).setMessage("update file1")
          .setAuthor("author", "author@email.com").call();

AddCommit.java


The next example deletes a tracked file. You can either use the -a option (setAll(true)) to automatically stage the change before the commit, or you explicitly call rm() to remove the file from the index and then commit it. rm() deletes the file in the working tree and index, unless you call setCached(true) on the rm command. With this option, rm() removes the file only from the index.

      Files.deleteIfExists(repoPath.resolve("file2.md"));
      git.commit().setAll(true).setMessage("delete file2")
          .setAuthor("author", "author@email.com").call();

      // or
      git.rm().addFilepattern("file2.md").call();
      git.commit().setMessage("delete file2").setAuthor("author", "author@email.com")
          .call();

AddCommit.java

Log

The log() method lists the commits of a repository. The command returns an iterable of RevCommit instances. With this object, you have access to commit information like the id, time, message, and author.

The following example lists all commits of the repository. Notice the call of the all() method.

      Iterable<RevCommit> logs = git.log().all().call();
      for (RevCommit rev : logs) {
        System.out.print(Instant.ofEpochSecond(rev.getCommitTime()));
        System.out.print(": ");
        System.out.print(rev.getFullMessage());
        System.out.println();
        System.out.println(rev.getId().getName());
        System.out.print(rev.getAuthorIdent().getName());
        System.out.println(rev.getAuthorIdent().getEmailAddress());
        System.out.println("-------------------------");
      }

Log.java

Instead of listing all commits, an application can limit the list to just commits that affect a particular file or directory.

Return only commits that affect file1.md.

      logs = git.log().addPath("file1.md").call();

Log.java

Status

git status is a command that displays the state of your working tree. It displays paths that have differences between the index and the current HEAD commit, paths that have differences between the working tree and the index file, and paths in the working tree that are not tracked (and are not ignored).

The JGit equivalent is git.status().call()

The following example creates a new Git repository and creates multiple files in different states to demonstrate the status command. The code also shows you how you can programmatically work with the .gitignore file.

      Files.writeString(repoPath.resolve("ignored_file.md"), "Ignore me");
      Files.writeString(repoPath.resolve(".gitignore"), "ignored_file.md");

      Files.writeString(repoPath.resolve("file1.md"), "Hello World 1");
      Files.writeString(repoPath.resolve("file2.md"), "Hello World 2");
      Files.writeString(repoPath.resolve("file3.md"), "Hello World 3");
      git.add().addFilepattern(".").call();
      git.commit().setMessage("create files").setAuthor("author", "author@email.com")
          .call();

      Files.writeString(repoPath.resolve("file4.md"), "Hello World 4");
      Files.writeString(repoPath.resolve("file5.md"), "Hello World 5");
      git.add().addFilepattern("file5.md").call();

      Files.writeString(repoPath.resolve("file2.md"), "Hello Earth 2");
      git.add().addFilepattern("file2.md").call();

      // Files.deleteIfExists(repoPath.resolve("file1.md"));
      git.rm().addFilepattern("file1.md").call();

      Files.deleteIfExists(repoPath.resolve("file3.md"));

      Files.createDirectory(repoPath.resolve("new_directory"));

      Status status = git.status().call();
      System.out.println("Added              : " + status.getAdded());
      System.out.println("Changed            : " + status.getChanged());
      System.out.println("Removed            : " + status.getRemoved());
      System.out.println("Uncommitted Changes: " + status.getUncommittedChanges());
      System.out.println("Untracked          : " + status.getUntracked());
      System.out.println("Untracked Folders  : " + status.getUntrackedFolders());
      System.out.println("Ignored Not Index  : " + status.getIgnoredNotInIndex());
      System.out.println("Conflicting        : " + status.getConflicting());
      System.out.println("Missing            : " + status.getMissing());

GitStatus.java

The status command returns an instance of Status, which contains all the differences. Most getter methods of Status return a set of strings.

The example above prints out this output:

Added              : [file5.md]
Changed            : [file2.md]
Removed            : [file1.md]
Uncommitted Changes: [file5.md, file3.md, file2.md, file1.md]
Untracked          : [file4.md]
Untracked Folders  : [new_directory]
Ignored Not Index  : [ignored_file.md]
Conflicting        : []
Missing            : [file3.md]

Another useful method of Status is isClean(), which returns true if there are no differences.

Branch

JGit provides a series of commands that work with branches.

To get the name of the current checked out branch issue the following command:

String currentBranch = git.getRepository().getFullBranch();

Create and checkout branch

You create a branch with the branchCreate() method. You have to specify the name of the branch with setName().
checkout() updates the files in the working tree to match the branch in the repository.

      git.branchCreate().setName("new_feature").call();
      git.checkout().setName("new_feature").call();

Branch.java

You can create and checkout a branch with the checkout() method when you call setCreateBranch(true). This option automatically creates the branch if it does not exist yet.

      git.checkout().setName("new_feature").setCreateBranch(true).call();

Branch.java


Merge branch

Usually, after you have finished working on a branch, you want to merge it with another branch.

First checkout the branch you want to merge into

      git.checkout().setName("master").call();

Branch.java

Then get an ObjectId to the branch and call the merge() method. As an argument, you specify the branch you want to merge with include().

      ObjectId branchObjectId = git.getRepository().resolve("new_feature");
      MergeResult mergeResult = git.merge()
          // .setFastForward(MergeCommand.FastForwardMode.NO_FF)
          // .setCommit(false)
          // .setMessage("merge")
          .include(branchObjectId).call();

      System.out.println(mergeResult);
      if (mergeResult.getConflicts() != null) {
        for (Map.Entry<String, int[][]> entry : mergeResult.getConflicts().entrySet()) {
          System.out.println("Key: " + entry.getKey());
          for (int[] arr : entry.getValue()) {
            System.out.println("value: " + Arrays.toString(arr));
          }
        }
      }

Branch.java

The merge command either does a fast-forward merge, by updating the branch pointer without creating a merge commit or incorporates the changes from the other branch into the current branch and creates a commit. If you always want to create a merge commit, disable fast-forward with setFastForward(MergeCommand.FastForwardMode.NO_FF). And if you don't want to create a commit, disable it with setCommit(false).


List branch

The branchList() method lists all branches. By default, it only returns local branches. You can change that with setListMode(). ListMode.ALL returns local and remote branches, ListMode.REMOTE returns only remote branches.

      List<Ref> branches = git.branchList().setListMode(ListMode.ALL).call();
      for (Ref branch : branches) {
        System.out.println(branch.getName());
      }

Branch.java


Rename branch

branchRename() renames an existing branch.

      git.branchRename().setOldName("new_feature").setNewName("amazing_feature").call();

Branch.java


Delete branch

The method branchDelete deletes a branch. This command fails when the specified branch is checked out, so you have to check out another branch first.

      git.checkout().setName("master").call();
      git.branchDelete().setBranchNames("amazing_feature").setForce(true).call();

Branch.java

By default, branchDelete() checks whether the branch you want to delete is already merged into the current branch. If the branch is not merged, deletion will be refused. You change this behavior by calling setForce(true), in that case, no check will be performed, and the branch will be deleted regardless if he's merged or not.

Tag

You create tags with the tag() method. JGit provides the tagDelete() method to delete a tag. setName() and setMessage() specify the name and the message of the tag .

With tagList() you get a list of all tags in the repository.

      Files.writeString(repoPath.resolve("file1.md"), "Hello World 1");
      git.add().addFilepattern(".").call();
      git.commit().setMessage("initial commit").setAuthor("author", "author@email.com")
          .call();

      git.tag().setName("v1.0").setMessage("version 1.0").call();

      Files.writeString(repoPath.resolve("file2.md"), "Hello World 2");
      git.add().addFilepattern(".").call();
      git.commit().setMessage("new feature").setAuthor("author", "author@email.com")
          .call();

      git.tag().setName("v1.0.1").setMessage("version 1.0.1").call();
      git.tagDelete().setTags("v1.0.1").call();

      git.tag().setName("v1.1").setMessage("version 1.1").call();

      List<Ref> tags = git.tagList().call();
      for (Ref tag : tags) {
        System.out.println(tag.getName());
      }

Tag.java

Diff

The diff() method returns a collection of changes between two objects (commits, tags, branches).

The following example opens the repository from the previous tag example and compares the two tags. The diff() method requires a reference to an old and new tree. To get this reference we need an ObjectReader instance, two ObjectId for the two objects we want to compare and an instance of CanonicalTreeParser for each of the objects. We then configure these parsers with setNewTree() and setOldTree().

To get the ObjectId, the application calls resolve() from the Repository class with the name of the tag. The diff command requires the tree id, which we get with the suffix ^{tree}.

    Path repoPath = Paths.get("./my_project_tag");
    try (Git git = Git.open(repoPath.toFile());
        ObjectReader reader = git.getRepository().newObjectReader()) {

      ObjectId oldObject = git.getRepository().resolve("v1.0^{tree}");
      ObjectId newObject = git.getRepository().resolve("v1.1^{tree}");

      CanonicalTreeParser oldIter = new CanonicalTreeParser();
      oldIter.reset(reader, oldObject);
      CanonicalTreeParser newIter = new CanonicalTreeParser();
      newIter.reset(reader, newObject);

      List<DiffEntry> diffs = git.diff().setNewTree(newIter).setOldTree(oldIter).call();

      for (DiffEntry entry : diffs) {
        System.out.println("type: " + entry.getChangeType());
        System.out.println("old : " + entry.getOldPath());
        System.out.println("new : " + entry.getNewPath());
      }
    }

Diff.java

The diff() method returns a List of DiffEntry instances. Each instance represents one change and contains the type of the change (ADD, MODIFY, DELETE, RENAME, COPY) as well as the old and new path.

There is only one change between the two tags; we added the file file2.md. Notice that the old path is /dev/null because the file did not exist in the old tree.

type: ADD
old : /dev/null
new : file2.md

Let's look at an example with a bit more changes. The following program compares two commits.

To get the reference to the two commits, we use the following code.

      ObjectId oldObject = git.getRepository().resolve("HEAD~^{tree}");
      ObjectId newObject = git.getRepository().resolve("HEAD^{tree}");

Diff.java

HEAD references the last commit in our repository and HEAD~ points to the second to last commit.

    repoPath = Paths.get("./my_project_diff");
    try (Git git = Git.init().setDirectory(repoPath.toFile()).call();
        ObjectReader reader = git.getRepository().newObjectReader()) {
      Files.writeString(repoPath.resolve("file1.md"), "Hello World 1");
      Files.writeString(repoPath.resolve("file2.md"), "Hello World 2");
      Files.writeString(repoPath.resolve("file3.md"), "Hello World 3");
      Files.writeString(repoPath.resolve("file4.md"), "Hello World 4");
      git.add().addFilepattern(".").call();
      git.commit().setMessage("initial commit").setAuthor("author", "author@email.com")
          .call();

      Files.writeString(repoPath.resolve("file1.md"), "Hello Earth 1");
      Files.delete(repoPath.resolve("file4.md"));
      Files.move(repoPath.resolve("file2.md"), repoPath.resolve("file22.md"));
      git.add().addFilepattern(".").call();
      git.rm().addFilepattern("file2.md").call();
      git.rm().addFilepattern("file4.md").call();

      git.commit().setMessage("update").setAuthor("author", "author@email.com").call();

      ObjectId oldObject = git.getRepository().resolve("HEAD~^{tree}");
      ObjectId newObject = git.getRepository().resolve("HEAD^{tree}");

      CanonicalTreeParser oldIter = new CanonicalTreeParser();
      oldIter.reset(reader, oldObject);
      CanonicalTreeParser newIter = new CanonicalTreeParser();
      newIter.reset(reader, newObject);

      List<DiffEntry> diffs = git.diff().setNewTree(newIter).setOldTree(oldIter).call();

      for (DiffEntry entry : diffs) {
        System.out.println("type: " + entry.getChangeType());
        System.out.println("old : " + entry.getOldPath());
        System.out.println("new : " + entry.getNewPath());
      }

    }

Diff.java

This code prints the following output. Whenever the file does not exist in one of the trees, the path method returns the string "/dev/null".

type: MODIFY
old : file1.md
new : file1.md

type: DELETE
old : file2.md
new : /dev/null

type: ADD
old : /dev/null
new : file22.md

type: DELETE
old : file4.md
new : /dev/null

JGit provides a special empty tree iterator if you need all the differences from the beginning of the repository to a certain point.

import org.eclipse.jgit.treewalk.EmptyTreeIterator;
List<DiffEntry> diffs = git.diff().setNewTree(newIter).setOldTree(new EmptyTreeIterator()).call();

Add remote and push

When you work with other developers together in a team, usually you not only have your local Git repository but also a remote Git repository where everybody pushes their changes to it and fetches the changes from the other team members.

Before you can push your changes to a remote repository, you need to configure the remote repository with the remoteAdd method.

After adding the remote config, you push your repository objects into the remote repository with push()

    Path repoPath = Paths.get("./test_repo");
    try (Git git = Git.init().setDirectory(repoPath.toFile()).call()) {

      Files.writeString(repoPath.resolve("README.md"), "# test_repo");
      git.add().addFilepattern("README.md").call();
      git.commit().setMessage("first commit").setAuthor("author", "author@email.com")
          .call();

      // git remote add origin git@github.com:ralscha/test_repo.git
      RemoteAddCommand remoteAddCommand = git.remoteAdd();
      remoteAddCommand.setName("origin");
      remoteAddCommand.setUri(new URIish("git@github.com:ralscha/test_repo.git"));
      remoteAddCommand.call();

      // git push -u origin master
      PushCommand pushCommand = git.push();
      pushCommand.add("master");
      pushCommand.setRemote("origin");
      pushCommand.call();

Remote.java

Usually you need to be authenticated for the push() command to work. When you work with a remote repository that requires a username/password authentication you have to configure a UsernamePasswordCredentialsProvider instance.

pushCommand.setCredentialsProvider(new UsernamePasswordCredentialsProvider("username", "password"));

The example above transfers the changes from the local repository to GitHub with SSH. In that case, the application uses public/private key authentication. The SSH library that JGit uses automatically looks for the private key in the user's home directory.

If the SSH private key is stored somewhere else or your key is protected with a passphrase you need to add the following code to configures the underlying SSH connection.

      SshSessionFactory sshSessionFactory = new JschConfigSessionFactory() {
        @Override
        protected void configure(Host host, Session session) {
          // do nothing
        }

        @Override
        protected JSch createDefaultJSch(FS fs) throws JSchException {
          JSch defaultJSch = super.createDefaultJSch(fs);
          defaultJSch.addIdentity("c:/path/to/my/private_key");

          // if key is protected with passphrase
          // defaultJSch.addIdentity("c:/path/to/my/private_key", "my_passphrase");

          return defaultJSch;
        }
      };

      pushCommand = git.push();
      pushCommand.setTransportConfigCallback(transport -> {
        SshTransport sshTransport = (SshTransport) transport;
        sshTransport.setSshSessionFactory(sshSessionFactory);
      });
      pushCommand.add("master");
      pushCommand.setRemote("origin");
      pushCommand.call();

Remote.java

Clone

To clone a remote repository to your local disk, you call the static cloneRepository() method. This method requires the URI of the remote repository and a path to a local directory as an argument. cloneRepository() creates the local directory if it does not exist before it clones the repository.

    Path localPath = Paths.get("./my_other_test_repo");
    try (Git git = Git.cloneRepository().setURI("git@github.com:ralscha/test_repo.git")
        .setDirectory(localPath.toFile()).call()) {

      Files.writeString(localPath.resolve("SECOND.md"), "# another file");
      git.add().addFilepattern("SECOND.md").call();
      git.commit().setMessage("second commit").setAuthor("author", "author@email.com")
          .call();

      git.push().call();
    }

Clone.java

The example first clones the repository, then adds and commits a second file and pushes the change to the remote repository.

Pull

When you ran the last two examples you have two local repositories "./test_repo" and "./my_other_test_repo" and a remote repository.

In the last example, we pushed a change into the remote repository that is not available in the "./test_repo" repository. Let's see what happens when we try to add another change then push it to the remote repository.

    Path repoPath = Paths.get("./test_repo");
    try (Git git = Git.open(repoPath.toFile())) {

      Files.writeString(repoPath.resolve("THIRD.md"), "# third file");
      git.add().addFilepattern("THIRD.md").call();
      git.commit().setMessage("third commit").setAuthor("author", "author@email.com")
          .call();

      Iterable<PushResult> pushResults = git.push().call();
      for (PushResult pushResult : pushResults) {
        for (RemoteRefUpdate update : pushResult.getRemoteUpdates()) {
          System.out.println(update.getStatus());
        }
      }

Pull.java

When we run this command, we get a push status of "REJECTED_NONFASTFORWARD". That makes sense because we can only push changes when our local repository is in sync with the remote repository. For that to happen, we have to pull all changes from the remote repository into our local repository. In JGit, the method pull() performs this task.

pull is a combination of the fetch and the merge command. That's the reason the PullResult object references the FetchResult and MergeResult instances. These result objects give us detailed information about what files have been changed and the state of the operations. In this case, merge does a FAST_FORWARD merge.

      PullResult result = git.pull().call();
      FetchResult fetchResult = result.getFetchResult();
      MergeResult mergeResult = result.getMergeResult();
      MergeStatus mergeStatus = mergeResult.getMergeStatus();

      for (TrackingRefUpdate update : fetchResult.getTrackingRefUpdates()) {
        System.out.println(update.getLocalName());
        System.out.println(update.getRemoteName());
        System.out.println(update.getResult());
      }

Pull.java

After a successful pull, we can now push our change from before to the remote repository. The following push() command returns the status "OK".

      pushResults = git.push().call();
      for (PushResult pushResult : pushResults) {
        for (RemoteRefUpdate update : pushResult.getRemoteUpdates()) {
          System.out.println(update.getStatus());
        }
      }

Pull.java

JGit Use Case: GitHub Backup

In this section, we create a simple backup application for GitHub. It clones all public repositories of a particular user to our disk and compresses them into a zip file.

The first task is to get a list of all public repository of a user. This is quite easy because GitHub provides an HTTP API to access this kind of information.

The endpoint we need is called List user repositories

You don't need an API key, and this particular endpoint can also be accessed anonymously.

To check it out, enter the following URL into your browser, replace :username with an existing GitHub user name, and you get back a JSON. This endpoint only returns public repositories.
https://api.github.com/users/:username/repos

We could now write Java code that sends a GET request to this endpoint and parses the response with a JSON parser. But there is an even simpler solution. The Eclipse developers wrote a client library for the GitHub HTTP API. All you have to do is add the following dependency to your project

      <groupId>org.eclipse.mylyn.github</groupId>
      <artifactId>org.eclipse.egit.github.core</artifactId>
      <version>2.1.5</version>
    </dependency>
  </dependencies>

pom.xml

To access the endpoint, we have to instantiate the class org.eclipse.egit.github.core.service.RepositoryService first, call the method getRepositories() and pass the username as an argument. Under the hood, the library sends a GET request to GitHub and parses the response into a collection of Repository instances.

    RepositoryService service = new RepositoryService();
    for (Repository repo : service.getRepositories(user)) {

Main.java

The Repository object contains the name of the repository and the clone URL. We can now easily implement the part of the application that clones all the repositories. The application clones the repositories into a folder specified with the backupDirectory parameter.

Before the application clones the repository, it checks if the backup folder already contains a copy of the repository. The isValidLocalRepository() method shows you how to perform this check with JGit. The method checks if the repository contains a valid Git object database and returns true, otherwise false if the directory does not exist or does not contain a Git repository.

If the backup folder already contains a copy of the repository, the program calls the fetch() method to download only the latest changes.

When the repository is not cloned yet, the application calls cloneRepository(). The difference to the clone example we saw in the example section is that we clone the repository as a bare repository (setBare(true)). A bare repository only contains the Git repository, the content of the .git folder, whereas a non-bare repository contains the Git repository and a checked-out working tree.

Because bare repositories don't contain a working tree they use less disk space and are, therefore, a perfect fit for our backup program.

  public static void backup(String user, Path backupDirectory) throws IOException,
      InvalidRemoteException, TransportException, IllegalStateException, GitAPIException {

    Files.createDirectories(backupDirectory);

    RepositoryService service = new RepositoryService();
    for (Repository repo : service.getRepositories(user)) {

      Path repoDir = backupDirectory.resolve(repo.getName());
      Files.createDirectories(repoDir);

      if (isValidLocalRepository(repoDir)) {
        System.out.println("fetching : " + repo.getName());
        Git.open(repoDir.toFile()).fetch().call();
      }
      else {
        System.out.println("cloning : " + repo.getName());
        Git.cloneRepository().setBare(true).setURI(repo.getCloneUrl())
            .setDirectory(repoDir.toFile()).call();
      }
    }
  }

  private static boolean isValidLocalRepository(Path repoDir) {
    try (FileRepository fileRepository = new FileRepository(repoDir.toFile())) {
      return fileRepository.getObjectDatabase().exists();
    }
    catch (IOException e) {
      return false;
    }
  }

Main.java

After the application cloned all the repositories, it compresses all files into a zip file with the following code:

  private static void compress(Path backupDirectory, Path archive) throws IOException {
    try (OutputStream os = Files.newOutputStream(archive);
        ZipOutputStream zipOS = new ZipOutputStream(os)) {
      zipOS.setLevel(Deflater.BEST_COMPRESSION);

      Files.walk(backupDirectory).filter(path -> !Files.isDirectory(path))
          .forEach(path -> {
            try {
              ZipEntry zipEntry = new ZipEntry(
                  backupDirectory.relativize(path).toString());
              zipOS.putNextEntry(zipEntry);
              Files.copy(path, zipOS);
              zipOS.closeEntry();
            }
            catch (IOException e) {
              e.printStackTrace();
            }
          });
    }
  }

Main.java

To simplify the deployment of the tool, we create a fat jar that can be called from the command line with java -jar ...jar.

In a Maven application, we use the shade plugin for this purpose.

I had to add a filter configuration that removes signature files from signed jars. The JGit jar is signed, and the application throws an error if you repackage it into a fat jar.

    <finalName>githubbackup</finalName>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>3.5.1</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <filters>
                <filter>
                  <artifact>*:*</artifact>
                  <excludes>
                    <exclude>META-INF/*.SF</exclude>
                    <exclude>META-INF/*.DSA</exclude>
                    <exclude>META-INF/*.RSA</exclude>
                  </excludes>
                </filter>
              </filters>
              <transformers>
                <transformer
                  implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <mainClass>ch.rasc.backup.Main</mainClass>
                </transformer>
              </transformers>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

pom.xml

On the command line, you build the application with ./mvnw package or .\mvnw.cmd package on Windows. This command compiles the application, then calls the shade plugin, which bundles all 3rd party libraries into one jar and inserts a MANIFEST.mf file into the jar with the path to the main class.

After a build, you find the bundled jar in the target folder. Start the application with the following command

java -jar githubbackup.jar github_username backup_folder backup.7z

This concludes my blog post about JGit. If you want to learn more about JGit check out the following resources:

Blog posts about JGit

JGit code examples and snippets