<?php
/**
* Perforce Swarm
*
* @copyright 2013 Perforce Software. All rights reserved.
* @license Please see LICENSE.txt in top-level folder of this distribution.
* @version <release>/<patch>
*/
namespace Reviews;
use Activity\Model\Activity;
use P4\Spec\Change;
use Record\Exception\NotFoundException as RecordNotFoundException;
use Reviews\Listener\Review as ReviewListener;
use Reviews\Filter\GitInfo;
use Reviews\Model\GitReview;
use Reviews\Model\Review;
use Zend\Mvc\MvcEvent;
class Module
{
/**
* Connect to queue event manager to handle review tasks.
*
* @param MvcEvent $event the bootstrap event
* @return void
*/
public function onBootstrap(MvcEvent $event)
{
$application = $event->getApplication();
$services = $application->getServiceManager();
$events = $services->get('queue')->getEventManager();
// attach listener to process review when its created or updated
$events->attach(new ReviewListener($services));
// Deal with git-fusion initiated reviews before traditional p4 reviews.
//
// If the shelf is already a git initiated review we'll update it.
//
// If the shelf has git-fusion keywords indicating this is a new review
// translate the existing shelf into a new git-review and process it.
$events->attach(
'task.shelve',
function ($event) use ($services) {
$p4Admin = $services->get('p4_admin');
$queue = $services->get('queue');
$config = $services->get('config');
$change = $event->getParam('change');
// if we didn't get a pending change to work with, bail
if (!$change instanceof Change || !$change->isPending()) {
return;
}
// if the change is by a user that is ignored for the purpose of reviews, bail
$ignored = isset($config['reviews']['ignored_users']) ? $config['reviews']['ignored_users'] : null;
if ($p4Admin->stringMatches($change->getUser(), (array) $ignored)) {
return;
}
// if this change doesn't have a valid looking git-fusion style review-id
// there's no need to further examine it here, return
$gitInfo = new GitInfo($change->getDescription());
if ($gitInfo->get('review-id') != $change->getId()) {
return;
}
try {
// using the change id, verify if a git review already exists
// note the review id and change id are the same for git-fusion reviews
$review = Review::fetch($change->getId(), $p4Admin);
// if we get a review but its the wrong type, we can't do anything with it
// this really shouldn't happen but good to confirm all is well
if ($review->getType() != 'git') {
return;
}
} catch (RecordNotFoundException $e) {
// couldn't fetch an existing review, create one!
$review = GitReview::createFromChange($change, $p4Admin);
$review->save();
// ensure we pass along to the review event that this is an add
$isAdd = true;
}
// put the fetched/created review on the existing event.
// the presence of a review on the event will cause the traditional
// shelf-commit handler to skip processing this change.
$event->setParam('review', $review);
// push the new review into queue for further processing.
$queue->addTask(
'review',
$review->getId(),
array(
'user' => $change->getUser(),
'updateFromChange' => $change->getId(),
'isAdd' => isset($isAdd) && $isAdd
)
);
},
100
);
// Listen for when a shelf is updated or change submitted.
// We will examine the change description to see if it contains a
// configured review pattern.
//
// If the change contains a review pattern that includes an existing
// review id we simply push it through to a 'review' task to carry
// out the work of updating the shelved files, participants, etc.
//
// For changes with a review pattern with no id (so its a 'start review')
// a new review record will be created and the original change's description
// is updated to include the id. We then push the change through to the
// 'review' task much like an update to take care of shelve transfer, etc.
//
// For more information on review patterns, see the review_keywords service.
$module = $this;
$events->attach(
array('task.shelve', 'task.commit'),
function ($event) use ($module, $services) {
$p4Admin = $services->get('p4_admin');
$queue = $services->get('queue');
$keywords = $services->get('review_keywords');
$config = $services->get('config');
$change = $event->getParam('change');
$data = (array) $event->getParam('data') + array('review' => null);
// if a review is already present on the event, someone has done the work for us
// most likely, this means it was a git-fusion review
if ($event->getParam('review') instanceof Review) {
return;
}
// if we didn't get a change to work with, bail
if (!$change instanceof Change) {
return;
}
// if the change is by a user that is ignored for the purpose of reviews, bail
$ignored = isset($config['reviews']['ignored_users']) ? $config['reviews']['ignored_users'] : null;
if ($p4Admin->stringMatches($change->getUser(), (array) $ignored)) {
return;
}
// when we update the swarm managed change it feeds back around
// to here and we need to ignore the event.
if (Review::exists($change->getOriginalId(), $p4Admin)) {
return;
}
// we have to determine if this change is already in a review. if it is we:
// - ensure the change updates that review (even if #review-123 isn't present)
// - block starting/updating any additional reviews
// - ignore the change if it is a new archive/version of the review
// - if change is in the midst of being committed against a specific review,
// use that review
$reviews = Review::fetchAll(array(Review::FETCH_BY_CHANGE => $change->getOriginalId()), $p4Admin);
// if the change is a new archive/version of the review, ignore event altogether.
// note: we use the raw versions value to avoid tickling on-the-fly upgrade code
foreach ($reviews as $review) {
$versions = (array) $review->getRawValue('versions');
foreach ($versions as $version) {
$version += array('change' => null, 'archiveChange' => null, 'pending' => null);
if (($version['change'] == $change->getId() || $version['archiveChange'] == $change->getId())
&& $version['pending']
) {
return;
}
}
}
// check for a review keyword in the description
$matches = $keywords->getMatches($change->getDescription());
// if this change is associated to a review; ignore the keyword and use
// the review id we're already associated with.
// we don't expect multiple reviews but should that occur use the first.
if ($reviews->count()) {
$matches['id'] = $reviews->first()->getId();
}
// if the change is in the midst of being committed against a review,
// that review's id should be used (even if it isn't the first review)
foreach ($reviews as $review) {
if ($review->getCommitStatus('change') == $change->getOriginalId()) {
$matches['id'] = $review->getId();
break;
}
}
// if an id was passed in data 'review' it always wins
if (strlen($data['review'])) {
$matches['id'] = $data['review'];
}
// if no review details could be located; nothing to do
if (!$matches) {
return;
}
// normalize matches now that we know we should be processing
$matches += array('id' => null);
// don't allow a change to be in more than one review
// - if the change is in a review, block adding another review
// - if the change is in a review, only allow updates to that review
// largely unnecessary but does protect us in the data['review'] case.
if ($reviews->count()) {
if (!strlen($matches['id'])) {
return;
}
if (!in_array($matches['id'], $reviews->invoke('getId'))) {
return;
}
}
// if this is an update to an existing review, fetch it
// otherwise create a new review.
if (strlen($matches['id'])) {
// fetch to make sure it exists and to normalize edits/adds
// when we push the queue event.
try {
$review = Review::fetch($matches['id'], $p4Admin);
} catch (\Record\Exception\NotFoundException $e) {
} catch (\InvalidArgumentException $e) {
}
// nothing to update if they provided a bad id
if (isset($e)) {
// @todo inform user via email their id was bad?
return;
}
// perforce users can only commit against a git review, they are
// not otherwise allowed to update it. if this is a git review
// and not a commit based update, bail
if ($review->getType() == 'git' && !$change->isSubmitted()) {
// @todo inform user via email they cannot update git reviews?
return;
}
// if the review is mid-commit for another change, bail
if ($review->isCommitting() && $review->getCommitStatus('change') != $change->getOriginalId()) {
// @todo inform user via email their update was skipped due to ongoing approve & commit?
return;
}
// add the on behalf of information if the user committing this review is not the same as the
// original author of it
$committer = $review->getCommitStatus('change') == $change->getOriginalId()
? $review->getCommitStatus('committer')
: $change->getUser();
if ($committer
&& $committer != $review->get('author')
&& $event->getParam('activity') instanceof Activity
) {
$activity = $event->getParam('activity');
$activity->set('behalfOf', $review->get('author'));
$activity->set('user', $committer);
}
} else {
// create the review record
$review = Review::createFromChange($change, $p4Admin);
// strip off the review keyword(s) and save it
$review->set('description', $keywords->filter($review->get('description')));
$review->save();
// ensure we pass along to the review event that this is an add
$isAdd = true;
// the change that started this review needs its description updated to include
// the review id. this will give the user feedback we've handled it and make it
// clear any future updates to shelved files on that change will impact the review.
$change->setDescription(
$keywords->update($change->getDescription(), array('id' => $review->getId()))
);
// saving won't work correctly without a valid client; grab one
// and ensure its released even if exceptions should occur.
try {
$change->getConnection()->getService('clients')->grab();
$change->save(true);
} catch (\Exception $e) {
// we're pretty committed to adding the review at this point so just log and carry on
$services->get('logger')->err($e);
}
$change->getConnection()->getService('clients')->release();
}
// put the fetched/created review on the existing event in case anyone cares for it
$event->setParam('review', $review);
// push the new review into queue for further processing.
$queue->addTask(
'review',
$review->getId(),
array(
'user' => $change->getUser(),
'updateFromChange' => $change->getId(),
'isAdd' => isset($isAdd) && $isAdd
)
);
},
90
);
}
public function getConfig()
{
return include __DIR__ . '/config/module.config.php';
}
public function getAutoloaderConfig()
{
return array(
'Zend\Loader\StandardAutoloader' => array(
'namespaces' => array(
__NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
),
),
);
}
}