'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 .= '
';
}
$output .= '- '. theme('commitlog_commit', $commit) .'
';
}
$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 .= '
'."\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 '';
}