Completed
Push — master ( fb09c0...db5833 )
by Alexander
03:24
created

RevisionPrinter   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 508
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 4.83%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 55
c 3
b 0
f 0
lcom 1
cbo 6
dl 0
loc 508
ccs 10
cts 207
cp 0.0483
rs 6.8

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 138 20
B _generateLogMessageColumn() 0 25 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_DETAILS = 1;
25
26
	const COLUMN_SUMMARY = 2;
27
28
	const COLUMN_REFS = 3;
29
30
	const COLUMN_MERGE_ORACLE = 4;
31
32
	const COLUMN_MERGE_STATUS = 5;
33
34
	/**
35
	 * Date helper.
36
	 *
37
	 * @var DateHelper
38
	 */
39
	private $_dateHelper;
40
41
	/**
42
	 * Output helper.
43
	 *
44
	 * @var OutputHelper
45
	 */
46
	private $_outputHelper;
47
48
	/**
49
	 * Columns.
50
	 *
51
	 * @var array
52
	 */
53
	private $_columns = array();
54
55
	/**
56
	 * Merge conflict regexps.
57
	 *
58
	 * @var array
59
	 */
60
	private $_mergeConflictRegExps = array();
61
62
	/**
63
	 * Log message limit.
64
	 *
65
	 * @var integer
66
	 */
67
	private $_logMessageLimit = 68;
68
69
	/**
70
	 * Current revision (e.g. in a working copy).
71
	 *
72
	 * @var integer|null
73
	 */
74
	private $_currentRevision;
75
76
	/**
77
	 * Creates instance of revision printer.
78
	 *
79
	 * @param DateHelper   $date_helper   Date helper.
80
	 * @param OutputHelper $output_helper Output helper.
81
	 */
82 1
	public function __construct(DateHelper $date_helper, OutputHelper $output_helper)
83
	{
84 1
		$this->_dateHelper = $date_helper;
85 1
		$this->_outputHelper = $output_helper;
86
87 1
		$this->_resetState();
88 1
	}
89
90
	/**
91
	 * Resets state.
92
	 *
93
	 * @return void
94
	 */
95 1
	private function _resetState()
96
	{
97 1
		$this->_columns = array();
98 1
		$this->_mergeConflictRegExps = array();
99 1
		$this->_logMessageLimit = 68;
100 1
	}
101
102
	/**
103
	 * Adds column to the output.
104
	 *
105
	 * @param integer $column Column.
106
	 *
107
	 * @return self
108
	 */
109
	public function withColumn($column)
110
	{
111
		$this->_columns[] = $column;
112
113
		return $this;
114
	}
115
116
	/**
117
	 * Sets merge conflict regexps.
118
	 *
119
	 * @param array $merge_conflict_regexps Merge conflict regexps.
120
	 *
121
	 * @return void
122
	 */
123
	public function setMergeConflictRegExps(array $merge_conflict_regexps)
124
	{
125
		$this->_mergeConflictRegExps = $merge_conflict_regexps;
126
	}
127
128
	/**
129
	 * Sets log message limit.
130
	 *
131
	 * @param integer $log_message_limit Log message limit.
132
	 *
133
	 * @return void
134
	 */
135
	public function setLogMessageLimit($log_message_limit)
136
	{
137
		$this->_logMessageLimit = $log_message_limit;
138
	}
139
140
	/**
141
	 * Sets current revision.
142
	 *
143
	 * @param integer $revision Revision.
144
	 *
145
	 * @return void
146
	 */
147
	public function setCurrentRevision($revision)
148
	{
149
		$this->_currentRevision = $revision;
150
	}
151
152
	/**
153
	 * Prints revisions.
154
	 *
155
	 * @param RevisionLog     $revision_log Revision log.
156
	 * @param array           $revisions    Revisions.
157
	 * @param OutputInterface $output       Output.
158
	 *
159
	 * @return void
160
	 */
161
	public function printRevisions(RevisionLog $revision_log, array $revisions, OutputInterface $output)
