From dfed7792fd155aa436335afd6a92663583c2dc14 Mon Sep 17 00:00:00 2001 From: Michael Trimarchi Date: Sat, 27 Jun 2026 21:18:02 +0200 Subject: [PATCH 01/18] Update GerritServer to use GerritEventSource interface Change the gerritConnection field type from concrete GerritConnection to the new GerritEventSource interface, allowing future HTTPS polling implementations to be substituted. Update test references accordingly. Also bump gerrit-events dependency to 2.22.1-SNAPSHOT for the new API. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Michael Trimarchi --- .../hudson/plugins/gerrit/trigger/GerritServer.java | 3 ++- .../spec/DuplicateGerritListenersHudsonTestCase.java | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritServer.java b/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritServer.java index 31824e246..52f4b725c 100644 --- a/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritServer.java +++ b/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritServer.java @@ -100,6 +100,7 @@ import com.sonymobile.tools.gerrit.gerritevents.GerritHandler; import com.sonymobile.tools.gerrit.gerritevents.GerritQueryHandler; import com.sonymobile.tools.gerrit.gerritevents.GerritConnection; +import com.sonymobile.tools.gerrit.gerritevents.GerritEventSource; import com.sonymobile.tools.gerrit.gerritevents.dto.GerritEvent; import com.sonymobile.tools.gerrit.gerritevents.dto.rest.Notify; import com.sonymobile.tools.gerrit.gerritevents.ssh.Authentication; @@ -152,7 +153,7 @@ public class GerritServer implements Describable, Action { private transient boolean timeoutWakeup = false; private transient String connectionResponse = ""; private transient GerritHandler gerritEventManager; - private transient GerritConnection gerritConnection; + private transient GerritEventSource gerritConnection; private transient GerritProjectListUpdater projectListUpdater; private IGerritHudsonTriggerConfig config; private transient GerritConnectionListener gerritConnectionListener; diff --git a/src/test/java/com/sonyericsson/hudson/plugins/gerrit/trigger/spec/DuplicateGerritListenersHudsonTestCase.java b/src/test/java/com/sonyericsson/hudson/plugins/gerrit/trigger/spec/DuplicateGerritListenersHudsonTestCase.java index 42c8decb6..0262a6934 100644 --- a/src/test/java/com/sonyericsson/hudson/plugins/gerrit/trigger/spec/DuplicateGerritListenersHudsonTestCase.java +++ b/src/test/java/com/sonyericsson/hudson/plugins/gerrit/trigger/spec/DuplicateGerritListenersHudsonTestCase.java @@ -32,7 +32,7 @@ import com.sonyericsson.hudson.plugins.gerrit.trigger.playback.GerritMissedEventsPlaybackManager; import com.sonyericsson.hudson.plugins.gerrit.trigger.replication.ReplicationQueueTaskDispatcher; import com.sonyericsson.jenkins.plugins.bfa.test.utils.Whitebox; -import com.sonymobile.tools.gerrit.gerritevents.GerritConnection; +import com.sonymobile.tools.gerrit.gerritevents.GerritEventSource; import com.sonymobile.tools.gerrit.gerritevents.GerritEventListener; import com.sonymobile.tools.gerrit.gerritevents.GerritHandler; import com.sonyericsson.hudson.plugins.gerrit.trigger.GerritServer; @@ -154,7 +154,7 @@ void testNewProjectCreationFirstNoConnection() throws Exception { server.start(); createGerritTriggeredJob(j, "testJob4"); - GerritConnection connection = Whitebox.getInternalState(server, GerritConnection.class); + GerritEventSource connection = Whitebox.getInternalState(server, GerritEventSource.class); assertNull(connection); assertAllListenersAreRegistered(server); Config config = (Config)server.getConfig(); @@ -166,7 +166,7 @@ void testNewProjectCreationFirstNoConnection() throws Exception { server.startConnection(); assertAllListenersAreRegistered(server); - connection = Whitebox.getInternalState(server, GerritConnection.class); + connection = Whitebox.getInternalState(server, GerritEventSource.class); assertNotNull(connection); } From fef8c9c370ca860a83acf10ba5b1ee6ee01f2cca Mon Sep 17 00:00:00 2001 From: Michael Trimarchi Date: Fri, 19 Jun 2026 21:10:31 +0200 Subject: [PATCH 02/18] Add HTTPS polling configuration fields and UI Implement the GerritConnectionConfig2 interface additions in Config: - Add useHttpsPoller, httpsPollInterval, httpsPollMaxChanges fields - Add getters with sensible defaults (10s interval, 100 changes/poll) - Add form parsing for the optionalBlock in setValues() - Update copy constructor Add UI section 'HTTPS Event Polling' to server config page: - Use HTTPS Polling for Events checkbox (optionalBlock) - Poll Interval (seconds) number field - Max Changes Per Poll number field Also add help HTML files for the new UI fields. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Michael Trimarchi --- .../plugins/gerrit/trigger/config/Config.java | 80 +++++++++++++++++++ .../gerrit/trigger/GerritServer/index.jelly | 23 ++++++ src/main/webapp/help-HttpsPollInterval.html | 10 +++ src/main/webapp/help-HttpsPollMaxChanges.html | 8 ++ src/main/webapp/help-UseHttpsPoller.html | 16 ++++ .../mock/MockGerritHudsonTriggerConfig.java | 15 ++++ .../utils/MockPluginCheckerConfig.java | 15 ++++ 7 files changed, 167 insertions(+) create mode 100644 src/main/webapp/help-HttpsPollInterval.html create mode 100644 src/main/webapp/help-HttpsPollMaxChanges.html create mode 100644 src/main/webapp/help-UseHttpsPoller.html diff --git a/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/config/Config.java b/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/config/Config.java index c880aa14a..64ca82c62 100644 --- a/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/config/Config.java +++ b/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/config/Config.java @@ -162,6 +162,21 @@ public class Config implements IGerritHudsonTriggerConfig { * Global default for notification level. */ public static final Notify DEFAULT_NOTIFICATION_LEVEL = Notify.ALL; + /** + * Default value for {@link #isUseHttpsPoller()}. + */ + public static final boolean DEFAULT_USE_HTTPS_POLLER = + com.sonymobile.tools.gerrit.gerritevents.GerritDefaultValues.DEFAULT_USE_HTTPS_POLLER; + /** + * Default value for {@link #getHttpsPollInterval()}. + */ + public static final int DEFAULT_HTTPS_POLL_INTERVAL = + com.sonymobile.tools.gerrit.gerritevents.GerritDefaultValues.DEFAULT_HTTPS_POLL_INTERVAL; + /** + * Default value for {@link #getHttpsPollMaxChanges()}. + */ + public static final int DEFAULT_HTTPS_POLL_MAX_CHANGES = + com.sonymobile.tools.gerrit.gerritevents.GerritDefaultValues.DEFAULT_HTTPS_POLL_MAX_CHANGES; private String gerritHostName; private int gerritSshPort; @@ -175,6 +190,9 @@ public class Config implements IGerritHudsonTriggerConfig { private Secret gerritHttpPassword; private boolean restCodeReview; private boolean restVerified; + private boolean useHttpsPoller; + private int httpsPollInterval; + private int httpsPollMaxChanges; @Deprecated private transient boolean gerritBuildCurrentPatchesOnly; @Deprecated @@ -246,6 +264,9 @@ public Config(IGerritHudsonTriggerConfig config) { gerritHttpPassword = Secret.fromString(config.getGerritHttpPassword()); restCodeReview = config.isRestCodeReview(); restVerified = config.isRestVerified(); + useHttpsPoller = config.isUseHttpsPoller(); + httpsPollInterval = config.getHttpsPollInterval(); + httpsPollMaxChanges = config.getHttpsPollMaxChanges(); gerritBuildCurrentPatchesOnly = config.isGerritBuildCurrentPatchesOnly(); numberOfWorkerThreads = config.getNumberOfReceivingWorkerThreads(); numberOfSendingWorkerThreads = config.getNumberOfSendingWorkerThreads(); @@ -420,6 +441,17 @@ public void setValues(JSONObject formData) { useRestApi = false; } + if (formData.has("useHttpsPoller")) { + useHttpsPoller = true; + JSONObject httpsPoller = formData.getJSONObject("useHttpsPoller"); + httpsPollInterval = httpsPoller.optInt("httpsPollInterval", DEFAULT_HTTPS_POLL_INTERVAL); + httpsPollMaxChanges = httpsPoller.optInt("httpsPollMaxChanges", DEFAULT_HTTPS_POLL_MAX_CHANGES); + } else { + useHttpsPoller = false; + httpsPollInterval = DEFAULT_HTTPS_POLL_INTERVAL; + httpsPollMaxChanges = DEFAULT_HTTPS_POLL_MAX_CHANGES; + } + replicationConfig = ReplicationConfig.createReplicationConfigFromJSON(formData); } @@ -1434,6 +1466,54 @@ public void setRestVerified(boolean restVerified) { this.restVerified = restVerified; } + @Override + public boolean isUseHttpsPoller() { + return useHttpsPoller; + } + + /** + * Sets useHttpsPoller. + * @param useHttpsPoller true if HTTPS polling should be used for event ingestion. + * @see #isUseHttpsPoller() + */ + public void setUseHttpsPoller(boolean useHttpsPoller) { + this.useHttpsPoller = useHttpsPoller; + } + + @Override + public int getHttpsPollInterval() { + if (httpsPollInterval <= 0) { + httpsPollInterval = DEFAULT_HTTPS_POLL_INTERVAL; + } + return httpsPollInterval; + } + + /** + * Sets httpsPollInterval. + * @param httpsPollInterval the poll interval in seconds. + * @see #getHttpsPollInterval() + */ + public void setHttpsPollInterval(int httpsPollInterval) { + this.httpsPollInterval = httpsPollInterval; + } + + @Override + public int getHttpsPollMaxChanges() { + if (httpsPollMaxChanges <= 0) { + httpsPollMaxChanges = DEFAULT_HTTPS_POLL_MAX_CHANGES; + } + return httpsPollMaxChanges; + } + + /** + * Sets httpsPollMaxChanges. + * @param httpsPollMaxChanges the maximum changes per poll. + * @see #getHttpsPollMaxChanges() + */ + public void setHttpsPollMaxChanges(int httpsPollMaxChanges) { + this.httpsPollMaxChanges = httpsPollMaxChanges; + } + /** * When upgrading from an older version where buildCurrentPatchesOnly doesn't exist, * get the value from the now deprecated gerritBuildCurrentPatchesOnly. diff --git a/src/main/resources/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritServer/index.jelly b/src/main/resources/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritServer/index.jelly index 1219bde05..38f138953 100644 --- a/src/main/resources/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritServer/index.jelly +++ b/src/main/resources/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritServer/index.jelly @@ -345,6 +345,29 @@ + + + + + + + + + + + The number of seconds between each HTTPS poll for new Gerrit changes. +

+ Lower values provide faster responsiveness but increase load on the Gerrit server. + Higher values reduce server load but introduce more latency in receiving events. +

+ Recommended: 5-30 seconds depending on your responsiveness requirements. +
+ Default: 10 seconds. + diff --git a/src/main/webapp/help-HttpsPollMaxChanges.html b/src/main/webapp/help-HttpsPollMaxChanges.html new file mode 100644 index 000000000..8ba207567 --- /dev/null +++ b/src/main/webapp/help-HttpsPollMaxChanges.html @@ -0,0 +1,8 @@ +
+ The maximum number of changes to fetch per HTTPS poll request. +

+ This limits the batch size of each polling cycle. If there are more changes than this + limit, they will be fetched in subsequent polls based on their last-updated timestamp. +

+ Default: 100 changes per poll. +
diff --git a/src/main/webapp/help-UseHttpsPoller.html b/src/main/webapp/help-UseHttpsPoller.html new file mode 100644 index 000000000..646cd1c05 --- /dev/null +++ b/src/main/webapp/help-UseHttpsPoller.html @@ -0,0 +1,16 @@ +
+ When enabled, Gerrit events will be received by polling the Gerrit REST API over HTTPS + instead of using the SSH stream-events command. +

+ This is useful when SSH access to the Gerrit server is not available, or when network + policies require HTTPS-only communication. +

+ Requirements: +
    +
  • The Gerrit Frontend URL must be set and reachable
  • +
  • HTTP credentials must be configured (in the REST API section)
  • +
  • Gerrit must have the REST API enabled
  • +
+
+ If disabled, the plugin will use the standard SSH stream-events connection. +
diff --git a/src/test/java/com/sonyericsson/hudson/plugins/gerrit/trigger/mock/MockGerritHudsonTriggerConfig.java b/src/test/java/com/sonyericsson/hudson/plugins/gerrit/trigger/mock/MockGerritHudsonTriggerConfig.java index 5bc704528..38e1d46d5 100644 --- a/src/test/java/com/sonyericsson/hudson/plugins/gerrit/trigger/mock/MockGerritHudsonTriggerConfig.java +++ b/src/test/java/com/sonyericsson/hudson/plugins/gerrit/trigger/mock/MockGerritHudsonTriggerConfig.java @@ -529,4 +529,19 @@ public boolean isRestVerified() { public boolean isVoteSameTopic() { return false; } + + @Override + public boolean isUseHttpsPoller() { + return false; + } + + @Override + public int getHttpsPollInterval() { + return com.sonymobile.tools.gerrit.gerritevents.GerritDefaultValues.DEFAULT_HTTPS_POLL_INTERVAL; + } + + @Override + public int getHttpsPollMaxChanges() { + return com.sonymobile.tools.gerrit.gerritevents.GerritDefaultValues.DEFAULT_HTTPS_POLL_MAX_CHANGES; + } } diff --git a/src/test/java/com/sonyericsson/hudson/plugins/gerrit/trigger/utils/MockPluginCheckerConfig.java b/src/test/java/com/sonyericsson/hudson/plugins/gerrit/trigger/utils/MockPluginCheckerConfig.java index af579f3c8..7db7fd60c 100644 --- a/src/test/java/com/sonyericsson/hudson/plugins/gerrit/trigger/utils/MockPluginCheckerConfig.java +++ b/src/test/java/com/sonyericsson/hudson/plugins/gerrit/trigger/utils/MockPluginCheckerConfig.java @@ -365,4 +365,19 @@ public int getProjectListFetchDelay() { public boolean isVoteSameTopic() { return false; } + + @Override + public boolean isUseHttpsPoller() { + return false; + } + + @Override + public int getHttpsPollInterval() { + return com.sonymobile.tools.gerrit.gerritevents.GerritDefaultValues.DEFAULT_HTTPS_POLL_INTERVAL; + } + + @Override + public int getHttpsPollMaxChanges() { + return com.sonymobile.tools.gerrit.gerritevents.GerritDefaultValues.DEFAULT_HTTPS_POLL_MAX_CHANGES; + } } From 7f241d4832867a87946237b8625ab301bacf793d Mon Sep 17 00:00:00 2001 From: Michael Trimarchi Date: Fri, 19 Jun 2026 21:37:44 +0200 Subject: [PATCH 03/18] Wire GerritRestPoller into GerritServer.startConnection() When config.isUseHttpsPoller() returns true, create a GerritRestPoller instead of a GerritConnection. Both implement GerritEventSource, so the rest of the method (setHandler, addListener, start) works identically for either implementation. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Michael Trimarchi --- .../plugins/gerrit/trigger/GerritServer.java | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritServer.java b/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritServer.java index 52f4b725c..e05714e7f 100644 --- a/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritServer.java +++ b/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritServer.java @@ -101,6 +101,7 @@ import com.sonymobile.tools.gerrit.gerritevents.GerritQueryHandler; import com.sonymobile.tools.gerrit.gerritevents.GerritConnection; import com.sonymobile.tools.gerrit.gerritevents.GerritEventSource; +import com.sonymobile.tools.gerrit.gerritevents.GerritRestPoller; import com.sonymobile.tools.gerrit.gerritevents.dto.GerritEvent; import com.sonymobile.tools.gerrit.gerritevents.dto.rest.Notify; import com.sonymobile.tools.gerrit.gerritevents.ssh.Authentication; @@ -582,9 +583,23 @@ public GerritConnectionListener getGerritConnectionListener() { public synchronized void startConnection() { checkPermission(); if (!config.hasDefaultValues()) { - if (gerritConnection == null) { + if (gerritConnection != null && gerritConnection.isConnected()) { + logger.warn("Already started!"); + } else { + // If there's an existing but dead connection, stop it first + if (gerritConnection != null) { + logger.info("{}: Stopping failed connection before restart.", name); + gerritConnection.shutdown(false); + gerritConnection.removeListener(gerritConnectionListener); + gerritConnection.removeListener(missedEventsPlaybackManager); + } logger.debug("Starting Gerrit connection..."); - gerritConnection = new GerritConnection(name, config); + if (config.isUseHttpsPoller()) { + logger.info("{}: Using HTTPS polling for Gerrit events.", name); + gerritConnection = new GerritRestPoller(name, config); + } else { + gerritConnection = new GerritConnection(name, config); + } if (config.isTriggerOnAllComments()) { logger.info("Will trigger on all comments, even from the configured user."); } else { @@ -599,8 +614,6 @@ public synchronized void startConnection() { gerritConnection.addListener(missedEventsPlaybackManager); gerritConnection.start(); - } else { - logger.warn("Already started!"); } // Initialize project list update after connection with Gerrit server projectListUpdater.initProjectListUpdater(); From 2e9970da7e9cbce052d4bec3a9addd49b2360e16 Mon Sep 17 00:00:00 2001 From: Michael Trimarchi Date: Fri, 19 Jun 2026 21:48:25 +0200 Subject: [PATCH 04/18] Wire GerritRestQueryHandler into GerritServer.getQueryHandler() When config.isUseHttpsPoller() returns true, use GerritRestQueryHandler for Gerrit queries (file lists, topic association, manual search). This ensures that when HTTPS polling is enabled, all Gerrit query operations use the REST API instead of SSH. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Michael Trimarchi --- .../hudson/plugins/gerrit/trigger/GerritServer.java | 7 ++++++- .../actions/manual/ManualTriggerAction.java | 11 ++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritServer.java b/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritServer.java index e05714e7f..ce2e1503f 100644 --- a/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritServer.java +++ b/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritServer.java @@ -102,6 +102,7 @@ import com.sonymobile.tools.gerrit.gerritevents.GerritConnection; import com.sonymobile.tools.gerrit.gerritevents.GerritEventSource; import com.sonymobile.tools.gerrit.gerritevents.GerritRestPoller; +import com.sonymobile.tools.gerrit.gerritevents.GerritRestQueryHandler; import com.sonymobile.tools.gerrit.gerritevents.dto.GerritEvent; import com.sonymobile.tools.gerrit.gerritevents.dto.rest.Notify; import com.sonymobile.tools.gerrit.gerritevents.ssh.Authentication; @@ -264,7 +265,11 @@ public void setConfig(IGerritHudsonTriggerConfig config) { */ public GerritQueryHandler getQueryHandler() { if (gerritQueryHnadler == null) { - gerritQueryHnadler = new GerritQueryHandler(config); + if (config.isUseHttpsPoller()) { + gerritQueryHnadler = new GerritRestQueryHandler(config); + } else { + gerritQueryHnadler = new GerritQueryHandler(config); + } } return gerritQueryHnadler; } diff --git a/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/hudsontrigger/actions/manual/ManualTriggerAction.java b/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/hudsontrigger/actions/manual/ManualTriggerAction.java index 5cceb1b6e..72e2355ce 100644 --- a/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/hudsontrigger/actions/manual/ManualTriggerAction.java +++ b/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/hudsontrigger/actions/manual/ManualTriggerAction.java @@ -153,6 +153,9 @@ private boolean isServerEnabled(String serverName) { * @return the config of the server or null if config not found. */ private IGerritHudsonTriggerConfig getServerConfig(String serverName) { + if (serverName == null) { + return null; + } GerritServer server = PluginImpl.getServer_(serverName); if (server != null) { IGerritHudsonTriggerConfig config = server.getConfig(); @@ -325,7 +328,13 @@ public void doGerritSearch(@QueryParameter("queryString") final String queryStri IGerritHudsonTriggerConfig config = getServerConfig(selectedServer); if (config != null) { - GerritQueryHandler handler = new GerritQueryHandler(config); + GerritServer server = PluginImpl.getServer_(selectedServer); + GerritQueryHandler handler = null; + if (server != null) { + handler = server.getQueryHandler(); + } else { + handler = new GerritQueryHandler(config); + } clearSessionData(session); session.setAttribute("queryString", queryString); From c6137db966efe64a5dc95a2dcbb6c6ca788301c4 Mon Sep 17 00:00:00 2001 From: Michael Trimarchi Date: Sat, 27 Jun 2026 21:22:15 +0200 Subject: [PATCH 05/18] Add HTTPS project list loading to GerritProjectListUpdater When config.isUseHttpsPoller() is true, use the Gerrit REST API GET /a/projects/?d endpoint instead of SSH 'gerrit ls-projects' to fetch the project list for auto-completion. Changes: - Extract SSH path into loadProjectsViaSsh() private method - Add loadProjectsViaRest() using Apache HttpClient with Basic Auth - Add readProjectsFromJson() utility to parse REST API project JSON (the response is a JSON object where keys are project names) - Handles Gerrit REST JSON hijacking prefix ()]}') - Respects existing connection/wait logic unchanged Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Michael Trimarchi --- .../trigger/GerritProjectListUpdater.java | 126 ++++++++++++++++-- 1 file changed, 118 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritProjectListUpdater.java b/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritProjectListUpdater.java index 51ae6e2de..fdd14e464 100644 --- a/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritProjectListUpdater.java +++ b/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritProjectListUpdater.java @@ -34,14 +34,29 @@ import com.sonyericsson.hudson.plugins.gerrit.trigger.config.IGerritHudsonTriggerConfig; import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStreamReader; import java.io.Reader; +import java.net.HttpURLConnection; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import net.sf.json.JSONObject; +import net.sf.json.JSONSerializer; + +import org.apache.http.HttpResponse; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.Credentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.HttpClients; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -264,20 +279,22 @@ private void tryLoadProjectList() { if (isConnected()) { logger.info("Trying to load project list."); IGerritHudsonTriggerConfig activeConfig = getConfig(); - SshConnection sshConnection = SshConnectionFactory.getConnection( - activeConfig.getGerritHostName(), - activeConfig.getGerritSshPort(), - activeConfig.getGerritProxy(), - activeConfig.getGerritAuthentication() - ); - List projects = readProjects(sshConnection.executeCommandReader(GERRIT_LS_PROJECTS)); + if (activeConfig == null) { + logger.error("Could not load project list: config is null for {}", serverName); + return; + } + List projects; + if (activeConfig.isUseHttpsPoller()) { + projects = loadProjectsViaRest(activeConfig); + } else { + projects = loadProjectsViaSsh(activeConfig); + } if (!projects.isEmpty()) { setGerritProjects(projects); logger.info("Project list from {} contains {} entries", serverName, projects.size()); } else { logger.warn("Project list from {} contains 0 projects", serverName); } - sshConnection.disconnect(); } else { logger.warn("Could not connect to Gerrit server when updating Gerrit project list: " + "Server is not connected (timeout)"); @@ -289,6 +306,99 @@ private void tryLoadProjectList() { } } + /** + * Loads the project list via the SSH {@code gerrit ls-projects} command. + * + * @param activeConfig the server configuration. + * @return the list of project names. + * @throws SshException if an SSH error occurs. + * @throws IOException if an I/O error occurs. + */ + private List loadProjectsViaSsh(IGerritHudsonTriggerConfig activeConfig) + throws SshException, IOException { + SshConnection sshConnection = SshConnectionFactory.getConnection( + activeConfig.getGerritHostName(), + activeConfig.getGerritSshPort(), + activeConfig.getGerritProxy(), + activeConfig.getGerritAuthentication() + ); + try { + return readProjects(sshConnection.executeCommandReader(GERRIT_LS_PROJECTS)); + } finally { + sshConnection.disconnect(); + } + } + + /** + * Loads the project list via the Gerrit REST API. + * + * @param activeConfig the server configuration. + * @return the list of project names. + * @throws IOException if an I/O error occurs. + */ + private List loadProjectsViaRest(IGerritHudsonTriggerConfig activeConfig) + throws IOException { + String frontEndUrl = activeConfig.getGerritFrontEndUrl(); + StringBuilder urlBuilder = new StringBuilder(frontEndUrl); + if (!frontEndUrl.endsWith("/")) { + urlBuilder.append('/'); + } + urlBuilder.append("a/projects/?d"); + + Credentials httpCredentials = activeConfig.getHttpCredentials(); + CredentialsProvider credsProvider = new BasicCredentialsProvider(); + credsProvider.setCredentials(AuthScope.ANY, httpCredentials); + HttpClient httpClient = HttpClients.custom() + .setDefaultCredentialsProvider(credsProvider) + .build(); + HttpGet httpGet = new HttpGet(urlBuilder.toString()); + HttpResponse response = httpClient.execute(httpGet); + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode != HttpURLConnection.HTTP_OK) { + throw new IOException("HTTP " + statusCode + " for project list query"); + } + + StringBuilder sb = new StringBuilder(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(response.getEntity().getContent(), + StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + } + String body = sb.toString(); + // Strip Gerrit JSON hijacking prevention prefix + if (body.startsWith(")]}'")) { + body = body.substring(")]}'".length()); + } + return readProjectsFromJson(body); + } + + /** + * Reads project names from a Gerrit REST API projects JSON response. + * The response format is a JSON object where each key is a project name. + * + * @param jsonBody the JSON response body. + * @return the list of project names. + * @throws IOException if JSON parsing fails. + */ + static List readProjectsFromJson(String jsonBody) throws IOException { + List projects = new ArrayList(); + try { + JSONObject json = (JSONObject)JSONSerializer.toJSON(jsonBody); + for (Iterator it = json.keys(); it.hasNext();) { + String key = (String)it.next(); + if (key != null && !key.isEmpty()) { + projects.add(key); + } + } + } catch (Exception ex) { + throw new IOException("Failed to parse projects JSON response", ex); + } + return projects; + } + /** * Get the the server config. * @return the server config or null if config not found. From 6ec28cd18fa9eb854ea31043d21b3fee0c63a490 Mon Sep 17 00:00:00 2001 From: Michael Trimarchi Date: Fri, 19 Jun 2026 21:57:04 +0200 Subject: [PATCH 06/18] Add GERRIT_GIT_URL parameter for HTTPS git clone URL Add a computed environment parameter GERRIT_GIT_URL that provides the git-over-HTTPS clone URL for the Gerrit project being built. Format: {frontend_url}/a/{project}.git This enables users to configure their git SCM step in Jenkins pipelines to use HTTPS instead of SSH: Repository URL: Branch Specifier: Changes: - Add GERRIT_GIT_URL enum to GerritTriggerParameters - Add StringUtil.makeGerritGitUrl(frontEndUrl, project) utility - Add getGitURL() helper method (models the existing getURL() pattern) - Set the parameter value during ChangeBasedEvent parameter creation Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Michael Trimarchi --- .../GerritTriggerParameters.java | 50 +++++++++++++++++++ .../gerrit/trigger/utils/StringUtil.java | 13 +++++ 2 files changed, 63 insertions(+) diff --git a/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/hudsontrigger/GerritTriggerParameters.java b/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/hudsontrigger/GerritTriggerParameters.java index 58a12a58f..90ea0c170 100644 --- a/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/hudsontrigger/GerritTriggerParameters.java +++ b/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/hudsontrigger/GerritTriggerParameters.java @@ -266,6 +266,11 @@ public enum GerritTriggerParameters { * The version of the Gerrit instance. */ GERRIT_VERSION, + /** + * Parameter name for the git-over-HTTPS clone URL of the project. + * Format: {frontend_url}/a/{project}.git + */ + GERRIT_GIT_URL, /** * A hashcode of the Gerrit event object, to make sure every set of parameters * is unique (allowing jenkins to queue duplicate builds). @@ -557,6 +562,9 @@ private static void setOrCreateParametersForChangeBasedEvent(ChangeBasedEvent ev } GERRIT_CHANGE_URL.setOrCreateStringParameterValue( parameters, url, escapeQuotes); + String gitUrl = getGitURL(event, project); + GERRIT_GIT_URL.setOrCreateStringParameterValue( + parameters, gitUrl, escapeQuotes); if (event instanceof ChangeAbandoned) { nameAndEmailParameterMode.setOrCreateParameterValue(GERRIT_CHANGE_ABANDONER, parameters, getNameAndEmail(((ChangeAbandoned)event).getAbandoner()), @@ -680,6 +688,48 @@ private static String getURL(ChangeBasedEvent event, Job project) { return url; } + /** + * Get the git clone URL for the project from a ChangeBasedEvent. + * Format: {frontend_url}/a/{project}.git + * + * @param event the event + * @param project the project for which the parameters are being set + * @return the git clone URL + */ + private static String getGitURL(ChangeBasedEvent event, Job project) { + String url = ""; + String serverName = null; + //Figure out what serverName to use + if (event.getProvider() != null) { + serverName = event.getProvider().getName(); + } else if (project != null) { + GerritTrigger trigger = GerritTrigger.getTrigger(project); + if (trigger != null) { + String name = trigger.getServerName(); + if (!GerritServer.ANY_SERVER.equals(name)) { + serverName = name; + } + } + } + GerritServer firstServer = PluginImpl.getFirstServer_(); + if (serverName == null && firstServer != null) { + serverName = firstServer.getName(); + } else if (serverName == null) { + return ""; + } + + GerritServer server = PluginImpl.getServer_(serverName); + if (server != null) { + IGerritHudsonTriggerConfig config = server.getConfig(); + if (config != null && event.getChange() != null) { + url = StringUtil.makeGerritGitUrl( + config.getGerritFrontEndUrl(), + event.getChange().getProject()); + } + } + return url; + } + /** * There are two uploader fields in the event, this method gets one of them if one is null. * diff --git a/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/utils/StringUtil.java b/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/utils/StringUtil.java index ea91315be..ceacf768d 100644 --- a/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/utils/StringUtil.java +++ b/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/utils/StringUtil.java @@ -29,6 +29,7 @@ import com.sonymobile.tools.gerrit.gerritevents.GerritEventListener; import com.sonymobile.tools.gerrit.gerritevents.dto.events.ChangeBasedEvent; import java.util.regex.Pattern; +import hudson.Functions; /** * Various string making utility methods. @@ -117,6 +118,18 @@ public static String getPluginJsUrl(String jsName) { return PLUGIN_JS_URL + jsName; } + /** + * Creates a Gerrit git clone URL from the frontend URL and project name. + * Gerrit exposes git repositories at {@code {frontend_url}/a/{project}.git}. + * + * @param frontEndUrl the Gerrit front-end URL (with or without trailing slash). + * @param project the Gerrit project name. + * @return the git clone URL. + */ + public static String makeGerritGitUrl(String frontEndUrl, String project) { + return Functions.joinPath(frontEndUrl, "a/" + project + ".git"); + } + /** * Escape quotes in String value. * @param value the name of String object having quotes. From 85b7a757e9dd3112234f5a07b48c133037d115c1 Mon Sep 17 00:00:00 2001 From: Michael Trimarchi Date: Sat, 20 Jun 2026 07:49:08 +0200 Subject: [PATCH 07/18] Add client-side JavaScript to toggle SSH fields dynamically Replace server-side conditions with CSS classes and JavaScript so that toggling 'Use HTTPS Polling for Events' immediately shows/hides SSH-specific fields without requiring a page save and reload. - Wrap SSH fields in
with initial state set via inline style based on the saved config value - Wrap Test Connection button in
- Add CDATA-wrapped JavaScript that listens on the useHttpsPoller checkbox change event and toggles display:none on both sections - Fields that toggle: SSH Port, Proxy, Username, SSH Keyfile, SSH Keyfile Password, Test Connection button Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Michael Trimarchi --- .../gerrit/trigger/GerritServer/index.jelly | 61 ++++++++++--------- src/main/webapp/js/https-poller-toggle.js | 20 ++++++ 2 files changed, 53 insertions(+), 28 deletions(-) create mode 100644 src/main/webapp/js/https-poller-toggle.js diff --git a/src/main/resources/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritServer/index.jelly b/src/main/resources/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritServer/index.jelly index 38f138953..eb501e897 100644 --- a/src/main/resources/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritServer/index.jelly +++ b/src/main/resources/com/sonyericsson/hudson/plugins/gerrit/trigger/GerritServer/index.jelly @@ -5,6 +5,7 @@