'commitlog', 'title' => t('Commit Log'), 'callback' => 'commitlog_page', 'access' => $access, 'type' => MENU_SUGGESTED_ITEM, ); $items[] = array( 'path' => 'commitlog/feed', 'title' => t('Commit Log'), 'callback' => 'commitlog_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_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'); } /** * Page callback for the 'commitlog' menu path. */ function commitlog_page() { if ($error_message = _commitlog_check_request()) { return $error_message; } list($constraints, $attributes) = _commitlog_get_commits(); $commits = commitlog_get_paged_commits($constraints); drupal_add_css(drupal_get_path('module', 'commitlog') .'/commitlog.css'); return theme('commitlog_page', $commits, $attributes); } /** * Page callback for the 'commitlog/feed' menu path. */ function commitlog_rss() { if ($error_message = _commitlog_check_request()) { exit(); } list($constraints, $attributes) = _commitlog_get_commits(); $commits = commitlog_get_paged_commits($constraints); drupal_set_header('Content-Type: text/xml; charset=utf-8'); print theme('commitlog_rss', $commits); exit(); } /** * 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(); } $_REQUEST['uid'] = $uid; drupal_set_title($user->name); list($constraints, $attributes) = _commitlog_get_commits(); $commits = commitlog_get_paged_commits($constraints); unset($_REQUEST['uid']); drupal_add_css(drupal_get_path('module', 'commitlog') .'/commitlog.css'); return theme('commitlog_page', $commits, array()); } /** * Retrieve the commit 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_commits(), * and $attributes is an associative array of the processed arguments * from $_REQUEST. */ function _commitlog_get_commits() { $constraints = array(); $attributes = array(); // Transform query string into query string. We use $_REQUEST because we // need to support both GET and POST requests. if (isset($_REQUEST['commits'])) { $constraints['commit_ids'] = explode(',', $_REQUEST['commits']); $attributes['commits'] = $_REQUEST['commits']; } if (isset($_REQUEST['usernames'])) { $constraints['usernames'] = explode(',', $_REQUEST['usernames']); $attributes['usernames'] = $_REQUEST['usernames']; } if (isset($_REQUEST['uids'])) { $constraints['uids'] = explode(',', $_REQUEST['uids']); $attributes['uids'] = $_REQUEST['uids']; } if (isset($_REQUEST['vcs'])) { $constraints['vcs'] = explode(',', $_REQUEST['vcs']); $attributes['vcs'] = $_REQUEST['vcs']; } if (isset($_REQUEST['branches'])) { $constraints['branches'] = explode(',', $_REQUEST['branches']); $attributes['branches'] = $_REQUEST['branches']; } if (isset($_REQUEST['paths'])) { $constraints['paths'] = explode(',', $_REQUEST['paths']); $attributes['paths'] = $_REQUEST['paths']; } if (isset($_REQUEST['repos'])) { $constraints['repo_ids'] = explode(',', $_REQUEST['repos']); $attributes['repos'] = $_REQUEST['repos']; } // filter by commit message search? ...use cases? if (module_exists('versioncontrol_project')) { $project_constraints = array(); if (isset($_REQUEST['nids'])) { $project_constraints['nids'] = explode(',', $_REQUEST['nids']); $attributes['nids'] = $_REQUEST['nids']; } if (isset($_REQUEST['maintainer_uids'])) { $project_constraints['maintainer_uids'] = explode(',', $_REQUEST['maintainers']); $attributes['maintainers'] = $_REQUEST['maintainers']; } if (!empty($project_constraints)) { $constraints = versioncontrol_project_get_commit_constraints( $constraints, $project_constraints ); } } return array($constraints, $attributes); } /** * Return an array of commits, resulting from a versioncontrol_get_commits() * call. Paging is also used by emulating pager_query(). */ function commitlog_get_paged_commits($constraints, $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]; $limit = variable_get('commitlog_pager', 10); $commits = versioncontrol_get_commits($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 $commits; } /** * 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; } /** * Return a formatted date string if the given timestamp is on a different day * than the one that was passed previous call to this function, or NULL if the * previous call's timestamp argument is on the same day as this one. */ function _commitlog_date($timestamp) { static $last; $date = format_date($timestamp, 'custom', 'F j, Y'); if ($date != $last) { $last = $date; return $date; } return NULL; } /** * Return formatted output for displaying the given commits on an HTML page. */ function theme_commitlog_page($commits, $attributes) { if (empty($commits)) { return '

'. t('No commit messages found.') .'

'; } $output = '
'; $initial = TRUE; foreach ($commits as $commit) { $date = _commitlog_date($commit['date']); if (isset($date)) { // begin a new list for each day if (!$initial) { $output .= ''; $initial = FALSE; } $output .= "

$date

"; $output .= ''; if ($pager = theme('pager', NULL, variable_get('commitlog_pager', 10), 0, $attributes)) { $output .= $pager; } $output .= '
'; $query_items = array(); foreach ($attributes as $key => $value) { $query_items[] = $key .'='. $value; } $query = empty($query_items) ? NULL : implode('&', $query_items); $output .= theme('feed_icon', url('commitlog/feed', $query)); return $output; } function theme_commitlog_rss($commits) { global $languages, $base_url; $language = empty($languages) ? 'en' : reset(array_keys($languages)); $items = ''; foreach ($commits as $commit) { $location = theme('commitlog_commit_location', $commit); $username = theme('commitlog_account_username', $commit['uid'], $commit['username'], $commit['repository'] ); $title = t('!location by @username', array('!location' => $location, '@username' => $username)); $link = url('commitlog', 'commits='. $commit['commit_id'], NULL, TRUE); $extra = array( array('key' => 'pubDate', 'value' => gmdate('r', $commit['date'])), array('key' => 'guid', 'value' => 'Commit '. $commit['commit_id'] .' at '. $base_url, 'attributes' => array('isPermaLink' => 'false'), ), ); $text = theme('commitlog_commit', $commit, $username); $items .= format_rss_item($title, $link, $text, $extra); } $title = t('Commit messages for @site', array('@site' => variable_get('site_name', 'Drupal'))); $link = url('commitlog', NULL, NULL, TRUE); $output = "\n"; $output .= "\n"; $output .= format_rss_channel($title, $link, $title, $items, $language); $output .= "\n"; return $output; } function theme_commitlog_commit($commit, $username = NULL) { if (!isset($username)) { $username = theme('commitlog_account_username', $commit['uid'], $commit['username'], $commit['repository'] ); } $commit_actions = versioncontrol_get_commit_actions($commit); $actions = theme('commitlog_commit_actions', $commit, $commit_actions); $message = theme('commitlog_commit_message', $commit); $branches = theme('commitlog_commit_branches', $commit); $output .= '
'; $output .= '
'; $output .= t('Commit !id by !name at !time in !repo:', array( '!id' => theme('commitlog_commit_identifier', $commit), '!repo' => theme('commitlog_repository', $commit['repository']), '!name' => $username, '!time' => format_date($commit['date'], 'custom', 'H:i'), )); $output .= "
\n"; // class "title" $output .= '
'. $actions .'
'."\n"; $output .= '
'. $message .'
'."\n"; $output .= "
\n"; // class "commit" return $output; } function theme_commitlog_account_username($uid, $username, $repository, $replace_with_drupal_username = TRUE) { if ($uid && $replace_with_drupal_username) { $user = user_load(array('uid' => $uid)); } if (!$user) { return l(check_plain($username), 'commitlog', NULL, 'usernames='. check_plain($username) .'&repos='. $repository['repo_id']); // TODO: find out the proper URL encoding function for the second $username } return theme('username', $user); } function theme_commitlog_repository($repository) { return l(check_plain($repository['name']), 'commitlog', NULL, 'repos='. $repository['repo_id']); } function theme_commitlog_commit_location($commit, $directory_item = NULL, $html = TRUE) { $item = isset($directory_item) ? $directory_item : versioncontrol_get_commit_directory_item($commit); $location = theme('commitlog_item', $commit['repository'], $item, $directory_item['path'], $html); $branches = theme('commitlog_commit_branches', $commit, $html); if (!empty($branches)) { $location = t('!branches: !directory', array( '!directory' => $location, '!branches' => $branches, )); } return $location; } function theme_commitlog_commit_identifier($commit, $html = TRUE) { $id = empty($commit['revision']) ? $commit['commit_id'] : $commit['revision']; if ($html) { $commit_url = versioncontrol_get_url_commit_view($commit); $id = empty($commit_url) ? theme('commitlog_commit_id', $commit['commit_id'], $id) : ''. $id .''; } return $id; } function theme_commitlog_commit_id($commit_id, $text = NULL) { if (!isset($text)) { $text = $commit_id; } return l($text, 'commitlog', NULL, 'commits='. $commit_id); } function theme_commitlog_commit_message($commit) { // Link to issues whose numbers are given in the commit message. $message = htmlspecialchars($commit['message']); $matches = array(); if (preg_match('/(?:(?:fix|add|patch)(?:ed)?\s+#?|#)(\d+)/i', $message, $matches)) { foreach ($matches as $match) { $link = strtr('!text', array( '!text' => $match[0], '!url' => versioncontrol_get_url_tracker($commit['repository'], $match[1]), )); $message = strtr($message, array($match[0] => $link)); } } return $message; } function theme_commitlog_commit_branches($commit, $html = TRUE) { $branches = versioncontrol_get_commit_branches($commit); if (empty($branches)) { return ''; } if ($html) { return l(implode(', ', $branches), 'commitlog', NULL, 'branches='. implode(',', $branches)); } return implode(', ', $branches); } function theme_commitlog_commit_actions($commit, $commit_actions, $html = TRUE) { if (empty($commit_actions)) { return ''; } $commit_directory_item = versioncontrol_get_commit_directory_item($commit); if (module_exists('versioncontrol_project')) { $project = versioncontrol_project_get_project_for_item($commit['repository'], $commit['directory']); if (isset($project)) { $project_item = versioncontrol_get_parent_item( $commit['repository'], $commit_directory_item, $project['directory'] ); $project_string = theme('commitlog_project', $commit['repository'], $project, $html); } } $directory_item = isset($project_item) ? $project_item : $commit_directory_item; $lines = array(); foreach ($commit_actions as $path => $action) { $item = versioncontrol_get_affected_item($action); if (module_exists('versioncontrol_project')) { if (isset($project)) { $item_project = $project; } else { $item_project = versioncontrol_project_get_project_for_item( $commit['repository'], $item['path'] ); } } $item_path = substr($item['path'], strlen($directory_item['path']) + 1); if ($item['type'] == VERSIONCONTROL_ITEM_DIRECTORY) { $item_path = t('!path [directory]', array('!path' => $item_path)); } $item_string = theme('commitlog_item', $commit['repository'], $item, $item_path, $html); if (isset($action['source items'])) { $oldrev_string = theme('commitlog_item_revision', $commit['repository'], $action['source items'][0], $html ); } if (isset($action['source items'])) { $olditems = array(); foreach ($action['source items'] as $source_item) { $item_path = substr($source_item['path'], strlen($directory_item['path']) + 1); $olditems[] = theme('commitlog_item', $commit['repository'], $source_item, $item_path, $html); } $olditems_string = implode(', ', $olditems); } $show_diff = FALSE; if ($html) { $diff_string = theme('commitlog_diff_link', $commit, $action); if (!empty($diff_string)) { $show_diff = TRUE; } } switch ($action['action']) { case VERSIONCONTROL_ACTION_ADDED: $action_string = t('!item (added)'); break; case VERSIONCONTROL_ACTION_MODIFIED: $action_string = $show_diff ? t('!item (modified, previous: !oldrev, !diff)') : t('!item (modified, previous: !oldrev)'); break; case VERSIONCONTROL_ACTION_MOVED: $action_string = $show_diff ? t('!item (moved from !olditems, !diff)') : t('!item (moved from !olditems)'); break; case VERSIONCONTROL_ACTION_COPIED: $action_string = $show_diff ? t('!item (copied from !olditems, !diff)') : t('!item (copied from !olditems)'); break; case VERSIONCONTROL_ACTION_MERGED: $action_string = $show_diff ? t('!item (merged from !olditems, !diff)') : t('!item (merged from !olditems)'); break; case VERSIONCONTROL_ACTION_DELETED: $action_string = t('!path (deleted: !oldrev)'); break; default: return 'Error: action type enum value not known: '. $action['action']; } if ($html && isset($item_project)) { $project_string = isset($project_string) ? $project_string : theme('commitlog_project', $commit['repository'], $item_project, $html); $action_string = t('!project: !action', array('!action' => $action_string)); } $lines[] = strtr(' '. $action_string, array( '!project' => $project_string, '!item' => $item_string, '!path' => $item_path, '!oldrev' => $oldrev_string, '!olditems' => $olditems_string, '!diff' => $diff_string, )); } $location = t('on !location', array( '!location' => theme('commitlog_commit_location', $commit, $directory_item), )); $output = '
'. $location .'
'."\n"; $output .= ($html ? '
'. theme('item_list', $lines) .'
' : implode("\n", $lines)) ."\n"; return $output; } function theme_commitlog_item($repository, $item, $path = NULL, $html = TRUE) { $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; } if (!isset($path)) { $path = $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. if (empty($item['revision']) || $has_atomic_commits) { $output = $path; if ($html) { $view_url = versioncontrol_get_url_item_view($repository, $item); if (!empty($view_url)) { $output = ''. $path .''; } } return $output; } else { return $path .' '. theme('commitlog_item_revision', $repository, $item, $html); } } function theme_commitlog_item_revision($repository, $item, $html = TRUE) { if (empty($item['revision'])) { return ''; } $revision = $item['revision']; if ($html) { $view_url = versioncontrol_get_url_item_view($repository, $item); if (!empty($view_url)) { $revision = ''. $item['revision'] .''; } } return $revision; } function theme_commitlog_diff_link($commit, $commit_action) { // If the item was modified, link to the diff URL. if (in_array($commit_action['action'], array(VERSIONCONTROL_ACTION_MODIFIED, VERSIONCONTROL_ACTION_MERGED)) || $action['modified']) { $diff_url = versioncontrol_get_url_diff( $commit['repository'], $commit_action['current item'], $commit_action['source items'][0] ); if (!empty($diff_url)) { return ''. t('diff') .''; } } return ''; } function theme_commitlog_project($repository, $project, $html = TRUE) { $project_node = node_load($project['nid']); if ($project_node) { if ($html) { return l(check_plain($project_node->title), 'node/'. $project_node->nid); } return check_plain($project_node->title); } return ''; }