JGitServerApplication.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Carsten Hammer
*******************************************************************************/
package org.eclipse.jgit.server;
import java.util.List;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.eclipse.jetty.ee10.servlet.FilterHolder;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.ServletHolder;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jgit.http.server.GitServlet;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.server.config.HibernateConfig;
import org.eclipse.jgit.server.config.RepositoryManagerConfig;
import org.eclipse.jgit.server.resolver.HibernateRepositoryResolver;
import org.eclipse.jgit.server.rest.AnalyticsResource;
import org.eclipse.jgit.server.rest.CorsFilter;
import org.eclipse.jgit.server.rest.HealthResource;
import org.eclipse.jgit.server.rest.RepositoryResource;
import org.eclipse.jgit.server.rest.SearchResource;
import org.eclipse.jgit.storage.hibernate.config.HibernateSessionFactoryProvider;
import org.eclipse.jgit.transport.ReceivePack;
import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
import jakarta.servlet.http.HttpServletRequest;
/**
* Main application entry point for the JGit database-backed server.
* <p>
* Starts an embedded Jetty server with:
* <ul>
* <li>Git Smart HTTP protocol on port 8443 (configurable)</li>
* <li>REST API on port 8080 (configurable)</li>
* </ul>
* Configuration is read from environment variables.
*/
public class JGitServerApplication {
private static final Logger LOG = Logger
.getLogger(JGitServerApplication.class.getName());
private static final int DEFAULT_REST_PORT = 8080;
private static final int DEFAULT_GIT_PORT = 8443;
private Server server;
private HibernateSessionFactoryProvider sessionFactoryProvider;
private HibernateRepositoryResolver repositoryResolver;
/**
* Main entry point.
*
* @param args
* command line arguments (unused)
* @throws Exception
* if server startup fails
*/
public static void main(String[] args) throws Exception {
JGitServerApplication app = new JGitServerApplication();
app.start();
app.join();
}
/**
* Start the server.
*
* @throws Exception
* if server startup fails
*/
public void start() throws Exception {
sessionFactoryProvider = HibernateConfig
.createSessionFactoryProvider();
repositoryResolver = new HibernateRepositoryResolver(
sessionFactoryProvider);
// Auto-create default repositories
String defaultRepos = System.getenv("JGIT_DEFAULT_REPOS"); //$NON-NLS-1$
if (defaultRepos != null && !defaultRepos.isEmpty()) {
RepositoryManagerConfig.initDefaultRepositories(
sessionFactoryProvider, defaultRepos);
}
int restPort = getIntEnv("JGIT_REST_PORT", DEFAULT_REST_PORT); //$NON-NLS-1$
int gitPort = getIntEnv("JGIT_GIT_PORT", DEFAULT_GIT_PORT); //$NON-NLS-1$
server = new Server();
// REST API connector
ServerConnector restConnector = new ServerConnector(server);
restConnector.setPort(restPort);
restConnector.setName("rest"); //$NON-NLS-1$
// Git HTTP connector
ServerConnector gitConnector = new ServerConnector(server);
gitConnector.setPort(gitPort);
gitConnector.setName("git"); //$NON-NLS-1$
server.addConnector(restConnector);
server.addConnector(gitConnector);
ContextHandlerCollection contexts = new ContextHandlerCollection();
// REST API context
ServletContextHandler restContext = new ServletContextHandler(
ServletContextHandler.SESSIONS);
restContext.setContextPath("/api"); //$NON-NLS-1$
restContext.setVirtualHosts(List.of("@rest")); //$NON-NLS-1$
// CORS filter
String corsOrigins = System.getenv("JGIT_CORS_ORIGINS"); //$NON-NLS-1$
if (corsOrigins != null && !corsOrigins.isEmpty()) {
restContext.addFilter(
new FilterHolder(new CorsFilter(corsOrigins)),
"/*", //$NON-NLS-1$
java.util.EnumSet
.allOf(jakarta.servlet.DispatcherType.class));
}
restContext.addServlet(new ServletHolder("health", //$NON-NLS-1$
new HealthResource(sessionFactoryProvider)), "/health"); //$NON-NLS-1$
restContext.addServlet(
new ServletHolder("repos", //$NON-NLS-1$
new RepositoryResource(sessionFactoryProvider,
repositoryResolver)),
"/repos/*"); //$NON-NLS-1$
restContext.addServlet(
new ServletHolder("search", //$NON-NLS-1$
new SearchResource(sessionFactoryProvider)),
"/search/*"); //$NON-NLS-1$
restContext.addServlet(
new ServletHolder("analytics", //$NON-NLS-1$
new AnalyticsResource(sessionFactoryProvider)),
"/analytics/*"); //$NON-NLS-1$
// Git Smart HTTP context
ServletContextHandler gitContext = new ServletContextHandler(
ServletContextHandler.SESSIONS);
gitContext.setContextPath("/git"); //$NON-NLS-1$
gitContext.setVirtualHosts(List.of("@git")); //$NON-NLS-1$
gitContext.addServlet(
new ServletHolder(createGitServlet(repositoryResolver)),
"/*"); //$NON-NLS-1$
contexts.addHandler(restContext);
// Also serve /api/v1/* for forward-compatible API versioning
ServletContextHandler v1Context = new ServletContextHandler(
ServletContextHandler.SESSIONS);
v1Context.setContextPath("/api/v1"); //$NON-NLS-1$
v1Context.setVirtualHosts(List.of("@rest")); //$NON-NLS-1$
if (corsOrigins != null && !corsOrigins.isEmpty()) {
v1Context.addFilter(
new FilterHolder(new CorsFilter(corsOrigins)),
"/*", //$NON-NLS-1$
java.util.EnumSet
.allOf(jakarta.servlet.DispatcherType.class));
}
v1Context.addServlet(new ServletHolder("health", //$NON-NLS-1$
new HealthResource(sessionFactoryProvider)), "/health"); //$NON-NLS-1$
v1Context.addServlet(
new ServletHolder("repos", //$NON-NLS-1$
new RepositoryResource(sessionFactoryProvider,
repositoryResolver)),
"/repos/*"); //$NON-NLS-1$
v1Context.addServlet(
new ServletHolder("search", //$NON-NLS-1$
new SearchResource(sessionFactoryProvider)),
"/search/*"); //$NON-NLS-1$
v1Context.addServlet(
new ServletHolder("analytics", //$NON-NLS-1$
new AnalyticsResource(sessionFactoryProvider)),
"/analytics/*"); //$NON-NLS-1$
contexts.addHandler(v1Context);
contexts.addHandler(gitContext);
GzipHandler gzip = new GzipHandler();
gzip.setHandler(contexts);
server.setHandler(gzip);
server.start();
LOG.log(Level.INFO, "JGit Server started"); //$NON-NLS-1$
LOG.log(Level.INFO, " REST API: http://0.0.0.0:{0}/api/", //$NON-NLS-1$
Integer.toString(restPort));
LOG.log(Level.INFO, " REST API v1: http://0.0.0.0:{0}/api/v1/", //$NON-NLS-1$
Integer.toString(restPort));
LOG.log(Level.INFO,
" Git HTTP: http://0.0.0.0:{0}/git/", //$NON-NLS-1$
Integer.toString(gitPort));
}
/**
* Start the server with explicit Hibernate properties and ports.
* <p>
* Useful for integration testing with Testcontainers where the database
* connection URL and ports are determined at runtime.
*
* @param hibernateProperties
* Hibernate configuration properties
* @param restPort
* REST API port (0 for dynamic allocation)
* @param gitPort
* Git HTTP port (0 for dynamic allocation)
* @throws Exception
* if server startup fails
*/
public void start(Properties hibernateProperties, int restPort,
int gitPort) throws Exception {
sessionFactoryProvider = HibernateConfig
.createSessionFactoryProvider(hibernateProperties);
repositoryResolver = new HibernateRepositoryResolver(
sessionFactoryProvider);
server = new Server();
// REST API connector
ServerConnector restConnector = new ServerConnector(server);
restConnector.setPort(restPort);
restConnector.setName("rest"); //$NON-NLS-1$
// Git HTTP connector
ServerConnector gitConnector = new ServerConnector(server);
gitConnector.setPort(gitPort);
gitConnector.setName("git"); //$NON-NLS-1$
server.addConnector(restConnector);
server.addConnector(gitConnector);
ContextHandlerCollection contexts = new ContextHandlerCollection();
// REST API context
ServletContextHandler restContext = new ServletContextHandler(
ServletContextHandler.SESSIONS);
restContext.setContextPath("/api"); //$NON-NLS-1$
restContext.setVirtualHosts(List.of("@rest")); //$NON-NLS-1$
restContext.addServlet(new ServletHolder("health", //$NON-NLS-1$
new HealthResource(sessionFactoryProvider)), "/health"); //$NON-NLS-1$
restContext.addServlet(
new ServletHolder("repos", //$NON-NLS-1$
new RepositoryResource(sessionFactoryProvider,
repositoryResolver)),
"/repos/*"); //$NON-NLS-1$
restContext.addServlet(
new ServletHolder("search", //$NON-NLS-1$
new SearchResource(sessionFactoryProvider)),
"/search/*"); //$NON-NLS-1$
restContext.addServlet(
new ServletHolder("analytics", //$NON-NLS-1$
new AnalyticsResource(sessionFactoryProvider)),
"/analytics/*"); //$NON-NLS-1$
// Git Smart HTTP context
ServletContextHandler gitContext = new ServletContextHandler(
ServletContextHandler.SESSIONS);
gitContext.setContextPath("/git"); //$NON-NLS-1$
gitContext.setVirtualHosts(List.of("@git")); //$NON-NLS-1$
gitContext.addServlet(
new ServletHolder(createGitServlet(repositoryResolver)),
"/*"); //$NON-NLS-1$
contexts.addHandler(restContext);
contexts.addHandler(gitContext);
GzipHandler gzip = new GzipHandler();
gzip.setHandler(contexts);
server.setHandler(gzip);
server.start();
LOG.log(Level.INFO, "JGit Server started (test mode)"); //$NON-NLS-1$
LOG.log(Level.INFO, " REST API: http://0.0.0.0:{0}/api/", //$NON-NLS-1$
Integer.toString(getRestPort()));
LOG.log(Level.INFO,
" Git HTTP: http://0.0.0.0:{0}/git/", //$NON-NLS-1$
Integer.toString(getGitPort()));
}
/**
* Wait for the server to stop.
*
* @throws InterruptedException
* if interrupted while waiting
*/
public void join() throws InterruptedException {
if (server != null) {
server.join();
}
}
/**
* Stop the server and release resources.
*
* @throws Exception
* if an error occurs during shutdown
*/
public void stop() throws Exception {
if (server != null) {
server.stop();
}
if (repositoryResolver != null) {
repositoryResolver.close();
}
if (sessionFactoryProvider != null) {
sessionFactoryProvider.close();
}
}
/**
* Get the REST API port the server is listening on.
*
* @return the REST port, or -1 if not started
*/
public int getRestPort() {
if (server != null) {
for (var connector : server.getConnectors()) {
if (connector instanceof ServerConnector sc
&& "rest".equals(sc.getName())) { //$NON-NLS-1$
return sc.getLocalPort();
}
}
}
return -1;
}
/**
* Get the Git HTTP port the server is listening on.
*
* @return the Git port, or -1 if not started
*/
public int getGitPort() {
if (server != null) {
for (var connector : server.getConnectors()) {
if (connector instanceof ServerConnector sc
&& "git".equals(sc.getName())) { //$NON-NLS-1$
return sc.getLocalPort();
}
}
}
return -1;
}
/**
* Get the repository resolver.
*
* @return the resolver
*/
public HibernateRepositoryResolver getRepositoryResolver() {
return repositoryResolver;
}
private static int getIntEnv(String name, int defaultValue) {
String val = System.getenv(name);
if (val != null && !val.isEmpty()) {
try {
return Integer.parseInt(val);
} catch (NumberFormatException e) {
LOG.log(Level.WARNING,
"Invalid integer for {0}: {1}, using default {2}", //$NON-NLS-1$
new Object[] { name, val,
Integer.toString(defaultValue) });
}
}
return defaultValue;
}
/**
* Create a ReceivePackFactory that allows anonymous push operations.
*
* @return a factory that creates ReceivePack instances without requiring
* authentication
*/
private static ReceivePackFactory<HttpServletRequest> createReceivePackFactory() {
return (HttpServletRequest req, Repository db) -> {
ReceivePack rp = new ReceivePack(db);
// Default 3 MiB limit is too low for repositories with many refs.
// Allow override via env var; default to 50 MiB.
long maxCmdBytes = 50L << 20;
String envVal = System.getenv("JGIT_RECEIVE_MAX_COMMAND_BYTES"); //$NON-NLS-1$
if (envVal != null && !envVal.isEmpty()) {
try {
maxCmdBytes = Long.parseLong(envVal);
} catch (NumberFormatException e) {
LOG.log(Level.WARNING,
"Invalid JGIT_RECEIVE_MAX_COMMAND_BYTES value: {0}, using default 50 MiB", //$NON-NLS-1$
envVal);
}
}
rp.setMaxCommandBytes(maxCmdBytes);
return rp;
};
}
private static GitServlet createGitServlet(
HibernateRepositoryResolver resolver) {
GitServlet gitServlet = new GitServlet();
gitServlet.setRepositoryResolver(resolver);
gitServlet.setReceivePackFactory(createReceivePackFactory());
gitServlet.setReceivePackErrorHandler(
(req, rsp, r) -> {
try {
r.receive();
} catch (Exception e) {
LOG.log(Level.SEVERE,
"ReceivePack error for " //$NON-NLS-1$
+ req.getRequestURI(),
e);
throw e;
}
});
return gitServlet;
}
}