Completed
Push — master ( 444b50...ece018 )
by Gábor
03:00
created

ReleaseCommand   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 177
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 177
rs 10
c 0
b 0
f 0
wmc 26

8 Methods

Rating   Name   Duplication   Size   Complexity  
B interact() 0 30 6
A assertGitIsUpToDate() 0 15 4
A configure() 0 10 1
A assertGitHasChangesSince() 0 4 2
B initialize() 0 26 3
A assertGitCurrentBranch() 0 13 4
B execute() 0 33 4
A assertGitRepository() 0 4 2
1
<?php
2
3
/*
4
 * This file is part of the egabor/composer-release-plugin package.
5
 *
6
 * (c) Gábor Egyed <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace egabor\Composer\ReleasePlugin\Command;
13
14
use Composer\Command\BaseCommand;
15
use Composer\Util\Git as GitUtil;
16
use egabor\Composer\ReleasePlugin\Config;
17
use egabor\Composer\ReleasePlugin\ReleaseManager;
18
use egabor\Composer\ReleasePlugin\Util\Git;
19
use egabor\Composer\ReleasePlugin\Version;
20
use egabor\Composer\ReleasePlugin\VersionManipulator;
21
use Symfony\Component\Console\Helper\FormatterHelper;
22
use Symfony\Component\Console\Input\InputArgument;
23
use Symfony\Component\Console\Input\InputInterface;
24
use Symfony\Component\Console\Input\InputOption;
25
use Symfony\Component\Console\Output\OutputInterface;
26
27
class ReleaseCommand extends BaseCommand
28
{
29
    /**
30
     * @var Git
31
     */
32
    private $git;
33
34
    /**
35
     * @var ReleaseManager
36
     */
37
    private $releaseManager;
38
39
    protected function configure()
40
    {
41
        $this
42
            ->setName('release')
43
            ->setDescription('Creates a new tagged release.')
44
            ->addArgument('version', InputArgument::REQUIRED, 'Version to release.')
45
            ->addOption('message', 'm', InputOption::VALUE_REQUIRED, 'Message for the annotated tag.', 'Release %version%')
46
            ->addOption('no-push', 'np', InputOption::VALUE_NONE, 'Not push the new release tag.')
47
            ->addOption('latest-release', 'l', InputOption::VALUE_REQUIRED, 'Latest release version.')
48
            ->setHelp(<<<EOT
49
This command assists you to tag a new release for a package. It tries
50
to protect you from common mistakes and provide some shortcuts.
51
52
To create a new major|minor|patch|stable|alpha|beta|rc release:
53
54
    <comment>%command.full_name% major</comment>
55
EOT
56
            )
57
        ;
58
    }
59
60
    protected function initialize(InputInterface $input, OutputInterface $output)
61
    {
62
        GitUtil::cleanEnv();
63
64
        $io = $this->getIO();
65
        $this->git = Git::create($io);
66
        $config = new Config($this->getComposer()->getPackage());
67
68
        $this->assertGitRepository();
69
        $this->assertGitCurrentBranch($config);
70
        $this->assertGitIsUpToDate();
71
72
        $io->writeError(sprintf('Using the <info>%s</info> branch as the release branch.', $config->getReleaseBranch()));
73
74
        $this->releaseManager = new ReleaseManager($config, $this->git);
75
76
        if (null !== $input->getOption('latest-release')) {
77
            return;
78
        }
79
80
        if (null !== $latestReleaseVersion = $this->releaseManager->getLatestReleaseVersion()) {
81
            $io->writeError('Latest release version: <info>'.$latestReleaseVersion.'</info>');
82
            $input->setOption('latest-release', $latestReleaseVersion);
83
            $this->assertGitHasChangesSince($latestReleaseVersion);
84
        } else {
85
            $io->writeError('<error>Latest release version can not be guessed or seems invalid.</error>');
86
        }
87
    }
88
89
    protected function interact(InputInterface $input, OutputInterface $output)
