Completed
Push — master ( 3ac3a2...9455ce )
by Alexander
02:37
created

RevisionPrinter   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 538
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 5.49%

Importance

Changes 0
Metric Value
wmc 60
lcom 1
cbo 6
dl 0
loc 538
ccs 10
cts 182
cp 0.0549
rs 3.6
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A _resetState() 0 6 1
A withColumn() 0 6 1
A setMergeConflictRegExps() 0 4 1
A setLogMessageLimit() 0 4 1
A setCurrentRevision() 0 4 1
F printRevisions() 0 145 23
A applyStyle() 0 10 2
A _generateLogMessageColumn() 0 27 4
B _generateSummaryColumn() 0 28 6
A _getMergeConflictPrediction() 0 18 5
A _generateMergedViaColumn() 0 21 4
B _generateDetailsRowContent() 0 47 7
A getPathCutOffRegExp() 0 14 1
A _getRelativeLogPath() 0 10 2

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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
	/**
37
	 * Date helper.
38
	 *
39
	 * @var DateHelper
40
	 */
41
	private $_dateHelper;
42
43
	/**
44
	 * Output helper.
45
	 *
46
	 * @var OutputHelper
47
	 */
48
	private $_outputHelper;
49
50
	/**
51
	 * Columns.
52
	 *
53
	 * @var array
54
	 */
55
	private $_columns = array();
56
57
	/**
58
	 * Merge conflict regexps.
59
	 *
60
	 * @var array
61
	 */
62
	private $_mergeConflictRegExps = array();
63
64
	/**
65
	 * Log message limit.
66
	 *
67
	 * @var integer
68
	 */
69
	private $_logMessageLimit = 68;
70
71
	/**
72
	 * Current revision (e.g. in a working copy).
73
	 *
74
	 * @var integer|null
75
	 */
76
	private $_currentRevision;
77
78
	/**
79
	 * Creates instance of revision printer.
80
	 *
81
	 * @param DateHelper   $date_helper   Date helper.
82
	 * @param OutputHelper $output_helper Output helper.
83
	 */
84 1
	public function __construct(DateHelper $date_helper, OutputHelper $output_helper)
85
	{
86 1
		$this->_dateHelper = $date_helper;
87 1
		$this->_outputHelper = $output_helper;
88
89 1
		$this->_resetState();
90 1
	}
91
92
	/**
93
	 * Resets state.
94
	 *
95
	 * @return void
96
	 */
97 1
	private function _resetState()
98
	{
99 1
		$this->_columns = array();
100 1
		$this->_mergeConflictRegExps = array();
101 1
		$this->_logMessageLimit = 68;
102 1
	}
103
104
	/**
105
	 * Adds column to the output.
106
	 *
107
	 * @param integer $column Column.
108
	 *
109
	 * @return self
110
	 */
111
	public function withColumn($column)
112
	{
113
		$this->_columns[] = $column;
114
115
		return $this;
116
	}
117
118
	/**
119
	 * Sets merge conflict regexps.
120
	 *
121
	 * @param array $merge_conflict_regexps Merge conflict regexps.
122
	 *
123
	 * @return void
124
	 */
125
	public function setMergeConflictRegExps(array $merge_conflict_regexps)
126
	{
127
		$this->_mergeConflictRegExps = $merge_conflict_regexps;
128
	}
129
130
	/**
131
	 * Sets log message limit.
132
	 *
133
	 * @param integer $log_message_limit Log message limit.
134
	 *
135
	 * @return void
136
	 */
137
	public function setLogMessageLimit($log_message_limit)
138
	{
139
		$this->_logMessageLimit = $log_message_limit;
140
	}
141
142
	/**
143
	 * Sets current revision.
144
	 *
145
	 * @param integer $revision Revision.
146
	 *
147
	 * @return void
148
	 */
149
	public function setCurrentRevision($revision)
150
	{
151
		$this->_currentRevision = $revision;
152
	}
153
154
	/**
155
	 * Prints revisions.
156
	 *
157
	 * @param RevisionLog     $revision_log Revision log.
158
	 * @param array           $revisions    Revisions.
159
	 * @param OutputInterface $output       Output.
160
	 *
161
	 * @return void
162
	 */
163
	public function printRevisions(RevisionLog $revision_log, array $revisions, OutputInterface $output)
