Skip to content

Instantly share code, notes, and snippets.

@mbaker3
Forked from omnicolor/RB-precommit.php
Last active November 18, 2022 08:42
Show Gist options
  • Save mbaker3/51e066e0647e0465c7fefca353a7c8ba to your computer and use it in GitHub Desktop.
Save mbaker3/51e066e0647e0465c7fefca353a7c8ba to your computer and use it in GitHub Desktop.
SVN pre-commit intergration with Review Board
#!/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 <omni@digitaldarkness.com>
*
* 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 <mbaker@karmaninteractive.com>
*/
/**
* 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