Failed Conditions
Push — master ( 41301b...74c660 )
by Jonathan
19s queued 11s
created

loadChangelogConfigFromInput()   B

Complexity

Conditions 7
Paths 64

Size

Total Lines 27
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 7

Importance

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