164
	{
165
		$table = new Table($output);
166
		$headers = array('Revision', 'Author', 'Date', 'Bug-ID', 'Log Message');
167
168
		$with_full_message = in_array(self::COLUMN_FULL_MESSAGE, $this->_columns);
169
		$with_details = in_array(self::COLUMN_DETAILS, $this->_columns);
170
171
		// Add "Summary" header.
172
		$with_summary = in_array(self::COLUMN_SUMMARY, $this->_columns);
173
174
		if ( $with_summary ) {
175
			$headers[] = 'Summary';
176
		}
177
178
		// Add "Refs" header.
179
		$with_refs = in_array(self::COLUMN_REFS, $this->_columns);
180
181
		if ( $with_refs ) {
182
			$headers[] = 'Refs';
183
		}
184
185
		$with_merge_oracle = in_array(self::COLUMN_MERGE_ORACLE, $this->_columns);
186
187
		// Add "M.O." header.
188
		if ( $with_merge_oracle ) {
189
			$headers[] = 'M.O.';
190
		}
191
192
		// Add "Merged Via" header.
193
		$with_merge_status = in_array(self::COLUMN_MERGE_STATUS, $this->_columns);
194
195
		if ( $with_merge_status ) {
196
			$headers[] = 'Merged Via';
197
		}
198
199
		$table->setHeaders($headers);
200
201
		$prev_bugs = null;
202
		$last_color = 'yellow';
203
		$last_revision = end($revisions);
204
205
		$project_path = $revision_log->getProjectPath();
206
207
		$bugs_per_row = $with_details ? 1 : 3;
208
209
		$revisions_data = $revision_log->getRevisionsData('summary', $revisions);
210
		$revisions_paths = $revision_log->getRevisionsData('paths', $revisions);
211
		$revisions_bugs = $revision_log->getRevisionsData('bugs', $revisions);
212
		$revisions_refs = $revision_log->getRevisionsData('refs', $revisions);
213
214
		if ( $with_merge_status ) {
215
			$revisions_merged_via = $revision_log->getRevisionsData('merges', $revisions);
216
			$revisions_merged_via_refs = $revision_log->getRevisionsData(
217
				'refs',
218
				call_user_func_array('array_merge', $revisions_merged_via)
219
			);
220
		}
221
222
		$first_revision = reset($revisions);
223
224
		foreach ( $revisions as $revision ) {
225
			$revision_data = $revisions_data[$revision];
226
227
			$new_bugs = $revisions_bugs[$revision];
228
229
			if ( isset($prev_bugs) && $new_bugs !== $prev_bugs ) {
230
				$last_color = $last_color === 'yellow' ? 'magenta' : 'yellow';
231
			}
232
233
			$row = array(
234
				$revision,
235
				$revision_data['author'],
236
				$this->_dateHelper->getAgoTime($revision_data['date']),
237
				$this->_outputHelper->formatArray($new_bugs, $bugs_per_row, $last_color),
238
				$this->_generateLogMessageColumn($with_full_message || $with_details, $revision_data),
239
			);
240
241
			$revision_paths = $revisions_paths[$revision];
242
243
			// Add "Summary" column.
244
			if ( $with_summary ) {
245
				$row[] = $this->_generateSummaryColumn($revision_paths);
246
			}
247
248
			// Add "Refs" column.
249
			if ( $with_refs ) {
250
				$row[] = $this->_outputHelper->formatArray(
251
					$revisions_refs[$revision],
252
					1
253
				);
254
			}
255
256
			// Add "M.O." column.
257
			if ( $with_merge_oracle ) {
258
				$merge_conflict_prediction = $this->_getMergeConflictPrediction($revision_paths);
259
				$row[] = $merge_conflict_prediction ? '<error>' . count($merge_conflict_prediction) . '</error>' : '';
260
			}
261
			else {
262
				$merge_conflict_prediction = array();
263
			}
264
265
			// Add "Merged Via" column.
266
			if ( $with_merge_status ) {
267
				$row[] = $this->_generateMergedViaColumn($revisions_merged_via[$revision], $revisions_merged_via_refs);
0 ignored issues
show
Bug introduced by
The variable $revisions_merged_via does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $revisions_merged_via_refs does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
268
			}
269
270
			if ( $revision === $this->_currentRevision ) {
271
				foreach ( $row as $index => $cell ) {
272
					$row[$index] = $this->applyStyle($cell, 'fg=white;options=bold');
273
				}
274
			}
275
276
			if ( $with_full_message && $revision !== $first_revision ) {
277
				$table->addRow(new TableSeparator());
278
			}
279
280
			$table->addRow($row);
281
282
			if ( $with_details ) {
283
				$details = $this->_generateDetailsRowContent(
284
					$revision,
285
					$revisions_refs,
286
					$revision_paths,
287
					$merge_conflict_prediction,
288
					$project_path
289
				);
290
291
				$table->addRow(new TableSeparator());
292
				$table->addRow(array(
293
					new TableCell($details, array('colspan' => count($headers))),
294
				));
295
296
				if ( $revision != $last_revision ) {
297
					$table->addRow(new TableSeparator());
298
				}
299
			}
300
301
			$prev_bugs = $new_bugs;
302
		}
303
304
		$table->render();
305
306
		$this->_resetState();
307
	}
