Passed
Push — main ( c1e8d7...13f864 )
by Sebastian
03:58
created

Installer::shouldRun()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 5
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 9
ccs 6
cts 6
cp 1
crap 3
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
declare(strict_types=1);
13
14
namespace CaptainHook\App\Runner;
15
16
use CaptainHook\App\Config;
17
use CaptainHook\App\Console\IO;
18
use CaptainHook\App\Console\IOUtil;
19
use CaptainHook\App\Hook\Template;
20
use CaptainHook\App\Storage\File;
21
use RuntimeException;
22
use SebastianFeldmann\Camino\Check;
23
use SebastianFeldmann\Git\Repository;
24
25
/**
26
 * Class Installer
27
 *
28
 * @package CaptainHook
29
 * @author  Sebastian Feldmann <[email protected]>
30
 * @link    https://github.com/captainhook-git/captainhook
31
 * @since   Class available since Release 0.9.0
32
 */
33
class Installer extends Files
34
{
35
    /**
36
     * Don't overwrite existing hooks
37
     *
38
     * @var bool
39
     */
40
    private bool $skipExisting = false;
41
42
    /**
43
     * Install only enabled hooks
44
     *
45
     * @var bool
46
     */
47
    private bool $onlyEnabled = false;
48
49
    /**
50
     * Hook template
51
     *
52
     * @var \CaptainHook\App\Hook\Template
53
     */
54
    private Template $template;
55
56
    /**
57
     * HookHandler constructor.
58
     *
59
     * @param \CaptainHook\App\Console\IO       $io
60
     * @param \CaptainHook\App\Config           $config
61
     * @param \SebastianFeldmann\Git\Repository $repository
62
     * @param \CaptainHook\App\Hook\Template    $template
63
     */
64 30
    public function __construct(IO $io, Config $config, Repository $repository, Template $template)
65
    {
66 30
        $this->template = $template;
67 30
        parent::__construct($io, $config, $repository);
68
    }
69
70
    /**
71
     * @param  bool $skip
72
     * @return \CaptainHook\App\Runner\Installer
73
     */
74 12
    public function setSkipExisting(bool $skip): Installer
75
    {
76 12
        if ($skip && !empty($this->moveExistingTo)) {
77 1
            throw new RuntimeException('choose --move-existing-to or --skip-existing');
78
        }
79 11
        $this->skipExisting = $skip;
80 11
        return $this;
81
    }
82
83
    /**
84
     * Set the path where the current hooks should be moved to
85
     *
86
     * @param  string $backup
87
     * @return static
88
     */
89 15
    public function setMoveExistingTo(string $backup): static
90
    {
91 15
        if (!empty($backup) && $this->skipExisting) {
92 1
            throw new RuntimeException('choose --skip-existing or --move-existing-to');
93
        }
94 14
        return parent::setMoveExistingTo($backup);
0 ignored issues
show
Bug Best Practice introduced by
The expression return parent::setMoveExistingTo($backup) returns the type CaptainHook\App\Runner\Files which includes types incompatible with the type-hinted return CaptainHook\App\Runner\Installer.
Loading history...
95
    }
96
97
    /**
98
     * @param bool $onlyEnabled
99
     * @return \CaptainHook\App\Runner\Installer
100
     */
101 11
    public function setOnlyEnabled(bool $onlyEnabled): Installer
102
    {
103 11
        if ($onlyEnabled && !empty($this->hooksToHandle)) {
104 2
            throw new RuntimeException('choose --only-enabled or specific hooks');
105
        }
106
107 9
        $this->onlyEnabled = $onlyEnabled;
108 9
        return $this;
109
    }
110
111
    /**
112
     * Hook setter
113
     *
114
     * @param  string $hook
115
     * @return \CaptainHook\App\Runner\Installer
116
     * @throws \CaptainHook\App\Exception\InvalidHookName
117
     */
118 27
    public function setHook(string $hook): Installer
119
    {
120 27
        if (empty($hook)) {
121 6
            return $this;
122
        }
123
124 21
        if ($this->onlyEnabled) {
125 1
            throw new RuntimeException('choose --only-enabled or specific hooks');
126
        }
127
128 20
        return parent::setHook($hook);
0 ignored issues
show
Bug Best Practice introduced by
The expression return parent::setHook($hook) returns the type CaptainHook\App\Runner\Files which includes types incompatible with the type-hinted return CaptainHook\App\Runner\Installer.
Loading history...
129
    }
130
131
    /**
132
     * Execute installation
133
     *
134
     * @return void
135
     */
136 18
    public function run(): void
137
    {
138 18
        if (!$this->shouldRun()) {
139 1
            return;
140
        }
141 17
        foreach ($this->getHooksToInstall() as $hook => $ask) {
142 17
            $this->installHook($hook, ($ask && !$this->force));
143
        }
144
    }
145
146
    /**
147
     * Return list of hooks to install
148
     *
149
     * <code>
150
     * [
151
     *   string    => bool
152
     *   HOOK_NAME => ASK_USER_TO_CONFIRM_INSTALL
153
     * ]
154
     * </code>
155
     *
156
     * @return array<string, bool>
157
     */
158 17
    public function getHooksToInstall(): array
159
    {
160 17
        $hooks = $this->getHooksToHandle();
161
        // if only enabled hooks should be installed, remove disabled ones from the $hooks array
162 17
        if ($this->onlyEnabled) {
163 5
            $hooks = array_filter(
164 5
                $hooks,
165 5
                fn(string $key): bool => $this->config->isHookEnabled($key),
166 5
                ARRAY_FILTER_USE_KEY
167 5
            );
168
        }
169
        // make sure to ask for every remaining hook if it should be installed
170 17
        return $hooks;
171
    }
172
173
    /**
174
     * Install given hook
175
     *
176
     * @param string $hook
177
     * @param bool   $ask
178
     */
179 17
    private function installHook(string $hook, bool $ask): void
180
    {
181 17
        if ($this->shouldHookBeSkipped($hook)) {
182 1
            $hint = $this->io->isDebug() ? ', remove the --skip-existing option to overwrite.' : '';
183 1
            $this->io->write(
184 1
                IOUtil::PREFIX_FAIL . ' <comment>' . $hook . '</comment> exists' . $hint,
185 1
                true,
186 1
                IO::VERBOSE
187 1
            );
188 1
            return;
189
        }
190
191 16
        $doIt = true;
192 16
        if ($ask) {
193 1
            $answer = $this->io->ask('Install <comment>' . $hook . '</comment> hook? <comment>[Y,n]</comment> ', 'y');
194 1
            $doIt   = IOUtil::answerToBool($answer);
195
        }
196
197 16
        if ($doIt) {
198 15
            if ($this->shouldHookBeMoved()) {
199 4
                $this->backupHook($hook);
200
            }
201 14
            $this->writeHookFile($hook);
202
        }
203
    }
204
205
    /**
206
     * Check if the hook is installed and should be skipped
207
     *
208
     * @param  string $hook
209
     * @return bool
210
     */
211 17
    private function shouldHookBeSkipped(string $hook): bool
212
    {
213 17
        return $this->skipExisting && $this->repository->hookExists($hook);
214
    }
215
216
    /**
217
     * Write given hook to .git/hooks directory
218
     *
219
     * @param  string $hook
220
     * @return void
221
     */
222 14
    private function writeHookFile(string $hook): void
223
    {
224 14
        $hooksDir = $this->repository->getHooksDir();
225 14
        $hookFile = $hooksDir . DIRECTORY_SEPARATOR . $hook;
226 14
        $doIt     = true;
227
228
        // if a hook is configured and no force option is set,
229
        // ask the user if overwriting the hook is ok
230 14
        if ($this->needConfirmation($hook)) {
231 1
            $ans  = $this->io->ask(
232 1
                'The <comment>' . $hook . '</comment> hook exists! Overwrite? <comment>[y,N]</comment> ',
233 1
                'n'
234 1
            );
235 1
            $doIt = IOUtil::answerToBool($ans);
236
        }
237
238 14
        if ($doIt) {
239 13
            $code = $this->getHookSourceCode($hook);
240 13
            $file = new File($hookFile);
241 13
            $this->checkForBrokenSymlink($file);
242 13
            $file->write($code);
243 13
            chmod($hookFile, 0755);
244 13
            $this->io->write(IOUtil::PREFIX_OK . ' <comment>' . $hook . '</comment> installed');
245 13
            return;
246
        }
247 1
        $this->io->write(IOUtil::PREFIX_FAIL . ' <comment>' . $hook . '</comment> skipped');
248
    }
249
250
    /**
251
     * Return the source code for a given hook script
252
     *
253
     * @param  string $hook
254
     * @return string
255
     */
256 13
    private function getHookSourceCode(string $hook): string
257
    {
258 13
        return $this->template->getCode($hook);
259
    }
260
261
    /**
262
     * Checks if the provided file is a broken symbolic link
263
     *
264
     * @param  File $file The File object representing the file.
265
     * @return void
266
     * @throws RuntimeException If the file is determined to be a broken symbolic link.
267
     */
268 16
    protected function checkForBrokenSymlink(File $file): void
269
    {
270 16
        if ($file->isLink()) {
271 3
            $target = $file->linkTarget();
272 3
            if (!Check::isAbsolutePath($target)) {
273 1
                $target = dirname($file->getPath()) . DIRECTORY_SEPARATOR . $target;
274
            }
275 3
            if (!is_dir(dirname($target))) {
276 2
                throw new RuntimeException(
277 2
                    'The hook at \'' . $file->getPath() . '\' is a broken symbolic link. ' . PHP_EOL .
278 2
                    'Please remove the symbolic link and try again.'
279 2
                );
280
            }
281
        }
282
    }
283
284
    /**
285
     * Check for problems
286
     *
287
     * @return bool
288
     */
289 18
    private function shouldRun(): bool
290
    {
291 18
        $hooksDir = $this->repository->getHooksDir();
292
        // check nix and win systems black holes
293 18
        if ($hooksDir === '/dev/null' || $hooksDir === 'NUL') {
294 1
            $this->io->write('<fg=red>can\'t install hooks into hooksPath: /dev/null</>');
295 1
            return false;
296
        }
297 17
        return true;
298
    }
299
}
300