Skip to content

Commit 5041114

Browse files
coopernetesclaude
andcommitted
feat: sideband UX — NO_COLOR/GITPROXY_NO_EMOJI support and proxy validation summary
Add conditional color/emoji helpers to GitClient: - isColorEnabled() checks NO_COLOR env var (standard CLICOLOR spec) - isEmojiEnabled() checks GITPROXY_NO_EMOJI env var - color(AnsiColor, String) wraps text in ANSI codes only when enabled - sym(SymbolCodes) returns emoji or plain variant based on GITPROXY_NO_EMOJI - buildValidationSummary(List<PushStep>) renders a per-filter pass/fail table for transparent proxy mode (filters at order 1000–4999) Update all S&F pre-receive hooks to use color()/sym() instead of hardcoded ANSI escape sequences and .emoji() calls. Enhance transparent proxy terminal filters: - ValidationSummaryFilter: prepend validation summary table before error details - PushFinalizerFilter: prepend validation summary before the pending-review link Fix pre-existing test assertions in ProxyModeFilterChainTest where "passes" tests incorrectly asserted ALLOWED — the test chain has no PushFinalizerFilter so the result stays PENDING when validation passes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 53c855e commit 5041114

13 files changed

Lines changed: 149 additions & 74 deletions

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ The Node.js original lives at `/home/tom/repos/git-proxy`. Refer to it for the A
2424
## Build & test
2525

2626
```bash
27+
./gradlew spotlessApply # fix formatting (palantir-java-format) — run this before build
2728
./gradlew build # compile + unit tests (no containers)
2829
./gradlew test # unit tests only (e2e excluded)
2930
./gradlew e2eTest # e2e tests — requires Docker/Podman
30-
./gradlew spotlessApply # fix formatting (palantir-java-format)
3131
```
3232

3333
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")`.

jgit-proxy-core/src/main/java/org/finos/gitproxy/git/ApprovalPreReceiveHook.java

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import static org.finos.gitproxy.git.GitClient.AnsiColor.*;
44
import static org.finos.gitproxy.git.GitClient.SymbolCodes.*;
5+
import static org.finos.gitproxy.git.GitClient.color;
6+
import static org.finos.gitproxy.git.GitClient.sym;
57

68
import java.io.IOException;
79
import java.io.OutputStream;
@@ -76,7 +78,7 @@ var record = pushStore.findById(validationRecordId).orElse(null);
7678
sendAndFlush(
7779
rp,
7880
msgOut,
79-
GREEN + "[git-proxy] " + HEAVY_CHECK_MARK.emoji() + " Push already approved — forwarding" + RESET);
81+
color(GREEN, "[git-proxy] " + sym(HEAVY_CHECK_MARK) + " Push already approved — forwarding"));
8082
return;
8183
}
8284

@@ -85,17 +87,16 @@ var record = pushStore.findById(validationRecordId).orElse(null);
8587
sendAndFlush(
8688
rp,
8789
msgOut,
88-
YELLOW + "[git-proxy] " + WARNING.emoji() + " Push requires review. Waiting for approval..."
89-
+ RESET);
90-
sendAndFlush(rp, msgOut, CYAN + "[git-proxy] " + KEY.emoji() + " Push ID: " + validationRecordId + RESET);
90+
color(YELLOW, "[git-proxy] " + sym(WARNING) + " Push requires review. Waiting for approval..."));
91+
sendAndFlush(rp, msgOut, color(CYAN, "[git-proxy] " + sym(KEY) + " Push ID: " + validationRecordId));
9192
if (serviceUrl != null) {
9293
sendAndFlush(
9394
rp,
9495
msgOut,
95-
CYAN + "[git-proxy] Review at: " + serviceUrl + "/#/push/" + validationRecordId + RESET);
96+
color(CYAN, "[git-proxy] Review at: " + serviceUrl + "/#/push/" + validationRecordId));
9697
}
9798
if (record.getBlockedMessage() != null) {
98-
sendAndFlush(rp, msgOut, YELLOW + "[git-proxy] Reason: " + record.getBlockedMessage() + RESET);
99+
sendAndFlush(rp, msgOut, color(YELLOW, "[git-proxy] Reason: " + record.getBlockedMessage()));
99100
}
100101

