Skip to content

Commit f0f2c6a

Browse files
coopernetesclaude
andcommitted
chore: add CLAUDE.md and apply spotless formatting
- CLAUDE.md: project context, architecture, build/test commands, Docker Compose usage, and configuration notes for AI-assisted development - spotlessApply: reformat e2e test classes to palantir-java-format style Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d92c9de commit f0f2c6a

6 files changed

Lines changed: 139 additions & 116 deletions

File tree

CLAUDE.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# jgit-proxy — Claude context
2+
3+
Java rewrite of [FINOS git-proxy](https://github.com/finos/git-proxy) (Node.js). Proxies git push/fetch operations and enforces security/compliance checks.
4+
5+
## Repository layout
6+
7+
| Module | Purpose |
8+
|--------|---------|
9+
| `jgit-proxy-core` | Shared library: filter chain, JGit hooks, push store, provider model |
10+
| `jgit-proxy-server` | Standalone Jetty server (`GitProxyJettyApplication`) |
11+
12+
## Architecture
13+
14+
Two proxy modes, both configurable per-provider:
15+
16+
- **Store-and-forward** (`/push/<provider>/<owner>/<repo>.git`) — JGit ReceivePack receives the push locally, runs a pre-receive hook chain (`AuthorEmailValidationHook``CommitMessageValidationHook``ValidationVerifierHook`), then `ForwardingPostReceiveHook` pushes upstream using the client's credentials.
17+
- **Transparent proxy** (`/proxy/<provider>/<owner>/<repo>.git`) — Jetty's `ProxyServlet` forwards the request; a servlet filter chain (`ParseGitRequestFilter``EnrichPushCommitsFilter` → validation filters) inspects the pack data before it reaches the upstream.
18+
19+
## Reference implementation
20+
21+
The Node.js original lives at `/home/tom/repos/git-proxy`. Refer to it for the Action/Step model, Sink interface, and filter chain patterns when porting features.
22+
23+
## Build & test
24+
25+
```bash
26+
./gradlew build # compile + unit tests (no containers)
27+
./gradlew test # unit tests only (e2e excluded)
28+
./gradlew e2eTest # e2e tests — requires Docker/Podman
29+
./gradlew spotlessApply # fix formatting (palantir-java-format)
30+
```
31+
32+
Unit tests live under each module's `src/test/`. E2e tests are in `jgit-proxy-server/src/test/java/org/finos/gitproxy/e2e/` and tagged `@Tag("e2e")`.
33+
34+
## Integration testing (manual)
35+
36+
Use the shell scripts in the repo root against a running server (`./gradlew :jgit-proxy-server:run`):
37+
38+
```bash
39+
test-push-pass.sh # store-and-forward — valid commits
40+
test-push-fail.sh # store-and-forward — commits that should be rejected
41+
test-proxy-pass.sh # proxy mode — valid commits
42+
test-proxy-fail.sh # proxy mode — commits that should be rejected
43+
```
44+
45+
## Docker Compose
46+
47+
```bash
48+
docker compose up -d # jgit-proxy + Gitea (h2-mem database)
49+
bash docker/setup.sh # one-time: create admin user + test repo in Gitea
50+
51+
# Optional database backends:
52+
docker compose --profile postgres up -d # swap git-proxy-local.yml for git-proxy-postgres.yml
53+
docker compose --profile mongo up -d # swap git-proxy-local.yml for git-proxy-mongo.yml
54+
```
55+
56+
Config override file mounted at `/app/conf/git-proxy-local.yml` inside the container. Templates in `docker/`.
57+
58+
## Configuration
59+
60+
`JettyConfigurationLoader` merges (lowest → highest priority):
61+
1. `git-proxy.yml` (classpath, shipped with jar) — defaults + GitHub/GitLab/Bitbucket providers
62+
2. `git-proxy-local.yml` (classpath, optional) — local overrides; put in `/app/conf/` for Docker
63+
3. `GITPROXY_*` environment variables
64+
65+
Supported env vars: `GITPROXY_SERVER_PORT`, `GITPROXY_GITPROXY_BASEPATH`, `GITPROXY_PROVIDERS_<NAME>_ENABLED`.
66+
67+
## Database backends
68+
69+
Configured via `database.type` in YAML. Supported: `memory`, `h2-mem`, `h2-file`, `sqlite`, `postgres`, `mongo`.
70+
71+
## Podman notes
72+
73+
Always use fully qualified image names (e.g. `docker.io/eclipse-temurin:21-jre`). Podman on Fedora enforces short-name resolution and will error without a TTY if bare names are used.
74+
75+
## Branch / PR target
76+
77+
Default branch for PRs: **`jetty`**

jgit-proxy-server/src/test/java/org/finos/gitproxy/e2e/GitHelper.java

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@
77
import java.util.List;
88

99
/**
10-
* Thin wrapper around the system {@code git} CLI for use in e2e tests. Mirrors the pattern in the
11-
* shell test scripts: each helper method maps directly to a sequence of git commands that those
12-
* scripts execute.
10+
* Thin wrapper around the system {@code git} CLI for use in e2e tests. Mirrors the pattern in the shell test scripts:
11+
* each helper method maps directly to a sequence of git commands that those scripts execute.
1312
*/
1413
class GitHelper {
1514

@@ -30,12 +29,10 @@ Path clone(String remoteUrl, String dirName) throws IOException, InterruptedExce
3029
}
3130

3231
/**
33-
* Sets the local git author identity for subsequent commits made in {@code repoDir}. Git
34-
* includes credentials in the remote URL, so the author is the only identity that matters for
35-
* validation purposes.
32+
* Sets the local git author identity for subsequent commits made in {@code repoDir}. Git includes credentials in
33+
* the remote URL, so the author is the only identity that matters for validation purposes.
3634
*/
37-
void setAuthor(Path repoDir, String name, String email)
38-
throws IOException, InterruptedException {
35+
void setAuthor(Path repoDir, String name, String email) throws IOException, InterruptedException {
3936
this.authorName = name;
4037
this.authorEmail = email;
4138
git(repoDir, "config", "user.name", name);
@@ -45,8 +42,7 @@ void setAuthor(Path repoDir, String name, String email)
4542
}
4643

4744
/** Writes {@code content} to {@code fileName} in {@code repoDir} and stages it. */
48-
void writeAndStage(Path repoDir, String fileName, String content)
49-
throws IOException, InterruptedException {
45+
void writeAndStage(Path repoDir, String fileName, String content) throws IOException, InterruptedException {
5046
Files.writeString(repoDir.resolve(fileName), content);
5147
git(repoDir, "add", fileName);
5248
}
@@ -76,10 +72,7 @@ boolean tryPush(Path repoDir) throws IOException, InterruptedException {
7672
return exitCode == 0;
7773
}
7874

79-
/**
80-
* Returns the captured combined stdout+stderr from a push that is expected to fail, for
81-
* assertion purposes.
82-
*/
75+
/** Returns the captured combined stdout+stderr from a push that is expected to fail, for assertion purposes. */
8376
String pushOutput(Path repoDir) throws IOException, InterruptedException {
8477
String branch = currentBranch(repoDir);
8578
ProcessBuilder pb = buildGitCommand(repoDir, "push", "origin", branch);

jgit-proxy-server/src/test/java/org/finos/gitproxy/e2e/GiteaContainer.java

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@
1212
/**
1313
* Testcontainers wrapper for a Gitea instance used as the upstream git server in e2e tests.
1414
*
15-
* <p>Starts Gitea with the install lock set (skips the setup wizard), creates an admin user via
16-
* the Gitea CLI, and initialises a public test repository ({@value #TEST_ORG}/{@value #TEST_REPO})
17-
* via the Gitea REST API.
15+
* <p>Starts Gitea with the install lock set (skips the setup wizard), creates an admin user via the Gitea CLI, and
16+
* initialises a public test repository ({@value #TEST_ORG}/{@value #TEST_REPO}) via the Gitea REST API.
1817
*/
1918
@SuppressWarnings("resource")
2019
class GiteaContainer extends GenericContainer<GiteaContainer> {
@@ -54,11 +53,11 @@ URI getBaseUri() {
5453
}
5554

5655
/**
57-
* Creates the admin user by running the Gitea CLI inside the container. Must be called after
58-
* the container has started.
56+
* Creates the admin user by running the Gitea CLI inside the container. Must be called after the container has
57+
* started.
5958
*
60-
* <p>Uses {@code su-exec} (Alpine's privilege-dropper, always present in the Gitea image) to
61-
* run as the {@code git} user — the Gitea binary refuses to start as root.
59+
* <p>Uses {@code su-exec} (Alpine's privilege-dropper, always present in the Gitea image) to run as the {@code git}
60+
* user — the Gitea binary refuses to start as root.
6261
*/
6362
void createAdminUser() throws IOException, InterruptedException {
6463
var result = execInContainer(
@@ -69,44 +68,39 @@ void createAdminUser() throws IOException, InterruptedException {
6968
"user",
7069
"create",
7170
"--admin",
72-
"--username", ADMIN_USER,
73-
"--password", ADMIN_PASSWORD,
74-
"--email", ADMIN_EMAIL,
71+
"--username",
72+
ADMIN_USER,
73+
"--password",
74+
ADMIN_PASSWORD,
75+
"--email",
76+
ADMIN_EMAIL,
7577
"--must-change-password=false");
7678
if (result.getExitCode() != 0 && !result.getStderr().contains("already exists")) {
77-
throw new RuntimeException("Failed to create Gitea admin user: " + result.getStderr()
78-
+ " / stdout: " + result.getStdout());
79+
throw new RuntimeException(
80+
"Failed to create Gitea admin user: " + result.getStderr() + " / stdout: " + result.getStdout());
7981
}
8082
}
8183

8284
/**
83-
* Creates the test organisation and a public repository via the Gitea REST API. The repo is
84-
* auto-initialised with a README so it has a {@code main} branch ready for pushes. Must be
85-
* called after {@link #createAdminUser()}.
85+
* Creates the test organisation and a public repository via the Gitea REST API. The repo is auto-initialised with a
86+
* README so it has a {@code main} branch ready for pushes. Must be called after {@link #createAdminUser()}.
8687
*/
8788
void createTestRepo() throws IOException, InterruptedException {
8889
var client = HttpClient.newHttpClient();
89-
String auth = Base64.getEncoder()
90-
.encodeToString((ADMIN_USER + ":" + ADMIN_PASSWORD).getBytes());
90+
String auth = Base64.getEncoder().encodeToString((ADMIN_USER + ":" + ADMIN_PASSWORD).getBytes());
9191
String base = getBaseUrl();
9292

9393
// Create organisation (ignore 422 if it already exists)
94-
apiPost(
95-
client,
96-
auth,
97-
base + "/api/v1/orgs",
98-
"{\"username\":\"" + TEST_ORG + "\",\"visibility\":\"public\"}");
94+
apiPost(client, auth, base + "/api/v1/orgs", "{\"username\":\"" + TEST_ORG + "\",\"visibility\":\"public\"}");
9995

10096
// Create repository under the org (auto_init gives an initial commit on main)
10197
var resp = apiPost(
10298
client,
10399
auth,
104100
base + "/api/v1/orgs/" + TEST_ORG + "/repos",
105-
"{\"name\":\"" + TEST_REPO
106-
+ "\",\"private\":false,\"auto_init\":true,\"default_branch\":\"main\"}");
101+
"{\"name\":\"" + TEST_REPO + "\",\"private\":false,\"auto_init\":true,\"default_branch\":\"main\"}");
107102
if (resp.statusCode() >= 400) {
108-
throw new RuntimeException(
109-
"Failed to create test repo (" + resp.statusCode() + "): " + resp.body());
103+
throw new RuntimeException("Failed to create test repo (" + resp.statusCode() + "): " + resp.body());
110104
}
111105
}
112106

jgit-proxy-server/src/test/java/org/finos/gitproxy/e2e/JettyProxyFixture.java

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,10 @@
2121
import org.finos.gitproxy.servlet.filter.*;
2222

2323
/**
24-
* Starts and stops a real Jetty server wired up identically to {@code GitProxyJettyApplication}
25-
* but with a single {@link GenericProxyProvider} pointing at the test Gitea instance, listening on
26-
* an ephemeral port.
24+
* Starts and stops a real Jetty server wired up identically to {@code GitProxyJettyApplication} but with a single
25+
* {@link GenericProxyProvider} pointing at the test Gitea instance, listening on an ephemeral port.
2726
*
28-
* <p>Intended for use inside {@code @Tag("e2e")} tests as a JUnit {@code @BeforeAll} / {@code
29-
* @AfterAll} resource.
27+
* <p>Intended for use inside {@code @Tag("e2e")} tests as a JUnit {@code @BeforeAll} / {@code @AfterAll} resource.
3028
*/
3129
class JettyProxyFixture implements AutoCloseable {
3230

@@ -43,8 +41,7 @@ class JettyProxyFixture implements AutoCloseable {
4341
server.addConnector(connector);
4442

4543
var pushStore = PushStoreFactory.inMemory();
46-
var storeForwardCache =
47-
new LocalRepositoryCache(Files.createTempDirectory("jgit-proxy-e2e-sf-"), 0, true);
44+
var storeForwardCache = new LocalRepositoryCache(Files.createTempDirectory("jgit-proxy-e2e-sf-"), 0, true);
4845
var proxyCache = new LocalRepositoryCache();
4946

5047
var provider = GenericProxyProvider.builder()
@@ -61,8 +58,7 @@ class JettyProxyFixture implements AutoCloseable {
6158
var resolver = new StoreAndForwardRepositoryResolver(storeForwardCache, provider);
6259
var gitServlet = new GitServlet();
6360
gitServlet.setRepositoryResolver(resolver);
64-
gitServlet.setReceivePackFactory(
65-
new StoreAndForwardReceivePackFactory(provider, commitConfig, pushStore));
61+
gitServlet.setReceivePackFactory(new StoreAndForwardReceivePackFactory(provider, commitConfig, pushStore));
6662
gitServlet.setUploadPackFactory(new StoreAndForwardUploadPackFactory());
6763

6864
String pushServletPath = PUSH_PREFIX + provider.servletPath();
@@ -71,13 +67,9 @@ class JettyProxyFixture implements AutoCloseable {
7167
gitHolder.setName("git-gitea-e2e");
7268
context.addServlet(gitHolder, pushMapping);
7369
context.addFilter(
74-
new FilterHolder(new SmartHttpErrorFilter()),
75-
pushMapping,
76-
EnumSet.of(DispatcherType.REQUEST));
70+
new FilterHolder(new SmartHttpErrorFilter()), pushMapping, EnumSet.of(DispatcherType.REQUEST));
7771
context.addFilter(
78-
new FilterHolder(new BasicAuthChallengeFilter()),
79-
pushMapping,
80-
EnumSet.of(DispatcherType.REQUEST));
72+
new FilterHolder(new BasicAuthChallengeFilter()), pushMapping, EnumSet.of(DispatcherType.REQUEST));
8173

8274
// Transparent proxy GitProxyServlet on /proxy/...
8375
String proxyServletPath = PROXY_PREFIX + provider.servletPath();
@@ -96,10 +88,7 @@ class JettyProxyFixture implements AutoCloseable {
9688
addFilter(context, proxyMapping, new PushStoreAuditFilter(pushStore));
9789
addFilter(context, proxyMapping, new ForceGitClientFilter());
9890
addFilter(context, proxyMapping, new ParseGitRequestFilter(provider, PROXY_PREFIX));
99-
addFilter(
100-
context,
101-
proxyMapping,
102-
new EnrichPushCommitsFilter(provider, proxyCache, PROXY_PREFIX));
91+
addFilter(context, proxyMapping, new EnrichPushCommitsFilter(provider, proxyCache, PROXY_PREFIX));
10392
addFilter(context, proxyMapping, new CheckAuthorEmailsFilter(commitConfig));
10493
addFilter(context, proxyMapping, new CheckCommitMessagesFilter(commitConfig));
10594
addFilter(context, proxyMapping, new GpgSignatureFilter(GpgConfig.defaultConfig()));
@@ -163,8 +152,7 @@ static CommitConfig buildCommitConfig() {
163152
.message(CommitConfig.MessageConfig.builder()
164153
.block(CommitConfig.BlockConfig.builder()
165154
.literals(List.of("WIP", "DO NOT MERGE", "fixup!", "squash!"))
166-
.patterns(List.of(
167-
Pattern.compile("(?i)(password|secret|token)\\s*[=:]\\s*\\S+")))
155+
.patterns(List.of(Pattern.compile("(?i)(password|secret|token)\\s*[=:]\\s*\\S+")))
168156
.build())
169157
.build())
170158
.build();

jgit-proxy-server/src/test/java/org/finos/gitproxy/e2e/ProxyModeE2ETest.java

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,11 @@
1212
/**
1313
* End-to-end tests for the <em>transparent proxy</em> path ({@code /proxy/...}).
1414
*
15-
* <p>Mirrors {@code test-proxy-pass.sh} and {@code test-proxy-fail.sh}: every test performs a real
16-
* {@code git clone} + commit + push through a live Jetty proxy that forwards to a containerised
17-
* Gitea instance.
15+
* <p>Mirrors {@code test-proxy-pass.sh} and {@code test-proxy-fail.sh}: every test performs a real {@code git clone} +
16+
* commit + push through a live Jetty proxy that forwards to a containerised Gitea instance.
1817
*
19-
* <p>Infrastructure is started once per class (containers are expensive) and each test clones into
20-
* its own temp directory so there are no ordering dependencies.
18+
* <p>Infrastructure is started once per class (containers are expensive) and each test clones into its own temp
19+
* directory so there are no ordering dependencies.
2120
*/
2221
@Tag("e2e")
2322
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@@ -61,14 +60,12 @@ private GitHelper helper() {
6160
}
6261

6362
/**
64-
* Clones the test repo, sets the given author identity, appends a timestamped line to a test
65-
* file, stages and commits with {@code message}, then attempts a push.
63+
* Clones the test repo, sets the given author identity, appends a timestamped line to a test file, stages and
64+
* commits with {@code message}, then attempts a push.
6665
*
6766
* @return {@code true} if the push succeeded
6867
*/
69-
private boolean cloneCommitPush(
70-
String dirSuffix, String authorEmail, String commitMessage)
71-
throws Exception {
68+
private boolean cloneCommitPush(String dirSuffix, String authorEmail, String commitMessage) throws Exception {
7269
GitHelper git = helper();
7370
Path repo = git.clone(repoUrl(), dirSuffix);
7471
git.setAuthor(repo, GiteaContainer.VALID_AUTHOR_NAME, authorEmail);
@@ -112,21 +109,15 @@ void multipleCleanCommits_pass() throws Exception {
112109
@Order(10)
113110
void noreplyLocalPart_blocked() throws Exception {
114111
assertFalse(
115-
cloneCommitPush(
116-
"proxy-fail-noreply",
117-
"noreply@example.com",
118-
"feat: this commit has a noreply author"),
112+
cloneCommitPush("proxy-fail-noreply", "noreply@example.com", "feat: this commit has a noreply author"),
119113
"push with noreply@ address should be rejected");
120114
}
121115

122116
@Test
123117
@Order(11)
124118
void noReplyHyphenLocalPart_blocked() throws Exception {
125119
assertFalse(
126-
cloneCommitPush(
127-
"proxy-fail-noreply2",
128-
"no-reply@example.com",
129-
"feat: no-reply local part"),
120+
cloneCommitPush("proxy-fail-noreply2", "no-reply@example.com", "feat: no-reply local part"),
130121
"push with no-reply@ address should be rejected");
131122
}
132123

@@ -157,9 +148,7 @@ void githubNoreplyEmail_blocked() throws Exception {
157148
void wipCommitMessage_blocked() throws Exception {
158149
assertFalse(
159150
cloneCommitPush(
160-
"proxy-fail-wip",
161-
GiteaContainer.VALID_AUTHOR_EMAIL,
162-
"WIP: still working on this feature"),
151+
"proxy-fail-wip", GiteaContainer.VALID_AUTHOR_EMAIL, "WIP: still working on this feature"),
163152
"push with WIP commit message should be rejected");
164153
}
165154

@@ -179,9 +168,7 @@ void fixupCommitMessage_blocked() throws Exception {
179168
void doNotMergeCommitMessage_blocked() throws Exception {
180169
assertFalse(
181170
cloneCommitPush(
182-
"proxy-fail-dnm",
183-
GiteaContainer.VALID_AUTHOR_EMAIL,
184-
"DO NOT MERGE - experimental branch"),
171+
"proxy-fail-dnm", GiteaContainer.VALID_AUTHOR_EMAIL, "DO NOT MERGE - experimental branch"),
185172
"push with DO NOT MERGE message should be rejected");
186173
}
187174

0 commit comments

Comments
 (0)