Completed
Push — feature/EVO-7007-selfversion-w... ( 736f9d...75c615 )
by
unknown
63:48
created

GenerateVersionsCommand::runGitInContext()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 10
Ratio 100 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 10
loc 10
rs 9.4285
cc 2
eloc 7
nc 2
nop 1
1
<?php
2
/**
3
 * cleans dynamic bundle directory
4
 */
5
6
namespace Graviton\CoreBundle\Command;
7
8
use Symfony\Component\Console\Command\Command;
9
use Symfony\Component\Console\Input\InputInterface;
10
use Symfony\Component\Console\Output\OutputInterface;
11
use Symfony\Component\Filesystem\Filesystem;
12
use Symfony\Component\Process\Process;
13
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 $composerCmd;
32
33
    /**
34
     * @var string
35
     */
36
    private $contextDir;
37
38
    /*
39
     * @var \Symfony\Component\Console\Output\OutputInterface
40
     */
41
    private $output;
42
43
    /**
44
     * {@inheritDoc}
45
     *
46
     * @return void
47
     */
48
    protected function configure()
49
    {
50
        parent::configure();
51
52
        $this->setName('graviton:core:generateversions')
53
            ->setDescription(
54
                'Generates the versions.yml file according to definition in app/config/version_service.yml'
55
            );
56
    }
57
58
    /**
59
     * {@inheritDoc}
60
     *
61
     * @param InputInterface $input input
62
     * @param OutputInterface $output output
63
     *
64
     * @return void
65
     */
66
    protected function execute(InputInterface $input, OutputInterface $output)
67
    {
68
        $container = $this->getApplication()->getKernel()->getContainer();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Symfony\Component\Console\Application as the method getKernel() does only exist in the following sub-classes of Symfony\Component\Console\Application: Symfony\Bundle\FrameworkBundle\Console\Application. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
69
        $rootDir = $container->getParameter('kernel.root_dir');
70
        $this->composerCmd = $container->getParameter('graviton.composer.cmd');
71
72
        $this->contextDir = $rootDir . '/../';
73
        if (strpos($rootDir, 'vendor')) {
74
            $this->contextDir = $rootDir . '/../../../../';
75
        }
76
77
        $this->output = $output;
78
79
        $filesystem = new Filesystem();
80
        $filesystem->dumpFile(
81
            $rootDir . '/../versions.yml',
82
            $this->getPackageVersions()
83
        );
84
    }
85
86
    /**
87
     * gets all versions
88
     *
89
     * @return array version numbers of packages
0 ignored issues
show
Documentation introduced by
Should the return type not be string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
90
     */
91
    public function getPackageVersions()
92
    {
93
        if ($this->isDesiredVersion('self')) {
94
            $versions = [
95
                $this->getContextVersion(),
96
            ];
97
        } else {
98
            $versions = array();
99
        }
100
        $versions = $this->getInstalledPackagesVersion($versions);
101
102
        $yamlDumper = new Dumper();
103
        return $yamlDumper->dump($versions);
104
    }
105
106
    /**
107
     * returns the version of graviton or wrapper using git
108
     *
109
     * @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...
110
     */
111
    private function getContextVersion()
112
    {
113
        // get current commit hash
114
        $currentHash = trim($this->runGitInContext('rev-parse --short HEAD'));
115
        // get version from hash:
116
        $version = trim($this->runGitInContext('tag --points-at ' . $currentHash));
117
        // if empty, set dev- and current branchname to version:
118
        if (!strlen($version)) {
119
            $version = 'dev-' . trim($this->runGitInContext('rev-parse --abbrev-ref HEAD'));
120
        }
121
122
        $wrapper['id'] = 'self';
123
        $wrapper['version'] = $version;
124
125
        return $wrapper;
126
    }
127
128
    /**
129
     * returns version for every installed package
130
     *
131
     * @param array $versions versions array
132
     * @return array
133
     */
134
    private function getInstalledPackagesVersion($versions)
135
    {
136
        $output = $this->runComposerInContext('show --installed');
137
        $packages = explode(PHP_EOL, $output);
138
        //last index is always empty
139
        array_pop($packages);
140
141
        foreach ($packages as $package) {
142
            $content = preg_split('/([\s]+)/', $package);
143
            if ($this->isDesiredVersion($content[0])) {
144
                array_push($versions, array('id' => $content[0], 'version' => $content[1]));
145
            }
146
        }
147
148
        return $versions;
149
    }
150
151
    /**
152
     * runs a composer command depending on the context
153
     *
154
     * @param string $command composer args
155
     * @return string
156
     *
157
     * @throws \RuntimeException
158
     */
159 View Code Duplication
    private function runComposerInContext($command)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
160
    {
161
        $process = new Process(
162
            'cd ' . escapeshellarg($this->contextDir)
163
            . ' && ' . escapeshellcmd($this->composerCmd)
164
            . ' ' . $command
165
        );
166
        try {
167
            $process->mustRun();
168
        } catch (ProcessFailedException $pFe) {
169
            $this->output->writeln($pFe->getMessage());
170
        }
171
        return $process->getOutput();
172
    }
173
174
    /**
175
     * runs a git command depending on the context
176
     *
177
     * @param string $command git args
178
     * @return string
179
     *
180
     * @throws \RuntimeException
181
     */
182 View Code Duplication
    private function runGitInContext($command)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
183
    {
184
        $process = new Process('cd ' . escapeshellarg($this->contextDir) . ' && git ' . $command);
185
        try {
186
            $process->mustRun();
187
        } catch (ProcessFailedException $pFe) {
188
            $this->output->writeln($pFe->getMessage());
189
        }
190
        return $process->getOutput();
191
    }
192
193
    /**
194
     * checks if the package version is configured
195
     *
196
     * @param string $packageName package name
197
     * @return boolean
198
     *
199
     * @throws \RuntimeException
200
     */
201
    private function isDesiredVersion($packageName)
202
    {
203
        if (empty($packageName)) {
204
            throw new \RuntimeException('Missing package name');
205
        }
206
207
        $config = $this->getConfiguration($this->contextDir . "/app/config/version_service.yml");
208
209
        if (!empty($config['desiredVersions'])) {
210
            foreach ($config['desiredVersions'] as $confEntry) {
211
                if ($confEntry == $packageName) {
212
                    return true;
213
                }
214
            }
215
        }
216
217
        return false;
218
    }
219
220
    /**
221
     * reads configuration information from the given file into an array.
222
     *
223
     * @param string $filePath Absolute path to the configuration file.
224
     *
225
     * @return array
226
     */
227
    private function getConfiguration($filePath)
228
    {
229
        $parser = new Parser();
230
        $config = $parser->parse(file_get_contents($filePath));
231
232
        return is_array($config) ? $config : [];
233
    }
234
235
    /**
236
     * Returns the version out of a given version string
237
     *
238
     * @param string $versionString SemVer version string
239
     * @return string
240
     */
241
    public function getVersionNumber($versionString)
242
    {
243
        try {
244
            $version = $this->getVersionOrBranchName($versionString);
245
        } catch (InvalidArgumentException $e) {
246
            $version = $this->normalizeVersionString($versionString);
247
        }
248
249
        return empty($version) ? $versionString : $version;
250
    }
251
252
    /**
253
     * Get a version string string using a regular expression
254
     *
255
     * @param string $versionString SemVer version string
256
     * @return string
257
     */
258
    private function getVersionOrBranchName($versionString)
259
    {
260
        // Regular expression for root package ('self') on a tagged version
261
        $tag = '^(?<version>[v]?[0-9]+\.[0-9]+\.[0-9]+)(?<prerelease>-[0-9a-zA-Z.]+)?(?<build>\+[0-9a-zA-Z.]+)?$';
262
        // Regular expression for root package on a git branch
263
        $branch = '^(?<branch>(dev\-){1}[0-9a-zA-Z\.\/\-\_]+)$';
264
        $regex = sprintf('/%s|%s/', $tag, $branch);
265
266
        $matches = [];
267
        if (0 === preg_match($regex, $versionString, $matches)) {
268
            throw new InvalidArgumentException(
269
                sprintf('"%s" is not a valid SemVer', $versionString)
270
            );
271
        }
272
273
        return empty($matches['version']) ? $matches['branch'] : $matches['version'];
274
    }
275
276
    /**
277
     * Normalizing the incorrect SemVer string to a valid one
278
     *
279
     * At the moment, we are getting the version of the root package ('self') using the
280
     * 'composer show -s'-command. Unfortunately Composer is adding an unnecessary ending.
281
     *
282
     * @param string $versionString SemVer version string
283
     * @param string $prefix Version prefix
284
     * @return string
285
     */
286
    private function normalizeVersionString($versionString, $prefix = 'v')
287
    {
288
        if (substr_count($versionString, '.') === 3) {
289
            return sprintf(
290
                '%s%s',
291
                $prefix,
292
                implode('.', explode('.', $versionString, -1))
293
            );
294
        }
295
        return $versionString;
296
    }
297
}
298