Context: git-proxy is a FINOS project that acts as a policy-enforcing proxy for git pushes. The current implementation is Node.js/Express. This document compares the server-side capabilities relevant to building a store-and-forward git proxy — where pushes land in a local repository first, run through validation hooks with real-time sideband feedback, then get forwarded upstream.
These findings come from prototyping git-proxy-java, a Java/Jetty implementation that uses JGit's native git protocol stack.
A git proxy that does more than passively forward bytes needs to:
- Parse git smart HTTP protocol — packet lines, pack data, sideband channels
- Stream real-time feedback — sideband messages (
remote: validating push...) that appear in the developer's terminal during a push, not after - Intercept and transform HTTP responses — e.g. force HTTP 200 on errors so git clients read the protocol body instead of showing a generic "403 Forbidden"
- Cache and replay request bodies — the request body (pack data) needs to be read by multiple stages: parsing, unpacking into a local repo, and forwarding
- Run ordered validation pipelines — author email checks, commit message policies, GPG signature verification, external approval workflows
- Handle long-running operations — external approval gates (ServiceNow, risk ledger) that block for 10–60+ seconds while keeping the connection alive
| Component | What it is |
|---|---|
| HTTP server | Node.js built-in http / Express 4 |
| Proxy | express-http-proxy — transparent byte-forwarding |
| Git protocol | None built-in — shells out to git receive-pack / git unpack-objects as child processes |
| Request body | express.raw() buffers once; multi-consumer requires explicit PassThrough stream cloning |
| Response interception | Monkey-patch res.status() / res.writeHead() — no standard wrapper pattern |
| Filter chain | Express middleware (registration order); next() continues chain |
| Sideband streaming | Not implemented — would require hand-rolling pkt-line framing + sideband bytes |
| Long-running hooks | Event loop — synchronous blocking freezes the server; async/await required throughout |
| Credentials | Passed to git subprocesses via URL or GIT_ASKPASS — risk of exposure in process listings |
| Component | What it is |
|---|---|
| HTTP server | Jetty 12 (embedded) |
| Proxy | ProxyServlet (transparent mode) + JGit ReceivePack (store-and-forward mode) |
| Git protocol | JGit GitServlet — native ReceivePack, UploadPack, pack parsing, sideband channels |
| Request body | HttpServletRequestWrapper caches byte array; all consumers re-read via ByteArrayInputStream — transparent |
| Response interception | HttpServletResponseWrapper — standard API; intercepts setStatus() / sendError() from any downstream component |
| Filter chain | Servlet filter registration order; FilterChain.doFilter() with request/response wrapping; typed ServletContext attributes |
| Sideband streaming | ReceivePack.sendMessage() → SideBandOutputStream → Jetty chunked encoding → developer's terminal |
| Long-running hooks | Thread-per-request (each push on its own thread); blocks freely; Java 21 virtual threads for scale |
| Credentials | CredentialsProvider in-memory object; never on disk or in child process environment |
| Capability | Jetty / Servlet | Node / Express | Impact |
|---|---|---|---|
| Git protocol stack | JGit's GitServlet — native ReceivePack, UploadPack, pack parsing, sideband channels |
None built-in. git-proxy shells out to git receive-pack as a child process |
JGit gives you the full git server for free. No child processes, no manual protocol parsing |
| Sideband streaming | ReceivePack.sendMessage() writes to SideBandOutputStream. flush() propagates through chunked transfer encoding. response.setBufferSize(256) ensures immediate dispatch |
You'd need to manually construct sideband packets (band 2, pkt-line framing) on top of res.write(). The event loop model also complicates long-running synchronous hooks |
This is the critical differentiator. Real-time sideband streaming in Jetty/JGit required ~3 lines. In Express, you'd hand-roll git protocol framing on top of raw HTTP chunked responses |
| Response interception | HttpServletResponseWrapper — wrap the response to intercept setStatus(), sendError(), setHeader(). Standard, composable |
Intercepting status codes set by downstream middleware requires monkey-patching res.status() or res.writeHead(). No standard wrapper pattern |
Servlet's wrapper pattern is a first-class API. Express equivalent is fragile |
| Request body caching | HttpServletRequestWrapper with cached byte array. getInputStream() returns a ByteArrayInputStream. All downstream filters transparently re-read the same body |
express.raw() can buffer the body, but piping to multiple consumers requires PassThrough cloning or explicit buffer management |
Servlet wrappers compose cleanly. Node streams are single-consume by default |
| Filter ordering | Servlet filters have explicit registration order. FilterChain.doFilter() gives precise pre/post control. Filters can short-circuit, wrap request/response, or modify attributes |
Express middleware ordering is implicit (registration order). next() continues the chain. Lacks the request/response wrapping pattern and typed attributes |
Comparable for simple chains. Servlet filters pull ahead with response wrapping and typed attributes |
| Long-running hooks | Thread-per-request — a hook can Thread.sleep(15_000) while sending periodic sideband keepalives without blocking other requests |
Event loop — a 15-second blocking operation freezes the entire server. No built-in mechanism to stream incremental sideband packets during a long-running handler | Thread-per-request is natural for approval workflows. The event loop requires careful async choreography and manual protocol framing |
| Pack data handling | JGit PackParser via ObjectInserter — unpack push objects into a local bare repo in ~5 lines. Full commit metadata via JGit's object model |
Shell out to git receive-pack or git unpack-objects. Commit metadata requires additional git log calls or manual object parsing |
JGit's in-process pack handling is dramatically simpler than shelling out |
| Credential handling | CredentialsProvider abstraction — extract from HTTP Basic auth, pass as in-memory object to Transport.push(). Never touches disk |
Credentials passed via URL to git subprocesses or via GIT_ASKPASS. Risk of credentials in process listings or temp files |
JGit keeps credentials in-memory. Subprocess model has a larger credential exposure surface |
| Capability | Express Advantage |
|---|---|
| Startup time | Node starts in milliseconds. Jetty + JGit is ~1 second. Not meaningful for a long-running server but matters for test iteration |
| Ecosystem / middleware | npm has middleware for everything — auth, rate limiting, CORS, logging. Java has equivalents but they're heavier |
| Developer familiarity | More developers know JavaScript/TypeScript than Java servlet APIs. Lower barrier to contribution |
| Simple proxy passthrough | express-http-proxy — transparent proxying is trivial in Node. Jetty's ProxyServlet works but is less commonly used |
| UI integration | git-proxy's React UI is naturally co-hosted with the Node backend. A Java backend needs separate static file serving |
For a policy-enforcing git proxy that needs real-time sideband feedback, store-and-forward capability, and long-running approval workflows:
Java/Jetty + JGit is the stronger foundation. The git protocol is inherently streaming and stateful — JGit handles this natively while Node requires subprocess orchestration and manual protocol framing. The sideband streaming capability (which took ~3 lines in Jetty) represents significant engineering effort in Express.
The tradeoff is contributor accessibility — Java servlet APIs have a steeper learning curve than Express middleware. Mitigated with a clean hook API that hides the servlet plumbing.
This section compares the dashboard and web UI of finos/git-proxy (React + Material UI) with git-proxy-java's MVP dashboard (Alpine.js + Tailwind CSS), and documents the decision on how to evolve the git-proxy-java UI going forward.
| Aspect | Details |
|---|---|
| Framework | React 16 (two major versions behind current) |
| UI kit | Material UI v4 (@material-ui/core) — end-of-life, deprecated JSS styling (makeStyles/withStyles) |
| Template origin | "Creative Tim Material Dashboard React" — free version; v5+ is now a commercial product |
| Bundler | Vite |
| Routing | react-router-dom v6 (client-side SPA) |
| HTTP client | Mixed — axios in some services, native fetch in others |
| State management | React Context (AuthContext, UserContext) |
| Auth (backend) | Passport.js — local (bcrypt), OIDC (openid-client), Active Directory, JWT. Login page dynamically renders auth method buttons from /api/auth/config |
| Auth (frontend) | AuthProvider with useAuth() hook, CSRF token via cookie, JWT token in localStorage |
| Styling | 40+ JSS style files in assets/jss/material-dashboard-react/ tightly coupled to the template |
| Other deps | moment.js (deprecated), PerfectScrollbar, font-awesome, material-design-icons |
Pages (8 views + login): Login, Push Requests (tabbed: All/Pending/Approved/Canceled/Rejected/Error), Push Details (commits/diff/steps timeline/attestation form), Repo List, Repo Details (add user, delete repo), User Profile, User List (admin), Settings (JWT token)
Notable features that git-proxy-java lacks:
- Attestation questionnaire — configurable checkbox questions with tooltip explanations, checked before approval
- User management — CRUD for users, admin role gating
- Repo management — add/remove repos, add authorized users per repo
- Route guards — redirect to login when unauthenticated
- OIDC login flow — dynamic auth method buttons, redirect-based OAuth
| Aspect | Details |
|---|---|
| Framework | React 19 — hooks-based functional components, no class components |
| Language | TypeScript (strict mode) throughout |
| Styling | Tailwind CSS v4 (via @tailwindcss/vite plugin) — utility classes, zero custom CSS files |
| Bundler | Vite 8 — HMR in dev, optimised static build output |
| Routing | react-router-dom v7 — client-side SPA with BrowserRouter |
| HTTP client | Native fetch() throughout; typed response models in api.ts |
| Diff rendering | diff2html — side-by-side with syntax highlighting |
| Auth | Spring Security session; /api/me endpoint; login page at /login |
| Served by | Spring MVC static resource handler (Jetty); Vite build output copied to resources/static |
Pages (4 views + login): Login, Push Records (filterable by status/search, auto-refresh), Push Detail (commits, validation steps with expandable details, diff, approve/reject), Providers
What it does well:
- Clean, modern TypeScript component model — props are typed, API responses are typed via
types.ts - Tailwind v4 via Vite plugin — no separate CLI, no config file, purged production build
- Vite HMR — instant feedback during development;
vite devproxies API calls to the Spring backend - Validation step display shows pass/fail per step with expandable error details and ANSI stripping
- Auto-refresh on push list keeps the approval workflow live
- diff2html rendering is feature-complete (side-by-side, syntax highlighting, file list)
- Minimal dependency surface:
react,react-dom,react-router-dom,diff2html— nothing else at runtime
The finos/git-proxy React UI has critical structural problems that make it unsuitable as a foundation to port from:
-
Material UI v4 is dead. MUI v5+ (
@mui/material) uses Emotion instead of JSS. Upgrading from v4 to v5 is a significant migration that touches every component. The 40+ JSS style files would all need to be rewritten. -
The template is now commercial. The "Creative Tim Material Dashboard React" template (which provides the sidebar, card components, grid system, and layout) is free only for v4. The v5+ version requires a paid license. Building on top of the free v4 version means no upstream fixes or improvements.
-
React 16 is outdated. Missing concurrent features, automatic batching,
useId, server components. Upgrading to React 18/19 while also migrating MUI v4→v5 is a compounding effort. -
Heavy dependency surface. The git-proxy UI pulls in:
@material-ui/core,@material-ui/icons,@fontsource/roboto,material-design-icons,font-awesome,perfect-scrollbar,moment,history,clsx,react-html-parser,axios— all for what is fundamentally a list-of-records + detail-view + forms application. -
Tight coupling to the template. Components like
Card,CardHeader,CardBody,CardFooter,Button,CustomTabs,GridContainer,GridItemare all wrappers around MUI with template-specific styling. They don't compose well outside the template's layout assumptions.
The answer is not "no React" — it's "no porting." git-proxy-java builds a fresh React 19 + Vite app from scratch: no MUI, no Creative Tim template, no legacy baggage. The feature set from finos/git-proxy is worth replicating; the implementation is not.
Build a new React 19 + Vite SPA, starting from scratch. As the planned feature set grew — user management, attestation flows, SCM OAuth account linking, multi-step approval UI — the complexity outgrew what a single-file Alpine.js dashboard could cleanly support. React's component model and TypeScript's type safety make that surface area tractable.
The decision is not to adopt the finos/git-proxy React codebase (see above — MUI v4 EOL, commercial template). It's to write a clean React app with a minimal modern stack: React 19 + Vite + Tailwind CSS v4 + TypeScript. No UI framework dependencies at runtime beyond React itself.
git-proxy-java-dashboard/
frontend/ ← Vite project root
src/
api.ts ← typed fetch wrappers for all REST endpoints
types.ts ← shared TypeScript types (PushRecord, Provider, etc.)
pages/ ← one component per route
components/ ← shared UI components (StatusBadge, etc.)
vite.config.ts ← dev server proxies /api/* → Spring backend on :8080
src/main/resources/
static/ ← Vite build output (copied by Gradle build)
The Spring backend exposes a REST API (/api/*). The React SPA calls it via fetch(). During development vite dev proxies API requests to the running Spring server. For production, Gradle builds the frontend and copies the output to resources/static/, where Spring MVC serves it as static files.
Auth is Spring Security session-based: /login is a plain HTML form (not part of the SPA), Spring Security sets a session cookie, and /api/me returns the current user for the SPA to display.
Phase 1 — Core approval workflow (done)
- Push list with status filtering and auto-refresh
- Push detail: commits, validation steps (expandable per-step errors), diff viewer, approve/reject actions
/api/me— current-user context in the UI; reviewer identity from Spring Security session
Phase 2 — Auth + user management
- User profile page — view/edit own profile, SCM account linking
- Admin user list — CRUD, role management
- Login page — Spring Security form login; OAuth2 buttons for configured providers
- Route guards —
ProtectedRoutecomponent wrapping authenticated pages; redirect to/loginon 401
Phase 3 — Repo management + attestation
- Repo list / details — pull from config/DB, add authorized users per repo
- Attestation questionnaire on push detail — configurable checkbox questions from YAML config, required before approval
- Settings page — server config display, feature toggles
Phase 4 — SCM integration
- PR creation flow — repo picker, branch pickers (populated from SCM API), title/body form
- Multi-file diff reviewer for pending pushes
- OIDC login flow — dynamic auth method buttons from
/api/auth/config
| Concern | How React addresses it |
|---|---|
| Component reuse | TypeScript React components are the reuse unit — <StatusBadge status={...} /> used everywhere, typed props prevent misuse |
| Complex state | Approval flow state (selected commits, attestation answers, reviewer notes) is modelled with useReducer/useState — clean, testable, no DOM manipulation |
| SCM OAuth UI | /api/auth/config returns available providers; React renders the right buttons dynamically — same pattern finos/git-proxy already proven out |
| Type safety | types.ts models every API response; TypeScript catches API contract drift at build time, not runtime |
| Contributor familiarity | React + TypeScript is the most common frontend stack in open source. Lower barrier than Alpine.js + Thymeleaf for contributors from a JS background |
| Vite DX | HMR, fast cold starts, first-class TypeScript — tighter dev loop than the previous CDN-only Alpine approach |
The finos/git-proxy React UI proves out the feature set that matters. git-proxy-java is building toward the same coverage on a clean foundation:
| Feature | finos/git-proxy | git-proxy-java status |
|---|---|---|
| Push list with status tabs | Implemented (CustomTabs + PushesTable) | Done — filterable list with auto-refresh |
| Push detail with commits/diff/steps | Implemented (tabbed view) | Done — per-step expandable details, diff viewer |
| Approve / reject / cancel actions | Implemented (with auth context) | Done — wired to Spring Security session via /api/me |
| Login page | Implemented (multi-method: local + OIDC buttons) | Done — Spring Security form login; separate login.html |
| Route guards | Implemented (client-side redirect to login) | Done — 401 from /api/me triggers redirect to /login |
| Attestation questionnaire | Implemented (configurable checkbox questions from config) | Planned (Phase 3) — questions from YAML config |
| User profile | Implemented (view/edit own profile, git account linking) | Planned (Phase 2) |
| User list (admin) | Implemented (CRUD, admin-only) | Planned (Phase 2) |
| Repo list / details | Implemented (CRUD, add users, delete) | Planned (Phase 3) — basic stub exists |
| Settings page | Implemented (JWT token config) | Planned (Phase 3) |
| OIDC login flow | Implemented (dynamic auth method buttons) | Planned (Phase 4) |
| Layer | finos/git-proxy | git-proxy-java (current) | git-proxy-java (target) |
|---|---|---|---|
| Server | Node.js / Express 5 | Jetty 12 / Spring MVC | Jetty 12 / Spring MVC |
| Git protocol | Child processes (git receive-pack) |
JGit native | JGit native |
| UI framework | React 16 | React 19 | React 19 |
| Language | TypeScript | TypeScript | TypeScript |
| UI components | Material UI v4 (EOL) | Tailwind utility classes | Tailwind + custom components |
| CSS | JSS (40+ style files) | Tailwind CSS v4 (via Vite plugin) | Tailwind CSS v4 |
| Bundler | Vite | Vite 8 | Vite 8 |
| Routing | react-router-dom v6 | react-router-dom v7 | react-router-dom v7 |
| Auth (server) | Passport.js (local, OIDC, AD, JWT) | Spring Security (form login, session) | Spring Security (form, OIDC, LDAP) |
| Auth (UI) | React Context + axios interceptors | /api/me + session cookie |
/api/me + session cookie; ProtectedRoute |
| Template engine | JSX | JSX | JSX |
| Diff rendering | diff2html (via React wrapper) | diff2html (vanilla JS) | diff2html (React wrapper) |
Updated April 2026. UI comparison based on finos/git-proxy main branch and git-proxy-java main branch as of this date.