Passed
Push — master ( 1a8193...4ba352 )
by Alexander
02:01
created

SemanticVersionUpdater::updateComposerJson()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 6
ccs 5
cts 5
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Vasoft\VersionIncrement;
6
7
use Vasoft\VersionIncrement\Commits\Commit;
8
use Vasoft\VersionIncrement\Commits\CommitCollection;
9
use Vasoft\VersionIncrement\Contract\GetExecutorInterface;
10
use Vasoft\VersionIncrement\Exceptions\BranchException;
11
use Vasoft\VersionIncrement\Exceptions\ChangesNotFoundException;
12
use Vasoft\VersionIncrement\Exceptions\ComposerException;
13
use Vasoft\VersionIncrement\Exceptions\GitCommandException;
14
use Vasoft\VersionIncrement\Exceptions\IncorrectChangeTypeException;
15
use Vasoft\VersionIncrement\Exceptions\UncommittedException;
16
17
class SemanticVersionUpdater
18
{
19
    private bool $debug = false;
20
    private GetExecutorInterface $gitExecutor;
21
    private array $availableTypes = [
22
        'major',
23
        'minor',
24
        'patch',
25
    ];
26
27 22
    public function __construct(
28
        private readonly string $projectPath,
29
        private readonly Config $config,
30
        private string $changeType = '',
31
        ?GetExecutorInterface $gitExecutor = null,
32
    ) {
33 22
        $this->gitExecutor = $gitExecutor ?? new GitExecutor();
34
    }
35
36
    /**
37
     * @throws IncorrectChangeTypeException
38
     */
39 22
    private function checkChangeType(): void
40
    {
41 22
        if ('' !== $this->changeType && !in_array($this->changeType, $this->availableTypes, true)) {
42 1
            throw  new IncorrectChangeTypeException($this->changeType);
43
        }
44
    }
45
46
    /**
47
     * @throws ComposerException
48
     */
49 21
    public function getComposerJson(): array
50
    {
51 21
        $composer = $this->projectPath . '/composer.json';
52 21
        if (!file_exists($composer)) {
53 1
            throw new ComposerException();
54
        }
55
56
        try {
57 20
            $result = json_decode(file_get_contents($composer), true, 512, JSON_THROW_ON_ERROR);
58 1
        } catch (\JsonException $e) {
59 1
            throw new ComposerException('JSON: ' . $e->getMessage());
60
        }
61
62 19
        return $result;
63
    }
64
65
    /**
66
     * @throws BranchException
67
     * @throws ChangesNotFoundException
68
     * @throws ComposerException
69
     * @throws GitCommandException
70
     * @throws IncorrectChangeTypeException
71
     * @throws UncommittedException
72
     */
73 22
    public function updateVersion(): void
74
    {
75 22
        $this->checkChangeType();
76 21
        $composerJson = $this->getComposerJson();
77 19
        $this->checkGitBranch();
78 18
        $this->checkUncommittedChanges();
79
80 16
        $lastTag = $this->gitExecutor->getLastTag();
81 16
        $commits = $this->gitExecutor->getCommitsSinceLastTag($lastTag);
82 16
        $commitCollection = $this->analyzeCommits($commits);
83 15
        $this->detectionTypeChange($commitCollection);
84
85 15
        $currentVersion = $composerJson['version'] ?? '1.0.0';
86
87 15
        $newVersion = $this->updateComposerVersion($currentVersion, $this->changeType);
88
89 15
        $composerJson['version'] = $newVersion;
90 15
        $this->updateComposerJson($composerJson);
91 15
        $changelog = $this->config->getChangelogFormatter()($commitCollection, $newVersion);
92
93 15
        $this->updateChangeLog($changelog);
94 15
        $this->commitRelease($newVersion);
95
    }
96
97
    /**
98
     * @throws GitCommandException
99
     */
100 15
    private function commitRelease(string $newVersion): void
101
    {
102 15
        if (!$this->debug) {
103 14
            $releaseScope = trim($this->config->getReleaseScope());
104 14
            if ('' !== $releaseScope) {
105 13
                $releaseScope = sprintf('(%s)', $releaseScope);
106
            }
107 14
            $this->gitExecutor->commit(
108 14
                sprintf(
109 14
                    '%s%s: v%s',
110 14
                    $this->config->getReleaseSection(),
111 14
                    $releaseScope,
112 14
                    $newVersion,
113 14
                ),
114 14
            );
115 14
            $this->gitExecutor->setVersionTag($newVersion);
116 14
            echo "Release {$newVersion} successfully created!\n";
117
        }
118
    }
119
120
    /**
121
     * @throws GitCommandException
122
     */
123 15
    private function updateChangeLog(string $changelog): void
124
    {
125 15
        if ($this->debug) {
126 1
            echo $changelog;
127
        } else {
128 14
            $fileChangelog = $this->projectPath . '/CHANGELOG.md';
129 14
            if (file_exists($fileChangelog)) {
130 13
                $changeLogContent = file_get_contents($fileChangelog);
131 13
                $changeLogAddToGit = false;
132
            } else {
133 1
                $changeLogContent = '';
134 1
                $changeLogAddToGit = true;
135
            }
136 14
            file_put_contents($fileChangelog, $changelog . $changeLogContent);
137 14
            if ($changeLogAddToGit) {
138 1
                $this->gitExecutor->addFile('CHANGELOG.md');
139
            }
140
        }
141
    }
142
143 15
    private function updateComposerJson(array $composerJson): void
144
    {
145 15
        if (!$this->debug) {
146 14
            file_put_contents(
147 14
                $this->projectPath . '/composer.json',
148 14
                json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
149 14
            );
150
        }
151
    }
152
153 15
    private function detectionTypeChange(CommitCollection $commitCollection): void
154
    {
155 15
        if ('' === $this->changeType) {
156 15
            if ($commitCollection->hasMajorMarker()) {
157 3
                $this->changeType = 'major';
158 12
            } elseif ($commitCollection->hasMinorMarker()) {
159 11
                $this->changeType = 'minor';
160
            } else {
161 1
                $this->changeType = 'patch';
162
            }
163
        }
164
    }
165
166
    /**
167
     * @throws GitCommandException
168
     * @throws UncommittedException
169
     */
170 18
    private function checkUncommittedChanges(): void
171
    {
172 18
        $out = $this->gitExecutor->status();
173 18
        if ($this->config->mastIgnoreUntrackedFiles()) {
174 2
            $out = array_filter($out, static fn(string $item): bool => !str_starts_with($item, '??'));
175
        }
176
177 18
        if (!empty($out)) {
178 2
            throw new UncommittedException();
179
        }
180
    }
181
182
    /**
183
     * @throws BranchException
184
     * @throws GitCommandException
185
     */
186 19
    private function checkGitBranch(): void
187
    {
188 19
        $currentBranch = $this->gitExecutor->getCurrentBranch();
189 19
        $targetBranch = $this->config->getMasterBranch();
190 19
        if ($currentBranch !== $targetBranch) {
191 1
            throw new BranchException($currentBranch, $targetBranch);
192
        }
193
    }
194
195
    /**
196
     * @throws ChangesNotFoundException
197
     * @throws GitCommandException
198
     */
199 16
    private function analyzeCommits(array $commits): CommitCollection
200
    {
201 16
        if (empty($commits)) {
202 1
            throw new ChangesNotFoundException();
203
        }
204 15
        $commitCollection = $this->config->getCommitCollection();
205 15
        $aggregateKey = $this->config->getAggregateSection();
206 15
        $shouldProcessDefaultSquashedCommit = $this->config->shouldProcessDefaultSquashedCommit();
207 15
        $squashedCommitMessage = $this->config->getSquashedCommitMessage();
208 15
        foreach ($commits as $commit) {
209 15
            if (preg_match(
210 15
                '/^(?<hash>[^ ]+) (?<commit>.+)/',
211 15
                $commit,
212 15
                $matches,
213 15
            )) {
214 15
                $hash = $matches['hash'];
215 15
                $commit = $matches['commit'];
216
                if (
217 15
                    $shouldProcessDefaultSquashedCommit
218 15
                    && str_ends_with($commit, $squashedCommitMessage)
219
                ) {
220 2
                    $this->processAggregated($hash, $commitCollection);
221
222 2
                    continue;
223
                }
224 15
                if (preg_match(
225 15
                    '/^(?<key>[a-z]+)(?:\((?<scope>[^)]+)\))?(?<breaking>!)?:\s+(?<message>.+)/',
226 15
                    $commit,
227 15
                    $matches,
228 15
                )) {
229 14
                    $key = trim($matches['key']);
230 14
                    if ($aggregateKey === $key) {
231 1
                        $commitCollection->setMajorMarker('!' === $matches['breaking']);
232 1
                        $this->processAggregated($hash, $commitCollection);
233
                    } else {
234 14
                        $commitCollection->add(
235 14
                            new Commit(
236 14
                                $commit,
237 14
                                $key,
238 14
                                $matches['message'],
239 14
                                '!' === $matches['breaking'],
240 14
                                $matches['scope'],
241 14
                                [$matches['breaking']],
242 14
                            ),
243 14
                        );
244
                    }
245
                } else {
246 1
                    $commitCollection->addRawMessage($commit);
247
                }
248
            }
249
        }
250
251 15
        return $commitCollection;
252
    }
253
254
    /**
255
     * @throws GitCommandException
256
     */
257 3
    private function processAggregated(string $hash, CommitCollection $commitCollection): void
258
    {
259 3
        $description = $this->gitExecutor->getCommitDescription($hash);
260 3
        foreach ($description as $line) {
261 3
            $matches = [];
262 3
            if (preg_match(
263 3
                "/^[\t *-]*((?<key>[a-z]+)(?:\\((?<scope>[^)]+)\\))?(?<breaking>!)?:\\s+(?<message>.+))/",
264 3
                $line,
265 3
                $matches,
266 3
            )) {
267 3
                $commitCollection->add(
268 3
                    new Commit(
269 3
                        $line,
270 3
                        $matches['key'],
271 3
                        $matches['message'],
272 3
                        '!' === $matches['breaking'],
273 3
                        $matches['scope'],
274 3
                        [$matches['breaking']],
275 3
                    ),
276 3
                );
277
            }
278
        }
279
    }
280
281 15
    private function updateComposerVersion(string $currentVersion, string $changeType): string
282
    {
283 15
        [$major, $minor, $patch] = explode('.', $currentVersion);
284
        switch ($changeType) {
285 15
            case 'major':
286 3
                $major++;
287 3
                $minor = 0;
288 3
                $patch = 0;
289 3
                break;
290 12
            case 'minor':
291 11
                $minor++;
292 11
                $patch = 0;
293 11
                break;
294 1
            case 'patch':
295
            default:
296 1
                $patch++;
297 1
                break;
298
        }
299
300 15
        return "{$major}.{$minor}.{$patch}";
301
    }
302
303 1
    public function setDebug(bool $debug): self
304
    {
305 1
        $this->debug = $debug;
306
307 1
        return $this;
308
    }
309
}
310