Completed
Push — master ( 930ba5...99d635 )
by Narcotic
29:20
created

GenerateVersionsCommand::getContextVersion()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3.1406

Importance

Changes 0
Metric Value
dl 0
loc 22
ccs 9
cts 12
cp 0.75
rs 9.2
c 0
b 0
f 0
cc 3
eloc 13
nc 3
nop 0
crap 3.1406
1
<?php
2
/**
3
 * Generates the versions.yml file
4
 */
5
6
namespace Graviton\CoreBundle\Command;
7
8
use Symfony\Component\Console\Command\Command;
9
use Symfony\Component\Console\Exception\CommandNotFoundException;
10
use Symfony\Component\Console\Input\InputInterface;
11
use Symfony\Component\Console\Output\OutputInterface;
12
use Symfony\Component\Filesystem\Filesystem;
13
use Symfony\Component\Process\Process;
14
use Symfony\Component\Process\Exception\ProcessFailedException;
15
use Symfony\Component\Yaml\Dumper;
16
use Symfony\Component\Yaml\Parser;
17
use InvalidArgumentException;
18
19
/**
20
 * Reads out the used versions with composer and git and writes them in a file 'versions.yml'
21
 *
22
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
23
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
24
 * @link     http://swisscom.ch
25
 */
26
class GenerateVersionsCommand extends Command
27
{
28
    /**
29
     * @var string
30
     */
31
    private $gitCmd;
32
33
    /**
34
     * @var string
35
     */
36
    private $rootDir;
37
38
    /**
39
     * @var string
40
     */
41
    private $contextDir;
42
43
    /**
44
     * @var OutputInterface
45
     */
46
    private $output;
47
48
    /**
49
     * @var Filesystem
50
     */
51
    private $filesystem;
52
53
    /**
54
     * @var Dumper
55
     */
56
    private $dumper;
57
58
    /**
59
     * @var Parser
60
     */
61
    private $parser;
62
63
    /**
64
     * {@inheritDoc}
65
     *
66
     * @return void
67
     */
68 4
    protected function configure()
69
    {
70 4
        parent::configure();
71
72 4
        $this->setName('graviton:core:generateversions')
73 4
            ->setDescription(
74 4
                'Generates the versions.yml file according to definition in app/config/version_service.yml'
75
            );
76 4
    }
77
78
    /**
79
     * GenerateVersionsCommand constructor.
80
     *
81
     * @param Filesystem $filesystem Sf File and directory runner
82
     * @param Dumper     $dumper     Sf YAML to string
83
     * @param Parser     $parser     Sf YAML reader
84
     * @param String     $gitCmd     Git command regex
85
     * @param String     $rootDir    Kernel Root Directory
86
     */
87 4
    public function __construct(
88
        Filesystem  $filesystem,
89
        Dumper $dumper,
90
        Parser $parser,
91
        $gitCmd,
92
        $rootDir
93
    ) {
94 4
        $this->filesystem = $filesystem;
95 4
        $this->dumper = $dumper;
96 4
        $this->parser = $parser;
97 4
        $this->gitCmd = $gitCmd;
98 4
        $this->rootDir = $rootDir;
99 4
        parent::__construct();
100 4
    }
101
102
    /**
103
     * {@inheritDoc}
104
     *
105
     * @param InputInterface  $input  input
106
     * @param OutputInterface $output output
107
     *
108
     * @return void
109
     */
110 2
    protected function execute(InputInterface $input, OutputInterface $output)
111
    {
112 2
        $this->contextDir = $this->rootDir . ((strpos($this->rootDir, 'vendor'))? '/../../../../' : '/../');
113
114 2
        $this->output = $output;
115
116 2
        $this->filesystem->dumpFile(
117 2
            $this->rootDir . '/../versions.yml',
118 2
            $this->getPackageVersions()
119
        );
120 2
    }
121
122
    /**
123
     * gets all versions
124
     *
125
     * @return string version numbers of packages
126
     */
127 2
    public function getPackageVersions()
128
    {
129 2
        if ($this->isDesiredVersion('self')) {
130
            $versions = [
131 2
                $this->getContextVersion(),
132
            ];
133
        } else {
134
            $versions = array();
135
        }
136 2
        $versions = $this->getInstalledPackagesVersion($versions);
137
138 2
        return $this->dumper->dump($versions);
139
    }
140
141
    /**
142
     * returns the version of graviton or wrapper (the context) using git
143
     *
144
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,string>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
145
     *
146
     * @throws CommandNotFoundException
147
     */
148 2
    private function getContextVersion()
149
    {
150 2
        $wrapper = [];
151
        // git available here?
152 2
        if ($this->commandAvailable($this->gitCmd)) {
153
            // get current commit hash
154 2
            $currentHash = trim($this->runCommandInContext($this->gitCmd.' rev-parse --short HEAD'));
155
            // get version from hash:
156 2
            $version = trim($this->runCommandInContext($this->gitCmd.' tag --points-at ' . $currentHash));
157
            // if empty, set dev- and current branchname to version:
158 2
            if (!strlen($version)) {
159
                $version = 'dev-' . trim($this->runCommandInContext($this->gitCmd.' rev-parse --abbrev-ref HEAD'));
160
            }
161 2
            $wrapper['id'] = 'self';
162 2
            $wrapper['version'] = $version;
163
        } else {
164
            throw new CommandNotFoundException(
165
                'getContextVersion: '. $this->gitCmd . ' not available in ' . $this->contextDir
166
            );
167
        }
168 2
        return $wrapper;
169
    }
170
171
    /**
172
     * returns version for every installed package
173
     *
174
     * @param array $versions versions array
175
     * @throws CommandNotFoundException | \RuntimeException
176
     * @return array
177
     */
178 2
    private function getInstalledPackagesVersion($versions)
179
    {
180 2
        $composerFile = $this->contextDir . 'composer.lock';
181 2
        if (!file_exists($composerFile)) {
182
            throw new CommandNotFoundException('Composer lock file not found: '.$composerFile);
183
        }
184
185 2
        $composerJson = json_decode(file_get_contents($composerFile));
186 2
        if (!$composerJson || json_last_error()) {
187
            throw new \RuntimeException('Error on parsing json file: '.$composerFile);
188
        }
189
190 2
        foreach ($composerJson->packages as $package) {
191 2
            if ($this->isDesiredVersion($package->name)) {
192 2
                array_push($versions, array('id' => $package->name, 'version' => $package->version));
193
            }
194
        }
195
196 2
        return $versions;
197
    }
198
199
    /**
200
     * runs a command depending on the context
201
     *
202
     * @param string $command in this case composer or git
203
     * @return string
204
     *
205
     * @throws \RuntimeException
206
     */
207 2
    private function runCommandInContext($command)
208
    {
209 2
        $process = new Process(
210 2
            'cd ' . escapeshellarg($this->contextDir)
211 2
            . ' && ' . escapeshellcmd($command)
212
        );
213
        try {
214 2
            $process->mustRun();
215
        } catch (ProcessFailedException $pFe) {
216
            $this->output->writeln($pFe->getMessage());
217
        }
218 2
        return $process->getOutput();
219
    }
220
221
    /**
222
     * Checks if a command is available in an enviroment and in the context. The command might be as well a path
223
     * to a command.
224
     *
225
     * @param String $command the command to be checked for availability
226
     * @return bool
227
     */
228 2
    private function commandAvailable($command)
229
    {
230 2
        $process = new Process(
231 2
            'cd ' . escapeshellarg($this->contextDir)
232 2
            . ' && which ' . escapeshellcmd($command)
233
        );
234 2
        $process->run();
235 2
        return (boolean) strlen(trim($process->getOutput()));
236
    }
237
238
    /**
239
     * checks if the package version is configured
240
     *
241
     * @param string $packageName package name
242
     * @return boolean
243
     *
244
     * @throws \RuntimeException
245
     */
246 2
    private function isDesiredVersion($packageName)
247
    {
248 2
        if (empty($packageName)) {
249
            throw new \RuntimeException('Missing package name');
250
        }
251
252 2
        $config = $this->getConfiguration($this->contextDir . "/app/config/version_service.yml");
253
254 2
        if (!empty($config['desiredVersions'])) {
255 2
            foreach ($config['desiredVersions'] as $confEntry) {
256 2
                if ($confEntry == $packageName) {
257 2
                    return true;
258
                }
259
            }
260
        }
261
262 2
        return false;
263
    }
264
265
    /**
266
     * reads configuration information from the given file into an array.
267
     *
268
     * @param string $filePath Absolute path to the configuration file.
269
     *
270
     * @return array
271
     */
272 2
    private function getConfiguration($filePath)
273
    {
274 2
        $config = $this->parser->parse(file_get_contents($filePath));
275
276 2
        return is_array($config) ? $config : [];
277
    }
278
279
    /**
280
     * Returns the version out of a given version string
281
     *
282
     * @param string $versionString SemVer version string
283
     * @return string
284
     */
285
    public function getVersionNumber($versionString)
286
    {
287
        try {
288
            $version = $this->getVersionOrBranchName($versionString);
289
        } catch (InvalidArgumentException $e) {
290
            $version = $this->normalizeVersionString($versionString);
291
        }
292
293
        return empty($version) ? $versionString : $version;
294
    }
295
296
    /**
297
     * Get a version string string using a regular expression
298
     *
299
     * @param string $versionString SemVer version string
300
     * @return string
301
     */
302
    private function getVersionOrBranchName($versionString)
303
    {
304
        // Regular expression for root package ('self') on a tagged version
305
        $tag = '^(?<version>[v]?[0-9]+\.[0-9]+\.[0-9]+)(?<prerelease>-[0-9a-zA-Z.]+)?(?<build>\+[0-9a-zA-Z.]+)?$';
306
        // Regular expression for root package on a git branch
307
        $branch = '^(?<branch>(dev\-){1}[0-9a-zA-Z\.\/\-\_]+)$';
308
        $regex = sprintf('/%s|%s/', $tag, $branch);
309
310
        $matches = [];
311
        if (0 === preg_match($regex, $versionString, $matches)) {
312
            throw new InvalidArgumentException(
313
                sprintf('"%s" is not a valid SemVer', $versionString)
314
            );
315
        }
316
317
        return empty($matches['version']) ? $matches['branch'] : $matches['version'];
318
    }
319
320
    /**
321
     * Normalizing the incorrect SemVer string to a valid one
322
     *
323
     * At the moment, we are getting the version of the root package ('self') using the
324
     * 'composer show -s'-command. Unfortunately Composer is adding an unnecessary ending.
325
     *
326
     * @param string $versionString SemVer  version string
327
     * @param string $prefix        Version prefix
328
     * @return string
329
     */
330
    private function normalizeVersionString($versionString, $prefix = 'v')
331
    {
332
        if (substr_count($versionString, '.') === 3) {
333
            return sprintf(
334
                '%s%s',
335
                $prefix,
336
                implode('.', explode('.', $versionString, -1))
337
            );
338
        }
339
        return $versionString;
340
    }
341
}
342