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

eliashaeussler / version-bumper / 11167493724

03 Oct 2024 06:22PM UTC coverage: 87.938% (-1.4%) from 89.351%
11167493724

Pull #7

github

web-flow
Merge 50957af26 into 4831e7102
Pull Request #7: [FEATURE] Introduce `VersionReleaser` and `--release` option

135 of 157 new or added lines in 12 files covered. (85.99%)

5 existing lines in 1 file now uncovered.

452 of 514 relevant lines covered (87.94%)

3.44 hits per line

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

96.77
/src/Command/BumpVersionCommand.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 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\Command;
25

26
use Composer\Command;
27
use Composer\Composer;
28
use CuyZ\Valinor;
29
use CzProject\GitPhp;
30
use EliasHaeussler\VersionBumper\Config;
31
use EliasHaeussler\VersionBumper\Enum;
32
use EliasHaeussler\VersionBumper\Exception;
33
use EliasHaeussler\VersionBumper\Result;
34
use EliasHaeussler\VersionBumper\Version;
35
use Symfony\Component\Console;
36
use Symfony\Component\Filesystem;
37

38
use function count;
39
use function dirname;
40
use function getcwd;
41
use function implode;
42
use function is_string;
43
use function method_exists;
44
use function reset;
45
use function sprintf;
46
use function trim;
47

48
/**
49
 * BumpVersionCommand.
50
 *
51
 * @author Elias Häußler <elias@haeussler.dev>
52
 * @license GPL-3.0-or-later
53
 */
