Completed
Push — master ( 59ac10...3a6c85 )
by Alexander
12s
created

RevisionPrinter::_generateSummaryColumn()   B

Complexity

Conditions 6
Paths 16

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 28
ccs 0
cts 14
cp 0
rs 8.439
c 0
b 0
f 0
cc 6
eloc 15
nc 16
nop 1
crap 42
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] = $this->applyStyle($cell, 'fg=white;options=bold');
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
	 * Applies a style to the text.
305
	 *
306
	 * @param string $text  Text.
307
	 * @param string $style Style.
308
	 *
309
	 * @return string
310
	 */
311
	protected function applyStyle($text, $style)
312
	{
313
		if ( strpos($text, PHP_EOL) === false ) {
314
			return '<' . $style . '>' . $text . '</>';
315
		}
316
317
		$lines = explode(PHP_EOL, $text);
318
319
		return '<' . $style . '>' . implode('</>' . PHP_EOL . '<' . $style . '>', $lines) . '</>';
320
	}
321
322
	/**
323
	 * Returns log message.
324
	 *
325
	 * @param boolean $with_full_message Show commit message without truncation.
326
	 * @param array   $revision_data     Revision data.
327
	 *
328
	 * @return string
329
	 */
330
	private function _generateLogMessageColumn($with_full_message, array $revision_data)
331
	{
332
		$commit_message = trim($revision_data['msg']);
333
334
		if ( $with_full_message ) {
335
			// When details requested don't transform commit message except for word wrapping.
336
			// FIXME: Not UTF-8 safe solution.
337
			$log_message = wordwrap($commit_message, $this->_logMessageLimit);
338
339
			return $log_message;
340
		}
341
		else {
342
			// When details not requested only operate on first line of commit message.
343
			list($log_message,) = explode(PHP_EOL, $commit_message);
344
			$log_message = preg_replace('/^\[fixes:.*?\]/s', "\xE2\x9C\x94", $log_message);
345
346
			if ( strpos($commit_message, PHP_EOL) !== false
347
				|| mb_strlen($log_message) > $this->_logMessageLimit
348
			) {
349
				$log_message = mb_substr($log_message, 0, $this->_logMessageLimit - 3) . '...';
350
351
				return $log_message;
352
			}
353
354
			return $log_message;
355
		}
356
	}
357
358
	/**
359
	 * Generates change summary for a revision.
360
	 *
361
	 * @param array $revision_paths Revision paths.
362
	 *
363
	 * @return string
364
	 */
365
	private function _generateSummaryColumn(array $revision_paths)
366
	{
367
		$summary = array('added' => 0, 'changed' => 0, 'removed' => 0);
368
369
		foreach ( $revision_paths as $path_data ) {
370
			$path_action = $path_data['action'];
371
372
			if ( $path_action === 'A' ) {
373
				$summary['added']++;
374
			}
375
			elseif ( $path_action === 'D' ) {
376
				$summary['removed']++;
377
			}
378
			else {
379
				$summary['changed']++;
380
			}
381
		}
382
383
		if ( $summary['added'] ) {
384
			$summary['added'] = '<fg=green>+' . $summary['added'] . '</>';
385
		}
386
387
		if ( $summary['removed'] ) {
388
			$summary['removed'] = '<fg=red>-' . $summary['removed'] . '</>';
389
		}
390
391
		return implode(' ', array_filter($summary));
392
	}
393
394
	/**
395
	 * Returns merge conflict path predictions.
396
	 *
397
	 * @param array $revision_paths Revision paths.
398
	 *
399
	 * @return array
400
	 */
401
	private function _getMergeConflictPrediction(array $revision_paths)
402
	{
403
		if ( !$this->_mergeConflictRegExps ) {
404
			return array();
405
		}
406
407
		$conflict_paths = array();
408
409
		foreach ( $revision_paths as $revision_path ) {
410
			foreach ( $this->_mergeConflictRegExps as $merge_conflict_regexp ) {
411
				if ( preg_match($merge_conflict_regexp, $revision_path['path']) ) {
412
					$conflict_paths[] = $revision_path['path'];
413
				}
414
			}
415
		}
416
417
		return $conflict_paths;
418
	}
419
420
	/**
421
	 * Generates content for "Merged Via" cell content.
422
	 *
423
	 * @param array $merged_via                Merged Via.
424
	 * @param array $revisions_merged_via_refs Merged Via Refs.
425
	 *
426
	 * @return string
427
	 */
