-
-
Save AnandPilania/4b5feea8706ae6fcfe913ff3d374274d to your computer and use it in GitHub Desktop.
SVN pre-commit intergration with Review Board
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/php | |
<?php | |
/** | |
* Pre-commit Subversion script. | |
* | |
* Forces the commit message to have a line like | |
* review: 42 | |
* and checks that the review has received a Ship It! from | |
* a peer. | |
* @author Omni Adams <[email protected]> | |
* | |
* UPDATE | |
* - Modified to support Reviewboard API 2.0 | |
* - Added ability to check non-public reviews via API_TOKEN | |
* - Added ability to define minimum number of unique ship-its given to a review | |
* - Added ability to enforce check on just a subpath (Ex: trunk) | |
* @author Mike Baker <[email protected]> | |
*/ | |
/** | |
* Review Board server address. | |
*/ | |
define('REVIEW_SERVER', 'http://your-server.com'); | |
/** | |
* Ship-its required for the commit to succeed | |
*/ | |
define('REQUIRED_SHIPITS', 2); | |
/** | |
* Review Board WebAPI Token | |
*/ | |
define('API_TOKEN', '<Your API Token>'); | |
/** | |
* Root path in repo to enforce on | |
*/ | |
define('ENFORCE_ON_PATH', 'trunk/'); | |
/** | |
* Path to svnlook binary. | |
*/ | |
define('SVNLOOK', '/usr/bin/svnlook'); | |
/** | |
* Divider to inject into error messages. | |
*/ | |
define('DIVIDER', '************************************ ERROR *************************************'); | |
/** | |
* Get the name of the user committing the transaction. | |
* @param string $transaction | |
* @param string $respository | |
* @return string Username of the commit author. | |
*/ | |
function getCommitUser($transaction, $repository) { | |
$authorOutput = array(); | |
$authorCommand = SVNLOOK . ' author -t "' | |
. $transaction . '" "' | |
. $repository . '"'; | |
exec($authorCommand, $authorOutput); | |
$authorOutput = implode(PHP_EOL, $authorOutput); | |
return trim($authorOutput); | |
} | |
/** | |
* Does the current transaction affect the configured path? | |
* @param string $transaction | |
* @param string $respository | |
* @return boolean true if this commit does affect the configured path. | |
*/ | |
function affectsEnforcedPath ($transaction, $repository) { | |
$dirsChanged = array(); | |
$dirsChangedCommand = SVNLOOK . ' dirs-changed -t "' | |
. $transaction . '" "' | |
. $repository . '"'; | |
exec($dirsChangedCommand, $dirsChanged); | |
if(count($dirsChanged) == 0){ | |
return false; | |
} | |
foreach ($dirsChanged as $dir){ | |
if(strpos($dir, ENFORCE_ON_PATH) === 0){ | |
return true; | |
} | |
} | |
return false; | |
} | |
/** | |
* Check a review to see if it has a ship-it. | |
* @param integer $id Review ID to load. | |
* @param string $author SVN commit author. | |
* @return array(boolean, string) Ship It status, author | |
* @throws NotFoundException If we can't find the review. | |
* @throws RetryException If RB returned garbage. | |
* @throws RequestFailedException If RB is taking a nap. | |
*/ | |
function getReviewStatus($id, $author) { | |
$url = REVIEW_SERVER . '/api/review-requests/' | |
. (int)$id . '/reviews/'; | |
$ch = curl_init(); | |
curl_setopt($ch, CURLOPT_URL, $url); | |
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); | |
//ignore an invalid SSl certificate | |
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); | |
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); | |
curl_setopt($ch, CURLOPT_SSL_CIPHER_LIST, 'ecdhe_rsa_aes_128_gcm_sha_256'); | |
//set auth token | |
curl_setopt($ch, CURLOPT_HTTPHEADER, array( | |
'Authorization: token ' . API_TOKEN | |
)); | |
$result = curl_exec($ch); | |
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); | |
if (404 == $statusCode) { | |
throw new NotFoundException(); | |
} | |
if (403 == $statusCode) { | |
throw new PermissionDeniedException(); | |
} | |
if (200 != $statusCode) { | |
throw new RequestFailedException(); | |
} | |
$result = json_decode($result); | |
if (!$result) { | |
throw new RetryException(); | |
} | |
if (!property_exists($result, 'reviews')) { | |
throw new RetryException(); | |
} | |
return $result->reviews; | |
} | |
/** | |
* Force the commit message to include the review number. | |
* | |
* If the commit message does not include a review number, | |
* writes an error message to standard error and returns | |
* true. Also checks to make sure the review has passed | |
* its review. | |
* @param string $transaction SVN transaction ID. | |
* @param string $repository Full path to the repository. | |
* @return boolean True if there was an error. | |
*/ | |
function forceReviewNumberInCommitMessage($transaction, | |
$repository, $author) { | |
$logOutput = array(); | |
$logCommand = SVNLOOK . ' log -t "' | |
. $transaction . '" "' . $repository . '"'; | |
exec($logCommand, $logOutput); | |
$logOutput = implode(PHP_EOL, $logOutput); | |
$logOutput = trim($logOutput); | |
// Enforce that a review number is in the commit | |
// message. | |
$match = array(); | |
if (!preg_match('/[Rr]eview:\s?[0-9]+/', $logOutput, | |
$match)) { | |
$message = PHP_EOL . DIVIDER . PHP_EOL . PHP_EOL | |
. 'You didn\'t include the review number for ' | |
. 'this change.' | |
. PHP_EOL | |
. 'Your commit message MUST include a line ' | |
. 'with the review' . PHP_EOL | |
. 'number like this:' . PHP_EOL . 'review: 42' | |
. PHP_EOL; | |
file_put_contents('php://stderr', $message); | |
return true; | |
} | |
$match = array_shift($match); | |
$match = explode(':', $match); | |
$reviewId = trim($match[1]); | |
if(!is_numeric($reviewId)) { | |
$messag = PHP_EOL . DIVIDER . PHP_EOL . PHP_EOL . 'ReviewID could not be parsed'; | |
file_put_contents('php://stderr', $message); | |
return true; | |
} | |
$reviewStatus = null; | |
// If our review board server is a bit flakey, we may | |
// need to retry. | |
$retryCount = 0; | |
while ($retryCount < 5) { | |
try { | |
$reviewStatus = getReviewStatus($reviewId, | |
$author); | |
break; | |
} catch (NotFoundException $unused_e) { | |
$message = PHP_EOL . DIVIDER . PHP_EOL | |
. PHP_EOL | |
. 'You put a review number that was not ' | |
. 'found. Check your review and try ' | |
. 'again.' . PHP_EOL; | |
file_put_contents('php://stderr', $message); | |
return true; | |
} catch (RetryException $unused_e) { | |
file_put_contents('php://stderr', | |
'Retrying...' . PHP_EOL); | |
$retryCount++; | |
} catch (RequestFailedException $unused_e) { | |
$message = PHP_EOL . DIVIDER . PHP_EOL | |
. PHP_EOL | |
. 'The request to the review board ' | |
. 'failed.' . PHP_EOL; | |
file_put_contents('php://stderr', $message); | |
return true; | |
} catch (PermissionDeniedException $unused_e) { | |
$message = PHP_EOL . DIVIDER . PHP_EOL | |
. PHP_EOL | |
. 'The configured API token does not have' | |
. ' permission to view this review or' | |
. ' repository' . PHP_EOL; | |
file_put_contents('php://stderr', $message); | |
return true; | |
} | |
} | |
$shipit_authors = array(); | |
foreach ($reviewStatus as $review) { | |
if ($review->ship_it == true | |
&& $review->links->user->title != $author){ | |
array_push($shipit_authors, $review->links->user->title); | |
} | |
} | |
//only count ship-its from unique authors | |
$shipit_authors = array_unique($shipit_authors); | |
$shipit_count = count($shipit_authors); | |
if($shipit_count < REQUIRED_SHIPITS) { | |
//If not enough people have given the review a ship-it | |
// will fail | |
$message = PHP_EOL . DIVIDER . PHP_EOL . PHP_EOL | |
. $shipit_count . ' Ship-its out of a required ' | |
. REQUIRED_SHIPITS . ' were given for review ID: ' | |
. $reviewId . PHP_EOL; | |
file_put_contents('php://stderr', $message); | |
return true; | |
} | |
return false; | |
/** | |
* Not found exception. | |
*/ | |
class NotFoundException extends Exception {} | |
/** | |
* Request failed exception. | |
*/ | |
class RequestFailedException extends Exception {} | |
/** | |
* Retry exception. | |
*/ | |
class RetryException extends Exception {} | |
/** | |
* Permission denied exception | |
*/ | |
class PermissionDeniedException extends Exception {} | |
$repository = $_SERVER['argv'][1]; | |
$transaction = $_SERVER['argv'][2]; | |
//bail out early if the commit doesn't affect an enforced path | |
if(!affectsEnforcedPath($transaction, $repository)){ | |
exit(0); | |
} | |
file_put_contents('php://stderr', 'Commit will affect a file in ' . ENFORCE_ON_PATH . '. Code reviews enforced...' . PHP_EOL); | |
$author = getCommitUser($transaction, $repository); | |
file_put_contents('php://stderr', 'Checking the code review status.' . PHP_EOL); | |
$errors = forceReviewNumberInCommitMessage($transaction, $repository, $author); | |
if ($errors) { | |
exit(1); | |
} | |
exit(0); | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment