1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* This file is part of CaptainHook |
5
|
|
|
* |
6
|
|
|
* (c) Sebastian Feldmann <[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 CaptainHook\App\Hook\Branch\Action; |
13
|
|
|
|
14
|
|
|
use CaptainHook\App\Config; |
15
|
|
|
use CaptainHook\App\Console\IO; |
16
|
|
|
use CaptainHook\App\Console\IOUtil; |
17
|
|
|
use CaptainHook\App\Exception\ActionFailed; |
18
|
|
|
use CaptainHook\App\Git\Range\Detector\PrePush; |
19
|
|
|
use CaptainHook\App\Hook\Action; |
20
|
|
|
use CaptainHook\App\Hook\Restriction; |
21
|
|
|
use CaptainHook\App\Hooks; |
22
|
|
|
use SebastianFeldmann\Git\Repository; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* Class BlockFixupAndSquashCommits |
26
|
|
|
* |
27
|
|
|
* This action blocks pushes that contain fixup! or squash! commits. |
28
|
|
|
* Just as a security layer, so you are not pushing stuff you wanted to autosquash. |
29
|
|
|
* |
30
|
|
|
* Configure like this: |
31
|
|
|
* |
32
|
|
|
* { |
33
|
|
|
* "action": "\\CaptainHook\\App\\Hook\\Branch\\Action\\BlockFixupAndSquashCommits", |
34
|
|
|
* "options": { |
35
|
|
|
* "blockSquashCommits": true, |
36
|
|
|
* "blockFixupCommits": true, |
37
|
|
|
* "protectedBranches": ["main", "master", "integration"] |
38
|
|
|
* }, |
39
|
|
|
* "conditions": [] |
40
|
|
|
* } |
41
|
|
|
* |
42
|
|
|
* @package CaptainHook |
43
|
|
|
* @author Sebastian Feldmann <[email protected]> |
44
|
|
|
* @link https://github.com/captainhook-git/captainhook |
45
|
|
|
* @since Class available since Release 5.11.0 |
46
|
|
|
*/ |
47
|
|
|
class BlockFixupAndSquashCommits implements Action |
48
|
|
|
{ |
49
|
|
|
/** |
50
|
|
|
* Should fixup! commits be blocked |
51
|
|
|
* |
52
|
|
|
* @var bool |
53
|
|
|
*/ |
54
|
|
|
private bool $blockFixupCommits = true; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Should squash! commits be blocked |
58
|
|
|
* |
59
|
|
|
* @var bool |
60
|
|
|
*/ |
61
|
|
|
private bool $blockSquashCommits = true; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* List of protected branches |
65
|
|
|
* |
66
|
|
|
* If not specified all branches are protected |
67
|
|
|
* |
68
|
|
|
* @var array<string> |
69
|
|
|
*/ |
70
|
|
|
private array $protectedBranches; |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* Return hook restriction |
74
|
|
|
* |
75
|
|
|
* @return \CaptainHook\App\Hook\Restriction |
76
|
|
|
*/ |
77
|
1 |
|
public static function getRestriction(): Restriction |
78
|
|
|
{ |
79
|
1 |
|
return Restriction::fromArray([Hooks::PRE_PUSH]); |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* Execute the BlockFixupAndSquashCommits action |
84
|
|
|
* |
85
|
|
|
* @param \CaptainHook\App\Config $config |
86
|
|
|
* @param \CaptainHook\App\Console\IO $io |
87
|
|
|
* @param \SebastianFeldmann\Git\Repository $repository |
88
|
|
|
* @param \CaptainHook\App\Config\Action $action |
89
|
|
|
* @return void |
90
|
|
|
* @throws \Exception |
91
|
|
|
*/ |
92
|
7 |
|
public function execute(Config $config, IO $io, Repository $repository, Config\Action $action): void |
93
|
|
|
{ |
94
|
7 |
|
$rangeDetector = new PrePush(); |
95
|
7 |
|
$rangesToPush = $rangeDetector->getRanges($io); |
96
|
|
|
|
97
|
7 |
|
if (!$this->hasFoundRangesToCheck($rangesToPush)) { |
98
|
2 |
|
return; |
99
|
|
|
} |
100
|
|
|
|
101
|
5 |
|
$this->handleOptions($action->getOptions()); |
102
|
|
|
|
103
|
5 |
|
foreach ($rangesToPush as $range) { |
104
|
5 |
|
if (!empty($this->protectedBranches) && !in_array($range->from()->branch(), $this->protectedBranches)) { |
105
|
1 |
|
return; |
106
|
|
|
} |
107
|
4 |
|
$commits = $this->getBlockedCommits($io, $repository, $range->from()->id(), $range->to()->id()); |
108
|
|
|
|
109
|
4 |
|
if (count($commits) > 0) { |
110
|
3 |
|
$this->handleFailure($commits, $range->from()->branch()); |
111
|
|
|
} |
112
|
|
|
} |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
/** |
116
|
|
|
* Check if fixup or squash should be blocked |
117
|
|
|
* |
118
|
|
|
* @param \CaptainHook\App\Config\Options $options |
119
|
|
|
* @return void |
120
|
|
|
*/ |
121
|
5 |
|
private function handleOptions(Config\Options $options): void |
122
|
|
|
{ |
123
|
5 |
|
$this->blockSquashCommits = (bool) $options->get('blockSquashCommits', true); |
124
|
5 |
|
$this->blockFixupCommits = (bool) $options->get('blockFixupCommits', true); |
125
|
5 |
|
$this->protectedBranches = $options->get('protectedBranches', []); |
|
|
|
|
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* Returns a list of commits that should be blocked |
130
|
|
|
* |
131
|
|
|
* @param \CaptainHook\App\Console\IO $io |
132
|
|
|
* @param \SebastianFeldmann\Git\Repository $repository |
133
|
|
|
* @param string $remoteHash |
134
|
|
|
* @param string $localHash |
135
|
|
|
* @return array<\SebastianFeldmann\Git\Log\Commit> |
136
|
|
|
* @throws \Exception |
137
|
|
|
*/ |
138
|
4 |
|
private function getBlockedCommits(IO $io, Repository $repository, string $remoteHash, string $localHash): array |
139
|
|
|
{ |
140
|
4 |
|
$typesToCheck = $this->getTypesToBlock(); |
141
|
4 |
|
$blocked = []; |
142
|
4 |
|
foreach ($repository->getLogOperator()->getCommitsBetween($remoteHash, $localHash) as $commit) { |
143
|
4 |
|
$prefix = IOUtil::PREFIX_OK; |
144
|
4 |
|
if ($this->hasToBeBlocked($commit->getSubject(), $typesToCheck)) { |
145
|
3 |
|
$prefix = IOUtil::PREFIX_FAIL; |
146
|
3 |
|
$blocked[] = $commit; |
147
|
|
|
} |
148
|
4 |
|
$io->write( |
149
|
4 |
|
' ' . $prefix . ' ' . $commit->getHash() . ' ' . $commit->getSubject(), |
150
|
4 |
|
true, |
151
|
4 |
|
IO::VERBOSE |
152
|
4 |
|
); |
153
|
|
|
} |
154
|
4 |
|
return $blocked; |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* Returns a list of strings to look for in commit messages |
159
|
|
|
* |
160
|
|
|
* Will most likely return ['fixup!', 'squash!'] |
161
|
|
|
* |
162
|
|
|
* @return array<string> |
163
|
|
|
*/ |
164
|
4 |
|
private function getTypesToBlock(): array |
165
|
|
|
{ |
166
|
4 |
|
$strings = []; |
167
|
4 |
|
if ($this->blockFixupCommits) { |
168
|
4 |
|
$strings[] = 'fixup!'; |
169
|
|
|
} |
170
|
4 |
|
if ($this->blockSquashCommits) { |
171
|
4 |
|
$strings[] = 'squash!'; |
172
|
|
|
} |
173
|
4 |
|
return $strings; |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* Checks if the commit message starts with any of the given strings |
178
|
|
|
* |
179
|
|
|
* @param string $message |
180
|
|
|
* @param array<string> $typesToCheck |
181
|
|
|
* @return bool |
182
|
|
|
*/ |
183
|
4 |
|
private function hasToBeBlocked(string $message, array $typesToCheck): bool |
184
|
|
|
{ |
185
|
4 |
|
foreach ($typesToCheck as $type) { |
186
|
4 |
|
if (str_starts_with($message, $type)) { |
187
|
3 |
|
return true; |
188
|
|
|
} |
189
|
|
|
} |
190
|
4 |
|
return false; |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* Generate a helpful error message and throw the exception |
195
|
|
|
* |
196
|
|
|
* @param \SebastianFeldmann\Git\Log\Commit[] $commits |
197
|
|
|
* @param string $branch |
198
|
|
|
* @return void |
199
|
|
|
* @throws \CaptainHook\App\Exception\ActionFailed |
200
|
|
|
*/ |
201
|
3 |
|
private function handleFailure(array $commits, string $branch): void |
202
|
|
|
{ |
203
|
3 |
|
$out = []; |
204
|
3 |
|
foreach ($commits as $commit) { |
205
|
3 |
|
$out[] = ' - ' . $commit->getHash() . ' ' . $commit->getSubject(); |
206
|
|
|
} |
207
|
3 |
|
throw new ActionFailed( |
208
|
3 |
|
'You are prohibited to push the following commits:' . PHP_EOL |
209
|
3 |
|
. ' --[ ' . $branch . ' ]-- ' . PHP_EOL |
210
|
3 |
|
. PHP_EOL |
211
|
3 |
|
. implode(PHP_EOL, $out) |
212
|
3 |
|
); |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* Checks if we found valid ranges to check |
217
|
|
|
* |
218
|
|
|
* @param array<\CaptainHook\App\Git\Range\PrePush> $rangesToPush |
219
|
|
|
* @return bool |
220
|
|
|
*/ |
221
|
7 |
|
private function hasFoundRangesToCheck(array $rangesToPush): bool |
222
|
|
|
{ |
223
|
7 |
|
if (empty($rangesToPush)) { |
224
|
1 |
|
return false; |
225
|
|
|
} |
226
|
6 |
|
if ($rangesToPush[0]->from()->isZeroRev() || $rangesToPush[0]->to()->isZeroRev()) { |
227
|
1 |
|
return false; |
228
|
|
|
} |
229
|
5 |
|
return true; |
230
|
|
|
} |
231
|
|
|
} |
232
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.