GitPolicyAnalyser   A
last analyzed

Complexity

Total Complexity 27

Size/Duplication

Total Lines 260
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 0
Metric Value
wmc 27
lcom 1
cbo 8
dl 0
loc 260
rs 10
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A doRun() 0 26 2
A analyseContext() 0 27 4
A getRefSpecificConfiguration() 0 9 3
B verifyPolicy() 0 38 7
A printMessages() 0 14 3
A loadConfig() 0 12 2
A prepEnvironment() 0 17 1
A out() 0 20 4
1
<?php
2
3
namespace GitPolicy\Application;
4
5
use Symfony\Component\Console\Application;
6
use Symfony\Component\Console\Input;
7
use Symfony\Component\Console\Input\InputDefinition;
8
use Symfony\Component\Console\Input\InputOption;
9
use Symfony\Component\Console\Input\InputInterface;
10
use Symfony\Component\Yaml\Parser;
11
use Symfony\Component\Console\Output\OutputInterface;
12
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
13
14
class GitPolicyAnalyser extends Application
15
{
16
    /**
17
     * @var array
18
     */
19
    protected $input;
20
21
    /**
22
     * @var OutputInterface
23
     */
24
    protected $output;
25
26
    /**
27
     * Name it.
28
     */
29
    public function __construct()
30
    {
31
        parent::__construct('GitPolicy hook analyser');
32
    }
33
34
    /**
35
     * GitPolicy' Brain.
36
     *
37
     * The following methods are a mix of functional patterns and OO patterns. It might take a bit to get the grasp
38
     * but once get the idea it will all be logical and show its immutable design.
39
     *
40
     * @param InputInterface  $input
41
     * @param OutputInterface $output
42
     *
43
     * @return int
44
     */
45
    public function doRun(InputInterface $input = null, OutputInterface $output = null)
46
    {
47
        // little prep, add some context to the set input: $this->input and "welcome" :)
48
        $this->prepEnvironment($input, $output);
0 ignored issues
show
Bug introduced by
It seems like $input defined by parameter $input on line 45 can be null; however, GitPolicy\Application\Gi...yser::prepEnvironment() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
Bug introduced by
It seems like $output defined by parameter $output on line 45 can be null; however, GitPolicy\Application\Gi...yser::prepEnvironment() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
49
        $this->analyseContext();
50
        $this->out('Checking GitPolicy', 'good');
51
52
        // get the right section of the config for this
53
        $config = $this->getRefSpecificConfiguration($this->loadConfig(), $this->input);
54
55
        // run the actual verification - if we are failing here - lets stop here.
56
        if ($this->verifyPolicy($config, $this->input)) {
57
            // print the messages and signal that we're done.
58
            $this->printMessages($config, $this->input);
59
            $this->out('Done :)', 'good');
60
61
            exit(0);
62
        }
63
64
        // I'm probably just too stupid to know how to exit a PHP application console in a git hook proper.
65
        $tempFile = fopen('./.tmp-endgp', 'w');
66
        fwrite($tempFile, 'failed :(');
67
        fclose($tempFile);
68
69
        exit(1);
70
    }
71
72
    /**
73
     * analyses the given attempt to push.
74
     */
75
    protected function analyseContext()
76
    {
77
        $this->input['context'] = [
78
            // type and the name of the remote ref as a structure for the following processes
79
            'ref_type' => (strpos($this->input['remote_ref'], 'refs/tags/') === 0) ? 'tag' : 'branch',
80
            'ref_name' => preg_replace('"refs/.+/"', '', $this->input['remote_ref']),
81
            'refs_different' => ($this->input['local_ref'] != $this->input['remote_ref']),
82
83
            // easy to match list of states
84
            'is' => [
85
                'tag' => (strpos($this->input['remote_ref'], 'refs/tags/') === 0),
86
                'branch' => (strpos($this->input['remote_ref'], 'refs/heads/') === 0),
87
                'create' => ($this->input['remote_sha'] == '0000000000000000000000000000000000000000'),
88
                'update' => (
89
                    $this->input['local_sha'] != '0000000000000000000000000000000000000000' &&
90
                    $this->input['remote_sha'] != '0000000000000000000000000000000000000000'
91
                ),
92
                'delete' => (
93
                    $this->input['local_ref'] == '(deleted)' ||
94
                    $this->input['local_sha'] == '0000000000000000000000000000000000000000'
95
                ),
96
            ],
97
        ];
98
99
        // @TODO add more extensive checks in here. For example go through the differences between the two hashs and
100
        //  verify all commit messages are fine by gitpolicy.
101
    }
102
103
    /**
104
     * Returns the specific section of the configuration for this push.
105
     *
106
     * E.g. if a tag is pushed/deleted returns the 'tag' section from the config.
107
     *
108
     * Fallback is always an empty array.
109
     *
110
     * @param array $config
111
     * @param array $push
112
     *
113
     * @return array
114
     */
115
    protected function getRefSpecificConfiguration(array $config, array $push)
116
    {
117
        return
118
            // no intention to process anything else than tags and branches for now ;)
119
            !(in_array($push['context']['ref_type'], ['tag', 'branch'])) ? [] :
120
121
            // get the right section of the config for this
122
            (array_key_exists($push['context']['ref_type'], $config)) ? $config[$push['context']['ref_type']] : [];
123
    }
124
125
    /**
126
     * verifies if any of the set policy parameters is/was violated.
127
     *
128
     * This method is used for some functional programming demonstration as well.
129
     *
130
     * @param array $config
131
     * @param array $push
132
     * @return boolean $passed
133
     */
134
    protected function verifyPolicy(array $config, array $push)
135
    {
136
        // We are merging the individual message parts together to get the complete array of messages
137
        $messages = implode("\n\n", array_merge(
138
            // Hint: A lot of the following comments are "written negated" compared to the actual
139
            // statement for better readability.
140
141
            // Are there any forbidden actions we should check?
142
            !array_key_exists('forbidden', $config) ? [] : array_intersect_key(
143
                // remove the elements which aren't applicable
144
                $config['forbidden'],
145
                array_filter($push['context']['is'], function ($state) { return $state; })
146
            ),
147
148
            // Should the name be validated?
149
            !(array_key_exists('name', $config) && isset($push['context']['ref_name'])) ? [] : array_merge(
150
                // simply check if the ref name is on the forbidden list
151
                isset($config['name']['forbidden'][$push['context']['ref_name']]) ?
152
                    [$config['name']['forbidden'][$push['context']['ref_name']]] : [],
153
154
                // check if the forbidden patterns brings any messages
155
                !isset($config['name']['forbidden_patterns']) ? [] : array_keys(array_filter(
156
                    array_flip($config['name']['forbidden_patterns']),
157
                    function ($pattern) use ($push) { return preg_match($pattern, $push['context']['ref_name']); }
158
                )),
159
160
                // do pretty much the same for the require pattern -
161
                //  but add the message if the pattern wasn't matched
162
                !isset($config['name']['required_patterns']) ? [] : array_keys(array_filter(
163
                    array_flip($config['name']['required_patterns']),
164
                    function ($pattern) use ($push) { return !preg_match($pattern, $push['context']['ref_name']); }
165
                ))
166
            )
167
        ));
168
169
        $this->out($messages, 'error');
170
        return (trim($messages) == '');
171
    }
172
173
    /**
174
     * prints notications (messages) after the push.
175
     *
176
     * Are there any messages which should be printed after the push has been confirmed to be accepted?
177
     *
178
     * This method is used for some functional programming demonstration as well.
179
     *
180
     * @param array $config
181
     * @param array $push
182
     */
183
    protected function printMessages(array $config, array $push)
184
    {
185
        // Pretty much the same again, just more wrapped into itself.
186
        $this->out(implode(
187
            "\n\n",
188
            !array_key_exists('after_push_messages', $config) ? [] : array_keys(array_filter(
189
                array_flip($config['after_push_messages']),
190
                function ($action) use ($push) {
191
                    // ensure we are displaying only the right messages for the context.
192
                    return isset($push['context']['is'][$action]) && $push['context']['is'][$action];
193
                }
194
            ))
195
        ), 'good');
196
    }
197
198
    /**
199
     * loads the configuration from the config.
200
     *
201
     * @param string $filename
202
     *
203
     * @return array
204
     *
205
     * @throws \Exception
206
     */
207
    protected function loadConfig($filename = '.gitpolicy.yml')
208
    {
209
        // parse the yml
210
        $yaml = new Parser();
211
212
        // check if the file exists
213
        if (!file_exists($filename)) {
214
            throw new \Exception($filename.' not found. Maybe you want to run the init command again?');
215
        }
216
217
        return $yaml->parse(file_get_contents($filename));
218
    }
219
220
    /**
221
     * Helps to set the environment for the application. Basically does these two things:.
222
     *
223
     *  * applies the input definition to the input and sets the result as property of the application.
224
     *  * prepares the outputinterface
225
     *
226
     * @param InputInterface  $input
227
     * @param OutputInterface $output
228
     */
229
    protected function prepEnvironment(InputInterface $input, OutputInterface $output)
230
    {
231
        $input->bind(new InputDefinition(array(
232
            new InputOption('local_ref', 'lref', InputOption::VALUE_REQUIRED, 'local ref'),
233
            new InputOption('local_sha', 'lsha', InputOption::VALUE_REQUIRED, 'local sha'),
234
            new InputOption('remote_ref', 'rref', InputOption::VALUE_REQUIRED, 'remote ref'),
235
            new InputOption('remote_sha', 'rsha', InputOption::VALUE_REQUIRED, 'remote sha'),
236
        )));
237
238
        $this->input = $input->getOptions();
239
240
        $output->getFormatter()->setStyle('error', new OutputFormatterStyle('white', 'red', array('bold')));
241
        $output->getFormatter()->setStyle('warning', new OutputFormatterStyle('black', 'yellow', array('bold')));
242
        $output->getFormatter()->setStyle('good', new OutputFormatterStyle('white', 'green', array('bold')));
243
244
        $this->output = $output;
245
    }
246
247
    /**
248
     * printing messages for lazy people like me.
249
     *
250
     * @param string $message
251
     * @param string $messageType
252
     */
253
    protected function out($message, $messageType = null)
254
    {
255
        // empty messages have no value for the user.
256
        if (trim($message) == '') {
257
            return;
258
        }
259
260
        // wrap message in tag to define the output
261
        if ($messageType != null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $messageType of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
262
            $message = "<{$messageType}>".trim($message)."</{$messageType}>";
263
        }
264
265
        // yay! print it :)
266
        $this->output->writeln($message."\n");
267
268
        // we really shouldn't continue if an error happens
269
        if ($messageType == 'error') {
270
            $this->output->writeln('<error>Stopping :/</error>');
271
        }
272
    }
273
}
274