/*********************************************************************************************************************** * CruiseControl, a Continuous Integration Toolkit * Copyright (c) 2001, ThoughtWorks, Inc. * 200 E. Randolph, 25th Floor * Chicago, IL 60601 USA * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * following conditions are met: * * Redistributions of source code must retain the above copyright notice, this list of conditions and the * following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the * following disclaimer in the documentation and/or other materials provided with the distribution. * * Neither the name of ThoughtWorks, Inc., CruiseControl, nor the names of its contributors may be used to endorse * or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **********************************************************************************************************************/ package net.sourceforge.cruisecontrol.sourcecontrols; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; import net.sourceforge.cruisecontrol.CruiseControlException; import net.sourceforge.cruisecontrol.Modification; import net.sourceforge.cruisecontrol.SourceControl; import net.sourceforge.cruisecontrol.util.Commandline; import net.sourceforge.cruisecontrol.util.ValidationHelper; import net.sourceforge.cruisecontrol.util.IO; import net.sourceforge.cruisecontrol.util.StreamLogger; import org.apache.log4j.Logger; /** * This class handles all VSS-related aspects of determining the modifications since the last good build. * * @author <a href="mailto:alden@thoughtworks.com">alden almagro</a> * @author Eli Tucker * @author <a href="mailto:jcyip@thoughtworks.com">Jason Yip</a> * @author Arun Aggarwal */ public class Vss implements SourceControl { private static final Logger LOG = Logger.getLogger(Vss.class); private SimpleDateFormat vssDateTimeFormat; private String ssDir; private String vssPath; private String serverPath; private String login; private String dateFormat = "MM/dd/yy"; private String timeFormat = "hh:mma"; private final SourceControlProperties properties = new SourceControlProperties(); public Vss() { constructVssDateTimeFormat(); } /** * Set the project to get history from * * @param vsspath the project to get history from */ public void setVsspath(String vsspath) { this.vssPath = "$" + vsspath; } /** * Set the path to the ss executable * * @param ssdir the path to the ss executable */ public void setSsDir(String ssdir) { this.ssDir = ssdir; } /** * Set the path to the directory containing the srcsafe.ini file. * * @param dirWithSrcsafeIni the path to the directory containing the srcsafe.ini file. */ public void setServerPath(final String dirWithSrcsafeIni) { serverPath = dirWithSrcsafeIni; } /** * Login for vss * * @param usernameCommaPassword Login for vss */ public void setLogin(final String usernameCommaPassword) { login = usernameCommaPassword; } /** * Choose a property to be set if the project has modifications if we have a change that only requires repackaging, * i.e. jsp, we don't need to recompile everything, just rejar. * * @param propertyName property to be set if the project has modifications */ public void setProperty(final String propertyName) { properties.assignPropertyName(propertyName); } /** * Choose a property to be set if the project has deletions * * @param propertyName property to be set if the project has deletions */ public void setPropertyOnDelete(final String propertyName) { properties.assignPropertyOnDeleteName(propertyName); } /** * Sets the date format to use for querying VSS and processing reports. The default date format is * <code>MM/dd/yy</code> . If your computer is set to a different region, you may wish to use a format such as * <code>dd/MM/yy</code> . * * @param format date format to use for querying VSS and processing reports * @see java.text.SimpleDateFormat */ public void setDateFormat(final String format) { dateFormat = format; constructVssDateTimeFormat(); } /** * Sets the time format to use for querying VSS and processing reports. The default time format is * <code>hh:mma</code> . If your computer is set to a different region, you may wish to use a format such as * <code>HH:mm</code> . * * @param format time format to use for querying VSS and processing reports * @see java.text.SimpleDateFormat */ public void setTimeFormat(final String format) { timeFormat = format; constructVssDateTimeFormat(); } public Map<String, String> getProperties() { return properties.getPropertiesAndReset(); } public void validate() throws CruiseControlException { ValidationHelper.assertIsSet(vssPath, "vsspath", this.getClass()); ValidationHelper.assertIsSet(login, "login", this.getClass()); } /** * Calls "ss history [dir] -R -Vd[now]~[lastBuild] -Y[login] -I-N -O[tempFileName]" Results written to a file since * VSS will start wrapping lines if read directly from the stream. * * @param lastBuild date of last build * @param now curent build date * @return List of modifications */ public List<Modification> getModifications(final Date lastBuild, final Date now) { final List<Modification> modifications = new ArrayList<Modification>(); Process p = null; try { LOG.info("Getting modifications for " + vssPath); p = Runtime.getRuntime().exec(getCommandLine(lastBuild, now), VSSHelper.loadVSSEnvironment(serverPath)); p.getOutputStream().close(); final Thread stderr = logErrorStream(p.getErrorStream()); p.waitFor(); stderr.join(); parseTempFile(modifications); } catch (Exception e) { // TODO: Revisit this when ThreadQueue is more stable. Would prefer throwing a RuntimeException. LOG.error("Problem occurred while attempting to get VSS modifications. Returning empty modifications.", e); return Collections.emptyList(); } finally { if (p != null) { IO.close(p); } } if (modifications.size() > 0) { properties.modificationFound(); } return modifications; } private Thread logErrorStream(final InputStream is) { final Thread stderr = new Thread(StreamLogger.getWarnPumper(LOG, is)); stderr.start(); return stderr; } void parseTempFile(final List<Modification> modifications) throws IOException, CruiseControlException { final File tempFile = getTempFile(); if (!getTempFile().isFile()) { throw new CruiseControlException("vss failed to create output file " + tempFile.getPath()); } if (LOG.isDebugEnabled()) { logVSSTempFile(); } final BufferedReader reader = new BufferedReader(new FileReader(tempFile)); try { parseHistoryEntries(modifications, reader); } finally { // need to close the InputStream before delete(), otherwise fails on windows reader.close(); } tempFile.delete(); } private void logVSSTempFile() throws IOException { final BufferedReader reader = new BufferedReader(new FileReader(getTempFile())); try { String currLine = reader.readLine(); LOG.debug(" "); while (currLine != null) { LOG.debug(getTempFile().getName() + ": " + currLine); currLine = reader.readLine(); } LOG.debug(" "); } finally { reader.close(); } } private File getTempFile() { //@todo Make this thread safe return new File(createFileNameFromVssPath()); } String createFileNameFromVssPath() { //@todo Make this thread safe - same temp file would be used if two projects w/ same vsspath exec together // don't include the leading $ String filename = vssPath.substring(1).replace('/', '_') + ".tmp"; while (filename.charAt(0) == '_') { filename = filename.substring(1); } // special case for vsspath == root ($/) where .tmp is not valid file on Windows. // http://jira.public.thoughtworks.org/browse/CC-783 if (".tmp".equals(filename)) { filename = "vssroot" + filename; } return filename; } void parseHistoryEntries(final List<Modification> modifications, final BufferedReader reader) throws IOException { String currLine = reader.readLine(); while (currLine != null) { if (isRelevantVssEntryHeader(currLine)) { final List<String> vssEntry = new ArrayList<String>(); vssEntry.add(currLine); currLine = reader.readLine(); while (currLine != null && !isRelevantVssEntryHeader(currLine)) { vssEntry.add(currLine); currLine = reader.readLine(); } Modification mod = handleEntry(vssEntry); if (mod != null) { modifications.add(mod); } } else { currLine = reader.readLine(); } } } /** * Most relevant VSS entry headers will be 5 asterisks, 2 spaces, file name, 2 spaces, 5 asterisks. However, if * adding to the root, there are apparently 17 asterisks, 2 spaces, version name, 2 spaces, 17 asterisks. * @param line the line to examine * @return if line is a relevant VSS entry header */ private boolean isRelevantVssEntryHeader(final String line) { // This can still fail if the entry has a comment containing something of the form '***** some text *****' but // is probably not worth handling at this point. If it does come up, we may need to look at implementing some // form of state machine. return line.matches("\\*+ {2}.+ {2}\\*+"); } protected String[] getCommandLine(final Date lastBuild, final Date now) throws IOException { final Commandline commandline = new Commandline(); final String execCommand = (ssDir != null) ? new File(ssDir, "ss.exe").getCanonicalPath() : "ss.exe"; commandline.setExecutable(execCommand); commandline.createArgument("history"); commandline.createArgument(vssPath); commandline.createArgument("-R"); commandline.createArgument("-Vd" + formatDateForVSS(now) + "~" + formatDateForVSS(lastBuild)); commandline.createArgument("-Y" + login); commandline.createArgument("-I-N"); commandline.createArgument("-O" + getTempFile().getName()); LOG.info("Command line to execute: " + commandline); return commandline.getCommandline(); } /** * Format a date for vss in the format specified by the dateFormat. By default, this is in the form * <code>12/21/2000;8:14A</code> (vss doesn't like the m in am or pm). This format can be changed with * <code>setDateFormat()</code> * * @param date * Date to format. * @return String of date in format that VSS requires. * @see #setDateFormat */ private String formatDateForVSS(final Date date) { final String vssFormattedDate = new SimpleDateFormat(dateFormat + ";" + timeFormat, Locale.US).format(date); if (timeFormat.endsWith("a")) { return vssFormattedDate.substring(0, vssFormattedDate.length() - 1); } return vssFormattedDate; } /** * Parse individual VSS history entry * * @param entry individual VSS history entry * @return a Modification for this entry or null */ protected Modification handleEntry(final List<String> entry) { LOG.debug("VSS history entry BEGIN"); for (final String anEntry : entry) { LOG.debug("entry: " + anEntry); } LOG.debug("VSS history entry END"); final String labelDelimiter = "**********************"; final boolean isLabelEntry = labelDelimiter.equals(entry.get(0)); if (isLabelEntry) { LOG.debug("this is a label; ignoring this entry"); return null; } // but need to adjust for cases where Label: line exists // // ***** DateChooser.java ***** // Version 8 // Label: "Completely new version!" // User: Arass Date: 10/21/02 Time: 12:48p // Checked in $/code/development/src/org/ets/cbtidg/common/gui // Comment: This is where I add a completely new, but alot nicer // version of the date chooser. // Label comment: int nameAndDateIndex = 2; if (entry.get(0).startsWith("***************** ")) { nameAndDateIndex = 1; } String nameAndDateLine = entry.get(nameAndDateIndex); if (nameAndDateLine.startsWith("Label:")) { nameAndDateIndex++; nameAndDateLine = entry.get(nameAndDateIndex); LOG.debug("adjusting for the line that starts with Label"); } final Modification modification = new Modification("vss"); modification.userName = parseUser(nameAndDateLine); modification.modifiedTime = parseDate(nameAndDateLine); final String folderLine = entry.get(0); final int fileIndex = nameAndDateIndex + 1; final String fileLine = entry.get(fileIndex); LOG.debug("File line is: " + fileLine); if (fileLine.startsWith("Checked in")) { LOG.debug("this is a checkin"); final int commentIndex = fileIndex + 1; modification.comment = parseComment(entry, commentIndex); final String fileName = folderLine.substring(7, folderLine.indexOf(" *")); final String folderName = fileLine.substring(12); final Modification.ModifiedFile modfile = modification.createModifiedFile(fileName, folderName); modfile.action = "checkin"; } else if (fileLine.endsWith("Created")) { modification.type = "create"; LOG.debug("this folder was created"); } else { final String fileName; final String folderName; if (nameAndDateIndex == 1) { folderName = vssPath; } else { folderName = vssPath + "\\" + folderLine.substring(7, folderLine.indexOf(" *")); } int lastSpace = fileLine.lastIndexOf(" "); if (lastSpace != -1) { fileName = fileLine.substring(0, lastSpace); } else { fileName = fileLine; if (fileName.equals("Branched")) { LOG.debug("Branched file, ignoring as branch directory is handled separately"); return null; } } final Modification.ModifiedFile modfile = modification.createModifiedFile(fileName, folderName); if (fileLine.endsWith("added")) { modfile.action = "add"; } else if (fileLine.endsWith("deleted")) { modfile.action = "delete"; properties.deletionFound(); } else if (fileLine.endsWith("destroyed")) { modfile.action = "destroy"; properties.deletionFound(); } else if (fileLine.endsWith("recovered")) { modfile.action = "recover"; } else if (fileLine.endsWith("shared")) { modfile.action = "share"; } else if (fileLine.endsWith("branched")) { modfile.action = "branch"; } else if (fileLine.indexOf(" renamed to ") != -1) { modfile.fileName = fileLine; modfile.action = "rename"; properties.deletionFound(); } else if (fileLine.startsWith("Labeled")) { return null; } else { LOG.warn("Don't know how to handle this line: " + fileLine); return null; } } return modification; } /** * Parse comment from VSS history (could be multi-line) * * @param commentList comment to parse * @param commentIndex comment index * @return the comment the single string representation of the given comment */ private String parseComment(final List commentList, final int commentIndex) { final StringBuilder comment = new StringBuilder(); comment.append(commentList.get(commentIndex)).append(" "); for (int i = commentIndex + 1; i < commentList.size(); i++) { comment.append(commentList.get(i)).append(" "); } return comment.toString().trim(); } /** * Parse date/time from VSS file history The nameAndDateLine will look like <br> * <code>User: Etucker Date: 6/26/01 Time: 11:53a</code><br> * Sometimes also this<br> * <code>User: Aaggarwa Date: 6/29/:1 Time: 3:40p</code><br> * Note the ":" instead of a "0" * * @param nameAndDateLine name and date line to parse * @return Date in form "'Date: 'MM/dd/yy 'Time: 'hh:mma", or a different form based on dateFormat * @see #setDateFormat */ public Date parseDate(final String nameAndDateLine) { String dateAndTime = nameAndDateLine.substring(nameAndDateLine.indexOf("Date: ")); int indexOfColon = dateAndTime.indexOf("/:"); if (indexOfColon != -1) { dateAndTime = dateAndTime.substring(0, indexOfColon) + dateAndTime.substring(indexOfColon, indexOfColon + 2).replace(':', '0') + dateAndTime.substring(indexOfColon + 2); } try { final Date lastModifiedDate; if (timeFormat.endsWith("a")) { lastModifiedDate = vssDateTimeFormat.parse(dateAndTime.trim() + "m"); } else { lastModifiedDate = vssDateTimeFormat.parse(dateAndTime.trim()); } return lastModifiedDate; } catch (ParseException pe) { LOG.warn("Could not parse date", pe); return null; } } /** * Parse username from VSS file history * * @param userLine username line * @return the user name who made the modification */ public String parseUser(final String userLine) { final int userIndex = "User: ".length(); return userLine.substring(userIndex, userLine.indexOf("Date: ") - 1).trim(); } /** * Constructs the vssDateTimeFormat based on the dateFormat for this element. * * @see #setDateFormat */ private void constructVssDateTimeFormat() { vssDateTimeFormat = new SimpleDateFormat("'Date: '" + dateFormat + " 'Time: '" + timeFormat, Locale.US); } protected SimpleDateFormat getVssDateTimeFormat() { return vssDateTimeFormat; } }