Skip to content

Instantly share code, notes, and snippets.

@coopernetes
Created March 26, 2026 08:00
Show Gist options
  • Select an option

  • Save coopernetes/96ce03ca5795ca9dc78367f064c20596 to your computer and use it in GitHub Desktop.

Select an option

Save coopernetes/96ce03ca5795ca9dc78367f064c20596 to your computer and use it in GitHub Desktop.

JGit Server-Side Abstractions: Reference Guide

Quick reference for integrating JGit's server-side git-receive-pack handling into jgit-proxy's servlet-based filter architecture.

Core Composition Model

GitServlet (HTTP layer)
  ├── RepositoryResolver<HttpServletRequest>  ← "which repo?"
  ├── ReceivePackFactory<HttpServletRequest>   ← "how to handle this push?"
  │     └── creates ReceivePack, wired with:
  │           ├── PreReceiveHook    ← compliance/validation logic
  │           ├── PostReceiveHook   ← notifications, audit, upstream push
  │           └── configuration (limits, allowed ops, etc.)
  └── UploadPackFactory (same pattern for fetch)

Request Lifecycle

HTTP POST /repo.git/git-receive-pack
    ↓
GitServlet → ReceivePackServlet.doPost()
    ↓
1. RepositoryResolver.open(req, name) → Repository
2. ReceivePackFactory.create(req, repo) → ReceivePack (configured)
3. rp.receive(inputStream, outputStream, messageStream)
    ↓
ReceivePack.service() protocol sequence:
    a) advertiseRefs()        — send refs + capabilities
    b) recvCommands()         — parse ref update commands
       └── enableCapabilities() — wraps streams with SideBandOutputStream if client supports it
    c) receivePack()          — unpack objects into Repository's ObjectDatabase
    d) PreReceiveHook         — YOUR VALIDATION LOGIC HERE
    e) executeCommands()      — apply ref updates (BatchRefUpdate)
    f) PostReceiveHook        — post-update actions
    g) sendStatusReport()     — per-command ok/rejected results
    h) cleanup                — flush, release locks, auto-GC

Key Interfaces

RepositoryResolver

public interface RepositoryResolver<C> {
    Repository open(C req, String name)
        throws RepositoryNotFoundException,      // 404
               ServiceNotAuthorizedException,    // 401
               ServiceNotEnabledException,       // 403
               ServiceMayNotContinueException;   // detailed error
}

jgit-proxy already has TemporaryRepositoryResolver implementing this, backed by LocalRepositoryCache.

ReceivePackFactory

public interface ReceivePackFactory<C> {
    ReceivePack create(C req, Repository db)
        throws ServiceNotEnabledException,
               ServiceNotAuthorizedException;
}

Called per-request. Use req to extract user identity, apply per-user limits, select hooks.

PreReceiveHook

public interface PreReceiveHook {
    PreReceiveHook NULL = (rp, commands) -> { };

    void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands);
}
  • When: After objects unpacked, before ref updates applied
  • Has access to:
    • rp.getRepository() — full object database, ready to inspect
    • rp.sendMessage(msg) — sideband-2 streaming (shows as remote: msg)
    • commands — each with oldId, newId, refName, type
    • Full JGit API: RevWalk, TreeWalk, DiffFormatter, ObjectReader
  • To block: command.setResult(Result.REJECTED_OTHER_REASON, "reason")
  • In atomic mode, rejecting any command fails all pending commands

PostReceiveHook

public interface PostReceiveHook {
    PostReceiveHook NULL = (rp, commands) -> { };

    void onPostReceive(ReceivePack rp, Collection<ReceiveCommand> commands);
}
  • When: After ref updates successfully applied
  • Only receives successful commands
  • Use for: upstream push, CI triggers, notifications, audit

ReceiveCommand

public class ReceiveCommand {
    ObjectId getOldId();        // before (null for creates)
    ObjectId getNewId();        // after (null for deletes)
    String getRefName();        // e.g. "refs/heads/main"
    Type getType();             // CREATE, UPDATE, UPDATE_NONFASTFORWARD, DELETE

    void setResult(Result status, String message);  // to reject
    Result getResult();
    String getMessage();
}

Result enum: OK, REJECTED_NOCREATE, REJECTED_NODELETE, REJECTED_NONFASTFORWARD, REJECTED_OTHER_REASON, LOCK_FAILURE, NOT_ATTEMPTED

Sideband Output

JGit handles sideband automatically when client sends side-band-64k capability.

Without sideband: direct output With sideband (automatic):

rawOut = SideBandOutputStream(CH_DATA, MAX_BUF, originalOut)      // 0x01
msgOut = SideBandOutputStream(CH_PROGRESS, MAX_BUF, originalOut)  // 0x02
errOut = SideBandOutputStream(CH_ERROR, MAX_BUF, originalOut)     // 0x03

Usage in hooks:

rp.sendMessage("Checking author emails... OK");
// → client sees: "remote: Checking author emails... OK"

SideBandOutputStream constructor: new SideBandOutputStream(int channel, int packetSize, OutputStream underlying)

  • Max buffer: 65520 bytes per packet
  • Auto-flushes on packet boundary

ReceivePack Configuration

ReceivePack rp = new ReceivePack(db);

// Ref control
rp.setAllowCreates(true);
rp.setAllowDeletes(true);
rp.setAllowBranchDeletes(true);
rp.setAllowNonFastForwards(true);
rp.setAtomic(true);               // all-or-nothing

// Validation
rp.setCheckReceivedObjects(true);
rp.setCheckReferencedObjectsAreReachable(true);
rp.setObjectChecker(new ObjectChecker());

