Skip to content

Commit 9b00257

Browse files
Add issue key injection action
The new action extracts an issue key from the current branch name and inject it into the commit message. Available options: - regex #([A-Z]+\\-[0-9]+)#i - into body|subject - mode append|prepend - prefix ""
1 parent f5ad41f commit 9b00257

File tree

3 files changed

+342
-0
lines changed

3 files changed

+342
-0
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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 SebastianFeldmann\Git\CommitMessage;
22+
use SebastianFeldmann\Git\Repository;
23+
24+
/**
25+
* Class PrepareFromFile
26+
*
27+
* Example configuration:
28+
* {
29+
* "action": "\\CaptainHook\\App\\Hook\\Message\\Action\\InjectIssueKeyFromBranch"
30+
* "options": {
31+
* "regex": "#([A-Z]+\\-[0-9]+)#i",
32+
* "into": "body"
33+
* "mode": "append",
34+
* "prefix": "\\nissue: ",
35+
* "force": true
36+
* }
37+
* }
38+
*
39+
* The regex option needs group $1 (...) to be the issue key
40+
*
41+
* @package CaptainHook
42+
* @author Sebastian Feldmann <[email protected]>
43+
* @link https://github.com/captainhookphp/captainhook
44+
* @since Class available since Release 5.16.0
45+
*/
46+
class InjectIssueKeyFromBranch implements Action
47+
{
48+
/**
49+
* Execute the configured action
50+
*
51+
* @param \CaptainHook\App\Config $config
52+
* @param \CaptainHook\App\Console\IO $io
53+
* @param \SebastianFeldmann\Git\Repository $repository
54+
* @param \CaptainHook\App\Config\Action $action
55+
* @return void
56+
* @throws \Exception
57+
*/
58+
public function execute(Config $config, IO $io, Repository $repository, Config\Action $action): void
59+
{
60+
$branch = $repository->getInfoOperator()->getCurrentBranch();
61+
$options = $action->getOptions();
62+
$match = [];
63+
$pattern = $options->get('regex', '#([A-Z]+\-[0-9]+)#i');
64+
65+
// can we actually find an issue id?
66+
if (!preg_match($pattern, $branch, $match)) {
67+
if ($options->get('force', false)) {
68+
throw new ActionFailed('No issue key found in branch name');
69+
}
70+
return;
71+
}
72+
73+
$issueID = $match[1] ?? '';
74+
$msg = $repository->getCommitMsg();
75+
76+
// make sure the issue key is not already in our commit message
77+
if (stripos($msg->getRawContent(), $issueID) !== false) {
78+
return;
79+
}
80+
$repository->setCommitMsg($this->createNewCommitMessage($options, $msg, $issueID));
81+
}
82+
83+
/**
84+
* Will create the new commit message with the injected issue key
85+
*
86+
* @param \CaptainHook\App\Config\Options $options
87+
* @param \SebastianFeldmann\Git\CommitMessage $msg
88+
* @param string $issueID
89+
* @return \SebastianFeldmann\Git\CommitMessage
90+
*/
91+
private function createNewCommitMessage(Options $options, CommitMessage $msg, string $issueID): CommitMessage
92+
{
93+
// let's figure out where to put the issueID
94+
$target = $options->get('into', 'body');
95+
$mode = $options->get('mode', 'append');
96+
$prefix = $options->get('prefix', '');
97+
98+
// overwrite either subject or body
99+
$newMsgData = ['subject' => $msg->getSubject(), 'body' => $msg->getBody()];
100+
$newMsgData[$target] = $this->injectIssueId($issueID, $newMsgData[$target], $mode, $prefix);
101+
102+
return new CommitMessage(
103+
$newMsgData['subject'] . PHP_EOL . PHP_EOL . $newMsgData['body'],
104+
$msg->getCommentCharacter()
105+
);
106+
}
107+
108+
/**
109+
* Appends or prepends the issue id to the given message part
110+
*
111+
* @param string $issueID
112+
* @param string $msg
113+
* @param string $mode
114+
* @param string $prefix
115+
* @return string
116+
*/
117+
private function injectIssueId(string $issueID, string $msg, string $mode, string $prefix): string
118+
{
119+
return $mode === 'prepend' ? $prefix . $issueID . ' ' . $msg : $msg . $prefix . $issueID;
120+
}
121+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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\Message\Action;
13+
14+
use CaptainHook\App\Config\Mockery as ConfigMockery;
15+
use CaptainHook\App\Config\Options;
16+
use CaptainHook\App\Console\IO\Mockery as IOMockery;
17+
use CaptainHook\App\Exception\ActionFailed;
18+
use CaptainHook\App\Mockery as CHMockery;
19+
use CaptainHook\App\RepoMock;
20+
use SebastianFeldmann\Git\CommitMessage;
21+
use PHPUnit\Framework\TestCase;
22+
23+
class InjectIssueKeyFromBranchTest extends TestCase
24+
{
25+
use ConfigMockery;
26+
use CHMockery;
27+
use IOMockery;
28+
29+
/**
30+
* Tests InjectIssueKeyFromBranch::execute
31+
*
32+
* @throws \Exception
33+
*/
34+
public function testPrependSubject(): void
35+
{
36+
$repo = new RepoMock();
37+
$info = $this->createGitInfoOperator('5.0.0', 'freature/ABCD-12345-foo-bar-baz');
38+
39+
$repo->setCommitMsg(new CommitMessage('foo' . PHP_EOL . PHP_EOL . 'bar'));
40+
$repo->setInfoOperator($info);
41+
42+
$io = $this->createIOMock();
43+
$config = $this->createConfigMock();
44+
$action = $this->createActionConfigMock();
45+
$action->method('getOptions')->willReturn(new Options([
46+
'into' => 'subject',
47+
'mode' => 'prepend',
48+
]));
49+
50+
$hook = new InjectIssueKeyFromBranch();
51+
$hook->execute($config, $io, $repo, $action);
52+
53+
$this->assertEquals('ABCD-12345 foo', $repo->getCommitMsg()->getSubject());
54+
}
55+
56+
/**
57+
* Tests InjectIssueKeyFromBranch::execute
58+
*
59+
* @throws \Exception
60+
*/
61+
public function testAppendBodyWithPrefix(): void
62+
{
63+
$repo = new RepoMock();
64+
$info = $this->createGitInfoOperator('5.0.0', 'freature/ABCD-12345-foo-bar-baz');
65+
66+
$repo->setCommitMsg(new CommitMessage('foo' . PHP_EOL . PHP_EOL . 'bar'));
67+
$repo->setInfoOperator($info);
68+
69+
$io = $this->createIOMock();
70+
$config = $this->createConfigMock();
71+
$action = $this->createActionConfigMock();
72+
$action->method('getOptions')->willReturn(new Options([
73+
'into' => 'body',
74+
'mode' => 'append',
75+
'prefix' => PHP_EOL . PHP_EOL . 'issue: '
76+
]));
77+
78+
$hook = new InjectIssueKeyFromBranch();
79+
$hook->execute($config, $io, $repo, $action);
80+
81+
$this->assertEquals('bar' . PHP_EOL . PHP_EOL . 'issue: ABCD-12345', $repo->getCommitMsg()->getBody());
82+
}
83+
84+
/**
85+
* Tests InjectIssueKeyFromBranch::execute
86+
*
87+
* @throws \Exception
88+
*/
89+
public function testIgnoreIssueKeyNotFound(): void
90+
{
91+
$repo = new RepoMock();
92+
$info = $this->createGitInfoOperator('5.0.0', 'foo');
93+
94+
$repo->setCommitMsg(new CommitMessage('foo' . PHP_EOL . PHP_EOL . 'bar'));
95+
$repo->setInfoOperator($info);
96+
97+
$io = $this->createIOMock();
98+
$config = $this->createConfigMock();
99+
$action = $this->createActionConfigMock();
100+
$action->method('getOptions')->willReturn(new Options([
101+
'into' => 'body',
102+
'mode' => 'append',
103+
'prefix' => PHP_EOL . PHP_EOL . 'issue: '
104+
]));
105+
106+
$hook = new InjectIssueKeyFromBranch();
107+
$hook->execute($config, $io, $repo, $action);
108+
109+
$this->assertEquals('bar', $repo->getCommitMsg()->getBody());
110+
}
111+
112+
/**
113+
* Tests InjectIssueKeyFromBranch::execute
114+
*
115+
* @throws \Exception
116+
*/
117+
public function testFailIssueKeyNotFound(): void
118+
{
119+
$this->expectException(ActionFailed::class);
120+
121+
$repo = new RepoMock();
122+
$info = $this->createGitInfoOperator('5.0.0', 'foo');
123+
124+
$repo->setCommitMsg(new CommitMessage('foo' . PHP_EOL . PHP_EOL . 'bar'));
125+
$repo->setInfoOperator($info);
126+
127+
$io = $this->createIOMock();
128+
$config = $this->createConfigMock();
129+
$action = $this->createActionConfigMock();
130+
$action->method('getOptions')->willReturn(new Options([
131+
'force' => true,
132+
]));
133+
134+
$hook = new InjectIssueKeyFromBranch();
135+
$hook->execute($config, $io, $repo, $action);
136+
}
137+
138+
/**
139+
* Tests InjectIssueKeyFromBranch::execute
140+
*
141+
* @throws \Exception
142+
*/
143+
public function testIssueKeyAlreadyInMSG(): void
144+
{
145+
$repo = new RepoMock();
146+
$info = $this->createGitInfoOperator('5.0.0', 'freature/ABCD-12345-foo-bar-baz');
147+
148+
$repo->setCommitMsg(new CommitMessage('ABCD-12345 foo' . PHP_EOL . PHP_EOL . 'bar'));
149+
$repo->setInfoOperator($info);
150+
151+
$io = $this->createIOMock();
152+
$config = $this->createConfigMock();
153+
$action = $this->createActionConfigMock();
154+
$action->method('getOptions')->willReturn(new Options([
155+
'into' => 'subject',
156+
]));
157+
158+
$hook = new InjectIssueKeyFromBranch();
159+
$hook->execute($config, $io, $repo, $action);
160+
161+
$this->assertEquals('ABCD-12345 foo', $repo->getCommitMsg()->getSubject());
162+
}
163+
}

tests/unit/RepoMock.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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;
13+
14+
use SebastianFeldmann\Git\Operator\Info;
15+
use SebastianFeldmann\Git\Repository;
16+
17+
/**
18+
* Class to more closely mimic the repo behaviour
19+
*
20+
* Only the operators ar supposed to be replaced by mocks
21+
*/
22+
class RepoMock extends Repository
23+
{
24+
/**
25+
* Info Operator Mock
26+
*
27+
* @var \SebastianFeldmann\Git\Operator\Info
28+
*/
29+
private Info $infoOperator;
30+
31+
/**
32+
* Overwrite the original constructor to not do any validation at all
33+
*/
34+
public function __construct()
35+
{
36+
}
37+
38+
/**
39+
* Set info operator mock
40+
*
41+
* @param \SebastianFeldmann\Git\Operator\Info $op
42+
* @return void
43+
*/
44+
public function setInfoOperator(Info $op): void
45+
{
46+
$this->infoOperator = $op;
47+
}
48+
49+
/**
50+
* Return the operator mock
51+
*
52+
* @return \SebastianFeldmann\Git\Operator\Info
53+
*/
54+
public function getInfoOperator(): Info
55+
{
56+
return $this->infoOperator;
57+
}
58+
}

0 commit comments

Comments
 (0)