101102
ApprovalResult result =
@@ -106,29 +107,28 @@ var record = pushStore.findById(validationRecordId).orElse(null);
106107
sendAndFlush(
107108
rp,
108109
msgOut,
109-
GREEN + "[git-proxy] " + HEAVY_CHECK_MARK.emoji() + " Push approved by reviewer" + RESET);
110+
color(GREEN, "[git-proxy] " + sym(HEAVY_CHECK_MARK) + " Push approved by reviewer"));
110111
case REJECTED -> {
111112
var updated = pushStore.findById(validationRecordId).orElse(null);
112113
String reason = updated != null && updated.getAttestation() != null
113114
? updated.getAttestation().getReason()
114115
: "Push rejected by reviewer";
115116
sendAndFlush(
116-
rp,
117-
msgOut,
118-
RED + "[git-proxy] " + CROSS_MARK.emoji() + " Push rejected: " + reason + RESET);
117+
rp, msgOut, color(RED, "[git-proxy] " + sym(CROSS_MARK) + " Push rejected: " + reason));
119118
rejectAll(commands, reason);
120119
}
121120
case CANCELED -> {
122-
sendAndFlush(rp, msgOut, YELLOW + "[git-proxy] " + WARNING.emoji() + " Push canceled" + RESET);
121+
sendAndFlush(rp, msgOut, color(YELLOW, "[git-proxy] " + sym(WARNING) + " Push canceled"));
123122
rejectAll(commands, "Push canceled");
124123
}
125124
case TIMED_OUT -> {
126125
sendAndFlush(
127126
rp,
128127
msgOut,
129-
RED + "[git-proxy] " + CROSS_MARK.emoji()
130-
+ " Approval timed out after " + timeout.toMinutes()
131-
+ " minutes" + RESET);
128+
color(
129+
RED,
130+
"[git-proxy] " + sym(CROSS_MARK) + " Approval timed out after "
131+
+ timeout.toMinutes() + " minutes"));
132132
rejectAll(commands, "Approval timed out");
133133
}
134134
}

jgit-proxy-core/src/main/java/org/finos/gitproxy/git/AuthorEmailValidationHook.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import static org.finos.gitproxy.git.GitClient.AnsiColor.*;
44
import static org.finos.gitproxy.git.GitClient.SymbolCodes.*;
5+
import static org.finos.gitproxy.git.GitClient.color;
6+
import static org.finos.gitproxy.git.GitClient.sym;
57

68
import java.util.ArrayList;
79
import java.util.Collection;
@@ -35,7 +37,7 @@ public class AuthorEmailValidationHook implements PreReceiveHook {
3537

3638
@Override
3739
public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
38-
rp.sendMessage(CYAN + "[git-proxy] " + KEY.emoji() + " Checking author emails..." + RESET);
40+
rp.sendMessage(color(CYAN, "[git-proxy] " + sym(KEY) + " Checking author emails..."));
3941

4042
Repository repo = rp.getRepository();
4143
List<String> logs = new ArrayList<>();
@@ -55,19 +57,18 @@ public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
5557
if (!isEmailAllowed(email)) {
5658
String detail = describeRejection(email);
5759
validationContext.addIssue("AuthorEmail", "Illegal author email: " + email, detail);
58-
rp.sendMessage(
59-
RED + "[git-proxy] " + CROSS_MARK.emoji() + " " + email + " — " + detail + RESET);
60+
rp.sendMessage(color(RED, "[git-proxy] " + sym(CROSS_MARK) + " " + email + " — " + detail));
6061
logs.add("FAIL: " + email + " — " + detail);
6162
anyFailed = true;
6263
} else {
63-
rp.sendMessage(GREEN + "[git-proxy] " + HEAVY_CHECK_MARK.emoji() + " " + email + RESET);
64+
rp.sendMessage(color(GREEN, "[git-proxy] " + sym(HEAVY_CHECK_MARK) + " " + email));
6465
logs.add("PASS: " + email);
6566
}
6667
}
6768
} catch (Exception e) {
6869
log.error("Failed to validate author emails for {}", cmd.getRefName(), e);
69-
rp.sendMessage(YELLOW + "[git-proxy] " + WARNING.emoji() + " Could not validate emails: "
70-
+ e.getMessage() + RESET);
70+
rp.sendMessage(color(
71+
YELLOW, "[git-proxy] " + sym(WARNING) + " Could not validate emails: " + e.getMessage()));
7172
logs.add("ERROR: " + cmd.getRefName() + " — " + e.getMessage());
7273
}
7374
}

