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.EnumSet;
import java.util.List;
import java.util.Objects;
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.config.ServerPersistenceContext;
import org.eclipse.jgit.server.repository.SandboxRepositoryService;
import org.eclipse.jgit.server.resolver.HibernateRepositoryResolver;
import org.eclipse.jgit.server.rest.AdminResource;
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.NativeSearchResource;
import org.eclipse.jgit.server.rest.RepositoryResource;
import org.eclipse.jgit.transport.ReceivePack;
import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
import org.hibernate.SessionFactory;

import jakarta.servlet.DispatcherType;
import jakarta.servlet.http.HttpServletRequest;

/** Main application entry point for the JGit database-backed server. */
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 ServerPersistenceContext persistenceContext;
	private HibernateRepositoryResolver repositoryResolver;

	public static void main(String[] args) throws Exception {
		JGitServerApplication app = new JGitServerApplication();
		app.start();
		app.join();
	}

	/** Starts the configured production server. */
	public void start() throws Exception {
		start(HibernateConfig.createPersistenceContext(),
				getIntEnv("JGIT_REST_PORT", DEFAULT_REST_PORT), //$NON-NLS-1$
				getIntEnv("JGIT_GIT_PORT", DEFAULT_GIT_PORT), //$NON-NLS-1$
				System.getenv("JGIT_CORS_ORIGINS"), //$NON-NLS-1$
				System.getenv("JGIT_DEFAULT_REPOS"), true); //$NON-NLS-1$
	}

	/** Starts a test server from explicit Hibernate properties and dynamic ports. */
	public void start(Properties hibernateProperties, int restPort, int gitPort) throws Exception {
		start(HibernateConfig.createPersistenceContext(hibernateProperties), restPort, gitPort, null, null, false);
	}

	private void start(ServerPersistenceContext context, int restPort, int gitPort,
			String corsOrigins, String defaultRepositories, boolean exposeVersionedApi) throws Exception {
		persistenceContext= Objects.requireNonNull(context, "context"); //$NON-NLS-1$
		try {
			SessionFactory sessionFactory= persistenceContext.sessionFactory();
			repositoryResolver= new HibernateRepositoryResolver(persistenceContext.repositoryService());
			SandboxRepositoryService repositories= repositoryResolver.getRepositoryService();
			if (defaultRepositories != null && !defaultRepositories.isBlank()) {
				RepositoryManagerConfig.initDefaultRepositories(repositories, defaultRepositories);
			}

			server= new Server();
			ServerConnector restConnector= connector("rest", restPort); //$NON-NLS-1$
			ServerConnector gitConnector= connector("git", gitPort); //$NON-NLS-1$
			server.addConnector(restConnector);
			server.addConnector(gitConnector);

			ContextHandlerCollection contexts= new ContextHandlerCollection();
			contexts.addHandler(createRestContext("/api", corsOrigins, sessionFactory, repositories)); //$NON-NLS-1$
			if (exposeVersionedApi) {
				contexts.addHandler(createRestContext("/api/v1", corsOrigins, sessionFactory, repositories)); //$NON-NLS-1$
			}
			contexts.addHandler(createGitContext());
			GzipHandler gzip= new GzipHandler();
			gzip.setHandler(contexts);
			server.setHandler(gzip);
			server.start();
		} catch (Exception failure) {
			closeAfterFailedStart(failure);
			throw failure;
		}

		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(getRestPort()));
		if (exposeVersionedApi) {
			LOG.log(Level.INFO, "  REST API v1: http://0.0.0.0:{0}/api/v1/", //$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()));
	}

	private ServerConnector connector(String name, int port) {
		ServerConnector connector= new ServerConnector(server);
		connector.setPort(port);
		connector.setName(name);
		return connector;
	}

	private ServletContextHandler createRestContext(String contextPath, String corsOrigins,
			SessionFactory sessionFactory, SandboxRepositoryService repositories) {
		ServletContextHandler context= new ServletContextHandler(ServletContextHandler.SESSIONS);
		context.setContextPath(contextPath);
		context.setVirtualHosts(List.of("@rest")); //$NON-NLS-1$
		if (corsOrigins != null && !corsOrigins.isBlank()) {
			context.addFilter(new FilterHolder(new CorsFilter(corsOrigins)), "/*", //$NON-NLS-1$
					EnumSet.allOf(DispatcherType.class));
		}

		context.addServlet(new ServletHolder("health", new HealthResource(sessionFactory)), "/health"); //$NON-NLS-1$ //$NON-NLS-2$
		context.addServlet(new ServletHolder("repos", new RepositoryResource(repositories)), "/repos/*"); //$NON-NLS-1$ //$NON-NLS-2$
		context.addServlet(new ServletHolder("search", new NativeSearchResource(sessionFactory)), "/search/*"); //$NON-NLS-1$ //$NON-NLS-2$
		context.addServlet(new ServletHolder("analytics", new AnalyticsResource(sessionFactory)), "/analytics/*"); //$NON-NLS-1$ //$NON-NLS-2$
		context.addServlet(new ServletHolder("admin", new AdminResource(sessionFactory)), "/admin/*"); //$NON-NLS-1$ //$NON-NLS-2$
		return context;
	}

	private ServletContextHandler createGitContext() {
		ServletContextHandler context= new ServletContextHandler(ServletContextHandler.SESSIONS);
		context.setContextPath("/git"); //$NON-NLS-1$
		context.setVirtualHosts(List.of("@git")); //$NON-NLS-1$
		context.addServlet(new ServletHolder(createGitServlet(repositoryResolver)), "/*"); //$NON-NLS-1$
		return context;
	}

	/** Waits for the server to stop. */
	public void join() throws InterruptedException {
		if (server != null) {
			server.join();
		}
	}

	/** Stops the server and closes the application-owned persistence context. */
	public void stop() throws Exception {
		Exception failure= null;
		try {
			if (server != null) {
				server.stop();
			}
		} catch (Exception exception) {
			failure= exception;
		}
		try {
			if (persistenceContext != null) {
				persistenceContext.close();
			}
		} catch (RuntimeException exception) {
			if (failure == null) {
				failure= exception;
			} else {
				failure.addSuppressed(exception);
			}
		} finally {
			server= null;
			repositoryResolver= null;
			persistenceContext= null;
		}
		if (failure != null) {
			throw failure;
		}
	}

	private void closeAfterFailedStart(Exception failure) {
		try {
			stop();
		} catch (Exception cleanupFailure) {
			failure.addSuppressed(cleanupFailure);
		}
	}

	/** Returns the REST port, or -1 while not started. */
	public int getRestPort() {
		return getPort("rest"); //$NON-NLS-1$
	}

	/** Returns the Git Smart HTTP port, or -1 while not started. */
	public int getGitPort() {
		return getPort("git"); //$NON-NLS-1$
	}

	private int getPort(String connectorName) {
		if (server != null) {
			for (var connector : server.getConnectors()) {
				if (connector instanceof ServerConnector serverConnector
						&& connectorName.equals(serverConnector.getName())) {
					return serverConnector.getLocalPort();
				}
			}
		}
		return -1;
	}

	/** Returns the repository resolver used by the running application. */
	public HibernateRepositoryResolver getRepositoryResolver() {
		return repositoryResolver;
	}

	/** Returns the application-owned native Hibernate factory. */
	public SessionFactory getSessionFactory() {
		if (persistenceContext == null) {
			throw new IllegalStateException("Server persistence context is not started"); //$NON-NLS-1$
		}
		return persistenceContext.sessionFactory();
	}

	private static int getIntEnv(String name, int defaultValue) {
		String value= System.getenv(name);
		if (value != null && !value.isEmpty()) {
			try {
				return Integer.parseInt(value);
			} catch (NumberFormatException exception) {
				LOG.log(Level.WARNING, "Invalid integer for {0}: {1}, using default {2}", //$NON-NLS-1$
						new Object[] { name, value, Integer.toString(defaultValue) });
			}
		}
		return defaultValue;
	}

	private static ReceivePackFactory<HttpServletRequest> createReceivePackFactory() {
		return (request, repository) -> {
			ReceivePack receivePack= new ReceivePack(repository);
			long maxCommandBytes= 50L << 20;
			String configured= System.getenv("JGIT_RECEIVE_MAX_COMMAND_BYTES"); //$NON-NLS-1$
			if (configured != null && !configured.isEmpty()) {
				try {
					maxCommandBytes= Long.parseLong(configured);
				} catch (NumberFormatException exception) {
					LOG.log(Level.WARNING,
							"Invalid JGIT_RECEIVE_MAX_COMMAND_BYTES value: {0}, using default 50 MiB", //$NON-NLS-1$
							configured);
				}
			}
			receivePack.setMaxCommandBytes(maxCommandBytes);
			return receivePack;
		};
	}

	private static GitServlet createGitServlet(HibernateRepositoryResolver resolver) {
		GitServlet gitServlet= new GitServlet();
		gitServlet.setRepositoryResolver(resolver);
		gitServlet.setReceivePackFactory(createReceivePackFactory());
		gitServlet.setReceivePackErrorHandler((request, response, receivePack) -> {
			try {
				receivePack.receive();
			} catch (Exception exception) {
				LOG.log(Level.SEVERE, "ReceivePack error for " + request.getRequestURI(), exception); //$NON-NLS-1$
				throw exception;
			}
		});
		return gitServlet;
	}
}