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