Completed
Push — master ( a4cfbb...5e49cf )
by Alexander
02:31
created

RevisionPrinter   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 513
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 4.78%

Importance

Changes 0
Metric Value
wmc 56
lcom 1
cbo 6
dl 0
loc 513
ccs 10
cts 209
cp 0.0478
rs 6.5957
c 0
b 0
f 0

14 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 139 21
B _generateLogMessageColumn() 0 27 4
B _generateSummaryColumn() 0 28 6
B _getMergeConflictPrediction() 0 18 5
A _generateMergedViaColumn() 0 21 4
C _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
		foreach ( $revisions as $revision ) {
223
			$revision_data = $revisions_data[$revision];
224
225
			$new_bugs = $revisions_bugs[$revision];
226
227
			if ( isset($prev_bugs) && $new_bugs !== $prev_bugs ) {
228
				$last_color = $last_color === 'yellow' ? 'magenta' : 'yellow';
229
			}
230
231
			$row = array(
232
				$revision,
233
				$revision_data['author'],
234
				$this->_dateHelper->getAgoTime($revision_data['date']),
235
				$this->_outputHelper->formatArray($new_bugs, $bugs_per_row, $last_color),
236
				$this->_generateLogMessageColumn($with_full_message || $with_details, $revision_data),
237
			);
238
239
			$revision_paths = $revisions_paths[$revision];
240
241
			// Add "Summary" column.
242
			if ( $with_summary ) {
243
				$row[] = $this->_generateSummaryColumn($revision_paths);
244
			}
245
246
			// Add "Refs" column.
247
			if ( $with_refs ) {
248
				$row[] = $this->_outputHelper->formatArray(
249
					$revisions_refs[$revision],
250
					1
251
				);
252
			}
253
254
			// Add "M.O." column.
255
			if ( $with_merge_oracle ) {
256
				$merge_conflict_prediction = $this->_getMergeConflictPrediction($revision_paths);
257
				$row[] = $merge_conflict_prediction ? '<error>' . count($merge_conflict_prediction) . '</error>' : '';
258
			}
259
			else {
260
				$merge_conflict_prediction = array();
261
			}
262
263
			// Add "Merged Via" column.
264
			if ( $with_merge_status ) {
265
				$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...
266
			}
267
268
			if ( $revision === $this->_currentRevision ) {
269
				foreach ( $row as $index => $cell ) {
270
					$row[$index] = sprintf('<fg=white;options=bold>%s</>', $cell);
271
				}
272
			}
273
274
			$table->addRow($row);
275
276
			if ( $with_details ) {
277
				$details = $this->_generateDetailsRowContent(
278
					$revision,
279
					$revisions_refs,
280
					$revision_paths,
281
					$merge_conflict_prediction,
282
					$project_path
283
				);
284
285
				$table->addRow(new TableSeparator());
286
				$table->addRow(array(
287
					new TableCell($details, array('colspan' => count($headers))),
288
				));
289
290
				if ( $revision != $last_revision ) {
291
					$table->addRow(new TableSeparator());
292
				}
293
			}
294
295
			$prev_bugs = $new_bugs;
296
		}
297
298
		$table->render();
299
300
		$this->_resetState();
301
	}
302
303
	/**
304
	 * Returns log message.
305
	 *
306
	 * @param boolean $with_full_message Show commit message without truncation.
307
	 * @param array   $revision_data     Revision data.
308
	 *
309
	 * @return string
310
	 */
311
	private function _generateLogMessageColumn($with_full_message, array $revision_data)
312
	{
313
		$commit_message = trim($revision_data['msg']);
314
315
		if ( $with_full_message ) {
316
			// When details requested don't transform commit message except for word wrapping.
317
			// FIXME: Not UTF-8 safe solution.
318
			$log_message = wordwrap($commit_message, $this->_logMessageLimit);
319
320
			return $log_message;
321
		}
322
		else {
323
			// When details not requested only operate on first line of commit message.
324
			list($log_message,) = explode(PHP_EOL, $commit_message);
325
			$log_message = preg_replace('/^\[fixes:.*?\]/s', "\xE2\x9C\x94", $log_message);
326
327
			if ( strpos($commit_message, PHP_EOL) !== false
328
				|| mb_strlen($log_message) > $this->_logMessageLimit
329
			) {
330
				$log_message = mb_substr($log_message, 0, $this->_logMessageLimit - 3) . '...';
331
332
				return $log_message;
333
			}
334
335
			return $log_message;
336
		}
337
	}
338
339
	/**
340
	 * Generates change summary for a revision.
341
	 *
342
	 * @param array $revision_paths Revision paths.
343
	 *
344
	 * @return string
345
	 */
346
	private function _generateSummaryColumn(array $revision_paths)
347
	{
348
		$summary = array('added' => 0, 'changed' => 0, 'removed' => 0);
349
350
		foreach ( $revision_paths as $path_data ) {
351
			$path_action = $path_data['action'];
352
353
			if ( $path_action === 'A' ) {
354
				$summary['added']++;
355
			}
356
			elseif ( $path_action === 'D' ) {
357
				$summary['removed']++;
358
			}
359
			else {
360
				$summary['changed']++;
361
			}
362
		}
363
364
		if ( $summary['added'] ) {
365
			$summary['added'] = '<fg=green>+' . $summary['added'] . '</>';
366
		}
367
368
		if ( $summary['removed'] ) {
369
			$summary['removed'] = '<fg=red>-' . $summary['removed'] . '</>';
370
		}
371
372
		return implode(' ', array_filter($summary));
373
	}