jgit-proxy-core/src/main/java/org/finos/gitproxy/git/CheckEmptyBranchHook.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import static org.finos.gitproxy.git.GitClient.AnsiColor.*;
44
import static org.finos.gitproxy.git.GitClient.SymbolCodes.*;
5+
import static org.finos.gitproxy.git.GitClient.color;
6+
import static org.finos.gitproxy.git.GitClient.sym;
57

68
import java.util.Collection;
79
import java.util.List;
@@ -46,7 +48,7 @@ public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
4648
msg = "Push blocked: Commit data not found. Please contact an administrator for support.";
4749
}
4850

49-
rp.sendMessage(RED + "[git-proxy] " + NO_ENTRY.emoji() + " " + msg + RESET);
51+
rp.sendMessage(color(RED, "[git-proxy] " + sym(NO_ENTRY) + " " + msg));
5052
cmd.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, msg);
5153
// Chain stops once a command is rejected; remaining commands will be skipped
5254
return;

jgit-proxy-core/src/main/java/org/finos/gitproxy/git/CheckHiddenCommitsHook.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import static org.finos.gitproxy.git.GitClient.AnsiColor.*;
44
import static org.finos.gitproxy.git.GitClient.SymbolCodes.*;
5+
import static org.finos.gitproxy.git.GitClient.color;
6+
import static org.finos.gitproxy.git.GitClient.sym;
57

68
import java.io.IOException;
79
import java.util.Collection;
@@ -59,9 +61,8 @@ public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
5961
+ " and pushed to the remote.\n"
6062
+ "Please get approval on the commits, push them and try again.";
6163

62-
rp.sendMessage(
63-
RED + "[git-proxy] " + NO_ENTRY.emoji() + " Push blocked — hidden commits detected" + RESET);
64-
rp.sendMessage(YELLOW + "[git-proxy] " + WARNING.emoji() + " " + msg + RESET);
64+
rp.sendMessage(color(RED, "[git-proxy] " + sym(NO_ENTRY) + " Push blocked — hidden commits detected"));
65+
rp.sendMessage(color(YELLOW, "[git-proxy] " + sym(WARNING) + " " + msg));
6566

6667
for (ReceiveCommand cmd : commands) {
6768
if (cmd.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) {

jgit-proxy-core/src/main/java/org/finos/gitproxy/git/CommitMessageValidationHook.java

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import static org.finos.gitproxy.git.GitClient.AnsiColor.*;
44
import static org.finos.gitproxy.git.GitClient.SymbolCodes.*;
5+
import static org.finos.gitproxy.git.GitClient.color;
6+
import static org.finos.gitproxy.git.GitClient.sym;
57

68
import java.util.ArrayList;
79
import java.util.Collection;
@@ -32,7 +34,7 @@ public class CommitMessageValidationHook implements PreReceiveHook {
3234

3335
@Override
3436
public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
35-
rp.sendMessage(CYAN + "[git-proxy] " + LINK.emoji() + " Checking commit messages..." + RESET);
37+
rp.sendMessage(color(CYAN, "[git-proxy] " + sym(LINK) + " Checking commit messages..."));
3638

3739
Repository repo = rp.getRepository();
3840
List<String> logs = new ArrayList<>();
@@ -54,21 +56,21 @@ public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
5456
if (reason != null) {
5557
validationContext.addIssue(
5658
"CommitMessage", shortSha + " — blocked: " + reason, "Message: " + firstLine);
57-
rp.sendMessage(RED + "[git-proxy] " + CROSS_MARK.emoji() + " " + shortSha + " " + firstLine
58-
+ RESET);
59-
rp.sendMessage(RED + "[git-proxy] " + reason + RESET);
59+
rp.sendMessage(
60+
color(RED, "[git-proxy] " + sym(CROSS_MARK) + " " + shortSha + " " + firstLine));
61+
rp.sendMessage(color(RED, "[git-proxy] " + reason));
6062
logs.add("FAIL: " + shortSha + " — " + reason + " [" + firstLine + "]");
6163
anyFailed = true;
6264
} else {
63-
rp.sendMessage(GREEN + "[git-proxy] " + HEAVY_CHECK_MARK.emoji() + " " + shortSha + " "
64-
+ firstLine + RESET);
65+
rp.sendMessage(color(
66+
GREEN, "[git-proxy] " + sym(HEAVY_CHECK_MARK) + " " + shortSha + " " + firstLine));
6567
logs.add("PASS: " + shortSha + " — " + firstLine);
6668
}
6769
}
6870
} catch (Exception e) {
6971
log.error("Failed to validate commit messages for {}", cmd.getRefName(), e);
70-
rp.sendMessage(YELLOW + "[git-proxy] " + WARNING.emoji() + " Could not validate messages: "
71-
+ e.getMessage() + RESET);
72+
rp.sendMessage(color(
73+
YELLOW, "[git-proxy] " + sym(WARNING) + " Could not validate messages: " + e.getMessage()));
7274
logs.add("ERROR: " + cmd.getRefName() + " — " + e.getMessage());
7375
}
7476
}

