Passed
Push — main ( 6e1fca...be5608 )
by Sebastian
03:35
created

hasFoundRangesToCheck()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.0312

Importance

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