374
375
	/**
376
	 * Returns merge conflict path predictions.
377
	 *
378
	 * @param array $revision_paths Revision paths.
379
	 *
380
	 * @return array
381
	 */
382
	private function _getMergeConflictPrediction(array $revision_paths)
383
	{
384
		if ( !$this->_mergeConflictRegExps ) {
1 ignored issue
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...
385
			return array();
386
		}
387
388
		$conflict_paths = array();
389
390
		foreach ( $revision_paths as $revision_path ) {
391
			foreach ( $this->_mergeConflictRegExps as $merge_conflict_regexp ) {
392
				if ( preg_match($merge_conflict_regexp, $revision_path['path']) ) {
393
					$conflict_paths[] = $revision_path['path'];
394
				}
395
			}
396
		}
397
398
		return $conflict_paths;
399
	}
400
401
	/**
402
	 * Generates content for "Merged Via" cell content.
403
	 *
404
	 * @param array $merged_via                Merged Via.
405
	 * @param array $revisions_merged_via_refs Merged Via Refs.
406
	 *
407
	 * @return string
408
	 */
409
	private function _generateMergedViaColumn(array $merged_via, array $revisions_merged_via_refs)
410
	{
411
		if ( !$merged_via ) {
1 ignored issue
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...
412
			return '';
413
		}
414
415
		$merged_via_enhanced = array();
416
417
		foreach ( $merged_via as $merged_via_revision ) {
418
			$merged_via_revision_refs = $revisions_merged_via_refs[$merged_via_revision];
419
420
			if ( $merged_via_revision_refs ) {
421
				$merged_via_enhanced[] = $merged_via_revision . ' (' . implode(',', $merged_via_revision_refs) . ')';
422
			}
423
			else {
424
				$merged_via_enhanced[] = $merged_via_revision;
425
			}
426
		}
427
428
		return $this->_outputHelper->formatArray($merged_via_enhanced, 1);
429
	}
430
431
	/**
432
	 * Generates details row content.
433
	 *
434
	 * @param integer $revision                  Revision.
435
	 * @param array   $revisions_refs            Refs.
436
	 * @param array   $revision_paths            Revision paths.
437
	 * @param array   $merge_conflict_prediction Merge conflict prediction.
438
	 * @param string  $project_path              Project path.
439
	 *
440
	 * @return string
441
	 */
442
	private function _generateDetailsRowContent(
443
		$revision,
444
		array $revisions_refs,
445
		array $revision_paths,
446
		array $merge_conflict_prediction,
447
		$project_path
448
	) {
449
		$details = '<fg=white;options=bold>Changed Paths:</>';
450
		$path_cut_off_regexp = $this->getPathCutOffRegExp($project_path, $revisions_refs[$revision]);
451
452
		foreach ( $revision_paths as $path_data ) {
453
			$path_action = $path_data['action'];
454
			$relative_path = $this->_getRelativeLogPath($path_data, 'path', $path_cut_off_regexp);
455
456
			$details .= PHP_EOL . ' * ';
457
458
			if ( $path_action === 'A' ) {
459
				$color_format = 'fg=green';
460
			}
461
			elseif ( $path_action === 'D' ) {
462
				$color_format = 'fg=red';
463
			}
464
			else {
465
				$color_format = in_array($path_data['path'], $merge_conflict_prediction) ? 'error' : '';
466
			}
467
468
			$to_colorize = array($path_action . '    ' . $relative_path);
469
470
			if ( isset($path_data['copyfrom-path']) ) {
471
				// TODO: When copy happened from different ref/project, then relative path = absolute path.
472
				$copy_from_rev = $path_data['copyfrom-rev'];
473
				$copy_from_path = $this->_getRelativeLogPath($path_data, 'copyfrom-path', $path_cut_off_regexp);
474
				$to_colorize[] = '        (from ' . $copy_from_path . ':' . $copy_from_rev . ')';
475
			}
476
477
			if ( $color_format ) {
478
				$details .= '<' . $color_format . '>';
479
				$details .= implode('</>' . PHP_EOL . '<' . $color_format . '>', $to_colorize);
480
				$details .= '</>';
481
			}
482
			else {
483
				$details .= implode(PHP_EOL, $to_colorize);
484
			}
485
		}
486
487
		return $details;
488
	}
489
490
	/**
491
	 * Returns path cut off regexp.
492
	 *
493
	 * @param string $project_path Project path.
494
	 * @param array  $refs         Refs.
495
	 *
496
	 * @return string
497
	 */
498
	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...
499
	{
500
		$ret = array();
501
502
		// Remove ref from path only for single-ref revision.
503
		/*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...
504
			$ret[] = $project_path . reset($refs) . '/';
505
		}*/
506
507
		// Always remove project path.
508
		$ret[] = $project_path;
509
510
		return '#^(' . implode('|', array_map('preg_quote', $ret)) . ')#';
511
	}
512
513
	/**
514
	 * Returns relative path to "svn log" returned path.
515
	 *
516
	 * @param array  $path_data           Path data.
517
	 * @param string $path_key            Path key.
518
	 * @param string $path_cut_off_regexp Path cut off regexp.
519
	 *
520
	 * @return string
521
	 */
522
	private function _getRelativeLogPath(array $path_data, $path_key, $path_cut_off_regexp)
523
	{
524
		$ret = preg_replace($path_cut_off_regexp, '', $path_data[$path_key], 1);
525
526
		if ( $ret === '' ) {
527
			$ret = '.';
528
		}
529
530
		return $ret;
531
	}
532
533
}
534