001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.maven.scm.provider.svn.svnexe.command.changelog; 020 021import java.util.ArrayList; 022import java.util.Date; 023import java.util.List; 024import java.util.regex.Matcher; 025import java.util.regex.Pattern; 026 027import org.apache.maven.scm.ChangeFile; 028import org.apache.maven.scm.ChangeSet; 029import org.apache.maven.scm.ScmFileStatus; 030import org.apache.maven.scm.provider.svn.SvnChangeSet; 031import org.apache.maven.scm.util.AbstractConsumer; 032 033/** 034 * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a> 035 */ 036public class SvnChangeLogConsumer extends AbstractConsumer { 037 /** 038 * Date formatter for svn timestamp (after a little massaging) 039 */ 040 private static final String SVN_TIMESTAMP_PATTERN = "yyyy-MM-dd HH:mm:ss zzzzzzzzz"; 041 042 /** 043 * State machine constant: expecting header. 044 */ 045 private static final int GET_HEADER = 1; 046 047 /** 048 * State machine constant: expecting file information. 049 */ 050 private static final int GET_FILE = 2; 051 052 /** 053 * State machine constant: expecting comments. 054 */ 055 private static final int GET_COMMENT = 3; 056 057 /** 058 * There is always action and affected path; when copying/moving, recognize also original path and revision. 059 */ 060 private static final Pattern FILE_PATTERN = Pattern.compile("^\\s\\s\\s([A-Z])\\s(.+)$"); 061 062 /** 063 * This matches the 'original file info' part of the complete file line. 064 * Note the use of [:alpha:] instead of literal 'from' - this is meant to allow non-English localizations. 065 */ 066 private static final Pattern ORIG_FILE_PATTERN = Pattern.compile("\\([A-Za-z]+ (.+):(\\d+)\\)"); 067 068 /** 069 * The file section ends with a blank line. 070 */ 071 private static final String FILE_END_TOKEN = ""; 072 073 /** 074 * The comment section ends with a dashed line. 075 */ 076 private static final String COMMENT_END_TOKEN = 077 "------------------------------------" + "------------------------------------"; 078 079 /** 080 * Current status of the parser. 081 */ 082 private int status = GET_HEADER; 083 084 /** 085 * List of change log entries. 086 */ 087 private final List<ChangeSet> entries = new ArrayList<>(); 088 089 /** 090 * The current log entry being processed by the parser. 091 */ 092 private SvnChangeSet currentChange; 093 094 /** 095 * The current revision of the entry being processed by the parser. 096 */ 097 private String currentRevision; 098 099 /** 100 * The current comment of the entry being processed by the parser. 101 */ 102 private StringBuilder currentComment; 103 104 /** 105 * The regular expression used to match header lines. 106 */ 107 private static final Pattern HEADER_REG_EXP = Pattern.compile("^(.+) \\| (.+) \\| (.+) \\|.*$"); 108 109 private static final int REVISION_GROUP = 1; 110 111 private static final int AUTHOR_GROUP = 2; 112 113 private static final int DATE_GROUP = 3; 114 115 private static final Pattern REVISION_REG_EXP1 = Pattern.compile("rev (\\d+):"); 116 117 private static final Pattern REVISION_REG_EXP2 = Pattern.compile("r(\\d+)"); 118 119 private static final Pattern DATE_REG_EXP = Pattern.compile("(\\d+-\\d+-\\d+ " + // date 2002-08-24 120 "\\d+:\\d+:\\d+) " 121 + // time 16:01:00 122 "([\\-+])(\\d\\d)(\\d\\d)"); // gmt offset -0400);) 123 124 private final String userDateFormat; 125 126 /** 127 * Default constructor. 128 */ 129 public SvnChangeLogConsumer(String userDateFormat) { 130 this.userDateFormat = userDateFormat; 131 } 132 133 public List<ChangeSet> getModifications() { 134 return entries; 135 } 136 137 // ---------------------------------------------------------------------- 138 // StreamConsumer Implementation 139 // ---------------------------------------------------------------------- 140 141 /** 142 * {@inheritDoc} 143 */ 144 public void consumeLine(String line) { 145 if (logger.isDebugEnabled()) { 146 logger.debug(line); 147 } 148 switch (status) { 149 case GET_HEADER: 150 processGetHeader(line); 151 break; 152 case GET_FILE: 153 processGetFile(line); 154 break; 155 case GET_COMMENT: 156 processGetComment(line); 157 break; 158 default: 159 throw new IllegalStateException("Unknown state: " + status); 160 } 161 } 162 163 // ---------------------------------------------------------------------- 164 // 165 // ---------------------------------------------------------------------- 166 167 /** 168 * Process the current input line in the GET_HEADER state. The 169 * author, date, and the revision of the entry are gathered. Note, 170 * Subversion does not have per-file revisions, instead, the entire 171 * repository is given a single revision number, which is used for 172 * the revision number of each file. 173 * 174 * @param line a line of text from the svn log output 175 */ 176 private void processGetHeader(String line) { 177 Matcher matcher = HEADER_REG_EXP.matcher(line); 178 if (!matcher.matches()) { 179 // The header line is not found. Intentionally do nothing. 180 return; 181 } 182 183 currentRevision = getRevision(matcher.group(REVISION_GROUP)); 184 185 currentChange = new SvnChangeSet(); 186 187 currentChange.setAuthor(matcher.group(AUTHOR_GROUP)); 188 189 currentChange.setDate(getDate(matcher.group(DATE_GROUP))); 190 191 currentChange.setRevision(currentRevision); 192 193 status = GET_FILE; 194 } 195 196 /** 197 * Gets the svn revision, from the svn log revision output. 198 * 199 * @param revisionOutput 200 * @return the svn revision 201 */ 202 private String getRevision(final String revisionOutput) { 203 Matcher matcher; 204 if ((matcher = REVISION_REG_EXP1.matcher(revisionOutput)).matches()) { 205 return matcher.group(1); 206 } else if ((matcher = REVISION_REG_EXP2.matcher(revisionOutput)).matches()) { 207 return matcher.group(1); 208 } else { 209 throw new IllegalOutputException(revisionOutput); 210 } 211 } 212 213 /** 214 * Process the current input line in the GET_FILE state. This state 215 * adds each file entry line to the current change log entry. Note, 216 * the revision number for the entire entry is used for the revision 217 * number of each file. 218 * 219 * @param line a line of text from the svn log output 220 */ 221 private void processGetFile(String line) { 222 Matcher matcher = FILE_PATTERN.matcher(line); 223 if (matcher.matches()) { 224 final String fileinfo = matcher.group(2); 225 String name = fileinfo; 226 String originalName = null; 227 String originalRev = null; 228 final int n = fileinfo.indexOf(" ("); 229 if (n > 1 && fileinfo.endsWith(")")) { 230 final String origFileInfo = fileinfo.substring(n); 231 Matcher matcher2 = ORIG_FILE_PATTERN.matcher(origFileInfo); 232 if (matcher2.find()) { 233 // if original file is present, we must extract the affected one from the beginning 234 name = fileinfo.substring(0, n); 235 originalName = matcher2.group(1); 236 originalRev = matcher2.group(2); 237 } 238 } 239 final String actionStr = matcher.group(1); 240 final ScmFileStatus action; 241 if ("A".equals(actionStr)) { 242 // TODO: this may even change to MOVED if we later explore whole changeset and find matching DELETED 243 action = originalRev == null ? ScmFileStatus.ADDED : ScmFileStatus.COPIED; 244 } else if ("D".equals(actionStr)) { 245 action = ScmFileStatus.DELETED; 246 } else if ("M".equals(actionStr)) { 247 action = ScmFileStatus.MODIFIED; 248 } else if ("R".equals(actionStr)) { 249 action = ScmFileStatus.UPDATED; // == REPLACED in svn terms 250 } else { 251 action = ScmFileStatus.UNKNOWN; 252 } 253 if (logger.isDebugEnabled()) { 254 logger.debug(actionStr + " : " + name); 255 } 256 final ChangeFile changeFile = new ChangeFile(name, currentRevision); 257 changeFile.setAction(action); 258 changeFile.setOriginalName(originalName); 259 changeFile.setOriginalRevision(originalRev); 260 currentChange.addFile(changeFile); 261 262 status = GET_FILE; 263 } else if (line.equals(FILE_END_TOKEN)) { 264 // Create a buffer for the collection of the comment now 265 // that we are leaving the GET_FILE state. 266 currentComment = new StringBuilder(); 267 268 status = GET_COMMENT; 269 } 270 } 271 272 /** 273 * Process the current input line in the GET_COMMENT state. This 274 * state gathers all of the comments that are part of a log entry. 275 * 276 * @param line a line of text from the svn log output 277 */ 278 private void processGetComment(String line) { 279 if (line.equals(COMMENT_END_TOKEN)) { 280 currentChange.setComment(currentComment.toString()); 281 282 entries.add(currentChange); 283 284 status = GET_HEADER; 285 } else { 286 currentComment.append(line).append('\n'); 287 } 288 } 289 290 /** 291 * Converts the date time stamp from the svn output into a date 292 * object. 293 * 294 * @param dateOutput the date output from an svn log command 295 * @return a date representing the time stamp of the log entry 296 */ 297 private Date getDate(final String dateOutput) { 298 Matcher matcher = DATE_REG_EXP.matcher(dateOutput); 299 if (!matcher.find()) { 300 throw new IllegalOutputException(dateOutput); 301 } 302 303 final StringBuilder date = new StringBuilder(); 304 date.append(matcher.group(1)); 305 date.append(" GMT"); 306 date.append(matcher.group(2)); 307 date.append(matcher.group(3)); 308 date.append(':'); 309 date.append(matcher.group(4)); 310 311 return parseDate(date.toString(), userDateFormat, SVN_TIMESTAMP_PATTERN); 312 } 313}