Passed
Push — master ( 2e31e0...207204 )
by Alexander
01:57
created

SemanticVersionUpdater::updateChangeLog()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 20
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 5

Importance

Changes 0
Metric Value
eloc 15
c 0
b 0
f 0
dl 0
loc 20
ccs 14
cts 14
cp 1
rs 9.4555
cc 5
nc 6
nop 1
crap 5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Vasoft\VersionIncrement;
6
7
use Vasoft\VersionIncrement\Commits\CommitCollection;
8
use Vasoft\VersionIncrement\Contract\VcsExecutorInterface;
9
use Vasoft\VersionIncrement\Events\Event;
10
use Vasoft\VersionIncrement\Events\EventType;
11
use Vasoft\VersionIncrement\Exceptions\BranchException;
12
use Vasoft\VersionIncrement\Exceptions\ChangelogException;
13
use Vasoft\VersionIncrement\Exceptions\ComposerException;
14
use Vasoft\VersionIncrement\Exceptions\GitCommandException;
15
use Vasoft\VersionIncrement\Exceptions\IncorrectChangeTypeException;
16
use Vasoft\VersionIncrement\Exceptions\UncommittedException;
17
18
class SemanticVersionUpdater
19
{
20
    public const DEFAULT_VERSION = '1.0.0';
21
    private bool $debug = false;
22
    private VcsExecutorInterface $gitExecutor;
23
    public static array $availableTypes = [
24
        'major',
25
        'minor',
26
        'patch',
27
    ];
28
29 33
    public function __construct(
30
        private readonly string $projectPath,
31
        private readonly Config $config,
32
        private string $changeType = '',
33
        private readonly bool $doCommit = true,
34
    ) {
35 33
        $this->gitExecutor = $config->getVcsExecutor();
36
    }
37
38
    /**
39
     * @throws IncorrectChangeTypeException
40
     */
41 33
    private function checkChangeType(): void
42
    {
43 33
        if ('' !== $this->changeType && !in_array($this->changeType, self::$availableTypes, true)) {
44 1
            throw  new IncorrectChangeTypeException($this->changeType);
45
        }
46
    }
47
48
    /**
49
     * @throws ComposerException
50
     */
51 25
    public function getComposerJson(): array
52
    {
53 25
        $composer = $this->projectPath . '/composer.json';
54 25
        if (!file_exists($composer)) {
55 1
            throw new ComposerException();
56
        }
57 24
        if (!is_writable($composer)) {
58 1
            throw new ComposerException('Composer file is not writable.');
59
        }
60
61
        try {
62 23
            $result = json_decode(file_get_contents($composer), true, 512, JSON_THROW_ON_ERROR);
63 1
        } catch (\JsonException $e) {
64 1
            throw new ComposerException('JSON: ' . $e->getMessage());
65
        }
66
67 22
        return $result;
68
    }
69
70
    /**
71
     * @throws BranchException
72
     * @throws ChangelogException
73
     * @throws ComposerException
74
     * @throws GitCommandException
75
     * @throws IncorrectChangeTypeException
76
     * @throws UncommittedException
77
     */
78 33
    public function updateVersion(): void
79
    {
80 33
        $this->checkChangeType();
81 32
        $this->checkGitBranch();
82 31
        $this->checkUncommittedChanges();
83
84 30
        $lastTag = $this->gitExecutor->getLastTag();
85 30
        if ($this->config->isEnabledComposerVersioning()) {
86 25
            $composerJson = $this->getComposerJson();
87 22
            $currentVersion = $composerJson['version'] ?? self::DEFAULT_VERSION;
88 5
        } elseif (null === $lastTag) {
89 1
            $currentVersion = self::DEFAULT_VERSION;
90
        } else {
91 4
            $currentVersion = $this->config->getTagFormatter()->extractVersion($lastTag);
92
        }
93
94 27
        $commitCollection = $this->config->getCommitParser()->process($lastTag);
95 26
        $this->detectionTypeChange($commitCollection);
96
97 26
        $newVersion = $this->incrementVersion($currentVersion, $this->changeType);
98
99 26
        if ($this->config->isEnabledComposerVersioning()) {
100 21
            $composerJson['version'] = $newVersion;
101 21
            $this->updateComposerJson($composerJson);
102
        }
103 26
        $changelog = $this->config->getChangelogFormatter()($commitCollection, $newVersion);
104
105 26
        $this->updateChangeLog($changelog);
106 25
        $this->commitRelease($newVersion);
107
    }
108
109
    /**
110
     * @throws GitCommandException
111
     */
112 25
    private function commitRelease(string $newVersion): void
113
    {
114 25
        if (!$this->debug) {
115 23
            $this->config->getEventBus()->dispatch(new Event(EventType::BEFORE_VERSION_SET, $newVersion));
116 23
            if ($this->doCommit) {
117 21
                $this->processWithCommit($newVersion);
118
            } else {
119 2
                $this->processWithOutCommit($newVersion);
120
            }
121 23
            $this->config->getEventBus()->dispatch(new Event(EventType::AFTER_VERSION_SET, $newVersion));
122
        }
123
    }
124
125
    /**
126
     * @throws GitCommandException
127
     */
128 21
    private function processWithCommit(string $newVersion): void
129
    {
130 21
        $releaseScope = trim($this->config->getReleaseScope());
131 21
        if ('' !== $releaseScope) {
132 19
            $releaseScope = sprintf('(%s)', $releaseScope);
133
        }
134 21
        $this->gitExecutor->commit(
135 21
            sprintf(
136 21
                '%s%s: v%s',
137 21
                $this->config->getReleaseSection(),
138 21
                $releaseScope,
139 21
                $newVersion,
140 21
            ),
141 21
        );
142 21
        $this->gitExecutor->setVersionTag($newVersion);
143 21
        echo "Release {$newVersion} successfully created!\n";
144
    }
145
146 2
    private function processWithOutCommit(string $newVersion): void
147
    {
148 2
        echo "Version {$newVersion} is ready for release.\n";
149 2
        echo "To complete the process, commit your changes and add a Git tag:\n";
150 2
        echo "    git commit -m \"chore(release): v{$newVersion}\"\n";
151 2
        echo "    git tag v{$newVersion}\n";
152
    }
153
154
    /**
155
     * @throws ChangelogException
156
     * @throws GitCommandException
157
     */
158 26
    private function updateChangeLog(string $changelog): void
159
    {
160 26
        if ($this->debug) {
161 2
            echo $changelog;
162
        } else {
163 24
            $fileChangelog = $this->projectPath . '/CHANGELOG.md';
164 24
            if (file_exists($fileChangelog)) {
165 22
                if (!is_writable($fileChangelog)) {
166 1
                    throw new ChangelogException();
167
                }
168
169 21
                $changeLogContent = file_get_contents($fileChangelog);
170 21
                $changeLogAddToGit = false;
171
            } else {
172 2
                $changeLogContent = '';
173 2
                $changeLogAddToGit = true;
174
            }
175 23
            file_put_contents($fileChangelog, $changelog . $changeLogContent);
176 23
            if ($changeLogAddToGit) {
177 2
                $this->gitExecutor->addFile('CHANGELOG.md');
178
            }
179
        }
180
    }
181
182 21
    private function updateComposerJson(array $composerJson): void
183
    {
184 21
        if (!$this->debug) {
185 20
            file_put_contents(
186 20
                $this->projectPath . '/composer.json',
187 20
                json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
188 20
            );
189
        }
190
    }
191
192 26
    private function detectionTypeChange(CommitCollection $commitCollection): void
193
    {
194 26
        if ('' === $this->changeType) {
195 25
            if ($commitCollection->hasMajorMarker()) {
196 5
                $this->changeType = 'major';
197 20
            } elseif ($commitCollection->hasMinorMarker()) {
198 16
                $this->changeType = 'minor';
199
            } else {
200 4
                $this->changeType = 'patch';
201
            }
202
        }
203
    }
204
205
    /**
206
     * @throws GitCommandException
207
     * @throws UncommittedException
208
     */
209 31
    private function checkUncommittedChanges(): void
210
    {
211 31
        $out = $this->gitExecutor->status();
212 31
        if ($this->config->mastIgnoreUntrackedFiles()) {
213 1
            $out = array_filter($out, static fn(string $item): bool => !str_starts_with($item, '??'));
214
        }
215
216 31
        if (!empty($out)) {
217 1
            throw new UncommittedException();
218
        }
219
    }
220
221
    /**
222
     * @throws BranchException
223
     * @throws GitCommandException
224
     */
225 32
    private function checkGitBranch(): void
226
    {
227 32
        $currentBranch = $this->gitExecutor->getCurrentBranch();
228 32
        $targetBranch = $this->config->getMasterBranch();
229 32
        if ($currentBranch !== $targetBranch) {
230 1
            throw new BranchException($currentBranch, $targetBranch);
231
        }
232
    }
233
234 26
    private function incrementVersion(string $currentVersion, string $changeType): string
235
    {
236 26
        [$major, $minor, $patch] = explode('.', $currentVersion);
237
        switch ($changeType) {
238 26
            case 'major':
239 6
                $major++;
240 6
                $minor = 0;
241 6
                $patch = 0;
242 6
                break;
243 20
            case 'minor':
244 16
                $minor++;
245 16
                $patch = 0;
246 16
                break;
247 4
            case 'patch':
248
            default:
249 4
                $patch++;
250 4
                break;
251
        }
252
253 26
        return "{$major}.{$minor}.{$patch}";
254
    }
255
256 5
    public function setDebug(bool $debug): self
257
    {
258 5
        $this->debug = $debug;
259
260 5
        return $this;
261
    }
262
}
263