BlockFixupAndSquashCommits::execute()   A
last analyzed

Complexity

Conditions 6
Paths 5

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 6

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 6
eloc 11
c 4
b 0
f 0
nc 5
nop 4
dl 0
loc 19
ccs 12
cts 12
cp 1
crap 6
rs 9.2222
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', []);
0 ignored issues
show
Documentation Bug introduced by
It seems like $options->get('protectedBranches', array()) can also be of type CaptainHook\App\Config\ProvidedDefault. However, the property $protectedBranches is declared as type string[]. Maybe add an additional type check?

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 the id property of an instance of the Account 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.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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