/********************************************************************************
* CruiseControl, a Continuous Integration Toolkit
* Copyright (c) 2001-2003, 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.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import net.sourceforge.cruisecontrol.CruiseControlException;
import net.sourceforge.cruisecontrol.Modification;
import net.sourceforge.cruisecontrol.SourceControl;
import net.sourceforge.cruisecontrol.Modification.ModifiedFile;
import net.sourceforge.cruisecontrol.util.CVSDateUtil;
import net.sourceforge.cruisecontrol.util.Commandline;
import net.sourceforge.cruisecontrol.util.DiscardConsumer;
import net.sourceforge.cruisecontrol.util.OSEnvironment;
import net.sourceforge.cruisecontrol.util.StreamLogger;
import net.sourceforge.cruisecontrol.util.StreamPumper;
import net.sourceforge.cruisecontrol.util.ValidationHelper;
import net.sourceforge.cruisecontrol.util.IO;
import org.apache.log4j.Logger;
/**
* This class implements the SourceControlElement methods for a CVS repository. The call to CVS is assumed to work
* without any setup. This implies that if the authentication type is pserver the call to cvs login should be done prior
* to calling this class. <p> There are also differing CVS client/server implementations (e.g. the <i>official</i> CVS
* and the CVSNT fork). <p> Note that the LOG formats of the official CVS have changed starting from version 1.12.9.
* This class currently knows of 2 different outputs referred to as the 'old' and the 'new' output formats.
*
* @author <a href="mailto:pj@thoughtworks.com">Paul Julius</a>
* @author Robert Watkins
* @author Frederic Lavigne
* @author <a href="mailto:jcyip@thoughtworks.com">Jason Yip</a>
* @author Marc Paquette
* @author <a href="mailto:johnny.cass@epiuse.com">Johnny Cass</a>
* @author <a href="mailto:m@loonsoft.com">McClain Looney</a>
* @author Detlef Keil
*/
public class ConcurrentVersionsSystem implements SourceControl, Cloneable {
private static final long serialVersionUID = -3714548093682602092L;
/**
* name of the official cvs as returned as part of the 'cvs version' command output
*/
static final String OFFICIAL_CVS_NAME = "CVS";
static final Version DEFAULT_CVS_SERVER_VERSION = new Version(OFFICIAL_CVS_NAME, "1.11");
public static final String LOG_DATE_FORMAT = "yyyy/MM/dd HH:mm:ss z";
private boolean reallyQuiet;
private String compression;
/**
* This paramter has only effect if local is set and is not itself
* a under control of CVS (that is, does not have a CVS subdirectory).
* If set to true all subdirectories are searched recursively.
* All subdirectories which are under control of CVS are searched for
* modifications in the usual manner.
*/
private boolean recurseLocalWorkingCopy;
/**
* Represents the version of a CVS client or server
*/
static class Version implements Serializable {
private static final long serialVersionUID = -2433230091640056090L;
private final String cvsName;
private final String cvsVersion;
public Version(String name, String version) {
if (name == null) {
throw new IllegalArgumentException("name can't be null");
}
if (version == null) {
throw new IllegalArgumentException("version can't be null");
}
this.cvsName = name;
this.cvsVersion = version;
}
public String getCvsName() {
return cvsName;
}
public String getCvsVersion() {
return cvsVersion;
}
public boolean equals(Object o) {
if (this == o) {
return true;
} else if (!(o instanceof Version)) {
return false;
}
final Version version = (Version) o;
if (!cvsName.equals(version.cvsName)) {
return false;
} else if (!cvsVersion.equals(version.cvsVersion)) {
return false;
}
return true;
}
public int hashCode() {
int result;
result = cvsName.hashCode();
result = 29 * result + cvsVersion.hashCode();
return result;
}
public String toString() {
return cvsName + " " + cvsVersion;
}
}
private SourceControlProperties properties = new SourceControlProperties();
/**
* CVS allows for mapping user names to email addresses. If CVSROOT/users exists, it's contents will be parsed and
* stored in this hashtable.
*/
private Hashtable<String, String> mailAliases;
/**
* The caller can provide the CVSROOT to use when calling CVS, or the CVSROOT environment variable will be used.
*/
private String cvsroot;
/**
* The caller must indicate where the local copy of the repository exists.
*/
private String local;
/**
* The CVS tag we are dealing with.
*/
private String tag;
/**
* The CVS module we are dealing with.
*/
private String module;
/**
* The version of the cvs server
*/
private Version cvsServerVersion;
/**
* If set to true, the mailAliases from CVSROOT/users are not fetched.
*/
private boolean skipEmailsFetching = false;
/**
* enable logging for this class
*/
private static final Logger LOG = Logger.getLogger(ConcurrentVersionsSystem.class);
/**
* This line delimits separate files in the CVS LOG information.
*/
private static final String CVS_FILE_DELIM = "==================================================================="
+ "==========";
/**
* This is the keyword that precedes the name of the RCS filename in the CVS LOG information.
*/
private static final String CVS_RCSFILE_LINE = "RCS file: ";
/**
* This is the keyword that precedes the name of the working filename in the CVS LOG information.
*/
private static final String CVS_WORKINGFILE_LINE = "Working file: ";
/**
* This line delimits the different revisions of a file in the CVS LOG information.
*/
private static final String CVS_REVISION_DELIM = "----------------------------";
/**
* This is the keyword that precedes the timestamp of a file revision in the CVS LOG information.
*/
private static final String CVS_REVISION_DATE = "date:";
/**
* This is the name of the tip of the main branch, which needs special handling with the LOG entry parser
*/
private static final String CVS_HEAD_TAG = "HEAD";
/**
* This is the keyword that tells us when we have reached the end of the header as found in the CVS LOG information.
*/
private static final String CVS_DESCRIPTION = "description:";
/**
* This is a state keyword which indicates that a revision to a file was not relevant to the current branch, or the
* revision consisted of a deletion of the file (removal from branch..).
*/
private static final String CVS_REVISION_DEAD = "dead";
/**
* System dependent new line separator.
*/
private static final String NEW_LINE = System.getProperty("line.separator");
/**
* This is the date format returned in the LOG information from CVS.
*/
private final SimpleDateFormat logDateFormatter = new SimpleDateFormat(LOG_DATE_FORMAT);
/**
* Sets the CVSROOT for all calls to CVS.
*
* @param cvsroot
* CVSROOT to use.
*/
public void setCvsRoot(String cvsroot) {
this.cvsroot = cvsroot;
}
/**
* Sets the local working copy to use when making calls to CVS.
*
* @param local
* String indicating the relative or absolute path to the local working copy of the module of which to
* find the LOG history.
*/
public void setLocalWorkingCopy(String local) {
this.local = local;
}
/**
* Sets the behavior when a local working coppy is set which is
* not under control of CVS itself (that is, does not have a CVS
* subdirectory).
* @param recurseLocalWorkingCopy If set to true all subdirectories are searched recursively.
* All subdirectories which are under control of CVS are searched for
* modifications in the usual manner.
*/
public void setRecurseLocalWorkingCopy(final boolean recurseLocalWorkingCopy) {
this.recurseLocalWorkingCopy = recurseLocalWorkingCopy;
}
/**
* Set the cvs tag. Note this should work with names, numbers, and anything else you can put on LOG -rTAG
*
* @param tag
* the cvs tag
*/
public void setTag(String tag) {
this.tag = tag;
}
/**
* Set the cvs module name. Note that this is only used when localworkingcopy is not set.
*
* @param module
* the cvs module
*/
public void setModule(String module) {
this.module = module;
}
public void setProperty(String property) {
properties.assignPropertyName(property);
}
public void setPropertyOnDelete(String propertyOnDelete) {
properties.assignPropertyOnDeleteName(propertyOnDelete);
}
/**
* @param reallyQuiet When true, this class should use the -Q cvs option instead of -q for the LOG command.
*/
public void setReallyQuiet(boolean reallyQuiet) {
this.reallyQuiet = reallyQuiet;
}
/**
* Sets the compression level used for the call to cvs, corresponding to the "-z" command line parameter. When not
* set, the command line parameter is NOT included.
*
* @param level Valid levels are 1 (high speed, low compression) to 9 (low speed, high compression), or 0
* to disable compression.
*/
public void setCompression(String level) {
compression = level;
}
/**
* @param skipEmailsFetching If set to true, the CVSROOT/users won't be fetched.
*/
public void setSkipEmailsFetching(final boolean skipEmailsFetching) {
this.skipEmailsFetching = skipEmailsFetching;
}
protected Version getCvsServerVersion() {
if (cvsServerVersion == null) {
Commandline commandLine = getCommandline();
commandLine.setExecutable("cvs");
if (cvsroot != null) {
commandLine.createArguments("-d", cvsroot);
}
commandLine.createArgument().setLine("version");
Process p = null;
try {
if (local != null) {
commandLine.setWorkingDirectory(local);
}
p = commandLine.execute();
Thread stderr = logErrorStream(p);
InputStream is = p.getInputStream();
BufferedReader in = new BufferedReader(new InputStreamReader(is));
cvsServerVersion = extractCVSServerVersionFromCVSVersionCommandOutput(in);
LOG.debug("cvs server version: " + cvsServerVersion);
p.waitFor();
stderr.join();
IO.close(p);
} catch (IOException e) {
LOG.error("Failed reading cvs server version", e);
} catch (CruiseControlException e) {
LOG.error("Failed reading cvs server version", e);
} catch (InterruptedException e) {
LOG.error("Failed reading cvs server version", e);
}
if (p == null || p.exitValue() != 0 || cvsServerVersion == null) {
if (p == null) {
LOG.debug("Process p was null in CVS.getCvsServerVersion()");
} else {
LOG.debug("Process exit value = " + p.exitValue());
}
cvsServerVersion = DEFAULT_CVS_SERVER_VERSION;
LOG.warn("problem getting cvs server version; using " + cvsServerVersion);
}
}
return cvsServerVersion;
}
/**
* This method retrieves the cvs server version from the specified output. The line it parses will have the
* following format:
*
* <pre>
* Server: Concurrent Versions System (CVS) 1.11.16 (client/server)
* </pre>
*
* @param in reader
* @return the version of null if the version couldn't be extracted
* @throws IOException if something breaks
*/
private Version extractCVSServerVersionFromCVSVersionCommandOutput(final BufferedReader in) throws IOException {
String line = in.readLine();
if (line == null) {
return null;
}
if (line.startsWith("Client:")) {
line = in.readLine();
if (line == null) {
return null;
}
if (!line.startsWith("Server:")) {
LOG.warn("Warning expected a line starting with \"Server:\" but got " + line);
// we try anyway
}
}
LOG.debug("server version line: " + line);
final int nameBegin = line.indexOf(" (");
final int nameEnd = line.indexOf(") ", nameBegin);
final String name;
final String version;
if (nameBegin == -1 || nameEnd < nameBegin || nameBegin + 2 >= line.length()) {
LOG.warn("cvs server version name couldn't be parsed from " + line);
return null;
}
name = line.substring(nameBegin + 2, nameEnd);
int verEnd = line.indexOf(" ", nameEnd + 2);
if (verEnd < nameEnd + 2) {
LOG.warn("cvs server version number couldn't be parsed from " + line);
return null;
}
version = line.substring(nameEnd + 2, verEnd);
return new Version(name, version);
}
public boolean isCvsNewOutputFormat() {
final Version version = getCvsServerVersion();
if (OFFICIAL_CVS_NAME.equals(version.getCvsName())) {
final String csv = version.getCvsVersion();
final StringTokenizer st = new StringTokenizer(csv, ".");
try {
st.nextToken();
final int subversion = Integer.parseInt(st.nextToken());
if (subversion > 11) {
if (subversion == 12) {
if (Integer.parseInt(st.nextToken()) < 9) {
return false;
}
}
return true;
}
} catch (Throwable e) {
LOG.warn("problem identifying cvs server. Assuming output is of 'old' type");
}
}
return false;
}
public Map<String, String> getProperties() {
return properties.getPropertiesAndReset();
}
/*
* for mocking
*/
protected OSEnvironment getOSEnvironment() {
return new OSEnvironment();
}
public void validate() throws CruiseControlException {
ValidationHelper.assertFalse(local == null && (cvsroot == null || module == null),
"must specify either 'localWorkingCopy' or 'cvsroot' and 'module' on CVS");
ValidationHelper.assertFalse(local != null && (cvsroot != null || module != null),
"if 'localWorkingCopy' is specified then cvsroot and module are not allowed on CVS");
ValidationHelper.assertFalse(local == null && recurseLocalWorkingCopy,
"'recurseLocalWorkingCopy' can only be set to true when 'localWorkingCopy' is specified.");
ValidationHelper.assertFalse(local != null && !new File(local).exists(), "Local working copy \"" + local
+ "\" does not exist!");
if (compression != null) {
ValidationHelper.assertIntegerInRange(compression, 0, 9,
"'compression' must be an integer between 0 and 9, inclusive.");
}
}
/**
* Returns a List of Modifications detailing all the changes between the last build and the latest revision at the
* repository
*
* @param lastBuild
* last build time
* @return maybe empty, never null.
*/
public List<Modification> getModifications(final Date lastBuild, final Date now) {
mailAliases = getMailAliases();
if (recurseLocalWorkingCopy) {
if (localDirectoryNotUnderCVS()) {
return getModificationsFromSubdirectories(lastBuild, now);
}
}
List<Modification> mods = null;
try {
mods = execHistoryCommand(buildHistoryCommand(lastBuild, now));
} catch (Exception e) {
LOG.error("Log command failed to execute successfully", e);
}
if (mods == null) {
return new ArrayList<Modification>();
}
return mods;
}
private List<Modification> getModificationsFromSubdirectories(Date lastBuild, Date now) {
List<Modification> modifications = new ArrayList<Modification>();
File[] subDirectories = new File(local).listFiles(new FileFilter() {
public boolean accept(File pathname) {
return pathname.isDirectory();
}
});
if (subDirectories == null) {
return modifications;
}
for (File dir : subDirectories) {
try {
ConcurrentVersionsSystem delegate = (ConcurrentVersionsSystem) this.clone();
delegate.setLocalWorkingCopy(dir.getPath());
delegate.setSkipEmailsFetching(true);
List<Modification> modsFromSubdirectory = delegate.getModifications(lastBuild, now);
for (Modification mod : modsFromSubdirectory) {
for (Object o : mod.files) {
ModifiedFile modfile = (ModifiedFile) o;
modfile.folderName = dir.getName() + '/' + modfile.folderName;
}
}
modifications.addAll(modsFromSubdirectory);
} catch (CloneNotSupportedException e) {
LOG.error("this should never happen", e);
throw new RuntimeException(e);
}
}
return modifications;
}
private boolean localDirectoryNotUnderCVS() {
File cvsDir = new File(local, "CVS");
return !cvsDir.exists() || !cvsDir.isDirectory();
}
/**
* Get CVS's idea of user/address mapping. Only runs once per class instance. Won't run if the mailAlias was already
* set.
*
* @return a Hashtable containing the mapping defined in CVSROOT/users. If CVSROOT/users doesn't exist, an empty
* Hashtable is returned.
*/
private Hashtable<String, String> getMailAliases() {
if (mailAliases == null) {
if (skipEmailsFetching) {
mailAliases = new Hashtable<String, String>();
return mailAliases;
}
mailAliases = new Hashtable<String, String>();
Commandline commandLine = getCommandline();
commandLine.setExecutable("cvs");
if (cvsroot != null) {
commandLine.createArguments("-d", cvsroot);
}
commandLine.createArgument().setLine("-q co -p CVSROOT/users");
Process p = null;
try {
if (local != null) {
commandLine.setWorkingDirectory(local);
}
p = commandLine.execute();
Thread stderr = logErrorStream(p);
InputStream is = p.getInputStream();
BufferedReader in = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = in.readLine()) != null) {
addAliasToMap(line);
}
p.waitFor();
stderr.join();
IO.close(p);
} catch (Exception e) {
LOG.error("Failed reading mail aliases", e);
}
if (p == null || p.exitValue() != 0) {
if (p == null) {
LOG.debug("Process p was null in CVS.getMailAliases()");
} else {
LOG.debug("Process exit value = " + p.exitValue());
}
LOG.warn("problem getting CVSROOT/users; using empty email map");
mailAliases = new Hashtable<String, String>();
}
}
return mailAliases;
}
void addAliasToMap(String line) {
LOG.debug("Mapping " + line);
int colon = line.indexOf(':');
if (colon >= 0) {
String user = line.substring(0, colon);
String address = line.substring(colon + 1);
mailAliases.put(user, address);
}
}
/**
* @param lastBuildTime last build date
* @param checkTime current time
* @return CommandLine for {@code "cvs -d CVSROOT -q LOG -N -dlastbuildtime<checktime "}
* @throws CruiseControlException if something breaks
*/
public Commandline buildHistoryCommand(final Date lastBuildTime, final Date checkTime)
throws CruiseControlException {
final Commandline commandLine = getCommandline();
commandLine.setExecutable("cvs");
if (compression != null) {
commandLine.createArgument("-z" + compression);
}
if (cvsroot != null) {
commandLine.createArguments("-d", cvsroot);
}
commandLine.createArgument(reallyQuiet ? "-Q" : "-q");
if (local != null) {
commandLine.setWorkingDirectory(local);
commandLine.createArgument("log");
} else {
commandLine.createArgument("rlog");
}
commandLine.createArgument("-N");
commandLine.createArgument("-S");
final String dateRange = formatCVSDate(lastBuildTime) + "<" + formatCVSDate(checkTime);
commandLine.createArgument("-d" + dateRange);
if (!useHead()) {
// add -b and -rTAG to list changes relative to the current branch,
// not relative to the default branch, which is HEAD
// note: -r cannot have a space between itself and the tag spec.
commandLine.createArgument("-r" + tag);
} else {
// This is used to include the head only if a Tag is not specified.
commandLine.createArgument("-b");
}
if (local == null) {
commandLine.createArgument(module);
}
return commandLine;
}
// factory method for mock...
protected Commandline getCommandline() {
return new Commandline();
}
static String formatCVSDate(Date date) {
return CVSDateUtil.formatCVSDate(date);
}
/**
* Parses the input stream, which should be from the cvs LOG command. This method will format the data found in the
* input stream into a List of Modification instances.
*
* @param input
* InputStream to get LOG data from.
* @return List of Modification elements, maybe empty never null.
* @throws IOException if something breaks
*/
protected List<Modification> parseStream(final InputStream input) throws IOException {
final BufferedReader reader = new BufferedReader(new InputStreamReader(input));
// Read to the first RCS file name. The first entry in the LOG
// information will begin with this line. A CVS_FILE_DELIMITER is NOT
// present. If no RCS file lines are found then there is nothing to do.
String line = readToNotPast(reader, CVS_RCSFILE_LINE, null);
final ArrayList<Modification> mods = new ArrayList<Modification>();
while (line != null) {
// Parse the single file entry, which may include several
// modifications.
final List<Modification> returnList = parseEntry(reader, line);
// Add all the modifications to the local list.
mods.addAll(returnList);
// Read to the next RCS file line. The CVS_FILE_DELIMITER may have
// been consumed by the parseEntry method, so we cannot read to it.
line = readToNotPast(reader, CVS_RCSFILE_LINE, null);
}
return mods;
}
private void getRidOfLeftoverData(InputStream stream) {
new StreamPumper(stream, new DiscardConsumer()).run();
}
List<Modification> execHistoryCommand(final Commandline command) throws Exception {
final Process p = command.execute();
final Thread stderr = logErrorStream(p);
final InputStream cvsLogStream = p.getInputStream();
final List<Modification> mods = parseStream(cvsLogStream);
getRidOfLeftoverData(cvsLogStream);
p.waitFor();
stderr.join();
IO.close(p);
return mods;
}
protected void setMailAliases(final Hashtable<String, String> mailAliases) {
this.mailAliases = mailAliases;
}
private static Thread logErrorStream(Process p) {
return logErrorStream(p.getErrorStream());
}
static Thread logErrorStream(InputStream error) {
Thread stderr = new Thread(StreamLogger.getWarnPumper(LOG, error));
stderr.start();
return stderr;
}
// (PENDING) Extract CVSEntryParser class
/**
* Parses a single file entry from the reader. This entry may contain zero or more revisions. This method may
* consume the next CVS_FILE_DELIMITER line from the reader, but no further. <p/> When the LOG is related to a non
* branch tag, only the last modification for each file will be listed.
*
* @param reader
* Reader to parse data from.
* @param rcsLine line to parse
* @return modifications found in this entry; maybe empty, never null.
* @throws IOException if something breaks
*/
private List<Modification> parseEntry(final BufferedReader reader, final String rcsLine) throws IOException {
final ArrayList<Modification> mods = new ArrayList<Modification>();
String nextLine = "";
// Read to the working file name line to get the filename.
// If working file name line isn't found we'll extract is from the RCS file line
final String workingFileName;
if (module != null && cvsroot != null) {
final String repositoryRoot = cvsroot.substring(cvsroot.lastIndexOf(":") + 1);
final int startAt = "RCS file: ".length() + repositoryRoot.length();
workingFileName = rcsLine.substring(startAt, rcsLine.length() - 2);
} else {
final String workingFileLine = readToNotPast(reader, CVS_WORKINGFILE_LINE, null);
workingFileName = workingFileLine.substring(CVS_WORKINGFILE_LINE.length());
}
final boolean newCVSVersion = isCvsNewOutputFormat();
while (nextLine != null && !nextLine.startsWith(CVS_FILE_DELIM)) {
nextLine = readToNotPast(reader, "revision", CVS_FILE_DELIM);
if (nextLine == null) {
// No more revisions for this file.
break;
}
StringTokenizer tokens = new StringTokenizer(nextLine, " ");
tokens.nextToken();
final String revision = tokens.nextToken();
// Read to the revision date. It is ASSUMED that each revision
// section will include this date information line.
nextLine = readToNotPast(reader, CVS_REVISION_DATE, CVS_FILE_DELIM);
if (nextLine == null) {
break;
}
tokens = new StringTokenizer(nextLine, " \t\n\r\f;");
// First token is the keyword for date, then the next two should be
// the date and time stamps.
tokens.nextToken();
final String dateStamp = tokens.nextToken();
final String timeStamp = tokens.nextToken();
// New format sometimes has a +0000 in it. This skips it if we don't see
// the start of the author: section
final String isThisTimeOffset = tokens.nextToken();
if (!isThisTimeOffset.equals("author:")) {
tokens.nextToken();
}
// The next token should be the author keyword, then the author name.
final String authorName = tokens.nextToken();
// The next token should be the state keyword, then the state name.
tokens.nextToken();
final String stateKeyword = tokens.nextToken();
// if no lines keyword then file is added
final boolean isAdded = !tokens.hasMoreTokens();
// All the text from now to the next revision delimiter or working
// file delimiter constitutes the message.
String message = "";
nextLine = reader.readLine();
boolean multiLine = false;
while (nextLine != null && !nextLine.startsWith(CVS_FILE_DELIM)
&& !nextLine.startsWith(CVS_REVISION_DELIM)) {
if (multiLine) {
message += NEW_LINE;
} else {
multiLine = true;
}
message += nextLine;
// Go to the next line.
nextLine = reader.readLine();
}
final Modification nextModification = new Modification("cvs");
nextModification.revision = revision;
final int lastSlashIndex = workingFileName.lastIndexOf("/");
final String fileName;
String folderName = null;
fileName = workingFileName.substring(lastSlashIndex + 1);
if (lastSlashIndex != -1) {
folderName = workingFileName.substring(0, lastSlashIndex);
}
final Modification.ModifiedFile modfile = nextModification.createModifiedFile(fileName, folderName);
modfile.revision = nextModification.revision;
try {
if (newCVSVersion) {
nextModification.modifiedTime = CVSDateUtil.parseCVSDate(dateStamp + " " + timeStamp + " GMT");
} else {
nextModification.modifiedTime = logDateFormatter.parse(dateStamp + " " + timeStamp + " GMT");
}
} catch (ParseException pe) {
LOG.error("Error parsing cvs LOG for date and time", pe);
return null;
}
nextModification.userName = authorName;
final String address = mailAliases.get(authorName);
if (address != null) {
nextModification.emailAddress = address;
}
nextModification.comment = message;
if (stateKeyword.equalsIgnoreCase(CVS_REVISION_DEAD)
&& message.indexOf("was initially added on branch") != -1) {
LOG.debug("skipping branch addition activity for " + nextModification);
// this prevents additions to a branch from showing up as action "deleted" from head
continue;
}
if (stateKeyword.equalsIgnoreCase(CVS_REVISION_DEAD)) {
modfile.action = "deleted";
properties.deletionFound();
} else if (isAdded) {
modfile.action = "added";
} else {
modfile.action = "modified";
}
properties.modificationFound();
mods.add(nextModification);
}
return mods;
}
/**
* Find the CVS branch revision name, when the tag is not HEAD The reader will consume all lines up to the next
* description.
*
* @param reader input stream to read
* @return the branch revision name, or <code>null</code> if not applicable or none was found.
* @throws IOException if something breaks
*/
private String parseBranchRevisionName(final BufferedReader reader) throws IOException {
String branchRevisionName = null;
if (!useHead()) {
// Look for the revision of the form "tag: *.(0.)y ". this doesn't work for HEAD
// get line with branch revision on it.
String branchRevisionLine = readToNotPast(reader, "\t" + tag + ": ", CVS_DESCRIPTION);
if (branchRevisionLine != null) {
// Look for the revision of the form "tag: *.(0.)y ", return "*.y"
branchRevisionName = branchRevisionLine.substring(tag.length() + 3);
if (branchRevisionName.charAt(branchRevisionName.lastIndexOf(".") - 1) == '0') {
branchRevisionName = branchRevisionName.substring(0, branchRevisionName.lastIndexOf(".") - 2)
+ branchRevisionName.substring(branchRevisionName.lastIndexOf("."));
}
}
}
return branchRevisionName;
}
/**
* This method will consume lines from the reader up to the line that begins with the String specified but not past
* a line that begins with the notPast String. If the line that begins with the beginsWith String is found then it
* will be returned. Otherwise null is returned.
*
* @param reader
* Reader to read lines from.
* @param beginsWith
* String to match to the beginning of a line.
* @param notPast
* String which indicates that lines should stop being consumed, even if the begins with match has not
* been found. Pass null to this method to ignore this string.
* @return String that begin as indicated, or null if none matched to the end of the reader or the notPast line was
* found.
* @throws IOException if something breaks
*/
private static String readToNotPast(final BufferedReader reader, final String beginsWith, final String notPast)
throws IOException {
final boolean checkingNotPast = notPast != null;
String nextLine = reader.readLine();
while (nextLine != null && !nextLine.startsWith(beginsWith)) {
if (checkingNotPast && nextLine.startsWith(notPast)) {
return null;
}
nextLine = reader.readLine();
}
return nextLine;
}
boolean useHead() {
return tag == null || tag.equals(CVS_HEAD_TAG) || tag.equals("");
}
}