Completed
Push — master ( 0d42e0...f1e76d )
by Jonathan
10s
created

GenerateChangelogCommand::getIncludeOpen()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 20
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

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