SemanticVersionUpdater::checkUncommittedChanges()   A
last analyzed

Complexity

Conditions 4
Paths 6

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

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