162
	{
163
		$table = new Table($output);
164
		$headers = array('Revision', 'Author', 'Date', 'Bug-ID', 'Log Message');
165
166
		$with_details = in_array(self::COLUMN_DETAILS, $this->_columns);
167
168
		// Add "Summary" header.
169
		$with_summary = in_array(self::COLUMN_SUMMARY, $this->_columns);
170
171
		if ( $with_summary ) {
172
			$headers[] = 'Summary';
173
		}
174
175
		// Add "Refs" header.
176
		$with_refs = in_array(self::COLUMN_REFS, $this->_columns);
177
178
		if ( $with_refs ) {
179
			$headers[] = 'Refs';
180
		}
181
182
		$with_merge_oracle = in_array(self::COLUMN_MERGE_ORACLE, $this->_columns);
183
184
		// Add "M.O." header.
185
		if ( $with_merge_oracle ) {
186
			$headers[] = 'M.O.';
187
		}
188
189
		// Add "Merged Via" header.
190
		$with_merge_status = in_array(self::COLUMN_MERGE_STATUS, $this->_columns);
191
192
		if ( $with_merge_status ) {
193
			$headers[] = 'Merged Via';
194
		}
195
196
		$table->setHeaders($headers);
197
198
		$prev_bugs = null;
199
		$last_color = 'yellow';
200
		$last_revision = end($revisions);
201
202
		$project_path = $revision_log->getProjectPath();
203
204
		$bugs_per_row = $with_details ? 1 : 3;
205
206
		$revisions_data = $revision_log->getRevisionsData('summary', $revisions);
207
		$revisions_paths = $revision_log->getRevisionsData('paths', $revisions);
208
		$revisions_bugs = $revision_log->getRevisionsData('bugs', $revisions);
209
		$revisions_refs = $revision_log->getRevisionsData('refs', $revisions);
210
211
		if ( $with_merge_status ) {
212
			$revisions_merged_via = $revision_log->getRevisionsData('merges', $revisions);
213
			$revisions_merged_via_refs = $revision_log->getRevisionsData(
214
				'refs',
215
				call_user_func_array('array_merge', $revisions_merged_via)
216
			);
217
		}
218
219
		foreach ( $revisions as $revision ) {
220
			$revision_data = $revisions_data[$revision];
221
222
			$new_bugs = $revisions_bugs[$revision];
223
224
			if ( isset($prev_bugs) && $new_bugs !== $prev_bugs ) {
225
				$last_color = $last_color == 'yellow' ? 'magenta' : 'yellow';
226
			}
227
228
			$row = array(
229
				$revision,
230
				$revision_data['author'],
231
				$this->_dateHelper->getAgoTime($revision_data['date']),
232
				$this->_outputHelper->formatArray($new_bugs, $bugs_per_row, $last_color),
233
				$this->_generateLogMessageColumn($with_details, $revision_data),
234
			);
235
236
			$revision_paths = $revisions_paths[$revision];
237
238
			// Add "Summary" column.
239
			if ( $with_summary ) {
240
				$row[] = $this->_generateSummaryColumn($revision_paths);
241
			}
242
243
			// Add "Refs" column.
244
			if ( $with_refs ) {
245
				$row[] = $this->_outputHelper->formatArray(
246
					$revisions_refs[$revision],
247
					1
248
				);
249
			}
250
251
			// Add "M.O." column.
252
			if ( $with_merge_oracle ) {
253
				$merge_conflict_prediction = $this->_getMergeConflictPrediction($revision_paths);
254
				$row[] = $merge_conflict_prediction ? '<error>' . count($merge_conflict_prediction) . '</error>' : '';
255
			}
256
			else {
257
				$merge_conflict_prediction = array();
258
			}
259
260
			// Add "Merged Via" column.
261
			if ( $with_merge_status ) {
262
				$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...
263
			}
264
265
			if ( $revision === $this->_currentRevision ) {
266
				foreach ( $row as $index => $cell ) {
267
					$row[$index] = sprintf('<fg=white;options=bold>%s</>', $cell);
268
				}
269
			}
270
271
			$table->addRow($row);
272
273
			if ( $with_details ) {
274
				$details = $this->_generateDetailsRowContent(
275
					$revision,
276
					$revisions_refs,
277
					$revision_paths,
278
					$merge_conflict_prediction,
279
					$project_path
280
				);
281
282
				$table->addRow(new TableSeparator());
283
				$table->addRow(array(
284
					new TableCell($details, array('colspan' => count($headers))),
285
				));
286
287
				if ( $revision != $last_revision ) {
288
					$table->addRow(new TableSeparator());
289
				}
290
			}
291
292
			$prev_bugs = $new_bugs;
293
		}
294
295
		$table->render();
296
297
		$this->_resetState();
298
	}