428
	private function _generateMergedViaColumn(array $merged_via, array $revisions_merged_via_refs)
429
	{
430
		if ( !$merged_via ) {
431
			return '';
432
		}
433
434
		$merged_via_enhanced = array();
435
436
		foreach ( $merged_via as $merged_via_revision ) {
437
			$merged_via_revision_refs = $revisions_merged_via_refs[$merged_via_revision];
438
439
			if ( $merged_via_revision_refs ) {
440
				$merged_via_enhanced[] = $merged_via_revision . ' (' . implode(',', $merged_via_revision_refs) . ')';
441
			}
442
			else {
443
				$merged_via_enhanced[] = $merged_via_revision;
444
			}
445
		}
446
447
		return $this->_outputHelper->formatArray($merged_via_enhanced, 1);
448
	}
449
450
	/**
451
	 * Generates details row content.
452
	 *
453
	 * @param integer $revision                  Revision.
454
	 * @param array   $revisions_refs            Refs.
455
	 * @param array   $revision_paths            Revision paths.
456
	 * @param array   $merge_conflict_prediction Merge conflict prediction.
457
	 * @param string  $project_path              Project path.
458
	 *
459
	 * @return string
460
	 */
461
	private function _generateDetailsRowContent(
462
		$revision,
463
		array $revisions_refs,
464
		array $revision_paths,
465
		array $merge_conflict_prediction,
466
		$project_path
467
	) {
468
		$details = '<fg=white;options=bold>Changed Paths:</>';
469
		$path_cut_off_regexp = $this->getPathCutOffRegExp($project_path, $revisions_refs[$revision]);
470
471
		foreach ( $revision_paths as $path_data ) {
472
			$path_action = $path_data['action'];
473
			$relative_path = $this->_getRelativeLogPath($path_data, 'path', $path_cut_off_regexp);
474
475
			$details .= PHP_EOL . ' * ';
476
477
			if ( $path_action === 'A' ) {
478
				$color_format = 'fg=green';
479
			}
480
			elseif ( $path_action === 'D' ) {
481
				$color_format = 'fg=red';
482
			}
483
			else {
484
				$color_format = in_array($path_data['path'], $merge_conflict_prediction) ? 'error' : '';
485
			}
486
487
			$to_colorize = array($path_action . '    ' . $relative_path);
488
489
			if ( isset($path_data['copyfrom-path']) ) {
490
				// TODO: When copy happened from different ref/project, then relative path = absolute path.
491
				$copy_from_rev = $path_data['copyfrom-rev'];
492
				$copy_from_path = $this->_getRelativeLogPath($path_data, 'copyfrom-path', $path_cut_off_regexp);
493
				$to_colorize[] = '        (from ' . $copy_from_path . ':' . $copy_from_rev . ')';
494
			}
495
496
			if ( $color_format ) {
497
				$details .= '<' . $color_format . '>';
498
				$details .= implode('</>' . PHP_EOL . '<' . $color_format . '>', $to_colorize);
499
				$details .= '</>';
500
			}
501
			else {
502
				$details .= implode(PHP_EOL, $to_colorize);
503
			}
504
		}
505
506
		return $details;
507
	}
508
509
	/**
510
	 * Returns path cut off regexp.
511
	 *
512
	 * @param string $project_path Project path.
513
	 * @param array  $refs         Refs.
514
	 *
515
	 * @return string
516
	 */
517
	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...
518
	{
519
		$ret = array();
520
521
		// Remove ref from path only for single-ref revision.
522
		/*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...
523
			$ret[] = $project_path . reset($refs) . '/';
524
		}*/
525
526
		// Always remove project path.
527
		$ret[] = $project_path;
528
529
		return '#^(' . implode('|', array_map('preg_quote', $ret)) . ')#';
530
	}
531
532
	/**
533
	 * Returns relative path to "svn log" returned path.
534
	 *
535
	 * @param array  $path_data           Path data.
536
	 * @param string $path_key            Path key.
537
	 * @param string $path_cut_off_regexp Path cut off regexp.
538
	 *
539
	 * @return string
540
	 */
541
	private function _getRelativeLogPath(array $path_data, $path_key, $path_cut_off_regexp)
542
	{
543
		$ret = preg_replace($path_cut_off_regexp, '', $path_data[$path_key], 1);
544
545
		if ( $ret === '' ) {
546
			$ret = '.';
547
		}
548
549
		return $ret;
550
	}
551
552
}
553