308
309
	/**
310
	 * Applies a style to the text.
311
	 *
312
	 * @param string $text  Text.
313
	 * @param string $style Style.
314
	 *
315
	 * @return string
316
	 */
317
	protected function applyStyle($text, $style)
318
	{
319
		if ( strpos($text, PHP_EOL) === false ) {
320
			return '<' . $style . '>' . $text . '</>';
321
		}
322
323
		$lines = explode(PHP_EOL, $text);
324
325
		return '<' . $style . '>' . implode('</>' . PHP_EOL . '<' . $style . '>', $lines) . '</>';
326
	}
327
328
	/**
329
	 * Returns log message.
330
	 *
331
	 * @param boolean $with_full_message Show commit message without truncation.
332
	 * @param array   $revision_data     Revision data.
333
	 *
334
	 * @return string
335
	 */
336
	private function _generateLogMessageColumn($with_full_message, array $revision_data)
337
	{
338
		$commit_message = trim($revision_data['msg']);
339
340
		if ( $with_full_message ) {
341
			// When details requested don't transform commit message except for word wrapping.
342
			// FIXME: Not UTF-8 safe solution.
343
			$log_message = wordwrap($commit_message, $this->_logMessageLimit);
344
345
			return $log_message;
346
		}
347
		else {
348
			// When details not requested only operate on first line of commit message.
349
			list($log_message,) = explode(PHP_EOL, $commit_message);
350
			$log_message = preg_replace('/(^|\s+)\[fixes:.*?\]/s', "$1\xE2\x9C\x94", $log_message);
351
352
			if ( strpos($commit_message, PHP_EOL) !== false
353
				|| mb_strlen($log_message) > $this->_logMessageLimit
354
			) {
355
				$log_message = mb_substr($log_message, 0, $this->_logMessageLimit - 3) . '...';
356
357
				return $log_message;
358
			}
359
360
			return $log_message;
361
		}
362
	}
363
364
	/**
365
	 * Generates change summary for a revision.
366
	 *
367
	 * @param array $revision_paths Revision paths.
368
	 *
369
	 * @return string
370
	 */
371
	private function _generateSummaryColumn(array $revision_paths)
372
	{
373
		$summary = array('added' => 0, 'changed' => 0, 'removed' => 0);
374
375
		foreach ( $revision_paths as $path_data ) {
376
			$path_action = $path_data['action'];
377
378
			if ( $path_action === 'A' ) {
379
				$summary['added']++;
380
			}
381
			elseif ( $path_action === 'D' ) {
382
				$summary['removed']++;
383
			}
384
			else {
385
				$summary['changed']++;
386
			}
387
		}
388
389
		if ( $summary['added'] ) {
390
			$summary['added'] = '<fg=green>+' . $summary['added'] . '</>';
391
		}
392
393
		if ( $summary['removed'] ) {
394
			$summary['removed'] = '<fg=red>-' . $summary['removed'] . '</>';
395
		}
396
397
		return implode(' ', array_filter($summary));
398
	}
399
400
	/**
401
	 * Returns merge conflict path predictions.
402
	 *
403
	 * @param array $revision_paths Revision paths.
404
	 *
405
	 * @return array
406
	 */
407
	private function _getMergeConflictPrediction(array $revision_paths)
408
	{
409
		if ( !$this->_mergeConflictRegExps ) {
410
			return array();
411
		}
412
413
		$conflict_paths = array();
414
415
		foreach ( $revision_paths as $revision_path ) {
416
			foreach ( $this->_mergeConflictRegExps as $merge_conflict_regexp ) {
417
				if ( preg_match($merge_conflict_regexp, $revision_path['path']) ) {
418
					$conflict_paths[] = $revision_path['path'];
419
				}
420
			}
421
		}
422
423
		return $conflict_paths;
424
	}
425
426
	/**
427
	 * Generates content for "Merged Via" cell content.
428
	 *
429
	 * @param array $merged_via                Merged Via.
430
	 * @param array $revisions_merged_via_refs Merged Via Refs.
431
	 *
432
	 * @return string
433
	 */
