Commit 658556c7 authored by Marco Villegas's avatar Marco Villegas
Browse files

Issue #443000: Added a git specific module extending versioncontrol_project_issue.

- Provides a versioncontrol event processor plugin, which do the
  matching.
- Implements hook_versioncontrol_repository_{pre,post}_resync() doing a
  best effort to maintain existing mappings based on git commit hashes.
- Provides a drush command, vc-project-issue-git-resync-maps, to
  re-synchronize mappings based on project or repository ids.
parent f9eed82e
Loading
Loading
Loading
Loading
+148 −0
Changes for versioncontrol_project_issue/versioncontrol_project_issue_git/plugins/event_processor/VersioncontrolEventProcessorGitIssueMapper.inc: 148 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php
/**
 * @file
 * Maps issues with operations on git repositories based on commit messages.
 */

class VersioncontrolEventProcessorGitIssueMapper implements VersioncontrolSynchronizationEventProcessorInterface, VersioncontrolPluginConfigurationInterface {
  /**
   * Associated repository.
   *
   * @var VersioncontrolRepository
   */
  protected $repository;

  /**
   * Pattern to find issue numbers in commit messages.
   */
  protected $messagePattern;

  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);
      // Try to deal with big pushes loading operations in chunks of 500 items.
      foreach (array_chunk($commit_hashes, 500) 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;
          }
          $valid_issue_nids = $this->checkValidIssues($nids_on_message);
          foreach ($valid_issue_nids as $valid_issue_nid) {
            $operation_nid_maps[] = array(
              'nid' => $valid_issue_nid,
              'vc_op_id' => $operation->vc_op_id
            );
          }
        }
      }
    }
    if (empty($operation_nid_maps)) {
      // Nothing to do.
      return;
    }
    // Insert mappings.
    $insert_query = db_insert('versioncontrol_project_issue_operations')->fields(array('nid', 'vc_op_id'));
    foreach ($operation_nid_maps as $operation_nid_map) {
      $insert_query->values($operation_nid_map);
    }
    $insert_query->execute();
  }

  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.
   */
  public function checkValidIssues($nids) {
    $valid_nids = array();
    // Make sure we are dealing with project issues.
    $possible_issues = node_load_multiple($nids);
    foreach ($possible_issues as $possible_issue) {
      if (!project_issue_node_is_issue($possible_issue)) {
        continue;
      }
      // Associate only if repository project is the same than issue
      // project.
      if (empty($possible_issue->field_project[LANGUAGE_NONE][0]['target_id'])) {
        continue;
      }
      if ($possible_issue->field_project[LANGUAGE_NONE][0]['target_id'] != $this->repository->project_nid) {
        continue;
      }
      $valid_nids[] = $possible_issue->nid;
    }
    return $valid_nids;
  }
}
+14 −0
Changes for versioncontrol_project_issue/versioncontrol_project_issue_git/plugins/event_processor/git_issue_mapper.inc: 14 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?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',
  ),
);
+127 −0
Changes for versioncontrol_project_issue/versioncontrol_project_issue_git/versioncontrol_project_issue_git.drush.inc: 127 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?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);
  $git_backend = versioncontrol_get_backends('git');

  $default_event_processors = variable_get('versioncontrol_repository_plugin_defaults_git_event_processors');
  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']->repo_id;
      }
      print_r($ids);
    case 'repositories':
      $repositories = $git_backend->loadEntities('repo', $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 = array();
        // 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);
          foreach ($valid_issue_nids as $valid_issue_nid) {
            $operation_nid_maps[] = array(
              'nid' => $valid_issue_nid,
              'vc_op_id' => $row['vc_op_id'],
            );
          }
        }
        if (empty($operation_nid_maps)) {
          // Nothing to do.
          return;
        }
        // Insert mappings.
        $insert_query = db_insert('versioncontrol_project_issue_operations')->fields(array('nid', 'vc_op_id'));
        foreach ($operation_nid_maps as $operation_nid_map) {
          $insert_query->values($operation_nid_map);
        }
        $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);
  }
}
+7 −0
Changes for versioncontrol_project_issue/versioncontrol_project_issue_git/versioncontrol_project_issue_git.info: 7 added lines, 0 removed lines.
Original line number Diff line number Diff line
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
+92 −0
Changes for versioncontrol_project_issue/versioncontrol_project_issue_git/versioncontrol_project_issue_git.module: 92 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?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);
}