Passed
Push — master ( 3fa076...a3b1cc )
by Stefano
02:23
created

ChangeLogCommand::createChangeLog()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 9
c 1
b 0
f 0
dl 0
loc 14
rs 9.9666
cc 4
nc 3
nop 1
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * BEdita, API-first content management framework
6
 * Copyright 2021 ChannelWeb Srl, Chialab Srl
7
 *
8
 * This file is part of BEdita: you can redistribute it and/or modify
9
 * it under the terms of the GNU Lesser General Public License as published
10
 * by the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
14
 */
15
16
namespace BEdita\DevTools\Command;
17
18
use Cake\Command\Command;
19
use Cake\Console\Arguments;
20
use Cake\Console\ConsoleIo;
21
use Cake\Console\ConsoleOptionParser;
22
use Cake\Core\Configure;
23
use Cake\Core\InstanceConfigTrait;
24
use Cake\Http\Client;
25
use Cake\Utility\Hash;
26
27
/**
28
 * Command to generate a changelog snippet to prepare a new relase.
29
 */
30
class ChangeLogCommand extends Command
31
{
32
    use InstanceConfigTrait;
33
34
    /**
35
     * Default configuration for command.
36
     *
37
     * @var array
38
     */
39
    protected $_defaultConfig = [
40
        // Classification filter on labels
41
        'filter' => [
42
            'integration' => [
43
                'Topic - Integration',
44
                'Topic - Tests',
45
            ],
46
            'api' => [
47
                'Topic - API',
48
            ],
49
            'core' => [
50
                'Topic - Core',
51
                'Topic - Database',
52
                'Topic - Authentication',
53
                'Topic - ORM',
54
            ],
55
        ],
56
        // Changelog sections
57
        'sections' => [
58
            'api' => 'API',
59
            'core' => 'Core',
60
            'integration' => 'Integration',
61
            'other' => 'Other',
62
        ],
63
        // Issues search url
64
        'url' => 'https://api.github.com/search/issues',
65
        // HTTP client configuration
66
        'client' => [],
67
    ];
68
69
    /**
70
     * @inheritDoc
71
     */
72
    protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
73
    {
74
        $parser->addArgument('from', [
75
                'help' => 'Read closed PRs from this date in "YYYY-MM-DD" format',
76
                'required' => true,
77
            ])
78
            ->addArgument('version', [
79
                'help' => 'Version to release',
80
                'required' => true,
81
            ]);
82
83
        return $parser;
84
    }
85
86
    /**
87
     * @inheritDoc
88
     */
89
    public function initialize(): void
90
    {
91
        $config = (array)Configure::read('ChangeLog');
92
        $this->setConfig($config);
93
    }
94
95
    /**
96
     * Read closed PRs from github and create changelog file.
97
     *
98
     * @param \Cake\Console\Arguments $args The command arguments.
99
     * @param \Cake\Console\ConsoleIo $io The console io
100
     * @return null|int The exit code or null for success
101
     */
102
    public function execute(Arguments $args, ConsoleIo $io): ?int
103
    {
104
        $from = $args->getArgument('from');
105
        $io->out("Reading closed PRs from from $from");
106
107
        $json = $this->fetchPrs((string)$from);
108
        $items = (array)Hash::get($json, 'items');
109
        $io->out(sprintf('Loaded %d prs', count($items)));
110
111
        $changeLog = $this->createChangeLog($items);
112
        $this->saveChangeLog($changeLog, (string)$args->getArgument('version'));
113
114
        $io->out('Changelog created. Bye.');
115
116
        return null;
117
    }
118
119
    /**
120
     * Fetch pull requests from Github
121
     *
122
     * @param string $from From date.
123
     * @return array
124
     */
125
    protected function fetchPrs(string $from): array
126
    {
127
        $client = new Client((array)$this->getConfig('client'));
128
        $query = [
129
            'q' => sprintf('is:pr draft:false repo:bedita/bedita merged:>%s', $from),
130
            'sort' => '-closed',
131
            'per_page' => 100,
132
        ];
133
        $headers = ['Accept' => 'application/vnd.github.v3+json'];
134
        /** @var string $url */
135
        $url = $this->getConfig('url');
136
        $response = $client->get($url, $query, compact('headers'));
137
138
        return (array)$response->getJson();
139
    }
140
141
    /**
142
     * Create changelog array with classified pull requests
143
     *
144
     * @param array $items PR items
145
     * @return array
146
     */
147
    protected function createChangeLog(array $items): array
148
    {
149
        $res = [];
150
        foreach ($items as $item) {
151
            $milestone = Hash::get($item, 'milestone.title');
152
            if (is_string($milestone) && substr($milestone, 0, 1) !== '4') {
153
                continue;
154
            }
155
            $labels = Hash::extract($item, 'labels.{n}.name');
156
            $type = $this->classify((array)$labels);
157
            $res[$type][] = $item;
158
        }
159
160
        return $res;
161
    }
162
163
    /**
164
     * Classify PR from its labels
165
     *
166
     * @param array $labels Labels array
167
     * @return string
168
     */
169
    protected function classify(array $labels): string
170
    {
171
        foreach ((array)$this->getConfig('filter') as $name => $data) {
172
            if (!empty(array_intersect($labels, (array)$data))) {
173
                return $name;
174
            }
175
        }
176
177
        return 'other';
178
    }
179
180
    /**
181
     * Save changelog to file
182
     *
183
     * @param array $changeLog Changelog items
184
     * @param string $version Version released
185
     * @return void
186
     */
187
    protected function saveChangeLog(array $changeLog, string $version): void
188
    {
189
        $out = sprintf("## Version %s - Cactus\n", $version);
190
191
        foreach ((array)$this->getConfig('sections') as $name => $label) {
192
            /** @var string $label */
193
            $out .= sprintf("\n### %s changes (%s)\n\n", $label, (string)$version);
194
            $out .= $this->loglines((array)Hash::get($changeLog, $name));
195
        }
196
197
        file_put_contents(sprintf('changelog-%s.md', $version), $out);
198
    }
199
200
    /**
201
     * Section changelog lines
202
     *
203
     * @param array $items Changelog items
204
     * @return string
205
     */
206
    protected function loglines(array $items): string
207
    {
208
        $res = '';
209
        foreach ($items as $item) {
210
            $id = (string)$item['number'];
211
            $url = (string)$item['html_url'];
212
            $title = (string)$item['title'];
213
            $res .= sprintf("* [#%s](%s) %s\n", $id, $url, $title);
214
        }
215
216
        return $res;
217
    }
218
}
219