<?php
namespace JiraPerforceSmartCommits;
use Application\Config\ConfigManager;
use Laminas\Http\Client as HttpClient;
use Laminas\Json\Json;
use Laminas\ServiceManager\ServiceLocatorInterface as ServiceLocator;
class Module
{
public static function handleSmartCommitMessage($item, ServiceLocator $services)
{
$callouts = self::getJiraCallouts($item->getDescription());
$config = $services->get('config');
$logger = $services->get('logger');
foreach ($callouts as $callout) {
if (!array_key_exists('issues', $callout) || !array_key_exists('commands', $callout)) {
continue;
}
foreach ($callout['issues'] as $issue) {
$commentCommand = self::getCommand('comment', $callout['commands']);
$timeCommand = self::getCommand('time', $callout['commands']);
$transitionCommands = self::getTransitionCommands($callout['commands']);
if ($commentCommand && array_key_exists('args', $commentCommand)) {
$logger->info('JiraSmartCommits: building comment');
if (ConfigManager::getValue($config, 'jirasmartcommits.cite_submitter_username', true)) {
$prefix = "[~" . $item->getUser() . "] says in c";
} else {
$prefix = "C";
}
if (ConfigManager::getValue($config, 'jirasmartcommits.link_changelist_comment_reference', true)) {
$qualifiedUrl = $services->get('ViewHelperManager')->get('qualifiedUrl');
$changelist = "[" . $item->getId() . "|" . $qualifiedUrl('change', array('change' => $item->getId())) . "]";
} else {
$changelist = $item->getId();
}
$commentStr = "${prefix}hangelist $changelist: ${commentCommand['args']}";
$commentObj = array(
'body' => $commentStr,
);
}
if ($timeCommand && array_key_exists('args', $timeCommand)) {
$logger->info('JiraSmartCommits: building time tracking');
$msg = array(
'timeSpent' => $timeCommand['args'],
);
if (isset($commentStr)) {
$logger->info('JiraSmartCommits: and bundling comment');
$msg['comment'] = $commentStr;
$commented = true;
$timeCommented = true;
}
if (self::doRequest(
'post',
"issue/$issue/worklog",
$msg,
$services) === false && isset($timeCommented)) {
// if time-tracking fails but we wanted a comment, try to still get the comment posted
unset($commented);
}
unset($timeCommented);
}
if (count($transitionCommands) > 0) {
$logger->info('JiraSmartCommits: asked for transition(s)');
$availableTransitions = self::doRequest(
'get',
"issue/$issue/transitions",
null,
$services);
$transitionId = null;
if ($availableTransitions && is_array($availableTransitions) && array_key_exists('transitions', $availableTransitions)
&& is_array($availableTransitions['transitions'])) {
foreach ($availableTransitions['transitions'] as $availableTransition) {
if (is_array($availableTransition) && array_key_exists('name', $availableTransition)) {
$availableTransitionName = str_replace(' ', '-', strtolower($availableTransition['name']));
$logger->debug("JiraSmartCommits: available transition: $availableTransitionName");
foreach ($transitionCommands as $transitionCommand) {
$logger->debug("JiraSmartCommits: comparing to ${transitionCommand['command']}");
if (strpos($availableTransitionName, $transitionCommand['command']) === 0) {
$transitionId = $availableTransition['id'];
break;
}
}
if ($transitionId) {
break;
}
}
}
}
if ($transitionId) {
$logger->info("JiraSmartCommits: found desired transition id $transitionId");
$msg = array(
'transition' => array(
'id' => $transitionId,
),
);
if (isset($commentObj) && !isset($commented)) {
$logger->info('JiraSmartCommits: bundling comment');
$msg['update'] = array(
'comment' => array(
array(
'add' => $commentObj,
),
),
);
$commented = true;
$transitionCommented = true;
}
if (self::doRequest(
'post',
"issue/$issue/transitions",
$msg,
$services) === false && isset($transitionCommented)) {
// if resolving fails but we wanted a comment, try to still get the comment posted
unset($commented);
}
} else {
$logger->notice('JiraSmartCommits: unable to locate a valid transition id. ' . print_r($transitionCommands, true) . print_r($availableTransitions, true));
}
unset($transitionCommented);
}
if (isset($commentObj) && !isset($commented)) {
$logger->info('JiraSmartCommits: comment wanted, not already handled. handling.');
self::doRequest(
'post',
"issue/$issue/comment",
$commentObj,
$services);
}
unset($commented, $commentObj, $commentStr);
}
}
}
private static function getCommand($key, $commandArray)
{
foreach ($commandArray as $command) {
if (is_array($command) && array_key_exists('command', $command) && $command['command'] == $key) {
return $command;
}
}
return null;
}
private static function getTransitionCommands($commandArray)
{
$ret = array();
foreach ($commandArray as $command) {
if (is_array($command) && array_key_exists('command', $command) && $command['command'] != 'comment' && $command['command'] != 'time') {
array_push($ret, $command);
}
}
return $ret;
}
public static function getJiraCallouts($value)
{
$projects = array_map('preg_quote', self::getProjects());
$callouts = array();
foreach (explode("\n", $value) as $line) {
$mode = 0;
foreach (preg_split('/(\s+)/', $line) as $word) {
$issues = self::getJiraIssue($word, $projects);
$command = self::getSmartCommitCommand($word);
if (!isset($last)) {
$last = array();
}
if (count($callouts) > 0) {
$last = &$callouts[count($callouts)-1];
}
if ($command && ($mode === 1 || $mode === 2)) {
if (array_key_exists('commands', $last)) {
$last['commands'][] = array('command' => $command);
} else {
$last['commands'] = array(array('command' => $command));
}
$mode = 2;
} elseif ($issues) {
if ($mode === 0 || !array_key_exists('issues', $last) || array_key_exists('commands', $last)) {
$callouts[] = array('issues' => $issues);
} else {
$last['issues'] = array_merge($last['issues'], $issues);
}
$mode = 1;
} elseif ($mode === 2) {
$lastCommand = &$last['commands'][count($last['commands'])-1];
if (array_key_exists('args', $lastCommand)) {
$lastCommand['args'] .= ' ';
} else {
$lastCommand['args'] = '';
}
$lastCommand['args'] .= $word;
}
}
}
return $callouts;
}
public static function getJiraIssue($value, $projects)
{
if (preg_match_all("/((?:" . implode('|', $projects) . ")-[0-9]+)/", $value, $match)) {
return $match[1];
}
return null;
}
public static function getSmartCommitCommand($value)
{
if (strpos($value, '#') === 0 && strlen($value) > 1) {
return strtolower(substr($value, 1));
}
return null;
}
public static function doRequest($method, $resource, $data, ServiceLocator $services)
{
// we commonly do a number of requests and don't want one failure to bork them all,
// if anything goes wrong just log it
try {
// setup the client and request details
$config = $services->get('config');
$url = ConfigManager::getValue($config, 'jirasmartcommits.host') . '/rest/api/latest/' . $resource;
$client = new HttpClient;
$client->setUri($url)
->setHeaders(array('Content-Type' => 'application/json'))
->setMethod($method);
// set the http client options; including any special overrides for our host
$options = $services->get('config') + array('http_client_options' => array());
$options = (array) $options['http_client_options'];
if (isset($options['hosts'][$client->getUri()->getHost()])) {
$options = (array) $options['hosts'][$client->getUri()->getHost()] + $options;
}
unset($options['hosts']);
$client->setOptions($options);
if ($method == 'post') {
$client->setRawBody(Json::encode($data));
} else {
$client->setParameterGet((array) $data);
}
if (ConfigManager::getValue($config, 'jirasmartcommits.user')) {
$client->setAuth(ConfigManager::getValue($config, 'jirasmartcommits.user'), ConfigManager::getValue($config, 'jirasmartcommits.password'));
}
// attempt the request and log any errors
$services->get('logger')->info('JiraSmartCommits making ' . $method . ' request to resource: ' . $url, (array) $data);
$response = $client->dispatch($client->getRequest());
if (!$response->isSuccess()) {
$services->get('logger')->err(
'JiraSmartCommits failed to ' . $method . ' resource: ' . $url . ' (' .
$response->getStatusCode() . " - " . $response->getReasonPhrase() . ').',
array(
'request' => $client->getLastRawRequest(),
'response' => $client->getLastRawResponse()
)
);
return false;
}
// looks like it worked, return the result
return json_decode($response->getBody(), true);
} catch (\Exception $e) {
// the Jira module is doing this, but it seems to be generating its own exceptions for me.
// fortunately the error is already logged from above, so we're good.
//$services->get('logger')->err($e);
}
return false;
}
public static function getProjects()
{
$file = DATA_PATH . '/cache/jirasmartcommits/projects';
if (!file_exists($file)) {
return array();
}
return (array) json_decode(file_get_contents($file), true);
}
public static function getCacheDir()
{
$dir = DATA_PATH . '/cache/jirasmartcommits';
if (!is_dir($dir)) {
@mkdir($dir, 0700, true);
}
if (!is_writable($dir)) {
@chmod($dir, 0700);
}
if (!is_dir($dir) || !is_writable($dir)) {
throw new \RuntimeException(
"Cannot write to cache directory ('" . $dir . "'). Check permissions."
);
}
return $dir;
}
public function getConfig()
{
return include __DIR__ . '/config/module.config.php';
}
}