// Limits
rp.setMaxObjectSizeLimit(500L * 1024 * 1024);
rp.setMaxPackSizeLimit(5L * 1024 * 1024 * 1024);
rp.setMaxCommandBytes(maxBytes);
rp.setTimeout(seconds);

// Features
rp.setAllowPushOptions(true);
rp.setAllowQuiet(true);
rp.setBiDirectionalPipe(false);    // false for HTTP

// Hooks
rp.setPreReceiveHook(hook);
rp.setPostReceiveHook(hook);
rp.setAdvertiseRefsHook(hook);
rp.setRefFilter(filter);

JGit Object Model Quick Reference

Git concept JGit class What it is
Commit RevCommit Tree + parent(s) + author + message
Tree RevTree Directory listing (names → blobs/subtrees)
Blob RevBlob File contents
Any object RevObject Base class, identified by SHA
Walking commits RevWalk Commit history iterator (filterable)
Walking files TreeWalk File iterator within a tree (ls-tree)
Reading raw objects ObjectReader Low-level byte access
Comparing trees DiffFormatter Diff between two trees
Person info PersonIdent Name + email + timestamp
Packet I/O PacketLineIn / PacketLineOut Git protocol packet-line format

Example: Scanning changed files in PreReceiveHook

rp.setPreReceiveHook((receivePack, commands) -> {
    Repository repo = receivePack.getRepository();

    for (ReceiveCommand cmd : commands) {
        if (cmd.getType() == ReceiveCommand.Type.DELETE) continue;

        try (RevWalk walk = new RevWalk(repo)) {
            RevCommit newCommit = walk.parseCommit(cmd.getNewId());

            // For updates, diff against old; for creates, diff against empty
            if (cmd.getType() == ReceiveCommand.Type.CREATE) {
                receivePack.sendMessage("New branch: " + cmd.getRefName());
            }

            // Walk all commits in the push range
            if (cmd.getOldId() != null && !cmd.getOldId().equals(ObjectId.zeroId())) {
                RevCommit oldCommit = walk.parseCommit(cmd.getOldId());
                List<DiffEntry> diffs = CommitInspectionService.getDiff(
                    repo, oldCommit.getName(), newCommit.getName());

                receivePack.sendMessage("Changed files: " + diffs.size());

                for (DiffEntry diff : diffs) {
                    // Inspect each changed file
                    receivePack.sendMessage("  " + diff.getChangeType() + " " + diff.getNewPath());
                }
            }

            // Check commit metadata
            for (RevCommit c : Git.wrap(repo).log()
                    .addRange(cmd.getOldId(), cmd.getNewId()).call()) {
                PersonIdent author = c.getAuthorIdent();
                receivePack.sendMessage("Commit " + c.abbreviate(7).name()
                    + " by " + author.getEmailAddress());

                if (!isValidEmail(author.getEmailAddress())) {
                    cmd.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON,
                        "Invalid author email: " + author.getEmailAddress());
                    return;
                }
            }
        }
    }
    receivePack.sendMessage("All checks passed.");
});

Example: Wiring into GitServlet

GitServlet gs = new GitServlet();
gs.setRepositoryResolver(new TemporaryRepositoryResolver(cache));
gs.setReceivePackFactory((req, db) -> {
    ReceivePack rp = new ReceivePack(db);
    rp.setBiDirectionalPipe(false);  // HTTP mode
    rp.setAllowPushOptions(true);
    rp.setPreReceiveHook(new CompositePreReceiveHook(filters));
    rp.setPostReceiveHook(new UpstreamPushHook(upstreamUrl));
    return rp;
});

Example: Composite hook from filter chain

public class CompositePreReceiveHook implements PreReceiveHook {
    private final List<GitProxyFilter> filters;

    @Override
    public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
        for (GitProxyFilter filter : filters) {
            rp.sendMessage("[git-proxy] " + filter.getClass().getSimpleName() + "...");
            // adapt filter to work with ReceivePack + commands
            // on failure: cmd.setResult(REJECTED_OTHER_REASON, reason)
            rp.sendMessage("[git-proxy] " + filter.getClass().getSimpleName() + "... OK");
        }
    }
}

What jgit-proxy currently does by hand that JGit handles natively

Current manual implementation JGit native equivalent
GitReceivePackParser (~240 lines of inflate/parse) ReceivePack unpacks into ObjectDatabase automatically
handleMessage / GitSmartHttpTools.sendError (single error) rp.sendMessage() for streaming, sideband handled automatically
ParseGitRequestFilter reading PacketLineIn ReceivePack.recvCommands() parses all commands
EnrichPushCommitsFilter clone + RevWalk Still useful — but objects already in local repo after unpack

JGit modules in jgit-proxy (build.gradle)

jgitVersion = '7.0.0.202409031743-r'  // check if newer available

dependencies {
    api "org.eclipse.jgit:org.eclipse.jgit:${jgitVersion}"
    implementation "org.eclipse.jgit:org.eclipse.jgit.http.apache:${jgitVersion}"
    implementation "org.eclipse.jgit:org.eclipse.jgit.http.server:${jgitVersion}"
}

All three modules needed: core (object model, transport), http.server (GitServlet, ReceivePackServlet), http.apache (HTTP client for upstream pushes).

Design decision: who pushes to upstream?

Option A: PostReceiveHook (store and forward)

  • After validation, PostReceiveHook does git push to GitHub via JGit transport
  • Local repo = source of truth, GitHub = downstream mirror
  • Cleanest model, enables full dispatcher/async pattern
  • Unlocks approval workflows that take hours/days

Option B: Transparent proxy for upstream leg

  • Validation via hooks, then replay original HTTP request to upstream
  • Lower risk, preserves existing behavior
  • More complex coordination
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment