Completed
Push — master ( 3d2f3e...de49bb )
by T
01:54
created

SuggestCommand::filterTags()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 0
cts 12
cp 0
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 4
nop 1
crap 12
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\Reporter\Reporter;
10
use PHPSemVerChecker\Scanner\Scanner;
11
use PHPSemVerChecker\SemanticVersioning\Level;
12
use PHPSemVerCheckerGit\Filter\SourceFilter;
13
use RuntimeException;
14
use Symfony\Component\Console\Helper\ProgressBar;
15
use Symfony\Component\Console\Input\InputInterface;
16
use Symfony\Component\Console\Input\InputOption;
17
use Symfony\Component\Console\Output\OutputInterface;
18
use vierbergenlars\SemVer\expression as SemanticExpression;
19
use vierbergenlars\SemVer\SemVerException as SemanticVersionException;
20
use vierbergenlars\SemVer\version as SemanticVersion;
21
22
class SuggestCommand extends BaseCommand
23
{
24
	/**
25
	 * @return void
26
	 */
27
	protected function configure()
28
	{
29
		$this->setName('suggest')->setDescription('Compare a semantic versioned tag against a commit and provide a semantic version suggestion')->setDefinition([
30
			new InputOption('include-before', null, InputOption::VALUE_REQUIRED, 'List of paths to include <info>(comma separated)</info>'),
31
			new InputOption('include-after', null, InputOption::VALUE_REQUIRED, 'List of paths to include <info>(comma separated)</info>'),
32
			new InputOption('exclude-before', null, InputOption::VALUE_REQUIRED, 'List of paths to exclude <info>(comma separated)</info>'),
33
			new InputOption('exclude-after', null, InputOption::VALUE_REQUIRED, 'List of paths to exclude <info>(comma separated)</info>'),
34
			new InputOption('tag', 't', InputOption::VALUE_REQUIRED, 'A tag to test against (latest by default)'),
35
			new InputOption('against', 'a', InputOption::VALUE_REQUIRED, 'What to test against the tag (HEAD by default)'),
36
			new InputOption('allow-detached', 'd', InputOption::VALUE_NONE, 'Allow suggest to start from a detached HEAD'),
37
			new InputOption('details', null, InputOption::VALUE_NONE, 'Report the changes on which the suggestion is based'),
38
			new InputOption('config', null, InputOption::VALUE_REQUIRED, 'A configuration file to configure php-semver-checker-git'),
39
		]);
40
	}
41
42
	/**
43
	 * @param \Symfony\Component\Console\Input\InputInterface   $input
44
	 * @param \Symfony\Component\Console\Output\OutputInterface $output
45
	 */
46
	protected function execute(InputInterface $input, OutputInterface $output)
47
	{
48
		$startTime = microtime(true);
49
50
		$targetDirectory = getcwd();
51
		$tag = $this->config->get('tag');
52
		$against = $this->config->get('against') ?: 'HEAD';
53
54
		$includeBefore = $this->config->get('include-before');
55
		$excludeBefore = $this->config->get('exclude-before');
56
57
		$includeAfter = $this->config->get('include-after');
58
		$excludeAfter = $this->config->get('exclude-after');
59
60
		$client = new Client();
61
62
		$repository = $client->getRepository($targetDirectory);
63
64
		if ($tag === null) {
65
			$tag = $this->findLatestTag($repository);
66
		} else {
67
			$tag = $this->findTag($repository, $tag);
68
		}
69
70
		if ($tag === null) {
71
			$output->writeln('<error>No tags to suggest against</error>');
72
			return;
73
		}
74
75
		$output->writeln('<info>Testing ' . $against . ' against tag: ' . $tag . '</info>');
76
77
		$finder = new Finder();
78
		$sourceFilter = new SourceFilter();
79
		$beforeScanner = new Scanner();
80
		$afterScanner = new Scanner();
81
82
		$modifiedFiles = $repository->getModifiedFiles($tag, $against);
83
		$modifiedFiles = array_filter($modifiedFiles, function ($modifiedFile) {
84
			return substr($modifiedFile, -4) === '.php';
85
		});
86
87
		$initialBranch = $repository->getCurrentBranch();
88
89
		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...
90
			$output->writeln('<error>You are on a detached HEAD, aborting.</error>');
91
			$output->writeln('<info>If you still wish to run against a detached HEAD, use --allow-detached.</info>');
92
			return -1;
93
		}
94
95
		// Start with the against commit
96
		$repository->checkout($against . ' --');
97
98
		$sourceAfter = $finder->findFromString($targetDirectory, $includeAfter, $excludeAfter);
99
		$sourceAfterMatchedCount = count($sourceAfter);
100
		$sourceAfter = $sourceFilter->filter($sourceAfter, $modifiedFiles);
101
		$progress = new ProgressBar($output, count($sourceAfter));
102
		foreach ($sourceAfter as $file) {
103
			$afterScanner->scan($file);
104
			$progress->advance();
105
		}
106
107
		$progress->clear();
108
109
		// Finish with the tag commit
110
		$repository->checkout($tag . ' --');
111
112
		$sourceBefore = $finder->findFromString($targetDirectory, $includeBefore, $excludeBefore);
113
		$sourceBeforeMatchedCount = count($sourceBefore);
114
		$sourceBefore = $sourceFilter->filter($sourceBefore, $modifiedFiles);
115
		$progress = new ProgressBar($output, count($sourceBefore));
116
		foreach ($sourceBefore as $file) {
117
			$beforeScanner->scan($file);
118
			$progress->advance();
119
		}
120
121
		$progress->clear();
122
123
		// Reset repository to initial branch
124
		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...
125
			$repository->checkout($initialBranch);
126
		}
