Completed
Pull Request — master (#27)
by
unknown
01:33
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\Finder\Finder;
9
use PHPSemVerChecker\Reporter\Reporter;
10
use PHPSemVerChecker\SemanticVersioning\Level;
11
use PHPSemVerCheckerGit\Filter\SourceFilter;
12
use PHPSemVerCheckerGit\SourceFileProcessor;
13
use RuntimeException;
14
use Symfony\Component\Console\Input\InputInterface;
15
use Symfony\Component\Console\Input\InputOption;
16
use Symfony\Component\Console\Output\OutputInterface;
17
use vierbergenlars\SemVer\expression as SemanticExpression;
18
use vierbergenlars\SemVer\SemVerException as SemanticVersionException;
19
use vierbergenlars\SemVer\version as SemanticVersion;
20
use PHPSemVerChecker\Report\Report;
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 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
		    array(
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
		    array(
129
		        '',
130
                '[Scanned files] Before: ' . count($before->getFiles()) . ' (' . $before->getOriginalAmount() . ' unfiltered), After: ' . count($after->getFiles()) . ' (' . $after->getOriginalAmount() . '  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 Report $report
140
     * @param SemanticVersion $tag
141
     * @return SemanticVersion
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 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
	private function filterTags(array $tags)
207
	{
208
		$filteredTags = [];
209
		foreach ($tags as $tag) {
210
			try {
211
				new SemanticVersion($tag);
212
				$filteredTags[] = $tag;
213
			} catch (SemanticVersionException $e) {
214
				// Do nothing
215
			}
216
		}
217
		return $filteredTags;
218
	}
219
220
	/**
221
	 * @param string[]                                   $tags
222
	 * @param \vierbergenlars\SemVer\version|string|null $versionTag
223
	 * @return string|null
224
	 */
225
	private function getMappedVersionTag(array $tags, $versionTag)
226
	{
227
		foreach ($tags as $tag) {
228
			try {
229
				if (SemanticVersion::eq($versionTag, $tag)) {
0 ignored issues
show
Bug introduced by
It seems like $versionTag defined by parameter $versionTag on line 225 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...
230
					return $tag;
231
				}
232
			} catch (RuntimeException $e) {
233
				// Do nothing
234
			}
235
		}
236
		return null;
237
	}
238
}
239