jgit-proxy-core/src/main/java/org/finos/gitproxy/git/GitClient.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package org.finos.gitproxy.git;
22

3+
import java.util.List;
4+
import java.util.stream.Collectors;
35
import lombok.Getter;
46
import lombok.RequiredArgsConstructor;
7+
import org.finos.gitproxy.db.model.PushStep;
8+
import org.finos.gitproxy.db.model.StepStatus;
59

610
public class GitClient {
711

@@ -205,4 +209,56 @@ private static String stripColors(String content) {
205209
}
206210
return content;
207211
}
212+
213+
/** Returns {@code true} unless the {@code NO_COLOR} environment variable is set (any value). */
214+
public static boolean isColorEnabled() {
215+
return System.getenv("NO_COLOR") == null;
216+
}
217+
218+
/** Returns {@code true} unless {@code GITPROXY_NO_EMOJI} is set. */
219+
public static boolean isEmojiEnabled() {
220+
return System.getenv("GITPROXY_NO_EMOJI") == null;
221+
}
222+
223+
/**
224+
* Wraps {@code text} in an ANSI color + reset sequence when color is enabled; otherwise returns the text unchanged.
225+
*/
226+
public static String color(AnsiColor c, String text) {
227+
if (!isColorEnabled()) return text;
228+
return c.getValue() + text + AnsiColor.RESET.getValue();
229+
}
230+
231+
/** Returns the emoji variant of {@code s} when emoji is enabled, or the plain text variant otherwise. */
232+
public static String sym(SymbolCodes s) {
233+
return isEmojiEnabled() ? s.emoji() : s.plain();
234+
}
235+
236+
/**
237+
* Builds a one-line-per-filter validation summary string for the transparent proxy pipeline. Only shows steps in
238+
* the content-filter order range (1000–4999) — infrastructure and finalizer steps are omitted. Returns an empty
239+
* string if there are no relevant steps.
240+
*/
241+
public static String buildValidationSummary(List<PushStep> steps) {
242+
List<PushStep> relevant = steps.stream()
243+
.filter(s -> s.getStepOrder() >= 1000 && s.getStepOrder() < 5000)
244+
.collect(Collectors.toList());
245+
if (relevant.isEmpty()) return "";
246+
247+
StringBuilder sb = new StringBuilder();
248+
sb.append(color(AnsiColor.CYAN, "[git-proxy] Validation Summary")).append("\n");
249+
for (PushStep step : relevant) {
250+
if (step.getStatus() == StepStatus.PASS) {
251+
sb.append(color(
252+
AnsiColor.GREEN,
253+
" " + sym(SymbolCodes.HEAVY_CHECK_MARK) + " " + step.getStepName() + " — passed"))
254+
.append("\n");
255+
} else if (step.getStatus() == StepStatus.FAIL) {
256+
sb.append(color(
257+
AnsiColor.RED,
258+
" " + sym(SymbolCodes.CROSS_MARK) + " " + step.getStepName() + " — failed"))
259+
.append("\n");
260+
}
261+
}
262+
return sb.toString();
263+
}
208264
}

jgit-proxy-core/src/main/java/org/finos/gitproxy/git/ProxyPreReceiveHook.java

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import static org.finos.gitproxy.git.GitClient.AnsiColor.*;
44
import static org.finos.gitproxy.git.GitClient.SymbolCodes.*;
5+
import static org.finos.gitproxy.git.GitClient.color;
6+
import static org.finos.gitproxy.git.GitClient.sym;
57

