Failed Conditions
Push — master ( c1c7d6...c5e998 )
by Alexander
03:04
created

RevisionPrinter::withColumn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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