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) { |
|
|
|
|
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) { |
|
|
|
|
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)) { |
|
|
|
|
221
|
|
|
return $tag; |
222
|
|
|
} |
223
|
|
|
} catch (RuntimeException $e) { |
224
|
|
|
// Do nothing |
225
|
|
|
} |
226
|
|
|
} |
227
|
|
|
return null; |
228
|
|
|
} |
229
|
|
|
} |
230
|
|
|
|
In PHP, under loose comparison (like
==
, or!=
, orswitch
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: