Completed
Push — master ( fbcd29...a9ae8f )
by Jonathan
24s queued 11s
created

GenerateChangelogCommand::getStringOption()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.0261

Importance

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