90
    {
91
        $io = $this->getIO();
92
93
        if (null === $input->getOption('latest-release')) {
94
            $answer = $io->askAndValidate('Please provide the latest released version [<comment>0.0.0</comment>]: ', function ($version) {
95
                if (null === $version) {
96
                    throw new \InvalidArgumentException('Version can not be empty.');
97
                }
98
99
                Version::fromString($version);
100
101
                return $version;
102
            }, 3, '0.0.0');
103
            $input->setOption('latest-release', $answer);
104
        }
105
106
        if (null === $input->getArgument('version')) {
107
            $answer = $io->askAndValidate('Please provide a version to release (eg. 1.0.0 or major): ', function ($version) {
108
                if (null === $version) {
109
                    throw new \InvalidArgumentException('Version can not be empty.');
110
                }
111
112
                if (!VersionManipulator::supportsLevel($version)) {
113
                    Version::fromString($version);
114
                }
115
116
                return $version;
117
            });
118
            $input->setArgument('version', $answer);
119
        }
120
    }
121
122
    protected function execute(InputInterface $input, OutputInterface $output)
123
    {
124
        $io = $this->getIO();
125
        /** @var FormatterHelper $formatter */
126
        $formatter = $this->getHelperSet()->get('formatter');
127
128
        $futureVersion = $input->getArgument('version');
129
        $latestReleaseVersion = Version::fromString($input->getOption('latest-release'));
130
131
        $futureVersion = $this->releaseManager->gatFutureVersion($futureVersion, $latestReleaseVersion);
132
133
        $doRelease = true;
134
        if ($input->isInteractive()) {
135
            $doRelease = $io->askConfirmation('<info>You are about to release version "'.$futureVersion.'". Do you want to continue?</info> [<comment>Y,n</comment>]? ', true);
136
        }
137
138
        if ($doRelease) {
139
            if ($input->getOption('no-push')) {
140
                $this->releaseManager->release($futureVersion, $input->getOption('message'));
141
            } else {
142
                $this->releaseManager->releaseAndPush($futureVersion, $input->getOption('message'));
143
            }
144
145
            $io->write([
146
                '',
147
                $formatter->formatBlock('Version "'.$futureVersion.'" has been released.', 'bg=green;fg=black', true),
148
                '',
149
            ]);
150
        } else {
151
            $io->write([
152
                '',
153
                $formatter->formatBlock('Aborted.', 'bg=red;fg=white', true),
154
                '',
155
            ]);
156
        }
157
    }
158
159
    private function assertGitRepository()
160
    {
161
        if (!is_dir('.git')) {
162
            throw new \RuntimeException('The .git directory is missing from "'.getcwd().'". Only git repositories are supported.');
163
        }
164
    }
165
166
    private function assertGitCurrentBranch(Config $config)
167
    {
168
        $branches = $this->git->getBranches();
169
        if (!count($branches)) {
170
            throw new \LogicException('The repository does not have any branches with commits. Please commit your work before release.');
171
        }
172
173
        if (!array_key_exists($config->getReleaseBranch(), $branches)) {
174
            throw new \LogicException('The release branch "'.$config->getReleaseBranch().'" does not exists. Please create and switch to the release branch.');
175
        }
176
177
        if ($this->git->getCurrentBranch() !== $config->getReleaseBranch()) {
178
            throw new \LogicException('Please switch to the release branch "'.$config->getReleaseBranch().'".');
179
        }
180
    }
181
182
    private function assertGitIsUpToDate()
183
    {
184
        if (!$this->git->hasUpstream()) {
185
            throw new \LogicException('The current branch does not have a remote tracking branch. Please setup one before continue.');
186
        }
187
188
        $this->git->fetch();
189
190
        $status = $this->git->getRemoteStatus();
191
        if (Git::STATUS_NEED_TO_PULL === $status) {
192
            throw new \LogicException('The current branch does not not in sync with the remote tracking branch. Please pull before continue.');
193
        }
194
195
        if (Git::STATUS_DIVERGED === $status) {
196
            throw new \LogicException('The current branch is diverged from its remote tracking branch. Please rebase or merge before continue.');
197
        }
198
    }
199
200
    private function assertGitHasChangesSince($rev)
201
    {
202
        if (1 > $this->git->countCommitsSince($rev)) {
203
            throw new \LogicException('There are no committed changes since the last release. Please commit your changes.');
204
        }
205
    }
206
}
207