Skip to content

Commit 0449347

Browse files
authored
Update PR title on label change (#97)
* Update PR title on label change * Updated alias list * CS fixes * Separate component labels * cs * Test fix * Fixed the alias issue
1 parent 3207135 commit 0449347

13 files changed

+915
-69
lines changed

config/services.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ parameters:
22
repositories:
33
symfony/symfony:
44
subscribers:
5+
- 'App\Subscriber\AutoUpdateTitleWithLabelSubscriber'
56
- 'App\Subscriber\StatusChangeByCommentSubscriber'
67
- 'App\Subscriber\StatusChangeByReviewSubscriber'
78
- 'App\Subscriber\NeedsReviewNewPRSubscriber'
@@ -13,6 +14,7 @@ parameters:
1314

1415
symfony/symfony-docs:
1516
subscribers:
17+
- 'App\Subscriber\AutoUpdateTitleWithLabelSubscriber'
1618
- 'App\Subscriber\StatusChangeByCommentSubscriber'
1719
- 'App\Subscriber\StatusChangeOnPushSubscriber'
1820
- 'App\Subscriber\StatusChangeByReviewSubscriber'
@@ -25,6 +27,7 @@ parameters:
2527
# used in a functional test
2628
carsonbot-playground/symfony:
2729
subscribers:
30+
- 'App\Subscriber\AutoUpdateTitleWithLabelSubscriber'
2831
- 'App\Subscriber\StatusChangeByCommentSubscriber'
2932
- 'App\Subscriber\StatusChangeOnPushSubscriber'
3033
- 'App\Subscriber\StatusChangeByReviewSubscriber'
@@ -55,6 +58,10 @@ services:
5558
factory: ['@Github\Client', api]
5659
arguments: [issue]
5760

61+
Github\Api\PullRequest:
62+
factory: ['@Github\Client', api]
63+
arguments: [pullRequest]
64+
5865
Github\Api\Issue\Labels:
5966
factory: ['@Github\Api\Issue', labels]
6067

src/Issues/GitHub/CachedLabelsApi.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,34 @@ public function getAllLabelsForRepository(Repository $repository): array
112112
$key = 'labels'.sha1($repository->getFullName());
113113

114114
return $this->cache->get($key, function (ItemInterface $item) use ($repository) {
115-
$labels = $this->labelsApi->all($repository->getVendor(), $repository->getName());
115+
$labels = $this->labelsApi->all($repository->getVendor(), $repository->getName()) ?? [];
116116
$item->expiresAfter(36000);
117117

118118
return array_column($labels, 'name');
119119
});
120120
}
121121

122+
/**
123+
* @return string[]
124+
*/
125+
public function getComponentLabelsForRepository(Repository $repository): array
126+
{
127+
$key = 'component_labels'.sha1($repository->getFullName());
128+
129+
return $this->cache->get($key, function (ItemInterface $item) use ($repository) {
130+
$labels = $this->labelsApi->all($repository->getVendor(), $repository->getName()) ?? [];
131+
$item->expiresAfter(36000);
132+
$componentLabels = [];
133+
foreach ($labels as $label) {
134+
if ('dddddd' === $label['color']) {
135+
$componentLabels[] = $label['name'];
136+
}
137+
}
138+
139+
return $componentLabels;
140+
});
141+
}
142+
122143
private function getCacheKey($issueNumber, Repository $repository)
123144
{
124145
return sprintf('%s_%s_%s', $issueNumber, $repository->getVendor(), $repository->getName());

src/Service/LabelNameExtractor.php

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
namespace App\Service;
4+
5+
use App\Issues\GitHub\CachedLabelsApi;
6+
use App\Repository\Repository;
7+
8+
/**
9+
* Extract label name from a PR/Issue.
10+
*
11+
* @author Tobias Nyholm <[email protected]>
12+
*/
13+
class LabelNameExtractor
14+
{
15+
private $labelsApi;
16+
17+
private static $labelAliases = [
18+
'bridge\doctrine' => 'DoctrineBridge',
19+
'bridge/doctrine' => 'DoctrineBridge',
20+
'bridge\monolog' => 'MonologBridge',
21+
'bridge/monolog' => 'MonologBridge',
22+
'bridge\phpunit' => 'PhpUnitBridge',
23+
'bridge/phpunit' => 'PhpUnitBridge',
24+
'bridge\proxymanager' => 'ProxyManagerBridge',
25+
'bridge/proxymanager' => 'ProxyManagerBridge',
26+
'bridge\twig' => 'TwigBridge',
27+
'bridge/twig' => 'TwigBridge',
28+
'di' => 'DependencyInjection',
29+
'fwb' => 'FrameworkBundle',
30+
'profiler' => 'WebProfilerBundle',
31+
'router' => 'Routing',
32+
'translation' => 'Translator',
33+
'wdt' => 'WebProfilerBundle',
34+
];
35+
36+
public function __construct(CachedLabelsApi $labelsApi)
37+
{
38+
$this->labelsApi = $labelsApi;
39+
}
40+
41+
/**
42+
* Get labels from title string.
43+
* Example title: "[PropertyAccess] [RFC] [WIP] Allow custom methods on property accesses".
44+
*/
45+
public function extractLabels($title, Repository $repository)
46+
{
47+
$labels = [];
48+
if (preg_match_all('/\[(?P<labels>.+)\]/U', $title, $matches)) {
49+
$validLabels = $this->getLabels($repository);
50+
foreach ($matches['labels'] as $label) {
51+
$label = $this->fixLabelName($label);
52+
53+
// check case-insensitively, but the apply the correctly-cased label
54+
if (isset($validLabels[strtolower($label)])) {
55+
$labels[] = $validLabels[strtolower($label)];
56+
}
57+
}
58+
}
59+
60+
return $labels;
61+
}
62+
63+
public function getAliasesForLabel($label)
64+
{
65+
foreach (self::$labelAliases as $alias => $name) {
66+
if ($name === $label) {
67+
yield $alias;
68+
}
69+
}
70+
}
71+
72+
/**
73+
* Creates a key=>val array, but the key is lowercased.
74+
*
75+
* @return array
76+
*/
77+
private function getLabels(Repository $repository)
78+
{
79+
$allLabels = $this->labelsApi->getAllLabelsForRepository($repository);
80+
$closure = function ($s) {
81+
return strtolower($s);
82+
};
83+
84+
return array_combine(array_map($closure, $allLabels), $allLabels);
85+
}
86+
87+
/**
88+
* It fixes common misspellings and aliases commonly used for label names
89+
* (e.g. DI -> DependencyInjection).
90+
*/
91+
private function fixLabelName($label)
92+
{
93+
$labelAliases = self::$labelAliases;
94+
95+
if (isset($labelAliases[strtolower($label)])) {
96+
return $labelAliases[strtolower($label)];
97+
}
98+
99+
return $label;
100+
}
101+
}

src/Subscriber/AutoLabelFromContentSubscriber.php

Lines changed: 7 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use App\Event\GitHubEvent;
66
use App\GitHubEvents;
77
use App\Issues\GitHub\CachedLabelsApi;
8-
use App\Repository\Repository;
8+
use App\Service\LabelNameExtractor;
99
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
1010

1111
/**
@@ -15,19 +15,12 @@ class AutoLabelFromContentSubscriber implements EventSubscriberInterface
1515
{
1616
private $labelsApi;
1717

18-
private static $labelAliases = [
19-
'di' => 'DependencyInjection',
20-
'bridge\twig' => 'TwigBridge',
21-
'router' => 'Routing',
22-
'translation' => 'Translator',
23-
'twig bridge' => 'TwigBridge',
24-
'wdt' => 'WebProfilerBundle',
25-
'profiler' => 'WebProfilerBundle',
26-
];
27-
28-
public function __construct(CachedLabelsApi $labelsApi)
18+
private $labelExtractor;
19+
20+
public function __construct(CachedLabelsApi $labelsApi, LabelNameExtractor $labelExtractor)
2921
{
3022
$this->labelsApi = $labelsApi;
23+
$this->labelExtractor = $labelExtractor;
3124
}
3225

3326
public function onPullRequest(GitHubEvent $event)
@@ -44,7 +37,7 @@ public function onPullRequest(GitHubEvent $event)
4437
$prLabels = [];
4538

4639
// the PR title usually contains one or more labels
47-
foreach ($this->extractLabels($prTitle, $repository) as $label) {
40+
foreach ($this->labelExtractor->extractLabels($prTitle, $repository) as $label) {
4841
$prLabels[] = $label;
4942
}
5043

@@ -83,7 +76,7 @@ public function onIssue(GitHubEvent $event)
8376
$labels = [];
8477

8578
// the issue title usually contains one or more labels
86-
foreach ($this->extractLabels($prTitle, $repository) as $label) {
79+
foreach ($this->labelExtractor->extractLabels($prTitle, $repository) as $label) {
8780
$labels[] = $label;
8881
}
8982

@@ -95,49 +88,6 @@ public function onIssue(GitHubEvent $event)
9588
]);
9689
}
9790

98-
private function extractLabels($title, Repository $repository)
99-
{
100-
$labels = [];
101-
102-
// e.g. "[PropertyAccess] [RFC] [WIP] Allow custom methods on property accesses"
103-
if (preg_match_all('/\[(?P<labels>.+)\]/U', $title, $matches)) {
104-
// creates a key=>val array, but the key is lowercased
105-
$allLabels = $this->labelsApi->getAllLabelsForRepository($repository);
106-
$validLabels = array_combine(
107-
array_map(function ($s) {
108-
return strtolower($s);
109-
}, $allLabels),
110-
$allLabels
111-
);
112-
113-
foreach ($matches['labels'] as $label) {
114-
$label = $this->fixLabelName($label);
115-
116-
// check case-insensitively, but the apply the correctly-cased label
117-
if (isset($validLabels[strtolower($label)])) {
118-
$labels[] = $validLabels[strtolower($label)];
119-
}
120-
}
121-
}
122-
123-
return $labels;
124-
}
125-
126-
/**
127-
* It fixes common misspellings and aliases commonly used for label names
128-
* (e.g. DI -> DependencyInjection).
129-
*/
130-
private function fixLabelName($label)
131-
{
132-
$labelAliases = self::$labelAliases;
133-
134-
if (isset($labelAliases[strtolower($label)])) {
135-
return $labelAliases[strtolower($label)];
136-
}
137-
138-
return $label;
139-
}
140-
14191
public static function getSubscribedEvents()
14292
{
14393
return [
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
namespace App\Subscriber;
4+
5+
use App\Event\GitHubEvent;
6+
use App\GitHubEvents;
7+
use App\Issues\GitHub\CachedLabelsApi;
8+
use App\Service\LabelNameExtractor;
9+
use Github\Api\PullRequest;
10+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
11+
12+
/**
13+
* When a label changed, then update PR title.
14+
*
15+
* @author Tobias Nyholm <[email protected]>
16+
*/
17+
class AutoUpdateTitleWithLabelSubscriber implements EventSubscriberInterface
18+
{
19+
private $labelsApi;
20+
21+
private $labelExtractor;
22+
private $pullRequestApi;
23+
24+
public function __construct(CachedLabelsApi $labelsApi, LabelNameExtractor $labelExtractor, PullRequest $pullRequestApi)
25+
{
26+
$this->labelsApi = $labelsApi;
27+
$this->labelExtractor = $labelExtractor;
28+
$this->pullRequestApi = $pullRequestApi;
29+
}
30+
31+
public function onPullRequest(GitHubEvent $event)
32+
{
33+
$data = $event->getData();
34+
if ('labeled' !== $action = $data['action']) {
35+
return;
36+
}
37+
if (!isset($data['pull_request'])) {
38+
// Only update PullRequests
39+
return;
40+
}
41+
42+
$originalTitle = $prTitle = $data['pull_request']['title'];
43+
$validLabels = [];
44+
foreach ($data['pull_request']['labels'] as $label) {
45+
if ('dddddd' === $label['color']) {
46+
$validLabels[] = $label['name'];
47+
// Remove label name from title
48+
$prTitle = str_replace('['.$label['name'].']', '', $prTitle);
49+
50+
// Remove label aliases from title
51+
foreach ($this->labelExtractor->getAliasesForLabel($label['name']) as $alias) {
52+
$prTitle = str_replace('['.$alias.']', '', $prTitle);
53+
}
54+
}
55+
}
56+
57+
sort($validLabels);
58+
$prPrefix = '';
59+
foreach ($validLabels as $label) {
60+
$prPrefix .= '['.$label.']';
61+
}
62+
63+
// Add back labels
64+
$prTitle = $prPrefix.' '.$prTitle;
65+
if ($originalTitle === $prTitle) {
66+
return;
67+
}
68+
69+
$repository = $event->getRepository();
70+
$prNumber = $data['number'];
71+
$this->pullRequestApi->update($repository->getVendor(), $repository->getName(), $prNumber, ['title' => $prTitle]);
72+
$event->setResponseData([
73+
'pull_request' => $prNumber,
74+
'new_title' => $prTitle,
75+
]);
76+
}
77+
78+
public static function getSubscribedEvents()
79+
{
80+
return [
81+
GitHubEvents::PULL_REQUEST => 'onPullRequest',
82+
];
83+
}
84+
}

tests/Controller/WebhookControllerTest.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,12 @@ public function getTests()
5656
'On pull request opened' => [
5757
'pull_request',
5858
'pull_request.opened.json',
59-
['pull_request' => 3, 'status_change' => 'needs_review', 'pr_labels' => ['Bug']],
59+
['pull_request' => 3, 'status_change' => 'needs_review', 'pr_labels' => ['Console', 'Bug']],
60+
],
61+
'On pull request labeled' => [
62+
'pull_request',
63+
'pull_request.labeled.json',
64+
['pull_request' => 3, 'new_title' => '[Messenger] Readme update'],
6065
],
6166
'On pull request opened with target branch' => [
6267
'pull_request',

0 commit comments

Comments
 (0)