Passed
Push — main ( 25f4a8...799be8 )
by Sebastian
03:52
created

InjectIssueKeyFromBranch::execute()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 11
c 1
b 0
f 0
nc 5
nop 4
dl 0
loc 22
ccs 12
cts 12
cp 1
crap 4
rs 9.9
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\Hook\Message\Action;
15
16
use CaptainHook\App\Config;
17
use CaptainHook\App\Config\Options;
18
use CaptainHook\App\Console\IO;
19
use CaptainHook\App\Exception\ActionFailed;
20
use CaptainHook\App\Hook\Action;
21
use CaptainHook\App\Hook\Constrained;
22
use CaptainHook\App\Hook\Restriction;
23
use CaptainHook\App\Hooks;
24
use SebastianFeldmann\Git\CommitMessage;
25
use SebastianFeldmann\Git\Repository;
26
27
/**
28
 * Class PrepareFromFile
29
 *
30
 * Example configuration:
31
 *
32
 * <code>
33
 * {
34
 *   "action": "CaptainHook.Message.InjectIssueKeyFromBranch",
35
 *   "options": {
36
 *     "regex": "#([A-Z]+\\-[0-9]+)#i",
37
 *     "into": "body|subject",
38
 *     "mode": "append|prepend",
39
 *     "prefix": "",
40
 *     "suffix": "",
41
 *     "modify": "lowercase|uppercase",
42
 *     "force": true
43
 *   }
44
 * }
45
 * </code>
46
 *
47
 * The regex option needs group $1 (...) to be the issue key
48
 *
49
 * @package CaptainHook
50
 * @author  Sebastian Feldmann <[email protected]>
51
 * @link    https://github.com/captainhook-git/captainhook
52
 * @since   Class available since Release 5.16.0
53
 */
