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

eliashaeussler / version-bumper / 25882115738

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

Pull #138

github

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

46 of 82 new or added lines in 7 files covered. (56.1%)

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

68
        // Inject custom repository caller
69
        if (null !== $this->caller) {
10✔
70
            $repository->setCaller($this->caller);
10✔
71
        }
72

73
        $version = $this->extractVersionFromResults($results) ?? $this->detectVersionFromVersionRange($versionRange, $repository);
10✔
74

75
        if (null === $version) {
9✔
76
            throw new Exception\TargetVersionIsMissing();
3✔
77
        }
78

79
        $modifiedFiles = $this->extractModifiedFilesFromResults($results);
6✔
80
        $tagName = Helper\VersionHelper::replaceVersionInPattern($options->tagName(), $version);
6✔
81

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

172
        return $version;
9✔
173
    }
174

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

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

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

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

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

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

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

215
        return $modifiedFiles;
6✔
216
    }
217
}
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