package org.jenkinsci.plugins.p4; import hudson.AbortException; import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.matrix.MatrixBuild; import hudson.matrix.MatrixRun; import hudson.model.BuildListener; import hudson.model.TaskListener; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Node; import hudson.remoting.VirtualChannel; import hudson.remoting.Callable; import hudson.scm.ChangeLogParser; import hudson.scm.PollingResult; import hudson.scm.SCMDescriptor; import hudson.scm.SCMRevisionState; import hudson.scm.SCM; import hudson.security.ACL; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Logger; import java.util.Properties; import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.acegisecurity.Authentication; import org.jenkinsci.plugins.p4.browsers.P4Browser; import org.jenkinsci.plugins.p4.changes.P4ChangeParser; import org.jenkinsci.plugins.p4.changes.P4ChangeSet; import org.jenkinsci.plugins.p4.changes.P4CBDChangeParser; import org.jenkinsci.plugins.p4.client.ClientHelper; import org.jenkinsci.plugins.p4.client.ConnectionHelper; import org.jenkinsci.plugins.p4.credentials.P4StandardCredentials; import org.jenkinsci.plugins.p4.filters.Filter; import org.jenkinsci.plugins.p4.filters.FilterPathImpl; import org.jenkinsci.plugins.p4.filters.FilterPerChangeImpl; import org.jenkinsci.plugins.p4.filters.FilterUserImpl; import org.jenkinsci.plugins.p4.populate.ForceCleanImpl; import org.jenkinsci.plugins.p4.populate.AutoCleanImpl; import org.jenkinsci.plugins.p4.populate.Populate; import org.jenkinsci.plugins.p4.tagging.TagAction; import org.jenkinsci.plugins.p4.workspace.Workspace; import org.jenkinsci.plugins.p4.populate.PopulateBuildBadgeAction; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.domains.DomainRequirement; import com.perforce.p4java.core.file.IFileSpec; import com.perforce.p4java.exception.AccessException; import com.perforce.p4java.exception.RequestException; import com.perforce.p4java.impl.generic.core.Changelist; import com.perforce.p4java.impl.generic.core.Label; public class PerforceScm extends SCM { private static Logger logger = Logger .getLogger(PerforceScm.class.getName()); private final String credential; private final Workspace workspace; private final List<Filter> filter; private final Populate populate; private final P4Browser browser; private final String cbdFileDepotLocation; // Strings used with build badges private static final String FORCE_CLEAN_BUILD = "Force Clean Build"; private static final String AUTO_CLEAN_AND_SYNC_BUILD = "Auto Clean Build"; public String getCredential() { return credential; } public Workspace getWorkspace() { return workspace; } public List<Filter> getFilter() { return filter; } public Populate getPopulate() { return populate; } @Override public P4Browser getBrowser() { return browser; } public String getCbdFileDepotLocation() { return cbdFileDepotLocation; } /** * Create a constructor that takes non-transient fields, and add the * annotation @DataBoundConstructor to it. Using the annotation helps the * Stapler class to find which constructor that should be used when * automatically copying values from a web form to a class. */ @DataBoundConstructor public PerforceScm(String credential, Workspace workspace, List<Filter> filter, Populate populate, P4Browser browser, String cbdFileDepotLocation) { this.credential = credential; this.workspace = workspace; this.filter = filter; this.populate = populate; this.browser = browser; this.cbdFileDepotLocation = cbdFileDepotLocation; } public PerforceScm(String credential, Workspace workspace, Populate populate) { this.credential = credential; this.workspace = workspace; this.filter = null; this.populate = populate; this.browser = null; this.cbdFileDepotLocation = ""; } /** * Calculate the state of the workspace of the given build. The returned * object is then fed into compareRemoteRevisionWith as the baseline * SCMRevisionState to determine if the build is necessary, and is added to * the build as an Action for later retrieval. */ @Override public SCMRevisionState calcRevisionsFromBuild(AbstractBuild<?, ?> build, Launcher launcher, TaskListener listener) throws IOException, InterruptedException { // Method not required by Perforce return null; } /** * This method does the actual polling and returns a PollingResult. The * change attribute of the PollingResult the significance of the changes * detected by this poll. */ @Override protected PollingResult compareRemoteRevisionWith( AbstractProject<?, ?> project, Launcher launcher, FilePath buildWorkspace, TaskListener listener, SCMRevisionState baseline) throws IOException, InterruptedException { PollingResult state = PollingResult.NO_CHANGES; PrintStream log = listener.getLogger(); AbstractBuild<?, ?> build = project.getLastBuild(); PerforceScm scm = (PerforceScm) build.getProject().getScm(); String scmCredential = scm.getCredential(); Populate scmPopulate = scm.getPopulate(); List<Filter> scmFilter = scm.getFilter(); // CAUTION: scmWorkspace environment will have limited access to the // environment variables (e.g. NODE_NAME is missing). Instead get the // expanded client workspace name from the last build. String client = "unset"; try { EnvVars envVars = build.getEnvironment(null); client = envVars.get("P4_CLIENT"); log.println("P4: Polling with client: " + client); } catch (Exception e) { logger.warning("P4: Unable to read P4_CLIENT"); return PollingResult.NO_CHANGES; } ClientHelper p4 = new ClientHelper(scmCredential, listener, client); try { // expand the label if required String pin = scmPopulate.getPin(); Workspace scmWorkspace = setEnvironment(build, listener); // find changes... List<Object> changes = new ArrayList<Object>(); if (pin != null && !pin.isEmpty()) { pin = scmWorkspace.expand(pin); List<Integer> have = p4.listHaveChanges(pin); int last = 0; if (!have.isEmpty()) { last = have.get(have.size() - 1); } log.println("P4: Polling with label/change: " + last + "," + pin); changes = p4.listChanges(last, pin); } else { List<Integer> have = p4.listHaveChanges(); int last = 0; if (!have.isEmpty()) { last = have.get(have.size() - 1); } log.println("P4: Polling with label/change: " + last + ",now"); changes = p4.listChanges(last); } // filter changes... List<Integer> remainder = new ArrayList<Integer>(); for (Object c : changes) { if (c instanceof Integer) { Changelist changelist = p4.getChange((Integer) c); // add unfiltered changes to remainder list if (!filterChange(changelist, scmFilter)) { remainder.add(changelist.getId()); log.println("... found change: " + changelist.getId()); } } } // if there is a remainder... if (!remainder.isEmpty()) { // if Poll per change, use lowest change to pin build. if (scmFilter != null) { for (Filter f : scmFilter) { if (f instanceof FilterPerChangeImpl) { FilterPerChangeImpl perChange = (FilterPerChangeImpl) f; if (perChange.isPerChange()) { int lowest = remainder .get(remainder.size() - 1); perChange.setNextChange(lowest); } } } } state = PollingResult.BUILD_NOW; } // if the workspace is out of date... if (p4.updateFiles()) { state = PollingResult.BUILD_NOW; } } catch (Exception e) { logger.severe("P4: Polling Error: " + e); e.printStackTrace(); return null; } finally { // close connection p4.disconnect(); } return state; } /** * Returns true if change should be filtered * * @param changelist * @return * @throws AccessException * @throws RequestException * @throws Exception */ private boolean filterChange(Changelist changelist, List<Filter> scmFilter) throws Exception { // exit early if no filters if (scmFilter == null) { return false; } String user = changelist.getUsername(); List<IFileSpec> files = changelist.getFiles(true); for (Filter f : scmFilter) { // Scan through User filters if (f instanceof FilterUserImpl) { // return is user matches filter String u = ((FilterUserImpl) f).getUser(); if (u.equalsIgnoreCase(user)) { return true; } } // Scan through Path filters if (f instanceof FilterPathImpl) { // add unmatched files to remainder list List<IFileSpec> remainder = new ArrayList<IFileSpec>(); String path = ((FilterPathImpl) f).getPath(); for (IFileSpec s : files) { String p = s.getDepotPathString(); if (!p.startsWith(path)) { remainder.add(s); } } // update files with remainder files = remainder; // add if all files are removed then remove change if (files.isEmpty()) { return true; } } } return false; } /* Allow boolean build parameters of P4_FORCE_CLEAN_AND_SYNC and P4_AUTO_CLEAN_AND_SYNC to override * the job configuration if they are set. */ private static Populate getPopulateOptionForThisBuild(AbstractBuild build, Populate origPopulate) { Map<String, String> buildVariables = build.getBuildVariables(); if (buildVariables != null) { if (buildVariables.containsKey("P4_FORCE_CLEAN_AND_SYNC")) { String forceClean = buildVariables.get("P4_FORCE_CLEAN_AND_SYNC"); if (forceClean.equals("true")) { // Will set this forced clean to populate the "have" list too. Populate populate = new ForceCleanImpl(true, null); return populate; } } if (buildVariables.containsKey("P4_AUTO_CLEAN_AND_SYNC")) { String autoClean = buildVariables.get("P4_AUTO_CLEAN_AND_SYNC"); if (autoClean.equals("true")) { // This auto clean will replace missing/modified files and delete generated files. Populate populate = new AutoCleanImpl(true, true, null); return populate; } } } return origPopulate; } private void setBuildBadges(final AbstractBuild buildToAddBadgeTo, Populate scmPopulate) { // Set an action related to the populate option which will be carried out with this build. // Just pass in the icon file name, so that the jelly files can set the full path according to the size of icon required. if (scmPopulate instanceof ForceCleanImpl) { buildToAddBadgeTo.addAction(new PopulateBuildBadgeAction("totalclean.png", FORCE_CLEAN_BUILD)); } else if (scmPopulate instanceof AutoCleanImpl) { buildToAddBadgeTo.addAction(new PopulateBuildBadgeAction("cleanbuild.png", AUTO_CLEAN_AND_SYNC_BUILD)); } } /** * The checkout method is expected to check out modified files into the * project workspace. In Perforce terms a 'p4 sync' on the project's * workspace. Authorisation */ @Override public boolean checkout(AbstractBuild<?, ?> build, Launcher launcher, FilePath buildWorkspace, BuildListener listener, File changelogFile) throws IOException, InterruptedException { boolean success = true; PrintStream log = listener.getLogger(); VirtualChannel channel = buildWorkspace.getChannel(); // Get some properties of the machine associated with this workspace/channel, which will run this part of the build MachineProperties machineProperties = setBuildMachineProperties(channel, listener); // Now set a string to represent the OS requirements for any CBD related commands to be run String osStringForCBD = getBuildMachineOSType(machineProperties); if (build instanceof MatrixBuild) { // Use allOS option for MatrixBuild so any CBD commands will cover all OS. osStringForCBD = "allOS"; } log.println("Setting OS option for CBD commands to: " + osStringForCBD); Workspace scmWorkspace = setEnvironment(build, listener); scmWorkspace.setRootPath(buildWorkspace.getRemote()); scmWorkspace.setOSStringForCBD(osStringForCBD); String scmCredential = getCredential(); Populate scmPopulate = getPopulateOptionForThisBuild(build, getPopulate()); // Set up build badges to indicate the populate option used with this build on the web pages setBuildBadges(build, scmPopulate); // Create task CheckoutTask task; task = new CheckoutTask(scmCredential, scmWorkspace, listener); // Get CBD file (and revision of that file) to be used in this build. // MatrixRun builds should use the same CBD file data as the parent build (so all configurations use the same code) // NOTE: CBD file data needs to be set up before calling task.setBuildOpts if (build instanceof MatrixRun) { task.setCBDFileRevisionFromParentBuild(build); } else { task.setCBDFileRevision(cbdFileDepotLocation); } task.setPopulateOpts(scmPopulate); task.setBuildOpts(scmWorkspace); log.println("Latest relevant change number: " + task.getBuildChange()); if (build instanceof MatrixRun) { // MatrixRun builds should run using the same buildChange value as used in the parent build // (therefore all build configurations in a run use the same changes) // Get buildChange value from tagAction of parent build MatrixBuild parentBuild = ((MatrixRun) build).getParentBuild(); TagAction tagAction = parentBuild.getAction(TagAction.class); if (tagAction != null) { if (tagAction.getBuildChange() != null) { log.println("Using change number from parent build: " + getChangeNumber(tagAction)); // Set this buildChange value in CheckoutTask // And then this buildChange value will be used in the TagAction added just below task.setBuildChange(tagAction.getBuildChange()); } else { log.println("WARNING: Failed to get change number from parent build!"); } } else { log.println("WARNING: Failed to find parent build tagAction, so not using change number from parent build!"); } } // Add tagging action to build, enabling label support. TagAction tag = new TagAction(build); tag.setClient(scmWorkspace.getFullName()); tag.setCredential(scmCredential); tag.setBuildChange(task.getBuildChange()); tag.setCbdFile(task.getCbdFileWithRevision()); tag.setCbdFileHeadChange(task.getCbdFileHeadChange()); build.addAction(tag); // Invoke build (i.e. sync workspace). // Don't get a copy of the files for the parent part of the matrix build // FOUNDRY_TODO: This breaks the polling mechanism - needs to be fixed! if (!(build instanceof MatrixBuild)) { success &= buildWorkspace.act(task); } // Only write change log if build (sync) succeeds. if (success) { // Calculate changes prior to build (based on last build) if (task.getCbdFileWithRevision().equals("")) { // Non-CBD workflow List<Object> changes = calculateChanges(build, task, listener); P4ChangeSet.store(changelogFile, changes); } else { // CBD-workflow. Calculate the changelog using our "wschanges" command. String changelog = calculateChangesCBD(build, task, listener); //String changelog = calculateFakeChangesCBD(build, task, listener); if (changelog.equals("")){ // Something went wrong in generating the changelog log.println("Warning - Empty change log - first build of job?"); } // Store the xml returned in the changelog file try { FileWriter fileToWriteTo = new FileWriter(changelogFile); fileToWriteTo.write(changelog); fileToWriteTo.close(); } catch (IOException e) { log.println("IOException when writing to changelog file: " + e); } } } else { String msg = "P4: Build failed"; logger.warning(msg); throw new AbortException(msg); } // FOR CBD WORKFLOW ONLY: Store a snapshot CBD file, so the build can be easily recreated at a later date. // NOTE: Snapshot creation and storage does not affect the return value of this function, // so even if this part of the process fails, the "build" will not fail. if (task.getCbdFileWithRevision().equals("")) { // Non-CBD workflow. Just return as normal. return success; } log.println("SNAPSHOT DETAILS - CBD FILE: " + task.getCbdFileWithRevision()+ " CHANGELIST: " + task.getBuildChange()); if (!(build instanceof MatrixRun)) { // Create the text for the snapshot CBD file String snapshotText = task.createSnapshotCBD(); if (snapshotText.equals("")) { log.println("Warning: Empty snapshot! Snapshot file will not be archived."); return success; } //log.println(snapshotText); // Store snapshotText into a file (file will be stored on the machine associated with "buildWorkspace") FilePath snapshotFile = channel.call(new CreateSnapshotFile(listener, buildWorkspace, machineProperties, snapshotText)); if (snapshotFile.getRemote().equals("")){ // Something went wrong in generating the snapshot file log.println("Warning - Something went wrong when creating the snapshot file!"); } else { if (snapshotFile.exists()) { // Now ensure the snapshot file is stored on "master" as an artifact - always! // "snapshot.xml" was created in the top of "buildWorkspace", and just want the file // in the archive - so no complicated paths seem to need to be required! Map<String, String> artifactMap = new HashMap<String, String>(); artifactMap.put("snapshot.xml", "snapshot.xml"); // NOTE: artifactMap.put(<path in archive>, <path in workspace>); build.getArtifactManager().archive(buildWorkspace, launcher, listener, artifactMap); } else { log.println("Snapshot file does not exist - file not archived!"); } } } return success; } private List<Object> calculateChanges(AbstractBuild<?, ?> build, CheckoutTask task, BuildListener listener) { List<Object> changes = new ArrayList<Object>(); AbstractBuild<?, ?> lastBuild = build.getPreviousBuild(); if (lastBuild != null) { TagAction lastTag = lastBuild.getAction(TagAction.class); if (lastTag != null) { Object lastChange = lastTag.getBuildChange(); if (lastChange != null) { // Old CBD functionality using "p4 changes" /* String lastCbdFileHeadChange = lastTag.getCbdFileHeadChange(); if (!lastCbdFileHeadChange.equals("")) { // CBD workflow - try to make sure "changes" relates to the correct CBD xml revision // (more details in CheckoutTask.getChanges) listener.getLogger().println("calculateChanges - lastChange set to: " + lastChange + " lastCbdFileHeadChange: " + lastCbdFileHeadChange); int lastCbdHeadChangeInt = Integer.parseInt(lastCbdFileHeadChange); if (lastChange instanceof Integer) { int lastChangeInt = (Integer) lastChange; if (lastCbdHeadChangeInt > lastChangeInt) { lastChange = lastCbdHeadChangeInt; listener.getLogger().println("Changes command will use CBD file head change from previous build as lower limit."); } } }*/ changes = task.getChanges(lastChange); } } } else { // No previous build, so add current changes.add(task.getBuildChange()); } return changes; } private String calculateChangesCBD(AbstractBuild<?, ?> build, CheckoutTask task, BuildListener listener) { String changelog = ""; // If there are no previous builds, then will just return an empty string. AbstractBuild<?, ?> lastBuild = build.getPreviousBuild(); if (lastBuild != null) { TagAction lastTag = lastBuild.getAction(TagAction.class); if (lastTag != null) { String previousBuildCBDFileWithRevision = lastTag.getCbdFile(); Object previousBuildChangelistNum = lastTag.getBuildChange(); changelog = task.getChangesCBD(previousBuildCBDFileWithRevision, previousBuildChangelistNum.toString()); } } return changelog; } private String calculateFakeChangesCBD(AbstractBuild<?, ?> build, CheckoutTask task, BuildListener listener) { // Call this function instead of "calculateChangesCBD" to create a fake change log. // Useful for testing out the xml parsing and formatting. String changelog = "<?xml version='1.0' encoding='UTF-8'?>\n"; changelog += "<changelog>\n"; changelog += "\t<addedchanges>\n"; changelog += "\t\t<entry>\n"; changelog += "\t\t\t<changenumber>173014</changenumber>\n"; changelog += "\t\t</entry>\n"; changelog += "\t\t<entry>\n"; changelog += "\t\t\t<changenumber>172997</changenumber>\n"; changelog += "\t\t</entry>\n"; changelog += "\t</addedchanges>\n"; changelog += "\t<removedchanges>\n"; changelog += "\t\t<entry>\n"; changelog += "\t\t\t<changenumber>172983</changenumber>\n"; changelog += "\t\t</entry>\n"; changelog += "\t</removedchanges>\n"; changelog += "\t<structurechanges>\n"; changelog += "\t\t<newmodule depotpath='//Foundry/Core/Build'/>\n"; changelog += "\t\t<removedmodule depotpath='//Foundry/Core/Math'/>\n"; changelog += "\t\t<changedmodule depotpath='//Foundry/Core/Base' codelinediffers='True' wspathdiffers='False'/>\n"; changelog += "\t\t<changedmodule depotpath='//Foundry/Core/Colour' codelinediffers='False' wspathdiffers='True'>\n"; changelog += "\t\t\t<addedsubdirs>\n"; changelog += "\t\t\t\t<subdir dir='include'/>\n"; changelog += "\t\t\t\t<subdir dir='test'/>\n"; changelog += "\t\t\t</addedsubdirs>\n"; changelog += "\t\t\t<removedsubdirs>\n"; changelog += "\t\t\t\t<subdir dir='src'/>\n"; changelog += "\t\t\t</removedsubdirs>\n"; changelog += "\t\t\t<addedsubfiles>\n"; changelog += "\t\t\t\t<subfile relativepath='SConscript.py'/>\n"; changelog += "\t\t\t\t<subfile relativepath='include/Colour/fnColour.h'/>\n"; changelog += "\t\t\t</addedsubfiles>\n"; changelog += "\t\t\t<removedsubfiles>\n"; changelog += "\t\t\t\t<subfile relativePath='src/fnColor4.cpp'/>\n"; changelog += "\t\t\t</removedsubfiles>\n"; changelog += "\t\t</changedmodule>\n"; changelog += "\t</structurechanges>\n"; changelog += "</changelog>"; return changelog; } private Workspace setEnvironment(AbstractBuild<?, ?> build, TaskListener listener) throws IOException, InterruptedException { Workspace scmWorkspace = (Workspace) getWorkspace().clone(); // load environments EnvVars envVars = build.getEnvironment(listener); scmWorkspace.clear(); scmWorkspace.load(envVars); // Set label in map, if pinning is used. Populate scmPopulate = getPopulate(); String pin = scmPopulate.getPin(); if (pin != null && !pin.isEmpty()) { pin = scmWorkspace.expand(pin); scmWorkspace.set("label", pin); } // Set label to next change if perBuild is used if (filter != null) { for (Filter f : filter) { if (f instanceof FilterPerChangeImpl) { FilterPerChangeImpl perChange = (FilterPerChangeImpl) f; int next = perChange.getNextChange(); scmWorkspace.set("label", Integer.toString(next)); } } } return scmWorkspace; } @Override public void buildEnvVars(AbstractBuild<?, ?> build, Map<String, String> env) { super.buildEnvVars(build, env); TagAction tagAction = build.getAction(TagAction.class); if (tagAction != null) { // Set P4_CHANGELIST value if (tagAction.getBuildChange() != null) { String change = getChangeNumber(tagAction); env.put("P4_CHANGELIST", change); } // Set P4_CLIENT workspace value if (tagAction.getClient() != null) { String client = tagAction.getClient(); env.put("P4_CLIENT", client); } } } private String getChangeNumber(TagAction tagAction) { Object buildChange = tagAction.getBuildChange(); if (buildChange instanceof Integer) { // it already an Integer, so add change... String change = String.valueOf(buildChange); return change; } try { // it is really a change number, so add change... int change = Integer.parseInt((String) buildChange); return String.valueOf(change); } catch (NumberFormatException n) { } ConnectionHelper p4 = new ConnectionHelper(getCredential(), null); String name = (String) buildChange; try { Label label = p4.getLabel(name); String spec = label.getRevisionSpec(); if (spec != null && !spec.isEmpty()) { if (spec.startsWith("@")) { spec = spec.substring(1); } return spec; } else { // a label, but no RevisionSpec return name; } } catch (Exception e) { // not a label return name; } finally { p4.disconnect(); } } /** * The checkout method should, besides checking out the modified files, * write a changelog.xml file that contains the changes for a certain build. * The changelog.xml file is specific for each SCM implementation, and the * createChangeLogParser returns a parser that can parse the file and return * a ChangeLogSet. */ @Override public ChangeLogParser createChangeLogParser() { if (cbdFileDepotLocation.equals("")) { // Non-CBD job return new P4ChangeParser(); } else { return new P4CBDChangeParser(); } } /** * Called before a workspace is deleted on the given node, to provide SCM an * opportunity to perform clean up. */ @Override public boolean processWorkspaceBeforeDeletion( AbstractProject<?, ?> project, FilePath buildWorkspace, Node node) { PerforceScm scm = (PerforceScm) project.getScm(); String scmCredential = scm.getCredential(); AbstractBuild<?, ?> build = project.getLastBuild(); String client = "unset"; try { EnvVars envVars = build.getEnvironment(null); client = envVars.get("P4_CLIENT"); } catch (Exception e) { logger.warning("P4: Unable to read P4_CLIENT"); return true; } ClientHelper p4 = new ClientHelper(scmCredential, null, client); try { ForceCleanImpl forceClean = new ForceCleanImpl(false, null); logger.info("P4: unsyncing client: " + client); p4.syncFiles(0, forceClean); } catch (Exception e) { logger.warning("P4: Not able to unsync client: " + client); return true; } finally { p4.disconnect(); } return true; } /** * Returns the ScmDescriptor<?> for the SCM object. The ScmDescriptor is * used to create new instances of the SCM. */ @Override public SCMDescriptor<PerforceScm> getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); /** * The relationship of Descriptor and SCM (the describable) is akin to class * and object. What this means is that the descriptor is used to create * instances of the describable. Usually the Descriptor is an internal class * in the SCM class named DescriptorImpl. The Descriptor should also contain * the global configuration options as fields, just like the SCM class * contains the configurations options for a job. * * @author pallen * */ @Extension public static class DescriptorImpl extends SCMDescriptor<PerforceScm> { /** * public no-argument constructor */ public DescriptorImpl() { super(PerforceScm.class, P4Browser.class); load(); } @Override public SCM newInstance(StaplerRequest req, JSONObject formData) throws FormException { PerforceScm scm = (PerforceScm) super.newInstance(req, formData); return scm; } /** * Returns the name of the SCM, this is the name that will show up next * to CVS and Subversion when configuring a job. */ @Override public String getDisplayName() { return "Perforce Software"; } /** * The configure method is invoked when the global configuration page is * submitted. In the method the data in the web form should be copied to * the Descriptor's fields. To persist the fields to the global * configuration XML file, the save() method must be called. Data is * defined in the global.jelly page. * */ @Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException { save(); return true; } /** * Credentials list, a Jelly config method for a build job. * * @return A list of Perforce credential items to populate the jelly * Select list. */ public ListBoxModel doFillCredentialItems() { ListBoxModel list = new ListBoxModel(); Class<P4StandardCredentials> type = P4StandardCredentials.class; Jenkins scope = Jenkins.getInstance(); Authentication acl = ACL.SYSTEM; DomainRequirement domain = new DomainRequirement(); List<P4StandardCredentials> credentials; credentials = CredentialsProvider.lookupCredentials(type, scope, acl, domain); if (credentials.isEmpty()) { list.add("Select credential...", null); } for (P4StandardCredentials c : credentials) { StringBuffer sb = new StringBuffer(); sb.append(c.getDescription()); sb.append(" ("); sb.append(c.getUsername()); sb.append(":"); sb.append(c.getP4port()); sb.append(")"); list.add(sb.toString(), c.getId()); } return list; } public FormValidation doCheckCredential(@QueryParameter String value) { if (value == null) { return FormValidation.ok(); } try { ConnectionHelper p4 = new ConnectionHelper(value, null); if (!p4.login()) { return FormValidation .error("Authentication Error: Unable to login."); } if (!p4.checkVersion(20121)) { return FormValidation .error("Server version is too old (min 2012.1)"); } return FormValidation.ok(); } catch (Exception e) { return FormValidation.error(e.getMessage()); } } } /** * This methods determines if the SCM plugin can be used for polling */ @Override public boolean supportsPolling() { return true; } /** * This method should return true if the SCM requires a workspace for * polling. Perforce however can report submitted, pending and shelved * changes without needing a workspace */ @Override public boolean requiresWorkspaceForPolling() { return true; } /** * Returns a string representing the OS of the machine whose details are stored in "properties". * Response is in the form required by our "wscreate" command i.e. either "win", "mac" or "linux" * This is used to help make the client workspace OS specific. * @return String representing the OS of the machine with the given "properties". */ private String getBuildMachineOSType(MachineProperties properties) { String osType = ""; if (properties.getOSName().startsWith("Windows")){ osType = "win"; } else if (properties.getOSName().startsWith("Mac")){ osType = "mac"; } else { // FOUNDRY_TODO: Defaults to linux - is this ok? osType = "linux"; } return osType; } private class MachineProperties { private String osSeparator; private String osName; MachineProperties(){ osSeparator = ""; osName = ""; } void setProperties(String osSeparator, String osName){ this.osSeparator = osSeparator; this.osName = osName; } String getOSSeparator(){ return osSeparator; } String getOSName(){ return osName; } } private MachineProperties setBuildMachineProperties(VirtualChannel channel, BuildListener listener) throws IOException, InterruptedException { MachineProperties properties = new MachineProperties(); // Get properties from the machine associated with this channel Properties systemProperties = channel.call(new GetSystemProperties()); String osName = systemProperties.getProperty("os.name"); String osSeparator = systemProperties.getProperty("file.separator"); listener.getLogger().println("Build machine system property osName is set to: " + osName); listener.getLogger().println("Build machine system property osSeparator is set to: " + osSeparator); properties.setProperties(osSeparator, osName); return properties; } private static class GetSystemProperties implements Callable<Properties,RuntimeException> { public Properties call() { return System.getProperties(); } private static final long serialVersionUID = 1L; } private static class CreateSnapshotFile implements Callable<FilePath,RuntimeException> { private BuildListener listener; private FilePath workspace; private String osSeparator; private String snapshotText; public CreateSnapshotFile(BuildListener listener, FilePath workspace, MachineProperties machineProperties, String snapshotText) { this.listener = listener; this.workspace = workspace; this.osSeparator = machineProperties.getOSSeparator(); this.snapshotText = snapshotText; } public FilePath call(){ // Write snapshot info to a file String pathToSnapshotFile = workspace.getRemote() + osSeparator + "snapshot.xml"; listener.getLogger().println("CreateSnapshotFile - pathToSnapshotFile is : " + pathToSnapshotFile); boolean snapshotCreationError = false; BufferedWriter snapshotFile = null; try { snapshotFile = new BufferedWriter(new FileWriter(pathToSnapshotFile)); snapshotFile.write(snapshotText); } catch (IOException e) { listener.getLogger().println("IOException when writing snapshot file: " + e); snapshotCreationError = true; } finally { try { if (snapshotFile != null) { snapshotFile.close(); } } catch (IOException e) { listener.getLogger().println("IOException when closing snapshot file: " + e); snapshotCreationError = true; } } if (snapshotCreationError) { return new FilePath(new File("")); } else { return new FilePath(new File(pathToSnapshotFile)); } } private static final long serialVersionUID = 1L; } }
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#17 | 19234 | dawn_foundry |
Changelogs will now be generated even if the "sync" stage fails. Changelog pages will also display details of which build was compared against to create the changelogs. |
||
#16 | 19172 | dawn_foundry |
Adding options to caculate changelogs since the last clean, last successful, or last clean and successful build. Options are part of the job configuration. (TP 196933) |
||
#15 | 16582 | dawn_foundry | Adding support for subfile elements in the wschanges output. | ||
#14 | 16574 | dawn_foundry |
Call to wschanges needed to set the os correctly (to the os of the build slave that the build was going to run on). Note: This was required because the wschanges command runs on master Jenkins, rather than directly on the build slave. |
||
#13 | 15395 | dawn_foundry | Changelogs also need to display the added and removed module structure changes, which I appeared to have forgotten previously! | ||
#12 | 15235 | dawn_foundry |
Adding a new way to parse the CBD change logs from wschanges, so we can display all the details. Also added a function which creates a fake change log. Useful for testing the xml parsing. |
||
#11 | 15122 | dawn_foundry |
Changelog generation for CBD projects will now use our new "wschanges" command. Output from the command is xml which is written directly to the changelog file. The xml format has remained the same for the moment, but will need to be altered (and the parsing updated) to allow us to display more detailed data about added/removed modules etc. |
||
#10 | 11471 | dawn_foundry | Adding in build badges to indicate if a build was a "force clean" build or an "auto clean" build. | ||
#9 | 11083 | dawn_foundry | Don't attempt to create a snapshot file if not using the CBD workflow. | ||
#8 | 11073 | dawn_foundry | Adding in functionality for creating and archiving a "snapshot CBD" file with each build (Foundry-specific functionality). | ||
#7 | 10746 | dawn_foundry |
Now storing data about the head change value for the CBD xml file. This is used to ensure that the "changes" broker script accurately knows when the build changed to use a newer version of the CBD xml file. |
||
#6 | 10708 | dawn_foundry | Adding the ability to deal with The Foundry CBD files and the use of our "wscreate" command to create workspaces for the build. | ||
#5 | 10622 | dawn_foundry | Ensure that all configurations of a matrix build run using the same changelist number. | ||
#4 | 10604 | dawn_foundry | Integrating over all the latest changes from Paul. | ||
#3 | 10584 | dawn_foundry |
Stop sync of files for parent part of matrix build. But WARNING this does break the polling mechanism! (to be fixed later...) |
||
#2 | 10146 | dawn_foundry | Adding a couple of boolean build parameters that could be set to override the "Populate option" set in the job configuration. | ||
#1 | 10123 | dawn_foundry | Branching using dawn_foundry_p4jenkins | ||
//guest/perforce_software/p4jenkins/main/src/main/java/org/jenkinsci/plugins/p4/PerforceScm.java | |||||
#10 | 10084 | Paul Allen | Polling will trigger on 'cstat' OR 'sync -n'. | ||
#9 | 9990 | Paul Allen | Remove unnecessary setting of Root. | ||
#8 | 9984 | Paul Allen |
Set P4_CHANGELIST to change for Automatic Labels. P4_CHANGELIST is set to a change number or label if defined in Populate as pinned or a build parameter. |
||
#7 | 9980 | Paul Allen |
Changed error reporting to use AbortException. - Use direct access of scm attributes, not via build.getProject().getScm() - Improved logging around labels/changes. |
||
#6 | 9851 | Paul Allen | Merging using p4-jenkins | ||
#5 | 9819 | Paul Allen | Merging using p4-jenkins | ||
#4 | 9803 | Paul Allen | Merging using p4-jenkins | ||
#3 | 9769 | Paul Allen | Copying using p4-jenkins | ||
#2 | 9738 | Paul Allen | Merging using p4-jenkins | ||
#1 | 9690 | Paul Allen |
[Branching using p4-jenkins] Release 1.0.1 |
||
//guest/paul_allen/dev/p4-jenkins/p4-client/src/main/java/org/jenkinsci/plugins/p4/PerforceScm.java | |||||
#1 | 9672 | Paul Allen | Refactor name from 'p4_client' to 'p4'. | ||
//guest/paul_allen/dev/p4-jenkins/p4-client/src/main/java/org/jenkinsci/plugins/p4_client/PerforceScm.java | |||||
#32 | 9495 | Paul Allen |
Fix Polling issue; using the wrong client. Removed static classes and internal referenced to PerforceScm attributes, to avoid threading issues. |
||
#31 | 9472 | Paul Allen |
Added support to pin build at a label in the populate configuration. - includes help and updates to tests. |
||
#30 | 9430 | Paul Allen |
Polling SCM: skip filter check if no filters - added more logging for Polling errors. |
||
#29 | 9429 | Paul Allen |
Fix SCM Polling bug. Client workspace was not set (unless by an earlier build). - Move listChanges method to ClientHelper. |
||
#28 | 9115 | Paul Allen |
Initial implementation of workspace Cleanup and Sync options. - Includes 3 modes: Automatic Clean/Sync, Force Clean/Sync, Sync Only Automatic Clean/Sync Uses reconcile to clean up workspace and sync changes. Force Clean/Sync Force sync of all files (does not remove files yet...) Sync Only Normal sync with no cleanup TODO: - remove of files in Force Clean/Sync mode - Inline help - Update docs - Add unit/functional tests |
||
#27 | 9091 | Paul Allen |
Added Changelist build filtering for SCM polling: - Configuration uses 'repeatableHeteroProperty' - Filter on Perforce username - Filter on Perforce Depot path (no wildcard support) |
||
#26 | 9069 | Paul Allen |
Adding initial support for tagging Jenkins builds as Perforce Automatic Labels. Only implements TagAction (manual labels); TagNotifier and test cases TODO. |
||
#25 | 9055 | Paul Allen |
Label support. Build at a label using the pram 'label'. This includes adding the label to the ChangeEntry, building the change reports and Browser links to Swarm. (TPI-102) |
||
#24 | 9038 | Paul Allen |
Improved error messages for Form Validate field checks. Uses Perforce Server messages (TPI-80) |
||
#23 | 8969 | Paul Allen |
Adds all contributing change-lists for the build to the change log (using p4 cstat). - Includes exception logging for server connection to the Jenkins console. |
||
#22 | 8940 | Paul Allen |
Major refactor for the ConnectionHelper class to simplify serialisation. Fixed remote Jenkins JNLP slave connection issue. ClientHelper now extends ConnectionHelper and takes on all methods that require a client workspace. P4StandardCredentials is sent to the remote node instead of Credentials ID due to an issue accessing the Credentials store over a remote connection. For simplicity Client ID (workspace name) is serialised instead of the Workspace object. |
||
#21 | 8929 | Paul Allen |
Clean up doCheck Warnings on configuration page (TPI-80). - Added check exceptions to log as warnings. |
||
#20 | 8915 | Paul Allen |
Support for ChangeLog and RepoBrowser. - Added RepoBrowser for Swarm (porting the others should be easy) - ChangeLog XML file now only stores the changelist number all other information is fetched from Perforce |
||
#19 | 8771 | Paul Allen |
Perforce Server 12.1 min check for: Build configuration and password/ticket credentials. Includes: - Added logging for Perforce connections (fine) and set connection pool to 2. - Add 'none' to empty charset list (connection bug?) - Supress P4Java errors for syncing ubinary, xtext, unicode |
||
#18 | 8770 | Paul Allen |
Implemented SCM hook for when Jenkins deletes a workspace. The PerforceScm plugin deletes the workspace, but leaves Jenkins to clean up the local files and directories. (includes dummy 'p4 cstat' code for change reporting) |
||
#17 | 8762 | Paul Allen |
Console Ouptut logging for SCM build steps. - Removed SLF4J and used old style logger (matching Jenkins) - Set Client Host filed to null, Jenkins sometimes gives an IP address. - Test p4java setps in SCM tasks for success(true/false) |
||
#16 | 8744 | Paul Allen |
Support basic SCM Polling. When Jenkins has "Poll SCM" checked the Perforce SCM provider just runs a 'p4 sync -n' on the Workspace. Simple and Fast. |
||
#15 | 8738 | Paul Allen |
Workspace Name Formatter. For Template and Stream workspaces it allows the substitution of the following tags: ${node} The name given to the slave Jenkins node. ${hostname} The hostname for the slave Jenkins node. ${project} The name of the Jenkins build Job. ${hash} Unique hash code of the Jenkins node. |
||
#14 | 8737 | Paul Allen |
Added basic Help for SCM Configuration page. Tidy up descriptions and fix (null:null) in Credential summary. |
||
#13 | 8694 | Paul Allen | Added support for unshelve and revert -w behaviour for builds. | ||
#12 | 8688 | Paul Allen | (Work in progress) Able to call build from review Action and pass URL params. | ||
#11 | 8664 | Paul Allen | Simplified connection to Perforce to get around the SCM initilisation (or lack of). | ||
#10 | 8663 | Paul Allen | Rollout of charset for all Workspace modes. | ||
#9 | 8661 | Paul Allen | Workspace auto fill | ||
#8 | 8641 | Paul Allen | Added workspace helper (setClient) and template/stream types. | ||
#7 | 8640 | Paul Allen |
Added Workspace and Singleton descriptor. Removed old connection code. |
||
#6 | 8639 | Paul Allen |
Added hint URL to Credentials page when no Credentials are defined. Minor refactor and UX changes. |
||
#5 | 8629 | Paul Allen |
Added p4java with connection/authorisation helper classes. Included SSL support and detection of Unicode servers. |
||
#4 | 8613 | Paul Allen | Tidy up unused Credentials data and for debug added getId() to SelectList title. | ||
#3 | 8612 | Paul Allen | Job can now select and save/load Credential choice (some cleanup TODO) | ||
#2 | 8611 | Paul Allen | Basic implementation of Credentials Store | ||
#1 | 8598 | Paul Allen | Experimentation with data binding for Jelly files and ExtensionPoint/Descriptor |