Passed
Push — master ( 49e198...27ddf9 )
by Alexander
16:42 queued 05:33
created

RevisionPrinter   F

Complexity

Total Complexity 73

Size/Duplication

Total Lines 632
Duplicated Lines 0 %

Test Coverage

Coverage 5.09%

Importance

Changes 11
Bugs 0 Features 0
Metric Value
wmc 73
eloc 215
c 11
b 0
f 0
dl 0
loc 632
ccs 11
cts 216
cp 0.0509
rs 2.56

17 Methods

Rating   Name   Duplication   Size   Complexity  
A setMergeConflictRegExps() 0 3 1
A _getMergeConflictPrediction() 0 17 5
A setLogMessageLimit() 0 3 1
A applyStyle() 0 9 2
A _resetState() 0 6 1
A setCurrentRevision() 0 3 1
A _generateMergedViaColumn() 0 20 4
A __construct() 0 6 1
A withColumn() 0 5 1
A _generateLogMessageColumn() 0 25 4
B _generateDetailsRowContent() 0 46 7
A _getRelativeLogPath() 0 9 2
A setAggregateByBug() 0 3 1
A getPathCutOffRegExp() 0 13 1
A _generateSummaryColumn() 0 27 6
F printRevisions() 0 171 28
B aggregateRevisionsByBug() 0 38 7

How to fix   Complexity   

Complex Class

Complex classes like RevisionPrinter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use RevisionPrinter, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file is part of the SVN-Buddy library.
4
 * For the full copyright and license information, please view
5
 * the LICENSE file that was distributed with this source code.
6
 *
7
 * @copyright Alexander Obuhovich <[email protected]>
8
 * @link      https://github.com/console-helpers/svn-buddy
9
 */
