1
|
|
|
<?php declare(strict_types=1); |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of Biurad opensource projects. |
5
|
|
|
* |
6
|
|
|
* @copyright 2022 Biurad Group (https://biurad.com/) |
7
|
|
|
* @license https://opensource.org/licenses/BSD-3-Clause License |
8
|
|
|
* |
9
|
|
|
* For the full copyright and license information, please view the LICENSE |
10
|
|
|
* file that was distributed with this source code. |
11
|
|
|
*/ |
12
|
|
|
|
13
|
|
|
namespace Biurad\Monorepo\Worker; |
14
|
|
|
|
15
|
|
|
use Biurad\Git\Repository; |
16
|
|
|
use Biurad\Monorepo\{Monorepo, WorkerInterface, WorkflowCommand}; |
17
|
|
|
use Symfony\Component\Console\Input\{InputInterface, InputOption}; |
18
|
|
|
use Symfony\Component\Console\Style\SymfonyStyle; |
19
|
|
|
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; |
20
|
|
|
use Symfony\Component\Process\Process; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* A workflow worker for splitting commits to repositories. |
24
|
|
|
* |
25
|
|
|
* @author Divine Niiquaye Ibok <[email protected]> |
26
|
|
|
*/ |
27
|
|
|
class SplitCommitsWorker implements WorkerInterface |
28
|
|
|
{ |
29
|
|
|
private function __construct() |
30
|
|
|
{ |
31
|
|
|
} |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* {@inheritdoc} |
35
|
|
|
*/ |
36
|
|
|
public function getDescription(): string |
37
|
|
|
{ |
38
|
|
|
return 'Running repositories commits splitting'; |
39
|
|
|
} |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* {@inheritdoc} |
43
|
|
|
*/ |
44
|
|
|
public static function configure(WorkflowCommand $command): self |
45
|
|
|
{ |
46
|
|
|
$multi = InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL; |
47
|
|
|
$command->addOption('branch', 'b', $multi, 'Defaults to all branches that match the configured branch filter. (also accepts -b "*")', []); |
48
|
|
|
$command->addOption('no-branch', null, InputOption::VALUE_NONE, 'If set, no branches will be pushed.'); |
49
|
|
|
$command->addOption('release', 't', InputOption::VALUE_OPTIONAL, 'Release of new tag (accepted pattern: <tag>[=<branch>])'); |
50
|
|
|
|
51
|
|
|
return new self(); |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* {@inheritdoc} |
56
|
|
|
*/ |
57
|
|
|
public function work(Monorepo $repo, InputInterface $input, SymfonyStyle $output): int |
58
|
|
|
{ |
59
|
|
|
[$mainRepo, $branches] = [$repo->getRepository(), $input->getOption('branch')]; |
60
|
|
|
$currentBranch = $mainRepo->getBranch()->getName(); |
61
|
|
|
|
62
|
|
|
if (!\is_executable($split = __DIR__.'/../../bin/splitsh-lite')) { |
63
|
|
|
$mainRepo->run('update-index', ['--chmod=+x']); |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
if ('\\' === \DIRECTORY_SEPARATOR) { |
67
|
|
|
$output->error([ |
68
|
|
|
'The splitsh-lite command used to split commits to repositories', |
69
|
|
|
'is currently not supported on Windows.', |
70
|
|
|
'Kindly use Windows Subsystem for Linux (WSL) to run this command.', |
71
|
|
|
'Support for Windows is being worked on and will be available soon.', |
72
|
|
|
]); |
73
|
|
|
|
74
|
|
|
return WorkflowCommand::FAILURE; |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
if ($branches && '*' === $branches[0]) { |
78
|
|
|
$allBranches = true; |
79
|
|
|
|
80
|
|
|
if (\count($branches) > 1) { |
81
|
|
|
$output->writeln(\sprintf('<error>Expected "*" as the only value for option "--branch", got "%s"</error>', \implode(', ', $branches))); |
82
|
|
|
|
83
|
|
|
return WorkflowCommand::FAILURE; |
84
|
|
|
} |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
foreach ($mainRepo->getBranches() as $branch) { |
88
|
|
|
if (isset($allBranches) || (1 === \preg_match($repo->config['branch_filter'], $branch->getName()) && !$input->getOption('no-branch'))) { |
89
|
|
|
$branches[] = $branch->isRemote() ? \substr($branch->getName(), 7) : $branch->getName(); |
90
|
|
|
} |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
return $repo->resolveRepository( |
94
|
|
|
$output, |
95
|
|
|
static function (array $required) use ($input, $output, $currentBranch, $branches, $split, $repo, $mainRepo): int { |
96
|
|
|
[$url, $remote, $path, $clonePath] = $required; |
97
|
|
|
|
98
|
|
|
if (!\file_exists($mainRepo->getPath()."/$path")) { |
99
|
|
|
throw new InvalidOptionsException(\sprintf('The repo for "%s" path "%s" does not exist.', $remote, $path)); |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
foreach (\array_unique($branches) as $branch) { |
103
|
|
|
$output->writeln(\sprintf('<info>Splitting commits from branch %s into %s</info>', $branch, $url)); |
104
|
|
|
$verify = ['-1', '--format=%ad | %s [%an]', '--date=short']; |
105
|
|
|
$pushChanges = []; |
106
|
|
|
|
107
|
|
|
($s = Process::fromShellCommandline( |
108
|
|
|
"{$split} --prefix={$path} --origin=origin/{$branch} --target=".$target = "refs/splits/$remote", |
109
|
|
|
$mainRepo->getPath(), |
110
|
|
|
timeout: 1200 |
111
|
|
|
))->run(); |
112
|
|
|
|
113
|
|
|
if ($output->isVerbose()) { |
114
|
|
|
$output->writeln(\sprintf('<%s>[debug] Command "%s": %s</%1$s>', $s->isSuccessful() ? 'info' : 'error', $s->getCommandLine(), $s->getErrorOutput())); |
115
|
|
|
|
116
|
|
|
if (!$s->isSuccessful()) { |
117
|
|
|
continue; |
118
|
|
|
} |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
if ($mainRepo->run('log', [$branch, ...$verify], cwd: $clonePath) !== $mainRepo->run('log', [$target, ...$verify])) { |
122
|
|
|
$count = (int) \rtrim($mainRepo->run('rev-list', ['--count', $target]) ?? '0'); |
123
|
|
|
$updates = (int) \rtrim($mainRepo->run('rev-list', ['--count', $branch], cwd: $clonePath) ?? '0'); |
124
|
|
|
|
125
|
|
|
$output->writeln(\sprintf("<info>Target commit count: %d</info>", $count)); |
126
|
|
|
$output->writeln(\sprintf("<info>Source commit count: %d</info>", $updates)); |
127
|
|
|
|
128
|
|
|
if (($count = $updates > $count ? $updates - $count : $count - $updates) < 0) { |
129
|
|
|
continue; |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
$output->writeln(\sprintf('<info>Pushing (%d) commits from branch %s to %s</info>', $count, $branch, $url)); |
133
|
|
|
$mainRepo->runConcurrent(0 === $updates ? [ |
134
|
|
|
['push', $input->getOption('force') ? '-f' : '-q', $remote, "+$target:refs/heads/$branch"], |
135
|
|
|
['update-ref', '-d', $target], |
136
|
|
|
] : [ |
137
|
|
|
['checkout', '--orphan', "split-$remote"], |
138
|
|
|
['reset', '--hard'], |
139
|
|
|
['pull', $remote, $branch], |
140
|
|
|
['cherry-pick', ...\explode(' ', "$target~".\implode(" $target~", \array_reverse(\range(0, $count - 1))))], |
141
|
|
|
['push', $input->getOption('force') ? '-f' : '-q', $remote, "+refs/heads/split-$remote:$branch"], |
142
|
|
|
['checkout', $currentBranch], |
143
|
|
|
['branch', '-D', "split-$remote"], |
144
|
|
|
['update-ref', '-d', $target], |
145
|
|
|
]); |
146
|
|
|
|
147
|
|
|
if (!$input->getOption('no-push')) { |
148
|
|
|
$pushChanges[] = ['push', ...($input->getOption('force') ? ['-f'] : []), 'origin', $branch]; |
149
|
|
|
} |
150
|
|
|
} else { |
151
|
|
|
$output->writeln(\sprintf('<info>Nothing to commit; On branch %s, "%s/%1$s" is up to date</info>', $branch, $remote)); |
152
|
|
|
} |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
if ($tagged = $input->getOption('release')) { |
156
|
|
|
[$tagged, $repo] = [\explode('=', $tagged, 2), new Repository($clonePath, [], $repo->isDebug(), $repo->getLogger())]; |
157
|
|
|
|
158
|
|
|
if (!$repo->getBranch($rBranch = $tagged[1] ?? $currentBranch)) { |
159
|
|
|
$output->writeln(\sprintf('<error>Release Branch %s does not exist</error>', $rBranch)); |
160
|
|
|
} else { |
161
|
|
|
if ($repo->getBranch()->getName() !== $rBranch) { |
162
|
|
|
$repo->run('checkout', [$rBranch]); |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
$tags = '*' === $tagged[0] ? \explode("\n", $mainRepo->run('tag', ['--list', '--points-at', $rBranch]) ?? '') : [$tagged[0]]; |
166
|
|
|
$tagPushes = []; |
167
|
|
|
|
168
|
|
|
if ($repo->getBranch()->getName() !== $rBranch) { |
169
|
|
|
$tagPushes[] = ['checkout', $rBranch]; |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
foreach (\array_filter($tags) as $tag) { |
173
|
|
|
if (\str_starts_with($tag, $remote.'/')) { |
174
|
|
|
$tag = \substr($tag, \strlen($remote.'/')); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
if (!$repo->getTag($tag)) { |
178
|
|
|
$output->writeln(\sprintf('<info>Creating tag %s for repo %s</info>', $tagged[0], $remote)); |
179
|
|
|
$tagPushes[] = ['tag', $tagged[0], '-m', 'Release '.$tagged[0]]; |
180
|
|
|
$tagPushes[] = ['push', ...($input->getOption('force') ? ['origin', '--tags', '-f'] : ['origin', '--tags']), $rBranch]; |
181
|
|
|
|
182
|
|
|
if (!$input->getOption('no-push')) { |
183
|
|
|
$pushChanges[] = \end($tagPushes); |
184
|
|
|
} |
185
|
|
|
} |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
$repo->runConcurrent($tagPushes); |
189
|
|
|
} |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
if (!empty($pushChanges)) { |
193
|
|
|
$output->writeln(\sprintf('<info>Preparing to push changes to %s</info>', $url)); |
194
|
|
|
$mainRepo->runConcurrent($pushChanges, cwd: $clonePath); |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
return $mainRepo->getExitCode(); |
198
|
|
|
} |
199
|
|
|
); |
200
|
|
|
|
201
|
|
|
return $mainRepo->getExitCode(); |
|
|
|
|
202
|
|
|
} |
203
|
|
|
} |
204
|
|
|
|
This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.
Unreachable code is most often the result of
return
,die
orexit
statements that have been added for debug purposes.In the above example, the last
return false
will never be executed, because a return statement has already been met in every possible execution path.