001: /********************************************************************************
002: * CruiseControl, a Continuous Integration Toolkit
003: * Copyright (c) 2001-2003, ThoughtWorks, Inc.
004: * 200 E. Randolph, 25th Floor
005: * Chicago, IL 60601 USA
006: * All rights reserved.
007: *
008: * Redistribution and use in source and binary forms, with or without
009: * modification, are permitted provided that the following conditions
010: * are met:
011: *
012: * + Redistributions of source code must retain the above copyright
013: * notice, this list of conditions and the following disclaimer.
014: *
015: * + Redistributions in binary form must reproduce the above
016: * copyright notice, this list of conditions and the following
017: * disclaimer in the documentation and/or other materials provided
018: * with the distribution.
019: *
020: * + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the
021: * names of its contributors may be used to endorse or promote
022: * products derived from this software without specific prior
023: * written permission.
024: *
025: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
026: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
027: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
028: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
029: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
030: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
031: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
032: * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
033: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
034: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
035: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
036: ********************************************************************************/package net.sourceforge.cruisecontrol.sourcecontrols;
037:
038: import java.io.BufferedReader;
039: import java.io.File;
040: import java.io.IOException;
041: import java.io.InputStream;
042: import java.io.InputStreamReader;
043: import java.io.Serializable;
044: import java.text.ParseException;
045: import java.text.SimpleDateFormat;
046: import java.util.ArrayList;
047: import java.util.Date;
048: import java.util.Hashtable;
049: import java.util.List;
050: import java.util.Map;
051: import java.util.StringTokenizer;
052:
053: import net.sourceforge.cruisecontrol.CruiseControlException;
054: import net.sourceforge.cruisecontrol.Modification;
055: import net.sourceforge.cruisecontrol.SourceControl;
056: import net.sourceforge.cruisecontrol.util.CVSDateUtil;
057: import net.sourceforge.cruisecontrol.util.Commandline;
058: import net.sourceforge.cruisecontrol.util.DiscardConsumer;
059: import net.sourceforge.cruisecontrol.util.OSEnvironment;
060: import net.sourceforge.cruisecontrol.util.StreamLogger;
061: import net.sourceforge.cruisecontrol.util.StreamPumper;
062: import net.sourceforge.cruisecontrol.util.ValidationHelper;
063: import net.sourceforge.cruisecontrol.util.IO;
064:
065: import org.apache.log4j.Logger;
066:
067: /**
068: * This class implements the SourceControlElement methods for a CVS repository. The call to CVS is assumed to work
069: * without any setup. This implies that if the authentication type is pserver the call to cvs login should be done prior
070: * to calling this class. <p/> There are also differing CVS client/server implementations (e.g. the <i>official</i> CVS
071: * and the CVSNT fork). <p/> Note that the log formats of the official CVS have changed starting from version 1.12.9.
072: * This class currently knows of 2 different outputs referred to as the 'old' and the 'new' output formats.
073: *
074: * @author <a href="mailto:pj@thoughtworks.com">Paul Julius</a>
075: * @author Robert Watkins
076: * @author Frederic Lavigne
077: * @author <a href="mailto:jcyip@thoughtworks.com">Jason Yip</a>
078: * @author Marc Paquette
079: * @author <a href="mailto:johnny.cass@epiuse.com">Johnny Cass</a>
080: * @author <a href="mailto:m@loonsoft.com">McClain Looney</a>
081: */
082: public class ConcurrentVersionsSystem implements SourceControl {
083: private static final long serialVersionUID = -3714548093682602092L;
084: /**
085: * name of the official cvs as returned as part of the 'cvs version' command output
086: */
087: static final String OFFICIAL_CVS_NAME = "CVS";
088: static final Version DEFAULT_CVS_SERVER_VERSION = new Version(
089: OFFICIAL_CVS_NAME, "1.11");
090: public static final String LOG_DATE_FORMAT = "yyyy/MM/dd HH:mm:ss z";
091: private boolean reallyQuiet;
092: private String compression;
093:
094: /**
095: * Represents the version of a CVS client or server
096: */
097: static class Version implements Serializable {
098:
099: private static final long serialVersionUID = -2433230091640056090L;
100:
101: private final String cvsName;
102: private final String cvsVersion;
103:
104: public Version(String name, String version) {
105: if (name == null) {
106: throw new IllegalArgumentException("name can't be null");
107: }
108: if (version == null) {
109: throw new IllegalArgumentException(
110: "version can't be null");
111: }
112: this .cvsName = name;
113: this .cvsVersion = version;
114: }
115:
116: public String getCvsName() {
117: return cvsName;
118: }
119:
120: public String getCvsVersion() {
121: return cvsVersion;
122: }
123:
124: public boolean equals(Object o) {
125: if (this == o) {
126: return true;
127: } else if (!(o instanceof Version)) {
128: return false;
129: }
130:
131: final Version version = (Version) o;
132:
133: if (!cvsName.equals(version.cvsName)) {
134: return false;
135: } else if (!cvsVersion.equals(version.cvsVersion)) {
136: return false;
137: }
138:
139: return true;
140: }
141:
142: public int hashCode() {
143: int result;
144: result = cvsName.hashCode();
145: result = 29 * result + cvsVersion.hashCode();
146: return result;
147: }
148:
149: public String toString() {
150: return cvsName + " " + cvsVersion;
151: }
152: }
153:
154: private SourceControlProperties properties = new SourceControlProperties();
155:
156: /**
157: * CVS allows for mapping user names to email addresses. If CVSROOT/users exists, it's contents will be parsed and
158: * stored in this hashtable.
159: */
160: private Hashtable mailAliases;
161:
162: /**
163: * The caller can provide the CVSROOT to use when calling CVS, or the CVSROOT environment variable will be used.
164: */
165: private String cvsroot;
166:
167: /**
168: * The caller must indicate where the local copy of the repository exists.
169: */
170: private String local;
171:
172: /**
173: * The CVS tag we are dealing with.
174: */
175: private String tag;
176:
177: /**
178: * The CVS module we are dealing with.
179: */
180: private String module;
181:
182: /**
183: * The version of the cvs server
184: */
185: private Version cvsServerVersion;
186:
187: /**
188: * enable logging for this class
189: */
190: private static Logger log = Logger
191: .getLogger(ConcurrentVersionsSystem.class);
192:
193: /**
194: * This line delimits separate files in the CVS log information.
195: */
196: private static final String CVS_FILE_DELIM = "==================================================================="
197: + "==========";
198:
199: /**
200: * This is the keyword that precedes the name of the RCS filename in the CVS log information.
201: */
202: private static final String CVS_RCSFILE_LINE = "RCS file: ";
203:
204: /**
205: * This is the keyword that precedes the name of the working filename in the CVS log information.
206: */
207: private static final String CVS_WORKINGFILE_LINE = "Working file: ";
208:
209: /**
210: * This line delimits the different revisions of a file in the CVS log information.
211: */
212: private static final String CVS_REVISION_DELIM = "----------------------------";
213:
214: /**
215: * This is the keyword that precedes the timestamp of a file revision in the CVS log information.
216: */
217: private static final String CVS_REVISION_DATE = "date:";
218:
219: /**
220: * This is the name of the tip of the main branch, which needs special handling with the log entry parser
221: */
222: private static final String CVS_HEAD_TAG = "HEAD";
223:
224: /**
225: * This is the keyword that tells us when we have reached the end of the header as found in the CVS log information.
226: */
227: private static final String CVS_DESCRIPTION = "description:";
228:
229: /**
230: * This is a state keyword which indicates that a revision to a file was not relevant to the current branch, or the
231: * revision consisted of a deletion of the file (removal from branch..).
232: */
233: private static final String CVS_REVISION_DEAD = "dead";
234:
235: /**
236: * System dependent new line separator.
237: */
238: private static final String NEW_LINE = System
239: .getProperty("line.separator");
240:
241: /**
242: * This is the date format returned in the log information from CVS.
243: */
244: private final SimpleDateFormat logDateFormatter = new SimpleDateFormat(
245: LOG_DATE_FORMAT);
246:
247: /**
248: * Sets the CVSROOT for all calls to CVS.
249: *
250: * @param cvsroot
251: * CVSROOT to use.
252: */
253: public void setCvsRoot(String cvsroot) {
254: this .cvsroot = cvsroot;
255: }
256:
257: /**
258: * Sets the local working copy to use when making calls to CVS.
259: *
260: * @param local
261: * String indicating the relative or absolute path to the local working copy of the module of which to
262: * find the log history.
263: */
264: public void setLocalWorkingCopy(String local) {
265: this .local = local;
266: }
267:
268: /**
269: * Set the cvs tag. Note this should work with names, numbers, and anything else you can put on log -rTAG
270: *
271: * @param tag
272: * the cvs tag
273: */
274: public void setTag(String tag) {
275: this .tag = tag;
276: }
277:
278: /**
279: * Set the cvs module name. Note that this is only used when localworkingcopy is not set.
280: *
281: * @param module
282: * the cvs module
283: */
284: public void setModule(String module) {
285: this .module = module;
286: }
287:
288: public void setProperty(String property) {
289: properties.assignPropertyName(property);
290: }
291:
292: public void setPropertyOnDelete(String propertyOnDelete) {
293: properties.assignPropertyOnDeleteName(propertyOnDelete);
294: }
295:
296: /**
297: * @param reallyQuiet When true, this class should use the -Q cvs option instead of -q for the log command.
298: */
299: public void setReallyQuiet(boolean reallyQuiet) {
300: this .reallyQuiet = reallyQuiet;
301: }
302:
303: /**
304: * Sets the compression level used for the call to cvs, corresponding to the "-z" command line parameter. When not
305: * set, the command line parameter is NOT included.
306: *
307: * @param level Valid levels are 1 (high speed, low compression) to 9 (low speed, high compression), or 0
308: * to disable compression.
309: */
310: public void setCompression(String level) {
311: compression = level;
312: }
313:
314: protected Version getCvsServerVersion() {
315: if (cvsServerVersion == null) {
316:
317: Commandline commandLine = getCommandline();
318: commandLine.setExecutable("cvs");
319:
320: if (cvsroot != null) {
321: commandLine.createArguments("-d", cvsroot);
322: }
323:
324: commandLine.createArgument().setLine("version");
325:
326: Process p = null;
327: try {
328: if (local != null) {
329: commandLine.setWorkingDirectory(local);
330: }
331:
332: p = commandLine.execute();
333: Thread stderr = logErrorStream(p);
334: InputStream is = p.getInputStream();
335: BufferedReader in = new BufferedReader(
336: new InputStreamReader(is));
337:
338: cvsServerVersion = extractCVSServerVersionFromCVSVersionCommandOutput(in);
339:
340: log.debug("cvs server version: " + cvsServerVersion);
341:
342: p.waitFor();
343: stderr.join();
344: IO.close(p);
345: } catch (IOException e) {
346: log.error("Failed reading cvs server version", e);
347: } catch (CruiseControlException e) {
348: log.error("Failed reading cvs server version", e);
349: } catch (InterruptedException e) {
350: log.error("Failed reading cvs server version", e);
351: }
352:
353: if (p == null || p.exitValue() != 0
354: || cvsServerVersion == null) {
355: if (p == null) {
356: log
357: .debug("Process p was null in CVS.getCvsServerVersion()");
358: } else {
359: log.debug("Process exit value = " + p.exitValue());
360: }
361: cvsServerVersion = DEFAULT_CVS_SERVER_VERSION;
362: log.warn("problem getting cvs server version; using "
363: + cvsServerVersion);
364: }
365: }
366: return cvsServerVersion;
367: }
368:
369: /**
370: * This method retrieves the cvs server version from the specified output. The line it parses will have the
371: * following format:
372: *
373: * <pre>
374: * Server: Concurrent Versions System (CVS) 1.11.16 (client/server)
375: * </pre>
376: *
377: * @param in
378: * @return the version of null if the version couldn't be extracted
379: * @throws IOException
380: */
381: private Version extractCVSServerVersionFromCVSVersionCommandOutput(
382: BufferedReader in) throws IOException {
383: String line = in.readLine();
384: if (line == null) {
385: return null;
386: }
387: if (line.startsWith("Client:")) {
388: line = in.readLine();
389: if (line == null) {
390: return null;
391: }
392: if (!line.startsWith("Server:")) {
393: log
394: .warn("Warning expected a line starting with \"Server:\" but got "
395: + line);
396: // we try anyway
397: }
398: }
399: log.debug("server version line: " + line);
400: int nameBegin = line.indexOf(" (");
401: int nameEnd = line.indexOf(") ", nameBegin);
402: final String name;
403: final String version;
404: if (nameBegin == -1 || nameEnd < nameBegin
405: || nameBegin + 2 >= line.length()) {
406: log.warn("cvs server version name couldn't be parsed from "
407: + line);
408: return null;
409: }
410: name = line.substring(nameBegin + 2, nameEnd);
411: int verEnd = line.indexOf(" ", nameEnd + 2);
412: if (verEnd < nameEnd + 2) {
413: log
414: .warn("cvs server version number couldn't be parsed from "
415: + line);
416: return null;
417: }
418: version = line.substring(nameEnd + 2, verEnd);
419:
420: return new Version(name, version);
421: }
422:
423: public boolean isCvsNewOutputFormat() {
424: Version version = getCvsServerVersion();
425: if (OFFICIAL_CVS_NAME.equals(version.getCvsName())) {
426: String csv = version.getCvsVersion();
427: StringTokenizer st = new StringTokenizer(csv, ".");
428: try {
429: st.nextToken();
430: int subversion = Integer.parseInt(st.nextToken());
431: if (subversion > 11) {
432: if (subversion == 12) {
433: if (Integer.parseInt(st.nextToken()) < 9) {
434: return false;
435: }
436: }
437: return true;
438: }
439: } catch (Throwable e) {
440: log
441: .warn("problem identifying cvs server. Assuming output is of 'old' type");
442: }
443: }
444: return false;
445: }
446:
447: public Map getProperties() {
448: return properties.getPropertiesAndReset();
449: }
450:
451: /**
452: * for mocking *
453: */
454: protected OSEnvironment getOSEnvironment() {
455: return new OSEnvironment();
456: }
457:
458: public void validate() throws CruiseControlException {
459: ValidationHelper
460: .assertFalse(local == null
461: && (cvsroot == null || module == null),
462: "must specify either 'localWorkingCopy' or 'cvsroot' and 'module' on CVS");
463: ValidationHelper
464: .assertFalse(
465: local != null
466: && (cvsroot != null || module != null),
467: "if 'localWorkingCopy' is specified then cvsroot and module are not allowed on CVS");
468: ValidationHelper.assertFalse(local != null
469: && !new File(local).exists(), "Local working copy \""
470: + local + "\" does not exist!");
471:
472: if (compression != null) {
473: ValidationHelper
474: .assertIntegerInRange(compression, 0, 9,
475: "'compression' must be an integer between 0 and 9, inclusive.");
476: }
477: }
478:
479: /**
480: * Returns a List of Modifications detailing all the changes between the last build and the latest revision at the
481: * repository
482: *
483: * @param lastBuild
484: * last build time
485: * @return maybe empty, never null.
486: */
487: public List getModifications(Date lastBuild, Date now) {
488: mailAliases = getMailAliases();
489:
490: List mods = null;
491: try {
492: mods = execHistoryCommand(buildHistoryCommand(lastBuild,
493: now));
494: } catch (Exception e) {
495: log.error("Log command failed to execute successfully", e);
496: }
497:
498: if (mods == null) {
499: return new ArrayList();
500: }
501: return mods;
502: }
503:
504: /**
505: * Get CVS's idea of user/address mapping. Only runs once per class instance. Won't run if the mailAlias was already
506: * set.
507: *
508: * @return a Hashtable containing the mapping defined in CVSROOT/users. If CVSROOT/users doesn't exist, an empty
509: * Hashtable is returned.
510: */
511: private Hashtable getMailAliases() {
512: if (mailAliases == null) {
513: mailAliases = new Hashtable();
514: Commandline commandLine = getCommandline();
515: commandLine.setExecutable("cvs");
516:
517: if (cvsroot != null) {
518: commandLine.createArguments("-d", cvsroot);
519: }
520:
521: commandLine.createArgument().setLine(
522: "-q co -p CVSROOT/users");
523:
524: Process p = null;
525: try {
526: if (local != null) {
527: commandLine.setWorkingDirectory(local);
528: }
529:
530: p = commandLine.execute();
531: Thread stderr = logErrorStream(p);
532: InputStream is = p.getInputStream();
533: BufferedReader in = new BufferedReader(
534: new InputStreamReader(is));
535:
536: String line;
537: while ((line = in.readLine()) != null) {
538: addAliasToMap(line);
539: }
540:
541: p.waitFor();
542: stderr.join();
543: IO.close(p);
544: } catch (Exception e) {
545: log.error("Failed reading mail aliases", e);
546: }
547:
548: if (p == null || p.exitValue() != 0) {
549: if (p == null) {
550: log
551: .debug("Process p was null in CVS.getMailAliases()");
552: } else {
553: log.debug("Process exit value = " + p.exitValue());
554: }
555: log
556: .warn("problem getting CVSROOT/users; using empty email map");
557: mailAliases = new Hashtable();
558: }
559: }
560:
561: return mailAliases;
562: }
563:
564: void addAliasToMap(String line) {
565: log.debug("Mapping " + line);
566: int colon = line.indexOf(':');
567:
568: if (colon >= 0) {
569: String user = line.substring(0, colon);
570: String address = line.substring(colon + 1);
571: mailAliases.put(user, address);
572:
573: }
574: }
575:
576: /**
577: * @param lastBuildTime
578: * @param checkTime
579: * @return CommandLine for "cvs -d CVSROOT -q log -N -dlastbuildtime<checktime "
580: */
581: public Commandline buildHistoryCommand(Date lastBuildTime,
582: Date checkTime) throws CruiseControlException {
583: Commandline commandLine = getCommandline();
584: commandLine.setExecutable("cvs");
585:
586: if (compression != null) {
587: commandLine.createArgument("-z" + compression);
588: }
589: if (cvsroot != null) {
590: commandLine.createArguments("-d", cvsroot);
591: }
592: commandLine.createArgument(reallyQuiet ? "-Q" : "-q");
593:
594: if (local != null) {
595: commandLine.setWorkingDirectory(local);
596: commandLine.createArgument("log");
597: } else {
598: commandLine.createArgument("rlog");
599: }
600:
601: if (useHead()) {
602: commandLine.createArgument("-N");
603: }
604: String dateRange = formatCVSDate(lastBuildTime) + "<"
605: + formatCVSDate(checkTime);
606: commandLine.createArgument("-d" + dateRange);
607:
608: if (!useHead()) {
609: // add -b and -rTAG to list changes relative to the current branch,
610: // not relative to the default branch, which is HEAD
611:
612: // note: -r cannot have a space between itself and the tag spec.
613: commandLine.createArgument("-r" + tag);
614: } else {
615: // This is used to include the head only if a Tag is not specified.
616: commandLine.createArgument("-b");
617: }
618:
619: if (local == null) {
620: commandLine.createArgument(module);
621: }
622:
623: return commandLine;
624: }
625:
626: // factory method for mock...
627: protected Commandline getCommandline() {
628: return new Commandline();
629: }
630:
631: static String formatCVSDate(Date date) {
632: return CVSDateUtil.formatCVSDate(date);
633: }
634:
635: /**
636: * Parses the input stream, which should be from the cvs log command. This method will format the data found in the
637: * input stream into a List of Modification instances.
638: *
639: * @param input
640: * InputStream to get log data from.
641: * @return List of Modification elements, maybe empty never null.
642: * @throws IOException
643: */
644: protected List parseStream(InputStream input) throws IOException {
645: BufferedReader reader = new BufferedReader(
646: new InputStreamReader(input));
647:
648: // Read to the first RCS file name. The first entry in the log
649: // information will begin with this line. A CVS_FILE_DELIMITER is NOT
650: // present. If no RCS file lines are found then there is nothing to do.
651:
652: String line = readToNotPast(reader, CVS_RCSFILE_LINE, null);
653: ArrayList mods = new ArrayList();
654:
655: while (line != null) {
656: // Parse the single file entry, which may include several
657: // modifications.
658: List returnList = parseEntry(reader, line);
659:
660: // Add all the modifications to the local list.
661: mods.addAll(returnList);
662:
663: // Read to the next RCS file line. The CVS_FILE_DELIMITER may have
664: // been consumed by the parseEntry method, so we cannot read to it.
665: line = readToNotPast(reader, CVS_RCSFILE_LINE, null);
666: }
667:
668: return mods;
669: }
670:
671: private void getRidOfLeftoverData(InputStream stream) {
672: new StreamPumper(stream, new DiscardConsumer()).run();
673: }
674:
675: private List execHistoryCommand(Commandline command)
676: throws Exception {
677: Process p = command.execute();
678:
679: Thread stderr = logErrorStream(p);
680: InputStream cvsLogStream = p.getInputStream();
681: List mods = parseStream(cvsLogStream);
682:
683: getRidOfLeftoverData(cvsLogStream);
684: p.waitFor();
685: stderr.join();
686: IO.close(p);
687:
688: return mods;
689: }
690:
691: protected void setMailAliases(Hashtable mailAliases) {
692: this .mailAliases = mailAliases;
693: }
694:
695: private static Thread logErrorStream(Process p) {
696: return logErrorStream(p.getErrorStream());
697: }
698:
699: static Thread logErrorStream(InputStream error) {
700: Thread stderr = new Thread(StreamLogger.getWarnPumper(log,
701: error));
702: stderr.start();
703: return stderr;
704: }
705:
706: // (PENDING) Extract CVSEntryParser class
707:
708: /**
709: * Parses a single file entry from the reader. This entry may contain zero or more revisions. This method may
710: * consume the next CVS_FILE_DELIMITER line from the reader, but no further. <p/> When the log is related to a non
711: * branch tag, only the last modification for each file will be listed.
712: *
713: * @param reader
714: * Reader to parse data from.
715: * @return modifications found in this entry; maybe empty, never null.
716: * @throws IOException
717: */
718: private List parseEntry(BufferedReader reader, String rcsLine)
719: throws IOException {
720: ArrayList mods = new ArrayList();
721:
722: String nextLine = "";
723:
724: // Read to the working file name line to get the filename.
725: // If working file name line isn't found we'll extract is from the RCS file line
726: String workingFileName;
727: if (module != null && cvsroot != null) {
728: final String repositoryRoot = cvsroot.substring(cvsroot
729: .lastIndexOf(":") + 1);
730: final int startAt = "RCS file: ".length()
731: + repositoryRoot.length();
732: workingFileName = rcsLine.substring(startAt, rcsLine
733: .length() - 2);
734: } else {
735: String workingFileLine = readToNotPast(reader,
736: CVS_WORKINGFILE_LINE, null);
737: workingFileName = workingFileLine
738: .substring(CVS_WORKINGFILE_LINE.length());
739: }
740:
741: String branchRevisionName = parseBranchRevisionName(reader);
742: boolean newCVSVersion = isCvsNewOutputFormat();
743: while (nextLine != null && !nextLine.startsWith(CVS_FILE_DELIM)) {
744: nextLine = readToNotPast(reader, "revision", CVS_FILE_DELIM);
745: if (nextLine == null) {
746: // No more revisions for this file.
747: break;
748: }
749:
750: StringTokenizer tokens = new StringTokenizer(nextLine, " ");
751: tokens.nextToken();
752: String revision = tokens.nextToken();
753: if (!useHead()) {
754: if (!revision.equals(branchRevisionName)) {
755: // Indeed this is a branch, not just a regular tag
756: String itsBranchRevisionName = revision.substring(
757: 0, revision.lastIndexOf('.'));
758: if (!itsBranchRevisionName
759: .equals(branchRevisionName)) {
760: break;
761: }
762: }
763: }
764:
765: // Read to the revision date. It is ASSUMED that each revision
766: // section will include this date information line.
767: nextLine = readToNotPast(reader, CVS_REVISION_DATE,
768: CVS_FILE_DELIM);
769: if (nextLine == null) {
770: break;
771: }
772:
773: tokens = new StringTokenizer(nextLine, " \t\n\r\f;");
774: // First token is the keyword for date, then the next two should be
775: // the date and time stamps.
776: tokens.nextToken();
777: String dateStamp = tokens.nextToken();
778: String timeStamp = tokens.nextToken();
779:
780: // New format sometimes has a +0000 in it. This skips it if we don't see
781: // the start of the author: section
782: String isThisTimeOffset = tokens.nextToken();
783: if (!isThisTimeOffset.equals("author:")) {
784: tokens.nextToken();
785: }
786: // The next token should be the author keyword, then the author name.
787: String authorName = tokens.nextToken();
788:
789: // The next token should be the state keyword, then the state name.
790: tokens.nextToken();
791: String stateKeyword = tokens.nextToken();
792:
793: // if no lines keyword then file is added
794: boolean isAdded = !tokens.hasMoreTokens();
795:
796: // All the text from now to the next revision delimiter or working
797: // file delimiter constitutes the message.
798: String message = "";
799: nextLine = reader.readLine();
800: boolean multiLine = false;
801:
802: while (nextLine != null
803: && !nextLine.startsWith(CVS_FILE_DELIM)
804: && !nextLine.startsWith(CVS_REVISION_DELIM)) {
805:
806: if (multiLine) {
807: message += NEW_LINE;
808: } else {
809: multiLine = true;
810: }
811: message += nextLine;
812:
813: // Go to the next line.
814: nextLine = reader.readLine();
815: }
816:
817: Modification nextModification = new Modification("cvs");
818: nextModification.revision = revision;
819:
820: int lastSlashIndex = workingFileName.lastIndexOf("/");
821:
822: String fileName, folderName = null;
823: fileName = workingFileName.substring(lastSlashIndex + 1);
824: if (lastSlashIndex != -1) {
825: folderName = workingFileName.substring(0,
826: lastSlashIndex);
827: }
828: Modification.ModifiedFile modfile = nextModification
829: .createModifiedFile(fileName, folderName);
830: modfile.revision = nextModification.revision;
831:
832: try {
833: if (newCVSVersion) {
834: nextModification.modifiedTime = CVSDateUtil
835: .parseCVSDate(dateStamp + " " + timeStamp
836: + " GMT");
837: } else {
838: nextModification.modifiedTime = logDateFormatter
839: .parse(dateStamp + " " + timeStamp + " GMT");
840: }
841: } catch (ParseException pe) {
842: log
843: .error(
844: "Error parsing cvs log for date and time",
845: pe);
846: return null;
847: }
848:
849: nextModification.userName = authorName;
850:
851: String address = (String) mailAliases.get(authorName);
852: if (address != null) {
853: nextModification.emailAddress = address;
854: }
855:
856: nextModification.comment = message;
857:
858: if (stateKeyword.equalsIgnoreCase(CVS_REVISION_DEAD)
859: && message.indexOf("was initially added on branch") != -1) {
860: log.debug("skipping branch addition activity for "
861: + nextModification);
862: // this prevents additions to a branch from showing up as action "deleted" from head
863: continue;
864: }
865:
866: if (stateKeyword.equalsIgnoreCase(CVS_REVISION_DEAD)) {
867: modfile.action = "deleted";
868: properties.deletionFound();
869: } else if (isAdded) {
870: modfile.action = "added";
871: } else {
872: modfile.action = "modified";
873: }
874: properties.modificationFound();
875: mods.add(nextModification);
876: }
877: return mods;
878: }
879:
880: /**
881: * Find the CVS branch revision name, when the tag is not HEAD The reader will consume all lines up to the next
882: * description.
883: *
884: * @return the branch revision name, or <code>null</code> if not applicable or none was found.
885: */
886: private String parseBranchRevisionName(BufferedReader reader)
887: throws IOException {
888: String branchRevisionName = null;
889:
890: if (!useHead()) {
891: // Look for the revision of the form "tag: *.(0.)y ". this doesn't work for HEAD
892: // get line with branch revision on it.
893:
894: String branchRevisionLine = readToNotPast(reader, "\t"
895: + tag + ": ", CVS_DESCRIPTION);
896:
897: if (branchRevisionLine != null) {
898: // Look for the revision of the form "tag: *.(0.)y ", return "*.y"
899: branchRevisionName = branchRevisionLine.substring(tag
900: .length() + 3);
901: if (branchRevisionName.charAt(branchRevisionName
902: .lastIndexOf(".") - 1) == '0') {
903: branchRevisionName = branchRevisionName.substring(
904: 0, branchRevisionName.lastIndexOf(".") - 2)
905: + branchRevisionName
906: .substring(branchRevisionName
907: .lastIndexOf("."));
908: }
909: }
910: }
911: return branchRevisionName;
912: }
913:
914: /**
915: * This method will consume lines from the reader up to the line that begins with the String specified but not past
916: * a line that begins with the notPast String. If the line that begins with the beginsWith String is found then it
917: * will be returned. Otherwise null is returned.
918: *
919: * @param reader
920: * Reader to read lines from.
921: * @param beginsWith
922: * String to match to the beginning of a line.
923: * @param notPast
924: * String which indicates that lines should stop being consumed, even if the begins with match has not
925: * been found. Pass null to this method to ignore this string.
926: * @return String that begin as indicated, or null if none matched to the end of the reader or the notPast line was
927: * found.
928: * @throws IOException
929: */
930: private static String readToNotPast(BufferedReader reader,
931: String beginsWith, String notPast) throws IOException {
932: boolean checkingNotPast = notPast != null;
933:
934: String nextLine = reader.readLine();
935: while (nextLine != null && !nextLine.startsWith(beginsWith)) {
936: if (checkingNotPast && nextLine.startsWith(notPast)) {
937: return null;
938: }
939: nextLine = reader.readLine();
940: }
941:
942: return nextLine;
943: }
944:
945: boolean useHead() {
946: return tag == null || tag.equals(CVS_HEAD_TAG)
947: || tag.equals("");
948: }
949:
950: }
|