10
11
namespace ConsoleHelpers\SVNBuddy\Repository\RevisionLog;
12
13
14
use ConsoleHelpers\SVNBuddy\Helper\DateHelper;
15
use ConsoleHelpers\SVNBuddy\Helper\OutputHelper;
16
use Symfony\Component\Console\Helper\Table;
17
use Symfony\Component\Console\Helper\TableCell;
18
use Symfony\Component\Console\Helper\TableSeparator;
19
use Symfony\Component\Console\Output\OutputInterface;
20
21
class RevisionPrinter
22
{
23
24
	const COLUMN_FULL_MESSAGE = 1;
25
26
	const COLUMN_DETAILS = 2;
27
28
	const COLUMN_SUMMARY = 3;
29
30
	const COLUMN_REFS = 4;
31
32
	const COLUMN_MERGE_ORACLE = 5;
33
34
	const COLUMN_MERGE_STATUS = 6;
35
36
	const COLUMN_REVISION_URL = 7;
37
38
	/**
39
	 * Date helper.
40
	 *
41
	 * @var DateHelper
42
	 */
43
	private $_dateHelper;
44
45
	/**
46
	 * Output helper.
47
	 *
48
	 * @var OutputHelper
49
	 */
50
	private $_outputHelper;
51
52
	/**
53
	 * Columns.
54
	 *
55
	 * @var array
56
	 */
57
	private $_columns = array();
58
59
	/**
60
	 * Aggregate by bug.
61
	 *
62
	 * @var boolean
63
	 */
64
	private $_aggregateByBug = false;
65
66
	/**
67
	 * Merge conflict regexps.
68
	 *
69
	 * @var array
70
	 */
71
	private $_mergeConflictRegExps = array();
72
73
	/**
74
	 * Log message limit.
75
	 *
76
	 * @var integer
77
	 */
78
	private $_logMessageLimit = 68;
79
80
	/**
81
	 * Current revision (e.g. in a working copy).
82
	 *
83
	 * @var integer|null
84
	 */
85
	private $_currentRevision;
86
87
	/**
88
	 * Creates instance of revision printer.
89
	 *
90
	 * @param DateHelper   $date_helper   Date helper.
91
	 * @param OutputHelper $output_helper Output helper.
92
	 */
93 1
	public function __construct(DateHelper $date_helper, OutputHelper $output_helper)
94
	{
95 1
		$this->_dateHelper = $date_helper;
96 1
		$this->_outputHelper = $output_helper;
97
98 1
		$this->_resetState();
99 1
	}
100
101
	/**
102
	 * Resets state.
103
	 *
104
	 * @return void
105
	 */
106 1
	private function _resetState()
107
	{
108 1
		$this->_columns = array();
109 1
		$this->_mergeConflictRegExps = array();
110 1
		$this->_logMessageLimit = 68;
111 1
		$this->_aggregateByBug = false;
112 1
	}
113
114
	/**
115
	 * Adds column to the output.
116
	 *
117
	 * @param integer $column Column.
118
	 *
119
	 * @return self
120
	 */
121
	public function withColumn($column)
122
	{
123
		$this->_columns[] = $column;
124
125
		return $this;
126
	}
127
128
	/**
129
	 * Sets aggregate by bug.
130
	 *
131
	 * @param boolean $aggregate_by_bug Aggregate by bug.
132
	 *
133
	 * @return void
134
	 */
135
	public function setAggregateByBug($aggregate_by_bug)
136
	{
137
		$this->_aggregateByBug = $aggregate_by_bug;
138
	}
139
140
	/**
141
	 * Sets merge conflict regexps.
142
	 *
143
	 * @param array $merge_conflict_regexps Merge conflict regexps.
144
	 *
145
	 * @return void
146
	 */
147
	public function setMergeConflictRegExps(array $merge_conflict_regexps)
148
	{
149
		$this->_mergeConflictRegExps = $merge_conflict_regexps;
150
	}
151
152
	/**
153
	 * Sets log message limit.
154
	 *
155
	 * @param integer $log_message_limit Log message limit.
156
	 *
157
	 * @return void
158
	 */
159
	public function setLogMessageLimit($log_message_limit)
160
	{
161
		$this->_logMessageLimit = $log_message_limit;
162
	}
163
164
	/**
165
	 * Sets current revision.
166
	 *
167
	 * @param integer $revision Revision.
168
	 *
169
	 * @return void
170
	 */
171
	public function setCurrentRevision($revision)
172
	{
173
		$this->_currentRevision = $revision;
174
	}
175
176
	/**
177
	 * Prints revisions.
178
	 *
179
	 * @param RevisionLog     $revision_log Revision log.
180
	 * @param array           $revisions    Revisions.
181
	 * @param OutputInterface $output       Output.
182
	 *
183
	 * @return void
184
	 */
185
	public function printRevisions(RevisionLog $revision_log, array $revisions, OutputInterface $output)
186
	{
187
		$table = new Table($output);
188
		$headers = array('Revision', 'Author', 'Date', 'Bug-ID', 'Log Message');
189
190
		$with_full_message = in_array(self::COLUMN_FULL_MESSAGE, $this->_columns);
191
		$with_details = in_array(self::COLUMN_DETAILS, $this->_columns);
192
193
		// Add "Summary" header.
194
		$with_summary = in_array(self::COLUMN_SUMMARY, $this->_columns);
195
196
		if ( $with_summary ) {
197
			$headers[] = 'Summary';
198
		}
199
200
		// Add "Refs" header.
201
		$with_refs = in_array(self::COLUMN_REFS, $this->_columns);
202
203
		if ( $with_refs ) {
204
			$headers[] = 'Refs';
205
		}
206
207
		$with_merge_oracle = in_array(self::COLUMN_MERGE_ORACLE, $this->_columns);
208
209
		// Add "M.O." header.
210
		if ( $with_merge_oracle ) {
211
			$headers[] = 'M.O.';
212
		}
213
214
		// Add "Merged Via" header.
215
		$with_merge_status = in_array(self::COLUMN_MERGE_STATUS, $this->_columns);
216
217
		if ( $with_merge_status ) {
218
			$headers[] = 'Merged Via';
219
		}
220
221
		// Add "Revision URL" header.
222
		$with_revision_url = in_array(self::COLUMN_REVISION_URL, $this->_columns);
223
224
		if ( $with_revision_url ) {
225
			$revision_url_mask = $revision_log->getRevisionURLBuilder()->getMask();
226
227
			if ( strpos($revision_url_mask, '://') !== false ) {
228
				// Add column only, when it will contain a URL.
229
				$headers[] = 'Revision URL';
230
			}
231
			else {
232
				// Don't add the column, when it won't contain a URL.
233
				$with_revision_url = false;
234
			}
235
		}
236
237
		$table->setHeaders($headers);
238
239
		$prev_bugs = null;
240
		$last_bug_color = 'yellow';
241
242
		if ( $this->_aggregateByBug ) {
243
			$aggregated_revisions = $this->aggregateRevisionsByBug($revisions, $revision_log);
244
			$revisions = array_keys($aggregated_revisions);
245
		}
246
247
		$last_revision = end($revisions);
248
249
		$project_path = $revision_log->getProjectPath();
250
251
		$bugs_per_row = $with_details ? 1 : 3;
252
253
		$revisions_data = $revision_log->getRevisionsData('summary', $revisions);
254
		$revisions_paths = $revision_log->getRevisionsData('paths', $revisions);
255
		$revisions_bugs = $revision_log->getRevisionsData('bugs', $revisions);
256
		$revisions_refs = $revision_log->getRevisionsData('refs', $revisions);
257
258
		if ( $with_merge_status ) {
259
			$revisions_merged_via = $revision_log->getRevisionsData('merges', $revisions);
260
			$revisions_merged_via_refs = $revision_log->getRevisionsData(
261
				'refs',
262
				call_user_func_array('array_merge', $revisions_merged_via)
263
			);
264
		}
265
266
		$first_revision = reset($revisions);
267
268
		foreach ( $revisions as $revision ) {
269
			$revision_data = $revisions_data[$revision];
270
271
			$new_bugs = $revisions_bugs[$revision];
272
273
			if ( isset($prev_bugs) && $new_bugs !== $prev_bugs ) {
274
				$last_bug_color = $last_bug_color === 'yellow' ? 'magenta' : 'yellow';
275
			}
276
277
			$row = array(
278
				$this->_aggregateByBug ? $aggregated_revisions[$revision] . ' cmts' : $revision,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $aggregated_revisions does not seem to be defined for all execution paths leading up to this point.
Loading history...
279
				$revision_data['author'],
280
				$this->_dateHelper->getAgoTime($revision_data['date']),
281
				$this->_outputHelper->formatArray($new_bugs, $bugs_per_row, $last_bug_color),
282
				$this->_generateLogMessageColumn($with_full_message || $with_details, $revision_data),
283
			);
284
285
			$revision_paths = $revisions_paths[$revision];
286
287
			// Add "Summary" column.
288
			if ( $with_summary ) {
289
				$row[] = $this->_generateSummaryColumn($revision_paths);
290
			}
291
292
			// Add "Refs" column.
293
			if ( $with_refs ) {
294
				$row[] = $this->_outputHelper->formatArray(
295
					$revisions_refs[$revision],
296
					1
297
				);
298
			}
299
300
			// Add "M.O." column.
301
			if ( $with_merge_oracle ) {
302
				$merge_conflict_prediction = $this->_getMergeConflictPrediction($revision_paths);
303
				$row[] = $merge_conflict_prediction ? '<error>' . count($merge_conflict_prediction) . '</error>' : '';
304
			}
305
			else {
306
				$merge_conflict_prediction = array();
307
			}
308
309
			// Add "Merged Via" column.
310
			if ( $with_merge_status ) {
311
				$row[] = $this->_generateMergedViaColumn($revisions_merged_via[$revision], $revisions_merged_via_refs);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $revisions_merged_via_refs does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $revisions_merged_via does not seem to be defined for all execution paths leading up to this point.
Loading history...
312
			}
313
314
			// Add "Revision URL" header.
315
			if ( $with_revision_url ) {
316
				$row[] = \str_replace('{revision}', $revision, $revision_url_mask);
317
			}
318
319
			if ( $revision === $this->_currentRevision ) {
320
				foreach ( $row as $index => $cell ) {
321
					$row[$index] = $this->applyStyle($cell, 'fg=white;options=bold');
322
				}
323
			}
324
325
			if ( $with_full_message && $revision !== $first_revision ) {
326
				$table->addRow(new TableSeparator());
327
			}
328
329
			$table->addRow($row);
330
331
			if ( $with_details ) {
332
				$details = $this->_generateDetailsRowContent(
333
					$revision,
334
					$revisions_refs,
335
					$revision_paths,
336
					$merge_conflict_prediction,
337
					$project_path
338
				);
339
340
				$table->addRow(new TableSeparator());
341
				$table->addRow(array(
342
					new TableCell($details, array('colspan' => count($headers))),
343
				));
344
345
				if ( $revision != $last_revision ) {
346
					$table->addRow(new TableSeparator());
347
				}
348
			}
349
350
			$prev_bugs = $new_bugs;
351
		}
352
353
		$table->render();
354
355
		$this->_resetState();
356
	}
357
358
	/**
359
	 * Aggregates revisions by bugs.
360
	 *
361
	 * @param array       $revisions    Revisions.
362
	 * @param RevisionLog $revision_log Revision Log.
363
	 *
364
	 * @return array
365
	 */
366
	protected function aggregateRevisionsByBug(array $revisions, RevisionLog $revision_log)
367
	{
368
		$bugs_revisions = array(
369
			'unknown' => array(),
370
		);
371
372
		$revisions_bugs = $revision_log->getRevisionsData('bugs', $revisions);
373
374
		foreach ( \array_reverse($revisions) as $revision ) {
375
			$revision_bugs = $revisions_bugs[$revision];
376
377
			if ( !$revision_bugs ) {
378
				$bugs_revisions['unknown'][] = $revision;
379
				continue;
380
			}
381
382
			foreach ( $revision_bugs as $revision_bug ) {
383
				if ( !isset($bugs_revisions[$revision_bug]) ) {
384
					$bugs_revisions[$revision_bug] = array();
385
				}
386
387
				$bugs_revisions[$revision_bug][] = $revision;
388
			}
389
		}
390
391
		if ( !$bugs_revisions['unknown'] ) {
392
			unset($bugs_revisions['unknown']);
393
		}
394
395
		$bugs_revisions = \array_reverse($bugs_revisions, true);
396
397
		$ret = array();
398
399
		foreach ( $bugs_revisions as $bug => $bug_revisions ) {
400
			$ret[reset($bug_revisions)] = count($bug_revisions);
401
		}
402
403
		return $ret;
404
	}
405
406
	/**
407
	 * Applies a style to the text.
408
	 *
409
	 * @param string $text  Text.
410
	 * @param string $style Style.
411
	 *
412
	 * @return string
413
	 */
414
	protected function applyStyle($text, $style)
415
	{
416
		if ( strpos($text, PHP_EOL) === false ) {
417
			return '<' . $style . '>' . $text . '</>';
418
		}
419
420
		$lines = explode(PHP_EOL, $text);
421
422
		return '<' . $style . '>' . implode('</>' . PHP_EOL . '<' . $style . '>', $lines) . '</>';
423
	}
424
425
	/**
426
	 * Returns log message.
427
	 *
428
	 * @param boolean $with_full_message Show commit message without truncation.
429
	 * @param array   $revision_data     Revision data.
430
	 *
431
	 * @return string
432
	 */
433
	private function _generateLogMessageColumn($with_full_message, array $revision_data)
434
	{
435
		$commit_message = trim($revision_data['msg']);
436
437
		if ( $with_full_message ) {
438
			// When details requested don't transform commit message except for word wrapping.
439
			// FIXME: Not UTF-8 safe solution.
440
			$log_message = wordwrap($commit_message, $this->_logMessageLimit);
441
442
			return $log_message;
443
		}
444
		else {
445
			// When details not requested only operate on first line of commit message.
446
			list($log_message,) = explode(PHP_EOL, $commit_message);
447
			$log_message = preg_replace('/(^|\s+)\[fixes:.*?\]/s', "$1\xE2\x9C\x94", $log_message);
448
449
			if ( strpos($commit_message, PHP_EOL) !== false
450
				|| mb_strlen($log_message) > $this->_logMessageLimit
451
			) {
452
				$log_message = mb_substr($log_message, 0, $this->_logMessageLimit - 3) . '...';
453
454
				return $log_message;
455
			}
456
457
			return $log_message;
458
		}
459
	}
460
461
	/**
462
	 * Generates change summary for a revision.
463
	 *
464
	 * @param array $revision_paths Revision paths.
465
	 *
466
	 * @return string
467
	 */
468
	private function _generateSummaryColumn(array $revision_paths)
469
	{
470
		$summary = array('added' => 0, 'changed' => 0, 'removed' => 0);
471
472
		foreach ( $revision_paths as $path_data ) {
473
			$path_action = $path_data['action'];
474
475
			if ( $path_action === 'A' ) {
476
				$summary['added']++;
477
			}
478
			elseif ( $path_action === 'D' ) {
479
				$summary['removed']++;
480
			}
481
			else {
482
				$summary['changed']++;
483
			}
484
		}
485
486
		if ( $summary['added'] ) {
487
			$summary['added'] = '<fg=green>+' . $summary['added'] . '</>';
488
		}
489
490
		if ( $summary['removed'] ) {
491
			$summary['removed'] = '<fg=red>-' . $summary['removed'] . '</>';
492
		}
493
494
		return implode(' ', array_filter($summary));
495
	}
496
497
	/**
498
	 * Returns merge conflict path predictions.
499
	 *
500
	 * @param array $revision_paths Revision paths.
501
	 *
502
	 * @return array
503
	 */
504
	private function _getMergeConflictPrediction(array $revision_paths)
505
	{
506
		if ( !$this->_mergeConflictRegExps ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->_mergeConflictRegExps of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
507
			return array();
508
		}
509
510
		$conflict_paths = array();
511
512
		foreach ( $revision_paths as $revision_path ) {
513
			foreach ( $this->_mergeConflictRegExps as $merge_conflict_regexp ) {
514
				if ( preg_match($merge_conflict_regexp, $revision_path['path']) ) {
515
					$conflict_paths[] = $revision_path['path'];
516
				}
517
			}
518
		}
519
520
		return $conflict_paths;
521
	}
522
523
	/**
524
	 * Generates content for "Merged Via" cell content.
525
	 *
526
	 * @param array $merged_via                Merged Via.
527
	 * @param array $revisions_merged_via_refs Merged Via Refs.
528
	 *
529
	 * @return string
530
	 */
531
	private function _generateMergedViaColumn(array $merged_via, array $revisions_merged_via_refs)
532
	{
533
		if ( !$merged_via ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $merged_via of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
534
			return '';
535
		}
536
537
		$merged_via_enhanced = array();
538
539
		foreach ( $merged_via as $merged_via_revision ) {
540
			$merged_via_revision_refs = $revisions_merged_via_refs[$merged_via_revision];
541
542
			if ( $merged_via_revision_refs ) {
543
				$merged_via_enhanced[] = $merged_via_revision . ' (' . implode(',', $merged_via_revision_refs) . ')';
544
			}
545
			else {
546
				$merged_via_enhanced[] = $merged_via_revision;
547
			}
548
		}
549
550
		return $this->_outputHelper->formatArray($merged_via_enhanced, 1);
551
	}
552
553
	/**
554
	 * Generates details row content.
555
	 *
556
	 * @param integer $revision                  Revision.
557
	 * @param array   $revisions_refs            Refs.
558
	 * @param array   $revision_paths            Revision paths.
559
	 * @param array   $merge_conflict_prediction Merge conflict prediction.
560
	 * @param string  $project_path              Project path.
561
	 *
562
	 * @return string
563
	 */
564
	private function _generateDetailsRowContent(
565
		$revision,
566
		array $revisions_refs,
567
		array $revision_paths,
568
		array $merge_conflict_prediction,
569
		$project_path
570
	) {
571
		$details = '<fg=white;options=bold>Changed Paths:</>';
572
		$path_cut_off_regexp = $this->getPathCutOffRegExp($project_path, $revisions_refs[$revision]);
573
574
		foreach ( $revision_paths as $path_data ) {
575
			$path_action = $path_data['action'];
576
			$relative_path = $this->_getRelativeLogPath($path_data, 'path', $path_cut_off_regexp);
577
578
			$details .= PHP_EOL . ' * ';
579
580
			if ( $path_action === 'A' ) {
581
				$color_format = 'fg=green';
582
			}
583
			elseif ( $path_action === 'D' ) {
584
				$color_format = 'fg=red';
585
			}
586
			else {
587
				$color_format = in_array($path_data['path'], $merge_conflict_prediction) ? 'error' : '';
588
			}
589
590
			$to_colorize = array($path_action . '    ' . $relative_path);
591
592
			if ( isset($path_data['copyfrom-path']) ) {
593
				// TODO: When copy happened from different ref/project, then relative path = absolute path.
594
				$copy_from_rev = $path_data['copyfrom-rev'];
595
				$copy_from_path = $this->_getRelativeLogPath($path_data, 'copyfrom-path', $path_cut_off_regexp);
596
				$to_colorize[] = '        (from ' . $copy_from_path . ':' . $copy_from_rev . ')';
597
			}
598
599
			if ( $color_format ) {
600
				$details .= '<' . $color_format . '>';
601
				$details .= implode('</>' . PHP_EOL . '<' . $color_format . '>', $to_colorize);
602
				$details .= '</>';
603
			}
604
			else {
605
				$details .= implode(PHP_EOL, $to_colorize);
606
			}
607
		}
608
609
		return $details;
610
	}
611
612
	/**
613
	 * Returns path cut off regexp.
614
	 *
615
	 * @param string $project_path Project path.
616
	 * @param array  $refs         Refs.
617
	 *
618
	 * @return string
619
	 */
620
	protected function getPathCutOffRegExp($project_path, array $refs)
0 ignored issues
show
Unused Code introduced by
The parameter $refs is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

620
	protected function getPathCutOffRegExp($project_path, /** @scrutinizer ignore-unused */ array $refs)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
621
	{
622
		$ret = array();
623
624
		// Remove ref from path only for single-ref revision.
625
		/*if ( count($refs) === 1 ) {
626
			$ret[] = $project_path . reset($refs) . '/';
627
		}*/
628
629
		// Always remove project path.
630
		$ret[] = $project_path;
631
632
		return '#^(' . implode('|', array_map('preg_quote', $ret)) . ')#';
633
	}
634
635
	/**
636
	 * Returns relative path to "svn log" returned path.
637
	 *
638
	 * @param array  $path_data           Path data.
639
	 * @param string $path_key            Path key.
640
	 * @param string $path_cut_off_regexp Path cut off regexp.
641
	 *
642
	 * @return string
643
	 */
644
	private function _getRelativeLogPath(array $path_data, $path_key, $path_cut_off_regexp)
645
	{
646
		$ret = preg_replace($path_cut_off_regexp, '', $path_data[$path_key], 1);
647
648
		if ( $ret === '' ) {
649
			$ret = '.';
650
		}
651
652
		return $ret;
653
	}
654
655
}
656