68
import java.util.ArrayList;
79
import java.util.Collection;
@@ -33,7 +35,7 @@ public ProxyPreReceiveHook(PushContext pushContext) {
3335

3436
@Override
3537
public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
36-
rp.sendMessage(CYAN + "[git-proxy] " + LINK.emoji() + " Validating push..." + RESET);
38+
rp.sendMessage(color(CYAN, "[git-proxy] " + sym(LINK) + " Validating push..."));
3739

3840
Repository repo = rp.getRepository();
3941
List<String> logs = new ArrayList<>();
@@ -44,12 +46,11 @@ public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
4446
String newId = cmd.getNewId().abbreviate(7).name();
4547

4648
rp.sendMessage(
47-
BLUE + "[git-proxy] " + cmd.getType() + " " + refName + " " + oldId + " -> " + newId + RESET);
49+
color(BLUE, "[git-proxy] " + cmd.getType() + " " + refName + " " + oldId + " -> " + newId));
4850
logs.add(cmd.getType() + " " + refName + " " + oldId + " -> " + newId);
4951

5052
if (cmd.getType() == ReceiveCommand.Type.DELETE) {
51-
rp.sendMessage(
52-
YELLOW + "[git-proxy] " + WARNING.emoji() + " Ref deletion, skipping inspection" + RESET);
53+
rp.sendMessage(color(YELLOW, "[git-proxy] " + sym(WARNING) + " Ref deletion, skipping inspection"));
5354
logs.add("Skipped inspection: ref deletion");
5455
continue;
5556
}
@@ -58,13 +59,13 @@ public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
5859
inspectCommits(rp, repo, cmd, logs);
5960
} catch (Exception e) {
6061
log.error("Failed to inspect commits for {}", refName, e);
61-
rp.sendMessage(RED + "[git-proxy] " + CROSS_MARK.emoji() + " Commit inspection failed: "
62-
+ e.getMessage() + RESET);
62+
rp.sendMessage(color(
63+
RED, "[git-proxy] " + sym(CROSS_MARK) + " Commit inspection failed: " + e.getMessage()));
6364
logs.add("ERROR inspecting " + refName + ": " + e.getMessage());
6465
}
6566
}
6667

67-
rp.sendMessage(GREEN + "[git-proxy] " + HEAVY_CHECK_MARK.emoji() + " Validation complete" + RESET);
68+
rp.sendMessage(color(GREEN, "[git-proxy] " + sym(HEAVY_CHECK_MARK) + " Validation complete"));
6869

6970
pushContext.addStep(PushStep.builder()
7071
.stepName("inspection")
@@ -84,20 +85,20 @@ private void inspectCommits(ReceivePack rp, Repository repo, ReceiveCommand cmd,
8485
String tipLine =
8586
"New branch - tip commit by " + tipCommit.getAuthor().getName() + " <"
8687
+ tipCommit.getAuthor().getEmail() + ">";
87-
rp.sendMessage(CYAN + "[git-proxy] " + tipLine + RESET);
88+
rp.sendMessage(color(CYAN, "[git-proxy] " + tipLine));
8889
logs.add(tipLine);
8990
return;
9091
}
9192

9293
List<Commit> commits = CommitInspectionService.getCommitRange(repo, fromCommit, toCommit);
93-
rp.sendMessage(CYAN + "[git-proxy] " + commits.size() + " commit(s) in push" + RESET);
94+
rp.sendMessage(color(CYAN, "[git-proxy] " + commits.size() + " commit(s) in push"));
9495

9596
for (Commit commit : commits) {
9697
String shortSha = commit.getSha().substring(0, 7);
9798
String firstLine = commit.getMessage().lines().findFirst().orElse("(empty)");
9899
String line = shortSha + " " + commit.getAuthor().getName() + " <"
99100
+ commit.getAuthor().getEmail() + "> " + firstLine;
100-
rp.sendMessage(MAGENTA + "[git-proxy] " + line + RESET);
101+
rp.sendMessage(color(MAGENTA, "[git-proxy] " + line));
101102
logs.add(line);
102103
}
103104
}

0 commit comments

Comments
 (0)