<?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__, ), ), ); } }