434
	private function _generateMergedViaColumn(array $merged_via, array $revisions_merged_via_refs)
435
	{
436
		if ( !$merged_via ) {
437
			return '';
438
		}
439
440
		$merged_via_enhanced = array();
441
442
		foreach ( $merged_via as $merged_via_revision ) {
443
			$merged_via_revision_refs = $revisions_merged_via_refs[$merged_via_revision];
444
445
			if ( $merged_via_revision_refs ) {
446
				$merged_via_enhanced[] = $merged_via_revision . ' (' . implode(',', $merged_via_revision_refs) . ')';
447
			}
448
			else {
449
				$merged_via_enhanced[] = $merged_via_revision;
450
			}
451
		}
452
453
		return $this->_outputHelper->formatArray($merged_via_enhanced, 1);
454
	}
455
456
	/**
457
	 * Generates details row content.
458
	 *
459
	 * @param integer $revision                  Revision.
460
	 * @param array   $revisions_refs            Refs.
461
	 * @param array   $revision_paths            Revision paths.
462
	 * @param array   $merge_conflict_prediction Merge conflict prediction.
463
	 * @param string  $project_path              Project path.
464
	 *
465
	 * @return string
466
	 */
467
	private function _generateDetailsRowContent(
468
		$revision,
469
		array $revisions_refs,
470
		array $revision_paths,
471
		array $merge_conflict_prediction,
472
		$project_path
473
	) {
474
		$details = '<fg=white;options=bold>Changed Paths:</>';
475
		$path_cut_off_regexp = $this->getPathCutOffRegExp($project_path, $revisions_refs[$revision]);
476
477
		foreach ( $revision_paths as $path_data ) {
478
			$path_action = $path_data['action'];
479
			$relative_path = $this->_getRelativeLogPath($path_data, 'path', $path_cut_off_regexp);
480
481
			$details .= PHP_EOL . ' * ';
482
483
			if ( $path_action === 'A' ) {
484
				$color_format = 'fg=green';
485
			}
486
			elseif ( $path_action === 'D' ) {
487
				$color_format = 'fg=red';
488
			}
489
			else {
490
				$color_format = in_array($path_data['path'], $merge_conflict_prediction) ? 'error' : '';
491
			}
492
493
			$to_colorize = array($path_action . '    ' . $relative_path);
494
495
			if ( isset($path_data['copyfrom-path']) ) {
496
				// TODO: When copy happened from different ref/project, then relative path = absolute path.
497
				$copy_from_rev = $path_data['copyfrom-rev'];
498
				$copy_from_path = $this->_getRelativeLogPath($path_data, 'copyfrom-path', $path_cut_off_regexp);
499
				$to_colorize[] = '        (from ' . $copy_from_path . ':' . $copy_from_rev . ')';
500
			}
501
502
			if ( $color_format ) {
503
				$details .= '<' . $color_format . '>';
504
				$details .= implode('</>' . PHP_EOL . '<' . $color_format . '>', $to_colorize);
505
				$details .= '</>';
506
			}
507
			else {
508
				$details .= implode(PHP_EOL, $to_colorize);
509
			}
510
		}
511
512
		return $details;
513
	}
514
515
	/**
516
	 * Returns path cut off regexp.
517
	 *
518
	 * @param string $project_path Project path.
519
	 * @param array  $refs         Refs.
520
	 *
521
	 * @return string
522
	 */
523
	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.

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

Loading history...
524
	{
525
		$ret = array();
526
527
		// Remove ref from path only for single-ref revision.
528
		/*if ( count($refs) === 1 ) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
48% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
529
			$ret[] = $project_path . reset($refs) . '/';
530
		}*/
531
532
		// Always remove project path.
533
		$ret[] = $project_path;
534
535
		return '#^(' . implode('|', array_map('preg_quote', $ret)) . ')#';
536
	}
537
538
	/**
539
	 * Returns relative path to "svn log" returned path.
540
	 *
541
	 * @param array  $path_data           Path data.
542
	 * @param string $path_key            Path key.
543
	 * @param string $path_cut_off_regexp Path cut off regexp.
544
	 *
545
	 * @return string
546
	 */
547
	private function _getRelativeLogPath(array $path_data, $path_key, $path_cut_off_regexp)
548
	{
549
		$ret = preg_replace($path_cut_off_regexp, '', $path_data[$path_key], 1);
550
551
		if ( $ret === '' ) {
552
			$ret = '.';
553
		}
554
555
		return $ret;
556
	}
557
558
}
559