SuggestCommand::getRepository()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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