/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.ant.taskdefs.cvslib; import java.io.File; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.nio.file.Files; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Properties; import java.util.Vector; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.Project; import org.apache.tools.ant.taskdefs.AbstractCvsTask; import org.apache.tools.ant.types.FileSet; /** * Examines the output of cvs log and group related changes together. * * It produces an XML output representing the list of changes. * <pre> * <font color=#0000ff><!-- Root element --></font> * <font color=#6a5acd><!ELEMENT</font> changelog <font color=#ff00ff> * (entry</font><font color=#ff00ff>+</font><font color=#ff00ff>) * </font><font color=#6a5acd>></font> * <font color=#0000ff><!-- CVS Entry --></font> * <font color=#6a5acd><!ELEMENT</font> entry <font color=#ff00ff> * (date,author,file</font><font color=#ff00ff>+</font><font color=#ff00ff>,msg) * </font><font color=#6a5acd>></font> * <font color=#0000ff><!-- Date of cvs entry --></font> * <font color=#6a5acd><!ELEMENT</font> date <font color=#ff00ff>(#PCDATA) * </font><font color=#6a5acd>></font> * <font color=#0000ff><!-- Author of change --></font> * <font color=#6a5acd><!ELEMENT</font> author <font color=#ff00ff>(#PCDATA) * </font><font color=#6a5acd>></font> * <font color=#0000ff><!-- List of files affected --></font> * <font color=#6a5acd><!ELEMENT</font> msg <font color=#ff00ff>(#PCDATA) * </font><font color=#6a5acd>></font> * <font color=#0000ff><!-- File changed --></font> * <font color=#6a5acd><!ELEMENT</font> file <font color=#ff00ff> * (name,revision,prevrevision</font><font color=#ff00ff>?</font> * <font color=#ff00ff>)</font><font color=#6a5acd>></font> * <font color=#0000ff><!-- Name of the file --></font> * <font color=#6a5acd><!ELEMENT</font> name <font color=#ff00ff>(#PCDATA) * </font><font color=#6a5acd>></font> * <font color=#0000ff><!-- Revision number --></font> * <font color=#6a5acd><!ELEMENT</font> revision <font color=#ff00ff> * (#PCDATA)</font><font color=#6a5acd>></font> * <font color=#0000ff><!-- Previous revision number --></font> * <font color=#6a5acd><!ELEMENT</font> prevrevision <font color=#ff00ff> * (#PCDATA)</font><font color=#6a5acd>></font> * </pre> * * @since Ant 1.5 * @ant.task name="cvschangelog" category="scm" */ public class ChangeLogTask extends AbstractCvsTask { /** User list */ private File usersFile; /** User list */ private List<CvsUser> cvsUsers = new Vector<>(); /** Input dir */ private File inputDir; /** Output file */ private File destFile; /** The earliest date at which to start processing entries. */ private Date startDate; /** The latest date at which to stop processing entries. */ private Date endDate; /** Determines whether log (false) or rlog (true) is used */ private boolean remote = false; /** Start tag when doing tag ranges. */ private String startTag; /** End tag when doing tag ranges. */ private String endTag; /** * Filesets containing list of files against which the cvs log will be * performed. If empty then all files in the working directory will * be checked. */ private final List<FileSet> filesets = new Vector<>(); /** * Set the base dir for cvs. * * @param inputDir The new dir value */ public void setDir(final File inputDir) { this.inputDir = inputDir; } /** * Set the output file for the log. * * @param destFile The new destfile value */ public void setDestfile(final File destFile) { this.destFile = destFile; } /** * Set a lookup list of user names & addresses * * @param usersFile The file containing the users info. */ public void setUsersfile(final File usersFile) { this.usersFile = usersFile; } /** * Add a user to list changelog knows about. * * @param user the user */ public void addUser(final CvsUser user) { cvsUsers.add(user); } /** * Set the date at which the changelog should start. * * @param start The date at which the changelog should start. */ public void setStart(final Date start) { this.startDate = start; } /** * Set the date at which the changelog should stop. * * @param endDate The date at which the changelog should stop. */ public void setEnd(final Date endDate) { this.endDate = endDate; } /** * Set the number of days worth of log entries to process. * * @param days the number of days of log to process. */ public void setDaysinpast(final int days) { // CheckStyle:MagicNumber OFF final long time = System.currentTimeMillis() - (long) days * 24 * 60 * 60 * 1000; // CheckStyle:MagicNumber ON setStart(new Date(time)); } /** * Whether to use rlog against a remote repository instead of log * in a working copy's directory. * * @since Ant 1.8.0 */ public void setRemote(final boolean remote) { this.remote = remote; } /** * Set the tag at which the changelog should start. * * @param start The date at which the changelog should start. */ public void setStartTag(final String start) { this.startTag = start; } /** * Set the tag at which the changelog should stop. * * @param end The date at which the changelog should stop. */ public void setEndTag(final String end) { this.endTag = end; } /** * Adds a set of files about which cvs logs will be generated. * * @param fileSet a set of files about which cvs logs will be generated. */ public void addFileset(final FileSet fileSet) { filesets.add(fileSet); } /** * Execute task * * @exception BuildException if something goes wrong executing the * cvs command */ @Override public void execute() throws BuildException { File savedDir = inputDir; // may be altered in validate try { validate(); final Properties userList = new Properties(); loadUserlist(userList); for (CvsUser user : cvsUsers) { user.validate(); userList.put(user.getUserID(), user.getDisplayname()); } if (!remote) { setCommand("log"); if (getTag() != null) { CvsVersion myCvsVersion = new CvsVersion(); myCvsVersion.setProject(getProject()); myCvsVersion.setTaskName("cvsversion"); myCvsVersion.setCvsRoot(getCvsRoot()); myCvsVersion.setCvsRsh(getCvsRsh()); myCvsVersion.setPassfile(getPassFile()); myCvsVersion.setDest(inputDir); myCvsVersion.execute(); if (myCvsVersion.supportsCvsLogWithSOption()) { addCommandArgument("-S"); } } } else { // supply 'rlog' as argument instead of command setCommand(""); addCommandArgument("rlog"); // Do not print name/header if no revisions // selected. This is quicker: less output to parse. addCommandArgument("-S"); // Do not list tags. This is quicker: less output to // parse. addCommandArgument("-N"); } if (null != startTag || null != endTag) { // man, do I get spoiled by C#'s ?? operator String startValue = startTag == null ? "" : startTag; String endValue = endTag == null ? "" : endTag; addCommandArgument("-r" + startValue + "::" + endValue); } else if (null != startDate) { final SimpleDateFormat outputDate = new SimpleDateFormat("yyyy-MM-dd"); // We want something of the form: -d ">=YYYY-MM-dd" final String dateRange = ">=" + outputDate.format(startDate); // Supply '-d' as a separate argument - Bug# 14397 addCommandArgument("-d"); addCommandArgument(dateRange); } // Check if list of files to check has been specified for (FileSet fileSet : filesets) { final DirectoryScanner scanner = fileSet.getDirectoryScanner(getProject()); for (String file : scanner.getIncludedFiles()) { addCommandArgument(file); } } final ChangeLogParser parser = new ChangeLogParser(remote, getPackage(), getModules()); final RedirectingStreamHandler handler = new RedirectingStreamHandler(parser); log(getCommand(), Project.MSG_VERBOSE); setDest(inputDir); setExecuteStreamHandler(handler); try { super.execute(); } finally { final String errors = handler.getErrors(); if (null != errors) { log(errors, Project.MSG_ERR); } } final CVSEntry[] entrySet = parser.getEntrySetAsArray(); final CVSEntry[] filteredEntrySet = filterEntrySet(entrySet); replaceAuthorIdWithName(userList, filteredEntrySet); writeChangeLog(filteredEntrySet); } finally { inputDir = savedDir; } } /** * Validate the parameters specified for task. * * @throws BuildException if fails validation checks */ private void validate() throws BuildException { if (null == inputDir) { inputDir = getProject().getBaseDir(); } if (null == destFile) { throw new BuildException("Destfile must be set."); } if (!inputDir.exists()) { throw new BuildException("Cannot find base dir %s", inputDir.getAbsolutePath()); } if (null != usersFile && !usersFile.exists()) { throw new BuildException("Cannot find user lookup list %s", usersFile.getAbsolutePath()); } if ((null != startTag || null != endTag) && (null != startDate || null != endDate)) { throw new BuildException( "Specify either a tag or date range, not both"); } } /** * Load the userlist from the userList file (if specified) and add to * list of users. * * @param userList the file of users * @throws BuildException if file can not be loaded for some reason */ private void loadUserlist(final Properties userList) throws BuildException { if (null != usersFile) { try { userList.load(Files.newInputStream(usersFile.toPath())); } catch (final IOException ioe) { throw new BuildException(ioe.toString(), ioe); } } } /** * Filter the specified entries according to an appropriate rule. * * @param entrySet the entry set to filter * @return the filtered entry set */ private CVSEntry[] filterEntrySet(final CVSEntry[] entrySet) { final List<CVSEntry> results = new ArrayList<>(); for (CVSEntry cvsEntry : entrySet) { final Date date = cvsEntry.getDate(); //bug#30471 //this is caused by Date.after throwing a NullPointerException //for some reason there's no date set in the CVSEntry //Java 1.3.1 API //http://java.sun.com/j2se/1.3/docs/api/java/util/Date.html#after(java.util.Date) //doesn't throw NullPointerException //Java 1.4.2 + 1.5 API //http://java.sun.com/j2se/1.4.2/docs/api/java/util/Date.html#after(java.util.Date) //according to the docs it doesn't throw, according to the bug report it does //http://java.sun.com/j2se/1.5.0/docs/api/java/util/Date.html#after(java.util.Date) //according to the docs it does throw //for now skip entries which are missing a date if (null == date) { continue; } if (null != startDate && startDate.after(date)) { //Skip dates that are too early continue; } if (null != endDate && endDate.before(date)) { //Skip dates that are too late continue; } results.add(cvsEntry); } return results.toArray(new CVSEntry[results.size()]); } /** * replace all known author's id's with their maven specified names */ private void replaceAuthorIdWithName(final Properties userList, final CVSEntry[] entrySet) { for (final CVSEntry entry : entrySet) { if (userList.containsKey(entry.getAuthor())) { entry.setAuthor(userList.getProperty(entry.getAuthor())); } } } /** * Print changelog to file specified in task. * * @param entrySet the entry set to write. * @throws BuildException if there is an error writing changelog. */ private void writeChangeLog(final CVSEntry[] entrySet) throws BuildException { try (final PrintWriter writer = new PrintWriter( new OutputStreamWriter(Files.newOutputStream(destFile.toPath()), "UTF-8"))) { new ChangeLogWriter().printChangeLog(writer, entrySet); if (writer.checkError()) { throw new IOException("Encountered an error writing changelog"); } } catch (final UnsupportedEncodingException uee) { getProject().log(uee.toString(), Project.MSG_ERR); } catch (final IOException ioe) { throw new BuildException(ioe.toString(), ioe); } } }