Completed
Push — master ( 5f4795...e3abfa )
by T
05:46
created

SuggestCommand   B

Complexity

Total Complexity 21

Size/Duplication

Total Lines 165
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 17

Test Coverage

Coverage 0%

Importance

Changes 8
Bugs 0 Features 5
Metric Value
wmc 21
c 8
b 0
f 5
lcom 1
cbo 17
dl 0
loc 165
ccs 0
cts 125
cp 0
rs 7.8571

5 Methods

Rating   Name   Duplication   Size   Complexity  
A findLatestTag() 0 4 1
A findTag() 0 8 1
A getMappedVersionTag() 0 13 4
A configure() 0 14 1
F execute() 0 119 14
1
<?php
2
3
namespace PHPSemVerCheckerGit\Console\Command;
4
5
use Gitter\Client;
6
use Gitter\Repository;
7
use PHPSemVerChecker\Analyzer\Analyzer;
8
use PHPSemVerChecker\Finder\Finder;
9
use PHPSemVerChecker\Registry\Registry;
10
use PHPSemVerChecker\Reporter\Reporter;
11
use PHPSemVerChecker\Scanner\Scanner;
12
use PHPSemVerChecker\SemanticVersioning\Level;
13
use PHPSemVerCheckerGit\Filter\SourceFilter;
14
use RuntimeException;
15
use Symfony\Component\Console\Helper\ProgressBar;
16
use Symfony\Component\Console\Input\InputArgument;
17
use Symfony\Component\Console\Input\InputInterface;
18
use Symfony\Component\Console\Input\InputOption;
19
use Symfony\Component\Console\Output\OutputInterface;
20
use vierbergenlars\SemVer\expression as SemanticExpression;
21
use vierbergenlars\SemVer\version as SemanticVersion;
22
23
class SuggestCommand extends BaseCommand
24
{
25
	protected function configure()
26
	{
27
		$this->setName('suggest')->setDescription('Compare a semantic versioned tag against a commit and provide a semantic version suggestion')->setDefinition([
28
			new InputOption('include-before', null,  InputOption::VALUE_OPTIONAL, 'List of paths to include <info>(comma separated)</info>'),
29
			new InputOption('include-after', null, InputOption::VALUE_OPTIONAL, 'List of paths to include <info>(comma separated)</info>'),
30
			new InputOption('exclude-before', null,  InputOption::VALUE_REQUIRED, 'List of paths to exclude <info>(comma separated)</info>'),
31
			new InputOption('exclude-after', null, InputOption::VALUE_REQUIRED, 'List of paths to exclude <info>(comma separated)</info>'),
32
			new InputOption('tag', 't', InputOption::VALUE_REQUIRED, 'A tag to test against (latest by default)'),
33
			new InputOption('against', 'a', InputOption::VALUE_REQUIRED, 'What to test against the tag (HEAD by default)'),
34
			new InputOption('allow-detached', 'd', InputOption::VALUE_NONE, 'Allow suggest to start from a detached HEAD'),
35
			new InputOption('details', null, InputOption::VALUE_NONE, 'Report the changes on which the suggestion is based'),
36
			new InputOption('config', null, InputOption::VALUE_REQUIRED, 'A configuration file to configure php-semver-checker-git'),
37
		]);
38
	}
39
40
	protected function execute(InputInterface $input, OutputInterface $output)
41
	{
42
		$startTime = microtime(true);
43
44
		$targetDirectory = getcwd();
45
		$tag = $this->config->get('tag');
46
		$against = $this->config->get('against') ?: 'HEAD';
47
48
		$includeBefore = $this->config->get('include-before');
49
		$excludeBefore = $this->config->get('exclude-before');
50
51
		$includeAfter = $this->config->get('include-after');
52
		$excludeAfter = $this->config->get('exclude-after');
53
54
		$client = new Client();
55
56
		$repository = $client->getRepository($targetDirectory);
57
58
		if ($tag === null) {
59
			$tag = $this->findLatestTag($repository);
60
		} else {
61
			$tag = $this->findTag($repository, $tag);
62
		}
63
64
		if ($tag === null) {
65
			$output->writeln('<error>No tags to suggest against</error>');
66
			return;
67
		}
68
69
		$output->writeln('<info>Testing ' . $against . ' against tag: ' . $tag . '</info>');
70
71
		$finder = new Finder();
72
		$sourceFilter = new SourceFilter();
73
		$beforeScanner = new Scanner();
74
		$afterScanner = new Scanner();
75
76
		$modifiedFiles = $repository->getModifiedFiles($tag, $against);
77
		$modifiedFiles = array_filter($modifiedFiles, function ($modifiedFile) {
78
			return substr($modifiedFile, -4) === '.php';
79
		});
80
81
		$initialBranch = $repository->getCurrentBranch();
82
83
		if ( ! $this->config->get('allow-detached') && ! $initialBranch) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $initialBranch of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
84
			$output->writeln('<error>You are on a detached HEAD, aborting.</error>');
85
			$output->writeln('<info>If you still wish to run against a detached HEAD, use --allow-detached.</info>');
86
			return -1;
87
		}
88
89
		// Start with the against commit
90
		$repository->checkout($against . ' --');
91
92
		$sourceAfter = $finder->findFromString($targetDirectory, $includeAfter, $excludeAfter);
93
		$sourceAfterMatchedCount = count($sourceAfter);
94
		$sourceAfter = $sourceFilter->filter($sourceAfter, $modifiedFiles);
95
		$progress = new ProgressBar($output, count($sourceAfter));
96
		foreach ($sourceAfter as $file) {
97
			$afterScanner->scan($file);
98
			$progress->advance();
99
		}
100
101
		$progress->clear();
102
103
		// Finish with the tag commit
104
		$repository->checkout($tag . ' --');
105
106
		$sourceBefore = $finder->findFromString($targetDirectory, $includeBefore, $excludeBefore);
107
		$sourceBeforeMatchedCount = count($sourceBefore);
108
		$sourceBefore = $sourceFilter->filter($sourceBefore, $modifiedFiles);
109
		$progress = new ProgressBar($output, count($sourceBefore));
110
		foreach ($sourceBefore as $file) {
111
			$beforeScanner->scan($file);
112
			$progress->advance();
113
		}
114
115
		$progress->clear();
116
117
		// Reset repository to initial branch
118
		if ($initialBranch) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $initialBranch of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
119
			$repository->checkout($initialBranch);
120
		}
