• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

eliashaeussler / version-bumper / 25880688608

14 May 2026 07:23PM UTC coverage: 86.276% (-2.2%) from 88.43%
25880688608

Pull #138

github

eliashaeussler
[!!!][FEATURE] Allow Git tagging without modified files
Pull Request #138: [!!!][FEATURE] Allow Git tagging without modified files

41 of 77 new or added lines in 7 files covered. (53.25%)

1163 of 1348 relevant lines covered (86.28%)

5.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.5
/src/Version/VersionReleaser.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the Composer package "eliashaeussler/version-bumper".
7
 *
8
 * Copyright (C) 2024-2026 Elias Häußler <elias@haeussler.dev>
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, either version 3 of the License, or
13
 * (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
 * GNU General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU General Public License
21
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22
 */
23

24
namespace EliasHaeussler\VersionBumper\Version;
25

26
use EliasHaeussler\VersionBumper\Config;
27
use EliasHaeussler\VersionBumper\Enum;
28
use EliasHaeussler\VersionBumper\Exception;
29
use EliasHaeussler\VersionBumper\Helper;
30
use EliasHaeussler\VersionBumper\Result;
31
use GitElephant\Command;
32
use GitElephant\Repository;
33

34
use function in_array;
35
use function is_string;
36

37
/**
38
 * VersionReleaser.
39
 *
40
 * @author Elias Häußler <elias@haeussler.dev>
41
 * @license GPL-3.0-or-later
42
 */
43
final readonly class VersionReleaser
44
{
45
    public function __construct(
10✔
46
        private ?Command\Caller\CallerInterface $caller = null,
47
    ) {}
10✔
48

49
    /**
50
     * @param list<Result\VersionBumpResult> $results
51
     *
52
     * @throws Exception\AmbiguousVersionsDetected
53
     * @throws Exception\CannotFetchLatestGitTag
54
     * @throws Exception\CouldNotCreateGitTag
55
     * @throws Exception\TagAlreadyExists
56
     * @throws Exception\TargetVersionIsMissing
57
     * @throws Exception\VersionIsNotSupported
58
     */
59
    public function release(
10✔
60
        array $results,
61
        string $rootPath,
62
        Config\ReleaseOptions $options = new Config\ReleaseOptions(),
63
        Enum\VersionRange|string|null $versionRange = null,
64
        bool $dryRun = false,
65
    ): Result\VersionReleaseResult {
66
        $repository = new Repository($rootPath);
10✔
67
        $version = $this->extractVersionFromResults($results) ?? $this->detectVersionFromVersionRange($versionRange, $repository);
10✔
68

69
        if (null === $version) {
9✔
70
            throw new Exception\TargetVersionIsMissing();
3✔
71
        }
72

73
        $modifiedFiles = $this->extractModifiedFilesFromResults($results);
6✔
74
        $tagName = Helper\VersionHelper::replaceVersionInPattern($options->tagName(), $version);
6✔
75

76
        // Inject custom repository caller
77
        if (null !== $this->caller) {
6✔
78
            $repository->setCaller($this->caller);
6✔
79
        }
80

81
        // Check if tag already exists
82
        if (null !== $repository->getTag($tagName)) {
6✔
83
            if (!$options->overwriteExistingTag()) {
2✔
84
                throw new Exception\TagAlreadyExists($tagName);
1✔
85
            }
86

87
            if (!$dryRun) {
1✔
88
                $repository->deleteTag($tagName);
1✔
89
            }
90
        }
91

92
        [$commitMessage, $commitId] = $this->commitModifiedFiles($modifiedFiles, $repository, $options, $version, $dryRun);
5✔
93

94
        if (!$dryRun) {
5✔
95
            $tagCommand = Command\TagCommand::getInstance($repository)->create($tagName, null, $tagName);
4✔
96

97
            if ($options->signTag()) {
4✔
98
                $tagCommand .= ' -s';
1✔
99
            }
100

101
            $repository->getCaller()->execute($tagCommand);
4✔
102

103
            if (null === $repository->getTag($tagName)) {
4✔
NEW
104
                throw new Exception\CouldNotCreateGitTag($tagName);
×
105
            }
106
        }
107

108
        return new Result\VersionReleaseResult($modifiedFiles, $tagName, $commitMessage, $commitId);
5✔
109
    }
110

111
    /**
112
     * @param list<Config\FileToModify> $modifiedFiles
113
     *
114
     * @return array{string|null, string|null}
115
     */
116
    private function commitModifiedFiles(
5✔
117
        array $modifiedFiles,
118
        Repository $repository,
119
        Config\ReleaseOptions $options,
120
        Version $version,
121
        bool $dryRun,
122
    ): array {
123
        if ([] === $modifiedFiles) {
5✔
NEW
124
            return [null, null];
×
125
        }
126

127
        $commitMessage = Helper\VersionHelper::replaceVersionInPattern($options->commitMessage(), $version);
5✔
128

129
        if ($dryRun) {
5✔
130
            return [$commitMessage, null];
1✔
131
        }
132

133
        // Add and commit modified files
134
        foreach ($modifiedFiles as $file) {
4✔
135
            $repository->stage($file->path());
4✔
136
        }
137

138
        $repository->commit($commitMessage);
4✔
139
        $commitId = $repository->getCommit()->getSha();
4✔
140

141
        return [$commitMessage, $commitId];
4✔
142
    }
143

144
    /**
145
     * @param list<Result\VersionBumpResult> $results
146
     *
147
     * @throws Exception\AmbiguousVersionsDetected
148
     */
149
    private function extractVersionFromResults(array $results): ?Version
10✔
150
    {
151
        $version = null;
10✔
152

153
        foreach ($results as $result) {
10✔
154
            foreach ($result->operations() as $operation) {
9✔
155
                $targetVersion = $operation->target();
8✔
156

157
                if (null === $targetVersion) {
8✔
158
                    continue;
2✔
159
                }
160

161
                if (null === $version) {
7✔
162
                    $version = $targetVersion;
7✔
163
                }
164

165
                if ($targetVersion->full() !== $version->full()) {
7✔
166
                    throw new Exception\AmbiguousVersionsDetected();
1✔
167
                }
168
            }
169
        }
170

171
        return $version;
9✔
172
    }
173

174
    /**
175
     * @throws Exception\CannotFetchLatestGitTag
176
     * @throws Exception\VersionIsNotSupported
177
     */
178
    private function detectVersionFromVersionRange(Enum\VersionRange|string|null $versionRange, Repository $repository): ?Version
3✔
179
    {
180
        if (null === $versionRange) {
3✔
181
            return null;
3✔
182
        }
183

NEW
184
        if (is_string($versionRange)) {
×
NEW
185
            return Version::fromFullVersion($versionRange);
×
186
        }
187

NEW
188
        $tag = Helper\GitHelper::fetchLatestVersionTag($repository);
×
189

NEW
190
        if (null === $tag) {
×
NEW
191
            return null;
×
192
        }
193

NEW
194
        return Version::fromFullVersion($tag->getName())->increase($versionRange);
×
195
    }
196

197
    /**
198
     * @param list<Result\VersionBumpResult> $results
199
     *
200
     * @return list<Config\FileToModify>
201
     */
202
    private function extractModifiedFilesFromResults(array $results): array
6✔
203
    {
204
        $modifiedFiles = [];
6✔
205

206
        foreach ($results as $result) {
6✔
207
            foreach ($result->operations() as $operation) {
6✔
208
                if ($operation->state()->modified() && !in_array($result->file(), $modifiedFiles, true)) {
6✔
209
                    $modifiedFiles[] = $result->file();
6✔
210
                }
211
            }
212
        }
213

214
        return $modifiedFiles;
6✔
215
    }
216
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc