Quick reference for integrating JGit's server-side git-receive-pack handling into jgit-proxy's servlet-based filter architecture.
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)
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
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.
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.
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 inspectrp.sendMessage(msg)— sideband-2 streaming (shows asremote: msg)commands— each witholdId,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
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
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
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 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);| 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 |
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.");
});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;
});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");
}
}
}| 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 |
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).
Option A: PostReceiveHook (store and forward)
- After validation, PostReceiveHook does
git pushto 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