'commitlog',
      'title' => t('Commit messages'),
      'callback' => 'commitlog_operations_page',
      'access' => $access,
      'type' => MENU_SUGGESTED_ITEM,
    );
    $items[] = array(
      'path' => 'commitlog/feed',
      'title' => t('Commit messages'),
      'callback' => 'commitlog_operations_rss',
      'access' => $access,
      'type' => MENU_CALLBACK,
    );
    // Search pages:
/*
    // until this works, no sense displaying this page
    // see http://drupal.org/node/59659
    $items[] = array(
      'path' => 'search/commitlog',
      'title' => t('Commit messages'),
      'callback' => 'commitlog_operations_page_search',
      'access' => $access,
      'type' => MENU_LOCAL_TASK,
      'weight' => 2,
    );
*/
  }
  else {
    if (arg(0) == 'user' && is_numeric(arg(1))) {
      $uid = arg(1);
      $accounts = versioncontrol_get_accounts(array('uids' => array($uid)));
      if (!empty($accounts)) {
        // If the user has a CVS account, add a 'track commit messages' tab to the tracker page.
        $items[] = array(
          'path' => 'user/'. $uid .'/track/code',
          'title' => t('Track code'),
          'callback' => 'commitlog_account_tracker',
          'callback arguments' => array($uid),
          'access' => user_access('access commit messages'),
          'type' => MENU_LOCAL_TASK,
          'weight' => 2,
        );
      }
    }
  }
  return $items;
}
/**
 * Implementation of hook_perm().
 */
function commitlog_perm() {
  return array('access commit messages');
}
/**
 * Implementation of hook_form_alter():
 * Add a fieldset for to the general settings form so that the user can
 * enable/disable admin notification mails and configure the pager limit.
 */
function commitlog_form_alter($form_id, &$form) {
  if ($form['#id'] == 'versioncontrol-settings-form') {
    $form['#validate']['commitlog_settings_validate'] = array();
    $form['#submit']['commitlog_settings_submit'] = array();
    $form['commitlog'] = array(
      '#type' => 'fieldset',
      '#title' => t('Commit messages'),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#weight' => 3,
    );
    $form['commitlog']['commitlog_send_notification_mails'] = array(
      '#type' => 'checkbox',
      '#title' => 'Send commit notification mails to the VCS administrator',
      '#description' => 'If this is enabled, each commit that is recorded on this site causes a notification mail to be sent to the VCS administrator\'s e-mail address. This mail includes all relevant commit data like the commit message and the files and directories that were changed. Note that disabling the Commit Log module also disables notification mails.',
      '#default_value' => variable_get('commitlog_send_notification_mails', 0),
    );
    $form['commitlog']['commitlog_pager'] = array(
      '#type' => 'textfield',
      '#title' => t('Number of commits per page'),
      '#description' => t('Controls how many commits can be shown on a single page of the !commitlog. Paging is used when there are more commits to show than specified by this value.', array('!commitlog' => l(t('commit message log'), 'commitlog'))),
      '#default_value' => variable_get('commitlog_pager', 10),
    );
  }
}
/**
 * Validation handler for the settings form:
 * Make sure that the pager limit is a positive number.
 */
function commitlog_settings_validate($form_id, $form_values) {
  if (!is_numeric($form_values['commitlog_pager']) || $form_values['commitlog_pager'] <= 0) {
    form_set_error('commitlog_pager', t('The number of commits per page needs to be a positive number.'));
  }
}
/**
 * Submit handler for the settings form.
 */
function commitlog_settings_submit($form_id, $form_values) {
  variable_set('commitlog_pager', $form_values['commitlog_pager']);
  variable_set('commitlog_send_notification_mails', $form_values['commitlog_send_notification_mails']);
}
/**
 * Implementation of hook_versioncontrol_operation():
 * If enabled, send out a notification mail to the VCS admin.
 */
function commitlog_versioncontrol_operation($op, $operation, $operation_items) {
  if ($op == 'insert') {
    if (variable_get('commitlog_send_notification_mails', 0)) {
      $mailto = variable_get('versioncontrol_email_address', 'versioncontrol@example.com');
      commitlog_send_notification_mail($mailto, $operation, $operation_items);
    }
  }
}
/**
 * Send out a notification mail to the given mail address.
 */
function commitlog_send_notification_mail($mailto, $operation, $operation_items) {
  $username = theme('versioncontrol_account_username',
    $operation['uid'], $operation['username'], $operation['repository'], TRUE, 'plaintext'
  );
  $variables = array();
  $variables['operation'] = $operation;
  $variables['operation_items'] = $operation_items;
  $subject = theme('commitlog_operation_subject', $variables, array('format' => 'plaintext'));
  $message = commitlog_operation($operation, $operation_items, array(
    'format' => 'plaintext',
    'username' => $username,
  ));
  $admin_mail = variable_get('versioncontrol_email_address', 'versioncontrol@example.com');
  $from = "$username <$admin_mail>";
  drupal_mail(
    'commitlog_notification_email', $mailto,
    $subject, $message, $from, array('X-Mailer' => 'Drupal')
  );
}
/**
 * Page callback for the 'commitlog' menu path.
 */
