'Repositories', 'page callback' => 'drupal_get_form', 'page arguments' => array('repoview_selection_form'), 'access arguments' => array($browse_access), 'type' => MENU_SUGGESTED_ITEM, ); $items['repoview/%repoview_location'] = array( 'title callback' => 'repoview_location_title', 'title arguments' => array(1), 'page callback' => 'drupal_get_form', 'page arguments' => array('repoview_browser_form', 1), 'access arguments' => array($browse_access), 'load arguments' => array('%map', '%index'), 'type' => MENU_CALLBACK, ); return $items; } /** * Menu wildcard loader for a repository location ('%repoview_location') that * can be browsed with Repoview. Returns FALSE if the repository is not * browsable, but does not check if the item in the repository actually exists. * If the repository is browsable, this function returns an associative array * with keys 'repository' and 'path' filled in. Requires array('%map', '%index') * as load arguments so that the item path can be loaded correctly. */ function repoview_location_load($repo_id, $map_array, $index) { $repository = versioncontrol_repository_load($repo_id); if (!$repository || !_repoview_repository_supports_browsing($repository)) { return FALSE; } // Construct the path out of the trailing menu path arguments. $path = (count($map_array) > $index + 1) ? '/'. join('/', array_slice($map_array, $index + 1)) : '/'; return array('repository' => $repository, 'path' => $path); } /** * Title callback for a repository location loaded by repoview_location_load(). */ function repoview_location_title($location) { if ($location['path'] != '/') { return check_plain($location['repository']['name']); } return check_plain(basename($location['path'])); } /** * Implementation of hook_perm(). */ function repoview_perm() { return array('browse version control repositories'); } /** * Function to determine if a VCS backend provides enough functionality * to support browsing. */ function _repoview_repository_supports_browsing($repository) { $vcs = $repository['vcs']; return versioncontrol_backend_implements($vcs, 'get_item') && versioncontrol_backend_implements($vcs, 'get_directory_contents') && versioncontrol_backend_implements($vcs, 'export_file'); } /** * Return the URL for viewing a given item with repoview. */ function repoview_item_url($repository, $item) { return 'repoview/'. $repository['repo_id'] . drupal_urlencode($item['path']); } /** * Form callback for the 'repoview' menu path. */ function repoview_selection_form($form_state) { $form = array(); $repositories = versioncontrol_get_repositories(); foreach ($repositories as $repo_id => $repository) { if (!_repoview_repository_supports_browsing($repository)) { unset($repositories[$repo_id]); } } if (empty($repositories)) { $form['empty'] = array( '#value' => '

'. t('No repositories available to browse.') .'

', ); return $form; } if (count($repositories) == 1) { $only_repo = reset($repositories); drupal_goto('repoview/'. $only_repo['repo_id']); } $header = array(''); $rows = array(); foreach ($repositories as $repo_id => $repository) { $rows[] = array(l($repository['name'], 'repoview/'. $repo_id)); } $form['table'] = array( '#value' => theme('table', $header, $rows), ); return $form; } /** * Form callback for the 'repoview/%repoview_location[/...]' menu path. */ function repoview_browser_form($form_state, $location) { $repository = $location['repository']; $path = $location['path']; $item = versioncontrol_get_item($repository, $path); _repoview_breadcrumb($repository, $path); if (empty($item)) { drupal_set_title(check_plain(basename($path))); $form['empty'] = array( '#value' => '

'. t('File or directory doesn\'t exist at this revision, or doesn\'t exist at all.') .'

', ); return $form; } drupal_add_css(drupal_get_path('module', 'repoview') .'/repoview.css'); if (versioncontrol_is_directory_item($item)) { return repoview_directory_contents_form($form_state, $repository, $item); } return repoview_file_contents_form($form_state, $repository, $item); } /** * Form callback for 'repoview/%repoview_location[/...]' * if the path is a directory item. */ function repoview_directory_contents_form($form_state, $repository, $dir_item) { $form = array(); $children = versioncontrol_get_directory_contents($repository, $dir_item); if (!isset($children)) { $form['empty'] = array( '#value' => '

'. t('Unable to fetch directory contents.') .'