121
122
		$registryBefore = $beforeScanner->getRegistry();
123
		$registryAfter = $afterScanner->getRegistry();
124
125
		$analyzer = new Analyzer();
126
		$report = $analyzer->analyze($registryBefore, $registryAfter);
127
128
		$tag = new SemanticVersion($tag);
129
		$newTag = new SemanticVersion($tag);
130
131
		$suggestedLevel = $report->getSuggestedLevel();
132
133
		if ($suggestedLevel !== Level::NONE) {
134
			if ($newTag->getPrerelease()) {
135
				$newTag->inc('prerelease');
136
			} else {
137
				if ($newTag->getMajor() < 1 && $suggestedLevel === Level::MAJOR) {
138
					$newTag->inc('minor');
139
				} else {
140
					$newTag->inc(strtolower(Level::toString($suggestedLevel)));
141
				}
142
			}
143
		}
144
145
		$output->writeln('');
146
		$output->writeln('<info>Initial semantic version: ' . $tag . '</info>');
147
		$output->writeln('<info>Suggested semantic version: ' . $newTag . '</info>');
148
149
		if ($this->config->get('details')) {
150
			$reporter = new Reporter($report);
151
			$reporter->output($output);
152
		}
153
154
		$duration = microtime(true) - $startTime;
155
		$output->writeln('');
156
		$output->writeln('[Scanned files] Before: ' . count($sourceBefore) . ' ('.$sourceBeforeMatchedCount.' unfiltered), After: ' . count($sourceAfter) . ' ('.$sourceAfterMatchedCount.'  unfiltered)');
157
		$output->writeln('Time: ' . round($duration, 3) . ' seconds, Memory: ' . round(memory_get_peak_usage() / 1024 / 1024, 3) . ' MB');
158
	}
159
160
	protected function findLatestTag(Repository $repository)
161
	{
162
		return $this->findTag($repository, '*');
163
	}
164
165
	protected function findTag(Repository $repository, $tag)
166
	{
167
		$tags = (array)$repository->getTags();
168
169
		$tagExpression = new SemanticExpression($tag);
170
171
		return $this->getMappedVersionTag($tags, $tagExpression->maxSatisfying($tags));
172
	}
173
174
	private function getMappedVersionTag(array $tags, $versionTag)
175
	{
176
		foreach ($tags as $tag) {
177
			try {
178
				if (SemanticVersion::eq($versionTag, $tag)) {
179
					return $tag;
180
				}
181
			} catch (RuntimeException $e) {
182
				// Do nothing
183
			}
184
		}
185
		return null;
186
	}
187
}
188