Skip to content
Snippets Groups Projects
Commit 4ff38350 authored by Neil Drumm's avatar Neil Drumm :wave:
Browse files

Issue #3327238: Remove versioncontrol_project_issue_git submodule

parent 264167f0
No related branches found
No related tags found
No related merge requests found
Showing
with 0 additions and 658 deletions
<?php
/**
* @file
* Base class for event processor plugins which deals with operation issue tasks.
*/
abstract class VersioncontrolEventProcessorGitOperationBase implements VersioncontrolSynchronizationEventProcessorInterface, VersioncontrolPluginConfigurationInterface {
/**
* Associated repository.
*
* @var VersioncontrolRepository
*/
protected $repository;
/**
* Pattern to find issue numbers in commit messages.
*/
protected $messagePattern;
public function setRepository(VersioncontrolRepository $repository) {
$this->repository = $repository;
}
public function setConfiguration($default_data) {
$this->messagePattern = !empty($default_data['message_pattern']) ? $default_data['message_pattern'] : '';
}
public function buildForm($default_data) {
$form = array();
$form['event-processor-issue-mapper-pattern'] = array(
'#type' => 'textfield',
'#title' => t('Extract pattern'),
'#description' => t('A pattern to be used to extract the issue ids from the commit message. i.e. "/#(\d+)\b/"'),
'#default_value' => !empty($default_data['message_pattern']) ? $default_data['message_pattern'] : '',
);
return $form;
}
public function getFormData($form_state_values_data) {
return array(
'message_pattern' => $form_state_values_data['event-processor-issue-mapper-pattern'],
);
}
public function submitForm(&$form, &$form_state) {
// Nothing special.
}
/**
* Retrieve the issues from operation message.
*/
public function getIssuesFromMessage($message) {
if (preg_match_all($this->messagePattern, $message, $matches)) {
return $matches[1];
}
return array();
}
/**
* Returns nids which can be associated.
*
* @param array
* The list of node ids to check.
* @param $reset
* Flag passet to node_load_multiple().
*
* @return array
* List of valid node ids.
*/
public function checkValidIssues($nids, $reset = FALSE) {
$valid_nids = array();
// Make sure we are dealing with project issues.
$possible_issues = node_load_multiple($nids, array(), $reset);
foreach ($possible_issues as $possible_issue) {
if ($this->isValidIssue($possible_issue)) {
$valid_nids[] = $possible_issue->nid;
}
}
return $valid_nids;
}
/**
* Checks if a node is a valid issue.
*
* @param object
* A node object to check.
*
* @return boolean
* TRUE on valid issue, FALSE if not.
*
* @see checkValidIssues()
*/
public function isValidIssue($possible_issue) {
if (!project_issue_node_is_issue($possible_issue)) {
return FALSE;
}
// Associate only if repository project is the same than issue
// project.
if (empty($possible_issue->field_project[LANGUAGE_NONE][0]['target_id'])) {
return FALSE;
}
if ($possible_issue->field_project[LANGUAGE_NONE][0]['target_id'] != $this->repository->project_nid) {
return FALSE;
}
return TRUE;
}
protected function filterRelevantOperationLabelNames(VersioncontrolOperation &$operation, VersioncontrolGitEvent $event) {
$event_label_names = $this->getRelevantEventLabelNames($event);
$relevant_label_names = array();
foreach ($operation->labels as $key => $label) {
if (!in_array($label->name, $event_label_names)) {
unset($operation->labels[$key]);
}
}
}
protected function getRelevantEventLabelNames(VersioncontrolGitEvent $event) {
static $event_label_names;
if (isset($event_label_names)) {
return $event_label_names;
}
$event_label_names = array();
foreach ($event as $ref) {
// @todo This assumes non-repeating label names between different types
// of labels, which is not really true, but we are only supporting
// branches, so it is ok for now.
if ($ref->reftype != VERSIONCONTROL_GIT_REFTYPE_BRANCH) {
// Only process branch refs for now.
continue;
}
$event_label_names[] = $ref->refname;
}
return $event_label_names;
}
}
<?php
/**
* @file
* Maps issues with operations on git repositories based on commit messages.
*/
// @todo Is this possible to be added to the ctools plugins metadada?
require_once drupal_get_path('module', 'versioncontrol_project_issue_git') . '/includes/VersioncontrolEventProcessorGitOperationBase.inc';
class VersioncontrolEventProcessorGitCommitsComment extends VersioncontrolEventProcessorGitOperationBase {
/**
* {@inheritdoc}
*/
public function setConfiguration($default_data) {
parent::setConfiguration($default_data);
$this->maxCommits = !empty($default_data['max_commits']) ? $default_data['max_commits'] : 0;
}
public function process(VersioncontrolEvent $event) {
if (!$event instanceof VersioncontrolGitEvent) {
throw new VersioncontrolSynchronizationEventProcessorException('Passed object of type @type is not a git event object', array('@type' => get_class($event)));
}
$project_nid = db_query("SELECT nid FROM {versioncontrol_project_projects} WHERE repo_id = :repo_id", array(':repo_id' => $event->getRepository()->repo_id))->fetchField();
if (!$project_nid) {
throw new VersioncontrolSynchronizationEventProcessorException(format_string('No project node associated with repository id @repo_id', array(':repo_id' => $event->getRepository()->repo_id)));
}
// Conservative check of repository.
if (!$event->getRepository()->isValidGitRepo()) {
// Not valid git repository.
throw new VersioncontrolSynchronizationEventProcessorException('Invalid repository ID @repo_id', ['@repo_id' => $event->getRepository()->repo_id]);
}
// We are depending on getCommitInterval(), so check we are using the right
// synchronization plugin.
$synchronizer = $event->getRepository()->getSynchronizer();
if (!method_exists($synchronizer, 'getCommitInterval')) {
throw new VersioncontrolSynchronizationEventProcessorException('Cannot use a repository synchronizer of type %type', array('%type' => get_class($synchronizer)));
}
$replies_per_nid = array();
foreach ($event as $ref) {
if ($ref->reftype != VERSIONCONTROL_GIT_REFTYPE_BRANCH) {
// Only process branch refs for now.
continue;
}
if ($ref->ff != 1) {
// @fixme support non-ff.
continue;
}
if ($ref->eventDeletedMe()) {
// Do not announce removal.
continue;
}
// Usual ff ref update.
$commit_hashes = $synchronizer->getCommitInterval($ref->old_sha1, $ref->new_sha1);
// Skip if there are too many commits.
if (!empty($this->maxCommits) && count($commit_hashes) > $this->maxCommits) {
watchdog('versioncontrol_project_issue', 'Skipping commits comments for large push <code>@old_sha1..@new_sha1</code> with @count commits to <code>@refname</code> in repository @name', [
'@old_sha1' => $ref->old_sha1,
'@new_sha1' => $ref->new_sha1,
'@count' => count($commit_hashes),
'@refname' => $ref->refname,
'@name' => $event->getRepository()->name,
], WATCHDOG_WARNING);
continue;
}
$alter_context = array(
'plugin_name' => 'git_commits_as_comment',
'ref' => $ref,
);
// Try to deal with big pushes loading operations in chunks of 100 items.
foreach (array_chunk($commit_hashes, 100) as $commit_hashes_chunk) {
$related_operations = $event->getRepository()->loadCommits(array(), array('revision' => $commit_hashes_chunk));
foreach ($related_operations as $operation) {
$nids_on_message = $this->getIssuesFromMessage($operation->message);
if (count($nids_on_message) < 1) {
// Nothing to do.
continue;
}
// Override labels from operations to exclude not updated labels in
// this push.
$this->filterRelevantOperationLabelNames($operation, $event);
$valid_issue_nids = $this->checkValidIssues($nids_on_message);
// Let other modules modify the target issues to comment.
drupal_alter('versioncontrol_project_issue_git_issue_notification_project_issue_nids', $valid_issue_nids, $alter_context);
// Group by node, since we could receive multiple commits in the same
// event.
foreach (node_load_multiple($valid_issue_nids) as $node) {
$comment = theme('versioncontrol_project_issue_git_operation_comment_item', array('operation' => $operation));
$replies_per_nid[$node->nid]['operation_comments'][] = $comment;
}
}
drupal_static_reset();
}
}
// Actually make the comments.
$followup_account = user_load(variable_get('project_issue_followup_user', 0));
foreach (array_chunk(array_keys($replies_per_nid), 100) as $replies_per_nid_chunk) {
foreach (node_load_multiple($replies_per_nid_chunk) as $node) {
$node->nodechanges_uid = $followup_account->uid;
$node->nodechanges_comment['comment_body'][LANGUAGE_NONE][0] = [
'value' => theme('item_list', [
'items' => $replies_per_nid[$node->nid]['operation_comments'],
'attributes' => ['class' => 'versioncontrol-project-commits']
]),
'format' => filter_default_format($followup_account),
];
// Do not send mail notifications.
$node->nodechanges_comment_attributes = [
'project_issue_no_email' => TRUE,
];
node_save($node);
}
drupal_static_reset();
}
}
public function isValidIssue($possible_issue) {
if (!parent::isValidIssue($possible_issue)) {
return FALSE;
}
// Do not make any comment on closed issues.
if (!in_array($possible_issue->field_issue_status[LANGUAGE_NONE][0]['value'], project_issue_open_states())) {
return FALSE;
}
return TRUE;
}
}
<?php
/**
* @file
* Maps issues with operations on git repositories based on commit messages.
*/
// @todo Is this possible to be added to the ctools plugins metadada?
require_once drupal_get_path('module', 'versioncontrol_project_issue_git') . '/includes/VersioncontrolEventProcessorGitOperationBase.inc';
class VersioncontrolEventProcessorGitIssueMapper extends VersioncontrolEventProcessorGitOperationBase {
public function process(VersioncontrolEvent $event) {
if (!$event instanceof VersioncontrolGitEvent) {
// Not a git event.
return;
}
$project_nid = db_query("SELECT nid FROM {versioncontrol_project_projects} WHERE repo_id = :repo_id", array(':repo_id' => $event->getRepository()->repo_id))->fetchField();
if (!$project_nid) {
// No project associated.
return;
}
// We are depending on getCommitInterval(), so check we are using the right
// synchronization plugin.
$synchronizer = $event->getRepository()->getSynchronizer();
if (!method_exists($synchronizer, 'getCommitInterval')) {
watchdog('versioncontrol_project_issue', 'git_issue_mapper: Cannot use a repository synchronizer of type %type', array('%type' => get_class($synchronizer)), WATCHDOG_WARNING);
return;
}
$operation_nid_maps = array();
foreach ($event as $ref) {
if ($ref->reftype != VERSIONCONTROL_GIT_REFTYPE_BRANCH) {
// Only process branch refs for now.
return;
}
if ($ref->ff != 1) {
// @fixme support non-ff.
continue;
}
if ($ref->eventDeletedMe()) {
// Nothing to do.
// We could try to figure out orphaned commits from label removal. Let's
// trust on hook_versioncontrol_entity_commit_delete() for now.
continue;
}
// Usual ff ref update.
$commit_hashes = $synchronizer->getCommitInterval($ref->old_sha1, $ref->new_sha1);
$alter_context = array(
'plugin_name' => 'git_issue_mapper',
'ref' => $ref,
);
// Try to deal with big pushes loading operations in chunks of 100 items.
foreach (array_chunk($commit_hashes, 100) as $commit_hashes_chunk) {
$related_operations = $event->getRepository()->loadCommits(array(), array('revision' => $commit_hashes_chunk));
foreach ($related_operations as $operation) {
$nids_on_message = $this->getIssuesFromMessage($operation->message);
if (count($nids_on_message) < 1) {
// Nothing to do.
continue;
}
$this->filterRelevantOperationLabelNames($operation, $event);
$valid_issue_nids = $this->checkValidIssues($nids_on_message);
// Let other modules modify the target issues to associate with.
drupal_alter('versioncontrol_project_issue_git_issue_notification_project_issue_nids', $valid_issue_nids, $alter_context);
// Make sure data is not already there, i.e same commits on a new
// branch.
$existing_maps = versioncontrol_project_issue_get_issue_operation_ids($valid_issue_nids);
foreach ($valid_issue_nids as $valid_issue_nid) {
if (empty($existing_maps[$valid_issue_nid]) || !in_array($operation->vc_op_id, $existing_maps[$valid_issue_nid])) {
$operation_nid_maps[$valid_issue_nid . ':' . $operation->vc_op_id] = [
'nid' => $valid_issue_nid,
'vc_op_id' => $operation->vc_op_id,
];
}
}
}
}
}
if (empty($operation_nid_maps)) {
// Nothing to do.
return;
}
// Insert mappings.
foreach ($operation_nid_maps as $operation_nid_map) {
db_merge('versioncontrol_project_issue_operations')
->key($operation_nid_map)
->execute();
}
}
}
<?php
/**
* @file
* Event processor plugin to add a comment referencing commits in git push
* related to an issue.
*/
$plugin = array(
'vcs' => 'git',
'title' => t('Git commits as comment'),
'handler' => array(
'class' => 'VersioncontrolEventProcessorGitCommitsComment',
'file' => 'VersioncontrolEventProcessorGitCommitsComment.inc',
'path' => drupal_get_path('module', 'versioncontrol_project_issue_git') . '/plugins/event_processor',
),
);
<?php
/**
* @file
* Maps issues on git commit messages.
*/
$plugin = array(
'vcs' => 'git',
'title' => t('Issue mapper for git repositories'),
'handler' => array(
'class' => 'VersioncontrolEventProcessorGitIssueMapper',
'file' => 'VersioncontrolEventProcessorGitIssueMapper.inc',
'path' => drupal_get_path('module', 'versioncontrol_project_issue_git') . '/plugins/event_processor',
),
);
<?php
/**
* @file
* Documentation for versioncontrol_project_issue_git hooks.
*/
/**
* Alter git_commits_as_comment and git_issue_mapper event processors issues
* to action into.
*
* This hook is called after general validation of nodes, for events on which
* its repository is configured to use either git_commits_as_comment or
* git_issue_mapper event processor plugins.
*
* @param array &$valid_issue_nids
* List of node ids of project issues to comment on.
* @param array $context
* An array with the following keys:
* - 'plugin_name': Machine name of the event processor plugin from which the
* hook was called.
* - 'ref': The associated ref.
*/
function hook_versioncontrol_project_issue_git_issue_notification_project_issue_nids_alter(&$valid_issue_nids, $context) {
// Skip comment if refname starts with "_".
$start = substr($context['ref']->refname, 0, 1);
if ($start !== FALSE && $start == '_') {
$valid_issue_nids = array();
}
}
<?php
/**
* @file
*
* Drush integration for versioncontrol_project_issue_git.
*/
function versioncontrol_project_issue_git_drush_command() {
$items = array();
$items['vc-project-issue-git-resync-maps'] = array(
'description' => 'Re-discover project issue/operation maps based on project repository commit messages using git_issue_mapper event processor plugin.',
'arguments' => array(
'type' => 'Either repositories, projects or operations.',
'target' => 'A comma-delimited list of repository ids, project ids or operation ids corresponging with the type argument.',
),
'examples' => array(
'drush vcpi-git-resync-maps repositories 1,2' => 'Re-discover issue/operation maps of the specified repositories.',
'drush vcpi-git-resync-maps projects 1,2' => 'Re-discover issue/operation maps of the specified projects.',
'drush vcpi-git-resync-maps operations 1,2' => 'Re-discover issue/operation maps of the specified operations.',
),
'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_FULL,
'aliases' => array('vcpi-git-resync-maps'),
);
return $items;
}
/**
* Implements hook_drush_help().
*/
function versioncontrol_project_issue_git_drush_help($section) {
switch ($section) {
case 'drush:vc-project-issue-git-resync-maps':
return dt('This command will try to re-synchronize issue/operation maps associated with the parmeters passed.');
}
}
/**
* Drush callback for 'vc-project-issue-git-resync-maps'.
*/
function drush_versioncontrol_project_issue_git_vc_project_issue_git_resync_maps() {
list($type, $ids) = func_get_args();
$ids = _convert_csv_to_array($ids);
$default_event_processors = variable_get('versioncontrol_repository_plugin_defaults_git_event_processors', array());
if (!in_array('git_issue_mapper', $default_event_processors)) {
// Nothing to do.
return;
}
ctools_include('plugins');
$git_issue_mapper_plugin = ctools_get_plugins('versioncontrol', 'event_processor', 'git_issue_mapper');
$git_issue_mapper_class_name = ctools_plugin_get_class($git_issue_mapper_plugin, 'handler');
$git_issue_mapper_instace = new $git_issue_mapper_class_name();
$default_event_processor_data = variable_get('versioncontrol_repository_plugin_defaults_git_event_processors_data', array());
if (empty($default_event_processor_data['git_issue_mapper'])) {
$message = dt('Git issue mapper event processor is not yet configured.');
drush_log($message, 'error');
return drush_set_error('vcpi_git_resync_maps_unconfig_issue_mapper', $message);
}
switch ($type) {
case 'projects':
$projects = node_load_multiple($ids);
if (count($ids) != count($projects)) {
// Some projects were not loaded correctly.
$missing_ids = array_diff($ids, array_keys($projects));
drush_log(dt('Some project ids were not loaded correctly: (!ids)', array('!ids' => implode(', ', $missing_ids))), 'warning');
}
$ids = array();
foreach ($projects as $project) {
$ids[] = $project->versioncontrol_project['repo_id'];
}
case 'repositories':
$repositories = versioncontrol_repository_load_multiple($ids);
if (count($ids) != count($repositories)) {
// Some repositories were not loaded correctly.
$missing_ids = array_diff($ids, array_keys($repositories));
drush_log(dt('Some repository ids were not loaded correctly: (!ids)', array('!ids' => implode(', ', $missing_ids))), 'warning');
}
foreach ($repositories as $repository) {
if (!versioncontrol_project_repository_has_project($repository)) {
// Nothing to do.
continue;
}
// Remove old entries.
versioncontrol_project_issue_delete_issue_operation_maps($repository);
// Set up the plugin.
$git_issue_mapper_instace->setRepository($repository);
$git_issue_mapper_instace->setConfiguration($default_event_processor_data['git_issue_mapper']);
$operation_nid_maps_counter = 0;
$insert_query = db_insert('versioncontrol_project_issue_operations')->fields(array('nid', 'vc_op_id'));
// Do a manual query for performance reasons.
$result = db_query('SELECT vc_op_id, message FROM versioncontrol_operations WHERE repo_id = :repo_id', array(':repo_id' => $repository->repo_id));
while ($row = $result->fetchAssoc()) {
$nids_on_message = $git_issue_mapper_instace->getIssuesFromMessage($row['message']);
if (count($nids_on_message) < 1) {
// Nothing to do.
continue;
}
$valid_issue_nids = $git_issue_mapper_instace->checkValidIssues($nids_on_message, TRUE);
foreach ($valid_issue_nids as $valid_issue_nid) {
$insert_query->values(array(
'nid' => $valid_issue_nid,
'vc_op_id' => $row['vc_op_id'],
));
++$operation_nid_maps_counter;
}
if ($operation_nid_maps_counter % 100) {
// Insert when we reach the limit.
$insert_query->execute();
$operation_nid_maps_counter = 0;
$insert_query = db_insert('versioncontrol_project_issue_operations')->fields(array('nid', 'vc_op_id'));
}
}
if ($operation_nid_maps_counter > 0) {
// Insert pending mappings.
$insert_query->execute();
}
}
break;
case 'operations':
// @todo Implement.
default:
$message = dt('"!type" type not yet implemented.', array('!type' => $type));
return drush_set_error('vcpi_git_resync_maps_unimplemented_type', $message);
}
}
name = "Version Control / Project issue integration - Git"
description = "Expose some functionality that integrates project_issue and Version Control API Git backend."
package = Version Control
dependencies[] = versioncontrol_project_issue
dependencies[] = versioncontrol_git
dependencies[] = project_issue
core = 7.x
files[] = includes/VersioncontrolEventProcessorGitOperationBase.inc
<?php
/**
* @file
* Version Control / Project issue integration - Git backend.
*/
/**
* Implements ctools hook_ctools_plugin_directory().
*/
function versioncontrol_project_issue_git_ctools_plugin_directory($module, $plugin_type) {
if ($module == 'versioncontrol') {
return "plugins/$plugin_type";
}
}
/**
* Implements hook_versioncontrol_repository_pre_resync().
*
* Best effort to preserve existing mappings.
* A full review will be too costly, specially if there are a lot of associated
* issues.
* @todo Maybe add an option to do full re-map re-parsing operation messages.
*/
function versioncontrol_project_issue_git_versioncontrol_repository_pre_resync(VersioncontrolRepository $repository, $bypass) {
if (empty($repository->project_nid)) {
// No project associated.
return;
}
if (!$repository instanceof VersioncontrolGitRepository) {
// Not a git repository.
return;
}
// Load all associated project issue nids.
$query = new EntityFieldQuery();
$query->entityCondition('entity_type', 'node')
->entityCondition('bundle', project_issue_issue_node_types())
->fieldCondition('field_project', 'target_id', $repository->project_nid);
$result = $query->execute();
if (empty($result['node'])) {
// No issues associated.
return;
}
$issue_nids = array_keys($result['node']);
// Skip full operation loading, and do it manually for performance reasons.
$vc_op_ids_by_id = db_query('SELECT vco.vc_op_id, vco.revision FROM {versioncontrol_operations} vco JOIN {versioncontrol_project_projects} vcp WHERE vcp.repo_id = :repo_id', array(':repo_id' => $repository->project_nid))->fetchAllKeyed();
$vc_op_ids_by_issue_nid = versioncontrol_project_issue_get_issue_operation_ids($issue_nids);
$commit_hashes_by_issue_nid = array();
foreach ($vc_op_ids_by_issue_nid as $issue_nid => $vc_op_ids) {
foreach ($vc_op_ids as $vc_op_id) {
$commit_hashes_by_issue_nid[$issue_nid][] = $vc_op_ids_by_id[$vc_op_id];
}
}
// Only set the cache if there were results that'll need refreshing later
if (!empty($commit_hashes_by_issue_nid)) {
drupal_static('versioncontrol_project_issue_git_resync_recover_' . $repository->repo_id, $commit_hashes_by_issue_nid);
// @todo Use delete query with join?
// Do it in chunks of 500 items.
foreach (array_chunk($issue_nids, 500) as $chunk) {
db_delete('versioncontrol_project_issue_operations')->condition('nid', array_values($chunk), 'IN')->execute();
}
}
}
/**
* Implements hook_versioncontrol_repository_post_resync().
*/
function versioncontrol_project_issue_git_versioncontrol_repository_post_resync(VersioncontrolRepository $repository, $bypass) {
if (!$repository instanceof VersioncontrolGitRepository) {
// Not a git repository.
return;
}
$commit_hashes_by_issue_nid = &drupal_static('versioncontrol_project_issue_git_resync_recover_' . $repository->repo_id);
if (empty($commit_hashes_by_issue_nid)) {
// Nothing to do.
return;
}
$data_to_insert = FALSE;
$insert_query = db_insert('versioncontrol_project_issue_operations')->fields(array('nid', 'vc_op_id'));
foreach ($commit_hashes_by_issue_nid as $issue_nid => $commit_hashes) {
// Add data only if the commits are still there.
foreach ($repository->loadCommits(array(), array('revision' => $commit_hashes)) as $vc_op_id => $operation) {
$data_to_insert = TRUE;
$insert_query->values(array('nid' => $issue_nid, 'vc_op_id' => $operation->vc_op_id));
}
}
if ($data_to_insert) {
$insert_query->execute();
}
drupal_static_reset('versioncontrol_project_issue_git_resync_recover_' . $repository->repo_id);
}
/**
* Implements hook_theme().
*/
function versioncontrol_project_issue_git_theme($variables) {
return array(
'versioncontrol_project_issue_git_operation_comment_item' => array(
'variables' => array(
'operation' => NULL,
),
),
);
}
/**
* Theme for an operation rendered inside a comment.
*/
function theme_versioncontrol_project_issue_git_operation_comment_item($variables) {
return theme('versioncontrol_project_issue_operation', $variables);
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment