package hudson.plugins.accurev; import hudson.FilePath; import hudson.Launcher; import hudson.Util; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.ModelObject; import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.jetty.security.Password; import hudson.remoting.Callable; import hudson.remoting.VirtualChannel; import hudson.scm.ChangeLogParser; import hudson.scm.ChangeLogSet; import hudson.scm.SCM; import hudson.scm.SCMDescriptor; import hudson.util.ArgumentListBuilder; import hudson.util.IOException2; import org.codehaus.plexus.util.StringOutputStream; import org.kohsuke.stapler.StaplerRequest; import org.xml.sax.SAXException; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; import java.io.StringReader; import java.net.InetAddress; import java.net.UnknownHostException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Logger; /** * Created by IntelliJ IDEA. * * @author connollys * @since 09-Oct-2007 16:17:34 */ public class AccurevSCM extends SCM { private static final Logger logger = Logger.getLogger(AccurevSCM.class.getName()); public static final SimpleDateFormat ACCUREV_DATETIME_FORMATTER = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); private static final long MILLIS_PER_SECOND = 1000L; private final String serverName; private final String depot; private final String stream; private final boolean useWorkspace; private final boolean useUpdate; private final boolean synctime; private final String workspace; private final String workspaceSubPath; private Map locationToKindMap; // key: String path, val: {incl / excl / incldo} private boolean stopStreamWalkup = false; /** * @stapler-constructor */ public AccurevSCM(String serverName, String depot, String stream, boolean useWorkspace, String workspace, String workspaceSubPath, boolean synctime, boolean useUpdate) { super(); this.serverName = serverName; this.depot = depot; this.stream = stream; this.useWorkspace = useWorkspace; this.workspace = workspace; this.workspaceSubPath = workspaceSubPath; this.synctime = synctime; this.useUpdate = useUpdate; } /** * {@inheritDoc} */ public boolean pollChanges(AbstractProject project, Launcher launcher, FilePath workspace, TaskListener listener) throws IOException, InterruptedException { final String accurevPath = workspace.act(new FindAccurevHome()); AccurevServer server = DESCRIPTOR.getServer(serverName); Map accurevEnv = new HashMap(); if (!accurevLogin(server, accurevEnv, workspace, listener, accurevPath, launcher)) { return false; } if (synctime) { listener.getLogger().println("Synchronizing clock with the server..."); if (!synctime(server, accurevEnv, workspace, listener, accurevPath, launcher)) { return false; } } final Run lastBuild = project.getLastBuild(); if (lastBuild == null) { listener.getLogger().println("Project has never been built"); return true; } final Date buildDate = lastBuild.getTimestamp().getTime(); listener.getLogger().println("Last build on " + buildDate); Map streams = getStreams(server, accurevEnv, workspace, listener, accurevPath, launcher); AccurevStream stream = streams.get(this.stream); if (stream == null) { // if there was a problem, fall back to simple stream check locationToKindMap = getIncludeExcludeRules(server, accurevEnv, workspace, listener, accurevPath, launcher, this.stream); return checkStreamForChanges(server, accurevEnv, workspace, listener, accurevPath, launcher, this.stream, buildDate); } // Use the same set of include exclude rules while walking up the stream hierarchy locationToKindMap = getIncludeExcludeRules(server, accurevEnv, workspace, listener, accurevPath, launcher, stream.getName()); // There may be changes in a parent stream that we need to factor in. // If the stopStreamWalkup flag is set, that means we hit a // snapshot or time basis stream, and found no relevant changes do { if (checkStreamForChanges(server, accurevEnv, workspace, listener, accurevPath, launcher, stream.getName(), buildDate)) { return true; } stream = stream.getParent(); } while ((stream != null) && (stopStreamWalkup == false)); stopStreamWalkup = false; return false; } private boolean checkStreamForChanges(AccurevServer server, Map accurevEnv, FilePath workspace, TaskListener listener, String accurevPath, Launcher launcher, String stream, Date buildDate) throws IOException, InterruptedException { // First, determine all changes to the stream since the last build listener.getLogger().println("Determining changes since last build for stream: " + stream); String buildDateString = ACCUREV_DATETIME_FORMATTER.format(buildDate); // Build the 'accurev hist' command ArgumentListBuilder histCmd = new ArgumentListBuilder(); histCmd.add(accurevPath); histCmd.add("hist"); addServer(histCmd, server); histCmd.add("-fx"); histCmd.add("-p"); histCmd.add(depot); histCmd.add("-a"); histCmd.add("-s"); histCmd.add(stream); histCmd.add("-t"); histCmd.add("now-" + buildDateString); // Execute 'accurev hist' command and save off output StringOutputStream histSos = new StringOutputStream(); int rv; if (0 != (rv = launchAccurev(launcher, histCmd, accurevEnv, null, histSos, workspace))) { listener.fatalError("History command failed with exit code " + rv); return false; } // Attributes of the transaction that should force a new build String transactionId = ""; String transactionType = ""; String transactionTime = ""; String transactionUser = ""; Date transactionDate = null; String transactionComment = null; boolean foundChange = false; // Parse the 'accurev hist' command, check to see if each transaction changed this stream // Note: comment capturing relies on comment element coming before version element // in the transaction element schema try { XmlPullParser parser = newPullParser(); parser.setInput(new StringReader(histSos.toString())); boolean parsingComplete = false; boolean stopParsingAtNextElement = false; boolean inComment = false; while ((parsingComplete == false) && (foundChange == false)) { switch (parser.next()) { case XmlPullParser.START_DOCUMENT: break; case XmlPullParser.START_TAG: final String tagName = parser.getName(); // If the transaction was a defcomp (change to include/exclude rules) // or chstream (reparent stream, or alter time basis), force a build if ("transaction".equalsIgnoreCase(tagName)) { // Cache the transaction attributes so we can print them later transactionId = parser.getAttributeValue("", "id"); transactionType = parser.getAttributeValue("", "type"); transactionTime = parser.getAttributeValue("", "time"); transactionUser = parser.getAttributeValue("", "user"); transactionDate = convertAccurevTimestamp(transactionTime); transactionComment = null; if ("defcomp".equalsIgnoreCase(transactionType) || "chstream".equalsIgnoreCase(transactionType) ) { listener.getLogger().println("Found transaction type: " + transactionType + " in stream: " + stream); // Signal that we want to get the text of the next comment element stopParsingAtNextElement = true; } } if ("comment".equalsIgnoreCase(tagName)) { inComment = true; } // If the 'path' modified in this version element is not excluded, force a build if ("version".equalsIgnoreCase(tagName)) { if (stopParsingAtNextElement == true) { // This means we detected a defcomp or chstream earlier, and there // was no comment element for this transaction foundChange = true; parsingComplete = true; } else { String path = parser.getAttributeValue("", "path"); if (path != null) { listener.getLogger().println("Checking to see if path: " + path + " will trigger a build."); // Check to see if the 'path' is in the include/exclude rules // Note: Special case: incldo on a path is means it is included // if it's the path itself. If it's an ancestor directory, // it means that our original path is excluded. String kind = locationToKindMap.get(path); if (kind != null) { listener.getLogger().println("Path: " + path + " has a " + kind + " rule."); if ("excl".equalsIgnoreCase(kind)) { listener.getLogger().println("Path: " + path + " is explicitly excluded."); } else { listener.getLogger().println("Path: " + path + " is explicitly included - force a build."); foundChange = true; } } // Walk up directory structure from current path to "/./" // looking for include / exclude rules on ancestor directories String ancestorPath = path; boolean currentChangeExcluded = false; boolean includedByRule = false; while ((ancestorPath.length() > 3) && (currentChangeExcluded == false) && (foundChange == false) ) { int indexOfFSlash = ancestorPath.lastIndexOf("/"); int indexOfBSlash = ancestorPath.lastIndexOf("\\"); int max = Math.max(indexOfFSlash, indexOfBSlash); if (max > 2) { ancestorPath = ancestorPath.substring(0, max); listener.getLogger().println("Checking to see if ancestor: " + ancestorPath + " will trigger a build."); kind = locationToKindMap.get(ancestorPath); if (kind != null) { if ("incl".equalsIgnoreCase(kind)) { listener.getLogger().println("Path: " + path + " is included because of " + kind + " rule on " + ancestorPath + " - force a build."); foundChange = true; includedByRule = true; } else { // Note: incldo of ancestor without incl of // intervening directory means the element is excluded // Of course, if the rule is excl, it is also excluded listener.getLogger().println("Path: " + path + " is excluded because of " + kind + " rule on " + ancestorPath + " ."); currentChangeExcluded = true; } } } } if ((currentChangeExcluded == false) && (includedByRule == false) ) { listener.getLogger().println("Path: " + path + " is included by default."); foundChange = true; } } } } break; case XmlPullParser.END_TAG: inComment = false; break; case XmlPullParser.TEXT: if (inComment) { transactionComment = parser.getText(); inComment = false; if (stopParsingAtNextElement == true) { // This means we detected a defcomp or chstream earlier, // and this is the text value of the comment element // for this transaction foundChange = true; parsingComplete = true; } } break; case XmlPullParser.END_DOCUMENT: parsingComplete = true; if (stopParsingAtNextElement == true) { // This means we detected a defcomp or chstream earlier, and // there was no comment or version tag in the 'accurev hist' output foundChange = true; parsingComplete = true; } break; } } if (foundChange == true) { String dateStr = ""; if (transactionDate != null) { dateStr = transactionDate.toString(); } listener.getLogger().println("Last change on " + dateStr); listener.getLogger().println("#" + transactionId + " " + transactionUser + " " + transactionType); if (transactionComment != null) { listener.getLogger().println(transactionComment); } return true; } } catch (XmlPullParserException e) { e.printStackTrace(listener.getLogger()); logger.warning(e.getMessage()); return false; } // If no changes were found in the transaction history, // check to see if the stream has a time basis, or is a snapshot. if (foundChange == false) { listener.getLogger().println("No relevant changes detected. (" + stream + ")"); listener.getLogger().println("Determining if stream: " + stream + " has a time basis or is a snapshot."); // Build the 'accurev show' command ArgumentListBuilder showCmd = new ArgumentListBuilder(); showCmd.add(accurevPath); showCmd.add("show"); addServer(showCmd, server); showCmd.add("-fx"); showCmd.add("-p"); showCmd.add(depot); showCmd.add("-s"); showCmd.add(stream); showCmd.add("streams"); // Execute 'accurev show' command and save off output StringOutputStream showSos = new StringOutputStream(); if (0 != (rv = launchAccurev(launcher, showCmd, accurevEnv, null, showSos, workspace))) { listener.fatalError("show command failed with exit code " + rv); return false; } // Parse the 'accurev show' command, check to see if the stream // has a time basis. // Note: Snapshots also have a 'time' element. try { XmlPullParser parser = newPullParser(); parser.setInput(new StringReader(showSos.toString())); boolean parsingComplete = false; while (parsingComplete == false) { switch (parser.next()) { case XmlPullParser.START_DOCUMENT: break; case XmlPullParser.END_DOCUMENT: parsingComplete = true; break; case XmlPullParser.START_TAG: final String tagName = parser.getName(); if ("stream".equalsIgnoreCase(tagName)) { String time = parser.getAttributeValue("", "time"); if (time != null) { // We now know the time basis was last changed // earlier than the given timestamp since we // would otherwise have encountered the chstream // in our inspection of the history. Therefore, // no changes above this stream could possibly // affect its contents relative to the given timestamp. listener.getLogger().println("Stream: " + stream + " has a time basis (or is a snapshot) -" + " not checking higher in the stream hierarchy."); stopStreamWalkup = true; return false; } } break; case XmlPullParser.END_TAG: break; case XmlPullParser.TEXT: break; } } } catch (XmlPullParserException e) { e.printStackTrace(listener.getLogger()); logger.warning(e.getMessage()); return false; } } return false; } /** * Helper method to retrieve include/exclude rules for a given stream. * * @return HashMap key: String path , val: String (enum) incl/excl rule type * **/ private HashMap getIncludeExcludeRules(AccurevServer server, Map accurevEnv, FilePath workspace, TaskListener listener, String accurevPath, Launcher launcher, String stream) throws IOException, InterruptedException { listener.getLogger().println("Retrieving include/exclude rules for stream: " + stream); // Build the 'accurev lsrules' command ArgumentListBuilder lsrulesCmd = new ArgumentListBuilder(); lsrulesCmd.add(accurevPath); lsrulesCmd.add("lsrules"); addServer(lsrulesCmd, server); lsrulesCmd.add("-fx"); lsrulesCmd.add("-s"); lsrulesCmd.add(stream); // Execute 'accurev lsrules' command and save off output StringOutputStream lsrulesSos = new StringOutputStream(); int rv; if (0 != (rv = launchAccurev(launcher, lsrulesCmd, accurevEnv, null, lsrulesSos, workspace))) { listener.fatalError("lsrules command failed with exit code " + rv); return null; } // Parse the 'accurev lsrules' command, and build up the include/exclude rules map HashMap locationToKindMap = new HashMap(); //key: String location, val: String kind (incl / excl / incldo) try { XmlPullParser parser = newPullParser(); parser.setInput(new StringReader(lsrulesSos.toString())); boolean parsingComplete = false; while (parsingComplete == false) { switch (parser.next()) { case XmlPullParser.START_DOCUMENT: break; case XmlPullParser.START_TAG: final String tagName = parser.getName(); if ("element".equalsIgnoreCase(tagName)) { String kind = parser.getAttributeValue("", "kind"); String location = parser.getAttributeValue("", "location"); if (location != null && kind != null) { locationToKindMap.put(location, kind); } } break; case XmlPullParser.END_TAG: break; case XmlPullParser.TEXT: break; case XmlPullParser.END_DOCUMENT: parsingComplete = true; break; } } } catch (XmlPullParserException e) { e.printStackTrace(listener.getLogger()); logger.warning(e.getMessage()); return null; } for (String location : locationToKindMap.keySet()) { String kind = locationToKindMap.get(location); listener.getLogger().println("Found rule: " + kind + " for: " + location); } return locationToKindMap; } private boolean synctime(AccurevServer server, Map accurevEnv, FilePath workspace, TaskListener listener, String accurevPath, Launcher launcher) throws IOException, InterruptedException { ArgumentListBuilder cmd = new ArgumentListBuilder(); cmd.add(accurevPath); cmd.add("synctime"); addServer(cmd, server); StringOutputStream sos = new StringOutputStream(); int rv; if (0 != (rv = launchAccurev(launcher, cmd, accurevEnv, null, sos, workspace))) { listener.fatalError("Synctime command failed with exit code " + rv); return false; } return true; } private Map getStreams(AccurevServer server, Map accurevEnv, FilePath workspace, TaskListener listener, String accurevPath, Launcher launcher) throws IOException, InterruptedException { Map streams = new HashMap(); ArgumentListBuilder cmd = new ArgumentListBuilder(); cmd.add(accurevPath); cmd.add("show"); addServer(cmd, server); cmd.add("-fx"); cmd.add("-p"); cmd.add(depot); cmd.add("streams"); StringOutputStream sos = new StringOutputStream(); int rv; if (0 != (rv = launchAccurev(launcher, cmd, accurevEnv, null, sos, workspace))) { listener.fatalError("Show streams command failed with exit code " + rv); return null; } try { XmlPullParser parser = newPullParser(); parser.setInput(new StringReader(sos.toString())); while (true) { switch (parser.next()) { case XmlPullParser.START_DOCUMENT: break; case XmlPullParser.END_DOCUMENT: // build the tree for (AccurevStream stream : streams.values()) { if (stream.getBasisName() != null) { stream.setParent(streams.get(stream.getBasisName())); } } return streams; case XmlPullParser.START_TAG: final String tagName = parser.getName(); if ("stream".equalsIgnoreCase(tagName)) { String streamName = parser.getAttributeValue("", "name"); String streamNumber = parser.getAttributeValue("", "streamNumber"); String basisStreamName = parser.getAttributeValue("", "basis"); String basisStreamNumber = parser.getAttributeValue("", "basisStreamNumber"); String streamType = parser.getAttributeValue("", "type"); String streamIsDynamic = parser.getAttributeValue("", "isDynamic"); String streamTimeString = parser.getAttributeValue("", "time"); Date streamTime = streamTimeString == null ? null : convertAccurevTimestamp(streamTimeString); String streamStartTimeString = parser.getAttributeValue("", "startTime"); Date streamStartTime = streamTimeString == null ? null : convertAccurevTimestamp(streamTimeString); try { AccurevStream stream = new AccurevStream(streamName, streamNumber == null ? null : Long.valueOf(streamNumber), depot, basisStreamName, basisStreamNumber == null ? null : Long.valueOf(basisStreamNumber), streamIsDynamic == null ? false : Boolean.parseBoolean(streamIsDynamic), AccurevStream.StreamType.parseStreamType(streamType), streamTime, streamStartTime); streams.put(streamName, stream); } catch (NumberFormatException e) { e.printStackTrace(listener.getLogger()); } } break; case XmlPullParser.END_TAG: break; case XmlPullParser.TEXT: break; } } } catch (XmlPullParserException e) { e.printStackTrace(listener.getLogger()); logger.warning(e.getMessage()); return null; } } private Map getWorkspaces(AccurevServer server, Map accurevEnv, FilePath workspace, TaskListener listener, String accurevPath, Launcher launcher) throws IOException, InterruptedException { Map workspaces = new HashMap(); ArgumentListBuilder cmd = new ArgumentListBuilder(); cmd.add(accurevPath); cmd.add("show"); addServer(cmd, server); cmd.add("-fx"); cmd.add("-p"); cmd.add(depot); cmd.add("wspaces"); StringOutputStream sos = new StringOutputStream(); int rv; if (0 != (rv = launchAccurev(launcher, cmd, accurevEnv, null, sos, workspace))) { listener.fatalError("Show workspaces command failed with exit code " + rv); return null; } try { XmlPullParser parser = newPullParser(); parser.setInput(new StringReader(sos.toString())); while (true) { switch (parser.next()) { case XmlPullParser.START_DOCUMENT: break; case XmlPullParser.END_DOCUMENT: return workspaces; case XmlPullParser.START_TAG: final String tagName = parser.getName(); if ("Element".equalsIgnoreCase(tagName)) { String name = parser.getAttributeValue("", "Name"); String storage = parser.getAttributeValue("", "Storage"); String host = parser.getAttributeValue("", "Host"); String streamNumber = parser.getAttributeValue("", "Stream"); String depot = parser.getAttributeValue("", "depot"); try { workspaces.put(name, new AccurevWorkspace( depot, streamNumber == null ? null : Long.valueOf(streamNumber), name, host, storage)); } catch (NumberFormatException e) { e.printStackTrace(listener.getLogger()); } } break; case XmlPullParser.END_TAG: break; case XmlPullParser.TEXT: break; } } } catch (XmlPullParserException e) { e.printStackTrace(listener.getLogger()); logger.warning(e.getMessage()); return null; } } /** * {@inheritDoc} */ public boolean checkout(AbstractBuild build, Launcher launcher, FilePath workspace, BuildListener listener, File changelogFile) throws IOException, InterruptedException { final String accurevPath = workspace.act(new FindAccurevHome()); if (!useWorkspace || !useUpdate || (build.getPreviousBuild() != null && build.getPreviousBuild().getResult().isWorseThan(Result.UNSTABLE))) { workspace.act(new PurgeWorkspaceContents(listener)); } AccurevServer server = DESCRIPTOR.getServer(serverName); Map accurevEnv = new HashMap(); if (!accurevLogin(server, accurevEnv, workspace, listener, accurevPath, launcher)) { return false; } if (synctime) { listener.getLogger().println("Synchronizing clock with the server..."); if (!synctime(server, accurevEnv, workspace, listener, accurevPath, launcher)) { return false; } } listener.getLogger().println("Getting a list of streams..."); final Map streams = getStreams(server, accurevEnv, workspace, listener, accurevPath, launcher); if (depot == null || "".equals(depot)) { listener.fatalError("Must specify a depot"); return false; } if (stream == null || "".equals(stream)) { listener.fatalError("Must specify a stream"); return false; } if (streams != null && !streams.containsKey(stream)) { listener.fatalError("The specified stream does not appear to exist!"); return false; } if (useWorkspace && (this.workspace == null || "".equals(this.workspace))) { listener.fatalError("Must specify a workspace"); return false; } if (useWorkspace) { listener.getLogger().println("Getting a list of workspaces..."); Map workspaces = getWorkspaces(server, accurevEnv, workspace, listener, accurevPath, launcher); if (workspaces == null) { listener.fatalError("Cannot determine workspace configuration information"); return false; } if (!workspaces.containsKey(this.workspace)) { listener.fatalError("The specified workspace does not appear to exist!"); return false; } AccurevWorkspace accurevWorkspace = workspaces.get(this.workspace); if (!depot.equals(accurevWorkspace.getDepot())) { listener.fatalError("The specified workspace, " + this.workspace + ", is based in the depot " + accurevWorkspace.getDepot() + " not " + depot); return false; } for (AccurevStream accurevStream : streams.values()) { if (accurevWorkspace.getStreamNumber().equals(accurevStream.getNumber())) { accurevWorkspace.setStream(accurevStream); break; } } RemoteWorkspaceDetails remoteDetails; try { remoteDetails = workspace.act(new DetermineRemoteHostname(workspace.getRemote())); } catch (IOException e) { listener.fatalError("Unable to validate workspace host."); e.printStackTrace(listener.getLogger()); return false; } boolean needsRelocation = false; ArgumentListBuilder cmd = new ArgumentListBuilder(); cmd.add(accurevPath); cmd.add("chws"); addServer(cmd, server); cmd.add("-w"); cmd.add(this.workspace); if (!stream.equals(accurevWorkspace.getStream().getParent().getName())) { listener.getLogger().println("Parent stream needs to be updated."); needsRelocation = true; cmd.add("-b"); cmd.add(this.stream); } if (!accurevWorkspace.getHost().equals(remoteDetails.getHostName())) { listener.getLogger().println("Host needs to be updated."); needsRelocation = true; cmd.add("-m"); cmd.add(remoteDetails.getHostName()); } final String oldStorage = accurevWorkspace.getStorage() .replace("/", remoteDetails.getFileSeparator()) .replace("\\", remoteDetails.getFileSeparator()); if (!oldStorage.equals(remoteDetails.getPath())) { listener.getLogger().println("Storage needs to be updated."); needsRelocation = true; cmd.add("-l"); cmd.add(workspace.getRemote()); } if (needsRelocation) { listener.getLogger().println("Relocating workspace..."); listener.getLogger().println(" Old host: " + accurevWorkspace.getHost()); listener.getLogger().println(" New host: " + remoteDetails.getHostName()); listener.getLogger().println(" Old storage: " + oldStorage); listener.getLogger().println(" New storage: " + remoteDetails.getPath()); listener.getLogger().println(" Old parent stream: " + accurevWorkspace.getStream().getParent() .getName()); listener.getLogger().println(" New parent stream: " + stream); listener.getLogger().println(cmd.toStringWithQuote()); int rv; rv = launchAccurev(launcher, cmd, accurevEnv, null, listener.getLogger(), workspace); if (rv != 0) { listener.fatalError("Relocation failed with exit code " + rv); return false; } listener.getLogger().println("Relocation successfully."); } listener.getLogger().println("Updating workspace..."); cmd = new ArgumentListBuilder(); cmd.add(accurevPath); cmd.add("update"); addServer(cmd, server); int rv; rv = launchAccurev(launcher, cmd, accurevEnv, null, listener.getLogger(), workspace); if (rv != 0) { listener.fatalError("Update failed with exit code " + rv); return false; } listener.getLogger().println("Update completed successfully."); listener.getLogger().println("Populating workspace..."); cmd = new ArgumentListBuilder(); cmd.add(accurevPath); cmd.add("pop"); addServer(cmd, server); cmd.add("-R"); if ((workspaceSubPath == null) || (workspaceSubPath.trim().length() == 0)) { cmd.add("."); } else { cmd.add(workspaceSubPath); } if (rv != 0) { listener.fatalError("Populate failed with exit code " + rv); return false; } listener.getLogger().println("Populate completed successfully."); } else { listener.getLogger().println("Populating workspace..."); ArgumentListBuilder cmd = new ArgumentListBuilder(); cmd.add(accurevPath); cmd.add("pop"); addServer(cmd, server); cmd.add("-v"); cmd.add(stream); cmd.add("-L"); cmd.add(workspace.getRemote()); cmd.add("-R"); if ((workspaceSubPath == null) || (workspaceSubPath.trim().length() == 0)) { cmd.add("."); } else { cmd.add(workspaceSubPath); } int rv; rv = launchAccurev(launcher, cmd, accurevEnv, null, listener.getLogger(), workspace); if (rv != 0) { listener.fatalError("Populate failed with exit code " + rv); return false; } listener.getLogger().println("Populate completed successfully."); } listener.getLogger().println("Calculating changelog..."); Calendar startTime = null; if (null == build.getPreviousBuild()) { listener.getLogger().println("Cannot find a previous build to compare against. Computing all changes."); } else { startTime = build.getPreviousBuild().getTimestamp(); } { AccurevStream stream = streams.get(this.stream); if (stream == null) { // if there was a problem, fall back to simple stream check return captureChangelog(server, accurevEnv, workspace, listener, accurevPath, launcher, build.getTimestamp().getTime(), startTime == null ? null : startTime.getTime(), this.stream, changelogFile); } // There may be changes in a parent stream that we need to factor in. // TODO produce a consolidated list of changes from the parent streams do { // This is a best effort to get as close to the changes as possible locationToKindMap = getIncludeExcludeRules(server, accurevEnv, workspace, listener, accurevPath, launcher, stream.getName()); if (checkStreamForChanges(server, accurevEnv, workspace, listener, accurevPath, launcher, stream.getName(), startTime == null ? new Date(0) : startTime.getTime())) { return captureChangelog(server, accurevEnv, workspace, listener, accurevPath, launcher, build.getTimestamp().getTime(), startTime == null ? null : startTime .getTime(), stream.getName(), changelogFile); } stream = stream.getParent(); } while (stream != null && stream.isReceivingChangesFromParent()); } return captureChangelog(server, accurevEnv, workspace, listener, accurevPath, launcher, build.getTimestamp().getTime(), startTime == null ? null : startTime.getTime(), this.stream, changelogFile); } private boolean captureChangelog(AccurevServer server, Map accurevEnv, FilePath workspace, BuildListener listener, String accurevPath, Launcher launcher, Date buildDate, Date startDate, String stream, File changelogFile) throws IOException, InterruptedException { ArgumentListBuilder cmd = new ArgumentListBuilder(); cmd.add(accurevPath); cmd.add("hist"); addServer(cmd, server); cmd.add("-fx"); cmd.add("-a"); cmd.add("-s"); cmd.add(stream); cmd.add("-t"); String dateRange = ACCUREV_DATETIME_FORMATTER.format(buildDate); if (startDate != null) { dateRange += "-" + ACCUREV_DATETIME_FORMATTER.format(startDate); } else { dateRange += ".100"; } cmd.add(dateRange); // if this breaks windows there's going to be fun FileOutputStream os = new FileOutputStream(changelogFile); try { BufferedOutputStream bos = new BufferedOutputStream(os); try { int rv = launchAccurev(launcher, cmd, accurevEnv, null, bos, workspace); if (rv != 0) { listener.fatalError("Changelog failed with exit code " + rv); return false; } } finally { bos.close(); } } finally { os.close(); } listener.getLogger().println("Changelog calculated successfully."); return true; } private boolean accurevLogin(AccurevServer server, Map accurevEnv, FilePath workspace, TaskListener listener, String accurevPath, Launcher launcher) throws IOException, InterruptedException { ArgumentListBuilder cmd; if (server != null) { accurevEnv.put("ACCUREV_HOME", workspace.getParent().getRemote()); listener.getLogger().println("Authenticating with Accurev server..."); boolean[] masks; cmd = new ArgumentListBuilder(); cmd.add(accurevPath); cmd.add("login"); addServer(cmd, server); cmd.add(server.getUsername()); if (server.getPassword() == null || "".equals(server.getPassword())) { cmd.addQuoted(""); masks = new boolean[cmd.toCommandArray().length]; } else { cmd.add(server.getPassword()); masks = new boolean[cmd.toCommandArray().length]; masks[masks.length - 1] = true; } String resp = null; DESCRIPTOR.ACCUREV_LOCK.lock(); try { StringOutputStream sos = new StringOutputStream(); int rv = launcher.launch(cmd.toCommandArray(), masks, Util.mapToEnv(accurevEnv), null, sos, workspace) .join(); if (rv == 0) { resp = null; } else { resp = sos.toString(); } } finally { DESCRIPTOR.ACCUREV_LOCK.unlock(); } if (null == resp || "".equals(resp)) { listener.getLogger().println("Authentication completed successfully."); return true; } else { listener.fatalError("Authentication failed: " + resp); return false; } } return true; } private int launchAccurev(Launcher launcher, ArgumentListBuilder cmd, Map env, InputStream in, OutputStream os, FilePath workspace) throws IOException, InterruptedException { int rv; DESCRIPTOR.ACCUREV_LOCK.lock(); try { rv = launcher.launch(cmd.toCommandArray(), Util.mapToEnv(env), in, os, workspace).join(); } finally { DESCRIPTOR.ACCUREV_LOCK.unlock(); } return rv; } private void addServer(ArgumentListBuilder cmd, AccurevServer server) { if (null != server && null != server.getHost() && !"".equals(server.getHost())) { cmd.add("-H"); if (server.getPort() != 0) { cmd.add(server.getHost() + ":" + server.getPort()); } else { cmd.add(server.getHost()); } } } /** * {@inheritDoc} */ public ChangeLogParser createChangeLogParser() { return new AccurevChangeLogParser(); } /** * Getter for property 'useWorkspace'. * * @return Value for property 'useWorkspace'. */ public boolean isUseWorkspace() { return useWorkspace; } /** * Getter for property 'useUpdate'. * * @return Value for property 'useUpdate'. */ public boolean isUseUpdate() { return useUpdate; } /** * Getter for property 'workspace'. * * @return Value for property 'workspace'. */ public String getWorkspace() { return workspace; } /** * Getter for property 'serverName'. * * @return Value for property 'serverName'. */ public String getServerName() { return serverName; } /** * Getter for property 'depot'. * * @return Value for property 'depot'. */ public String getDepot() { return depot; } /** * Getter for property 'stream'. * * @return Value for property 'stream'. */ public String getStream() { return stream; } /** * Getter for property 'workspaceSubPath'. * * @return Value for property 'workspaceSubPath'. */ public String getWorkspaceSubPath() { return workspaceSubPath; } /** * Getter for property 'synctime'. * * @return Value for property 'synctime'. */ public boolean isSynctime() { return synctime; } private static Date convertAccurevTimestamp(String transactionTime) { if (transactionTime == null) { return null; } try { final long time = Long.parseLong(transactionTime); final long date = time * MILLIS_PER_SECOND; return new Date(date); } catch (NumberFormatException e) { return null; } } private static XmlPullParser newPullParser() throws XmlPullParserException { XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); factory.setNamespaceAware(false); factory.setValidating(false); XmlPullParser parser = factory.newPullParser(); return parser; } /** * {@inheritDoc} */ public SCMDescriptor getDescriptor() { return DESCRIPTOR; } public static final AccurevSCMDescriptor DESCRIPTOR = new AccurevSCMDescriptor(); public static final class AccurevSCMDescriptor extends SCMDescriptor implements ModelObject { /** * The accurev server has been known to crash if more than one copy of the accurev has been run concurrently * on the local machine. */ transient static final Lock ACCUREV_LOCK = new ReentrantLock(); private List servers; /** * Constructs a new AccurevSCMDescriptor. */ protected AccurevSCMDescriptor() { super(AccurevSCM.class, null); load(); } /** * {@inheritDoc} */ @Override public String getDisplayName() { return "Accurev"; } /** * {@inheritDoc} */ @Override public boolean configure(StaplerRequest req) throws FormException { req.bindParameters(this, "accurev."); servers = req.bindParametersToList(AccurevServer.class, "accurev.server."); save(); return true; } /** * {@inheritDoc} */ @Override public SCM newInstance(StaplerRequest req) throws FormException { return req.bindParameters(AccurevSCM.class, "accurev."); } /** * Getter for property 'servers'. * * @return Value for property 'servers'. */ public List getServers() { if (servers == null) { servers = new ArrayList(); } return servers; } /** * Setter for property 'servers'. * * @param servers Value to set for property 'servers'. */ public void setServers(List servers) { this.servers = servers; } public AccurevServer getServer(String name) { if (name == null) { return null; } for (AccurevServer server : servers) { if (name.equals(server.getName())) { return server; } } return null; } /** * Getter for property 'serverNames'. * * @return Value for property 'serverNames'. */ public String[] getServerNames() { String[] result = new String[servers.size()]; for (int i = 0; i < result.length; i++) { result[i] = servers.get(i).getName(); } return result; } } public static final class AccurevServer { private String name; private String host; private int port; private String username; private String password; /** * Constructs a new AccurevServer. */ public AccurevServer() { } public AccurevServer(String name, String host, int port, String username, String password) { this.name = name; this.host = host; this.port = port; this.username = username; this.password = password; } /** * Getter for property 'name'. * * @return Value for property 'name'. */ public String getName() { return name; } /** * Setter for property 'name'. * * @param name Value to set for property 'name'. */ public void setName(String name) { this.name = name; } /** * Getter for property 'host'. * * @return Value for property 'host'. */ public String getHost() { return host; } /** * Setter for property 'host'. * * @param host Value to set for property 'host'. */ public void setHost(String host) { this.host = host; } /** * Getter for property 'port'. * * @return Value for property 'port'. */ public int getPort() { return port; } /** * Setter for property 'port'. * * @param port Value to set for property 'port'. */ public void setPort(int port) { this.port = port; } /** * Getter for property 'username'. * * @return Value for property 'username'. */ public String getUsername() { return username; } /** * Setter for property 'username'. * * @param username Value to set for property 'username'. */ public void setUsername(String username) { this.username = username; } /** * Getter for property 'password'. * * @return Value for property 'password'. */ public String getPassword() { return Password.deobfuscate(password); } /** * Setter for property 'password'. * * @param password Value to set for property 'password'. */ public void setPassword(String password) { this.password = Password.obfuscate(password); } } private static final class PurgeWorkspaceContents implements FilePath.FileCallable { private final TaskListener listener; public PurgeWorkspaceContents(TaskListener listener) { this.listener = listener; } /** * {@inheritDoc} */ public Boolean invoke(File ws, VirtualChannel channel) throws IOException { listener.getLogger().println("Purging workspace..."); Util.deleteContentsRecursive(ws); listener.getLogger().println("Workspace purged."); return Boolean.TRUE; } } private static final class FindAccurevHome implements FilePath.FileCallable { private String[] nonWindowsPaths = { "/usr/local/bin/accurev", "/usr/bin/accurev", "/bin/accurev", "/local/bin/accurev", }; private String[] windowsPaths = { "C:\\Program Files\\AccuRev\\bin\\accurev.exe", "C:\\Program Files (x86)\\AccuRev\\bin\\accurev.exe" }; private static String getExistingPath(String[] paths) { for (int i = 0; i < paths.length; i++) { if (new File(paths[i]).exists()) { return paths[i]; } } return paths[0]; } /** * {@inheritDoc} */ public String invoke(File f, VirtualChannel channel) throws IOException { if (System.getProperty("os.name").toLowerCase().startsWith("windows")) { // we are running on windows return getExistingPath(windowsPaths); } else { // we are running on *nix return getExistingPath(nonWindowsPaths); } } } private static final class AccurevChangeLogParser extends ChangeLogParser { /** * {@inheritDoc} */ public ChangeLogSet parse(AbstractBuild build, File changelogFile) throws IOException, SAXException { List transactions = null; try { XmlPullParser parser = newPullParser(); FileReader fis = null; BufferedReader bis = null; try { fis = new FileReader(changelogFile); bis = new BufferedReader(fis); parser.setInput(bis); transactions = parseTransactions(parser); } finally { if (bis != null) { bis.close(); } if (fis != null) { fis.close(); } } } catch (XmlPullParserException e) { throw new IOException2(e); } logger.info("transactions size = " + transactions.size()); return new AccurevChangeLogSet(build, transactions); } private List parseTransactions(XmlPullParser parser) throws IOException, XmlPullParserException { List transactions = new ArrayList(); AccurevTransaction currentTransaction = null; boolean inComment = false; while (true) { switch (parser.next()) { case XmlPullParser.START_DOCUMENT: break; case XmlPullParser.END_DOCUMENT: return transactions; case XmlPullParser.START_TAG: final String tagName = parser.getName(); inComment = "comment".equalsIgnoreCase(tagName); if ("transaction".equalsIgnoreCase(tagName)) { currentTransaction = new AccurevTransaction(); transactions.add(currentTransaction); currentTransaction.setRevision(parser.getAttributeValue("", "id")); currentTransaction.setUser(parser.getAttributeValue("", "user")); currentTransaction.setDate(convertAccurevTimestamp(parser.getAttributeValue("", "time"))); currentTransaction.setAction(parser.getAttributeValue("", "type")); } else if ("version".equalsIgnoreCase(tagName) && currentTransaction != null) { String path = parser.getAttributeValue("", "path"); if (path != null) { path = path.replace("\\", "/"); if (path.startsWith("/./")) { path = path.substring(3); } } currentTransaction.addAffectedPath(path); } break; case XmlPullParser.END_TAG: inComment = false; break; case XmlPullParser.TEXT: if (inComment && currentTransaction != null) { currentTransaction.setMsg(parser.getText()); } break; } } } } private static class RemoteWorkspaceDetails implements Serializable { private final String hostName; private final String path; private final String fileSeparator; public RemoteWorkspaceDetails(String hostName, String path, String fileSeparator) { this.hostName = hostName; this.path = path; this.fileSeparator = fileSeparator; } /** * Getter for property 'hostName'. * * @return Value for property 'hostName'. */ public String getHostName() { return hostName; } /** * Getter for property 'path'. * * @return Value for property 'path'. */ public String getPath() { return path; } /** * Getter for property 'fileSeparator'. * * @return Value for property 'fileSeparator'. */ public String getFileSeparator() { return fileSeparator; } } private static class DetermineRemoteHostname implements Callable { private final String path; public DetermineRemoteHostname(String path) { this.path = path; } /** * {@inheritDoc} */ public RemoteWorkspaceDetails call() throws UnknownHostException { InetAddress addr = InetAddress.getLocalHost(); File f = new File(path); String path; try { path = f.getCanonicalPath(); } catch (IOException e) { path = f.getAbsolutePath(); } return new RemoteWorkspaceDetails(addr.getCanonicalHostName(), path, File.separator); } } }