1 package org.apache.maven.scm.provider.jazz.command.changelog;
2
3 /*
4 * Licensed to the Apache Software Foundation (ASF) under one
5 * or more contributor license agreements. See the NOTICE file
6 * distributed with this work for additional information
7 * regarding copyright ownership. The ASF licenses this file
8 * to you under the Apache License, Version 2.0 (the
9 * "License"); you may not use this file except in compliance
10 * with the License. You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 * KIND, either express or implied. See the License for the
18 * specific language governing permissions and limitations
19 * under the License.
20 */
21
22 import org.apache.maven.scm.ChangeFile;
23 import org.apache.maven.scm.ChangeSet;
24 import org.apache.maven.scm.ScmFileStatus;
25 import org.apache.maven.scm.log.ScmLogger;
26 import org.apache.maven.scm.provider.ScmProviderRepository;
27 import org.apache.maven.scm.provider.jazz.command.consumer.AbstractRepositoryConsumer;
28
29 import java.util.ArrayList;
30 import java.util.Calendar;
31 import java.util.Date;
32 import java.util.List;
33 import java.util.Locale;
34 import java.util.regex.Matcher;
35 import java.util.regex.Pattern;
36
37 /**
38 * Consume the output of the scm command for the "list changesets" operation.
39 * <p>
40 * This parses the contents of the output and uses it to fill in the remaining
41 * information in the <code>entries</code> list.
42 *
43 * @author <a href="mailto:ChrisGWarp@gmail.com">Chris Graham</a>
44 */
45 public class JazzListChangesetConsumer
46 extends AbstractRepositoryConsumer
47 {
48 //Change sets:
49 // (1589) ---$ Deb "[maven-release-plugin] prepare for next development iteration"
50 // Component: (1158) "GPDB"
51 // Modified: Feb 25, 2012 10:15 PM (Yesterday)
52 // Changes:
53 // ---c- (1170) \GPDB\GPDBEAR\pom.xml
54 // ---c- (1171) \GPDB\GPDBResources\pom.xml
55 // ---c- (1167) \GPDB\GPDBWeb\pom.xml
56 // ---c- (1165) \GPDB\pom.xml
57 // (1585) ---$ Deb "[maven-release-plugin] prepare release GPDB-1.0.21"
58 // Component: (1158) "GPDB"
59 // Modified: Feb 25, 2012 10:13 PM (Yesterday)
60 // Changes:
61 // ---c- (1170) \GPDB\GPDBEAR\pom.xml
62 // ---c- (1171) \GPDB\GPDBResources\pom.xml
63 // ---c- (1167) \GPDB\GPDBWeb\pom.xml
64 // ---c- (1165) \GPDB\pom.xml
65 // (1584) ---$ Deb "This is my first changeset (2)"
66 // Component: (1158) "GPDB"
67 // Modified: Feb 25, 2012 10:13 PM (Yesterday)
68 // (1583) ---$ Deb "This is my first changeset (1)"
69 // Component: (1158) "GPDB"
70 // Modified: Feb 25, 2012 10:13 PM (Yesterday)
71 // (1323) ---$ Deb <No comment>
72 // Component: (1158) "GPDB"
73 // Modified: Feb 24, 2012 11:04 PM (Last Week)
74 // Changes:
75 // ---c- (1170) \GPDB\GPDBEAR\pom.xml
76 // ---c- (1171) \GPDB\GPDBResources\pom.xml
77 // ---c- (1167) \GPDB\GPDBWeb\pom.xml
78 // ---c- (1165) \GPDB\pom.xml
79 // (1319) ---$ Deb <No comment>
80 // Component: (1158) "GPDB"
81 // Modified: Feb 24, 2012 11:03 PM (Last Week)
82 // Changes:
83 // ---c- (1170) \GPDB\GPDBEAR\pom.xml
84 // ---c- (1171) \GPDB\GPDBResources\pom.xml
85 // ---c- (1167) \GPDB\GPDBWeb\pom.xml
86 // ---c- (1165) \GPDB\pom.xml
87 //
88 // NOTE: If the change sets originate on the current date, the date is not
89 // displayed, only the time is.
90 // EG:
91 //Change sets:
92 // (1809) ---$ Deb "[maven-release-plugin] prepare for next development iteration"
93 // Component: (1158) "GPDB"
94 // Modified: 6:20 PM (5 minutes ago)
95 // Changes:
96 // ---c- (1170) \GPDB\GPDBEAR\pom.xml
97 // ---c- (1171) \GPDB\GPDBResources\pom.xml
98 // ---c- (1167) \GPDB\GPDBWeb\pom.xml
99 // ---c- (1165) \GPDB\pom.xml
100 // (1801) ---$ Deb "[maven-release-plugin] prepare release GPDB-1.0.26"
101 // Component: (1158) "GPDB"
102 // Modified: 6:18 PM (10 minutes ago)
103 // Changes:
104 // ---c- (1170) \GPDB\GPDBEAR\pom.xml
105 // ---c- (1171) \GPDB\GPDBResources\pom.xml
106 // ---c- (1167) \GPDB\GPDBWeb\pom.xml
107 // (1799) ---$ Deb <No comment>
108 // Component: (1158) "GPDB"
109 // Modified: 6:18 PM (10 minutes ago)
110 // Changes:
111 // ---c- (1165) \GPDB\pom.xml
112 // (1764) ---$ Deb <No comment>
113 // Component: (1158) "GPDB"
114 // Modified: Mar 1, 2012 2:34 PM
115 // Changes:
116 // ---c- (1165) \GPDB\pom.xml
117
118
119 // State Machine Definitions
120 private static final int STATE_CHANGE_SETS = 0;
121
122 private static final int STATE_CHANGE_SET = 1;
123
124 private static final int STATE_COMPONENT = 2;
125
126 private static final int STATE_MODIFIED = 3;
127
128 private static final int STATE_CHANGES = 4;
129
130 // Header definitions.
131 private static final String HEADER_CHANGE_SETS = "Change sets:";
132
133 private static final String HEADER_CHANGE_SET = "(";
134
135 private static final String HEADER_COMPONENT = "Component:";
136
137 private static final String HEADER_MODIFIED = "Modified:";
138
139 private static final String HEADER_CHANGES = "Changes:";
140
141 private static final String JAZZ_TIMESTAMP_PATTERN = "MMM d, yyyy h:mm a";
142 // Actually: DateFormat.getDateTimeInstance( DateFormat.MEDIUM, DateFormat.SHORT );
143
144 private static final String JAZZ_TIMESTAMP_PATTERN_TIME = "h:mm a";
145 // Only seen when the data = today. Only the time is displayed.
146
147 // (1589) ---$ Deb "[maven-release-plugin] prepare for next development iteration"
148 // (1585) ---$ Deb "[maven-release-plugin] prepare release GPDB-1.0.21"
149 private static final Pattern CHANGESET_PATTERN = Pattern.compile( "\\((\\d+)\\) (....) (\\w+) (.*)" );
150
151 // ---c- (1170) \GPDB\GPDBEAR\pom.xml
152 // ---c- (1171) \GPDB\GPDBResources\pom.xml
153 // ---c- (1167) \GPDB\GPDBWeb\pom.xml
154 // ---c- (1165) \GPDB\pom.xml
155 private static final Pattern CHANGES_PATTERN = Pattern.compile( "(.....) \\((\\d+)\\) (.*)" );
156
157
158 private List<ChangeSet> entries;
159
160 private final String userDateFormat;
161
162 // This is incremented at the beginning of every change set line. So we start at -1 (to get zero on first
163 // processing)
164 private int currentChangeSetIndex = -1;
165
166 private int currentState = STATE_CHANGE_SETS;
167
168 /**
169 * Constructor for our "scm list changeset" consumer.
170 *
171 * @param repo The JazzScmProviderRepository being used.
172 * @param logger The ScmLogger to use.
173 * @param entries The List of ChangeSet entries that we will populate.
174 */
175 public JazzListChangesetConsumer( ScmProviderRepository repo, ScmLogger logger, List<ChangeSet> entries,
176 String userDateFormat )
177 {
178 super( repo, logger );
179 this.entries = entries;
180 this.userDateFormat = userDateFormat;
181 }
182
183 /**
184 * Process one line of output from the execution of the "scm list changeset" command.
185 *
186 * @param line The line of output from the external command that has been pumped to us.
187 * @see org.codehaus.plexus.util.cli.StreamConsumer#consumeLine(java.lang.String)
188 */
189 public void consumeLine( String line )
190 {
191 super.consumeLine( line );
192
193 // Process the "Change sets:" line - do nothing
194 if ( line.trim().startsWith( HEADER_CHANGE_SETS ) )
195 {
196 currentState = STATE_CHANGE_SETS;
197 }
198 else
199 {
200 if ( line.trim().startsWith( HEADER_CHANGE_SET ) )
201 {
202 currentState = STATE_CHANGE_SET;
203 }
204 else
205 {
206 if ( line.trim().startsWith( HEADER_COMPONENT ) )
207 {
208 currentState = STATE_COMPONENT;
209 }
210 else
211 {
212 if ( line.trim().startsWith( HEADER_MODIFIED ) )
213 {
214 currentState = STATE_MODIFIED;
215 }
216 else
217 {
218 if ( line.trim().startsWith( HEADER_CHANGES ) )
219 {
220 // Note: processChangesLine() will also be passed the "Changes:" line
221 // So, it needs to be able to deal with that.
222 // Changes:
223 // ---c- (1170) \GPDB\GPDBEAR\pom.xml
224 // ---c- (1171) \GPDB\GPDBResources\pom.xml
225 // ---c- (1167) \GPDB\GPDBWeb\pom.xml
226 // ---c- (1165) \GPDB\pom.xml
227 currentState = STATE_CHANGES;
228 }
229 }
230 }
231 }
232 }
233
234 switch ( currentState )
235 {
236 case STATE_CHANGE_SETS:
237 // Nothing to do.
238 break;
239
240 case STATE_CHANGE_SET:
241 processChangeSetLine( line );
242 break;
243
244 case STATE_COMPONENT:
245 // Nothing to do. Not used (Yet?)
246 break;
247
248 case STATE_MODIFIED:
249 processModifiedLine( line );
250 break;
251
252 case STATE_CHANGES:
253 processChangesLine( line );
254 break;
255
256 default:
257 }
258
259 }
260
261 private void processChangeSetLine( String line )
262 {
263 // Process the headerless change set line - starts with a '(', eg:
264 // (1589) ---$ Deb "[maven-release-plugin] prepare for next development iteration"
265 // (1585) ---$ Deb "[maven-release-plugin] prepare release GPDB-1.0.21"
266 Matcher matcher = CHANGESET_PATTERN.matcher( line );
267 if ( matcher.find() )
268 {
269 // This is the only place this gets incremented.
270 // It starts at -1, and on first execution is incremented to 0 - which is correct.
271 currentChangeSetIndex++;
272 ChangeSet currentChangeSet = entries.get( currentChangeSetIndex );
273
274 // Init the file of files, so it is not null, but it can be empty!
275 List<ChangeFile> files = new ArrayList<ChangeFile>();
276 currentChangeSet.setFiles( files );
277
278 String changesetAlias = matcher.group( 1 );
279 String changeFlags = matcher.group( 2 ); // Not used.
280 String author = matcher.group( 3 );
281 String comment = matcher.group( 4 );
282
283 if ( getLogger().isDebugEnabled() )
284 {
285 getLogger().debug( " Parsing ChangeSet Line : " + line );
286 getLogger().debug( " changesetAlias : " + changesetAlias );
287 getLogger().debug( " changeFlags : " + changeFlags );
288 getLogger().debug( " author : " + author );
289 getLogger().debug( " comment : " + comment );
290 }
291
292 // Sanity check.
293 if ( currentChangeSet.getRevision() != null && !currentChangeSet.getRevision().equals( changesetAlias ) )
294 {
295 getLogger().warn( "Warning! The indexes appear to be out of sequence! "
296 + "For currentChangeSetIndex = " + currentChangeSetIndex + ", we got '"
297 + changesetAlias + "' and not '" + currentChangeSet.getRevision()
298 + "' as expected." );
299 }
300
301 comment = stripDelimiters( comment );
302 currentChangeSet.setAuthor( author );
303 currentChangeSet.setComment( comment );
304 }
305 }
306
307 private void processModifiedLine( String line )
308 {
309 // Process the "Modified: ..." line, eg:
310 // Modified: Feb 25, 2012 10:15 PM (Yesterday)
311 // Modified: Feb 25, 2012 10:13 PM (Yesterday)
312 // Modified: Feb 24, 2012 11:03 PM (Last Week)
313 // Modified: Mar 1, 2012 2:34 PM
314 // Modified: 6:20 PM (5 minutes ago)
315
316 if ( getLogger().isDebugEnabled() )
317 {
318 getLogger().debug( " Parsing Modified Line : " + line );
319 }
320
321 int colonPos = line.indexOf( ":" );
322 int parenPos = line.indexOf( "(" );
323
324 String date = null;
325
326 if ( colonPos != -1 && parenPos != -1 )
327 {
328 date = line.substring( colonPos + 2, parenPos - 1 );
329 }
330 else
331 {
332 if ( colonPos != -1 && parenPos == -1 )
333 {
334 // No trailing bracket
335 date = line.substring( colonPos + 2 );
336 }
337 }
338
339 if ( date != null )
340 {
341 Date changesetDate = parseDate( date.toString(), userDateFormat, JAZZ_TIMESTAMP_PATTERN );
342 // try again forcing en locale
343 if ( changesetDate == null )
344 {
345 changesetDate = parseDate( date.toString(), userDateFormat, JAZZ_TIMESTAMP_PATTERN, Locale.ENGLISH );
346 }
347 // changesetDate will be null when the date is not given, it only has just the time. The date is today.
348 if ( changesetDate == null )
349 {
350 changesetDate = parseDate( date.toString(), userDateFormat, JAZZ_TIMESTAMP_PATTERN_TIME );
351 // try again forcing en locale
352 if ( changesetDate == null )
353 {
354 changesetDate =
355 parseDate( date.toString(), userDateFormat, JAZZ_TIMESTAMP_PATTERN_TIME, Locale.ENGLISH );
356 }
357 // Get today's time/date. Used to get the date.
358 Calendar today = Calendar.getInstance();
359 // Get a working one.
360 Calendar changesetCal = Calendar.getInstance();
361 // Set the date/time. Used to set the time.
362 changesetCal.setTimeInMillis( changesetDate.getTime() );
363 // Now set the date (today).
364 changesetCal.set( today.get( Calendar.YEAR ), today.get( Calendar.MONTH ),
365 today.get( Calendar.DAY_OF_MONTH ) );
366 // Now get the date of the combined results.
367 changesetDate = changesetCal.getTime();
368 }
369
370 if ( getLogger().isDebugEnabled() )
371 {
372 getLogger().debug( " date : " + date );
373 getLogger().debug( " changesetDate : " + changesetDate );
374 }
375
376 ChangeSet currentChangeSet = entries.get( currentChangeSetIndex );
377 currentChangeSet.setDate( changesetDate );
378 }
379 }
380
381 private void processChangesLine( String line )
382 {
383 // Process the changes line, eg:
384 // ---c- (1170) \GPDB\GPDBEAR\pom.xml
385 // ---c- (1171) \GPDB\GPDBResources\pom.xml
386 // ---c- (1167) \GPDB\GPDBWeb\pom.xml
387 // ---c- (1165) \GPDB\pom.xml
388 Matcher matcher = CHANGES_PATTERN.matcher( line );
389 if ( matcher.find() )
390 {
391 ChangeSet currentChangeSet = entries.get( currentChangeSetIndex );
392
393 String changeFlags = matcher.group( 1 ); // Not used.
394 String fileAlias = matcher.group( 2 );
395 String file = matcher.group( 3 );
396
397 if ( getLogger().isDebugEnabled() )
398 {
399 getLogger().debug( " Parsing Changes Line : " + line );
400 getLogger().debug(
401 " changeFlags : " + changeFlags + " Translated to : " + parseFileChangeState( changeFlags ) );
402 getLogger().debug( " filetAlias : " + fileAlias );
403 getLogger().debug( " file : " + file );
404 }
405
406 ChangeFile changeFile = new ChangeFile( file );
407 ScmFileStatus status = parseFileChangeState( changeFlags );
408 changeFile.setAction( status );
409 currentChangeSet.getFiles().add( changeFile );
410 }
411 }
412
413 /**
414 * String the leading/trailing ", < and > from the text.
415 *
416 * @param text The text to process.
417 * @return The striped text.
418 */
419 protected String stripDelimiters( String text )
420 {
421 if ( text == null )
422 {
423 return null;
424 }
425 String workingText = text;
426 if ( workingText.startsWith( "\"" ) || workingText.startsWith( "<" ) )
427 {
428 workingText = workingText.substring( 1 );
429 }
430 if ( workingText.endsWith( "\"" ) || workingText.endsWith( ">" ) )
431 {
432 workingText = workingText.substring( 0, workingText.length() - 1 );
433 }
434
435 return workingText;
436 }
437
438 /**
439 * Parse the change state file flags from Jazz and map them to the maven SCM ones.
440 * <p>
441 * "----" Character positions 0-3.
442 * <p>
443 * [0] is '*' or '-' Indicates that this is the current change set ('*') or not ('-'). STATE_CHANGESET_CURRENT
444 * [1] is '!' or '-' Indicates a Potential Conflict ('!') or not ('-'). STATE_POTENTIAL_CONFLICT
445 * [2] is '#' or '-' Indicates a Conflict ('#') or not ('-'). STATE_CONFLICT
446 * [3] is '@' or '$' Indicates whether the changeset is active ('@') or not ('$'). STATE_CHANGESET_ACTIVE
447 *
448 * @param state The 5 character long state string
449 * @return The ScmFileStatus value.
450 */
451 private ScmFileStatus parseChangeSetChangeState( String state )
452 {
453 if ( state.length() != 4 )
454 {
455 throw new IllegalArgumentException( "Change State string must be 4 chars long!" );
456 }
457
458 // This is not used, but is here for potential future usage and for documentation purposes.
459 return ScmFileStatus.UNKNOWN;
460 }
461
462 /**
463 * Parse the change state file flags from Jazz and map them to the maven SCM ones.
464 * <p>
465 * "-----" Character positions 0-4. The default is '-'.
466 * <p>
467 * [0] is '-' or '!' Indicates a Potential Conflict. STATE_POTENTIAL_CONFLICT
468 * [1] is '-' or '#' Indicates a Conflict. STATE_CONFLICT
469 * [2] is '-' or 'a' Indicates an addition. STATE_ADD
470 * or 'd' Indicates a deletion. STATE_DELETE
471 * or 'm' Indicates a move. STATE_MOVE
472 * [3] is '-' or 'c' Indicates a content change. STATE_CONTENT_CHANGE
473 * [4] is '-' or 'p' Indicates a property change. STATE_PROPERTY_CHANGE
474 * <p>
475 * NOTE: [3] and [4] can only be set it [2] is NOT 'a' or 'd'.
476 *
477 * @param state The 5 character long state string
478 * @return The SCMxxx value.
479 */
480 private ScmFileStatus parseFileChangeState( String state )
481 {
482 if ( state.length() != 5 )
483 {
484 throw new IllegalArgumentException( "Change State string must be 5 chars long!" );
485 }
486
487 // NOTE: We have an impedance mismatch here. The Jazz file change flags represent
488 // many different states. However, we can only return *ONE* ScmFileStatus value,
489 // so we need to be careful as to the precedence that we give to them.
490
491 ScmFileStatus status = ScmFileStatus.UNKNOWN; // Probably not a valid initial default value.
492
493 // [0] is '-' or '!' Indicates a Potential Conflict. STATE_POTENTIAL_CONFLICT
494 if ( state.charAt( 0 ) == '!' )
495 {
496 status = ScmFileStatus.CONFLICT;
497 }
498 // [1] is '-' or '#' Indicates a Conflict. STATE_CONFLICT
499 if ( state.charAt( 1 ) == '#' )
500 {
501 status = ScmFileStatus.CONFLICT;
502 }
503
504 // [2] is '-' or 'a' Indicates an addition. STATE_ADD
505 // or 'd' Indicates a deletion. STATE_DELETE
506 // or 'm' Indicates a move. STATE_MOVE
507 if ( state.charAt( 2 ) == 'a' )
508 {
509 status = ScmFileStatus.ADDED;
510 }
511 else
512 {
513 if ( state.charAt( 2 ) == 'd' )
514 {
515 status = ScmFileStatus.DELETED;
516 }
517 else
518 {
519 if ( state.charAt( 2 ) == 'm' )
520 {
521 status = ScmFileStatus.RENAMED; // Has been renamed or moved.
522 }
523
524 // [3] is '-' or 'c' Indicates a content change. STATE_CONTENT_CHANGE
525 if ( state.charAt( 3 ) == 'c' )
526 {
527 status = ScmFileStatus.MODIFIED; // The file has been modified in the working tree.
528 }
529
530 // [4] is '-' or 'p' Indicates a property change. STATE_PROPERTY_CHANGE
531 if ( state.charAt( 4 ) == 'p' )
532 {
533 status =
534 ScmFileStatus.MODIFIED; // ScmFileStatus has no concept of property or meta data changes.
535 }
536 }
537 }
538
539 return status;
540 }
541 }