JGit: Using Git Directly Inside a Java Application
Git is usually something we interact with from a terminal:
git clone
git status
git pull
git checkout
git commit
git pushThat works perfectly when a developer is manually working with a repository.
But sometimes Git operations need to be performed by an application.
For example, a Java application may need to:
- Clone a repository
- Read commit history
- Detect changed files
- Switch branches
- Pull the latest configuration
- Create a commit
- Apply a specific change
- Build a repository analysis tool
One option is to execute Git commands using ProcessBuilder:
ProcessBuilder processBuilder =
new ProcessBuilder("git", "pull");
processBuilder
.directory(new File("./repository"))
.start();Although this can work, the application now depends on an external Git installation and must handle process execution, exit codes, standard output, and error output.
A cleaner Java-native option is JGit.
What Is JGit?
JGit is a pure Java implementation of the Git version-control system.
It provides Java APIs for reading and writing Git repositories, accessing repository history, and performing common Git operations. It can run as a Java library without starting the native Git command-line process. (GitHub)
In simple terms:
Instead of executing:
git pulla Java application can call:
git.pull().call();Instead of:
git checkout developit can use:
git.checkout()
.setName("develop")
.call();JGit is not an automation platform by itself.
It is the library that allows your Java or Spring Boot application to perform Git operations. Your application decides when and why those operations should happen.
For example:
Adding JGit to a Maven Project
Add the following dependency:
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>${jgit.version}</version>
</dependency>The version can be defined in the Maven properties:
<properties>
<java.version>21</java.version>
<jgit.version>YOUR_JGIT_VERSION</jgit.version>
</properties>Using a property keeps the dependency version easy to update.
The core dependency is enough for the examples in this article.
The Git Object
The main entry point for common JGit operations is the Git class.
It provides a high-level API similar to common Git porcelain commands such as clone, status, log, add, commit, branch, checkout, fetch, pull, and push. (archive.eclipse.org)
A Git object normally represents a local repository:
Git git = Git.open(new File("./my-repository"));Because Git implements AutoCloseable, it should normally be used with try-with-resources:
try (Git git = Git.open(new File("./my-repository"))) {
// Use the repository
}This ensures that repository resources are closed correctly.
Cloning a Repository
The first practical example is cloning a remote repository.
import org.eclipse.jgit.api.Git;
import java.io.File;
public class CloneRepositoryExample {
public static void main(String[] args) {
String repositoryUrl =
"https://github.com/eclipse-jgit/jgit.git";
File targetDirectory =
new File("./repositories/jgit");
try (Git git = Git.cloneRepository()
.setURI(repositoryUrl)
.setDirectory(targetDirectory)
.call()) {
System.out.println(
"Repository cloned to: "
+ git.getRepository()
.getWorkTree()
.getAbsolutePath()
);
} catch (Exception exception) {
System.err.println(
"Clone failed: " + exception.getMessage()
);
}
}
}The important part is:
Git.cloneRepository()
.setURI(repositoryUrl)
.setDirectory(targetDirectory)
.call();This is the JGit equivalent of:
git clone https://github.com/eclipse-jgit/jgit.gitThe call() method executes the configured operation.
Many JGit commands follow the same pattern:
Opening an Existing Repository
When a repository has already been cloned, it can be opened directly:
import org.eclipse.jgit.api.Git;
import java.io.File;
public class OpenRepositoryExample {
public static void main(String[] args) {
File repositoryDirectory =
new File("./repositories/jgit");
try (Git git = Git.open(repositoryDirectory)) {
System.out.println(
"Current branch: "
+ git.getRepository().getBranch()
);
System.out.println(
"Repository directory: "
+ git.getRepository()
.getDirectory()
.getAbsolutePath()
);
} catch (Exception exception) {
System.err.println(
"Could not open repository: "
+ exception.getMessage()
);
}
}
}There are two useful concepts here:
git.getRepository().getWorkTree();This returns the working directory containing the project files.
git.getRepository().getDirectory();This normally returns the repository metadata directory, usually .git.
Reading the Current Branch
The currently checked-out branch can be retrieved with:
String currentBranch =
git.getRepository().getBranch();
System.out.println("Current branch: " + currentBranch);For example:
Current branch: mainThis is useful when an application must validate that an operation is being performed on the expected branch.
Listing Local Branches
JGit can list the local branches of a repository:
import org.eclipse.jgit.lib.Ref;
import java.util.List;
List<Ref> branches =
git.branchList().call();
for (Ref branch : branches) {
System.out.println(branch.getName());
}Possible output:
refs/heads/main
refs/heads/develop
refs/heads/feature/jgit-demoThe returned names are complete Git references.
To display only the short branch name:
for (Ref branch : branches) {
String shortName =
branch.getName()
.replace("refs/heads/", "");
System.out.println(shortName);
}Output:
main
develop
feature/jgit-demoChecking Out a Branch
Switching to an existing local branch is straightforward:
git.checkout()
.setName("develop")
.call();This corresponds to:
git checkout developA simple reusable method could look like this:
public static void checkout(
Git git,
String branchName
) throws Exception {
git.checkout()
.setName(branchName)
.call();
System.out.println(
"Checked out branch: " + branchName
);
}To create and checkout a new branch:
git.checkout()
.setCreateBranch(true)
.setName("feature/jgit-example")
.call();This is similar to:
git checkout -b feature/jgit-exampleReading Commit History
JGit can access the repository history using the log() command.
import org.eclipse.jgit.revwalk.RevCommit;
Iterable<RevCommit> commits =
git.log().call();
for (RevCommit commit : commits) {
System.out.println(
"Commit: " + commit.getName()
);
System.out.println(
"Author: "
+ commit.getAuthorIdent().getName()
);
System.out.println(
"Message: " + commit.getShortMessage()
);
System.out.println("--------------------");
}Example output:
Commit: 0c879869cf91c860c44f6d8e1458c6d7c4a91234
Author: XXX XXX
Message: Add repository status endpoint
--------------------
Commit: e247da43a2fd3f6a70f8381d5f293ab741bc5678
Author: XXX XXX
Message: Update JGit dependency
--------------------To display only the latest five commits:
Iterable<RevCommit> commits =
git.log()
.setMaxCount(5)
.call();A shorter commit hash can be displayed like this:
String shortHash =
commit.abbreviate(7).name();A practical commit output might therefore be:
for (RevCommit commit : commits) {
String shortHash =
commit.abbreviate(7).name();
System.out.printf(
"%s | %s | %s%n",
shortHash,
commit.getAuthorIdent().getName(),
commit.getShortMessage()
);
}Checking Repository Status
The JGit status API reports differences between the working tree, index, and current HEAD. It exposes collections for states such as modified, added, removed, untracked, missing, and conflicting files. (archive.eclipse.org)
import org.eclipse.jgit.api.Status;
Status status = git.status().call();
System.out.println(
"Clean: " + status.isClean()
);
System.out.println(
"Modified: " + status.getModified()
);
System.out.println(
"Added: " + status.getAdded()
);
System.out.println(
"Removed: " + status.getRemoved()
);
System.out.println(
"Untracked: " + status.getUntracked()
);
System.out.println(
"Conflicting: " + status.getConflicting()
);Possible output:
Clean: false
Modified: [README.md]
Added: [src/main/java/JGitExample.java]
Removed: []
Untracked: [notes.txt]
Conflicting: []A useful validation is checking whether the repository is clean before performing another operation:
Status status = git.status().call();
if (!status.isClean()) {
throw new IllegalStateException(
"Repository contains uncommitted changes."
);
}This is important for automated operations such as checkout, pull, merge, reset, or cherry-pick.
Adding and Committing Files
Suppose the Java application creates or modifies a file inside the repository.
The changes can be added to the Git index:
git.add()
.addFilepattern(".")
.call();Then committed:
git.commit()
.setMessage("Update generated configuration")
.call();Together:
git.add()
.addFilepattern(".")
.call();
var commit = git.commit()
.setMessage("Update generated configuration")
.call();
System.out.println(
"Created commit: "
+ commit.abbreviate(7).name()
);This corresponds to:
git add .
git commit -m "Update generated configuration"To add only one file:
git.add()
.addFilepattern("README.md")
.call();The file pattern is relative to the repository working tree.
Pulling the Latest Changes
To pull changes from the configured remote branch:
var result = git.pull().call();
System.out.println(
"Pull successful: " + result.isSuccessful()
);This is equivalent to:
git pullA better implementation checks the result:
var pullResult = git.pull().call();
if (!pullResult.isSuccessful()) {
throw new IllegalStateException(
"Could not pull repository changes."
);
}Before pulling in an automated application, it is a good idea to check that the working tree is clean.
if (!git.status().call().isClean()) {
throw new IllegalStateException(
"Cannot pull because the repository "
+ "contains local changes."
);
}
var pullResult = git.pull().call();Fetching Remote Changes
fetch downloads remote references and objects without integrating them into the current branch.
git.fetch()
.setRemote("origin")
.call();This corresponds to:
git fetch originThe difference between fetch and pull remains the same as in regular Git:
Fetch is useful when an application needs to inspect remote branches or commits before deciding what to do.
Pushing Changes
Changes can be pushed to the remote repository using:
git.push()
.setRemote("origin")
.call();For a private HTTPS repository, credentials can be provided through a credentials provider:
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
var credentials =
new UsernamePasswordCredentialsProvider(
"username",
"personal-access-token"
);
git.push()
.setRemote("origin")
.setCredentialsProvider(credentials)
.call();Tokens should not be hardcoded in source code.
Get Mahad’s stories in your inbox
Join Medium for free to get updates from this writer.
They should be loaded from a secure location such as:
- Environment variables
- A secrets manager
- Kubernetes Secrets
- CI/CD credentials
- An encrypted configuration provider
For example:
String token =
System.getenv("GIT_ACCESS_TOKEN");
if (token == null || token.isBlank()) {
throw new IllegalStateException(
"GIT_ACCESS_TOKEN is not configured."
);
}Applying a Specific Commit with Cherry-Pick
Cherry-pick is only one of the operations available through JGit.
Suppose a specific commit needs to be applied to the currently checked-out branch.
First, resolve its commit hash:
import org.eclipse.jgit.lib.ObjectId;
ObjectId commitId =
git.getRepository()
.resolve("a1b2c3d");Then apply it:
var result = git.cherryPick()
.include(commitId)
.call();
System.out.println(
"Cherry-pick status: " + result.getStatus()
);The complete version should verify that the commit exists:
ObjectId commitId =
git.getRepository().resolve(commitHash);
if (commitId == null) {
throw new IllegalArgumentException(
"Commit not found: " + commitHash
);
}
var result = git.cherryPick()
.include(commitId)
.call();This corresponds to:
git cherry-pick a1b2c3dCherry-pick is therefore not the purpose of JGit. It is simply one command offered by the broader JGit API.
A Small Practical JGit Program
The following example combines several useful operations.
It:
- Clones a repository if it does not exist
- Opens the repository
- Prints the current branch
- Prints repository status
- Lists the latest five commits
- Lists local branches
package com.example.jgit;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevCommit;
import java.io.File;
public class JGitInspector {
private static final String REPOSITORY_URL =
"https://github.com/eclipse-jgit/jgit.git";
private static final File REPOSITORY_DIRECTORY =
new File("./repositories/jgit");
public static void main(String[] args) {
try {
cloneIfMissing();
try (Git git = Git.open(REPOSITORY_DIRECTORY)) {
printRepositoryInformation(git);
printStatus(git);
printBranches(git);
printLatestCommits(git, 5);
}
} catch (Exception exception) {
System.err.println(
"JGit operation failed: "
+ exception.getMessage()
);
}
}
private static void cloneIfMissing()
throws Exception {
File gitDirectory =
new File(REPOSITORY_DIRECTORY, ".git");
if (gitDirectory.exists()) {
return;
}
System.out.println("Cloning repository...");
try (Git ignored = Git.cloneRepository()
.setURI(REPOSITORY_URL)
.setDirectory(REPOSITORY_DIRECTORY)
.call()) {
System.out.println(
"Repository cloned successfully."
);
}
}
private static void printRepositoryInformation(
Git git
) throws Exception {
System.out.println();
System.out.println("Repository Information");
System.out.println("----------------------");
System.out.println(
"Branch: "
+ git.getRepository().getBranch()
);
System.out.println(
"Working tree: "
+ git.getRepository()
.getWorkTree()
.getAbsolutePath()
);
}
private static void printStatus(
Git git
) throws Exception {
Status status = git.status().call();
System.out.println();
System.out.println("Repository Status");
System.out.println("-----------------");
System.out.println(
"Clean: " + status.isClean()
);
System.out.println(
"Modified: " + status.getModified()
);
System.out.println(
"Untracked: " + status.getUntracked()
);
System.out.println(
"Conflicting: " + status.getConflicting()
);
}
private static void printBranches(
Git git
) throws Exception {
System.out.println();
System.out.println("Local Branches");
System.out.println("--------------");
for (Ref branch : git.branchList().call()) {
String shortName =
branch.getName()
.replace("refs/heads/", "");
System.out.println(shortName);
}
}
private static void printLatestCommits(
Git git,
int maximumCommits
) throws Exception {
System.out.println();
System.out.println("Latest Commits");
System.out.println("--------------");
Iterable<RevCommit> commits =
git.log()
.setMaxCount(maximumCommits)
.call();
for (RevCommit commit : commits) {
String shortHash =
commit.abbreviate(7).name();
System.out.printf(
"%s | %s | %s%n",
shortHash,
commit.getAuthorIdent().getName(),
commit.getShortMessage()
);
}
}
}This is still a small application, but it demonstrates the main idea behind JGit:
No shell command needs to be started by the Java program.
Using JGit in Spring Boot
JGit does not require Spring Boot.
It is a normal Java library and can be used in:
- Plain Java applications
- Spring Boot services
- Command-line applications
- Maven plugins
- Gradle plugins
- Desktop applications
- CI/CD tools
Inside Spring Boot, Git operations can be placed in a normal service:
import org.eclipse.jgit.api.Git;
import org.springframework.stereotype.Service;
import java.nio.file.Path;
@Service
public class RepositoryService {
public String getCurrentBranch(
Path repositoryPath
) throws Exception {
try (Git git =
Git.open(repositoryPath.toFile())) {
return git.getRepository().getBranch();
}
}
public boolean isClean(
Path repositoryPath
) throws Exception {
try (Git git =
Git.open(repositoryPath.toFile())) {
return git.status().call().isClean();
}
}
public void pull(
Path repositoryPath
) throws Exception {
try (Git git =
Git.open(repositoryPath.toFile())) {
if (!git.status().call().isClean()) {
throw new IllegalStateException(
"Repository is not clean."
);
}
var result = git.pull().call();
if (!result.isSuccessful()) {
throw new IllegalStateException(
"Pull operation failed."
);
}
}
}
}The Spring Boot application can then trigger this service from:
- A REST controller
- A scheduled job
- A webhook
- A Kafka consumer
- A deployment workflow
- An internal administration API
Spring Boot provides the application workflow.
JGit provides the Git operations.
When Should You Use JGit?
JGit is a good choice when Git is part of the application’s behavior.
Typical examples include:
- Repository analysis tools
- Configuration synchronization
- GitOps applications
- Deployment systems
- Release automation
- Code inspection services
- Documentation generators
- Internal developer platforms
- Automated repository maintenance
- Commit and branch reporting
For example, a configuration analyzer may follow this workflow:
A release tool may use:
JGit enables the Git-related parts of these workflows.
When Should You Not Use JGit?
JGit is not always necessary.
When a developer simply wants to run Git commands manually, the normal Git CLI is usually easier.
For example, there is little reason to replace this:
git statuswith a custom Java program unless the status is needed by the application itself.
JGit is most valuable when Git operations must be:
- Programmatic
- Integrated into Java
- Triggered automatically
- Combined with application logic
- Processed as structured Java results
Practical Considerations
JGit makes Git operations accessible through Java, but the same Git rules still apply.
Keep the working tree clean
Before checkout, pull, merge, reset, or cherry-pick:
if (!git.status().call().isClean()) {
throw new IllegalStateException(
"Repository contains local changes."
);
}Do not hardcode credentials
Use secure external configuration for tokens and passwords.
Check command results
Do not assume that a command succeeded only because no exception was immediately visible.
Inspect results such as:
pullResult.isSuccessful();or:
cherryPickResult.getStatus();Avoid concurrent modifications
Two threads should not modify the same working repository simultaneously.
For server-side applications, operations on a repository should be serialized or protected by a lock.
Close repositories
Use try-with-resources:
try (Git git = Git.open(repositoryDirectory)) {
// Git operations
}Validate repository paths
When repository paths come from API requests, never allow users to access arbitrary server directories.
Resolve repositories inside a controlled base directory and validate the final path.
JGit Versus Executing Git Commands
Both approaches can be valid.
Git through ProcessBuilder
new ProcessBuilder(
"git",
"status",
"--short"
).start();Advantages:
- Uses the installed native Git implementation
- Supports the complete Git CLI
- Familiar command syntax
Disadvantages:
- Requires Git to be installed
- Requires process management
- Output must often be parsed
- Behavior may depend on the environment
JGit
Status status =
git.status().call();Advantages:
- Java-native API
- Structured Java results
- No native Git process
- Easier integration with Java application logic
- Better suited to reusable Java services
Disadvantages:
- The API differs from the Git CLI
- Some advanced Git behavior may require deeper knowledge of JGit
- Developers must understand both Git concepts and the JGit object model
The right choice depends on the application.
For Java-native repository processing and automation, JGit is often the cleaner option.
Final Thoughts
JGit brings Git operations directly into Java applications.
It allows developers to:
- Clone repositories
- Open existing repositories
- Read commit history
- Inspect repository status
- Work with branches
- Checkout revisions
- Fetch and pull changes
- Add and commit files
- Push changes
- Cherry-pick commits
- Build custom repository workflows
The most important point is that JGit is not an automation system by itself.
It is a Java library that enables automation from the application side.
Your application provides the trigger and business logic:
For manual repository work, the Git CLI remains simple and effective.
But when Git becomes part of a Java application’s workflow, JGit provides a practical and powerful API without requiring shell commands or external process execution.