127
128
		$registryBefore = $beforeScanner->getRegistry();
129
		$registryAfter = $afterScanner->getRegistry();
130
131
		$analyzer = new Analyzer();
132
		$report = $analyzer->analyze($registryBefore, $registryAfter);
133
134
		$tag = new SemanticVersion($tag);
135
		$newTag = new SemanticVersion($tag);
136
137
		$suggestedLevel = $report->getSuggestedLevel();
138
139
		if ($suggestedLevel !== Level::NONE) {
140
			if ($newTag->getPrerelease()) {
141
				$newTag->inc('prerelease');
142
			} else {
143
				if ($newTag->getMajor() < 1 && $suggestedLevel === Level::MAJOR) {
144
					$newTag->inc('minor');
145
				} else {
146
					$newTag->inc(strtolower(Level::toString($suggestedLevel)));
147
				}
148
			}
149
		}
150
151
		$output->writeln('');
152
		$output->writeln('<info>Initial semantic version: ' . $tag . '</info>');
153
		$output->writeln('<info>Suggested semantic version: ' . $newTag . '</info>');
154
155
		if ($this->config->get('details')) {
156
			$reporter = new Reporter($report);
157
			$reporter->output($output);
158
		}
159
160
		$duration = microtime(true) - $startTime;
161
		$output->writeln('');
162
		$output->writeln('[Scanned files] Before: ' . count($sourceBefore) . ' (' . $sourceBeforeMatchedCount . ' unfiltered), After: ' . count($sourceAfter) . ' (' . $sourceAfterMatchedCount . '  unfiltered)');
163
		$output->writeln('Time: ' . round($duration, 3) . ' seconds, Memory: ' . round(memory_get_peak_usage() / 1024 / 1024, 3) . ' MB');
164
	}
165
166
	/**
167
	 * @param \Gitter\Repository $repository
168
	 * @return string|null
169
	 */
170
	protected function findLatestTag(Repository $repository)
171
	{
172
		return $this->findTag($repository, '*');
173
	}
174
175
	/**
176
	 * @param \Gitter\Repository $repository
177
	 * @param string             $tag
178
	 * @return string|null
179
	 */
180
	protected function findTag(Repository $repository, $tag)
181
	{
182
		$tags = (array)$repository->getTags();
183
		$tags = $this->filterTags($tags);
184
185
		$tagExpression = new SemanticExpression($tag);
186
187
		try {
188
			// Throws an exception if it cannot find a matching version
189
			$satisfyingTag = $tagExpression->maxSatisfying($tags);
190
		} catch (SemanticVersionException $e) {
191
			return null;
192
		}
193
194
		return $this->getMappedVersionTag($tags, $satisfyingTag);
195
	}
196
197
	private function filterTags(array $tags)
198
	{
199
		$filteredTags = [];
200
		foreach ($tags as $tag) {
201
			try {
202
				new SemanticVersion($tag);
203
				$filteredTags[] = $tag;
204
			} catch (SemanticVersionException $e) {
205
				// Do nothing
206
			}
207
		}
208
		return $filteredTags;
209
	}
210
211
	/**
212
	 * @param string[]                                   $tags
213
	 * @param \vierbergenlars\SemVer\version|string|null $versionTag
214
	 * @return string|null
215
	 */
216
	private function getMappedVersionTag(array $tags, $versionTag)
217
	{
218
		foreach ($tags as $tag) {
219
			try {
220
				if (SemanticVersion::eq($versionTag, $tag)) {
0 ignored issues
show
Bug introduced by
It seems like $versionTag defined by parameter $versionTag on line 216 can also be of type null; however, vierbergenlars\SemVer\version::eq() does only seem to accept string|object<vierbergenlars\SemVer\version>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
221
					return $tag;
222
				}
223
			} catch (RuntimeException $e) {
224
				// Do nothing
225
			}
226
		}
227
		return null;
228
	}
229
}
230