2010-09-07

Integrating Subversion and Review Board

At Google, all production code must be peer reviewed before it is allowed to be submitted to their source control system. Since I'm no longer a Googler, I don't have access to their awesome infrastructure so I had to roll my own. Review Board is a pretty good code review system, and unlike the open source implementation of Google's internal one, you can host Review Board on your own hardware.

We use Subversion as our SCM. Out of the box, Subversion doesn't force code to be reviewed before submission, and I couldn't find any documentation about forcing code reviews through pre-submit hooks. Subversion's hooks are pretty powerful, and Review Board has a decent API, so I wrote my own. Hopefully this is useful to someone out there. It's written in PHP5. Put this in your subversion hooks directory, call it pre-submit, and make it executable.

#!/usr/local/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>
*/
/**
* Review Board server address.
*/
define('REVIEW_SERVER', 'http://your-server.com');
/**
* Path to svnlook binary.
*/
define('SVNLOOK', '/usr/bin/svnlook');
/**
* Divider to inject into error messages.
*/
define('DIVIDER',
'********************************************************************************');
/**
* 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);
}
/**
* 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/json/reviewrequests/'
. (int)$id . '/reviews/';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (404 == $statusCode) {
throw new NotFoundException();
}
if (200 != $statusCode) {
throw new RequestFailedException();
}
$result = json_decode($result);
if (!$result) {
throw new RetryException();
}
if (!property_exists($result, 'reviews')) {
throw new RetryException();
}
$review = $result->reviews[
count($result->reviews) - 1];
return array($review->ship_it,
$review->user->username);
}
/**
* 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: [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 = $match[1];
$reviewStatus = null;
// Our review board server is a bit flakey, so 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. Assuming you got a Ship '
. 'It.' . PHP_EOL;
file_put_contents('php://stderr', $message);
return false;
}
}
list($status, $reviewAuthor) = $reviewStatus;
if (!$status) {
// If no one has given the review a ship-it, fail
// immediately.
$message = PHP_EOL . DIVIDER . PHP_EOL . PHP_EOL
. 'You included a review number that has not '
. 'passed its review.'
. PHP_EOL;
file_put_contents('php://stderr', $message);
return true;
}
// Make sure the reviewer isn't the same as the
// author.
if ($reviewAuthor == $author) {
$message = PHP_EOL . DIVIDER . PHP_EOL . PHP_EOL
. 'You can\'t review your own code. Get '
. 'someone else to do it!'
. 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 {}
$repository = $_SERVER['argv'][1];
$transaction = $_SERVER['argv'][2];
$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);

8 comments:

  1. Thank you for this!

    ReplyDelete
  2. Thanks for the info!
    I'm looking, though, to the script to do pre-commints in reviewboard 1.5. The API is based on REST instead of JSON so this script would not work (at least it does not when I tried). Do you have this same script for the REST interface? :)
    Thanks a lot!

    ReplyDelete
  3. We haven't upgraded our Review Board instance at $employer, so I haven't really needed to try it. I'll see if I can rewrite the script to handle 1.5 as well.

    ReplyDelete
  4. Hi, We wanted to implemented the same. We made changes to pre-submit hook. It was giving error in the following 2 lines..

    $repository = $_SERVER['argv'][1];
    $transaction = $_SERVER['argv'][2];

    Hence I hard coded the $repository path as /var/www/svn/project1 where project files exist in repo.
    Also changed the $transaction path to the reviewboard path. After changes the code looks as follows.

    #$repository = $_SERVER['argv'][1];
    #$transaction = $_SERVER['argv'][2];
    $repository = '/var/www/svn/project1';
    $transaction = '/var/www/html/htdocs';

    But after uploading files to SVN, I am not able to see in Reviewboard. Please let me know how to fix this issue.

    ReplyDelete
    Replies
    1. I think my script may not work for you. This script is supposed to keep you from being able to submit code to SVN unless there's a review with a Ship It in Review Board.

      Delete
  5. Hi Omni Adams,
    after applying you script following error has been encountered. please look into this.

    [root@reviewboard ART]# svn ci -m 'review: 39'
    Sending mukul.txt
    Transmitting file data .svn: Commit failed (details follow):
    svn: Commit blocked by pre-commit hook (exit code 1) with output:
    PHP Parse error: syntax error, unexpected ';' in /home/cmteam/csvn/data/repositories/CCRForms3/hooks/pre-submit.php on line 130

    ReplyDelete
    Replies
    1. It looks like my gist had some escaped HTML in it. Change the < to < and it should work.

      Delete
  6. This is a super old thread but for what it's worth I've made some updates to the original gist. The updates aren't pretty but they do the job. We're using this daily with RB 2.5.2. Link and summary below:
    - 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)

    https://gist.github.com/mbaker3/51e066e0647e0465c7fefca353a7c8ba

    ReplyDelete