Skip to content

Commit b57d964

Browse files
Bug 2010638 - Add Review Helper extension for AI-assisted code review (#71)
1 parent 06f3f95 commit b57d964

File tree

7 files changed

+328
-0
lines changed

7 files changed

+328
-0
lines changed

moz-extensions.conf.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
'LandoLinkEventListener',
99
'NewChangesLinkEventListener',
1010
'RiskAnalyzerEventListener',
11+
'ReviewHelperEventListener',
1112
)
1213
);

moz-extensions/src/__phutil_library_map__.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@
7373
'PhabricatorEmailConfigOptions' => 'email/config/PhabricatorEmailConfigOptions.php',
7474
'PhabricatorFeedIDQuery' => 'feed/query/PhabricatorFeedIDQuery.php',
7575
'PhabricatorLandoConfigOptions' => 'lando/config/PhabricatorLandoConfigOptions.php',
76+
'PhabricatorReviewHelperApplication' => 'reviewhelper/application/PhabricatorReviewHelperApplication.php',
77+
'PhabricatorReviewHelperConfigOptions' => 'reviewhelper/config/PhabricatorReviewHelperConfigOptions.php',
78+
'ReviewHelperEventListener' => 'reviewhelper/events/ReviewHelperEventListener.php',
79+
'ReviewHelperRequestController' => 'reviewhelper/controller/ReviewHelperRequestController.php',
80+
'ReviewHelperServiceException' => 'reviewhelper/exception/ReviewHelperServiceException.php',
7681
'PhabricatorReviewer' => 'email/adapter/PhabricatorReviewer.php',
7782
'PhabricatorStory' => 'email/adapter/PhabricatorStory.php',
7883
'PhabricatorStoryBuilder' => 'email/adapter/PhabricatorStoryBuilder.php',
@@ -160,6 +165,11 @@
160165
'PhabricatorEmailConfigOptions' => 'PhabricatorApplicationConfigOptions',
161166
'PhabricatorFeedIDQuery' => 'PhabricatorFeedQuery',
162167
'PhabricatorLandoConfigOptions' => 'PhabricatorApplicationConfigOptions',
168+
'PhabricatorReviewHelperApplication' => 'PhabricatorApplication',
169+
'PhabricatorReviewHelperConfigOptions' => 'PhabricatorApplicationConfigOptions',
170+
'ReviewHelperEventListener' => 'PhabricatorEventListener',
171+
'ReviewHelperRequestController' => 'PhabricatorController',
172+
'ReviewHelperServiceException' => 'Exception',
163173
'PhutilBMOAuthAdapter' => 'PhutilOAuthAuthAdapter',
164174
'PolicyQueryConduitAPIMethod' => 'ConduitAPIMethod',
165175
'SecureEmailRevisionAbandoned' => 'SecureEmailBody',
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
// This Source Code Form is subject to the terms of the Mozilla Public
3+
// License, v. 2.0. If a copy of the MPL was not distributed with this
4+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
6+
final class PhabricatorReviewHelperApplication extends PhabricatorApplication {
7+
8+
public function getName() {
9+
return pht('Review Helper');
10+
}
11+
12+
public function getShortDescription() {
13+
return pht('AI-Assisted Code Review');
14+
}
15+
16+
public function getBaseURI() {
17+
return '/reviewhelper/';
18+
}
19+
20+
public function getIcon() {
21+
return 'fa-magic';
22+
}
23+
24+
public function isLaunchable() {
25+
return false;
26+
}
27+
28+
public function getRoutes() {
29+
return array(
30+
'/reviewhelper/' => array(
31+
'request/(?P<revisionID>[1-9]\d*)/' => 'ReviewHelperRequestController',
32+
),
33+
);
34+
}
35+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
// This Source Code Form is subject to the terms of the Mozilla Public
3+
// License, v. 2.0. If a copy of the MPL was not distributed with this
4+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
6+
final class PhabricatorReviewHelperConfigOptions
7+
extends PhabricatorApplicationConfigOptions {
8+
9+
public function getName() {
10+
return pht('Review Helper');
11+
}
12+
13+
public function getDescription() {
14+
return pht('Configure AI Review Helper settings.');
15+
}
16+
17+
public function getIcon() {
18+
return 'fa-magic';
19+
}
20+
21+
public function getGroup() {
22+
return 'apps';
23+
}
24+
25+
public function getOptions() {
26+
return array(
27+
$this->newOption(
28+
'reviewhelper.url',
29+
'string',
30+
''
31+
)
32+
->setDescription(pht('Full URL for the AI review service endpoint. ' .
33+
'Set to an empty string to disable the feature.')),
34+
$this->newOption(
35+
'reviewhelper.auth-key',
36+
'string',
37+
''
38+
)
39+
->setDescription(pht('Authentication key for the AI review service. ' .
40+
'This will be sent as a Bearer token in the Authorization header.')),
41+
$this->newOption(
42+
'reviewhelper.timeout',
43+
'int',
44+
30
45+
)
46+
->setDescription(pht('Request timeout in seconds for the AI review service.')),
47+
);
48+
}
49+
50+
protected function didValidateOption(PhabricatorConfigOption $option, $value) {
51+
$key = $option->getKey();
52+
if ($key === 'reviewhelper.url') {
53+
$this->validateServiceURL($value);
54+
}
55+
}
56+
57+
private function validateServiceURL($value) {
58+
if ($value === '' || $value === null) {
59+
return;
60+
}
61+
62+
try {
63+
PhabricatorEnv::requireValidRemoteURIForFetch(
64+
$value,
65+
array('https')
66+
);
67+
} catch (Exception $ex) {
68+
throw new PhabricatorConfigValidationException(
69+
pht(
70+
'The Review Helper URL "%s" is not valid: %s',
71+
$value,
72+
$ex->getMessage()
73+
)
74+
);
75+
}
76+
77+
$auth_key = PhabricatorEnv::getEnvConfig('reviewhelper.auth-key');
78+
if ($auth_key === '' || $auth_key === null) {
79+
throw new PhabricatorConfigValidationException(
80+
pht(
81+
'The Review Helper authentication key must be set before a Review Helper URL is configured.'
82+
)
83+
);
84+
}
85+
}
86+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
// This Source Code Form is subject to the terms of the Mozilla Public
3+
// License, v. 2.0. If a copy of the MPL was not distributed with this
4+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
6+
final class ReviewHelperRequestController extends PhabricatorController {
7+
8+
public function handleRequest(AphrontRequest $request) {
9+
$viewer = $request->getViewer();
10+
$revision_id = $request->getURIData('revisionID');
11+
12+
$revision = id(new DifferentialRevisionQuery())
13+
->setViewer($viewer)
14+
->withIDs(array($revision_id))
15+
->needDiffIDs(true)
16+
->executeOne();
17+
18+
if (!$revision) {
19+
return new Aphront404Response();
20+
}
21+
22+
$revision_uri = '/D' . $revision->getID();
23+
24+
$status = $revision->getStatusObject();
25+
if ($status->isClosedStatus() && !$request->isFormPost()) {
26+
$warning_box = id(new PHUIInfoView())
27+
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
28+
->setTitle(pht('Revision is ') . $status->getDisplayName())
29+
->appendChild(
30+
pht('This revision is closed. Requesting a review for a closed ' .
31+
'revision may not be what you intended.')
32+
);
33+
return $this->newDialog()
34+
->setTitle(pht('Review Helper'))
35+
->appendChild($warning_box)
36+
->appendParagraph(pht('Are you sure you want to proceed?'))
37+
->addSubmitButton(pht('Request Review'))
38+
->addCancelButton($revision_uri);
39+
}
40+
41+
try {
42+
$data = $this->requestReview($viewer, $revision);
43+
} catch (ReviewHelperServiceException $ex) {
44+
MozLogger::log(
45+
'Review Helper request failed',
46+
'reviewhelper.request.error',
47+
array('Fields' => array(
48+
'revision_id' => $revision_id,
49+
'error' => $ex->getMessage(),
50+
))
51+
);
52+
return $this->newDialog()
53+
->setTitle(pht('Review Helper'))
54+
->setErrors(array($ex->getMessage()))
55+
->addCancelButton($revision_uri);
56+
}
57+
58+
return $this->newDialog()
59+
->setTitle(pht('Review Helper'))
60+
->appendParagraph($data['message'])
61+
->addCancelButton($revision_uri);
62+
}
63+
64+
private function requestReview(
65+
PhabricatorUser $viewer,
66+
DifferentialRevision $revision
67+
) {
68+
$service_url = PhabricatorEnv::getEnvConfig('reviewhelper.url');
69+
$auth_key = PhabricatorEnv::getEnvConfig('reviewhelper.auth-key');
70+
$timeout = PhabricatorEnv::getEnvConfig('reviewhelper.timeout');
71+
72+
$payload = array(
73+
'revision_id' => $revision->getID(),
74+
'diff_id' => max($revision->getDiffIDs()),
75+
'user_id' => $viewer->getID(),
76+
'user_name' => $viewer->getUsername(),
77+
);
78+
79+
$future = id(new HTTPSFuture($service_url))
80+
->setMethod('POST')
81+
->addHeader('Content-Type', 'application/json')
82+
->addHeader('Authorization', 'Bearer ' . $auth_key)
83+
->addHeader('User-Agent', 'Phabricator')
84+
->addHeader('Origin', rtrim(PhabricatorEnv::getAnyBaseURI(), '/'))
85+
->setTimeout($timeout)
86+
->setData(phutil_json_encode($payload));
87+
88+
try {
89+
list($status, $body) = $future->resolve();
90+
} catch (HTTPFutureResponseStatus $ex) {
91+
throw new ReviewHelperServiceException(
92+
pht('The AI review service encountered a connection or unexpected response error (%s).', $ex->getStatusCode())
93+
);
94+
}
95+
96+
if ($status->isTimeout()) {
97+
throw new ReviewHelperServiceException(
98+
pht('The AI review service request timed out. Please try again later.')
99+
);
100+
}
101+
102+
if ($status->isError()) {
103+
throw new ReviewHelperServiceException(
104+
pht('The AI review service returned an HTTP error response (%s).', $status->getStatusCode())
105+
);
106+
}
107+
108+
try {
109+
$data = phutil_json_decode($body);
110+
} catch (PhutilJSONParserException $ex) {
111+
throw new ReviewHelperServiceException(
112+
pht('The AI review service returned malformed JSON.')
113+
);
114+
}
115+
116+
if (!is_array($data) || !array_key_exists('message', $data)) {
117+
throw new ReviewHelperServiceException(
118+
pht('The AI review service returned an unexpected or malformed response.')
119+
);
120+
}
121+
122+
return $data;
123+
}
124+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
// This Source Code Form is subject to the terms of the Mozilla Public
3+
// License, v. 2.0. If a copy of the MPL was not distributed with this
4+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
6+
/**
7+
* Adds a "Request AI Review" action to the revision page
8+
*/
9+
final class ReviewHelperEventListener extends PhabricatorEventListener {
10+
11+
public function register() {
12+
$this->listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS);
13+
}
14+
15+
public function handleEvent(PhutilEvent $event) {
16+
if ($event->getType() == PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS) {
17+
$this->handleActionEvent($event);
18+
}
19+
}
20+
21+
private function handleActionEvent($event) {
22+
$object = $event->getValue('object');
23+
24+
if (!($object && $object->getPHID() && $object instanceof DifferentialRevision)) {
25+
return;
26+
}
27+
28+
$service_url = PhabricatorEnv::getEnvConfig('reviewhelper.url');
29+
if (!$service_url) {
30+
return;
31+
}
32+
33+
$user = $event->getUser();
34+
if (!$user || !$user->isLoggedIn()) {
35+
return;
36+
}
37+
38+
$active_diff = $object->getActiveDiff();
39+
if (!$active_diff) {
40+
return;
41+
}
42+
43+
$status = $object->getStatusObject();
44+
$is_open = !$status->isClosedStatus();
45+
46+
$request_uri = '/reviewhelper/request/' . $object->getID() . '/';
47+
48+
$tag = id(new PHUITagView())
49+
->setType(PHUITagView::TYPE_SHADE)
50+
->setColor(PHUITagView::COLOR_GREEN)
51+
->setSlimShady(true)
52+
->setName(pht('New'));
53+
54+
$action = id(new PhabricatorActionView())
55+
->setHref($request_uri)
56+
->setName(array(pht('Request AI Review'), ' ', $tag))
57+
->setIcon('fa-magic')
58+
->setDisabled(!$is_open)
59+
->setWorkflow(true);
60+
61+
$actions = $event->getValue('actions');
62+
array_unshift($actions, $action);
63+
64+
$event->setValue('actions', $actions);
65+
}
66+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?php
2+
// This Source Code Form is subject to the terms of the Mozilla Public
3+
// License, v. 2.0. If a copy of the MPL was not distributed with this
4+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
6+
final class ReviewHelperServiceException extends Exception {}

0 commit comments

Comments
 (0)