function commitlog_operations_page() {
  if ($error_message = _commitlog_check_request()) {
    return $error_message;
  }
  $constraints = _commitlog_get_constraints();
  return commitlog_operations($constraints);
}
/**
 * Return a (paged) log of version control operations rendered to HTML.
 */
function commitlog_operations($constraints) {
  drupal_add_css(drupal_get_path('module', 'commitlog') .'/commitlog.css');
  return theme('commitlog_operations_page', $constraints);
}
/**
 * Page callback for the 'track code' tab on the user page.
 */
function commitlog_account_tracker($uid) {
  $user = user_load(array('uid' => $uid));
  if (!$user) {
    drupal_not_found();
    exit();
  }
  drupal_set_title($user->name);
  $constraints = _commitlog_get_constraints();
  $constraints['uids'] = array($uid);
  drupal_add_css(drupal_get_path('module', 'commitlog') .'/commitlog.css');
  return theme('commitlog_operations_page', $constraints);
}
/**
 * Page callback for the 'commitlog/feed' menu path.
 */
function commitlog_operations_rss() {
  if ($error_message = _commitlog_check_request()) {
    exit();
  }
  $constraints = _commitlog_get_constraints();
  drupal_set_header('Content-Type: application/rss+xml; charset=utf-8');
  print theme('commitlog_rss', $constraints);
  exit();
}
/**
 * Check the $_REQUEST variable if it contains elements that can't be processed
 * in the current Drupal configuration.
 *
 * @return
 *   A string containing the appropriate error message, or NULL if everything
 *   can proceed as planned.
 */
function _commitlog_check_request() {
  if (isset($_REQUEST['nids']) || isset($_REQUEST['maintainers'])) {
    if (!module_exists('versioncontrol_project')) {
      return '
'. t('The given commit constraints can\'t be applied as the Version Control / Project Node Integration module is not enabled. In order to use the "nids" or "maintainer_uids" constraints, please enable this module.') .'
';
    }
  }
  return NULL;
}
/**
 * Retrieve the operation constraints for the page display by dissecting the
 * $_REQUEST variable.
 *
 * @return
 *   An array($constraints, $attributes) where $constraints is an array of
 *   commit constraints supposed to be passed to versioncontrol_get_operations(),
 *   and $attributes is an associative array of the processed arguments
 *   from $_REQUEST.
 */
function _commitlog_get_constraints() {
  $constraints = array();
  $attributes = array();
  $constraint_request_map = module_invoke_all('commitlog_constraints');
  // Transform query string into commit constraints. We use $_REQUEST because
  // we need to support both GET and POST requests.
  foreach ($constraint_request_map as $constraint => $request) {
    if (isset($request['multiple']) && isset($_REQUEST[$request['multiple']])) {
      $constraints[$constraint] = explode(',', $_REQUEST[$request['multiple']]);
    }
    if (isset($request['single']) && isset($_REQUEST[$request['single']])) {
      $constraints[$constraint][] = $_REQUEST[$request['single']];
    }
  }
  return $constraints;
}
/**
 * Implementation of hook_commitlog_constraints():
 * Provide a list of supported constraints and corresponding request attributes.
 */
function commitlog_commitlog_constraints() {
  return array(
    'vc_op_ids' => array('single' => 'id',       'multiple' => 'ids'),
    'usernames' => array('single' => 'username', 'multiple' => 'usernames'),
    'uids'      => array('single' => 'uid',      'multiple' => 'uids'),
    'vcs'       => array('multiple' => 'vcs'), // for both single and multiple
    'repo_ids'  => array('single' => 'repo',     'multiple' => 'repos'),
    'branches'  => array('single' => 'branch',   'multiple' => 'branches'),
    'tags'      => array('single' => 'tag',      'multiple' => 'tags'),
    'labels'    => array('single' => 'label',    'multiple' => 'labels'),
    'paths'     => array('single' => 'path',     'multiple' => 'paths'),
    // TODO: 'types' constraint
  );
}
function _commitlog_query_attributes($constraints) {
  $query_items = array();
  $constraint_request_map = module_invoke_all('commitlog_constraints');
  foreach ($constraints as $key => $values) {
    if (isset($constraint_request_map[$key])) {
      if (!is_array($values)) {
        $values = array($values);
      }
      if (isset($constraint_request_map[$key]['single']) && count($values) == 1) {
        $query_items[] = $constraint_request_map[$key]['single'] .'='. (string) reset($values);
      }
      else {
        $query_items[] = $constraint_request_map[$key]['multiple'] .'='. implode(',', $values);
      }
    }
  }
  return $query_items;
}
/**
 * Return an array of operations retrieved by versioncontrol_get_operations().
 * Paging is also used by emulating pager_query().
 */
