diff --git a/src/main/java/org/jenkinsci/plugins/p4/PerforceScm.java b/src/main/java/org/jenkinsci/plugins/p4/PerforceScm.java index f37752589..85278e43a 100644 --- a/src/main/java/org/jenkinsci/plugins/p4/PerforceScm.java +++ b/src/main/java/org/jenkinsci/plugins/p4/PerforceScm.java @@ -47,6 +47,7 @@ import org.jenkinsci.plugins.p4.changes.P4ChangeSet; import org.jenkinsci.plugins.p4.changes.P4GraphRef; import org.jenkinsci.plugins.p4.changes.P4LabelRef; +import org.jenkinsci.plugins.p4.changes.P4PollRef; import org.jenkinsci.plugins.p4.changes.P4Ref; import org.jenkinsci.plugins.p4.client.ConnectionHelper; import org.jenkinsci.plugins.p4.credentials.P4BaseCredentials; @@ -530,10 +531,13 @@ private List lookForChanges(FilePath buildWorkspace, Workspace ws, Run lastPollPathRefs = TagAction.getLastPollChange(lastRun, listener, syncID); + // Create task PollTask task = new PollTask(credential, lastRun, listener, filter, lastRefs); task.setWorkspace(ws); task.setLimit(pin); + task.setPollRefChanges(lastPollPathRefs); // Execute remote task List changes = buildWorkspace.act(task); @@ -654,6 +658,7 @@ To help lookForChanges() find the correct change to build, sending it the previo TagAction tag = new TagAction(run, credential); tag.setWorkspace(ws); tag.setRefChanges(task.getSyncChange()); + tag.setPollPathChanges(task.resolvePollPathsToLatestChanges()); // JENKINS-37442: Make the log file name available tag.setChangelog(changelogFile); // JENKINS-39107: Make Depot location of Jenkins file available diff --git a/src/main/java/org/jenkinsci/plugins/p4/changes/P4PollRef.java b/src/main/java/org/jenkinsci/plugins/p4/changes/P4PollRef.java new file mode 100644 index 000000000..428f9dcd3 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/p4/changes/P4PollRef.java @@ -0,0 +1,79 @@ +package org.jenkinsci.plugins.p4.changes; + +import com.perforce.p4java.core.IChangelistSummary; +import com.perforce.p4java.core.file.IFileSpec; +import org.jenkinsci.plugins.p4.client.ClientHelper; +import org.jenkinsci.plugins.p4.client.ConnectionHelper; + +import java.util.List; + +public class P4PollRef implements P4Ref { + + private final long change; + private final String pollPath; + + public P4PollRef(long change, String pollPath) { + this.change = change; + this.pollPath = pollPath; + } + + @Override + public P4ChangeEntry getChangeEntry(ClientHelper p4) throws Exception { + P4ChangeEntry cl = new P4ChangeEntry(); + IChangelistSummary summary = p4.getChangeSummary(change); + cl.setChange(p4, summary); + return cl; + } + + @Override + public boolean isLabel() { + return false; + } + + @Override + public boolean isCommit() { + return false; + } + + @Override + public long getChange() { + return change; + } + + public String getPollPath() { + return pollPath; + } + + @Override + public List getFiles(ConnectionHelper p4, int limit) throws Exception { + return p4.getChangeFiles(change, limit); + } + + @Override + public String toString() { + return Long.toString(change); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof P4PollRef) { + P4PollRef ref = (P4PollRef) obj; + if (ref.getPollPath().equals(this.pollPath) && ref.getChange()==this.change) { + return true; + } + } + return false; + } + + @Override + public int compareTo(Object obj) { + if (equals(obj)) { + return 0; + } + if (obj instanceof P4PollRef) { + P4PollRef ref = (P4PollRef) obj; + return (int) (change - ref.getChange()); + } + throw new ClassCastException(); + } +} diff --git a/src/main/java/org/jenkinsci/plugins/p4/client/ClientHelper.java b/src/main/java/org/jenkinsci/plugins/p4/client/ClientHelper.java index b92a2f53a..04453370e 100644 --- a/src/main/java/org/jenkinsci/plugins/p4/client/ClientHelper.java +++ b/src/main/java/org/jenkinsci/plugins/p4/client/ClientHelper.java @@ -1291,12 +1291,12 @@ public List listChanges(List fromRefs, P4Ref to) throws Exception // return empty array, if from and to are equal, or Perforce will report // a change if (from.equals(to)) { - return new ArrayList(); + return new ArrayList<>(); } // JENKINS-68516: skip changelist calculation if maxChanges=0. if (getMaxChangeLimit() <= 0) { - return new ArrayList(); + return new ArrayList<>(); } String ws = "//" + iclient.getName() + "/...@" + from + "," + to; @@ -1346,7 +1346,7 @@ public List listChanges() throws Exception { } private List listChanges(String ws) throws Exception { - List list = new ArrayList(); + List list = new ArrayList<>(); GetChangelistsOptions opts = new GetChangelistsOptions(); opts.setMaxMostRecent(getMaxChangeLimit()); @@ -1406,7 +1406,7 @@ public List listHaveChanges(List fromRefs, P4Ref changeLimit) thro } // return empty array, if from and changeLimit are equal, or Perforce will report a change if (from.equals(changeLimit)) { - return new ArrayList(); + return new ArrayList<>(); } if (from.getChange() > 0) { @@ -1422,7 +1422,7 @@ public List listHaveChanges(List fromRefs, P4Ref changeLimit) thro private List listHaveChanges(String fileSpec) throws Exception { log("P4: Polling with cstat: " + fileSpec); - List haveChanges = new ArrayList(); + List haveChanges = new ArrayList<>(); Map[] map; map = getConnection().execMapCmd("cstat", new String[]{fileSpec}, null); diff --git a/src/main/java/org/jenkinsci/plugins/p4/client/ConnectionHelper.java b/src/main/java/org/jenkinsci/plugins/p4/client/ConnectionHelper.java index d8d508ab5..e96c7627b 100644 --- a/src/main/java/org/jenkinsci/plugins/p4/client/ConnectionHelper.java +++ b/src/main/java/org/jenkinsci/plugins/p4/client/ConnectionHelper.java @@ -35,7 +35,9 @@ import org.jenkinsci.plugins.p4.PerforceScm; import org.jenkinsci.plugins.p4.changes.P4GraphRef; import org.jenkinsci.plugins.p4.changes.P4LabelRef; +import org.jenkinsci.plugins.p4.changes.P4PollRef; import org.jenkinsci.plugins.p4.changes.P4Ref; +import org.jenkinsci.plugins.p4.changes.P4ChangeRef; import org.jenkinsci.plugins.p4.credentials.P4BaseCredentials; import java.io.IOException; @@ -43,6 +45,7 @@ import java.util.List; import java.util.ListIterator; import java.util.Map; +import java.util.Collections; import java.util.logging.Level; import java.util.logging.Logger; @@ -607,4 +610,36 @@ public long getHeadLimit() { public void close() throws Exception { disconnect(); } -} + + /** + * Retrieves the latest Perforce changelist for a given polling path since a specified changelist number. + * If no changes exist or the input is invalid, the method returns {@code null}. + * + * @param from the reference containing the polling path and starting changelist number; + * @return a {@link P4PollRef} representing the latest change found for the given path, + * or {@code null} if no changes are found or if the input reference is invalid. + * @throws Exception if an error occurs while retrieving the list of changes from Perforce. + */ + + public P4PollRef getLatestChangeForPollPath(P4PollRef from) throws Exception { + if (from == null || from.getChange() < 0) { + return null; + } + + String pollPath = from.getPollPath(); + String path = pollPath.endsWith("/...") + ? pollPath + "@" + from.getChange() + ",now" + : pollPath + "/...@" + from.getChange() + ",now"; + + List spec = FileSpecBuilder.makeFileSpecList(path); + GetChangelistsOptions opts = new GetChangelistsOptions(); + opts.setMaxMostRecent(1); + List changes = getConnection().getChangelists(spec, opts); + if (changes.isEmpty()) { + return null; + } + + P4PollRef finalChange = new P4PollRef(changes.get(0).getId(), pollPath); + return from.equals(finalChange) ? null : finalChange; + } +} \ No newline at end of file diff --git a/src/main/java/org/jenkinsci/plugins/p4/tagging/TagAction.java b/src/main/java/org/jenkinsci/plugins/p4/tagging/TagAction.java index edaac9678..fe0cdc2db 100644 --- a/src/main/java/org/jenkinsci/plugins/p4/tagging/TagAction.java +++ b/src/main/java/org/jenkinsci/plugins/p4/tagging/TagAction.java @@ -10,6 +10,7 @@ import org.jenkinsci.plugins.p4.PerforceScm; import org.jenkinsci.plugins.p4.changes.P4ChangeRef; import org.jenkinsci.plugins.p4.changes.P4LabelRef; +import org.jenkinsci.plugins.p4.changes.P4PollRef; import org.jenkinsci.plugins.p4.changes.P4Ref; import org.jenkinsci.plugins.p4.changes.P4Revision; import org.jenkinsci.plugins.p4.client.ClientHelper; @@ -29,7 +30,9 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -41,6 +44,7 @@ public class TagAction extends AbstractScmTagAction { private List tags = new ArrayList(); private List refChanges; + private List pollRefChanges = new ArrayList<>(); private P4Revision buildChange; private P4Review review; @@ -298,6 +302,47 @@ public static List getLastChange(Run run, TaskListener listener, St return changes; } + public static List getLastPollChange(Run run, TaskListener listener, String syncID) { + // Use LinkedHashSet to preserve insertion order while deduplicating by + // P4PollRef.equals()/hashCode(). + Set result = new LinkedHashSet<>(); + + List actions = lastActions(run); + if (actions == null || syncID == null || syncID.isEmpty()) { + listener.getLogger().println("No previous build found..."); + return new ArrayList<>(); + } + + logger.fine(" using syncID: " + syncID); + + for (TagAction action : actions) { + if (!syncID.equals(action.getSyncID())) { + continue; + } + + List pollChanges = action.getPollPathChanges(); + if (pollChanges == null || pollChanges.isEmpty()) { + continue; + } + + for (P4PollRef poll : pollChanges) { + if (poll == null) { + continue; + } + String path = poll.getPollPath(); + if (path == null) { + continue; + } + // LinkedHashSet.add will ignore duplicates based on equals/hashCode + if (result.add(poll)) { + listener.getLogger().println("Found last poll change " + poll.toString() + " on syncID " + syncID + " path " + path); + } + } + } + + return new ArrayList<>(result); + } + /** * Find the last action; use this for environment variable as the last action has the latest values. * @@ -353,4 +398,21 @@ public void setJenkinsPath(String jenkinsPath) { public String getJenkinsPath() { return jenkinsPath; } + + public void setPollPathChanges(List finalPollPathList) { + List changes = new ArrayList<>(); + + for(P4Ref ref : finalPollPathList) { + if(ref != null) { + P4PollRef poll = new P4PollRef(ref.getChange(), ((P4PollRef)ref).getPollPath()); + changes.add(poll); + } + } + this.pollRefChanges = changes; + } + + public List getPollPathChanges() { + return this.pollRefChanges; + } + } diff --git a/src/main/java/org/jenkinsci/plugins/p4/tasks/CheckoutTask.java b/src/main/java/org/jenkinsci/plugins/p4/tasks/CheckoutTask.java index 3485fc386..76f8f6c34 100644 --- a/src/main/java/org/jenkinsci/plugins/p4/tasks/CheckoutTask.java +++ b/src/main/java/org/jenkinsci/plugins/p4/tasks/CheckoutTask.java @@ -14,13 +14,16 @@ import org.jenkinsci.plugins.p4.changes.P4ChangeEntry; import org.jenkinsci.plugins.p4.changes.P4ChangeRef; import org.jenkinsci.plugins.p4.changes.P4LabelRef; +import org.jenkinsci.plugins.p4.changes.P4PollRef; import org.jenkinsci.plugins.p4.changes.P4Ref; import org.jenkinsci.plugins.p4.client.ClientHelper; +import org.jenkinsci.plugins.p4.client.ConnectionHelper; import org.jenkinsci.plugins.p4.console.P4Logging; import org.jenkinsci.plugins.p4.populate.AutoCleanImpl; import org.jenkinsci.plugins.p4.populate.Populate; import org.jenkinsci.plugins.p4.review.ReviewProp; import org.jenkinsci.plugins.p4.workspace.Expand; +import org.jenkinsci.plugins.p4.workspace.ManualWorkspaceImpl; import org.jenkinsci.plugins.p4.workspace.Workspace; import org.jenkinsci.remoting.RoleChecker; import org.jenkinsci.remoting.RoleSensitive; @@ -29,8 +32,10 @@ import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.logging.Logger; +import java.util.stream.Collectors; public class CheckoutTask extends AbstractTask implements FileCallable, Serializable { @@ -387,4 +392,59 @@ public long getReview() { public void checkRoles(RoleChecker checker) throws SecurityException { checker.check((RoleSensitive) this, Roles.SLAVE); } + + + /** + * Resolve workspace-defined poll paths to their latest Perforce changes. + * + *

This method: + * - Only runs when the configured workspace is a {@link ManualWorkspaceImpl}. + * - Reads the comma-separated poll paths from the workspace spec, trims entries + * and ignores empty values. + * - For up to {@code MAX_PATHS_TO_CHECK} poll paths, queries Perforce for the latest + * changelist for each poll path using {@link ConnectionHelper#getLatestChangeForPollPath(P4PollRef)}. + * - Returns a list of non-null {@link P4Ref} objects (one per path with a found change) + * in the same order the paths were specified. + * + * @return list of latest per-path {@code P4PollRef} results (empty if none) + * @throws RuntimeException if an error occurs while querying Perforce (current behavior) + */ + public List resolvePollPathsToLatestChanges() { + final int MAX_PATHS_TO_CHECK = 10; + + List results = new ArrayList<>(); + Workspace ws = getWorkspace(); + + if (!(ws instanceof ManualWorkspaceImpl)) { + return results; + } + + String pollSpec = ((ManualWorkspaceImpl) ws).getSpec().getPollPath(); + if (pollSpec == null || pollSpec.trim().isEmpty()) { + return results; + } + + List paths = Arrays.stream(pollSpec.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList()); + + if (paths.isEmpty()) { + return results; + } + + try (ConnectionHelper p4 = new ConnectionHelper(getCredential(), getListener())) { + int limit = Math.min(MAX_PATHS_TO_CHECK, paths.size()); + for (int i = 0; i < limit; i++) { + P4PollRef ref = p4.getLatestChangeForPollPath(new P4PollRef(0, paths.get(i))); + if (ref != null) { + results.add(ref); + } + } + } catch (Exception ex) { + throw new RuntimeException("Error while processing polling paths", ex); + } + + return results; + } } diff --git a/src/main/java/org/jenkinsci/plugins/p4/tasks/PollTask.java b/src/main/java/org/jenkinsci/plugins/p4/tasks/PollTask.java index 7923602a5..484c7d5a0 100644 --- a/src/main/java/org/jenkinsci/plugins/p4/tasks/PollTask.java +++ b/src/main/java/org/jenkinsci/plugins/p4/tasks/PollTask.java @@ -12,6 +12,7 @@ import jenkins.security.Roles; import org.jenkinsci.plugins.p4.changes.P4ChangeRef; import org.jenkinsci.plugins.p4.changes.P4LabelRef; +import org.jenkinsci.plugins.p4.changes.P4PollRef; import org.jenkinsci.plugins.p4.changes.P4Ref; import org.jenkinsci.plugins.p4.client.ClientHelper; import org.jenkinsci.plugins.p4.filters.Filter; @@ -37,6 +38,7 @@ public class PollTask extends AbstractTask implements FileCallable>, private final List filter; private final List lastRefs; + private List lastPollRefs = new ArrayList<>(); private String pin; @@ -65,21 +67,6 @@ public Object task(ClientHelper p4) throws Exception { changes = p4.listHaveChanges(lastRefs); } - // filter changes... - List remainder = new ArrayList(); - for (P4Ref c : changes) { - long change = c.getChange(); - if (change > 0) { - Changelist changelist = p4.getChange(change); - // add unfiltered changes to remainder list - if (!filterChange(changelist, filter)) { - remainder.add(new P4ChangeRef(changelist.getId())); - p4.log("... found change: " + changelist.getId()); - } - } - } - changes = remainder; - // Poll Graph commit changes if (p4.checkVersion(20171)) { List repos = p4.listRepos(); @@ -94,6 +81,31 @@ public Object task(ClientHelper p4) throws Exception { } } + // Poll polling path changes + if (changes.isEmpty() && !lastPollRefs.isEmpty()) { + for (P4PollRef ref : lastPollRefs) { + P4PollRef changeRef = p4.getLatestChangeForPollPath(ref); + if (changeRef != null) { + changes.add(changeRef); + } + } + } + + // filter changes... + List remainder = new ArrayList<>(); + for (P4Ref c : changes) { + long change = c.getChange(); + if (change > 0) { + Changelist changelist = p4.getChange(change); + // add unfiltered changes to remainder list + if (!filterChange(changelist, filter)) { + remainder.add(new P4ChangeRef(changelist.getId())); + p4.log("... found change: " + changelist.getId()); + } + } + } + changes = remainder; + return changes; } @@ -101,6 +113,14 @@ public void setLimit(String expandedPin) { pin = expandedPin; } + public void setPollRefChanges(List pollRefs) { + this.lastPollRefs = pollRefs; + } + + public List getPollRefChanges() { + return lastPollRefs; + } + /** * Returns true if change should be filtered * diff --git a/src/main/java/org/jenkinsci/plugins/p4/workspace/WorkspaceSpec.java b/src/main/java/org/jenkinsci/plugins/p4/workspace/WorkspaceSpec.java index f780bfaed..34ef2670d 100644 --- a/src/main/java/org/jenkinsci/plugins/p4/workspace/WorkspaceSpec.java +++ b/src/main/java/org/jenkinsci/plugins/p4/workspace/WorkspaceSpec.java @@ -36,6 +36,16 @@ public class WorkspaceSpec extends AbstractDescribableImpl implem private final String serverID; private final boolean backup; + private String pollPath; + + public String getPollPath() { + return pollPath; + } + + @DataBoundSetter + public void setPollPath(String pollPath) { + this.pollPath = pollPath; + } public String getStreamName() { return streamName; diff --git a/src/main/resources/org/jenkinsci/plugins/p4/workspace/ManualWorkspaceImpl/config.jelly b/src/main/resources/org/jenkinsci/plugins/p4/workspace/ManualWorkspaceImpl/config.jelly index 9b5887d1a..8e49462b7 100644 --- a/src/main/resources/org/jenkinsci/plugins/p4/workspace/ManualWorkspaceImpl/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/p4/workspace/ManualWorkspaceImpl/config.jelly @@ -40,6 +40,7 @@ document.getElementById("id.stream").value = json.stream; document.getElementById("id.line").value = json.line; document.getElementById("id.view").value = json.view; + document.getElementById("id.pollPath").value = json.pollPath; }); } diff --git a/src/main/resources/org/jenkinsci/plugins/p4/workspace/WorkspaceSpec/config.jelly b/src/main/resources/org/jenkinsci/plugins/p4/workspace/WorkspaceSpec/config.jelly index 49050b51a..cdd423785 100644 --- a/src/main/resources/org/jenkinsci/plugins/p4/workspace/WorkspaceSpec/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/p4/workspace/WorkspaceSpec/config.jelly @@ -44,4 +44,36 @@ + + +

+ + + + \ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/p4/workspace/WorkspaceSpec/help-pollPath.html b/src/main/resources/org/jenkinsci/plugins/p4/workspace/WorkspaceSpec/help-pollPath.html new file mode 100644 index 000000000..8ef0e461e --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/p4/workspace/WorkspaceSpec/help-pollPath.html @@ -0,0 +1,4 @@ +
+ Polling Path +

Polling Path refers to specifying additional paths outside the client view mapping for polling. Once configured and saved, build the project to apply changes. These paths are registered with the project, and any detected changes during polling will automatically trigger a build. Only comma-separated values are allowed, with a maximum of 10 entries.

+
\ No newline at end of file