diff --git a/versioncontrol_project_issue/versioncontrol_project_issue.info b/versioncontrol_project_issue/versioncontrol_project_issue.info
new file mode 100644
index 0000000..aa965d5
--- /dev/null
+++ b/versioncontrol_project_issue/versioncontrol_project_issue.info
@@ -0,0 +1,6 @@
+name = "Version Control / Project issue integration"
+description = "Expose some functionality that integrates project_issue and Version Control API."
+package = Version Control
+dependencies[] = versioncontrol_project
+dependencies[] = project_issue
+core = 7.x
diff --git a/versioncontrol_project_issue/versioncontrol_project_issue.install b/versioncontrol_project_issue/versioncontrol_project_issue.install
new file mode 100644
index 0000000..c519135
--- /dev/null
+++ b/versioncontrol_project_issue/versioncontrol_project_issue.install
@@ -0,0 +1,32 @@
+<?php
+/**
+ * @file
+ * Version Control / Project issue integration
+ */
+
+/**
+ * Implements hook_schema().
+ */
+function versioncontrol_project_issue_schema() {
+  $schema['versioncontrol_project_issue_operations'] = array(
+    'description' => 'This table associates project issue nodes with versioncontrol operations.',
+    'fields' => array(
+      'nid' => array(
+        'description' => 'Foreign key for the project issue nid.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'vc_op_id' => array(
+        'description' => 'Foreign key for the operation({versioncontrol_operations}.vc_op_id).',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'primary key' => array('nid', 'vc_op_id'),
+  );
+  return $schema;
+}
diff --git a/versioncontrol_project_issue/versioncontrol_project_issue.module b/versioncontrol_project_issue/versioncontrol_project_issue.module
new file mode 100644
index 0000000..2afb20b
--- /dev/null
+++ b/versioncontrol_project_issue/versioncontrol_project_issue.module
@@ -0,0 +1,118 @@
+<?php
+/**
+ * @file
+ * Version Control / Project issue integration.
+ */
+
+/**
+ * Implements hook_node_load().
+ */
+function versioncontrol_project_issue_node_load($nodes, $types) {
+  $issue_node_types = project_issue_issue_node_types();
+  $intersection = array_intersect($issue_node_types, $types);
+  if (empty($intersection)) {
+    // Nothing to do.
+    return;
+  }
+  $vc_op_ids_by_nid = versioncontrol_project_issue_get_issue_operation_ids(array_keys($nodes));
+  if (empty($vc_op_ids_by_nid)) {
+    // Nothing to load.
+    return;
+  }
+  foreach ($vc_op_ids_by_nid as $nid => $vc_op_ids) {
+    $repository = versioncontrol_project_repository_load($nodes[$nid]->field_project[LANGUAGE_NONE][0]['target_id']);
+    $nodes[$nid]->versioncontrol_project_issue['operations'] = $repository->loadCommits($vc_op_ids);
+  }
+}
+
+/**
+ * Implements hook_node_update().
+ *
+ * Change the mappings when a project issue is changed of project.
+ */
+function versioncontrol_project_issue_node_update($node) {
+  if (!project_issue_node_type_is_issue($node->type)) {
+    return;
+  }
+  $project_id = $node->field_project[LANGUAGE_NONE][0]['target_id'];
+  $original_project_id = $node->original->field_project[LANGUAGE_NONE][0]['target_id'];
+  if ($original_project_id == $project_id) {
+    // Nothing to do.
+    return;
+  }
+  // Project was changed, remove operation associations with the issue.
+  db_delete('versioncontrol_project_issue_operations')->condition('nid', $node->nid)->execute();
+  // @todo Search all operations associated with the new project? (if decided,
+  // use a queue, since it can be time consuming).
+}
+
+/**
+ * Implements hook_node_delete().
+ */
+function versioncontrol_project_issue_node_delete($node) {
+  if (!project_issue_node_is_issue($node)) {
+    // Nothing to do.
+    return;
+  }
+  // Remove mappings on delete.
+  db_delete('versioncontrol_project_issue_operations')->condition('nid', $node->nid)->execute();
+}
+
+/**
+ * Implements hook_versioncontrol_entity_commit_delete().
+ *
+ * Repository synchronization removes commits, event processor plugins are
+ * executed later, so we cannot react at that time. So we use this hook to
+ * remove relations from our table.
+ */
+function versioncontrol_project_issue_versioncontrol_entity_commit_delete(VersioncontrolOperation $operation) {
+  db_delete('versioncontrol_project_issue_operations')->condition('vc_op_id', $operation->vc_op_id)->execute();
+}
+
+/**
+ * Implements hook_versioncontrol_repository_bypassing_purge().
+ *
+ * @todo db_delete seems to not support joins, is that possible to use for the
+ * generic case? i.e. no mysql(should be more efficient, in case a lot of
+ * issues are associated).
+ */
+function versioncontrol_project_issue_versioncontrol_repository_bypassing_purge(VersioncontrolRepository $repository) {
+  if (empty($repository->project_nid)) {
+    // No project associated.
+    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;
+  }
+  // Remove related rows from versioncontrol_project_issue_operations table.
+  $issue_nids = array_keys($result['node']);
+  // 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();
+  }
+}
+
+/**
+ * Helper to get the operation IDs.
+ *
+ * @param array $project_issue_nids
+ *   List of project issue nids.
+ *
+ * @return array
+ *   List of operation ids keyed by project issue node id.
+ */
+function versioncontrol_project_issue_get_issue_operation_ids($project_issue_nids) {
+  $vc_op_ids = array();
+  $result = db_query('SELECT nid, vc_op_id FROM {versioncontrol_project_issue_operations} WHERE nid IN (:nids)', array(':nids' => $project_issue_nids));
+  foreach ($result as $row) {
+    $vc_op_ids[$row->nid][] = $row->vc_op_id;
+  }
+  return $vc_op_ids;
+}
diff --git a/versioncontrol_project_issue/versioncontrol_project_issue_git/plugins/event_processor/VersioncontrolEventProcessorGitIssueMapper.inc b/versioncontrol_project_issue/versioncontrol_project_issue_git/plugins/event_processor/VersioncontrolEventProcessorGitIssueMapper.inc
new file mode 100644
index 0000000..9164b17
--- /dev/null
+++ b/versioncontrol_project_issue/versioncontrol_project_issue_git/plugins/event_processor/VersioncontrolEventProcessorGitIssueMapper.inc
@@ -0,0 +1,136 @@
+<?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;
+          }
+          // Make sure we are dealing with project issues.
+          $possible_issues = node_load_multiple($nids_on_message);
+          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'] != $project_nid) {
+              continue;
+            }
+            $operation_nid_maps[] = array(
+              'nid' => $possible_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.
+   */
+  function getIssuesFromMessage($message) {
+    if (preg_match_all($this->messagePattern, $message, $matches)) {
+      return $matches[1];
+    }
+    return array();
+  }
+}
diff --git a/versioncontrol_project_issue/versioncontrol_project_issue_git/plugins/event_processor/git_issue_mapper.inc b/versioncontrol_project_issue/versioncontrol_project_issue_git/plugins/event_processor/git_issue_mapper.inc
new file mode 100644
index 0000000..2f9fec3
--- /dev/null
+++ b/versioncontrol_project_issue/versioncontrol_project_issue_git/plugins/event_processor/git_issue_mapper.inc
@@ -0,0 +1,14 @@
+<?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',
+  ),
+);
diff --git a/versioncontrol_project_issue/versioncontrol_project_issue_git/versioncontrol_project_issue_git.info b/versioncontrol_project_issue/versioncontrol_project_issue_git/versioncontrol_project_issue_git.info
new file mode 100644
index 0000000..4c1ef66
--- /dev/null
+++ b/versioncontrol_project_issue/versioncontrol_project_issue_git/versioncontrol_project_issue_git.info
@@ -0,0 +1,7 @@
+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
diff --git a/versioncontrol_project_issue/versioncontrol_project_issue_git/versioncontrol_project_issue_git.module b/versioncontrol_project_issue/versioncontrol_project_issue_git/versioncontrol_project_issue_git.module
new file mode 100644
index 0000000..54e3cbc
--- /dev/null
+++ b/versioncontrol_project_issue/versioncontrol_project_issue_git/versioncontrol_project_issue_git.module
@@ -0,0 +1,92 @@
+<?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);
+}