54
class InjectIssueKeyFromBranch implements Action, Constrained
55
{
56
    /**
57
     * Mode constants
58
     */
59
    private const MODE_APPEND  = 'append';
60
    private const MODE_PREPEND = 'prepend';
61
62
    /**
63
     * Target constants
64
     */
65
    private const TARGET_SUBJECT = 'subject';
66
    private const TARGET_BODY    = 'body';
67
68
    /**
69
     * Returns a list of applicable hooks
70
     *
71
     * @return \CaptainHook\App\Hook\Restriction
72
     */
73 1
    public static function getRestriction(): Restriction
74
    {
75 1
        return Restriction::fromArray([Hooks::PREPARE_COMMIT_MSG, Hooks::COMMIT_MSG]);
76
    }
77
78
    /**
79
     * Execute the configured action
80
     *
81
     * @param  \CaptainHook\App\Config           $config
82
     * @param  \CaptainHook\App\Console\IO       $io
83
     * @param  \SebastianFeldmann\Git\Repository $repository
84
     * @param  \CaptainHook\App\Config\Action    $action
85
     * @return void
86
     * @throws \Exception
87
     */
88 10
    public function execute(Config $config, IO $io, Repository $repository, Config\Action $action): void
89
    {
90 10
        $options = $action->getOptions();
91 10
        $branch  = $repository->getInfoOperator()->getCurrentBranch();
92 10
        $pattern = $options->get('regex', '#([A-Z]+\-[0-9]+)#i');
93 10
        $issueID = $this->extractIssueId($branch, $pattern);
94
95
        // did we actually find an issue id?
96 10
        if (empty($issueID)) {
97 2
            if ($options->get('force', false)) {
98 1
                throw new ActionFailed('No issue key found in branch name');
99
            }
100
        }
101
102 9
        $msg = $repository->getCommitMsg();
103
104
        // make sure the issue key is not already in the commit message
105 9
        if (stripos($msg->getSubject() . $msg->getContent(), $issueID) !== false) {
106 2
            return;
107
        }
108
109 7
        $repository->setCommitMsg($this->createNewCommitMessage($options, $msg, $issueID));
110
    }
111
112
    /**
113
     * Extract issue id from branch name
114
     *
115
     * @param  string $branch
116
     * @param  string $pattern
117
     * @return string
118
     */
119 10
    private function extractIssueId(string $branch, string $pattern): string
120
    {
121 10
        $match = [];
122
        // can we actually find an issue id?
123 10
        if (!preg_match($pattern, $branch, $match)) {
124 2
            return '';
125
        }
126 8
        return $match[1] ?? '';
127
    }
128
129
    /**
130
     * Will create the new commit message with the injected issue key
131
     *
132
     * @param  \CaptainHook\App\Config\Options      $options
133
     * @param  \SebastianFeldmann\Git\CommitMessage $msg
134
     * @param  string                               $issueID
135
     * @return \SebastianFeldmann\Git\CommitMessage
136
     */
137 7
    private function createNewCommitMessage(Options $options, CommitMessage $msg, string $issueID): CommitMessage
138
    {
139
        // let's figure out where to put the issueID
140 7
        $target = $options->get('into', self::TARGET_BODY);
141 7
        $mode   = $options->get('mode', self::MODE_APPEND);
142
143 7
        $issueID = $this->modify($issueID, (string) $options->get('modify', ''));
144
145
        // overwrite either subject or body
146 7
        $pattern          = $this->handlePrefixAndSuffix($mode, $options);
147 7
        $msgData          = [self::TARGET_SUBJECT => $msg->getSubject(), self::TARGET_BODY => $msg->getBody()];
148 7
        $msgData[$target] = $this->injectIssueId($issueID, $msgData[$target], $mode, $pattern);
149
150
        // combine all the parts to create a new commit message
151 7
        $msgText = $msgData[self::TARGET_SUBJECT] . PHP_EOL
152 7
                   . PHP_EOL
153 7
                   . $msgData[self::TARGET_BODY] . PHP_EOL
154 7
                   . $msg->getComments();
155
156 7
        return new CommitMessage($msgText, $msg->getCommentCharacter());
157
    }
158
159
    /**
160
     * Appends or prepends the issue id to the given message part
161
     *
162
     * @param  string $issueID
163
     * @param  string $msg
164
     * @param  string $mode
165
     * @param  string $pattern
166
     * @return string
167
     */
168 7
    private function injectIssueId(string $issueID, string $msg, string $mode, string $pattern): string
169
    {
170 7
        $issueID = preg_replace_callback(
171 7
            '/\$(\d+)/',
172 7
            function ($matches) use ($issueID) {
173 7
                return $matches[1] === '1' ? $issueID : '';
174 7
            },
175 7
            $pattern
176 7
        );
177
178 7
        return ltrim($mode === self::MODE_PREPEND ? $issueID . $msg : $msg . $issueID);
179
    }
180
181
    /**
182
     * Make sure the prefix and suffix options still works even if they should not be used anymore
183
     *
184
     * @param  string                          $mode
185
     * @param  \CaptainHook\App\Config\Options $options
186
     * @return string
187
     */
188 7
    private function handlePrefixAndSuffix(string $mode, Options $options): string
189
    {
190 7
        $space   = '';
191 7
        $pattern = $options->get('pattern', '');
192 7
        if (empty($pattern)) {
193 6
            $space   = ' ';
194 6
            $pattern = '$1';
195
        }
196
        // depending on the mode use a whitespace as prefix or suffix
197 7
        $prefix = $options->get('prefix', $mode == 'append' ? $space : '');
198 7
        $suffix = $options->get('suffix', $mode == 'prepend' ? $space : '');
199 7
        return $prefix . $pattern . $suffix;
200
    }
201
202
    /**
203
     * Run modifier
204
     *
205
     * @param  string $issueID
206
     * @param  string $modify
207
     * @return string
208
     */
209 7
    private function modify(string $issueID, string $modify): string
210
    {
211 7
        if (empty($modify)) {
212 6
            return $issueID;
213
        }
214
215 1
        $availableModifiers = [
216 1
            'lowercase' => fn (string $str): string => strtolower($str),
217 1
            'uppercase' => fn (string $str): string => strtoupper($str),
218 1
        ];
219
220 1
        if (array_key_exists($modify, $availableModifiers)) {
221 1
            $issueID = $availableModifiers[$modify]($issueID);
222
        }
223 1
        return $issueID;
224
    }
225
}
226