299
300
	/**
301
	 * Returns log message.
302
	 *
303
	 * @param boolean $with_details  With details.
304
	 * @param array   $revision_data Revision data.
305
	 *
306
	 * @return string
307
	 */
308
	private function _generateLogMessageColumn($with_details, array $revision_data)
309
	{
310
		if ( $with_details ) {
311
			// When details requested don't transform commit message except for word wrapping.
312
			// FIXME: Not UTF-8 safe solution.
313
			$log_message = wordwrap($revision_data['msg'], $this->_logMessageLimit);
314
315
			return $log_message;
316
		}
317
		else {
318
			// When details not requested only operate on first line of commit message.
319
			list($log_message,) = explode(PHP_EOL, $revision_data['msg']);
320
			$log_message = preg_replace('/^\[fixes:.*?\]/s', "\xE2\x9C\x94", $log_message);
321
322
			if ( strpos($revision_data['msg'], PHP_EOL) !== false
323
				|| mb_strlen($log_message) > $this->_logMessageLimit
324
			) {
325
				$log_message = mb_substr($log_message, 0, $this->_logMessageLimit - 3) . '...';
326
327
				return $log_message;
328
			}
329
330
			return $log_message;
331
		}
332
	}
333
334
	/**
335
	 * Generates change summary for a revision.
336
	 *
337
	 * @param array $revision_paths Revision paths.
338
	 *
339
	 * @return string
340
	 */
341
	private function _generateSummaryColumn(array $revision_paths)
342
	{
343
		$summary = array('added' => 0, 'changed' => 0, 'removed' => 0);
344
345
		foreach ( $revision_paths as $path_data ) {
346
			$path_action = $path_data['action'];
347
348
			if ( $path_action == 'A' ) {
349
				$summary['added']++;
350
			}
351
			elseif ( $path_action == 'D' ) {
352
				$summary['removed']++;
353
			}
354
			else {
355
				$summary['changed']++;
356
			}
357
		}
358
359
		if ( $summary['added'] ) {
360
			$summary['added'] = '<fg=green>+' . $summary['added'] . '</>';
361
		}
362
363
		if ( $summary['removed'] ) {
364
			$summary['removed'] = '<fg=red>-' . $summary['removed'] . '</>';
365
		}
366
367
		return implode(' ', array_filter($summary));
368
	}
369
370
	/**
371
	 * Returns merge conflict path predictions.
372
	 *
373
	 * @param array $revision_paths Revision paths.
374
	 *
375
	 * @return array
376
	 */
377
	private function _getMergeConflictPrediction(array $revision_paths)
378
	{
379
		if ( !$this->_mergeConflictRegExps ) {
380
			return array();
381
		}
382
383
		$conflict_paths = array();
384
385
		foreach ( $revision_paths as $revision_path ) {
386
			foreach ( $this->_mergeConflictRegExps as $merge_conflict_regexp ) {
387
				if ( preg_match($merge_conflict_regexp, $revision_path['path']) ) {
388
					$conflict_paths[] = $revision_path['path'];
389
				}
390
			}
391
		}
392
393
		return $conflict_paths;
394
	}
395
396
	/**
397
	 * Generates content for "Merged Via" cell content.
398
	 *
399
	 * @param array $merged_via                Merged Via.
400
	 * @param array $revisions_merged_via_refs Merged Via Refs.
401
	 *
402
	 * @return string
403
	 */
404
	private function _generateMergedViaColumn(array $merged_via, array $revisions_merged_via_refs)