function commitlog_get_paged_operations($constraints, $limit = NULL, $element = 0) {
  global $pager_page_array, $pager_total, $pager_total_items;
  $page = isset($_GET['page']) ? $_GET['page'] : '';
  $pager_page_array = explode(',', $page);
  $page = empty($pager_page_array[$element]) ? 0 : $pager_page_array[$element];
  if (!isset($limit)) {
    $limit = variable_get('commitlog_pager', 10);
  }
  $operations = versioncontrol_get_operations($constraints, $result_count, $page, $limit);
  // Emulate pager_query() in order to get a proper theme('pager').
  $pager_total_items[$element] = $result_count;
  $pager_total[$element] = ceil($pager_total_items[$element] / $limit);
  $pager_page_array[$element] = max(0, min((int)$pager_page_array[$element], ((int)$pager_total[$element]) - 1));
  return $operations;
}
/**
 * Return a formatted date string for the creation date of the given operation.
 * (This will also be used for grouping log messages.)
 */
function commitlog_operation_date($operation) {
  return format_date($operation['date'], 'custom', 'F j, Y');
}
/**
 * Return formatted output for displaying the given operations on an HTML page.
 */
function theme_commitlog_operations_page($constraints) {
  $operations = commitlog_get_paged_operations($constraints);
  if (empty($operations)) {
    return ''. t('No log messages found.') .'
';
  $operation_descriptions = array();
  foreach ($operations as $operation) {
    $operation_items = versioncontrol_get_operation_items($operation);
    $date = commitlog_operation_date($operation);
    $operation_descriptions[$date][] = array(
      commitlog_operation($operation, $operation_items, array('format' => 'html')),
    );
  }
  $header = array();
  foreach ($operation_descriptions as $date => $rows) {
    $output .= '
'. $date .'
      '. theme('table', $header, $rows) .'
';
  }
  if ($pager = theme('pager', NULL, variable_get('commitlog_pager', 10), 0, $attributes)) {
    $output .= $pager;
  }
  $output .= '
';
  $output .= '
'. $variables['caption'] ."
\n";
  if (!empty($variables['message'])) {
    $output .= '
'. $variables['message'] .'
'."\n";
  }
  $output .= '
'. $variables['items'] .'
'."\n";
  $output .= "
'. $item_header .'
'."\n";
      $output .= ''. theme('item_list', $lines) ."
\n";
    }
    else {
      $output .= ''. $item_header .' '. reset($lines) ."
\n";
    }
    return $output;
  }
  // else: plaintext
  $output = $location ."\n";
  foreach ($lines as $key => $line) {
    $lines[$key] = '- '. $line;
  }
  $output .= implode("\n", $lines);
  return $output;
}
function theme_commitlog_item($repository, $item, $path_override = NULL, $format = 'html') {
  $backends = versioncontrol_get_backends();
  if (!isset($backends[$repository['vcs']])) {
    return '';
  }
  $has_atomic_commits = FALSE;
  $backend = $backends[$repository['vcs']];
  if (in_array(VERSIONCONTROL_CAPABILITY_ATOMIC_COMMITS, $backend['capabilities'])) {
    $has_atomic_commits = TRUE;
  }
  $path = isset($path_override) ? $path_override : $item['path'];
  // For version control systems without atomic commits, display the revision
  // next to the path. For all other ones this doesn't make sense, as there is
  // only one global revision for all items, and that one is already displayed
  // in the commit title.
  $output = $path;
  if ($format == 'html') {
    $log_url = versioncontrol_get_url_item_log_view($repository, $item);
    if (!empty($log_url)) {
      $output = l($path, $log_url);
    }
  }
  if (empty($item['revision']) || $has_atomic_commits) {
    if ($format == 'html') {
      $view_url = versioncontrol_get_url_item_view($repository, $item);
      if (!empty($view_url)) {
        $output = l($path, $view_url);
      }
    }
    return $output;
  }
  else {
    return $output .' '. theme('commitlog_item_revision', $repository, $item, $format);
  }
}
function theme_commitlog_item_revision($repository, $item, $format = 'html') {
  $revision = versioncontrol_format_item_revision($repository, $item);
  if ($format == 'html' && !empty($revision)) {
    $view_url = versioncontrol_get_url_item_view($repository, $item);
    if (!empty($view_url)) {
      $revision = l($revision, $view_url);
    }
  }
  return $revision;
}
function theme_commitlog_diff_link($operation, $item) {
  // We can only come up with a diff link if we've got a source item.
  if (empty($item['source_items'])) {
    return '';
  }
  // Also, a diff only makes sense if the item was actually modified.
  if (in_array($item['action'], array(VERSIONCONTROL_ACTION_MODIFIED, VERSIONCONTROL_ACTION_MERGED))) {
    // Ok, everything's alright, let's get that diff link.
    $diff_url = versioncontrol_get_url_diff(
      $operation['repository'], $item, $item['source_items'][0]
    );
    if (!empty($diff_url)) {
      return l(t('diff'), $diff_url);
    }
  }
  return '';
}
function theme_commitlog_project($repository, $project, $format = 'html') {
  $project_node = node_load($project['nid']);
  if ($project_node) {
    if ($format == 'html') {
      return l($project_node->title, 'node/'. $project_node->nid);
    }
    return check_plain($project_node->title);
  }
  return '';
}