54
final class BumpVersionCommand extends Command\BaseCommand
55
{
56
    private readonly Version\VersionBumper $bumper;
57
    private readonly Config\ConfigReader $configReader;
58
    private readonly Version\VersionReleaser $releaser;
59
    private Console\Style\SymfonyStyle $io;
60

61
    public function __construct(?Composer $composer = null)
9✔
62
    {
63
        if (null !== $composer) {
9✔
64
            $this->setComposer($composer);
1✔
65
        }
66

67
        parent::__construct('bump-version');
9✔
68

69
        $this->bumper = new Version\VersionBumper();
9✔
70
        $this->configReader = new Config\ConfigReader();
9✔
71
        $this->releaser = new Version\VersionReleaser();
9✔
72
    }
73

74
    protected function configure(): void
9✔
75
    {
76
        $this->setAliases(['bv']);
9✔
77
        $this->setDescription('Bump package version in specific files during release preparations');
9✔
78

79
        $this->addArgument(
9✔
80
            'range',
9✔
81
            Console\Input\InputArgument::REQUIRED,
9✔
82
            sprintf(
9✔
83
                'Version range (one of "%s") or explicit version to bump in configured files',
9✔
84
                implode('", "', Enum\VersionRange::all()),
9✔
85
            ),
9✔
86
        );
9✔
87

88
        $this->addOption(
9✔
89
            'config',
9✔
90
            'c',
9✔
91
            Console\Input\InputOption::VALUE_REQUIRED,
9✔
92
            'Path to configuration file (JSON or YAML) with files in which to bump new versions',
9✔
93
            $this->readConfigFileFromRootPackage(),
9✔
94
        );
9✔
95
        $this->addOption(
9✔
96
            'release',
9✔
97
            'r',
9✔
98
            Console\Input\InputOption::VALUE_NONE,
9✔
99
            'Create a new Git tag after versions are bumped',
9✔
100
        );
9✔
101
        $this->addOption(
9✔
102
            'dry-run',
9✔
103
            null,
9✔
104
            Console\Input\InputOption::VALUE_NONE,
9✔
105
            'Do not perform any write operations, just calculate version bumps',
9✔
106
        );
9✔
107
        $this->addOption(
9✔
108
            'strict',
9✔
109
            null,
9✔
110
            Console\Input\InputOption::VALUE_NONE,
9✔
111
            'Fail if any unmatched file pattern is reported',
9✔
112
        );
9✔
113
    }
114

115
    protected function initialize(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): void
9✔
116
    {
117
        $this->io = new Console\Style\SymfonyStyle($input, $output);
9✔
118
    }
119

120
    protected function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
9✔
121
    {
122
        $rootPath = (string) getcwd();
9✔
123
        $rangeOrVersion = $input->getArgument('range');
9✔
124
        $configFile = $input->getOption('config') ?? $this->configReader->detectFile($rootPath);
9✔
125
        $release = $input->getOption('release');
9✔
126
        $dryRun = $input->getOption('dry-run');
9✔
127
        $strict = $input->getOption('strict');
9✔
128

129
        if (null === $configFile) {
9✔
130
            $this->io->error('Please provide a config file path using the --config option.');
1✔
131

132
            return self::INVALID;
1✔
133
        }
134

135
        if (Filesystem\Path::isRelative($configFile)) {
8✔
136
            $configFile = Filesystem\Path::makeAbsolute($configFile, $rootPath);
1✔
137
        } else {
138
            $rootPath = dirname($configFile);
7✔
139
        }
140

141
        try {
142
            $result = $this->bumpVersions($configFile, $rangeOrVersion, $rootPath, $dryRun);
8✔
143

144
            if (null === $result) {
8✔
145
                return self::FAILURE;
4✔
146
            }
147

148
            [$config, $results] = $result;
4✔
149

150
            if ($release && !$this->releaseVersion($results, $rootPath, $config->releaseOptions(), $dryRun)) {
4✔
151
                return self::FAILURE;
1✔
152
            }
153
        } finally {
154
            if ($dryRun) {
8✔
155
                $this->io->note('No write operations were performed (dry-run mode).');
5✔
156
            }
157
        }
158

159
        if ($strict) {
3✔
160
            foreach ($results as $versionBumpResult) {
1✔
161
                if ($versionBumpResult->hasUnmatchedReports()) {
1✔
162
                    return self::FAILURE;
1✔
163
                }
164
            }
165
        }
166

167
        return self::SUCCESS;
2✔
168
    }
169

170
    /**
171
     * @return array{Config\VersionBumperConfig, list<Result\VersionBumpResult>}|null
172
     */
173
    private function bumpVersions(string $configFile, string $rangeOrVersion, string $rootPath, bool $dryRun): ?array
8✔
174
    {
175
        try {
176
            $config = $this->configReader->readFromFile($configFile);
8✔
177
            $versionRange = Enum\VersionRange::tryFromInput($rangeOrVersion);
7✔
178

179
            $results = $this->bumper->bump(
7✔
180
                $config->filesToModify(),
7✔
181
                $config->rootPath() ?? $rootPath,
7✔
182
                $versionRange ?? $rangeOrVersion,
7✔
183
                $dryRun,
7✔
184
            );
7✔
185

186
            $this->decorateVersionBumpResults($results, $rootPath);
4✔
187

188
            return [$config, $results];
4✔
189
        } catch (Valinor\Mapper\MappingError $error) {
4✔
190
            $this->decorateMappingError($error, $configFile);
1✔
191
        } catch (Exception\Exception $exception) {
3✔
192
            $this->io->error($exception->getMessage());
3✔
193
        }
194

195
        return null;
4✔
196
    }
197

198
    /**
199
     * @param list<Result\VersionBumpResult> $results
200
     */
201
    private function releaseVersion(array $results, string $rootPath, Config\ReleaseOptions $options, bool $dryRun): bool
2✔
202
    {
203
        $this->io->title('Release');
2✔
204

205
        try {
206
            $releaseResult = $this->releaser->release($results, $rootPath, $options, $dryRun);
2✔
207

208
            $this->decorateVersionReleaseResult($releaseResult);
1✔
209

210
            return true;
1✔
211
        } catch (Exception\Exception $exception) {
1✔
212
            $this->io->error($exception->getMessage());
1✔
NEW
213
        } catch (GitPhp\GitException $exception) {
×
NEW
214
            $this->io->error('Git error during release: '.$exception->getMessage());
×
215
        }
216

217
        return false;
1✔
218
    }
219

220
    /**
221
     * @param list<Result\VersionBumpResult> $results
222
     */
223
    private function decorateVersionBumpResults(array $results, string $rootPath): void
4✔
224
    {
225
        $titleDisplayed = false;
4✔
226

227
        foreach ($results as $result) {
4✔
228
            if (!$result->hasOperations()) {
4✔
229
                continue;
4✔
230
            }
231

232
            $path = $result->file()->path();
4✔
233

234
            if (Filesystem\Path::isAbsolute($path)) {
4✔
235
                $path = Filesystem\Path::makeRelative($path, $rootPath);
×
236
            }
237

238
            if (!$titleDisplayed) {
4✔
239
                $this->io->title('Bumped versions');
4✔
240
                $titleDisplayed = true;
4✔
241
            }
242

243
            $this->io->section($path);
4✔
244

245
            foreach ($result->groupedOperations() as $operations) {
4✔
246
                $operation = reset($operations);
4✔
247
                $numberOfOperations = count($operations);
4✔
248
                $message = match ($operation->state()) {
4✔
249
                    Enum\OperationState::Modified => sprintf(
4✔
250
                        '✅ Bumped version from "%s" to "%s"',
4✔
251
                        $operation->source()?->full(),
4✔
252
                        $operation->target()?->full(),
4✔
253
                    ),
4✔
254
                    Enum\OperationState::Skipped => '⏩ Skipped file due to unmodified contents',
4✔
255
                    Enum\OperationState::Unmatched => '❓ Unmatched file pattern: '.$operation->pattern()->original(),
4✔
256
                };
4✔
257

258
                if ($numberOfOperations > 1) {
4✔
259
                    $message .= sprintf(' (%dx)', $numberOfOperations);
4✔
260
                }
261

262
                $this->io->writeln($message);
4✔
263
            }
264
        }
265
    }
266

267
    private function decorateVersionReleaseResult(Result\VersionReleaseResult $result): void
1✔
268
    {
269
        $numberOfCommittedFiles = count($result->committedFiles());
1✔
270
        $releaseInformation = [
1✔
271
            sprintf('Added %d file%s.', $numberOfCommittedFiles, 1 !== $numberOfCommittedFiles ? 's' : ''),
1✔
272
            sprintf('Committed: <info>%s</info>', $result->commitMessage()),
1✔
273
            sprintf('Commit hash: %s', $result->commitId()),
1✔
274
            sprintf('Tagged: <info>%s</info>', $result->tagName()),
1✔
275
        ];
1✔
276

277
        if (null === $result->commitId()) {
1✔
278
            unset($releaseInformation[2]);
1✔
279
        }
280

281
        $this->io->listing($releaseInformation);
1✔
282
    }
283

284
    private function decorateMappingError(Valinor\Mapper\MappingError $error, string $configFile): void
1✔
285
    {
286
        $errorMessages = [];
1✔
287
        $errors = Valinor\Mapper\Tree\Message\Messages::flattenFromNode($error->node())->errors();
1✔
288

289
        $this->io->error(
1✔
290
            sprintf('The config file "%s" is invalid.', $configFile),
1✔
291
        );
1✔
292

293
        foreach ($errors as $propertyError) {
1✔
294
            $errorMessages[] = sprintf('%s: %s', $propertyError->node()->path(), $propertyError->toString());
1✔
295
        }
296

297
        $this->io->listing($errorMessages);
1✔
298
    }
299

300
    private function readConfigFileFromRootPackage(): ?string
9✔
301
    {
302
        if (method_exists($this, 'tryComposer')) {
9✔
303
            // Composer >= 2.3
304
            $composer = $this->tryComposer();
9✔
305
        } else {
306
            // Composer < 2.3
307
            $composer = $this->getComposer(false);
×
308
        }
309

310
        if (null === $composer) {
9✔
311
            return null;
9✔
312
        }
313

314
        $extra = $composer->getPackage()->getExtra();
1✔
315
        /* @phpstan-ignore offsetAccess.nonOffsetAccessible */
316
        $configFile = $extra['version-bumper']['config-file'] ?? null;
1✔
317

318
        if (is_string($configFile) && '' !== trim($configFile)) {
1✔
319
            return $configFile;
1✔
320
        }
321

322
        return null;
×
323
    }
324
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc