Passed
Pull Request — master (#71)
by Cees-Jan
02:05
created

GenerateChangelogCommand::loadConfigFile()   A

Complexity

Conditions 6
Paths 7

Size

Total Lines 32
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 6

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
eloc 15
nc 7
nop 1
dl 0
loc 32
ccs 15
cts 15
cp 1
crap 6
rs 9.2222
c 2
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ChangelogGenerator\Command;
6
7
use ChangelogGenerator\ChangelogConfig;
8
use ChangelogGenerator\ChangelogGenerator;
9
use InvalidArgumentException;
10
use RuntimeException;
11
use Symfony\Component\Console\Command\Command;
12
use Symfony\Component\Console\Input\InputInterface;
13
use Symfony\Component\Console\Input\InputOption;
14
use Symfony\Component\Console\Output\BufferedOutput;
15
use Symfony\Component\Console\Output\OutputInterface;
16
use Symfony\Component\Console\Output\StreamOutput;
17
18
use function assert;
19
use function count;
20
use function current;
21
use function file_exists;
22
use function file_get_contents;
23
use function file_put_contents;
24
use function fopen;
25
use function getcwd;
26
use function gettype;
27
use function in_array;
28
use function is_array;
29
use function is_string;
30
use function sprintf;
31
use function touch;
32
33
class GenerateChangelogCommand extends Command
34
{
35
    public const WRITE_STRATEGY_REPLACE = 'replace';
36
    public const WRITE_STRATEGY_APPEND  = 'append';
37
    public const WRITE_STRATEGY_PREPEND = 'prepend';
38
39
    private ChangelogGenerator $changelogGenerator;
40
41 22
    public function __construct(ChangelogGenerator $changelogGenerator)
42
    {
43 22
        $this->changelogGenerator = $changelogGenerator;
44
45 22
        parent::__construct();
46 22
    }
47
48 22
    protected function configure(): void
49
    {
50
        $this
51 22
            ->setName('generate')
52 22
            ->setDescription('Generate a changelog markdown document from a GitHub milestone.')
53 22
            ->setHelp(<<<EOT
54 22
The <info>%command.name%</info> command generates a changelog markdown document from a GitHub milestone:
55
56
    <info>%command.full_name% --user=doctrine --repository=migrations --milestone=2.0</info>
57
58
You can filter the changelog by label names using the --label option:
59
60
    <info>%command.full_name% --user=doctrine --repository=migrations --milestone=2.0 --label=Enhancement --label=Bug</info>
61
EOT
62
            )
63 22
            ->addOption(
64 22
                'user',
65 22
                null,
66 22
                InputOption::VALUE_REQUIRED,
67 22
                'User that owns the repository.'
68
            )
69 22
            ->addOption(
70 22
                'repository',
71 22
                null,
72 22
                InputOption::VALUE_REQUIRED,
73 22
                'The repository owned by the user.'
74
            )
75 22
            ->addOption(
76 22
                'milestone',
77 22
                null,
78 22
                InputOption::VALUE_REQUIRED,
79 22
                'The milestone to build the changelog for.'
80
            )
81 22
            ->addOption(
82 22
                'file',
83 22
                null,
84 22
                InputOption::VALUE_OPTIONAL,
85 22
                'Write the changelog to a file.',
86 22
                false
87
            )
88 22
            ->addOption(
89 22
                'append',
90 22
                null,
91 22
                InputOption::VALUE_NONE,
92 22
                'Append the changelog to the file.'
93
            )
94 22
            ->addOption(
95 22
                'prepend',
96 22
                null,
97 22
                InputOption::VALUE_NONE,
98 22
                'Prepend the changelog to the file.'
99
            )
100 22
            ->addOption(
101 22
                'label',
102 22
                null,
103 22
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
104 22
                'The labels to generate a changelog for.'
105
            )
106 22
            ->addOption(
107 22
                'non-grouped-label',
108 22
                null,
109 22
                InputOption::VALUE_OPTIONAL,
110 22
                'Provide a label for any item that isn\'t grouped under any other label.'
111
            )
112 22
            ->addOption(
113 22
                'config',
114 22
                'c',
115 22
                InputOption::VALUE_REQUIRED,
116 22
                'The path to a configuration file.'
117
            )
118 22
            ->addOption(
119 22
                'project',
120 22
                'p',
121 22
                InputOption::VALUE_REQUIRED,
122 22
                'The project from the configuration to generate a changelog for.'
123
            )
124 22
            ->addOption(
125 22
                'include-open',
126 22
                'a',
127 22
                InputOption::VALUE_OPTIONAL,
128 22
                'Whether to also include open issues.',
129 22
                ''
130
            )
131 22
            ->addOption(
132 22
                'show-contributors',
133 22
                '',
134 22
                InputOption::VALUE_OPTIONAL,
135 22
                'Whether to include a section with a list of contributors.',
136 22
                ''
137
            );
138 22
    }
139
140 20
    protected function execute(InputInterface $input, OutputInterface $output): int
141
    {
142 20
        $changelogConfig = $this->getChangelogConfig($input);
143
144 17
        if (! $changelogConfig->isValid()) {
145 1
            throw new InvalidArgumentException('You must pass a config file with the --config option or manually specify the --user --repository and --milestone options.');
146
        }
147
148 16
        $changelogOutput = $this->getChangelogOutput($input, $output);
149
150 16
        $this->changelogGenerator->generate(
151 16
            $changelogConfig,
152
            $changelogOutput
153
        );
154
155 16
        if ($this->getFileWriteStrategy($input) !== self::WRITE_STRATEGY_PREPEND) {
156 13
            return 0;
157
        }
158
159 3
        $file = $input->getOption('file');
160
161 3
        if ($file === null) {
162
            $file = $this->getChangelogFilePath();
163
        }
164
165 3
        if (! ($changelogOutput instanceof BufferedOutput)) {
166 1
            return 0;
167
        }
168
169
        assert(is_string($file));
170
171 2
        if (! file_exists($file)) {
172 1
            touch($file);
173
        }
174
175 2
        file_put_contents($file, $changelogOutput->fetch() . file_get_contents($file));
176
177 2
        return 0;
178
    }
179
180 20
    private function getChangelogConfig(InputInterface $input): ChangelogConfig
181
    {
182 20
        $changelogConfig = $this->loadConfigFile($input);
183
184 17
        if ($changelogConfig !== null) {
185 6
            return $changelogConfig;
186
        }
187
188 11
        $changelogConfig = (new ChangelogConfig());
189
190 11
        $this->loadChangelogConfigFromInput($input, $changelogConfig);
191
192 11
        return $changelogConfig;
193
    }
194
195
    /**
196
     * @throws RuntimeException
197
     */
198 16
    private function getStringOption(InputInterface $input, string $name): string
199
    {
200 16
        $value = $input->getOption($name);
201
202 16
        if ($value === null) {
203
            return '';
204
        }
205
206 16
        if (is_string($value)) {
207 16
            return $value;
208
        }
209
210
        throw new RuntimeException(sprintf('Invalid option value type: %s', gettype($value)));
211
    }
212
213 4
    private function getBooleanOption(InputInterface $input, string $name): bool
214
    {
215 4
        $value = $input->getOption($name);
216
217
        // option not provided, default to false
218 4
        if ($value === '') {
219
            return false;
220
        }
221
222
        // option provided, but no value was given, default to true
223 4
        if ($value === null) {
224 2
            return true;
225
        }
226
227
        // option provided and value was provided
228 2
        return is_string($value) && in_array($value, ['1', 'true'], true);
229
    }
230
231
    /**
232
     * @return string[]
233
     */
234 7
    private function getArrayOption(InputInterface $input, string $name): array
235
    {
236
        /** @var string[] $value */
237 7
        $value = $input->getOption($name);
238
239 7
        return $value;
240
    }
241
242
    /**
243
     * @return false|resource
244
     */
245 1
    protected function fopen(string $file, string $mode)
246
    {
247 1
        return fopen($file, $mode);
248
    }
249
250
    /**
251
     * @throws InvalidArgumentException
252
     */
253 2
    protected function createOutput(string $file, string $fileWriteStrategy): OutputInterface
254
    {
255 2
        if ($fileWriteStrategy === self::WRITE_STRATEGY_PREPEND) {
256 1
            return new BufferedOutput();
257
        }
258
259 2
        $handle = $this->fopen($file, $this->getFileHandleMode($fileWriteStrategy));
260
261 2
        if ($handle === false) {
262 1
            throw new InvalidArgumentException(sprintf('Could not open handle for %s', $file));
263
        }
264
265 1
        return new StreamOutput($handle);
266
    }
267
268 2
    private function getFileHandleMode(string $fileWriteStrategy): string
269
    {
270 2
        if ($fileWriteStrategy === self::WRITE_STRATEGY_APPEND) {
271 2
            return 'a+';
272
        }
273
274 1
        return 'w+';
275
    }
276
277 16
    private function getChangelogOutput(InputInterface $input, OutputInterface $output): OutputInterface
278
    {
279 16
        $file              = $input->getOption('file');
280 16
        $fileWriteStrategy = $this->getFileWriteStrategy($input);
281
282 16
        $changelogOutput = $output;
283
284 16
        if ($file !== false) {
285 6
            if (is_string($file)) {
286 5
                $changelogOutput = $this->createOutput($file, $fileWriteStrategy);
287 1
            } elseif ($file === null) {
288 1
                $changelogOutput = $this->createOutput($this->getChangelogFilePath(), $fileWriteStrategy);
289
            }
290
        }
291
292 16
        return $changelogOutput;
293
    }
294
295 16
    private function getFileWriteStrategy(InputInterface $input): string
296
    {
297 16
        $append  = (bool) $input->getOption('append');
298 16
        $prepend = (bool) $input->getOption('prepend');
299
300 16
        if ($append) {
301 1
            return self::WRITE_STRATEGY_APPEND;
302
        }
303
304 15
        if ($prepend) {
305 3
            return self::WRITE_STRATEGY_PREPEND;
306
        }
307
308 12
        return self::WRITE_STRATEGY_REPLACE;
309
    }
310
311 1
    private function getChangelogFilePath(): string
312
    {
313 1
        return sprintf('%s/CHANGELOG.md', getcwd());
314
    }
315
316
    /**
317
     * @throws InvalidArgumentException
318
     */
319 20
    private function loadConfigFile(InputInterface $input): ?ChangelogConfig
320
    {
321 20
        $config = $input->getOption('config');
322
323 20
        if ($config === null) {
324 11
            $config = 'changelog-generator-config.php';
325
326 11
            if (! file_exists($config)) {
327 11
                return null;
328
            }
329
        }
330
331
        assert(is_string($config));
332
333 9
        if (! file_exists($config)) {
334 1
            throw new InvalidArgumentException(sprintf('Configuration file "%s" does not exist.', $config));
335
        }
336
337 8
        $configReturn = include $config;
338
339 8
        if (! is_array($configReturn) || count($configReturn) === 0) {
340 1
            throw new InvalidArgumentException(sprintf('Configuration file "%s" did not return anything.', $config));
341
        }
342
343
        /** @var ChangelogConfig[] $changelogConfigs */
344 7
        $changelogConfigs = $configReturn;
345
346 7
        $changelogConfig = $this->findChangelogConfig($input, $changelogConfigs);
347
348 6
        $this->loadChangelogConfigFromInput($input, $changelogConfig);
349
350 6
        return $changelogConfig;
351
    }
352
353
    /**
354
     * @param ChangelogConfig[] $changelogConfigs
355
     */
356 7
    private function findChangelogConfig(InputInterface $input, array $changelogConfigs): ChangelogConfig
357
    {
358 7
        $project = $input->getOption('project');
359
360 7
        $changelogConfig = current($changelogConfigs);
361
362
        assert($changelogConfig !== false);
363
364 7
        if ($project !== null) {
365
            assert(is_string($project));
366
367 7
            if (! isset($changelogConfigs[$project])) {
368 1
                throw new InvalidArgumentException(sprintf('Could not find project named "%s" configured', $project));
369
            }
370
371 6
            $changelogConfig = $changelogConfigs[$project];
372
        }
373
374 6
        return $changelogConfig;
375
    }
376
377 17
    private function loadChangelogConfigFromInput(InputInterface $input, ChangelogConfig $changelogConfig): void
378
    {
379 17
        if ($input->getOption('user') !== null) {
380 16
            $changelogConfig->setUser($this->getStringOption($input, 'user'));
381
        }
382
383 17
        if ($input->getOption('repository') !== null) {
384 16
            $changelogConfig->setRepository($this->getStringOption($input, 'repository'));
385
        }
386
387 17
        if ($input->getOption('milestone') !== null) {
388 16
            $changelogConfig->setMilestone($this->getStringOption($input, 'milestone'));
389
        }
390
391 17
        if ($input->getOption('label') !== []) {
392 7
            $changelogConfig->setLabels($this->getArrayOption($input, 'label'));
393
        }
394
395 17
        if ($input->getOption('non-grouped-label') !== null) {
396 1
            $changelogConfig->setNonGroupedLabel($this->getStringOption($input, 'non-grouped-label'));
397
        }
398
399 17
        if ($input->getOption('include-open') !== '') {
400 3
            $changelogConfig->setIncludeOpen($this->getBooleanOption($input, 'include-open'));
401
        }
402
403 17
        if ($input->getOption('show-contributors') === '') {
404 16
            return;
405
        }
406
407 1
        $changelogConfig->setShowContributors($this->getBooleanOption($input, 'show-contributors'));
408 1
    }
409
}
410