405
	{
406
		if ( !$merged_via ) {
407
			return '';
408
		}
409
410
		$merged_via_enhanced = array();
411
412
		foreach ( $merged_via as $merged_via_revision ) {
413
			$merged_via_revision_refs = $revisions_merged_via_refs[$merged_via_revision];
414
415
			if ( $merged_via_revision_refs ) {
416
				$merged_via_enhanced[] = $merged_via_revision . ' (' . implode(',', $merged_via_revision_refs) . ')';
417
			}
418
			else {
419
				$merged_via_enhanced[] = $merged_via_revision;
420
			}
421
		}
422
423
		return $this->_outputHelper->formatArray($merged_via_enhanced, 1);
424
	}
425
426
	/**
427
	 * Generates details row content.
428
	 *
429
	 * @param integer $revision                  Revision.
430
	 * @param array   $revisions_refs            Refs.
431
	 * @param array   $revision_paths            Revision paths.
432
	 * @param array   $merge_conflict_prediction Merge conflict prediction.
433
	 * @param string  $project_path              Project path.
434
	 *
435
	 * @return string
436
	 */
437
	private function _generateDetailsRowContent(
438
		$revision,
439
		array $revisions_refs,
440
		array $revision_paths,
441
		array $merge_conflict_prediction,
442
		$project_path
443
	) {
444
		$details = '<fg=white;options=bold>Changed Paths:</>';
445
		$path_cut_off_regexp = $this->getPathCutOffRegExp($project_path, $revisions_refs[$revision]);
446
447
		foreach ( $revision_paths as $path_data ) {
448
			$path_action = $path_data['action'];
449
			$relative_path = $this->_getRelativeLogPath($path_data, 'path', $path_cut_off_regexp);
450
451
			$details .= PHP_EOL . ' * ';
452
453
			if ( $path_action == 'A' ) {
454
				$color_format = 'fg=green';
455
			}
456
			elseif ( $path_action == 'D' ) {
457
				$color_format = 'fg=red';
458
			}
459
			else {
460
				$color_format = in_array($path_data['path'], $merge_conflict_prediction) ? 'error' : '';
461
			}
462
463
			$to_colorize = array($path_action . '    ' . $relative_path);
464
465
			if ( isset($path_data['copyfrom-path']) ) {
466
				// TODO: When copy happened from different ref/project, then relative path = absolute path.
467
				$copy_from_rev = $path_data['copyfrom-rev'];
468
				$copy_from_path = $this->_getRelativeLogPath($path_data, 'copyfrom-path', $path_cut_off_regexp);
469
				$to_colorize[] = '        (from ' . $copy_from_path . ':' . $copy_from_rev . ')';
470
			}
471
472
			if ( $color_format ) {
473
				$details .= '<' . $color_format . '>';
474
				$details .= implode('</>' . PHP_EOL . '<' . $color_format . '>', $to_colorize);
475
				$details .= '</>';
476
			}
477
			else {
478
				$details .= implode(PHP_EOL, $to_colorize);
479
			}
480
		}
481
482
		return $details;
483
	}
484
485
	/**
486
	 * Returns path cut off regexp.
487
	 *
488
	 * @param string $project_path Project path.
489
	 * @param array  $refs         Refs.
490
	 *
491
	 * @return string
492
	 */
493
	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...
494
	{
495
		$ret = array();
496
497
		// Remove ref from path only for single-ref revision.
498
		/*if ( count($refs) === 1 ) {
499
			$ret[] = $project_path . reset($refs) . '/';
500
		}*/
501
502
		// Always remove project path.
503
		$ret[] = $project_path;
504
505
		return '#^(' . implode('|', array_map('preg_quote', $ret)) . ')#';
506
	}
507
508
	/**
509
	 * Returns relative path to "svn log" returned path.
510
	 *
511
	 * @param array  $path_data           Path data.
512
	 * @param string $path_key            Path key.
513
	 * @param string $path_cut_off_regexp Path cut off regexp.
514
	 *
515
	 * @return string
516
	 */
517
	private function _getRelativeLogPath(array $path_data, $path_key, $path_cut_off_regexp)
518
	{
519
		$ret = preg_replace($path_cut_off_regexp, '', $path_data[$path_key], 1);
520
521
		if ( $ret === '' ) {
522
			$ret = '.';
523
		}
524
525
		return $ret;
526
	}
527
528
}
529