', ); return $form; } unset($children[$dir_item['path']]); $children = _repoview_item_listing_sort($children); $header = array(t('File'), t('Rev.'), t('Author'), t('Age'), t('Message')); $rows = array(); if ($dir_item['path'] != '/') { $parent_item = versioncontrol_get_parent_item($repository, $dir_item); $item_url = repoview_item_url($repository, $parent_item); // First column: image. $image = theme('image', drupal_get_path('module', 'repoview') .'/icons/folder-parent.png'); $image_link = l($image, $item_url, array('html' => TRUE)); $image = '
'. $image_link .'
'; // Second column: directory name. $item_link = l(''. t('Parent directory') .'', $item_url, array('html' => TRUE)); // Rest of the columns (even easier). $rows[] = array( '
'. $image . $item_link .'
', '', '', '', '' ); } if (empty($children)) { $rows[] = array(t('Directory is empty.'), '', '', '', ''); } else { versioncontrol_fetch_item_commit_operations($repository, $children); foreach ($children as $path => $item) { // First & second column: icon & file/directory name. $name = check_plain(basename($item['path'])); $item_url = repoview_item_url($repository, $item); if (versioncontrol_is_directory_item($item)) { $image = theme('image', drupal_get_path('module', 'repoview') .'/icons/folder.png'); $name = ''. $name .'/'; } else { $image = theme('image', drupal_get_path('module', 'repoview') .'/icons/application-octet-stream.png'); } $image_link = l($image, $item_url, array('html' => TRUE)); $image = '
'. $image_link .'
'; $item_link = l($name, $item_url, array('html' => TRUE)); // Author and commit message columns. if (empty($item['commit_operation'])) { $author = ''; $age = ''; $message = ''; } else { $operation = $item['commit_operation']; $author = theme('versioncontrol_account_username', $operation['uid'], $operation['username'], $repository, FALSE ); $age = t('!time ago', array( '!time' => format_interval(time() - $operation['date'], 1), )); if (drupal_strlen($operation['message']) <= 60) { $message = check_plain($operation['message']); } else { $tooltip = check_plain($operation['message']); $message = check_plain(drupal_substr($operation['message'], 0, 57)) .'...'; $message = ''. $message .''; } } // Combine all of these texts into a single row. $rows[] = array( '
'. $image . $item_link .'
', check_plain($item['revision']), $author, $age, $message, ); } } $form['listing'] = array( '#value' => theme('table', $header, $rows), ); return $form; } /** * Sort items by type (directories before files), then name (alphabetically). */ function _repoview_item_listing_sort($items) { $dirs = array(); $files = array(); foreach ($items as $path => $item) { if (versioncontrol_is_directory_item($item)) { $dirs[$path] = $item; } else { $files[$path] = $item; } } ksort($dirs); ksort($files); return array_merge($dirs, $files); } /** * Set the breadcrumb according to the given repository and path. */ function _repoview_breadcrumb($repository, $path) { if ($path == '/') { return; // no need to change the breadcrumb, it's already correct } $breadcrumb = drupal_get_breadcrumb(); $parts = explode('/', $path); array_pop($parts); // don't include the last name, we're currently viewing it $path = ''; foreach ($parts as $part) { $title = empty($part) ? $repository['name'] : $part; $path .= $part .'/'; $breadcrumb[] = l($title, 'repoview/'. $repository['repo_id'] . rtrim($path, '/')); } drupal_set_breadcrumb($breadcrumb); } /** * Form callback for 'repoview/%repoview_location[/...]' * if the path is a file item. */ function repoview_file_contents_form($form_state, $repository, $file_item, $force_download = FALSE) { $form = array(); $filepath = versioncontrol_export_file($repository, $file_item); if (empty($filepath)) { $form['error'] = array( '#value' => '

'. t('Error accessing the file.') .'

', ); return $form; } // Retrieve the mimetype of the file - if possible, because that function is // deprecated and the fileinfo PECL extension seems not to be built into // PHP 5 at least. if (function_exists('mime_content_type')) { $mimetype = mime_content_type($filepath); } $is_text = $is_image = FALSE; if (isset($mimetype) && (strpos($mimetype, 'text/') !== FALSE)) { $is_text = TRUE; } if (isset($mimetype) && in_array($mimetype, array('image/png', 'image/jpeg', 'image/gif'))) { $is_image = TRUE; } // We don't want to show normal binary files, let's just transfer them as is. if ($force_download || (!$is_text /* && !$is_image ...later */)) { // Also, make sure we delete the file even if file_transfer() exits. register_shutdown_function('_repoview_delete_file_copy', $filepath); // Transfer the file! $headers = array( 'Content-Type: '. mime_header_encode($mimetype), 'Content-Length: '. filesize($filepath), // Force file download and set the default target filename. 'Content-Disposition: attachment; filename="'. basename($file_item['path']) .'";', ); file_transfer($filepath, $headers); // exit(); -- called by file_transfer() } if ($is_text) { // Cool, it's a text file. Let's display it in a nice table with a row // for each line. (repoview.css makes sure it looks nice.) $lines = file($filepath); _repoview_delete_file_copy($filepath); $linecount = 1; $line_numbers = ''; $content = ''; $header = array(); $rows = array(); foreach ($lines as $line) { $rows[] = array( '
'. $linecount .'
', '
'. trim(check_plain($line), "\n\r") .'
' ); ++$linecount; } $contents = theme('table', $header, $rows, $attributes); $contents = '
'. $contents .'
'; } else if ($is_image) { // add a link to this same function with $force_download == TRUE, // i.e. same path with 'view=download' query attribute (right?). } $form['contents'] = array( '#value' => '
'. $contents .'
', ); return $form; } /** * Registered shutdown function, called after the request has ended: * Delete the file copy that versioncontrol_export_file() had created. */ function _repoview_delete_file_copy($file_path) { @unlink($file_path); }