Completed
Pull Request — master (#27)
by Björn
01:22
created

SuggestCommand::getRepository()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

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 9.4285
c 0
b 0
f 0
cc 1
eloc 3
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\Console\Command\BaseCommand;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, PHPSemVerCheckerGit\Console\Command\BaseCommand.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

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