gforge-plugin-scmcvs/rpm-specific/cron.d/gforge-plugin-scmcvs -text svneol=unset#application/octet-stream
gforge-plugin-scmcvs/rpm-specific/cronjobs/.keepme -text svneol=unset#application/octet-stream
gforge-plugin-scmcvs/rpm-specific/languages/.keepme -text svneol=unset#application/octet-stream
-gforge-plugin-scmcvs/www/cvsweb/icons/back.gif -text
-gforge-plugin-scmcvs/www/cvsweb/icons/binary.gif -text
-gforge-plugin-scmcvs/www/cvsweb/icons/dir.gif -text
-gforge-plugin-scmcvs/www/cvsweb/icons/miniback.gif -text
-gforge-plugin-scmcvs/www/cvsweb/icons/minidir.gif -text
-gforge-plugin-scmcvs/www/cvsweb/icons/minigraph.png -text
-gforge-plugin-scmcvs/www/cvsweb/icons/minitext.gif -text
-gforge-plugin-scmcvs/www/cvsweb/icons/text.gif -text
gforge-plugin-scmsvn/www/viewcvs/icons/back.gif svneol=native#unset
gforge-plugin-scmsvn/www/viewcvs/icons/forward.gif svneol=native#unset
gforge-plugin-scmsvn/www/viewcvs/icons/small/back.gif svneol=native#unset
gforge/www/jscook/ThemeXP/plus.gif svneol=native#unset
gforge/www/jscook/ThemeXP/plusbottom.gif svneol=native#unset
gforge/www/jscook/ThemeXP/spacer.gif svneol=native#unset
+gforge/www/scm/viewvc/lib/accept.pyc svneol=native#unset
+gforge/www/scm/viewvc/lib/compat.pyc svneol=native#unset
+gforge/www/scm/viewvc/lib/config.pyc svneol=native#unset
+gforge/www/scm/viewvc/lib/debug.pyc svneol=native#unset
+gforge/www/scm/viewvc/lib/ezt.pyc svneol=native#unset
+gforge/www/scm/viewvc/lib/idiff.pyc svneol=native#unset
+gforge/www/scm/viewvc/lib/popen.pyc svneol=native#unset
+gforge/www/scm/viewvc/lib/sapi.pyc svneol=native#unset
+gforge/www/scm/viewvc/lib/vclib/__init__.pyc svneol=native#unset
+gforge/www/scm/viewvc/lib/vclib/svn/__init__.pyc svneol=native#unset
+gforge/www/scm/viewvc/lib/viewvc.pyc svneol=native#unset
+gforge/www/scm/viewvc/templates/docroot/images/annotate.png -text
+gforge/www/scm/viewvc/templates/docroot/images/back.png -text
+gforge/www/scm/viewvc/templates/docroot/images/back_small.png -text
+gforge/www/scm/viewvc/templates/docroot/images/broken.png -text
+gforge/www/scm/viewvc/templates/docroot/images/chalk.jpg svneol=native#unset
+gforge/www/scm/viewvc/templates/docroot/images/cvsgraph_16x16.png -text
+gforge/www/scm/viewvc/templates/docroot/images/cvsgraph_32x32.png -text
+gforge/www/scm/viewvc/templates/docroot/images/diff.png -text
+gforge/www/scm/viewvc/templates/docroot/images/dir.png -text
+gforge/www/scm/viewvc/templates/docroot/images/down.png -text
+gforge/www/scm/viewvc/templates/docroot/images/download.png -text
+gforge/www/scm/viewvc/templates/docroot/images/feed-icon-16x16.jpg svneol=native#unset
+gforge/www/scm/viewvc/templates/docroot/images/forward.png -text
+gforge/www/scm/viewvc/templates/docroot/images/list.png -text
+gforge/www/scm/viewvc/templates/docroot/images/log.png -text
+gforge/www/scm/viewvc/templates/docroot/images/logo.png -text
+gforge/www/scm/viewvc/templates/docroot/images/text.png -text
+gforge/www/scm/viewvc/templates/docroot/images/up.png -text
+gforge/www/scm/viewvc/templates/docroot/images/view.png -text
+gforge/www/scm/viewvc/viewvc.org/images/bg-grad.jpg svneol=native#unset
+gforge/www/scm/viewvc/viewvc.org/images/title.jpg svneol=native#unset
+gforge/www/scm/viewvc/viewvc.org/images/title.xcf svneol=native#unset
+gforge/www/scm/viewvc/windows/aspfool/aspfool.dll svneol=native#unset
gforge/www/search/include/engines/DocsGroupSearchEngine.class -text svneol=unset#application/octet-stream
gforge/www/search/include/engines/ForumsGroupSearchEngine.class -text svneol=unset#application/octet-stream
gforge/www/search/include/engines/FrsGroupSearchEngine.class -text svneol=unset#application/octet-stream
gforge/www/themes/gforge/images/theme-toptab-selected-end.png -text
gforge/www/themes/gforge/images/theme-toptab-selected-notselected.png -text
gforge/www/themes/gforge/images/vert-grad.png -text
+gforge/www/themes/gforge/viewvc/images/annotate.png -text
+gforge/www/themes/gforge/viewvc/images/back.png -text
+gforge/www/themes/gforge/viewvc/images/back_small.png -text
+gforge/www/themes/gforge/viewvc/images/broken.png -text
+gforge/www/themes/gforge/viewvc/images/chalk.jpg svneol=native#unset
+gforge/www/themes/gforge/viewvc/images/cvsgraph_16x16.png -text
+gforge/www/themes/gforge/viewvc/images/cvsgraph_32x32.png -text
+gforge/www/themes/gforge/viewvc/images/diff.png -text
+gforge/www/themes/gforge/viewvc/images/dir.png -text
+gforge/www/themes/gforge/viewvc/images/down.png -text
+gforge/www/themes/gforge/viewvc/images/download.png -text
+gforge/www/themes/gforge/viewvc/images/feed-icon-16x16.jpg svneol=native#unset
+gforge/www/themes/gforge/viewvc/images/forward.png -text
+gforge/www/themes/gforge/viewvc/images/list.png -text
+gforge/www/themes/gforge/viewvc/images/log.png -text
+gforge/www/themes/gforge/viewvc/images/logo.png -text
+gforge/www/themes/gforge/viewvc/images/text.png -text
+gforge/www/themes/gforge/viewvc/images/up.png -text
+gforge/www/themes/gforge/viewvc/images/view.png -text
gforge/www/themes/lite/images/gforge_logo.png -text
gforge/www/themes/osx/images/background.png -text
gforge/www/themes/osx/images/clear.png -text
--- /dev/null
+gforge/www/scm/viewvc/viewvc.org/*.tar.gz
+gforge/www/scm/viewvc/viewvc.org/*.zip
$this->hooks[] = 'scm_plugin';
// $this->hooks[] = 'group_approved';
- $this->hooks[] = 'cssfile';
require_once('plugins/scmcvs/config.php') ;
// case "group_approved":
// $this->groupApproved ($params) ;
// break;
- case 'cssfile':
- $this->getCSSfile ($params) ;
- break;
case 'scm_plugin':
$scm_plugins=& $params['scm_plugins'];
$scm_plugins[]=$this->name;
}
}
- function getCSSfile ($layout) {
- echo '<link rel="stylesheet" type="text/css" href="/plugins/scmcvs/cvsweb/css/cvsweb.css" />';
- }
-
function getPage ($group_id) {
global $Language, $HTML ;
echo $this->getDetailedStats(array('group_id'=>$group_id)).'<p>';
if ($displayCvsBrowser){
echo $Language->getText('plugin_scmcvs', 'browsetree');
- echo '<p>[<a href="'.$this->getAccountGroupCVSwebUrl($group_id).'">'.$Language->getText('plugin_scmcvs', 'browseit').'</a>]</p>' ;
+ echo '<p>[<a href="/scm/viewvc.php/?root='.$project->getUnixName().'">'.$Language->getText('plugin_scmcvs', 'browseit').'</a>]</p>' ;
$hook_params['project_name'] = $project->getUnixName();
plugin_hook ("cvs_stats", $hook_params) ;
}
}
}
- /**
- * getAccountGroupCvswebUrl() - Returns URL for group's CVS interface WWW
- *
- * @param string The group name
- * @return URL to access CVS over HTTP
- */
- function getAccountGroupCVSwebUrl($group_id) {
- $project =& group_get_object($group_id);
- //return 'http://'.$project->getSCMBox().'/plugins/scmcvs/cvsweb.php/?cvsroot='.$project->getUnixName();
- // external SCM box is now handled inside cvsweb.php
- return '/plugins/scmcvs/cvsweb.php/?cvsroot='.$project->getUnixName();
- }
-
/*
function groupApproved ($params) {
$group_id = $params['group_id'] ;
+++ /dev/null
-<?php
-
-/**
- *
- * Gforge cvsweb php wrapper
- *
- * Copyright 2003-2004 (c) Gforge
- * http://gforge.org
- *
- * @version $Id$
- *
- */
-
-require_once('pre.php'); // Initial db and session library, opens session
-require_once('www/scm/include/scm_utils.php');
-
-if (!$sys_use_scm) {
- exit_disabled();
-}
-
-// content types with html output
-$supportedContentTypes = array('text/html', 'text/x-cvsweb-markup');
-
-$plainTextDiffTypes = array('c', 's', 'u', '');
-
-$contentType = 'text/html';
-
-// we analyze the query to find the needed information
-// this will allow us to determine the project name and the content type
-if(getStringFromGet('cvsroot') && strpos(getStringFromGet('cvsroot'), ';') === false) {
- $projectName = getStringFromGet('cvsroot');
- if(getStringFromGet('r1') && getStringFromGet('r2') && in_array(getStringFromGet('f'), $plainTextDiffTypes)) {
- $contentType = 'text/plain';
- }
-} else {
- $queryString = getStringFromServer('QUERY_STRING');
- if(preg_match_all('/[;]?([^\?;=]+)=([^;]+)/', $queryString, $matches, PREG_SET_ORDER)) {
- for($i = 0, $size = sizeof($matches); $i < $size; $i++) {
- $query[$matches[$i][1]] = urldecode($matches[$i][2]);
- }
- $projectName = $query['cvsroot'];
- if(isset($query['content-type'])) {
- $contentType = $query['content-type'];
- }
- if(isset($query['r1']) && isset($query['r2']) && (!isset($query['f']) || in_array($query['f'], $plainTextDiffTypes))) {
- $contentType = 'text/plain';
- }
- }
-}
-// remove eventual leading /cvsroot/ or cvsroot/
-$projectName = ereg_replace('^..[^/]*/','', $projectName);
-
-// we found a project name in the query
-if ($projectName) {
- $Group =& group_get_object_by_name($projectName);
- if (!$Group || !is_object($Group) || $Group->isError()) {
- exit_no_group();
- }
- if (!$Group->usesSCM()) {
- exit_error('Error',$Language->getText('scm_index','error_this_project_has_turned_off'));
- }
-
- // check if the scm_box is located in another server
- $scm_box = $Group->getSCMBox();
- //$external_scm = (gethostbyname($sys_default_domain) != gethostbyname($scm_box));
- $external_scm = !$sys_scm_single_host;
-
- if (session_loggedin()) {
- if (user_ismember($Group->getID())) {
- $perm = & $Group->getPermission(session_get_user());
-
- if (!($perm && is_object($perm) && $perm->isCVSReader()) && !$Group->enableAnonSCM()) {
- exit_permission_denied();
- }
- } else if (!$Group->enableAnonSCM()) {
- exit_permission_denied();
- }
-
- } else if (!$Group->enableAnonSCM()) { // user is not logged in... check if group accepts anonymous CVS
- exit_permission_denied();
- }
-
- // User has access to the CVS... check for valid script
- // (only if we're working on a local CVS server, if the CVS server is external the variable
- // $sys_path_to_scmweb points to the path of the cvsweb script on the remote server)
- if (!isset($GLOBALS['sys_path_to_scmweb']) || (!$external_scm && !is_file($GLOBALS['sys_path_to_scmweb'].'/cvsweb'))) {
- exit_error('Error',"cvsweb script doesn't exist");
- }
-
- // should we output html ?
- $isHtml = in_array($contentType, $supportedContentTypes);
-
- // If we are accessing an external SCM box, execute the cvsweb script remotely and
- // pipe the results
- if ($external_scm) {
- //$server_script = "/cgi-bin/cvsweb";
-
- $scmweb = $GLOBALS["sys_path_to_scmweb"];
- // remove trailing slash
- $scmweb = preg_replace("/\\/\$/", "", $scmweb);
-
- $server_script = $scmweb."/cvsweb";
- // remove leading / (if any)
- $server_script = preg_replace("/^\\//", "", $server_script);
-
- // pass the parameters passed to this script to the remote script in the same fashion
- $script_url = "http://".$scm_box."/".$server_script.$_SERVER["PATH_INFO"]."?".$_SERVER["QUERY_STRING"];
- $fh = @fopen($script_url, "r");
- if (!$fh) {
- exit_error('Error', 'Could not open script <b>'.$script_url.'</b>.');
- }
-
- // start reading the output of the script (in 8k chunks)
- $content = "";
- while (!feof($fh)) {
- $content .= fread($fh, 8192);
- }
-
- if ($isHtml) {
- // Now, we must replace the occurencies of $server_script with this script
- // (do this only of outputting HTML)
- // We must do this because we can't pass the environment variable SCRIPT_NAME
- // to the cvsweb script (maybe this can be fixed in the future?)
- $content = str_replace("/".$server_script, $_SERVER["SCRIPT_NAME"], $content);
- }
-
- } else {
- // SCM Box is the same server as this... execute the cvsweb script locally
- ob_start();
- // call to CVSWeb cgi. We use environment variables to pass parameters to the cgi.
- passthru('PHP_WRAPPER="1" SCRIPT_NAME="'.getStringFromServer('SCRIPT_NAME').'" PATH_INFO="'.getStringFromServer('PATH_INFO').'" QUERY_STRING="'.getStringFromServer('QUERY_STRING').'" '.$GLOBALS['sys_path_to_scmweb'].'/cvsweb 2>&1');
- $content = ob_get_contents();
- ob_end_clean();
- }
-
-
- if ($isHtml) {
- scm_header(array('title'=>$Language->getText('scm_index','cvs_repository'),'group'=>$Group->getID()));
- echo '<div id="cvsweb">';
- } else {
- header("Content-type: $contentType" );
- }
-
- // if we output html and we found the mbstring extension, we should try to encode the output of CVSWeb in UTF-8
- if($isHtml && extension_loaded('mbstring')) {
- $encoding = mb_detect_encoding($content);
- if($encoding != 'UTF-8') {
- $content = mb_convert_encoding($content, 'UTF-8', $encoding);
- }
- }
-
- echo $content;
-
- if ($isHtml) {
- echo '</div>';
- scm_footer(array());
- }
-} else {
- exit_no_group();
-}
-
-?>
+++ /dev/null
-/* CSS for FreeBSD-CVSweb integrated into GForge */
-div#cvsweb th {
- text-align: left;
-}
-div#cvsweb hr {
- height: 1px;
- border: none;
- background-color: #000;
-}
-div#cvsweb h1 {
- text-align: center;
-}
-div#cvsweb fieldset {
- background-color: #eee;
- padding: 0.8em;
-}
-div#cvsweb input[type="submit"] {
- padding-left: 0.5em;
- padding-right: 0.5em;
-}
-
-/* Generic nowrap class */
-div#cvsweb .nowrap {
- white-space: nowrap;
-}
-
-/* Source, diff and annotate views */
-div#cvsweb .src {
- color: #000;
- background-color: #eee;
- font-style: normal;
- font-weight: normal;
-}
-
-/* Navigation header for source views, diffs and annotations */
-div#cvsweb .navigate-header {
- background-color: #99e;
- padding: 2px;
- border: 2px outset;
-}
-
-/* Directory table */
-div#cvsweb table.dir {
- border-right: 1px solid #ccc;
-}
-/* Cells */
-div#cvsweb table.dir * td {
- border-left: 1px solid #ccc;
- border-bottom: 1px solid #ccc;
- padding-left: 5px;
- padding-right: 5px;
-}
-/* Column headers */
-div#cvsweb table.dir * th {
- /*
- background-color:#ffc ;
- */
- background-color:#bbbbbb;
- border: thin outset;
- padding-left: 5px;
- padding-right: 5px;
-}
-/* Sorted column header */
-div#cvsweb table.dir * th.sorted {
- /*
- background-color: #fc6;
- */
- background-color:#bbbbbb;
- border: thin inset;
-}
-/* Even rows */
-div#cvsweb table.dir * tr.even {
- background-color: #fff;
-}
-/* Odd rows */
-div#cvsweb table.dir * tr.odd {
- background-color: #fff;
-}
-/* File and dir name columns */
-div#cvsweb table.dir * td.file, table.dir * td.dir {
- white-space: nowrap;
-}
-/* Graph link column */
-div#cvsweb table.dir * td.graph {
- padding-left: 3px;
- padding-right: 3px;
- text-align: center;
- width: 1%;
-}
-/* Age column */
-div#cvsweb table.dir * td.age {
- font-style: italic;
- white-space: nowrap;
-}
-div#cvsweb table.dir * td.author {
- white-space: nowrap;
-}
-/* Log entry column */
-div#cvsweb table.dir * td.log {
- font-size: smaller;
-}
-/* Attic toggles in directory view */
-div#cvsweb .attic {
- font-size: smaller;
-}
-
-/* Option table labels and values */
-div#cvsweb .opt-label {
- text-align: right;
- padding-left: 0.5em;
-}
-div#cvsweb .opt-value {
- padding-right: 0.5em;
-}
-
-/* Log entry in markup */
-div#cvsweb .log-markup {
- background-color: #fff;
- width: 100%;
-}
-
-/* Diff-selected revision in log */
-div#cvsweb .diff-selected {
- padding-right: 0.5em;
- border-right: 10px solid #fc6;
-}
-
-/* Line-header of each diffed file */
-div#cvsweb .diff-heading {
- background-color: #9cc;
- border: 2px outset;
- padding: 5px;
-}
-/* Lines that are the same */
-div#cvsweb .diff-same {
- font-family: Helvetica, Arial, sans-serif;
- font-size: smaller;
-}
-/* Empty lines */
-div#cvsweb .diff-empty {
- background-color: #ccc;
- font-size: smaller;
-}
-/* Added lines */
-div#cvsweb .diff-added {
- background-color: #ccf;
- font-family: Helvetica, Arial, sans-serif;
- font-size: smaller;
-}
-/* Removed lines */
-div#cvsweb .diff-removed {
- background-color: #f99;
- font-family: Helvetica, Arial, sans-serif;
- font-size: smaller;
-}
-/* Changed lines */
-div#cvsweb .diff-changed {
- background-color: #9f9;
- font-family: Helvetica, Arial, sans-serif;
- font-size: smaller;
-}
-/* Empty changed lines */
-div#cvsweb .diff-changed-missing {
- background-color: #9c9;
- font-size: smaller;
-}
-/* Unchanged text in ediffs */
-div#cvsweb .diff-unchanged {
- background-color: #ccc;
- font-family: Helvetica, Arial, sans-serif;
- font-size: smaller;
-}
-
-/* Current revision lines in annotate view */
-div#cvsweb .current-rev {
- font-weight: bold;
-}
-
-/* Download links */
-div#cvsweb .download-link {
- font-weight: bold;
-}
-/* Display links */
-div#cvsweb .display-link {
- font-weight: bold;
-}
$this->hooks[] = 'scm_stats';
// $this->hooks[] = 'group_approved';
$this->hooks[] = 'scm_plugin';
- $this->hooks[] = 'cssfile';
require_once('plugins/scmsvn/config.php') ;
$scm_plugins=& $params['scm_plugins'];
$scm_plugins[]=$this->name;
break;
- case 'cssfile':
- $this->getCSSfile ($params) ;
- break;
default:
// Forgot something
}
echo $this->getDetailedStats(array('group_id'=>$group_id)).'<p>';
if ($displaySvnBrowser) {
echo $Language->getText('plugin_scmsvn', 'browsetree');
- echo '<p>[<a href="'.$this->getAccountGroupViewcvsUrl($group_id).'">'.$Language->getText('plugin_scmsvn', 'browseit').'</a>]</p>' ;
+ echo '<p>[<a href="/scm/viewvc.php/?root='.$project->getUnixName().'">'.$Language->getText('plugin_scmsvn', 'browseit').'</a>]</p>' ;
}
echo $HTML->boxBottom();
return true;
}
- /**
- * getAccountGroupViewcvsUrl() - Returns URL for group's SVN interface WWW.
- *
- * @param int The group id
- * @return URL to access CVS over HTTP
- */
- function getAccountGroupViewcvsUrl($group_id) {
- $project =& group_get_object($group_id);
- //return 'http'.(($this->use_ssl) ? 's' : '').'://'.$project->getSCMBox().'/plugins/scmsvn/viewcvs.php/?root='.$project->getUnixName();
- return '/plugins/scmsvn/viewcvs.php/?root='.$project->getUnixName();
- }
-
- function getCSSfile ($layout) {
- // Only show the stylesheet if we're actually in the SVN pages.
- // This stops styles inadvertently clashing with the main site.
- if (strncmp($_SERVER['REQUEST_URI'], '/plugins/scmsvn/', 16) == 0) {
- echo '<link rel="stylesheet" type="text/css" href="/plugins/scmsvn/viewcvs/styles.css" />';
- }
- }
-
function getDetailedStats ($params) {
global $Language, $HTML;
$group_id = $params['group_id'] ;
#
# *.company.com creation of group home dirs
#
-12 * * * * $GFORGE/plugins/scmsvn/cronjobs/create_group_home.php /var/www/homedirs/
+12 * * * * $PHP $GFORGE/plugins/scmsvn/cronjobs/create_group_home.php /var/www/homedirs/
#
# users.company.com creation of user dirs
#
-15 * * * * $GFORGE/plugins/scmsvn/cronjobs/create_users.php /var/www/homedirs/
+15 * * * * $PHP $GFORGE/plugins/scmsvn/cronjobs/create_users.php /var/www/homedirs/
#
# structure for versioned docman
#
-18 * * * * $GFORGE/plugins/scmsvn/cronjobs/create_docman.php
+18 * * * * $PHP $GFORGE/plugins/scmsvn/cronjobs/create_docman.php
#
# structure for subversion repos
#
-20 * * * * $GFORGE/plugins/scmsvn/cronjobs/create_svn.php
+20 * * * * $PHP $GFORGE/plugins/scmsvn/cronjobs/create_svn.php
+++ /dev/null
-/*******************************/
-/*** ViewCVS CSS Stylesheet ***/
-/*******************************/
-
-/*** Standard Tags ***/
-
-/** Navigation Headers ***/
-.vc_navheader {
- background-color: #DDDDDD;
-}
-
-
-/*** Table Headers ***/
-.vc_header {
- text-align: left;
- background-color: #BBBBBB;
-}
-.vc_header_sort {
- text-align: left;
- background-color: #BBBBBB;
-}
-
-
-/*** Table Rows ***/
-.vc_row_even {
- background-color: #dddddd;
-}
-.vc_row_odd {
- background-color: #cccccc;
-}
-
-
-/*** Log messages ***/
-.vc_log {
- /* unfortunately, white-space: pre-wrap isn't widely supported ... */
- white-space: -moz-pre-wrap; /* Mozilla based browsers */
- white-space: -pre-wrap; /* Opera 4 - 6 */
- white-space: -o-pre-wrap; /* Opera >= 7 */
- white-space: pre-wrap; /* CSS3 */
- word-wrap: break-word; /* IE 5.5+ */
-}
-
-
-/*** Markup Summary Header ***/
-.vc_summary {
- background-color: #eeeeee;
-}
-
-
-/*** Diff Styles ***/
-.vc_diff_header {
- background-color: #ffffff;
-}
-.vc_diff_chunk_header {
- background-color: #99cccc;
-}
-.vc_diff_chunk_extra {
- font-size: smaller;
-}
-.vc_diff_empty {
- background-color: #cccccc;
- font-family: sans-serif;
- font-size: smaller;
-}
-.vc_diff_add {
- background-color: #aaffaa;
- font-family: sans-serif;
- font-size: smaller;
-}
-.vc_diff_remove {
- background-color: #ffaaaa;
- font-family: sans-serif;
- font-size: smaller;
-}
-.vc_diff_change {
- background-color: #ffff77;
- font-family: sans-serif;
- font-size: smaller;
-}
-.vc_diff_change_empty {
- background-color: #eeee77;
- font-family: sans-serif;
- font-size: smaller;
-}
-.vc_diff_nochange {
- font-family: sans-serif;
- font-size: smaller;
-}
-.vc_raw_diff {
- background-color: #cccccc;
- font-size: smaller;
-}
-
-/*** Annotate Styles ***/
-.vc_blame_line, .vc_blame_author, .vc_blame_rev {
- font-family: monospace;
- text-align: right;
- white-space: nowrap;
- padding-right: 0.5em;
-}
-.vc_blame_text {
- font-family: monospace;
- text-align: left;
- white-space: pre;
- width: 100%;
-}
-
-/*** Query Form ***/
-.vc_query_form {
- background-color: #e6e6e6;
-}
*
* @return String the output of the ViewCVS command.
*/
-function viewcvs_execute() {
+function viewcvs_execute($repos_name, $repos_type) {
$request_uri = getStringFromServer('REQUEST_URI');
$query_string = getStringFromServer('QUERY_STRING');
-
+
+ $viewcvs_path = $GLOBALS['sys_urlroot'].'/scm/viewvc';
+
// this is very important ...
- if (getStringFromServer('PATH_INFO') == '') {
- $path = '/';
- } else {
- $path = getStringFromServer('PATH_INFO');
- // hack: path must always end with /
- if (strrpos($path,'/') != (strlen($path)-1)) {
- $path .= '/';
- }
- }
- $query_string = str_replace('\\&', '&', make_arg_cmd_safe($query_string));
- $query_string = str_replace('\\*', '*', $query_string);
+ if (getStringFromServer('PATH_INFO') == '') {
+ $path = '/';
+ } else {
+ $path = getStringFromServer('PATH_INFO');
+ // hack: path must always end with /
+ if (strrpos($path,'/') != (strlen($path)-1)) {
+ $path .= '/';
+ }
+ }
+
+ if ($repos_type == "cvs") {
+ $repos_root = $GLOBALS['cvsdir_prefix'].'/'.$repos_name;
+ } else if ($repos_type == "svn") {
+ $repos_root = $GLOBALS['svndir_prefix'].'/'.$repos_name;
+ } else {
+ die("Invalid repository type");
+ }
+
+ $query_string = str_replace('\\&', '&', make_arg_cmd_safe($query_string));
+ $query_string = str_replace('\\*', '*', $query_string);
+
$path = str_replace('\\*', '*', make_arg_cmd_safe($path));
$command = 'HTTP_COOKIE="'.make_arg_cmd_safe(getStringFromServer('HTTP_COOKIE')).'" '.
'REMOTE_ADDR="'.make_arg_cmd_safe(getStringFromServer('REMOTE_ADDR')).'" '.
'HTTP_ACCEPT_LANGUAGE="'.make_arg_cmd_safe(getStringFromServer('HTTP_ACCEPT_LANGUAGE')).'" '.
'PATH_INFO="'.$path.'" '.
'PATH="'.make_arg_cmd_safe(getStringFromServer('PATH')).'" '.
+ 'REPOSITORY_ROOT="'.make_arg_cmd_safe($repos_root).'" '.
+ 'REPOSITORY_TYPE="'.$repos_type.'" '.
+ 'REPOSITORY_NAME="'.make_arg_cmd_safe($repos_name).'" '.
'HTTP_HOST="'.make_arg_cmd_safe(getStringFromServer('HTTP_HOST')).'" '.
- $GLOBALS['sys_path_to_scmweb'].'/viewcvs.cgi 2>&1';
+ 'DOCROOT="/themes/'.$GLOBALS['sys_theme'].'/viewvc" '.
+ $viewcvs_path.'/bin/cgi/viewvc.cgi 2>&1';
ob_start();
passthru($command);
require_once('pre.php');
require_once('www/scm/include/scm_utils.php');
-require_once('www/plugins/scmsvn/viewcvs_utils.php');
+require_once('www/scm/include/viewvc_utils.php');
if (!$sys_use_scm) {
exit_disabled();
exit_permission_denied();
}
-// User has access to the repository. Check for valid script
-// (only if we're working on a local SVN server, if the SVN server is external the variable
-// $sys_path_to_scmweb points to the path of the viewcvs script on the remote server)
-if (!isset($GLOBALS['sys_path_to_scmweb']) || (!$external_scm && !is_file($GLOBALS['sys_path_to_scmweb'].'/viewcvs.cgi'))) {
- exit_error('Error',"viewcvs.cgi script doesn't exist in ".$GLOBALS['sys_path_to_scmweb']);
-}
-
-
if ($external_scm) {
//$server_script = "/cgi-bin/viewcvs.cgi";
$server_script = $GLOBALS["sys_path_to_scmweb"]."/viewcvs.cgi";
$content = str_replace("/".$server_script, $_SERVER["SCRIPT_NAME"], $content);
}
} else {
+ $unix_name = $Group->getUnixName();
+
// Call to ViewCVS CGI locally (see viewcvs_utils.php)
- $content = viewcvs_execute();
+
+ // see what type of plugin this project if using
+ if ($Group->usesPlugin('scmcvs')) {
+ $repos_type = 'cvs';
+ } else if ($Group->usesPlugin('scmsvn')) {
+ $repos_type = 'svn';
+ }
+
+ $content = viewcvs_execute($unix_name, $repos_type);
}
// Set content type header from the value set by ViewCVS
$content = mb_convert_encoding($content, 'UTF-8', $encoding);
}
}
- scm_header(array('title'=>$Language->getText('scm_index','svn_repository'),
+ scm_header(array('title'=>$Language->getText('scm_index','scm_repository'),
'group'=>$Group->getID()));
- ?><link href="/plugins/scmsvn/styles.css" rel="stylesheet" TYPE="text/css"><?php
+
echo $content;
scm_footer(array());
} else {
--- /dev/null
+Version 1.0 (1-May-2006)
+
+ * add support for viewing Subversion repositories
+ * add support for running on MS Windows (2003-Feb-09)
+ * generate strict XHTML output (2005-Sep-08)
+ * add support for caching by sending "Last-Modified", "Expires",
+ "ETag", and "Cache-Control" headers (2004-Jun-03)
+ * add support for Mod_Python on Apache 2.x and ASP on IIS
+ * Several changes to standalone.py:
+ - -h commandline option to specify hostname for non local use.
+ - -r commandline option may be repeated to use more than repository
+ before actually installing ViewCVS.
+ - New GUI field to test paging.
+ * add new, better-integrated query interface (2004-Jul-17)
+ * add integrated RSS feeds (2005-Dec-22)
+ * add new "root_as_url_component" option to embed root names as
+ path components in ViewCVS URLs for a more natural URL scheme
+ in ViewCVS configurations with multiple repositories.
+ (2002-Dec-12)
+ * add new "use_localtime" option to display local times instead of
+ UTC times (2002-May-06)
+ * add new "root_parents" option to make it possible to add and
+ remove repositories without modifying the ViewCVS configuration
+ (2004-Jul-16)
+ * add new "template_dir" option to facilitate switching between
+ sets of templates (2005-Feb-08)
+ * add new "sort_group_dirs" option to disable grouping of
+ directories in directory listings (2005-Mar-07)
+ * add new "port" option to connect to a MySQL database on a nonstandard
+ port (2005-Dec-22)
+ * make "default_root" option optional. When no root is specified,
+ show a page listing all available repositories (2005-Feb-04)
+ * add "default_file_view" option to make it possible for relative
+ links and image paths in checked out HTML files to work without
+ the need for special /*checkout*/ prefixes in URLs. Deprecate
+ "checkout_magic" option and disable by default (2006-Apr-03)
+ * add "limit_changes" option to limit number of changed files shown
+ per commit by default in query results and in the Subversion revision
+ view (2005-Dec-23)
+ * hide CVS "Attic" directories and add simple toggle for showing
+ dead files in directory listings (2004-Jul-31)
+ * show Unified, Context and Side-by-side diffs in HTML instead of
+ in bare text pages (2004-Jun-22)
+ * make View/Download links work the same for all file types
+ (2004-Jan-21)
+ * add links to tip of selected branch on log page (2005-Oct-03)
+ * allow use of "Highlight" program for colorizing (2005-Dec-20)
+ * enable enscript colorizing for more file types
+ * add sorting arrows for directory views (2004-Jul-21)
+ * get rid of popup windows for checkout links (2004-Jan-21)
+ * obfuscate email addresses in html output by encoding @ symbol
+ with an HTML character reference (2004-Jul-29)
+ * add paging capability (2001-Dec-31)
+ * Improvements to templates
+ - add new template authoring guide
+ - increase coverage, use templates to produce HTML for diff pages,
+ markup pages, annotate pages, and error pages
+ - move more common page elements into includes
+ - add new template variables providing ViewCVS URLs for more
+ links between related pages and less URL generation inside
+ templates
+ * add new [define] EZT directive for assigning variables within
+ templates (2004-Apr-21)
+ * add command line argument parsing to install script to allow
+ non-interactive installs (2005-Jan-06)
+ * add stricter parameter validation to lower likelihood of CSS
+ vulnerabilities (2002-May-24)
+ * add support for cvsweb's "mime_type=text/x-cvsweb-markup" URLs
+ (2002-Oct-10)
+ * fix incompatibility with enscript 1.6.3 (2002-Feb-05)
+ * fix bug in parsing FreeBSD rlog output (2003-Jul-24)
+ * work around rlog assumption all two digit years in RCS files are
+ relative to the year 1900. (2005-Sep-30)
+ * change loginfo-handler to cope with spaces in filenames and
+ support a simpler command line invocation from CVS (2003-Feb-11)
+ * make cvsdbadmin work properly when invoked on CVS subdirectory
+ paths instead of top-level CVS root paths (2006-Mar-17)
+ * show diff error when comparing two binary files (2002-Jan-23)
+ * make regular expression search skip binary files (2002-Jan-17)
+ * make regular expression search skip nonversioned files in CVS
+ directories instead of choking on them (2002-Sep-27)
+ * fix tarball generator so it doesn't include forbidden modules
+ (2002-Feb-22)
+ * output "404 Not Found" errors instead of "403 Forbidden" errors
+ to not reveal whether forbidden paths exist (2005-May-17)
+ * fix sorting bug in directory view (2002-Apr-18)
+ * reset log and directory page numbers when leaving those pages
+ (2005-Jan-29)
+ * reset sort direction in directory listing when clicking new
+ columns (2004-Jul-21)
+ * fix "Accept-Language" handling for Netscape 4.x browsers
+ (2002-May-23)
+ * fix file descriptor leak in standalone server (2004-Jul-17)
+ * clean up zombie processes from running enscript (2002-Jun-15)
+ * fix mysql "Too many connections" error in cvsdbadmin (2003-Jul-24)
+ * get rid of mxDateTime dependency for query database (2003-Feb-09)
+ * store query database times in UTC instead of local time
+ (2003-Feb-09)
+ * fix daylight saving time bugs in various parts of the code
+
+Version 0.9.4 (released 17-Aug-2005)
+
+ * security fix: omit forbidden/hidden modules from query results.
+
+Version 0.9.3 (released 17-May-2005)
+
+ * security fix: disallow bad "content-type" input [CAN-2004-1062]
+ * security fix: disallow bad "sortby" and "cvsroot" input [CAN-2002-0771]
+ * security fix: omit forbidden/hidden modules from tarballs [CAN-2004-0915]
+
+Version 0.9.2 (released 15-Jan-2002)
+
+ * fix redirects to Attic for diffs
+ * fix diffs that have no changes (causing an infinite loop)
+
+Version 0.9.1 (released 26-Dec-2001)
+
+ * fix a problem with some syntax in ndiff.py which isn't compatible
+ with Python 1.5.2 (causing problems at install time)
+ * remove a debug statement left in the code which continues to
+ append lines to /tmp/log
+
+Version 0.9 (released 23-Dec-2001)
+
+ * create templates for the rest of the pages: markup pages, graphs,
+ annotation, and diff.
+ * add multiple language support and dynamic selection based on the
+ Accept-Language request header
+ * add support for key/value files to provide a way for user-defined
+ variables within templates
+ * add optional regex searching for file contents
+ * add new templates for the navigation header and the footer
+ * EZT changes:
+ - add formatting into print directives
+ - add parameters to [include] directives
+ - relax what can go in double quotes
+ - [include] directives are now relative to the current template
+ - throw an exception for unclosed blocks
+ * changes to standalone.py: add flag for regex search
+ * add more help pages
+ * change installer to optionally show diffs
+ * fix to log.ezt and log_table.ezt to select "Side by Side" properly
+ * create dir_alternate.ezt for the flipped rev/name links
+ * various UI tweaks for the directory pages
+
+
+Version 0.8 (released 10-Dec-2001)
+
+ * add EZT templating mechanism for generating output pages
+ * big update of cvs commit database
+ - updated MySQL support
+ - new CGI
+ - better database caching
+ - switch from old templates to new EZT templates (and integration
+ of look-and-feel)
+ * optional usage of CVSGraph is now builtin
+ * standalone server (for testing) is now provided
+ * shifted some options from viewcvs.conf to the templates
+ * the help at the top of the pages has been shifted to separate help
+ pages, so experienced users don't have to keep seeing it
+ * paths in viewcvs.conf don't require trailing slashes any more
+ * tweak the colorizing for Pascal and Fortran files
+ * fix file readability problem where the user had access via the
+ group, but the process' group did not match that group
+ * some Daylight Savings Time fixes in the CVS commit database
+ * fix tarball generation (the file name) for the root dir
+ * changed default human-readable-diff colors to "stoplight" metaphor
+ * web site and doc revamps
+ * fix the mime types on the download, view, etc links
+ * improved error response when the cvs root is missing
+ * don't try to process vhosts if the config section is not present
+ * various bug fixes and UI tweaks
--- /dev/null
+The following people have commit access to the ViewVC sources.
+Note that this is not a full list of ViewVC's authors, however --
+for that, you'd need to look over the log messages to see all the
+patch contributors.
+
+If you have a question or comment, it's probably best to mail
+dev@viewvc.tigris.org, rather than mailing any of these people
+directly.
+
+ gstein Greg Stein <gstein@lyra.org>
+ jpaint Jay Painter <???>
+ akr Tanaka Akira <???>
+ timcera Tim Cera <???>
+ pefu Peter Funk <???>
+ lbruand Lucas Bruand <???>
+ cmpilato C. Michael Pilato <cmpilato@collab.net>
+ rey4 Russell Yanofsky <rey4@columbia.edu>
+ mharig Mark Harig <???>
+ northeye Takuo Kitame <???>
+ jamesh James Henstridge <???>
+ maxb Max Bowsher <maxb1@ukf.net>
+ eh Erik Hülsmann <e.huelsmann@gmx.net>
+
+## Local Variables:
+## coding:utf-8
+## End:
+## vim:encoding=utf8
--- /dev/null
+CONTENTS
+--------
+ TO THE IMPATIENT
+ INSTALLING VIEWVC
+ APACHE CONFIGURATION
+ UPGRADING VIEWVC
+ SQL CHECKIN DATABASE
+ ENSCRIPT AND HIGHLIGHT CONFIGURATION
+ CVSGRAPH CONFIGURATION
+ IF YOU HAVE PROBLEMS...
+
+
+TO THE IMPATIENT
+----------------
+Congratulations on getting this far. :-)
+
+ Required Software And Configuration Needed To Run ViewVC:
+
+ For CVS Support:
+
+ * Python 1.5.2 or later
+ (http://www.python.org/)
+ * RCS, Revision Control System
+ (http://www.cs.purdue.edu/homes/trinkle/RCS/)
+ * GNU-diff to replace diff implementations without the -u option
+ (http://www.gnu.org/software/diffutils/diffutils.html)
+ * read-only, physical access to a CVS repository
+ (See http://www.cvshome.org/ for more information)
+
+ For Subversion Support:
+
+ * Python 2.0 or later
+ (http://www.python.org/)
+ * Subversion, Version Control System, 1.2.0 or later
+ (binary installation and Python bindings)
+ (http://subversion.tigris.org/)
+
+ Optional:
+
+ * a web server capable of running CGI programs
+ (for example, Apache at http://httpd.apache.org/)
+ * MySQL 3.22 and MySQLdb 0.9.0 or later to create a commit database
+ (http://www.mysql.com/)
+ (http://sourceforge.net/projects/mysql-python)
+ * Enscript, code colorizer
+ (http://www.codento.com/people/mtr/genscript/)
+ * Highlight, code colorizer, 2.2.10 or later required, 2.4.5 or
+ later recommended for reliable line numbering
+ (http://www.andre-simon.de/)
+ * CvsGraph 1.5.0 or later, graphical CVS revision tree generator
+ (http://www.akhphd.au.dk/~bertho/cvsgraph/)
+
+ GUI Operation:
+
+ If you just want to see what your CVS repository looks like with
+ ViewVC, type "bin/standalone.py -g -r /PATH/TO/CVS/ROOT". This
+ will start a tiny webserver serving at http://localhost:7467/.
+ PLEASE NOTE: This requires Python with thread support enabled and
+ the Tkinter GUI. If you don't have one of these, omit the '-g' option.
+
+
+ Standard operation:
+
+ To start installing right away (on UNIX): type "./viewvc-install"
+ in the current directory and answer the prompts. When it
+ finishes, edit the file viewvc.conf in the installation directory
+ to tell viewvc the paths to your CVS and Subversion repositories. Next,
+ configure your web server to run <INSTALL>/cgi/viewvc.cgi, as
+ appropriate for your web server. The section `INSTALLING VIEWVC'
+ below is still recommended reading.
+
+
+INSTALLING VIEWVC
+------------------
+
+NOTE: Windows users can refer to windows/README for Windows-specific
+installation instructions.
+
+1) To get viewvc.cgi to work, make sure that you have Python installed
+ and a webserver which is capable of executing CGI scripts (either
+ based on the .cgi extension, or by placing the script within a specific
+ directory).
+
+ Note that to browse CVS repositories, the viewvc.cgi script needs to
+ have READ-ONLY, physical access to the repository (or a copy of it).
+ Therefore, rsh/ssh or pserver access to the repository will not work.
+ And you need to have the RCS utilities installed, specifically "rlog",
+ "rcsdiff", and "co".
+
+2) Installation is handled by the ./viewvc-install script. Run this
+ script and you will be prompted for a installation root path.
+ The default is /usr/local/viewvc-VERSION, where VERSION is
+ the version of this ViewVC release. The installer sets the install
+ path in some of the files, and ViewVC cannot be moved to a
+ different path after the install.
+
+ Note: while 'root' is usually required to create /usr/local/viewvc,
+ ViewVC does not have to be installed as root, nor does it run as root.
+ It is just as valid to place ViewVC in a home directory, too.
+
+ Note: viewvc-install will create directories if needed. It will
+ prompt before overwriting files that may have been modified (such
+ as viewvc.conf), thus making it safe to install over the top of
+ a previous installation. It will always overwrite program files,
+ however.
+
+3) Edit <VIEWVC_INSTALLATION_DIRECTORY>/viewvc.conf for your specific
+ configuration. In particular, examine the following configuration options:
+
+ cvs_roots
+ default_root
+ rcs_path
+ mime_types_file
+
+ There are some other options that are usually nice to change. See
+ viewvc.conf for more information. ViewVC provides a working,
+ default look. However, if you want to customize the look of ViewVC
+ then edit the files in <VIEWVC_INSTALLATION_DIRECTORY>/templates.
+ You need knowledge about HTML to edit the templates.
+
+4) The CGI programs are in <VIEWVC_INSTALLATION_DIRECTORY>/www/cgi/. You can
+ symlink to this directory from somewhere in your published HTTP server
+ path if your webserver is configured to follow symbolic links. You can
+ also copy the installed <VIEWVC_INSTALLATION_DIRECTORY>/www/cgi/*.cgi
+ scripts after the install (unlike the other files in ViewVC, the scripts
+ under www/ can be moved).
+
+ If you are using Apache, then see below at the section
+ titled APACHE CONFIGURATION.
+
+ NOTE: for security reasons, it is not advisable to install ViewVC
+ directly into your published HTTP directory tree (due to the MySQL
+ passwords in viewvc.conf).
+
+5) That's it for repository browsing. Instructions for getting the
+ SQL checkin database working are below.
+
+
+APACHE CONFIGURATION
+--------------------
+
+1) Find out where the web server configuration file is kept. Typical
+ locations are /etc/httpd/httpd.conf, /etc/httpd/conf/httpd.conf,
+ and /etc/apache/httpd.conf. Depending on how apache was installed,
+ you may also look under /usr/local/etc or /etc/local. Use the vendor
+ documentation or the find utility if in doubt.
+
+Either METHOD A:
+2) The ScriptAlias directive is very useful for pointing
+ directly to the viewvc.cgi script. Simply insert a line containing
+
+ ScriptAlias /viewvc <VIEWVC_INSTALLATION_DIRECTORY>/www/cgi/viewvc.cgi
+
+ into your httpd.conf file. Choose the location in httpd.conf where
+ also the other ScriptAlias lines reside. Some examples:
+
+ ScriptAlias /viewvc /usr/local/viewvc-1.0/www/cgi/viewvc.cgi
+ ScriptAlias /query /usr/local/viewvc-1.0/www/cgi/query.cgi
+
+ continue with step 3).
+
+or alternatively METHOD B:
+2) Copy the CGI scripts from
+ <VIEWVC_INSTALLATION_DIRECTORY>/www/cgi/*.cgi
+ to the /cgi-bin/ directory configured in your httpd.conf file.
+
+ continue with step 3).
+
+and then there's METHOD C:
+2) Copy the CGI scripts from
+ <VIEWVC_INSTALLATION_DIRECTORY>/www/cgi/*.cgi
+ to the directory of your choosing in the Document Root adding the following
+ apache directives for the directory in httpd.conf or an .htaccess file:
+
+ Options +ExecCGI
+ AddHandler cgi-script .cgi
+
+ (Note: For this to work mod_cgi has to be loaded. And for the .htaccess file
+ to be effective, "AllowOverride All" or "AllowOverride Options FileInfo"
+ need to have been specified for the directory.)
+
+ continue with step 3).
+
+or if you've got Mod_Python installed you can use METHOD D:
+2) Copy the Python scripts and .htaccess file from
+ <VIEWVC_INSTALLATION_DIRECTORY>/www/mod_python/
+ to a directory being served by apache.
+
+ In httpd.conf, make sure that "AllowOverride All" or at least
+ "AllowOverride FileInfo Options" are enabled for the directory
+ you copied the files to.
+
+ Note: If you are using Mod_Python under Apache 1.3 the tarball generation
+ and enscript colorizing features may not work because they use
+ multithreading. They do work fine with Apache 2.
+
+ continue with step 3).
+
+3) Restart apache. The commands to do this vary. "httpd -k restart" and
+ "apache -k restart" are two common variants. On RedHat Linux it is
+ done using the command "/sbin/service httpd restart" and on SuSE Linux
+ it is done with "rcapache restart"
+
+4) Optional: Add access control.
+
+ In your httpd.conf you can control access to certain modules by adding
+ directives like this:
+
+ <Location "<url to viewvc.cgi>/<modname_you_wish_to_access_ctl>">
+ AllowOverride None
+ AuthUserFile /path/to/passwd/file
+ AuthName "Client Access"
+ AuthType Basic
+ require valid-user
+ </Location>
+
+ WARNING: If you enable the "checkout_magic" or "allow_tar" options, you
+ will need to add additional location directives to prevent people
+ from sneaking in with URLs like:
+
+ http://<server_name>/viewvc/*checkout*/<module_name>
+ http://<server_name>/viewvc/~checkout~/<module_name>
+ http://<server_name>/viewvc/<module_name>.tar.gz?view=tar
+
+
+UPGRADING VIEWVC
+-----------------
+
+Please read the file upgrading.html in the viewvc.org/ subdirectory or
+at <http://viewvc.org/upgrading.html>.
+
+
+SQL CHECKIN DATABASE
+--------------------
+
+This feature is a clone of the Mozilla Project's Bonsai database. It
+catalogs every commit in the CVS or Subversion repository into a SQL
+database. In fact, the databases are 100% compatible.
+
+Various queries can be performed on the database. After installing ViewVC,
+there are some additional steps required to get the database working.
+
+1) You need MySQL and MySQLdb (a Python DBAPI 2.0 module) installed.
+
+2) You need to create a MySQL user who has permission to create databases.
+ Optionally, you can create a second user with read-only access to the
+ database.
+
+3) Run the <VIEWVC_INSTALLATION_DIRECTORY>/make-database script. It will
+ prompt you for your MySQL user, password, and the name of database you
+ want to create. The database name defaults to "ViewVC". This script
+ creates the database and sets up the empty tables. If you run this on a
+ existing ViewVC database, you will lose all your data!
+
+4) Edit your <VIEWVC_INSTALLATION_DIRECTORY>/viewvc.conf file.
+ There is a [cvsdb] section. You will need to set:
+
+ enabled = 1 # Whether to enable query support in viewvc.cgi
+ host = # MySQL database server host
+ port = # MySQL database server port (default is 3306)
+ database_name = # the name of the database you created with
+ # make-database
+ user = # the read/write database user
+ passwd = # password for read/write database user
+ readonly_user = # the readonly database user -- it's pretty
+ # safe to use the read/write user here
+ readonly_passwd = # password for the readonly user
+
+5) Two programs are provided for updating the checkin database from a
+ CVS repository, cvsdbadmin and loginfo-handler. They serve two
+ different purposes. The cvsdbadmin program walks through your CVS
+ repository and adds every commit in every file. This is commonly
+ used for initializing the database from a repository which has been
+ in use. The loginfo-handler script is executed by the CVS server's
+ CVSROOT/loginfo system upon each commit. It makes real-time
+ updates to the checkin database as commits are made to the
+ repository.
+
+ To build a database of all the commits in the CVS repository /home/cvs,
+ invoke: "./cvsdbadmin rebuild /home/cvs". If you want to update
+ the checkin database, invoke: "./cvsdbadmin update /home/cvs". The
+ update mode checks to see if a commit is already in the database,
+ and only adds it if it is absent.
+
+ To get real-time updates, you'll want to checkout the CVSROOT module
+ from your CVS repository and edit CVSROOT/loginfo. Add the line:
+
+ ALL <VIEWVC_INSTALLATION_DIRECTORY>/loginfo-handler %{sVv}
+
+ If you have other scripts invoked by CVSROOT/loginfo, you will want
+ to make sure to change any running under the "DEFAULT" keyword to
+ "ALL" like the loginfo handler, and probably carefully read the
+ execution rules for CVSROOT/loginfo from the CVS manual.
+
+ If you are running the Unix port of CVS-NT, you'll need to use a
+ slightly different command line:
+
+ ALL <VIEWVC_INSTALLATION_DIRECTORY>/loginfo-handler %{sVv} cvsnt
+
+ The extra 'cvsnt' parameter tells the handler script to parse the
+ commit information in a different way.
+
+ For Subversion repositories, there is a single script called
+ svndbadmin that performs both of the above tasks.
+
+ To build a database of all the commits in the Subversion repository
+ /home/svn, invoke: "./svndbadmin rebuild /home/svn". If you want
+ to update the checkin database, invoke: "./svndbadmin update
+ /home/svn".
+
+ To get real time updates, you will need to add a post-commit hook
+ (for the repository example above, the script should go in
+ /home/svn/hooks/post-commit). The script should look something
+ like this:
+
+ #!/bin/sh
+ REPOS="$1"
+ REV="$2"
+ <VIEWVC_INSTALLATION_DIRECTORY>/svndbadmin rebuild "$REPOS" "$REV"
+
+ If you allow revision property changes in your repository, create a
+ post-revprop-change hook script containing the same commands as the
+ post-commit one. This will make sure that the checkin database
+ stays consistent when you change the svn:log, svn:author or
+ svn:date revision properties.
+
+6) You should be ready to go. Click one of the "Query revision history"
+ links in ViewVC directory listings and give it a try.
+
+
+ENSCRIPT AND HIGHLIGHT CONFIGURATION
+------------------------------------
+
+Enscript and Highlight are programs that can colorize source code for
+a lot of languages. ViewVC can be configured to use either one.
+
+1) Install Enscript or Highlight using your system's package manager
+ or downloading from the project home pages.
+
+2) Set the 'use_enscript' or 'use_highlight' options in viewvc.conf to 1.
+
+3) You may also need to set 'enscript_path' and 'highlight_path' options
+ if the executables are not located on the system PATH.
+
+4) That's it!
+
+
+CVSGRAPH CONFIGURATION
+----------------------
+
+CvsGraph is a program that can display a clickable, graphical tree
+of files in a CVS repository.
+
+WARNING: Under certain circumstances (many revisions of a file
+or many branches or both) CvsGraph can generate very huge images.
+Especially on thin clients these images may crash the Web-Browser.
+Currently there is no known way to avoid this behavior of CvsGraph.
+So you have been warned!
+
+Nevertheless, CvsGraph can be quite helpful on repositories with
+a reasonable number of revisions and branches.
+
+1) Install CvsGraph using your system's package manager or downloading
+ from the project home page.
+
+2) Set the 'use_cvsgraph' options in viewvc.conf to 1.
+
+3) You may also need to set the 'cvsgraph_path' option if the
+ CvsGraph executable is not located on the system PATH.
+
+4) There is a file <VIEWVC_INSTALLATION_DIRECTORY>/cvsgraph.conf that
+ you may want to edit if desired to set color and font characteristics.
+ See the cvsgraph.conf documentation. No edits are required in
+ cvsgraph.conf for operation with viewvc.
+
+
+SUBVERSION INTEGRATION
+----------------------
+
+ViewVC supports browsing of Subversion repositories. To use ViewVC
+with Subversion, make sure you have both Subversion itself and
+the Subversion Python bindings installed. See Subversion's
+installation notes for more details on how to build and install these
+items.
+
+Generally speaking, you'll know when your installation of Subversion's
+bindings has been successful if you can import the 'svn.repos' module
+from within your Python interpreter:
+
+ % python
+ Python 2.2.2 (#1, Oct 29 2002, 02:47:30)
+ [GCC 2.96 20000731 (Red Hat Linux 7.2 2.96-108.7.2)] on linux2
+ Type "help", "copyright", "credits" or "license" for more information.
+ >>> import svn.repos
+ >>>
+
+Note that by default, Subversion installs its bindings in a location
+that is not in Python's default module search path (for example, on
+Linux systems the default is usually /usr/local/lib/svn-python). You
+need to remedy this, either by adding this path to Python's module
+search path, or by relocation the bindings to some place in that
+search path.
+
+Configuration of the Subversion repositories happens in much the same
+way as with CVS repositories, only with the 'svn_roots' configuration
+variable instead of the 'cvs_roots' one.
+
+
+IF YOU HAVE PROBLEMS ...
+------------------------
+
+If you've trouble to make viewvc.cgi work:
+
+=== If nothing seems to work:
+
+ o check if you can execute CGI-scripts (Apache needs to have an
+ ScriptAlias /cgi-bin or cgi-script Handler defined). Try to
+ execute a simple CGI-script that often comes with the distribution
+ of the webserver; locate the logfiles and try to find hints
+ which explain the malfunction
+
+ o view the entries in the webserver's error.log
+
+=== If viewvc seems to work but doesn't show the expected result
+ (Typical error: you can't see any files)
+
+ o check whether the CGI-script has read-permissions to your
+ CVS-Repository. The CGI-script often runs as the user 'nobody'
+ or 'httpd' ..
+
+ o does viewvc find your RCS utilities? (edit rcs_path)
+
+=== If something else happens or you can't get it to work:
+
+ o check the ViewVC home page:
+
+ http://viewvc.org/
+
+ o review the ViewVC mailing list archive to see if somebody else had
+ the same problem, and it was solved:
+
+ http://viewvc.tigris.org/servlets/SummarizeList?listName=users
+
+ o send mail to the ViewVC mailing list: users@viewvc.tigris.org
+
+ NOTE: make sure you provide an accurate description of the problem
+ and any relevant tracebacks or error logs.
--- /dev/null
+ViewVC -- Viewing the content of CVS/SVN repositories with a Webbrowser.
+
+Please read the file INSTALL for more information.
+
+And see windows/README for more information on running ViewVC on
+Microsoft Windows.
--- /dev/null
+PREFACE
+-------
+This file will go away soon after release 0.8. Please use the SourceForge
+tracker to resubmit any of the items listed below, if you think, it is
+still an issue:
+ http://sourceforge.net/tracker/?group_id=18760
+Before reporting please check, whether someone else has already done this.
+Working patches increase the chance to be included into the next release.
+ -- PeFu / October 2001
+
+TODO ITEMS
+----------
+*) add Tamminen Eero's comments on how to make Linux directly execute
+ the Python script. From email on Feb 19.
+ [ add other examples, such as my /bin/sh hack or the teeny CGI stub
+ importing the bulk hack ]
+
+*) insert rcs_path into PATH before calling "rcsdiff". rcsdiff might
+ use "co" and needs to find it on the path.
+
+*) show the "locked" flag (attach it to the LogEntry objects).
+ Idea from Russell Gordon <russell@hoopscotch.dhs.org>
+
+*) committing with a specific revision number:
+ http://mailman.lyra.org/pipermail/viewcvs/2000q1/000008.html
+
+*) add capability similar to cvs2cl.pl:
+ http://mailman.lyra.org/pipermail/viewcvs/2000q2/000050.html
+ suggestion from Chris Meyer <cmeyer@gatan.com>.
+
+*) add a tree view of the directory structure (and files?)
+
+*) include a ConfigParser.py to help older Python installations
+
+*) add a check for the rcs programs/paths to viewvc-install. clarify the
+ dependency on RCS in the docs.
+
+*) have a "check" mode that verifies binaries are available on rcs_path
+
+ -> alternately (probably?): use rcsparse rather than external tools
+
+KNOWN BUGS
+----------
+*) time.timezone seems to not be available on some 1.5.2 installs.
+ I was unable to verify this. On RedHat and SuSE Linux this bug
+ is non existant.
+
+*) With old repositories containing many branches, tags or thousands
+ or revisions, the cvsgraph feature becomes unusable (see INSTALL).
+ ViewVC can't do much about this, but it might be possible to
+ investigate the number of branches, tags and revision in advance
+ and disable the cvsgraph links, if the numbers exceed a certain
+ treshold.
--- /dev/null
+<%@ LANGUAGE = Python %>
+<%
+
+# -*-python-*-
+#
+# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewVC
+# distribution or at http://viewvc.org/license-1.html.
+#
+# For more information, visit http://viewvc.org/
+#
+# -----------------------------------------------------------------------
+#
+# query.asp: View CVS/SVN commit database by web browser
+#
+# -----------------------------------------------------------------------
+#
+# This is a teeny stub to launch the main ViewVC app. It checks the load
+# average, then loads the (precompiled) query.py file and runs it.
+#
+# -----------------------------------------------------------------------
+#
+
+#########################################################################
+#
+# INSTALL-TIME CONFIGURATION
+#
+# These values will be set during the installation process. During
+# development, they will remain None.
+#
+
+LIBRARY_DIR = None
+CONF_PATHNAME = None
+
+#########################################################################
+#
+# Adjust sys.path to include our library directory
+#
+
+import sys
+
+if LIBRARY_DIR:
+ if not LIBRARY_DIR in sys.path:
+ sys.path.insert(0, LIBRARY_DIR)
+
+#########################################################################
+
+import sapi
+import viewvc
+import query
+
+server = sapi.AspServer(Server, Request, Response, Application)
+try:
+ cfg = viewvc.load_config(CONF_PATHNAME, server)
+ query.main(server, cfg, "viewvc.asp")
+finally:
+ s.close()
+
+%>
--- /dev/null
+<%@ LANGUAGE = Python %>
+<%
+
+# -*-python-*-
+#
+# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewVC
+# distribution or at http://viewvc.org/license-1.html.
+#
+# For more information, visit http://viewvc.org/
+#
+# -----------------------------------------------------------------------
+#
+# viewvc: View CVS/SVN repositories via a web browser
+#
+# -----------------------------------------------------------------------
+#
+# This is a teeny stub to launch the main ViewVC app. It checks the load
+# average, then loads the (precompiled) viewvc.py file and runs it.
+#
+# -----------------------------------------------------------------------
+#
+
+#########################################################################
+#
+# INSTALL-TIME CONFIGURATION
+#
+# These values will be set during the installation process. During
+# development, they will remain None.
+#
+
+LIBRARY_DIR = None
+CONF_PATHNAME = None
+
+#########################################################################
+#
+# Adjust sys.path to include our library directory
+#
+
+import sys
+
+if LIBRARY_DIR:
+ if not LIBRARY_DIR in sys.path:
+ sys.path.insert(0, LIBRARY_DIR)
+
+#########################################################################
+
+### add code for checking the load average
+
+#########################################################################
+
+# go do the work
+import sapi
+import viewvc
+
+server = sapi.AspServer(Server, Request, Response, Application)
+try:
+ cfg = viewvc.load_config(CONF_PATHNAME, server)
+ viewvc.main(server, cfg)
+finally:
+ s.close()
+
+%>
--- /dev/null
+#!/usr/bin/env python
+# -*-python-*-
+#
+# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewVC
+# distribution or at http://viewvc.org/license-1.html.
+#
+# For more information, visit http://viewvc.org/
+#
+# -----------------------------------------------------------------------
+#
+# query.cgi: View CVS/SVN commit database by web browser
+#
+# -----------------------------------------------------------------------
+#
+# This is a teeny stub to launch the main ViewVC app. It checks the load
+# average, then loads the (precompiled) viewvc.py file and runs it.
+#
+# -----------------------------------------------------------------------
+#
+
+#########################################################################
+#
+# INSTALL-TIME CONFIGURATION
+#
+# These values will be set during the installation process. During
+# development, they will remain None.
+#
+
+LIBRARY_DIR = None
+CONF_PATHNAME = None
+
+#########################################################################
+#
+# Adjust sys.path to include our library directory
+#
+
+import sys
+import os
+
+if LIBRARY_DIR:
+ sys.path.insert(0, LIBRARY_DIR)
+else:
+ sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0],
+ "../../../lib")))
+
+#########################################################################
+
+import sapi
+import viewvc
+import query
+
+server = sapi.CgiServer()
+cfg = viewvc.load_config(CONF_PATHNAME, server)
+query.main(server, cfg, "viewvc.cgi")
--- /dev/null
+#!/bin/sh
+#
+# Set this script up with something like:
+#
+# ScriptAlias /viewvc-strace /home/gstein/src/viewvc/cgi/viewvc-strace.sh
+#
+thisdir="`dirname $0`"
+exec strace -q -r -o /tmp/v-strace.log "${thisdir}/viewvc.cgi"
--- /dev/null
+#!/usr/bin/env python
+# -*-python-*-
+#
+# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewVC
+# distribution or at http://viewvc.org/license-1.html.
+#
+# For more information, visit http://viewvc.org/
+#
+# -----------------------------------------------------------------------
+#
+# viewvc: View CVS/SVN repositories via a web browser
+#
+# -----------------------------------------------------------------------
+#
+# This is a teeny stub to launch the main ViewVC app. It checks the load
+# average, then loads the (precompiled) viewvc.py file and runs it.
+#
+# -----------------------------------------------------------------------
+#
+
+# THIS CONFIGURATION FILE HAS BEEN MODIFIED WITH THE PURPOSE OF
+# INTEGRATING VIEWVC WITH GFORGE.
+
+#########################################################################
+#
+# INSTALL-TIME CONFIGURATION
+#
+# These values will be set during the installation process. During
+# development, they will remain None.
+#
+
+#LIBRARY_DIR = None
+#CONF_PATHNAME = None
+
+#########################################################################
+#
+# Adjust sys.path to include our library directory
+#
+
+import sys
+import os
+
+#if LIBRARY_DIR:
+# sys.path.insert(0, LIBRARY_DIR)
+#else:
+# sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0],
+# "../../../lib")))
+
+sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../../lib")))
+CONF_PATHNAME = os.path.abspath(os.path.join(sys.argv[0], "../../../viewvc.conf"))
+
+
+#########################################################################
+
+### add code for checking the load average
+
+#########################################################################
+
+# go do the work
+import sapi
+import viewvc
+
+server = sapi.CgiServer()
+
+# read the main configuration
+cfg = viewvc.load_config(CONF_PATHNAME, server)
+
+# BEGIN OF GForge customization
+
+# Read the repository root dir from the environment.
+# This way, we will only have ONE repository configured (the one we're browsing). This
+# is more secure than having one (CVS|SVN) root configured with all the repositories inside
+
+if os.environ["REPOSITORY_TYPE"] == 'cvs':
+ cfg.general.cvs_roots[os.environ["REPOSITORY_NAME"]] = os.environ["REPOSITORY_ROOT"]
+elif os.environ["REPOSITORY_TYPE"] == 'svn':
+ cfg.general.svn_roots[os.environ["REPOSITORY_NAME"]] = os.environ["REPOSITORY_ROOT"]
+
+cfg.general.address = "<a href='mailto:root@"+os.environ["HTTP_HOST"]+"'>root@" + os.environ["HTTP_HOST"]+ "</a>"
+cfg.options.docroot = os.environ["DOCROOT"]
+
+# END OF GForge customization
+
+viewvc.main(server, cfg)
--- /dev/null
+#!/usr/bin/env python
+# -*-python-*-
+#
+# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewVC
+# distribution or at http://viewvc.org/license-1.html.
+#
+# For more information, visit http://viewvc.org/
+#
+# -----------------------------------------------------------------------
+#
+# administrative program for CVSdb; this is primarily
+# used to add/rebuild CVS repositories to the database
+#
+# -----------------------------------------------------------------------
+#
+
+#########################################################################
+#
+# INSTALL-TIME CONFIGURATION
+#
+# These values will be set during the installation process. During
+# development, they will remain None.
+#
+
+LIBRARY_DIR = None
+CONF_PATHNAME = None
+
+# Adjust sys.path to include our library directory
+import sys
+import os
+
+if LIBRARY_DIR:
+ sys.path.insert(0, LIBRARY_DIR)
+else:
+ sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../lib")))
+
+#########################################################################
+
+import os
+import string
+import cvsdb
+import viewvc
+import vclib.bincvs
+
+
+def UpdateFile(db, repository, path, update):
+ try:
+ if update:
+ commit_list = cvsdb.GetUnrecordedCommitList(repository, path, db)
+ else:
+ commit_list = cvsdb.GetCommitListFromRCSFile(repository, path)
+ except cvsdb.error, e:
+ print '[ERROR] %s' % (e)
+ return
+
+ file = string.join(path, "/")
+ if update:
+ print '[%s [%d new commits]]' % (file, len(commit_list)),
+ else:
+ print '[%s [%d commits]]' % (file, len(commit_list)),
+
+ ## add the commits into the database
+ for commit in commit_list:
+ db.AddCommit(commit)
+ sys.stdout.write('.')
+ sys.stdout.flush()
+ print
+
+
+def RecurseUpdate(db, repository, directory, update):
+ for entry in repository.listdir(directory, None, {}):
+ path = directory + [entry.name]
+
+ if entry.errors:
+ continue
+
+ if entry.kind is vclib.DIR:
+ RecurseUpdate(db, repository, path, update)
+ continue
+
+ if entry.kind is vclib.FILE:
+ UpdateFile(db, repository, path, update)
+
+def RootPath(path):
+ """Break os path into cvs root path and other parts"""
+ root = os.path.abspath(path)
+ path_parts = []
+
+ p = root
+ while 1:
+ if os.path.exists(os.path.join(p, 'CVSROOT')):
+ root = p
+ print "Using repository root `%s'" % root
+ break
+
+ p, pdir = os.path.split(p)
+ if not pdir:
+ del path_parts[:]
+ print "Using repository root `%s'" % root
+ print "Warning: CVSROOT directory not found."
+ break
+
+ path_parts.append(pdir)
+
+ root = cvsdb.CleanRepository(root)
+ path_parts.reverse()
+ return root, path_parts
+
+def usage():
+ print 'Usage: %s <command> [arguments]' % (sys.argv[0])
+ print 'Performs administrative functions for the CVSdb database'
+ print 'Commands:'
+ print ' rebuild <repository> rebuilds the CVSdb database'
+ print ' for all files in the repository'
+ print ' update <repository> updates the CVSdb database for'
+ print ' all unrecorded commits'
+ print
+ sys.exit(1)
+
+
+## main
+if __name__ == '__main__':
+ ## check that a command was given
+ if len(sys.argv) <= 2:
+ usage()
+
+ ## set the handler function for the command
+ command = sys.argv[1]
+ if string.lower(command) == 'rebuild':
+ update = 0
+ elif string.lower(command) == 'update':
+ update = 1
+ else:
+ print 'ERROR: unknown command %s' % (command)
+ usage()
+
+ # get repository path
+ root, path_parts = RootPath(sys.argv[2])
+
+ ## run command
+ try:
+ ## connect to the database we are updating
+ cfg = viewvc.load_config(CONF_PATHNAME)
+ db = cvsdb.ConnectDatabase(cfg)
+
+ repository = vclib.bincvs.BinCVSRepository(None, root, cfg.general)
+
+ RecurseUpdate(db, repository, path_parts, update)
+
+ except KeyboardInterrupt:
+ print
+ print '** break **'
+
+ sys.exit(0)
--- /dev/null
+#!/usr/bin/env python
+# -*-python-*-
+#
+# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewVC
+# distribution or at http://viewvc.org/license-1.html.
+#
+# For more information, visit http://viewvc.org/
+#
+# -----------------------------------------------------------------------
+#
+# updates SQL database with new commit records
+#
+# -----------------------------------------------------------------------
+#
+
+#########################################################################
+#
+# INSTALL-TIME CONFIGURATION
+#
+# These values will be set during the installation process. During
+# development, they will remain None.
+#
+
+LIBRARY_DIR = None
+CONF_PATHNAME = None
+
+# Adjust sys.path to include our library directory
+import sys
+import os
+
+if LIBRARY_DIR:
+ sys.path.insert(0, LIBRARY_DIR)
+else:
+ sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../lib")))
+
+#########################################################################
+
+import os
+import string
+import getopt
+import re
+import cvsdb
+import viewvc
+import vclib.bincvs
+
+DEBUG_FLAG = 0
+
+## output functions
+def debug(text):
+ if DEBUG_FLAG:
+ print 'DEBUG(viewvc-loginfo):', text
+
+def warning(text):
+ print 'WARNING(viewvc-loginfo):', text
+
+def error(text):
+ print 'ERROR(viewvc-loginfo):', text
+ sys.exit(1)
+
+_re_revisions = re.compile(
+ r",(?P<old>(?:\d+\.\d+)(?:\.\d+\.\d+)*|NONE)" # comma and first revision number
+ r",(?P<new>(?:\d+\.\d+)(?:\.\d+\.\d+)*|NONE)" # comma and second revision number
+ r"(?:$| )" # space or end of string
+)
+
+def HeuristicArgParse(s, repository):
+ """Current versions of CVS (except for CVSNT) do not escape spaces in file
+ and directory names that are passed to the loginfo handler. Since the input
+ to loginfo is a space separated string, this can lead to ambiguities. This
+ function attempts to guess intelligently which spaces are separators and
+ which are part of file or directory names. It disambiguates spaces in
+ filenames from the separator spaces between files by assuming that every
+ space which is preceded by two well-formed revision numbers is in fact a
+ separator. It disambiguates the first separator space from spaces in the
+ directory name by choosing the longest possible directory name that actually
+ exists in the repository"""
+
+ if s[-16:] == ' - New directory':
+ return None, None
+
+ if s[-19:] == r' - Imported sources':
+ return None, None
+
+ file_data_list = []
+ start = 0
+
+ while 1:
+ m = _re_revisions.search(s, start)
+
+ if start == 0:
+ if m is None:
+ error('Argument "%s" does not contain any revision numbers' % s)
+
+ directory, filename = FindLongestDirectory(s[:m.start()], repository)
+ if directory is None:
+ error('Argument "%s" does not start with a valid directory' % s)
+
+ debug('Directory name is "%s"' % directory)
+
+ else:
+ if m is None:
+ warning('Failed to interpret past position %i in the loginfo argument, '
+ 'leftover string is "%s"' % start, pos[start:])
+
+ filename = s[start:m.start()]
+
+ old_version, new_version = m.group('old', 'new')
+
+ file_data_list.append((filename, old_version, new_version))
+
+ debug('File "%s", old revision %s, new revision %s'
+ % (filename, old_version, new_version))
+
+ start = m.end()
+
+ if start == len(s): break
+
+ return directory, file_data_list
+
+def FindLongestDirectory(s, repository):
+ """Splits the first part of the argument string into a directory name
+ and a file name, either of which may contain spaces. Returns the longest
+ possible directory name that actually exists"""
+
+ parts = string.split(s, " ")
+
+ for i in range(len(parts)-1, 0, -1):
+ directory = string.join(parts[:i])
+ filename = string.join(parts[i:])
+ if os.path.isdir(os.path.join(repository, directory)):
+ return directory, filename
+
+ return None, None
+
+_re_cvsnt_revisions = re.compile(
+ r"(?P<filename>.*)" # comma and first revision number
+ r",(?P<old>(?:\d+\.\d+)(?:\.\d+\.\d+)*|NONE)" # comma and first revision number
+ r",(?P<new>(?:\d+\.\d+)(?:\.\d+\.\d+)*|NONE)" # comma and second revision number
+ r"$" # end of string
+)
+
+def CvsNtArgParse(s, repository):
+ """CVSNT escapes all spaces in filenames and directory names with
+ backslashes"""
+
+ if s[-18:] == r' -\ New\ directory':
+ return None, None
+
+ if s[-21:] == r' -\ Imported\ sources':
+ return None, None
+
+ file_data_list = []
+
+ directory, pos = NextFile(s)
+
+ debug('Directory name is "%s"' % directory)
+
+ while 1:
+ fileinfo, pos = NextFile(s, pos)
+ if fileinfo is None:
+ break
+
+ m = _re_cvsnt_revisions.match(fileinfo)
+ if m is None:
+ warning('Can\'t parse file information in "%s"' % fileinfo)
+ continue
+
+ file_data = m.group('filename', 'old', 'new')
+
+ debug('File "%s", old revision %s, new revision %s' % file_data)
+
+ file_data_list.append(file_data)
+
+ return directory, file_data_list
+
+def NextFile(s, pos = 0):
+ escaped = 0
+ ret = ''
+ i = pos
+ while i < len(s):
+ c = s[i]
+ if escaped:
+ ret += c
+ escaped = 0
+ elif c == '\\':
+ escaped = 1
+ elif c == ' ':
+ return ret, i + 1
+ else:
+ ret += c
+ i += 1
+
+ return ret or None, i
+
+def ProcessLoginfo(rootpath, directory, files):
+ cfg = viewvc.load_config(CONF_PATHNAME)
+ db = cvsdb.ConnectDatabase(cfg)
+ repository = vclib.bincvs.BinCVSRepository(None, rootpath, cfg.general)
+
+ # split up the directory components
+ dirpath = filter(None, string.split(os.path.normpath(directory), os.sep))
+
+ ## build a list of Commit objects
+ commit_list = []
+ for filename, old_version, new_version in files:
+ filepath = dirpath + [filename]
+
+ ## XXX: this is nasty: in the case of a removed file, we are not
+ ## given enough information to find it in the rlog output!
+ ## So instead, we rlog everything in the removed file, and
+ ## add any commits not already in the database
+ if new_version == 'NONE':
+ commits = cvsdb.GetUnrecordedCommitList(repository, filepath, db)
+ else:
+ commits = cvsdb.GetCommitListFromRCSFile(repository, filepath,
+ new_version)
+
+ commit_list.extend(commits)
+
+ ## add to the database
+ db.AddCommitList(commit_list)
+
+
+## MAIN
+if __name__ == '__main__':
+ ## get the repository from the environment
+ try:
+ repository = os.environ['CVSROOT']
+ except KeyError:
+ error('CVSROOT not in environment')
+
+ debug('Repository name is "%s"' % repository)
+
+ ## parse arguments
+ if len(sys.argv) > 1:
+ # the first argument should contain file version information
+ arg = sys.argv[1]
+ else:
+ # if there are no arguments, read version information from first line
+ # of input like old versions of viewcvs
+ arg = string.rstrip(sys.stdin.readline())
+
+ if len(sys.argv) > 2:
+ # if there is a second argument it indicates which parser should be
+ # used to interpret the version information
+ if sys.argv[2] == 'cvs':
+ fun = HeuristicArgParse
+ elif sys.argv[2] == 'cvsnt':
+ fun = CvsNtArgParse
+ else:
+ error('Bad arguments')
+ else:
+ # if there is no second argument, guess which parser to use based
+ # on the operating system. Since CVSNT now runs on Windows and
+ # Linux, the guess isn't neccessarily correct
+ if sys.platform == "win32":
+ fun = CvsNtArgParse
+ else:
+ fun = HeuristicArgParse
+
+ if len(sys.argv) > 3:
+ error('Bad arguments')
+
+ repository = cvsdb.CleanRepository(repository)
+ directory, files = fun(arg, repository)
+
+ if files is None:
+ debug('Not a checkin, nothing to do')
+ else:
+ ProcessLoginfo(repository, directory, files)
+
+ sys.exit(0)
--- /dev/null
+#!/usr/bin/env python
+# -*-python-*-
+#
+# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewVC
+# distribution or at http://viewvc.org/license-1.html.
+#
+# For more information, visit http://viewvc.org/
+#
+# -----------------------------------------------------------------------
+#
+# administrative program for CVSdb; creates a clean database in
+# MySQL 3.22 or later
+#
+# -----------------------------------------------------------------------
+
+import os, sys, string
+import popen2
+
+INTRO_TEXT = """\
+This script creates the database and tables in MySQL used by the ViewVC
+checkin database. You will be prompted for: database user, database user
+password, and database name. This script will use mysql to create the
+database for you. You will then need to set the appropriate parameters
+in your viewvc.conf file under the [cvsdb] section.
+"""
+
+DATABASE_SCRIPT="""\
+DROP DATABASE IF EXISTS <dbname>;
+CREATE DATABASE <dbname>;
+
+USE <dbname>;
+
+DROP TABLE IF EXISTS branches;
+CREATE TABLE branches (
+ id mediumint(9) NOT NULL auto_increment,
+ branch varchar(64) binary DEFAULT '' NOT NULL,
+ PRIMARY KEY (id),
+ UNIQUE branch (branch)
+);
+
+DROP TABLE IF EXISTS checkins;
+CREATE TABLE checkins (
+ type enum('Change','Add','Remove'),
+ ci_when datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
+ whoid mediumint(9) DEFAULT '0' NOT NULL,
+ repositoryid mediumint(9) DEFAULT '0' NOT NULL,
+ dirid mediumint(9) DEFAULT '0' NOT NULL,
+ fileid mediumint(9) DEFAULT '0' NOT NULL,
+ revision varchar(32) binary DEFAULT '' NOT NULL,
+ stickytag varchar(255) binary DEFAULT '' NOT NULL,
+ branchid mediumint(9) DEFAULT '0' NOT NULL,
+ addedlines int(11) DEFAULT '0' NOT NULL,
+ removedlines int(11) DEFAULT '0' NOT NULL,
+ descid mediumint(9),
+ UNIQUE repositoryid (repositoryid,dirid,fileid,revision),
+ KEY ci_when (ci_when),
+ KEY whoid (whoid),
+ KEY repositoryid_2 (repositoryid),
+ KEY dirid (dirid),
+ KEY fileid (fileid),
+ KEY branchid (branchid)
+);
+
+DROP TABLE IF EXISTS descs;
+CREATE TABLE descs (
+ id mediumint(9) NOT NULL auto_increment,
+ description text,
+ hash bigint(20) DEFAULT '0' NOT NULL,
+ PRIMARY KEY (id),
+ KEY hash (hash)
+);
+
+DROP TABLE IF EXISTS dirs;
+CREATE TABLE dirs (
+ id mediumint(9) NOT NULL auto_increment,
+ dir varchar(255) binary DEFAULT '' NOT NULL,
+ PRIMARY KEY (id),
+ UNIQUE dir (dir)
+);
+
+DROP TABLE IF EXISTS files;
+CREATE TABLE files (
+ id mediumint(9) NOT NULL auto_increment,
+ file varchar(255) binary DEFAULT '' NOT NULL,
+ PRIMARY KEY (id),
+ UNIQUE file (file)
+);
+
+DROP TABLE IF EXISTS people;
+CREATE TABLE people (
+ id mediumint(9) NOT NULL auto_increment,
+ who varchar(32) binary DEFAULT '' NOT NULL,
+ PRIMARY KEY (id),
+ UNIQUE who (who)
+);
+
+DROP TABLE IF EXISTS repositories;
+CREATE TABLE repositories (
+ id mediumint(9) NOT NULL auto_increment,
+ repository varchar(64) binary DEFAULT '' NOT NULL,
+ PRIMARY KEY (id),
+ UNIQUE repository (repository)
+);
+
+DROP TABLE IF EXISTS tags;
+CREATE TABLE tags (
+ repositoryid mediumint(9) DEFAULT '0' NOT NULL,
+ branchid mediumint(9) DEFAULT '0' NOT NULL,
+ dirid mediumint(9) DEFAULT '0' NOT NULL,
+ fileid mediumint(9) DEFAULT '0' NOT NULL,
+ revision varchar(32) binary DEFAULT '' NOT NULL,
+ UNIQUE repositoryid (repositoryid,dirid,fileid,branchid,revision),
+ KEY repositoryid_2 (repositoryid),
+ KEY dirid (dirid),
+ KEY fileid (fileid),
+ KEY branchid (branchid)
+);
+"""
+
+if __name__ == "__main__":
+ print INTRO_TEXT
+
+ user = raw_input("MySQL User: ")
+ passwd = raw_input("MySQL Password: ")
+ dbase = raw_input("ViewVC Database Name [default: ViewVC]: ")
+ if not dbase:
+ dbase = "ViewVC"
+
+ dscript = string.replace(DATABASE_SCRIPT, "<dbname>", dbase)
+
+ if sys.platform == "win32":
+ # popen2.Popen3 is not provided on windows
+ cmd = "mysql --user=%s --password=%s" % (user, passwd)
+ mysql = os.popen(cmd, "w")
+ mysql.write(dscript)
+ status = mysql.close()
+ else:
+ cmd = "{ mysql --user=%s --password=%s ; } 2>&1" % (user, passwd)
+ pipes = popen2.Popen3(cmd)
+ pipes.tochild.write(dscript)
+ pipes.tochild.close()
+ print pipes.fromchild.read()
+ status = pipes.wait()
+
+ if status:
+ print "[ERROR] the database did not create sucessfully."
+ sys.exit(1)
+
+ print "Database created successfully."
+ sys.exit(0)
+
--- /dev/null
+AddHandler python-program .py
+PythonHandler handler
+PythonDebug On
--- /dev/null
+# -*-python-*-
+#
+# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewVC
+# distribution or at http://viewvc.org/license-1.html.
+#
+# For more information, visit http://viewvc.org/
+#
+# -----------------------------------------------------------------------
+#
+# Mod_Python handler based on mod_python.publisher
+#
+# -----------------------------------------------------------------------
+
+from mod_python import apache
+import os.path
+
+def handler(req):
+ path, module_name = os.path.split(req.filename)
+ module_name, module_ext = os.path.splitext(module_name)
+ try:
+ module = apache.import_module(module_name, path=[path])
+ except ImportError:
+ raise apache.SERVER_RETURN, apache.HTTP_NOT_FOUND
+
+ req.add_common_vars()
+ module.index(req)
+
+ return apache.OK
--- /dev/null
+# -*-python-*-
+#
+# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewVC
+# distribution or at http://viewvc.org/license-1.html.
+#
+# For more information, visit http://viewvc.org/
+#
+# -----------------------------------------------------------------------
+#
+# ViewVC: View CVS/SVN repositories via a web browser
+#
+# -----------------------------------------------------------------------
+#
+# This is a teeny stub to launch the main ViewVC app. It checks the load
+# average, then loads the (precompiled) viewvc.py file and runs it.
+#
+# -----------------------------------------------------------------------
+#
+
+#########################################################################
+#
+# INSTALL-TIME CONFIGURATION
+#
+# These values will be set during the installation process. During
+# development, they will remain None.
+#
+
+LIBRARY_DIR = None
+CONF_PATHNAME = None
+
+#########################################################################
+#
+# Adjust sys.path to include our library directory
+#
+
+import sys
+
+if LIBRARY_DIR:
+ sys.path.insert(0, LIBRARY_DIR)
+
+import sapi
+import viewvc
+import query
+reload(query) # need reload because initial import loads this stub file
+
+cfg = viewvc.load_config(CONF_PATHNAME)
+
+def index(req):
+ server = sapi.ModPythonServer(req)
+ try:
+ query.main(server, cfg, "viewvc.py")
+ finally:
+ server.close()
+
--- /dev/null
+# -*-python-*-
+#
+# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewVC
+# distribution or at http://viewvc.org/license-1.html.
+#
+# For more information, visit http://viewvc.org/
+#
+# -----------------------------------------------------------------------
+#
+# viewvc: View CVS/SVN repositories via a web browser
+#
+# -----------------------------------------------------------------------
+#
+# This is a teeny stub to launch the main ViewVC app. It checks the load
+# average, then loads the (precompiled) viewvc.py file and runs it.
+#
+# -----------------------------------------------------------------------
+#
+
+#########################################################################
+#
+# INSTALL-TIME CONFIGURATION
+#
+# These values will be set during the installation process. During
+# development, they will remain None.
+#
+
+LIBRARY_DIR = None
+CONF_PATHNAME = None
+
+#########################################################################
+#
+# Adjust sys.path to include our library directory
+#
+
+import sys
+
+if LIBRARY_DIR:
+ sys.path.insert(0, LIBRARY_DIR)
+
+import sapi
+import viewvc
+reload(viewvc) # need reload because initial import loads this stub file
+
+
+def index(req):
+ server = sapi.ModPythonServer(req)
+ cfg = viewvc.load_config(CONF_PATHNAME, server)
+ try:
+ viewvc.main(server, cfg)
+ finally:
+ server.close()
--- /dev/null
+#!/usr/bin/env python
+# -*-python-*-
+#
+# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewVC
+# distribution or at http://viewvc.org/license-1.html.
+#
+# For more information, visit http://viewvc.org/
+#
+# -----------------------------------------------------------------------
+
+"""Run "standalone.py -p <port>" to start an HTTP server on a given port
+on the local machine to generate ViewVC web pages.
+"""
+
+__author__ = "Peter Funk <pf@artcom-gmbh.de>"
+__date__ = "11 November 2001"
+__version__ = "$Revision$"
+__credits__ = """Guido van Rossum, for an excellent programming language.
+Greg Stein, for writing ViewCVS in the first place.
+Ka-Ping Yee, for the GUI code and the framework stolen from pydoc.py.
+"""
+
+# INSTALL-TIME CONFIGURATION
+#
+# These values will be set during the installation process. During
+# development, they will remain None.
+#
+
+LIBRARY_DIR = None
+CONF_PATHNAME = None
+
+import sys
+import os
+import os.path
+import stat
+import string
+import urllib
+import rfc822
+import socket
+import select
+import BaseHTTPServer
+
+if LIBRARY_DIR:
+ sys.path.insert(0, LIBRARY_DIR)
+else:
+ sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../lib")))
+
+import sapi
+import viewvc
+import compat; compat.for_standalone()
+
+
+class Options:
+ port = 7467 # default TCP/IP port used for the server
+ start_gui = 0 # No GUI unless requested.
+ repositories = {} # use default repositories specified in config
+ if sys.platform == 'mac':
+ host = '127.0.0.1'
+ else:
+ host = 'localhost'
+ script_alias = 'viewvc'
+
+# --- web browser interface: ----------------------------------------------
+
+class StandaloneServer(sapi.CgiServer):
+ def __init__(self, handler):
+ sapi.CgiServer.__init__(self, inheritableOut = sys.platform != "win32")
+ self.handler = handler
+
+ def header(self, content_type='text/html', status=None):
+ if not self.headerSent:
+ self.headerSent = 1
+ if status is None:
+ statusCode = 200
+ statusText = 'OK'
+ else:
+ p = string.find(status, ' ')
+ if p < 0:
+ statusCode = int(status)
+ statusText = ''
+ else:
+ statusCode = int(status[:p])
+ statusText = status[p+1:]
+ self.handler.send_response(statusCode, statusText)
+ self.handler.send_header("Content-type", content_type)
+ for (name, value) in self.headers:
+ self.handler.send_header(name, value)
+ self.handler.end_headers()
+
+
+def serve(host, port, callback=None):
+ """start a HTTP server on the given port. call 'callback' when the
+ server is ready to serve"""
+
+ class ViewVC_Handler(BaseHTTPServer.BaseHTTPRequestHandler):
+
+ def do_GET(self):
+ """Serve a GET request."""
+ if not self.path or self.path == "/":
+ self.redirect()
+ elif self.is_viewvc():
+ try:
+ self.run_viewvc()
+ except IOError:
+ # ignore IOError: [Errno 32] Broken pipe
+ pass
+ else:
+ self.send_error(404)
+
+ def do_POST(self):
+ """Serve a POST request."""
+ if self.is_viewvc():
+ self.run_viewvc()
+ else:
+ self.send_error(501, "Can only POST to %s"
+ % (options.script_alias))
+
+ def is_viewvc(self):
+ """Check whether self.path is, or is a child of, the ScriptAlias"""
+ if self.path == '/' + options.script_alias:
+ return 1
+ if self.path[:len(options.script_alias)+2] == \
+ '/' + options.script_alias + '/':
+ return 1
+ if self.path[:len(options.script_alias)+2] == \
+ '/' + options.script_alias + '?':
+ return 1
+ return 0
+
+ def redirect(self):
+ """redirect the browser to the viewvc URL"""
+ new_url = self.server.url + options.script_alias + '/'
+ self.send_response(301, "Moved (redirection follows)")
+ self.send_header("Content-type", "text/html")
+ self.send_header("Location", new_url)
+ self.end_headers()
+ self.wfile.write("""<html>
+<head>
+<meta http-equiv="refresh" content="1; URL=%s">
+</head>
+<body>
+<h1>Redirection to <a href="%s">ViewVC</a></h1>
+Wait a second. You will be automatically redirected to <b>ViewVC</b>.
+If this doesn't work, please click on the link above.
+</body>
+</html>
+""" % tuple([new_url]*2))
+
+ def run_viewvc(self):
+ """This is a quick and dirty cut'n'rape from Python's
+ standard library module CGIHTTPServer."""
+ scriptname = '/' + options.script_alias
+ assert string.find(self.path, scriptname) == 0
+ viewvc_url = self.server.url[:-1] + scriptname
+ rest = self.path[len(scriptname):]
+ i = string.rfind(rest, '?')
+ if i >= 0:
+ rest, query = rest[:i], rest[i+1:]
+ else:
+ query = ''
+ # sys.stderr.write("Debug: '"+scriptname+"' '"+rest+"' '"+query+"'\n")
+ env = os.environ
+ # Since we're going to modify the env in the parent, provide empty
+ # values to override previously set values
+ for k in env.keys():
+ if k[:5] == 'HTTP_':
+ del env[k]
+ for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
+ 'HTTP_USER_AGENT', 'HTTP_COOKIE'):
+ if env.has_key(k):
+ env[k] = ""
+ # XXX Much of the following could be prepared ahead of time!
+ env['SERVER_SOFTWARE'] = self.version_string()
+ env['SERVER_NAME'] = self.server.server_name
+ env['GATEWAY_INTERFACE'] = 'CGI/1.1'
+ env['SERVER_PROTOCOL'] = self.protocol_version
+ env['SERVER_PORT'] = str(self.server.server_port)
+ env['REQUEST_METHOD'] = self.command
+ uqrest = urllib.unquote(rest)
+ env['PATH_INFO'] = uqrest
+ env['SCRIPT_NAME'] = scriptname
+ if query:
+ env['QUERY_STRING'] = query
+ env['HTTP_HOST'] = self.server.address[0]
+ host = self.address_string()
+ if host != self.client_address[0]:
+ env['REMOTE_HOST'] = host
+ env['REMOTE_ADDR'] = self.client_address[0]
+ # AUTH_TYPE
+ # REMOTE_USER
+ # REMOTE_IDENT
+ if self.headers.typeheader is None:
+ env['CONTENT_TYPE'] = self.headers.type
+ else:
+ env['CONTENT_TYPE'] = self.headers.typeheader
+ length = self.headers.getheader('content-length')
+ if length:
+ env['CONTENT_LENGTH'] = length
+ accept = []
+ for line in self.headers.getallmatchingheaders('accept'):
+ if line[:1] in string.whitespace:
+ accept.append(string.strip(line))
+ else:
+ accept = accept + string.split(line[7:], ',')
+ env['HTTP_ACCEPT'] = string.joinfields(accept, ',')
+ ua = self.headers.getheader('user-agent')
+ if ua:
+ env['HTTP_USER_AGENT'] = ua
+ modified = self.headers.getheader('if-modified-since')
+ if modified:
+ env['HTTP_IF_MODIFIED_SINCE'] = modified
+ etag = self.headers.getheader('if-none-match')
+ if etag:
+ env['HTTP_IF_NONE_MATCH'] = etag
+ # XXX Other HTTP_* headers
+ decoded_query = string.replace(query, '+', ' ')
+
+ # Preserve state, because we execute script in current process:
+ save_argv = sys.argv
+ save_stdin = sys.stdin
+ save_stdout = sys.stdout
+ save_stderr = sys.stderr
+ # For external tools like enscript we also need to redirect
+ # the real stdout file descriptor. (On windows, reassigning the
+ # sys.stdout variable is sufficient because pipe_cmds makes it
+ # the standard output for child processes.)
+ if sys.platform != "win32": save_realstdout = os.dup(1)
+ try:
+ try:
+ sys.stdout = self.wfile
+ if sys.platform != "win32":
+ os.dup2(self.wfile.fileno(), 1)
+ sys.stdin = self.rfile
+ viewvc.main(StandaloneServer(self), cfg)
+ finally:
+ sys.argv = save_argv
+ sys.stdin = save_stdin
+ sys.stdout.flush()
+ if sys.platform != "win32":
+ os.dup2(save_realstdout, 1)
+ os.close(save_realstdout)
+ sys.stdout = save_stdout
+ sys.stderr = save_stderr
+ except SystemExit, status:
+ self.log_error("ViewVC exit status %s", str(status))
+ else:
+ self.log_error("ViewVC exited ok")
+
+ class ViewVC_Server(BaseHTTPServer.HTTPServer):
+ def __init__(self, host, port, callback):
+ self.address = (host, port)
+ self.url = 'http://%s:%d/' % (host, port)
+ self.callback = callback
+ BaseHTTPServer.HTTPServer.__init__(self, self.address,
+ self.handler)
+
+ def serve_until_quit(self):
+ self.quit = 0
+ while not self.quit:
+ rd, wr, ex = select.select([self.socket.fileno()], [], [], 1)
+ if rd:
+ self.handle_request()
+
+ def server_activate(self):
+ BaseHTTPServer.HTTPServer.server_activate(self)
+ if self.callback:
+ self.callback(self)
+
+ def server_bind(self):
+ # set SO_REUSEADDR (if available on this platform)
+ if hasattr(socket, 'SOL_SOCKET') \
+ and hasattr(socket, 'SO_REUSEADDR'):
+ self.socket.setsockopt(socket.SOL_SOCKET,
+ socket.SO_REUSEADDR, 1)
+ BaseHTTPServer.HTTPServer.server_bind(self)
+
+ ViewVC_Server.handler = ViewVC_Handler
+
+ try:
+ # XXX Move this code out of this function.
+ # Early loading of configuration here. Used to
+ # allow tinkering with some configuration settings:
+ handle_config()
+ if options.repositories:
+ cfg.general.default_root = "Development"
+ cfg.general.cvs_roots.update(options.repositories)
+ elif cfg.general.cvs_roots.has_key("Development") and \
+ not os.path.isdir(cfg.general.cvs_roots["Development"]):
+ sys.stderr.write("*** No repository found. Please use the -r option.\n")
+ sys.stderr.write(" Use --help for more info.\n")
+ raise KeyboardInterrupt # Hack!
+ os.close(0) # To avoid problems with shell job control
+
+ # always use default docroot location
+ cfg.options.docroot = None
+
+ # if cvsnt isn't found, fall back to rcs
+ if (cfg.conf_path is None
+ and cfg.general.cvsnt_exe_path):
+ import popen
+ cvsnt_works = 0
+ try:
+ fp = popen.popen(cfg.general.cvsnt_exe_path, ['--version'], 'rt')
+ try:
+ while 1:
+ line = fp.readline()
+ if not line: break
+ if string.find(line, "Concurrent Versions System (CVSNT)")>=0:
+ cvsnt_works = 1
+ while fp.read(4096):
+ pass
+ break
+ finally:
+ fp.close()
+ except:
+ pass
+ if not cvsnt_works:
+ cfg.cvsnt_exe_path = None
+
+ ViewVC_Server(host, port, callback).serve_until_quit()
+ except (KeyboardInterrupt, select.error):
+ pass
+ print 'server stopped'
+
+def handle_config():
+ global cfg
+ cfg = viewvc.load_config(CONF_PATHNAME)
+
+# --- graphical interface: --------------------------------------------------
+
+def nogui(missing_module):
+ sys.stderr.write(
+ "Sorry! Your Python was compiled without the %s module"%missing_module+
+ " enabled.\nI'm unable to run the GUI part. Please omit the '-g'\n"+
+ "and '--gui' options or install another Python interpreter.\n")
+ raise SystemExit, 1
+
+def gui(host, port):
+ """Graphical interface (starts web server and pops up a control window)."""
+ class GUI:
+ def __init__(self, window, host, port):
+ self.window = window
+ self.server = None
+ self.scanner = None
+
+ try:
+ import Tkinter
+ except ImportError:
+ nogui("Tkinter")
+
+ self.server_frm = Tkinter.Frame(window)
+ self.title_lbl = Tkinter.Label(self.server_frm,
+ text='Starting server...\n ')
+ self.open_btn = Tkinter.Button(self.server_frm,
+ text='open browser', command=self.open, state='disabled')
+ self.quit_btn = Tkinter.Button(self.server_frm,
+ text='quit serving', command=self.quit, state='disabled')
+
+
+ self.window.title('ViewVC standalone')
+ self.window.protocol('WM_DELETE_WINDOW', self.quit)
+ self.title_lbl.pack(side='top', fill='x')
+ self.open_btn.pack(side='left', fill='x', expand=1)
+ self.quit_btn.pack(side='right', fill='x', expand=1)
+
+ # Early loading of configuration here. Used to
+ # allow tinkering with configuration settings through the gui:
+ handle_config()
+ if not LIBRARY_DIR:
+ cfg.options.cvsgraph_conf = "../cgi/cvsgraph.conf.dist"
+
+ self.options_frm = Tkinter.Frame(window)
+
+ # cvsgraph toggle:
+ self.cvsgraph_ivar = Tkinter.IntVar()
+ self.cvsgraph_ivar.set(cfg.options.use_cvsgraph)
+ self.cvsgraph_toggle = Tkinter.Checkbutton(self.options_frm,
+ text="enable cvsgraph (needs binary)", var=self.cvsgraph_ivar,
+ command=self.toggle_use_cvsgraph)
+ self.cvsgraph_toggle.pack(side='top', anchor='w')
+
+ # enscript toggle:
+ self.enscript_ivar = Tkinter.IntVar()
+ self.enscript_ivar.set(cfg.options.use_enscript)
+ self.enscript_toggle = Tkinter.Checkbutton(self.options_frm,
+ text="enable enscript (needs binary)", var=self.enscript_ivar,
+ command=self.toggle_use_enscript)
+ self.enscript_toggle.pack(side='top', anchor='w')
+
+ # show_subdir_lastmod toggle:
+ self.subdirmod_ivar = Tkinter.IntVar()
+ self.subdirmod_ivar.set(cfg.options.show_subdir_lastmod)
+ self.subdirmod_toggle = Tkinter.Checkbutton(self.options_frm,
+ text="show subdir last mod (dir view)", var=self.subdirmod_ivar,
+ command=self.toggle_subdirmod)
+ self.subdirmod_toggle.pack(side='top', anchor='w')
+
+ # use_re_search toggle:
+ self.useresearch_ivar = Tkinter.IntVar()
+ self.useresearch_ivar.set(cfg.options.use_re_search)
+ self.useresearch_toggle = Tkinter.Checkbutton(self.options_frm,
+ text="allow regular expr search", var=self.useresearch_ivar,
+ command=self.toggle_useresearch)
+ self.useresearch_toggle.pack(side='top', anchor='w')
+
+ # use_localtime toggle:
+ self.use_localtime_ivar = Tkinter.IntVar()
+ self.use_localtime_ivar.set(cfg.options.use_localtime)
+ self.use_localtime_toggle = Tkinter.Checkbutton(self.options_frm,
+ text="use localtime (instead of UTC)",
+ var=self.use_localtime_ivar,
+ command=self.toggle_use_localtime)
+ self.use_localtime_toggle.pack(side='top', anchor='w')
+
+ # use_pagesize integer var:
+ self.usepagesize_lbl = Tkinter.Label(self.options_frm,
+ text='Paging (number of items per page, 0 disables):')
+ self.usepagesize_lbl.pack(side='top', anchor='w')
+ self.use_pagesize_ivar = Tkinter.IntVar()
+ self.use_pagesize_ivar.set(cfg.options.use_pagesize)
+ self.use_pagesize_entry = Tkinter.Entry(self.options_frm,
+ width=10, textvariable=self.use_pagesize_ivar)
+ self.use_pagesize_entry.bind('<Return>', self.set_use_pagesize)
+ self.use_pagesize_entry.pack(side='top', anchor='w')
+
+ # directory view template:
+ self.dirtemplate_lbl = Tkinter.Label(self.options_frm,
+ text='Chooose HTML Template for the Directory pages:')
+ self.dirtemplate_lbl.pack(side='top', anchor='w')
+ self.dirtemplate_svar = Tkinter.StringVar()
+ self.dirtemplate_svar.set(cfg.templates.directory)
+ self.dirtemplate_entry = Tkinter.Entry(self.options_frm,
+ width = 40, textvariable=self.dirtemplate_svar)
+ self.dirtemplate_entry.bind('<Return>', self.set_templates_directory)
+ self.dirtemplate_entry.pack(side='top', anchor='w')
+ self.templates_dir = Tkinter.Radiobutton(self.options_frm,
+ text="directory.ezt", value="templates/directory.ezt",
+ var=self.dirtemplate_svar, command=self.set_templates_directory)
+ self.templates_dir.pack(side='top', anchor='w')
+ self.templates_dir_alt = Tkinter.Radiobutton(self.options_frm,
+ text="dir_alternate.ezt", value="templates/dir_alternate.ezt",
+ var=self.dirtemplate_svar, command=self.set_templates_directory)
+ self.templates_dir_alt.pack(side='top', anchor='w')
+
+ # log view template:
+ self.logtemplate_lbl = Tkinter.Label(self.options_frm,
+ text='Chooose HTML Template for the Log pages:')
+ self.logtemplate_lbl.pack(side='top', anchor='w')
+ self.logtemplate_svar = Tkinter.StringVar()
+ self.logtemplate_svar.set(cfg.templates.log)
+ self.logtemplate_entry = Tkinter.Entry(self.options_frm,
+ width = 40, textvariable=self.logtemplate_svar)
+ self.logtemplate_entry.bind('<Return>', self.set_templates_log)
+ self.logtemplate_entry.pack(side='top', anchor='w')
+ self.templates_log = Tkinter.Radiobutton(self.options_frm,
+ text="log.ezt", value="templates/log.ezt",
+ var=self.logtemplate_svar, command=self.set_templates_log)
+ self.templates_log.pack(side='top', anchor='w')
+ self.templates_log_table = Tkinter.Radiobutton(self.options_frm,
+ text="log_table.ezt", value="templates/log_table.ezt",
+ var=self.logtemplate_svar, command=self.set_templates_log)
+ self.templates_log_table.pack(side='top', anchor='w')
+
+ # query view template:
+ self.querytemplate_lbl = Tkinter.Label(self.options_frm,
+ text='Template for the database query page:')
+ self.querytemplate_lbl.pack(side='top', anchor='w')
+ self.querytemplate_svar = Tkinter.StringVar()
+ self.querytemplate_svar.set(cfg.templates.query)
+ self.querytemplate_entry = Tkinter.Entry(self.options_frm,
+ width = 40, textvariable=self.querytemplate_svar)
+ self.querytemplate_entry.bind('<Return>', self.set_templates_query)
+ self.querytemplate_entry.pack(side='top', anchor='w')
+ self.templates_query = Tkinter.Radiobutton(self.options_frm,
+ text="query.ezt", value="templates/query.ezt",
+ var=self.querytemplate_svar, command=self.set_templates_query)
+ self.templates_query.pack(side='top', anchor='w')
+
+ # pack and set window manager hints:
+ self.server_frm.pack(side='top', fill='x')
+ self.options_frm.pack(side='top', fill='x')
+
+ self.window.update()
+ self.minwidth = self.window.winfo_width()
+ self.minheight = self.window.winfo_height()
+ self.expanded = 0
+ self.window.wm_geometry('%dx%d' % (self.minwidth, self.minheight))
+ self.window.wm_minsize(self.minwidth, self.minheight)
+
+ try:
+ import threading
+ except ImportError:
+ nogui("thread")
+ threading.Thread(target=serve,
+ args=(host, port, self.ready)).start()
+
+ def toggle_use_cvsgraph(self, event=None):
+ cfg.options.use_cvsgraph = self.cvsgraph_ivar.get()
+
+ def toggle_use_enscript(self, event=None):
+ cfg.options.use_enscript = self.enscript_ivar.get()
+
+ def toggle_use_localtime(self, event=None):
+ cfg.options.use_localtime = self.use_localtime_ivar.get()
+
+ def toggle_subdirmod(self, event=None):
+ cfg.options.show_subdir_lastmod = self.subdirmod_ivar.get()
+
+ def toggle_useresearch(self, event=None):
+ cfg.options.use_re_search = self.useresearch_ivar.get()
+
+ def set_use_pagesize(self, event=None):
+ cfg.options.use_pagesize = self.use_pagesize_ivar.get()
+
+ def set_templates_log(self, event=None):
+ cfg.templates.log = self.logtemplate_svar.get()
+
+ def set_templates_directory(self, event=None):
+ cfg.templates.directory = self.dirtemplate_svar.get()
+
+ def set_templates_query(self, event=None):
+ cfg.templates.query = self.querytemplate_svar.get()
+
+ def ready(self, server):
+ """used as callback parameter to the serve() function"""
+ self.server = server
+ self.title_lbl.config(
+ text='ViewVC standalone server at\n' + server.url)
+ self.open_btn.config(state='normal')
+ self.quit_btn.config(state='normal')
+
+ def open(self, event=None, url=None):
+ """opens a browser window on the local machine"""
+ url = url or self.server.url
+ try:
+ import webbrowser
+ webbrowser.open(url)
+ except ImportError: # pre-webbrowser.py compatibility
+ if sys.platform == 'win32':
+ os.system('start "%s"' % url)
+ elif sys.platform == 'mac':
+ try:
+ import ic
+ ic.launchurl(url)
+ except ImportError: pass
+ else:
+ rc = os.system('netscape -remote "openURL(%s)" &' % url)
+ if rc: os.system('netscape "%s" &' % url)
+
+ def quit(self, event=None):
+ if self.server:
+ self.server.quit = 1
+ self.window.quit()
+
+ import Tkinter
+ try:
+ gui = GUI(Tkinter.Tk(), host, port)
+ Tkinter.mainloop()
+ except KeyboardInterrupt:
+ pass
+
+# --- command-line interface: ----------------------------------------------
+
+def cli(argv):
+ """Command-line interface (looks at argv to decide what to do)."""
+ import getopt
+ class BadUsage(Exception): pass
+
+ try:
+ opts, args = getopt.getopt(argv[1:], 'gp:r:h:s:',
+ ['gui', 'port=', 'repository=', 'script-alias='])
+ for opt, val in opts:
+ if opt in ('-g', '--gui'):
+ options.start_gui = 1
+ elif opt in ('-r', '--repository'):
+ if options.repositories: # option may be used more than once:
+ num = len(options.repositories.keys())+1
+ symbolic_name = "Repository"+str(num)
+ options.repositories[symbolic_name] = val
+ else:
+ options.repositories["Development"] = val
+ elif opt in ('-p', '--port'):
+ try:
+ options.port = int(val)
+ except ValueError:
+ raise BadUsage
+ elif opt in ('-h', '--host'):
+ options.host = val
+ elif opt in ('-s', '--script-alias'):
+ options.script_alias = \
+ string.join(filter(None, string.split(val, '/')), '/')
+ if options.start_gui:
+ gui(options.host, options.port)
+ return
+ elif options.port:
+ def ready(server):
+ print 'server ready at %s%s' % (server.url,
+ options.script_alias)
+ serve(options.host, options.port, ready)
+ return
+ raise BadUsage
+ except (getopt.error, BadUsage):
+ cmd = os.path.basename(sys.argv[0])
+ port = options.port
+ host = options.host
+ script_alias = options.script_alias
+ print """ViewVC standalone - a simple standalone HTTP-Server
+
+Usage: %(cmd)s [OPTIONS]
+
+Available Options:
+
+-h <host>, --host=<host>:
+ Start the HTTP server listening on <host>. You need to provide
+ the hostname if you want to access the standalone server from a
+ remote machine. [default: %(host)s]
+
+-p <port>, --port=<port>:
+ Start an HTTP server on the given port. [default: %(port)d]
+
+-r <path>, --repository=<path>:
+ Specify a path for a CVS repository. Repository definitions are
+ typically read from the viewvc.conf file, if available. This
+ option may be used more than once.
+
+-s <path>, --script-alias=<path>:
+ Specify the ScriptAlias, the artificial path location that at
+ which ViewVC appears to be located. For example, if your
+ ScriptAlias is "cgi-bin/viewvc", then ViewVC will appear to be
+ accessible at the URL "http://%(host)s:%(port)s/cgi-bin/viewvc".
+ [default: %(script_alias)s]
+
+-g, --gui:
+ Pop up a graphical interface for serving and testing ViewVC.
+ NOTE: this requires a valid X11 display connection.
+""" % locals()
+
+if __name__ == '__main__':
+ options = Options()
+ cli(sys.argv)
--- /dev/null
+#!/usr/bin/env python
+# -*-python-*-
+#
+# Copyright (C) 2004 James Henstridge
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewVC
+# distribution or at http://viewvc.org/license-1.html.
+#
+# For more information, visit http://viewvc.org/
+#
+# -----------------------------------------------------------------------
+#
+# administrative program for loading Subversion revision information
+# into the checkin database. It can be used to add a single revision
+# to the database, or rebuild/update all revisions.
+#
+# To add all the checkins from a Subversion repository to the checkin
+# database, run the following:
+# /path/to/svndbadmin rebuild /path/to/repo
+#
+# This script can also be called from the Subversion post-commit hook,
+# something like this:
+# REPOS="$1"
+# REV="$2"
+# /path/to/svndbadmin update "$REPOS" "$REV"
+#
+# If you allow changes to revision properties in your repository, you
+# might also want to set up something similar in the
+# post-revprop-change hook using "rebuild" instead of "update" to keep
+# the checkin database consistent with the repository.
+#
+# -----------------------------------------------------------------------
+#
+
+#########################################################################
+#
+# INSTALL-TIME CONFIGURATION
+#
+# These values will be set during the installation process. During
+# development, they will remain None.
+#
+
+LIBRARY_DIR = None
+CONF_PATHNAME = None
+
+# Adjust sys.path to include our library directory
+import sys
+import os
+
+if LIBRARY_DIR:
+ sys.path.insert(0, LIBRARY_DIR)
+else:
+ sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../lib")))
+
+#########################################################################
+
+import os
+import string
+import re
+
+import svn.core
+import svn.repos
+import svn.fs
+import svn.delta
+
+import cvsdb
+import viewvc
+
+class SvnRepo:
+ """Class used to manage a connection to a SVN repository."""
+ def __init__(self, path, pool):
+ self.scratch_pool = svn.core.svn_pool_create(pool)
+ self.path = path
+ self.repo = svn.repos.svn_repos_open(path, pool)
+ self.fs = svn.repos.svn_repos_fs(self.repo)
+ # youngest revision of base of file system is highest revision number
+ self.rev_max = svn.fs.youngest_rev(self.fs, pool)
+ def __getitem__(self, rev):
+ if rev is None:
+ rev = self.rev_max
+ elif rev < 0:
+ rev = rev + self.rev_max + 1
+ assert 0 <= rev <= self.rev_max
+ rev = SvnRev(self, rev, self.scratch_pool)
+ svn.core.svn_pool_clear(self.scratch_pool)
+ return rev
+
+_re_diff_change_command = re.compile('(\d+)(?:,(\d+))?([acd])(\d+)(?:,(\d+))?')
+
+def _get_diff_counts(diff_fp):
+ """Calculate the plus/minus counts by parsing the output of a
+ normal diff. The reasons for choosing Normal diff format are:
+ - the output is short, so should be quicker to parse.
+ - only the change commands need be parsed to calculate the counts.
+ - All file data is prefixed, so won't be mistaken for a change
+ command.
+ This code is based on the description of the format found in the
+ GNU diff manual."""
+
+ plus, minus = 0, 0
+ line = diff_fp.readline()
+ while line:
+ match = re.match(_re_diff_change_command, line)
+ if match:
+ # size of first range
+ if match.group(2):
+ count1 = int(match.group(2)) - int(match.group(1)) + 1
+ else:
+ count1 = 1
+ cmd = match.group(3)
+ # size of second range
+ if match.group(5):
+ count2 = int(match.group(5)) - int(match.group(4)) + 1
+ else:
+ count2 = 1
+
+ if cmd == 'a':
+ # LaR - insert after line L of file1 range R of file2
+ plus = plus + count2
+ elif cmd == 'c':
+ # FcT - replace range F of file1 with range T of file2
+ minus = minus + count1
+ plus = plus + count2
+ elif cmd == 'd':
+ # RdL - remove range R of file1, which would have been
+ # at line L of file2
+ minus = minus + count1
+ line = diff_fp.readline()
+ return plus, minus
+
+
+class SvnRev:
+ """Class used to hold information about a particular revision of
+ the repository."""
+ def __init__(self, repo, rev, pool):
+ self.repo = repo
+ self.rev = rev
+ self.pool = pool
+ self.rev_roots = {} # cache of revision roots
+
+ subpool = svn.core.svn_pool_create(pool)
+
+ # revision properties ...
+ properties = svn.fs.revision_proplist(repo.fs, rev, pool)
+ self.author = str(properties.get(svn.core.SVN_PROP_REVISION_AUTHOR,''))
+ self.date = str(properties.get(svn.core.SVN_PROP_REVISION_DATE, ''))
+ self.log = str(properties.get(svn.core.SVN_PROP_REVISION_LOG, ''))
+
+ # convert the date string to seconds since epoch ...
+ self.date = svn.core.secs_from_timestr(self.date, pool)
+
+ # get a root for the current revisions
+ fsroot = self._get_root_for_rev(rev)
+
+ # find changes in the revision
+ editor = svn.repos.RevisionChangeCollector(repo.fs, rev, pool)
+ e_ptr, e_baton = svn.delta.make_editor(editor, pool)
+ svn.repos.svn_repos_replay(fsroot, e_ptr, e_baton, pool)
+
+ self.changes = []
+ for path, change in editor.changes.items():
+
+ # clear the iteration subpool
+ svn.core.svn_pool_clear(subpool)
+
+ # skip non-file changes
+ if change.item_kind != svn.core.svn_node_file: continue
+
+ # deal with the change types we handle
+ base_root = None
+ if change.base_path:
+ base_root = self._get_root_for_rev(change.base_rev)
+
+ if not change.path:
+ action = 'remove'
+ elif change.added:
+ action = 'add'
+ else:
+ action = 'change'
+
+ diffobj = svn.fs.FileDiff(base_root and base_root or None,
+ base_root and change.base_path or None,
+ change.path and fsroot or None,
+ change.path and change.path or None,
+ subpool, [])
+ diff_fp = diffobj.get_pipe()
+ plus, minus = _get_diff_counts(diff_fp)
+ self.changes.append((path, action, plus, minus))
+
+ def _get_root_for_rev(self, rev):
+ """Fetch a revision root from a cache of such, or a fresh root
+ (which is then cached for later use."""
+ if not self.rev_roots.has_key(rev):
+ self.rev_roots[rev] = svn.fs.revision_root(self.repo.fs, rev,
+ self.pool)
+ return self.rev_roots[rev]
+
+
+def handle_revision(db, command, repo, rev, verbose):
+ """Adds a particular revision of the repository to the checkin database."""
+ revision = repo[rev]
+ committed = 0
+
+ if verbose: print "Building commit info for revision %d..." % (rev),
+
+ if not revision.changes:
+ if verbose: print "skipped (no changes)."
+ return
+
+ for (path, action, plus, minus) in revision.changes:
+ directory, file = os.path.split(path)
+ commit = cvsdb.CreateCommit()
+ commit.SetRepository(repo.path)
+ commit.SetDirectory(directory)
+ commit.SetFile(file)
+ commit.SetRevision(str(rev))
+ commit.SetAuthor(revision.author)
+ commit.SetDescription(revision.log)
+ commit.SetTime(revision.date)
+ commit.SetPlusCount(plus)
+ commit.SetMinusCount(minus)
+ commit.SetBranch(None)
+
+ if action == 'add':
+ commit.SetTypeAdd()
+ elif action == 'remove':
+ commit.SetTypeRemove()
+ elif action == 'change':
+ commit.SetTypeChange()
+
+ if command == 'update':
+ result = db.CheckCommit(commit)
+ if result:
+ continue # already recorded
+
+ # commit to database
+ db.AddCommit(commit)
+ committed = 1
+
+ if verbose:
+ if committed:
+ print "done."
+ else:
+ print "skipped (already recorded)."
+
+def main(pool, command, repository, rev=None, verbose=0):
+ cfg = viewvc.load_config(CONF_PATHNAME)
+ db = cvsdb.ConnectDatabase(cfg)
+
+ repo = SvnRepo(repository, pool)
+ if rev:
+ handle_revision(db, command, repo, rev, verbose)
+ else:
+ for rev in range(repo.rev_max+1):
+ handle_revision(db, command, repo, rev, verbose)
+
+def usage():
+ cmd = os.path.basename(sys.argv[0])
+ sys.stderr.write("""
+Usage: 1. %s [-v] rebuild REPOSITORY [REVISION]
+ 2. %s [-v] update REPOSITORY [REVISION]
+
+1. Rebuild the commit database information for REPOSITORY across all revisions
+ or, optionally, only for the specified REVISION.
+
+2. Update the commit database information for REPOSITORY across all revisions
+ or, optionally, only for the specified REVISION. This is just like
+ rebuilding, except that no commit information will be stored for
+ commits already present in the database.
+
+Use the -v flag to cause this script to give progress information as it works.
+""" % (cmd, cmd))
+ sys.exit(1)
+
+if __name__ == '__main__':
+ verbose = 0
+
+ args = sys.argv
+ try:
+ index = args.index('-v')
+ verbose = 1
+ del args[index]
+ except ValueError:
+ pass
+
+ if len(args) < 3:
+ usage()
+
+ command = string.lower(args[1])
+ if command not in ('rebuild', 'update'):
+ sys.stderr.write('ERROR: unknown command %s\n' % command)
+ usage()
+
+ repository = args[2]
+ if not os.path.exists(repository):
+ sys.stderr.write('ERROR: could not find repository %s\n' % repository)
+ usage()
+
+ if len(sys.argv) > 3:
+ rev = sys.argv[3]
+ try:
+ rev = int(rev)
+ except ValueError:
+ sys.stderr.write('ERROR: revision "%s" is not numeric\n' % rev)
+ usage()
+ else:
+ rev = None
+
+ repository = cvsdb.CleanRepository(os.path.abspath(repository))
+ svn.core.run_app(main, command, repository, rev, verbose)
--- /dev/null
+# CvsGraph configuration
+#
+# - Empty lines and whitespace are ignored.
+#
+# - Comments start with '#' and everything until
+# end of line is ignored.
+#
+# - Strings are C-style strings in which characters
+# may be escaped with '\' and written in octal
+# and hex escapes. Note that '\' must be escaped
+# if it is to be entered as a character.
+#
+# - Some strings are expanded with printf like
+# conversions which start with '%'. Not all
+# are applicable at all times, in which case they
+# will expand to nothing.
+# %c = cvsroot (with trailing '/')
+# %C = cvsroot (*without* trailing '/')
+# %m = module (with trailing '/')
+# %M = module (*without* trailing '/')
+# %f = filename without path
+# %F = filename without path and with ",v" stripped
+# %p = path part of filename (with trailing '/')
+# %r = number of revisions
+# %b = number of branches
+# %% = '%'
+# %R = the revision number (e.g. '1.2.4.4')
+# %P = previous revision number
+# %B = the branch number (e.g. '1.2.4')
+# %d = date of revision
+# %a = author of revision
+# %s = state of revision
+# %t = current tag of branch or revision
+# %0..%9 = command-line argument -0 .. -9
+# %l = HTMLized log entry of the revision
+# NOTE: %l is obsolete. See %(%) and cvsgraph.conf(5) for
+# more details.
+# %L = log entry of revision
+# The log entry expansion takes an optional argument to
+# specify maximum length of the expansion like %L[25].
+# %(...%) = HTMLize the string within the parenthesis.
+# ViewVC currently uses the following four command-line arguments to
+# pass URL information to cvsgraph:
+# -3 link to current file's log page
+# -4 link to current file's checkout page minus "rev" parameter
+# -5 link to current file's diff page minus "r1" and "r2" parameters
+# -6 link to current directory page minus "pathrev" parameter
+#
+# - Numbers may be entered as octal, decimal or
+# hex as in 0117, 79 and 0x4f respectively.
+#
+# - Fonts are numbered 0..4 (defined as in libgd)
+# 0 = tiny
+# 1 = small
+# 2 = medium (bold)
+# 3 = large
+# 4 = giant
+#
+# - Colors are a string like HTML type colors in
+# the form "#rrggbb" with parts written in hex
+# rr = red (00..ff)
+# gg = green (00-ff)
+# bb = blue (00-ff)
+#
+# - There are several reserved words besides of the
+# feature-keywords. These additional reserved words
+# expand to numerical values:
+# * false = 0
+# * true = 1
+# * not = -1
+# * left = 0
+# * center = 1
+# * right = 2
+# * gif = 0
+# * png = 1
+# * jpeg = 2
+# * tiny = 0
+# * small = 1
+# * medium = 2
+# * large = 3
+# * giant = 4
+#
+# - Booleans have three possible arguments: true, false
+# and not. `Not' means inverse of what it was (logical
+# negation) and is represented by the value -1.
+# For the configuration file that means that the default
+# value is negated.
+#
+
+# cvsroot <string>
+# The *absolute* base directory where the
+# CVS/RCS repository can be found
+# cvsmodule <string>
+#
+cvsroot = "--unused--"; # unused with ViewVC, will be overridden
+cvsmodule = ""; # unused with ViewVC -- please leave it blank
+
+# color_bg <color>
+# The background color of the image
+# transparent_bg <boolean>
+# Make color_bg the transparent color (only useful with PNG)
+color_bg = "#ffffff";
+transparent_bg = false;
+
+# date_format <string>
+# The strftime(3) format string for date and time
+date_format = "%d-%b-%Y %H:%M:%S";
+
+# box_shadow <boolean>
+# Add a shadow around the boxes
+# upside_down <boolean>
+# Reverse the order of the revisions
+# left_right <boolean>
+# Draw the image left to right instead of top down,
+# or right to left is upside_down is set simultaneously.
+# strip_untagged <boolean>
+# Remove all untagged revisions except the first, last and tagged ones
+# strip_first_rev <boolean>
+# Also remove the first revision if untagged
+# auto_stretch <boolean>
+# Try to reformat the tree to minimize image size
+# use_ttf <boolean>
+# Use TrueType fonts for text
+# anti_alias <boolean>
+# Enable pretty TrueType anti-alias drawing
+# thick_lines <number>
+# Draw all connector lines thicker (range: 1..11)
+box_shadow = true;
+upside_down = false;
+left_right = false;
+strip_untagged = false;
+strip_first_rev = false;
+#auto_stretch = true; # not yet stable.
+use_ttf = false;
+anti_alias = true;
+thick_lines = 1;
+
+# msg_color <color>
+# Sets the error/warning message color
+# msg_font <number>
+# msg_ttfont <string>
+# msg_ttsize <float>
+# Sets the error/warning message font
+msg_color = "#800000";
+msg_font = medium;
+msg_ttfont = "/dos/windows/fonts/ariali.ttf";
+msg_ttsize = 11.0;
+
+# parse_logs <boolean>
+# Enable the parsing of the *entire* ,v file to read the
+# log-entries between revisions. This is necessary for
+# the %L expansion to work, but slows down parsing by
+# a very large factor. You're warned.
+parse_logs = false;
+
+# tag_font <number>
+# The font of the tag text
+# tag_color <color>
+# The color of the tag text
+# tag_ignore <string>
+# A extended regular expression to exclude certain tags from view.
+# See regex(7) for details on the format.
+# Note 1: tags matched in merge_from/merge_to are always displayed unless
+# tag_ignore_merge is set to true.
+# Note 2: normal string rules apply and special characters must be
+# escaped.
+# tag_ignore_merge <boolean>
+# If set to true, allows tag_ignore to also hide merge_from and merge_to
+# tags.
+# tag_nocase <boolean>
+# Ignore the case is tag_ignore expressions
+# tag_negate <boolean>
+# Negate the matching criteria of tag_ignore. When true, only matching
+# tags will be shown.
+# Note: tags matched with merge_from/merge_to will still be displayed.
+tag_font = medium;
+#tag_ttfont = "/dos/windows/fonts/ariali.ttf";
+#tag_ttsize = 11.0;
+tag_color = "#007000";
+#tag_ignore = "(test|alpha)_release";
+#tag_ignore_merge = false;
+#tag_nocase = false;
+#tag_negate = false;
+
+# rev_hidenumber <boolean>
+# If set to true no revision numbers will be printed in the graph.
+#rev_hidenumber = false;
+rev_font = giant;
+#rev_ttfont = "/dos/windows/fonts/arial.ttf";
+#rev_ttsize = 12.0;
+rev_color = "#000000";
+rev_bgcolor = "#f0f0f0";
+rev_separator = 1;
+rev_minline = 15;
+rev_maxline = 75;
+rev_lspace = 5;
+rev_rspace = 5;
+rev_tspace = 3;
+rev_bspace = 3;
+rev_text = "%d"; # or "%d\n%a, %s" for author and state too
+rev_text_font = tiny;
+#rev_text_ttfont = "/dos/windows/fonts/times.ttf";
+#rev_text_ttsize = 9.0;
+rev_text_color = "#500020";
+rev_maxtags = 25;
+
+# merge_color <color>
+# The color of the line connecting merges
+# merge_front <boolean>
+# If true, draw the merge-lines on top if the image
+# merge_nocase <boolean>
+# Ignore case in regular expressions
+# merge_from <string>
+# A regex describing a tag that is used as the merge source
+# merge_to <string>
+# A regex describing a tag that is the target of the merge
+# merge_findall <boolean>
+# Try to match all merge_to targets possible. This can result in
+# multiple lines originating from one tag.
+# merge_arrows <boolean>
+# Use arrows to point to the merge destination. Default is true.
+# merge_cvsnt <boolean>
+# Use CVSNT's mergepoint registration for merges
+# merge_cvsnt_color <color>
+# The color of the line connecting merges from/to registered
+# mergepoints.
+# arrow_width <number>
+# arrow_length <number>
+# Specify the size of the arrows. Default is 3 wide and 12 long.
+#
+# NOTE:
+# - The merge_from is an extended regular expression as described in
+# regex(7) and POSIX 1003.2 (see also Single Unix Specification at
+# http://www.opengroup.com).
+# - The merge_to is an extended regular expression with a twist. All
+# subexpressions from the merge_from are expanded into merge_to
+# using %[1-9] (in contrast to \[1-9] for backreferences). Care is
+# taken to escape the constructed expression.
+# - A '$' at the end of the merge_to expression can be important to
+# prevent 'near match' references. Normally, you want the destination
+# to be a good representation of the source. However, this depends
+# on how well you defined the tags in the first place.
+#
+# Example:
+# merge_from = "^f_(.*)";
+# merge_to = "^t_%1$";
+# tags: f_foo, f_bar, f_foobar, t_foo, t_bar
+# result:
+# f_foo -> "^t_foo$" -> t_foo
+# f_bar -> "^t_bar$" -> t_bar
+# f_foobar-> "^t_foobar$" -> <no match>
+#
+merge_color = "#a000a0";
+merge_front = false;
+merge_nocase = false;
+merge_from = "^f_(.*)";
+merge_to = "^t_%1$";
+merge_findall = false;
+
+#merge_arrows = true;
+#arrow_width = 3;
+#arrow_length = 12;
+
+merge_cvsnt = true;
+merge_cvsnt_color = "#606000";
+
+# branch_font <number>
+# The font of the number and tags
+# branch_color <color>
+# All branch element's color
+# branch_[lrtb]space <number>
+# Interior spacing (margin)
+# branch_margin <number>
+# Exterior spacing
+# branch_connect <number>
+# Length of the vertical connector
+# branch_dupbox <boolean>
+# Add the branch-tag also at the bottom/top of the trunk
+# branch_fold <boolean>
+# Fold empty branches in one box to save space
+# branch_foldall <boolean>
+# Put all empty branches in one box, even if they
+# were interspaced with branches with revisions.
+# branch_resort <boolean>
+# Resort the branches by the number of revisions to save space
+# branch_subtree <string>
+# Only show the branch denoted or all branches that sprout
+# from the denoted revision. The argument may be a symbolic
+# tag. This option you would normally want to set from the
+# command line with the -O option.
+branch_font = medium;
+#branch_ttfont = "/dos/windows/fonts/arialbd.ttf";
+#branch_ttsize = 18.0;
+branch_tag_color= "#000080";
+branch_tag_font = medium;
+#branch_tag_ttfont = "/dos/windows/fonts/arialbi.ttf";
+#branch_tag_ttsize = 14.0;
+branch_color = "#0000c0";
+branch_bgcolor = "#ffffc0";
+branch_lspace = 5;
+branch_rspace = 5;
+branch_tspace = 3;
+branch_bspace = 3;
+branch_margin = 15;
+branch_connect = 8;
+branch_dupbox = false;
+branch_fold = true;
+branch_foldall = false;
+branch_resort = false;
+#branch_subtree = "1.2.4";
+
+# title <string>
+# The title string is expanded (see above for details)
+# title_[xy] <number>
+# Position of title
+# title_font <number>
+# The font
+# title_align <number>
+# 0 = left
+# 1 = center
+# 2 = right
+# title_color <color>
+title = "%c%p%f\nRevisions: %r, Branches: %b";
+title_x = 10;
+title_y = 5;
+title_font = small;
+#title_ttfont = "/dos/windows/fonts/times.ttf";
+#title_ttsize = 10.0;
+title_align = left;
+title_color = "#800000";
+
+# Margins of the image
+# Note: the title is outside the margin
+margin_top = 35;
+margin_bottom = 10;
+margin_left = 10;
+margin_right = 10;
+
+# Image format(s)
+# image_type <number|{gif,jpeg,png}>
+# gif (0) = Create gif image
+# png (1) = Create png image
+# jpeg (2) = Create jpeg image
+# Image types are available if they can be found in
+# the gd library. Newer versions of gd do not have
+# gif anymore. CvsGraph will automatically generate
+# png images instead.
+# image_quality <number>
+# The quality of a jpeg image (1..100)
+# image_compress <number>
+# Set the compression of a PNG image (gd version >= 2.0.12).
+# Values range from -1 to 9 where:
+# - -1 default compression (usually 3)
+# - 0 no compression
+# - 1 lowest level compression
+# - ... ...
+# - 9 highest level of compression
+# image_interlace <boolean>
+# Write interlaces PNG/JPEG images for progressive loading.
+image_type = png;
+image_quality = 75;
+image_compress = 3;
+image_interlace = true;
+
+# HTML image map generation
+# map_name <string>
+# The name= attribute in <map name="mapname">...</map>
+# map_branch_href <string>
+# map_branch_alt <string>
+# map_rev_href <string>
+# map_rev_alt <string>
+# map_diff_href <string>
+# map_diff_alt <string>
+# map_merge_href <string>
+# map_merge_alt <string>
+# These are the href= and alt= attributes in the <area>
+# tags of HTML. The strings are expanded (see above).
+map_name = "MyMapName\" name=\"MyMapName";
+map_branch_href = "href=\"%6pathrev=%(%t%)\"";
+map_branch_alt = "alt=\"%0 %(%t%) (%B)\"";
+# You might want to experiment with the following setting:
+# 1. The default setting will take you to a ViewVC generated page displaying
+# that revision of the file, if you click into a revision box:
+map_rev_href = "href=\"%4rev=%R\"";
+# 2. This alternative setting will take you to the anchor representing this
+# revision on a ViewVC generated Log page for that file:
+# map_rev_href = "href=\"%3#rev%R\"";
+#
+map_rev_alt = "alt=\"%1 %(%t%) (%R)\"";
+map_diff_href = "href=\"%5r1=%P&r2=%R\"";
+map_diff_alt = "alt=\"%2 %P <-> %R\"";
+map_merge_href = "href=\"%5r1=%P&r2=%R\"";
+map_merge_alt = "alt=\"%2 %P <-> %R\"";
+
--- /dev/null
+"""Module to analyze Python source code; for syntax coloring tools.
+
+Interface:
+
+ tags = fontify(pytext, searchfrom, searchto)
+
+The PYTEXT argument is a string containing Python source code. The
+(optional) arguments SEARCHFROM and SEARCHTO may contain a slice in
+PYTEXT.
+
+The returned value is a list of tuples, formatted like this:
+
+ [('keyword', 0, 6, None),
+ ('keyword', 11, 17, None),
+ ('comment', 23, 53, None),
+ ...
+ ]
+
+The tuple contents are always like this:
+
+ (tag, startindex, endindex, sublist)
+
+TAG is one of 'keyword', 'string', 'comment' or 'identifier'
+SUBLIST is not used, hence always None.
+"""
+
+# Based on FontText.py by Mitchell S. Chapman,
+# which was modified by Zachary Roadhouse,
+# then un-Tk'd by Just van Rossum.
+# Many thanks for regular expression debugging & authoring are due to:
+# Tim (the-incredib-ly y'rs) Peters and Cristian Tismer
+# So, who owns the copyright? ;-) How about this:
+# Copyright 1996-1997:
+# Mitchell S. Chapman,
+# Zachary Roadhouse,
+# Tim Peters,
+# Just van Rossum
+
+__version__ = "0.3.1"
+
+import string, re
+
+
+# This list of keywords is taken from ref/node13.html of the
+# Python 1.3 HTML documentation. ("access" is intentionally omitted.)
+
+keywordsList = ["and", "assert", "break", "class", "continue", "def",
+ "del", "elif", "else", "except", "exec", "finally",
+ "for", "from", "global", "if", "import", "in", "is",
+ "lambda", "not", "or", "pass", "print", "raise",
+ "return", "try", "while",
+ ]
+
+# First a little helper, since I don't like to repeat things. (Tismer speaking)
+def replace(where, what, with):
+ return string.join(string.split(where, what), with)
+
+# A regexp for matching Python comments.
+commentPat = "#.*"
+
+# A regexp for matching simple quoted strings.
+pat = "q[^q\\n]*(\\[\000-\377][^q\\n]*)*q"
+quotePat = replace(pat, "q", "'") + "|" + replace(pat, 'q', '"')
+
+# A regexp for matching multi-line tripled-quoted strings. (Way to go, Tim!)
+pat = """
+ qqq
+ [^q]*
+ (
+ ( \\[\000-\377]
+ | q
+ ( \\[\000-\377]
+ | [^q]
+ | q
+ ( \\[\000-\377]
+ | [^q]
+ )
+ )
+ )
+ [^q]*
+ )*
+ qqq
+"""
+pat = string.join(string.split(pat), '') # get rid of whitespace
+tripleQuotePat = replace(pat, "q", "'") + "|" + replace(pat, 'q', '"')
+
+# A regexp which matches all and only Python keywords. This will let
+# us skip the uninteresting identifier references.
+nonKeyPat = "(^|[^a-zA-Z0-9_.\"'])" # legal keyword-preceding characters
+keyPat = nonKeyPat + "(" + string.join(keywordsList, "|") + ")" + nonKeyPat
+
+# Our final syntax-matching regexp is the concatation of the regexp's we
+# constructed above.
+syntaxPat = keyPat + \
+ "|" + commentPat + \
+ "|" + tripleQuotePat + \
+ "|" + quotePat
+syntaxRE = re.compile(syntaxPat)
+
+# Finally, we construct a regexp for matching indentifiers (with
+# optional leading whitespace).
+idKeyPat = "[ \t]*[A-Za-z_][A-Za-z_0-9.]*"
+idRE = re.compile(idKeyPat)
+
+
+def fontify(pytext, searchfrom=0, searchto=None):
+ if searchto is None:
+ searchto = len(pytext)
+ tags = []
+ commentTag = 'comment'
+ stringTag = 'string'
+ keywordTag = 'keyword'
+ identifierTag = 'identifier'
+
+ start = 0
+ end = searchfrom
+ while 1:
+ # Look for some syntax token we're interested in. If find
+ # nothing, we're done.
+ matchobj = syntaxRE.search(pytext, end)
+ if not matchobj:
+ break
+
+ # If we found something outside our search area, it doesn't
+ # count (and we're done).
+ start = matchobj.start()
+ if start >= searchto:
+ break
+
+ match = matchobj.group(0)
+ end = start + len(match)
+ c = match[0]
+ if c == '#':
+ # We matched a comment.
+ tags.append((commentTag, start, end, None))
+ elif c == '"' or c == '\'':
+ # We matched a string.
+ tags.append((stringTag, start, end, None))
+ else:
+ # We matched a keyword.
+ if start != searchfrom:
+ # there's still a redundant char before and after it, strip!
+ match = match[1:-1]
+ start = start + 1
+ else:
+ # This is the first keyword in the text.
+ # Only a space at the end.
+ match = match[:-1]
+ end = end - 1
+ tags.append((keywordTag, start, end, None))
+ # If this was a defining keyword, look ahead to the
+ # following identifier.
+ if match in ["def", "class"]:
+ matchobj = idRE.search(pytext, end)
+ if matchobj:
+ start = matchobj.start()
+ if start == end and start < searchto:
+ end = start + len(matchobj.group(0))
+ tags.append((identifierTag, start, end, None))
+ return tags
+
+
+def test(path):
+ f = open(path)
+ text = f.read()
+ f.close()
+ tags = fontify(text)
+ for tag, start, end, sublist in tags:
+ print tag, `text[start:end]`
+
+if __name__ == "__main__":
+ import sys
+ test(sys.argv[0])
--- /dev/null
+# -*-python-*-
+#
+# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewVC
+# distribution or at http://viewvc.org/license-1.html.
+#
+# For more information, visit http://viewvc.org/
+#
+# -----------------------------------------------------------------------
+#
+# accept.py: parse/handle the various Accept headers from the client
+#
+# -----------------------------------------------------------------------
+
+import re
+import string
+
+
+def language(hdr):
+ "Parse an Accept-Language header."
+
+ # parse the header, storing results in a _LanguageSelector object
+ return _parse(hdr, _LanguageSelector())
+
+# -----------------------------------------------------------------------
+
+_re_token = re.compile(r'\s*([^\s;,"]+|"[^"]*")+\s*')
+_re_param = re.compile(r';\s*([^;,"]+|"[^"]*")+\s*')
+_re_split_param = re.compile(r'([^\s=])\s*=\s*(.*)')
+
+def _parse(hdr, result):
+ # quick exit for empty or not-supplied header
+ if not hdr:
+ return result
+
+ pos = 0
+ while pos < len(hdr):
+ name = _re_token.match(hdr, pos)
+ if not name:
+ raise AcceptParseError()
+ a = result.item_class(string.lower(name.group(1)))
+ pos = name.end()
+ while 1:
+ # are we looking at a parameter?
+ match = _re_param.match(hdr, pos)
+ if not match:
+ break
+ param = match.group(1)
+ pos = match.end()
+
+ # split up the pieces of the parameter
+ match = _re_split_param.match(param)
+ if not match:
+ # the "=" was probably missing
+ continue
+
+ pname = string.lower(match.group(1))
+ if pname == 'q' or pname == 'qs':
+ try:
+ a.quality = float(match.group(2))
+ except ValueError:
+ # bad float literal
+ pass
+ elif pname == 'level':
+ try:
+ a.level = float(match.group(2))
+ except ValueError:
+ # bad float literal
+ pass
+ elif pname == 'charset':
+ a.charset = string.lower(match.group(2))
+
+ result.append(a)
+ if hdr[pos:pos+1] == ',':
+ pos = pos + 1
+
+ return result
+
+class _AcceptItem:
+ def __init__(self, name):
+ self.name = name
+ self.quality = 1.0
+ self.level = 0.0
+ self.charset = ''
+
+ def __str__(self):
+ s = self.name
+ if self.quality != 1.0:
+ s = '%s;q=%.3f' % (s, self.quality)
+ if self.level != 0.0:
+ s = '%s;level=%.3f' % (s, self.level)
+ if self.charset:
+ s = '%s;charset=%s' % (s, self.charset)
+ return s
+
+class _LanguageRange(_AcceptItem):
+ def matches(self, tag):
+ "Match the tag against self. Returns the qvalue, or None if non-matching."
+ if tag == self.name:
+ return self.quality
+
+ # are we a prefix of the available language-tag
+ name = self.name + '-'
+ if tag[:len(name)] == name:
+ return self.quality
+ return None
+
+class _LanguageSelector:
+ """Instances select an available language based on the user's request.
+
+ Languages found in the user's request are added to this object with the
+ append() method (they should be instances of _LanguageRange). After the
+ languages have been added, then the caller can use select_from() to
+ determine which user-request language(s) best matches the set of
+ available languages.
+
+ Strictly speaking, this class is pretty close for more than just
+ language matching. It has been implemented to enable q-value based
+ matching between requests and availability. Some minor tweaks may be
+ necessary, but simply using a new 'item_class' should be sufficient
+ to allow the _parse() function to construct a selector which holds
+ the appropriate item implementations (e.g. _LanguageRange is the
+ concrete _AcceptItem class that handles matching of language tags).
+ """
+
+ item_class = _LanguageRange
+
+ def __init__(self):
+ self.requested = [ ]
+
+ def select_from(self, avail):
+ """Select one of the available choices based on the request.
+
+ Note: if there isn't a match, then the first available choice is
+ considered the default. Also, if a number of matches are equally
+ relevant, then the first-requested will be used.
+
+ avail is a list of language-tag strings of available languages
+ """
+
+ # tuples of (qvalue, language-tag)
+ matches = [ ]
+
+ # try matching all pairs of desired vs available, recording the
+ # resulting qvalues. we also need to record the longest language-range
+ # that matches since the most specific range "wins"
+ for tag in avail:
+ longest = 0
+ final = 0.0
+
+ # check this tag against the requests from the user
+ for want in self.requested:
+ qvalue = want.matches(tag)
+ #print 'have %s. want %s. qvalue=%s' % (tag, want.name, qvalue)
+ if qvalue is not None and len(want.name) > longest:
+ # we have a match and it is longer than any we may have had.
+ # the final qvalue should be from this tag.
+ final = qvalue
+ longest = len(want.name)
+
+ # a non-zero qvalue is a potential match
+ if final:
+ matches.append((final, tag))
+
+ # if there are no matches, then return the default language tag
+ if not matches:
+ return avail[0]
+
+ # get the highest qvalue and its corresponding tag
+ matches.sort()
+ qvalue, tag = matches[-1]
+
+ # if the qvalue is zero, then we have no valid matches. return the
+ # default language tag.
+ if not qvalue:
+ return avail[0]
+
+ # if there are two or more matches, and the second-highest has a
+ # qvalue equal to the best, then we have multiple "best" options.
+ # select the one that occurs first in self.requested
+ if len(matches) >= 2 and matches[-2][0] == qvalue:
+ # remove non-best matches
+ while matches[0][0] != qvalue:
+ del matches[0]
+ #print "non-deterministic choice", matches
+
+ # sequence through self.requested, in order
+ for want in self.requested:
+ # try to find this one in our best matches
+ for qvalue, tag in matches:
+ if want.matches(tag):
+ # this requested item is one of the "best" options
+ ### note: this request item could match *other* "best" options,
+ ### so returning *this* one is rather non-deterministic.
+ ### theoretically, we could go further here, and do another
+ ### search based on the ordering in 'avail'. however, note
+ ### that this generally means that we are picking from multiple
+ ### *SUB* languages, so I'm all right with the non-determinism
+ ### at this point. stupid client should send a qvalue if they
+ ### want to refine.
+ return tag
+
+ # NOTREACHED
+
+ # return the best match
+ return tag
+
+ def append(self, item):
+ self.requested.append(item)
+
+class AcceptParseError(Exception):
+ pass
+
+def _test():
+ s = language('en')
+ assert s.select_from(['en']) == 'en'
+ assert s.select_from(['en', 'de']) == 'en'
+ assert s.select_from(['de', 'en']) == 'en'
+
+ # Netscape 4.x and early version of Mozilla may not send a q value
+ s = language('en, ja')
+ assert s.select_from(['en', 'ja']) == 'en'
+
+ s = language('fr, de;q=0.9, en-gb;q=0.7, en;q=0.6, en-gb-foo;q=0.8')
+ assert s.select_from(['en']) == 'en'
+ assert s.select_from(['en-gb-foo']) == 'en-gb-foo'
+ assert s.select_from(['de', 'fr']) == 'fr'
+ assert s.select_from(['de', 'en-gb']) == 'de'
+ assert s.select_from(['en-gb', 'en-gb-foo']) == 'en-gb-foo'
+ assert s.select_from(['en-bar']) == 'en-bar'
+ assert s.select_from(['en-gb-bar', 'en-gb-foo']) == 'en-gb-foo'
+
+ # non-deterministic. en-gb;q=0.7 matches both avail tags.
+ #assert s.select_from(['en-gb-bar', 'en-gb']) == 'en-gb'
--- /dev/null
+#!/usr/bin/env python
+# -*-python-*-
+#
+# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
+# Copyright (C) 2000 Curt Hagenlocher <curt@hagenlocher.org>
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewVC
+# distribution or at http://viewvc.org/license-1.html.
+#
+# For more information, visit http://viewvc.org/
+#
+# -----------------------------------------------------------------------
+#
+# blame.py: Annotate each line of a CVS file with its author,
+# revision #, date, etc.
+#
+# -----------------------------------------------------------------------
+#
+# This file is based on the cvsblame.pl portion of the Bonsai CVS tool,
+# developed by Steve Lamm for Netscape Communications Corporation. More
+# information about Bonsai can be found at
+# http://www.mozilla.org/bonsai.html
+#
+# cvsblame.pl, in turn, was based on Scott Furman's cvsblame script
+#
+# -----------------------------------------------------------------------
+
+import string
+import os
+import re
+import time
+import math
+import cgi
+import vclib
+import vclib.ccvs.blame
+
+
+re_includes = re.compile('\\#(\\s*)include(\\s*)"(.*?)"')
+
+def link_includes(text, repos, path_parts, include_url):
+ match = re_includes.match(text)
+ if match:
+ incfile = match.group(3)
+
+ # check current directory and parent directory for file
+ for depth in (-1, -2):
+ include_path = path_parts[:depth] + [incfile]
+ try:
+ # will throw if path doesn't exist
+ if repos.itemtype(include_path, None) == vclib.FILE:
+ break
+ except vclib.ItemNotFound:
+ pass
+ else:
+ include_path = None
+
+ if include_path:
+ url = string.replace(include_url, '/WHERE/',
+ string.join(include_path, '/'))
+ return '#%sinclude%s<a href="%s">"%s"</a>' % \
+ (match.group(1), match.group(2), url, incfile)
+
+ return text
+
+
+class HTMLBlameSource:
+ """Wrapper around a the object by the vclib.annotate() which does
+ HTML escaping, diff URL generation, and #include linking."""
+ def __init__(self, repos, path_parts, diff_url, include_url, opt_rev=None):
+ self.repos = repos
+ self.path_parts = path_parts
+ self.diff_url = diff_url
+ self.include_url = include_url
+ self.annotation, self.revision = self.repos.annotate(path_parts, opt_rev)
+
+ def __getitem__(self, idx):
+ item = self.annotation.__getitem__(idx)
+ diff_url = None
+ if item.prev_rev:
+ diff_url = '%sr1=%s&r2=%s' % (self.diff_url, item.prev_rev, item.rev)
+ thisline = link_includes(cgi.escape(item.text), self.repos,
+ self.path_parts, self.include_url)
+ return _item(text=thisline, line_number=item.line_number,
+ rev=item.rev, prev_rev=item.prev_rev,
+ diff_url=diff_url, date=item.date, author=item.author)
+
+
+def blame(repos, path_parts, diff_url, include_url, opt_rev=None):
+ source = HTMLBlameSource(repos, path_parts, diff_url, include_url, opt_rev)
+ return source, source.revision
+
+
+class _item:
+ def __init__(self, **kw):
+ vars(self).update(kw)
+
+
+def make_html(root, rcs_path):
+ bs = vclib.ccvs.blame.BlameSource(os.path.join(root, rcs_path))
+
+ count = bs.num_lines
+ if count == 0:
+ count = 1
+
+ line_num_width = int(math.log10(count)) + 1
+ revision_width = 3
+ author_width = 5
+ line = 0
+ old_revision = 0
+ row_color = ''
+ inMark = 0
+ rev_count = 0
+
+ open_table_tag = '<table cellpadding="0" cellspacing="0">'
+ startOfRow = '<tr><td colspan="3"%s><pre>'
+ endOfRow = '</td></tr>'
+
+ print open_table_tag + (startOfRow % '')
+
+ for line_data in bs:
+ revision = line_data.rev
+ thisline = line_data.text
+ line = line_data.line_number
+ author = line_data.author
+ prev_rev = line_data.prev_rev
+
+ output = ''
+
+ if old_revision != revision and line != 1:
+ if row_color == '':
+ row_color = ' style="background-color:#e7e7e7"'
+ else:
+ row_color = ''
+
+ if not inMark:
+ output = output + endOfRow + (startOfRow % row_color)
+
+ output = output + '<a name="%d">%*d</a>' % (line, line_num_width, line)
+
+ if old_revision != revision or rev_count > 20:
+ revision_width = max(revision_width, len(revision))
+ output = output + ' '
+ author_width = max(author_width, len(author))
+ output = output + ('%-*s ' % (author_width, author))
+ output = output + revision
+ if prev_rev:
+ output = output + '</a>'
+ output = output + (' ' * (revision_width - len(revision) + 1))
+
+ old_revision = revision
+ rev_count = 0
+ else:
+ output = output + ' ' + (' ' * (author_width + revision_width))
+ rev_count = rev_count + 1
+
+ output = output + thisline
+
+ # Close the highlighted section
+ #if (defined $mark_cmd and mark_cmd != 'begin'):
+ # chop($output)
+ # output = output + endOfRow + (startOfRow % row_color)
+ # inMark = 0
+
+ print output
+ print endOfRow + '</table>'
+
+
+def main():
+ import sys
+ if len(sys.argv) != 3:
+ print 'USAGE: %s cvsroot rcs-file' % sys.argv[0]
+ sys.exit(1)
+ make_html(sys.argv[1], sys.argv[2])
+
+if __name__ == '__main__':
+ main()
--- /dev/null
+# -*-python-*-
+#
+# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewVC
+# distribution or at http://viewvc.org/license-1.html.
+#
+# For more information, visit http://viewvc.org/
+#
+# -----------------------------------------------------------------------
+#
+# compat.py: compatibility functions for operation across Python 1.5.x to 2.2.x
+#
+# -----------------------------------------------------------------------
+
+import urllib
+import string
+import time
+import calendar
+import re
+import os
+import rfc822
+import tempfile
+import errno
+
+#
+# urllib.urlencode() is new to Python 1.5.2
+#
+try:
+ urlencode = urllib.urlencode
+except AttributeError:
+ def urlencode(dict):
+ "Encode a dictionary as application/x-url-form-encoded."
+ if not dict:
+ return ''
+ quote = urllib.quote_plus
+ keyvalue = [ ]
+ for key, value in dict.items():
+ keyvalue.append(quote(key) + '=' + quote(str(value)))
+ return string.join(keyvalue, '&')
+
+#
+# time.strptime() is new to Python 1.5.2
+#
+if hasattr(time, 'strptime'):
+ def cvs_strptime(timestr):
+ 'Parse a CVS-style date/time value.'
+ return time.strptime(timestr, '%Y/%m/%d %H:%M:%S')[:-1] + (0,)
+else:
+ _re_rev_date = re.compile('([0-9]{4})/([0-9][0-9])/([0-9][0-9]) '
+ '([0-9][0-9]):([0-9][0-9]):([0-9][0-9])')
+ def cvs_strptime(timestr):
+ 'Parse a CVS-style date/time value.'
+ match = _re_rev_date.match(timestr)
+ if match:
+ return tuple(map(int, match.groups())) + (0, 1, 0)
+ else:
+ raise ValueError('date is not in cvs format')
+
+#
+# os.makedirs() is new to Python 1.5.2
+#
+try:
+ makedirs = os.makedirs
+except AttributeError:
+ def makedirs(path, mode=0777):
+ head, tail = os.path.split(path)
+ if head and tail and not os.path.exists(head):
+ makedirs(head, mode)
+ os.mkdir(path, mode)
+
+#
+# rfc822.formatdate() is new to Python 1.6
+#
+try:
+ formatdate = rfc822.formatdate
+except AttributeError:
+ def formatdate(timeval):
+ if timeval is None:
+ timeval = time.time()
+ timeval = time.gmtime(timeval)
+ return "%s, %02d %s %04d %02d:%02d:%02d GMT" % (
+ ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][timeval[6]],
+ timeval[2],
+ ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][timeval[1]-1],
+ timeval[0], timeval[3], timeval[4], timeval[5])
+
+#
+# calendar.timegm() is new to Python 2.x and
+# calendar.leapdays() was wrong in Python 1.5.2
+#
+try:
+ timegm = calendar.timegm
+except AttributeError:
+ def leapdays(year1, year2):
+ """Return number of leap years in range [year1, year2).
+ Assume year1 <= year2."""
+ year1 = year1 - 1
+ year2 = year2 - 1
+ return (year2/4 - year1/4) - (year2/100 -
+ year1/100) + (year2/400 - year1/400)
+
+ EPOCH = 1970
+ def timegm(tuple):
+ """Unrelated but handy function to calculate Unix timestamp from GMT."""
+ year, month, day, hour, minute, second = tuple[:6]
+ # assert year >= EPOCH
+ # assert 1 <= month <= 12
+ days = 365*(year-EPOCH) + leapdays(EPOCH, year)
+ for i in range(1, month):
+ days = days + calendar.mdays[i]
+ if month > 2 and calendar.isleap(year):
+ days = days + 1
+ days = days + day - 1
+ hours = days*24 + hour
+ minutes = hours*60 + minute
+ seconds = minutes*60 + second
+ return seconds
+
+#
+# tempfile.mkdtemp() is new to Python 2.3
+#
+try:
+ mkdtemp = tempfile.mkdtemp
+except AttributeError:
+ def mkdtemp():
+ for i in range(10):
+ dir = tempfile.mktemp()
+ try:
+ os.mkdir(dir, 0700)
+ return dir
+ except OSError, e:
+ if e.errno == errno.EEXIST:
+ continue # try again
+ raise
+
+ raise IOError, (errno.EEXIST, "No usable temporary directory name found")
+
+#
+# the following stuff is *ONLY* needed for standalone.py.
+# For that reason I've encapsulated it into a function.
+#
+
+def for_standalone():
+ import SocketServer
+ if not hasattr(SocketServer.TCPServer, "close_request"):
+ #
+ # method close_request() was missing until Python 2.1
+ #
+ class TCPServer(SocketServer.TCPServer):
+ def process_request(self, request, client_address):
+ """Call finish_request.
+
+ Overridden by ForkingMixIn and ThreadingMixIn.
+
+ """
+ self.finish_request(request, client_address)
+ self.close_request(request)
+
+ def close_request(self, request):
+ """Called to clean up an individual request."""
+ request.close()
+
+ SocketServer.TCPServer = TCPServer
--- /dev/null
+#! /usr/bin/env python
+# Backported to Python 1.5.2 for the ViewCVS project by pf@artcom-gmbh.de
+# 24-Dec-2001, original version "stolen" from Python-2.1.1
+"""
+Module difflib -- helpers for computing deltas between objects.
+
+Function get_close_matches(word, possibilities, n=3, cutoff=0.6):
+
+ Use SequenceMatcher to return list of the best "good enough" matches.
+
+ word is a sequence for which close matches are desired (typically a
+ string).
+
+ possibilities is a list of sequences against which to match word
+ (typically a list of strings).
+
+ Optional arg n (default 3) is the maximum number of close matches to
+ return. n must be > 0.
+
+ Optional arg cutoff (default 0.6) is a float in [0, 1]. Possibilities
+ that don't score at least that similar to word are ignored.
+
+ The best (no more than n) matches among the possibilities are returned
+ in a list, sorted by similarity score, most similar first.
+
+ >>> get_close_matches("appel", ["ape", "apple", "peach", "puppy"])
+ ['apple', 'ape']
+ >>> import keyword
+ >>> get_close_matches("wheel", keyword.kwlist)
+ ['while']
+ >>> get_close_matches("apple", keyword.kwlist)
+ []
+ >>> get_close_matches("accept", keyword.kwlist)
+ ['except']
+
+Class SequenceMatcher
+
+SequenceMatcher is a flexible class for comparing pairs of sequences of any
+type, so long as the sequence elements are hashable. The basic algorithm
+predates, and is a little fancier than, an algorithm published in the late
+1980's by Ratcliff and Obershelp under the hyperbolic name "gestalt pattern
+matching". The basic idea is to find the longest contiguous matching
+subsequence that contains no "junk" elements (R-O doesn't address junk).
+The same idea is then applied recursively to the pieces of the sequences to
+the left and to the right of the matching subsequence. This does not yield
+minimal edit sequences, but does tend to yield matches that "look right"
+to people.
+
+Example, comparing two strings, and considering blanks to be "junk":
+
+>>> s = SequenceMatcher(lambda x: x == " ",
+... "private Thread currentThread;",
+... "private volatile Thread currentThread;")
+>>>
+
+.ratio() returns a float in [0, 1], measuring the "similarity" of the
+sequences. As a rule of thumb, a .ratio() value over 0.6 means the
+sequences are close matches:
+
+>>> print round(s.ratio(), 3)
+0.866
+>>>
+
+If you're only interested in where the sequences match,
+.get_matching_blocks() is handy:
+
+>>> for block in s.get_matching_blocks():
+... print "a[%d] and b[%d] match for %d elements" % block
+a[0] and b[0] match for 8 elements
+a[8] and b[17] match for 6 elements
+a[14] and b[23] match for 15 elements
+a[29] and b[38] match for 0 elements
+
+Note that the last tuple returned by .get_matching_blocks() is always a
+dummy, (len(a), len(b), 0), and this is the only case in which the last
+tuple element (number of elements matched) is 0.
+
+If you want to know how to change the first sequence into the second, use
+.get_opcodes():
+
+>>> for opcode in s.get_opcodes():
+... print "%6s a[%d:%d] b[%d:%d]" % opcode
+ equal a[0:8] b[0:8]
+insert a[8:8] b[8:17]
+ equal a[8:14] b[17:23]
+ equal a[14:29] b[23:38]
+
+See Tools/scripts/ndiff.py for a fancy human-friendly file differencer,
+which uses SequenceMatcher both to view files as sequences of lines, and
+lines as sequences of characters.
+
+See also function get_close_matches() in this module, which shows how
+simple code building on SequenceMatcher can be used to do useful work.
+
+Timing: Basic R-O is cubic time worst case and quadratic time expected
+case. SequenceMatcher is quadratic time for the worst case and has
+expected-case behavior dependent in a complicated way on how many
+elements the sequences have in common; best case time is linear.
+
+SequenceMatcher methods:
+
+__init__(isjunk=None, a='', b='')
+ Construct a SequenceMatcher.
+
+ Optional arg isjunk is None (the default), or a one-argument function
+ that takes a sequence element and returns true iff the element is junk.
+ None is equivalent to passing "lambda x: 0", i.e. no elements are
+ considered to be junk. For example, pass
+ lambda x: x in " \\t"
+ if you're comparing lines as sequences of characters, and don't want to
+ synch up on blanks or hard tabs.
+
+ Optional arg a is the first of two sequences to be compared. By
+ default, an empty string. The elements of a must be hashable.
+
+ Optional arg b is the second of two sequences to be compared. By
+ default, an empty string. The elements of b must be hashable.
+
+set_seqs(a, b)
+ Set the two sequences to be compared.
+
+ >>> s = SequenceMatcher()
+ >>> s.set_seqs("abcd", "bcde")
+ >>> s.ratio()
+ 0.75
+
+set_seq1(a)
+ Set the first sequence to be compared.
+
+ The second sequence to be compared is not changed.
+
+ >>> s = SequenceMatcher(None, "abcd", "bcde")
+ >>> s.ratio()
+ 0.75
+ >>> s.set_seq1("bcde")
+ >>> s.ratio()
+ 1.0
+ >>>
+
+ SequenceMatcher computes and caches detailed information about the
+ second sequence, so if you want to compare one sequence S against many
+ sequences, use .set_seq2(S) once and call .set_seq1(x) repeatedly for
+ each of the other sequences.
+
+ See also set_seqs() and set_seq2().
+
+set_seq2(b)
+ Set the second sequence to be compared.
+
+ The first sequence to be compared is not changed.
+
+ >>> s = SequenceMatcher(None, "abcd", "bcde")
+ >>> s.ratio()
+ 0.75
+ >>> s.set_seq2("abcd")
+ >>> s.ratio()
+ 1.0
+ >>>
+
+ SequenceMatcher computes and caches detailed information about the
+ second sequence, so if you want to compare one sequence S against many
+ sequences, use .set_seq2(S) once and call .set_seq1(x) repeatedly for
+ each of the other sequences.
+
+ See also set_seqs() and set_seq1().
+
+find_longest_match(alo, ahi, blo, bhi)
+ Find longest matching block in a[alo:ahi] and b[blo:bhi].
+
+ If isjunk is not defined:
+
+ Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where
+ alo <= i <= i+k <= ahi
+ blo <= j <= j+k <= bhi
+ and for all (i',j',k') meeting those conditions,
+ k >= k'
+ i <= i'
+ and if i == i', j <= j'
+
+ In other words, of all maximal matching blocks, return one that starts
+ earliest in a, and of all those maximal matching blocks that start
+ earliest in a, return the one that starts earliest in b.
+
+ >>> s = SequenceMatcher(None, " abcd", "abcd abcd")
+ >>> s.find_longest_match(0, 5, 0, 9)
+ (0, 4, 5)
+
+ If isjunk is defined, first the longest matching block is determined as
+ above, but with the additional restriction that no junk element appears
+ in the block. Then that block is extended as far as possible by
+ matching (only) junk elements on both sides. So the resulting block
+ never matches on junk except as identical junk happens to be adjacent
+ to an "interesting" match.
+
+ Here's the same example as before, but considering blanks to be junk.
+ That prevents " abcd" from matching the " abcd" at the tail end of the
+ second sequence directly. Instead only the "abcd" can match, and
+ matches the leftmost "abcd" in the second sequence:
+
+ >>> s = SequenceMatcher(lambda x: x==" ", " abcd", "abcd abcd")
+ >>> s.find_longest_match(0, 5, 0, 9)
+ (1, 0, 4)
+
+ If no blocks match, return (alo, blo, 0).
+
+ >>> s = SequenceMatcher(None, "ab", "c")
+ >>> s.find_longest_match(0, 2, 0, 1)
+ (0, 0, 0)
+
+get_matching_blocks()
+ Return list of triples describing matching subsequences.
+
+ Each triple is of the form (i, j, n), and means that
+ a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in i
+ and in j.
+
+ The last triple is a dummy, (len(a), len(b), 0), and is the only triple
+ with n==0.
+
+ >>> s = SequenceMatcher(None, "abxcd", "abcd")
+ >>> s.get_matching_blocks()
+ [(0, 0, 2), (3, 2, 2), (5, 4, 0)]
+
+get_opcodes()
+ Return list of 5-tuples describing how to turn a into b.
+
+ Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple has
+ i1 == j1 == 0, and remaining tuples have i1 == the i2 from the tuple
+ preceding it, and likewise for j1 == the previous j2.
+
+ The tags are strings, with these meanings:
+
+ 'replace': a[i1:i2] should be replaced by b[j1:j2]
+ 'delete': a[i1:i2] should be deleted.
+ Note that j1==j2 in this case.
+ 'insert': b[j1:j2] should be inserted at a[i1:i1].
+ Note that i1==i2 in this case.
+ 'equal': a[i1:i2] == b[j1:j2]
+
+ >>> a = "qabxcd"
+ >>> b = "abycdf"
+ >>> s = SequenceMatcher(None, a, b)
+ >>> for tag, i1, i2, j1, j2 in s.get_opcodes():
+ ... print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" %
+ ... (tag, i1, i2, a[i1:i2], j1, j2, b[j1:j2]))
+ delete a[0:1] (q) b[0:0] ()
+ equal a[1:3] (ab) b[0:2] (ab)
+ replace a[3:4] (x) b[2:3] (y)
+ equal a[4:6] (cd) b[3:5] (cd)
+ insert a[6:6] () b[5:6] (f)
+
+ratio()
+ Return a measure of the sequences' similarity (float in [0,1]).
+
+ Where T is the total number of elements in both sequences, and M is the
+ number of matches, this is 2,0*M / T. Note that this is 1 if the
+ sequences are identical, and 0 if they have nothing in common.
+
+ .ratio() is expensive to compute if you haven't already computed
+ .get_matching_blocks() or .get_opcodes(), in which case you may want to
+ try .quick_ratio() or .real_quick_ratio() first to get an upper bound.
+
+ >>> s = SequenceMatcher(None, "abcd", "bcde")
+ >>> s.ratio()
+ 0.75
+ >>> s.quick_ratio()
+ 0.75
+ >>> s.real_quick_ratio()
+ 1.0
+
+quick_ratio()
+ Return an upper bound on .ratio() relatively quickly.
+
+ This isn't defined beyond that it is an upper bound on .ratio(), and
+ is faster to compute.
+
+real_quick_ratio():
+ Return an upper bound on ratio() very quickly.
+
+ This isn't defined beyond that it is an upper bound on .ratio(), and
+ is faster to compute than either .ratio() or .quick_ratio().
+"""
+
+TRACE = 0
+
+class SequenceMatcher:
+ def __init__(self, isjunk=None, a='', b=''):
+ """Construct a SequenceMatcher.
+
+ Optional arg isjunk is None (the default), or a one-argument
+ function that takes a sequence element and returns true iff the
+ element is junk. None is equivalent to passing "lambda x: 0", i.e.
+ no elements are considered to be junk. For example, pass
+ lambda x: x in " \\t"
+ if you're comparing lines as sequences of characters, and don't
+ want to synch up on blanks or hard tabs.
+
+ Optional arg a is the first of two sequences to be compared. By
+ default, an empty string. The elements of a must be hashable. See
+ also .set_seqs() and .set_seq1().
+
+ Optional arg b is the second of two sequences to be compared. By
+ default, an empty string. The elements of b must be hashable. See
+ also .set_seqs() and .set_seq2().
+ """
+
+ # Members:
+ # a
+ # first sequence
+ # b
+ # second sequence; differences are computed as "what do
+ # we need to do to 'a' to change it into 'b'?"
+ # b2j
+ # for x in b, b2j[x] is a list of the indices (into b)
+ # at which x appears; junk elements do not appear
+ # b2jhas
+ # b2j.has_key
+ # fullbcount
+ # for x in b, fullbcount[x] == the number of times x
+ # appears in b; only materialized if really needed (used
+ # only for computing quick_ratio())
+ # matching_blocks
+ # a list of (i, j, k) triples, where a[i:i+k] == b[j:j+k];
+ # ascending & non-overlapping in i and in j; terminated by
+ # a dummy (len(a), len(b), 0) sentinel
+ # opcodes
+ # a list of (tag, i1, i2, j1, j2) tuples, where tag is
+ # one of
+ # 'replace' a[i1:i2] should be replaced by b[j1:j2]
+ # 'delete' a[i1:i2] should be deleted
+ # 'insert' b[j1:j2] should be inserted
+ # 'equal' a[i1:i2] == b[j1:j2]
+ # isjunk
+ # a user-supplied function taking a sequence element and
+ # returning true iff the element is "junk" -- this has
+ # subtle but helpful effects on the algorithm, which I'll
+ # get around to writing up someday <0.9 wink>.
+ # DON'T USE! Only __chain_b uses this. Use isbjunk.
+ # isbjunk
+ # for x in b, isbjunk(x) == isjunk(x) but much faster;
+ # it's really the has_key method of a hidden dict.
+ # DOES NOT WORK for x in a!
+
+ self.isjunk = isjunk
+ self.a = self.b = None
+ self.set_seqs(a, b)
+
+ def set_seqs(self, a, b):
+ """Set the two sequences to be compared.
+
+ >>> s = SequenceMatcher()
+ >>> s.set_seqs("abcd", "bcde")
+ >>> s.ratio()
+ 0.75
+ """
+
+ self.set_seq1(a)
+ self.set_seq2(b)
+
+ def set_seq1(self, a):
+ """Set the first sequence to be compared.
+
+ The second sequence to be compared is not changed.
+
+ >>> s = SequenceMatcher(None, "abcd", "bcde")
+ >>> s.ratio()
+ 0.75
+ >>> s.set_seq1("bcde")
+ >>> s.ratio()
+ 1.0
+ >>>
+
+ SequenceMatcher computes and caches detailed information about the
+ second sequence, so if you want to compare one sequence S against
+ many sequences, use .set_seq2(S) once and call .set_seq1(x)
+ repeatedly for each of the other sequences.
+
+ See also set_seqs() and set_seq2().
+ """
+
+ if a is self.a:
+ return
+ self.a = a
+ self.matching_blocks = self.opcodes = None
+
+ def set_seq2(self, b):
+ """Set the second sequence to be compared.
+
+ The first sequence to be compared is not changed.
+
+ >>> s = SequenceMatcher(None, "abcd", "bcde")
+ >>> s.ratio()
+ 0.75
+ >>> s.set_seq2("abcd")
+ >>> s.ratio()
+ 1.0
+ >>>
+
+ SequenceMatcher computes and caches detailed information about the
+ second sequence, so if you want to compare one sequence S against
+ many sequences, use .set_seq2(S) once and call .set_seq1(x)
+ repeatedly for each of the other sequences.
+
+ See also set_seqs() and set_seq1().
+ """
+
+ if b is self.b:
+ return
+ self.b = b
+ self.matching_blocks = self.opcodes = None
+ self.fullbcount = None
+ self.__chain_b()
+
+ # For each element x in b, set b2j[x] to a list of the indices in
+ # b where x appears; the indices are in increasing order; note that
+ # the number of times x appears in b is len(b2j[x]) ...
+ # when self.isjunk is defined, junk elements don't show up in this
+ # map at all, which stops the central find_longest_match method
+ # from starting any matching block at a junk element ...
+ # also creates the fast isbjunk function ...
+ # note that this is only called when b changes; so for cross-product
+ # kinds of matches, it's best to call set_seq2 once, then set_seq1
+ # repeatedly
+
+ def __chain_b(self):
+ # Because isjunk is a user-defined (not C) function, and we test
+ # for junk a LOT, it's important to minimize the number of calls.
+ # Before the tricks described here, __chain_b was by far the most
+ # time-consuming routine in the whole module! If anyone sees
+ # Jim Roskind, thank him again for profile.py -- I never would
+ # have guessed that.
+ # The first trick is to build b2j ignoring the possibility
+ # of junk. I.e., we don't call isjunk at all yet. Throwing
+ # out the junk later is much cheaper than building b2j "right"
+ # from the start.
+ b = self.b
+ self.b2j = b2j = {}
+ self.b2jhas = b2jhas = b2j.has_key
+ for i in xrange(len(b)):
+ elt = b[i]
+ if b2jhas(elt):
+ b2j[elt].append(i)
+ else:
+ b2j[elt] = [i]
+
+ # Now b2j.keys() contains elements uniquely, and especially when
+ # the sequence is a string, that's usually a good deal smaller
+ # than len(string). The difference is the number of isjunk calls
+ # saved.
+ isjunk, junkdict = self.isjunk, {}
+ if isjunk:
+ for elt in b2j.keys():
+ if isjunk(elt):
+ junkdict[elt] = 1 # value irrelevant; it's a set
+ del b2j[elt]
+
+ # Now for x in b, isjunk(x) == junkdict.has_key(x), but the
+ # latter is much faster. Note too that while there may be a
+ # lot of junk in the sequence, the number of *unique* junk
+ # elements is probably small. So the memory burden of keeping
+ # this dict alive is likely trivial compared to the size of b2j.
+ self.isbjunk = junkdict.has_key
+
+ def find_longest_match(self, alo, ahi, blo, bhi):
+ """Find longest matching block in a[alo:ahi] and b[blo:bhi].
+
+ If isjunk is not defined:
+
+ Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where
+ alo <= i <= i+k <= ahi
+ blo <= j <= j+k <= bhi
+ and for all (i',j',k') meeting those conditions,
+ k >= k'
+ i <= i'
+ and if i == i', j <= j'
+
+ In other words, of all maximal matching blocks, return one that
+ starts earliest in a, and of all those maximal matching blocks that
+ start earliest in a, return the one that starts earliest in b.
+
+ >>> s = SequenceMatcher(None, " abcd", "abcd abcd")
+ >>> s.find_longest_match(0, 5, 0, 9)
+ (0, 4, 5)
+
+ If isjunk is defined, first the longest matching block is
+ determined as above, but with the additional restriction that no
+ junk element appears in the block. Then that block is extended as
+ far as possible by matching (only) junk elements on both sides. So
+ the resulting block never matches on junk except as identical junk
+ happens to be adjacent to an "interesting" match.
+
+ Here's the same example as before, but considering blanks to be
+ junk. That prevents " abcd" from matching the " abcd" at the tail
+ end of the second sequence directly. Instead only the "abcd" can
+ match, and matches the leftmost "abcd" in the second sequence:
+
+ >>> s = SequenceMatcher(lambda x: x==" ", " abcd", "abcd abcd")
+ >>> s.find_longest_match(0, 5, 0, 9)
+ (1, 0, 4)
+
+ If no blocks match, return (alo, blo, 0).
+
+ >>> s = SequenceMatcher(None, "ab", "c")
+ >>> s.find_longest_match(0, 2, 0, 1)
+ (0, 0, 0)
+ """
+
+ # CAUTION: stripping common prefix or suffix would be incorrect.
+ # E.g.,
+ # ab
+ # acab
+ # Longest matching block is "ab", but if common prefix is
+ # stripped, it's "a" (tied with "b"). UNIX(tm) diff does so
+ # strip, so ends up claiming that ab is changed to acab by
+ # inserting "ca" in the middle. That's minimal but unintuitive:
+ # "it's obvious" that someone inserted "ac" at the front.
+ # Windiff ends up at the same place as diff, but by pairing up
+ # the unique 'b's and then matching the first two 'a's.
+
+ a, b, b2j, isbjunk = self.a, self.b, self.b2j, self.isbjunk
+ besti, bestj, bestsize = alo, blo, 0
+ # find longest junk-free match
+ # during an iteration of the loop, j2len[j] = length of longest
+ # junk-free match ending with a[i-1] and b[j]
+ j2len = {}
+ nothing = []
+ for i in xrange(alo, ahi):
+ # look at all instances of a[i] in b; note that because
+ # b2j has no junk keys, the loop is skipped if a[i] is junk
+ j2lenget = j2len.get
+ newj2len = {}
+ for j in b2j.get(a[i], nothing):
+ # a[i] matches b[j]
+ if j < blo:
+ continue
+ if j >= bhi:
+ break
+ k = newj2len[j] = j2lenget(j-1, 0) + 1
+ if k > bestsize:
+ besti, bestj, bestsize = i-k+1, j-k+1, k
+ j2len = newj2len
+
+ # Now that we have a wholly interesting match (albeit possibly
+ # empty!), we may as well suck up the matching junk on each
+ # side of it too. Can't think of a good reason not to, and it
+ # saves post-processing the (possibly considerable) expense of
+ # figuring out what to do with it. In the case of an empty
+ # interesting match, this is clearly the right thing to do,
+ # because no other kind of match is possible in the regions.
+ while besti > alo and bestj > blo and \
+ isbjunk(b[bestj-1]) and \
+ a[besti-1] == b[bestj-1]:
+ besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
+ while besti+bestsize < ahi and bestj+bestsize < bhi and \
+ isbjunk(b[bestj+bestsize]) and \
+ a[besti+bestsize] == b[bestj+bestsize]:
+ bestsize = bestsize + 1
+
+ if TRACE:
+ print "get_matching_blocks", alo, ahi, blo, bhi
+ print " returns", besti, bestj, bestsize
+ return besti, bestj, bestsize
+
+ def get_matching_blocks(self):
+ """Return list of triples describing matching subsequences.
+
+ Each triple is of the form (i, j, n), and means that
+ a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in
+ i and in j.
+
+ The last triple is a dummy, (len(a), len(b), 0), and is the only
+ triple with n==0.
+
+ >>> s = SequenceMatcher(None, "abxcd", "abcd")
+ >>> s.get_matching_blocks()
+ [(0, 0, 2), (3, 2, 2), (5, 4, 0)]
+ """
+
+ if self.matching_blocks is not None:
+ return self.matching_blocks
+ self.matching_blocks = []
+ la, lb = len(self.a), len(self.b)
+ self.__helper(0, la, 0, lb, self.matching_blocks)
+ self.matching_blocks.append( (la, lb, 0) )
+ if TRACE:
+ print '*** matching blocks', self.matching_blocks
+ return self.matching_blocks
+
+ # builds list of matching blocks covering a[alo:ahi] and
+ # b[blo:bhi], appending them in increasing order to answer
+
+ def __helper(self, alo, ahi, blo, bhi, answer):
+ i, j, k = x = self.find_longest_match(alo, ahi, blo, bhi)
+ # a[alo:i] vs b[blo:j] unknown
+ # a[i:i+k] same as b[j:j+k]
+ # a[i+k:ahi] vs b[j+k:bhi] unknown
+ if k:
+ if alo < i and blo < j:
+ self.__helper(alo, i, blo, j, answer)
+ answer.append(x)
+ if i+k < ahi and j+k < bhi:
+ self.__helper(i+k, ahi, j+k, bhi, answer)
+
+ def get_opcodes(self):
+ """Return list of 5-tuples describing how to turn a into b.
+
+ Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple
+ has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the
+ tuple preceding it, and likewise for j1 == the previous j2.
+
+ The tags are strings, with these meanings:
+
+ 'replace': a[i1:i2] should be replaced by b[j1:j2]
+ 'delete': a[i1:i2] should be deleted.
+ Note that j1==j2 in this case.
+ 'insert': b[j1:j2] should be inserted at a[i1:i1].
+ Note that i1==i2 in this case.
+ 'equal': a[i1:i2] == b[j1:j2]
+
+ >>> a = "qabxcd"
+ >>> b = "abycdf"
+ >>> s = SequenceMatcher(None, a, b)
+ >>> for tag, i1, i2, j1, j2 in s.get_opcodes():
+ ... print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" %
+ ... (tag, i1, i2, a[i1:i2], j1, j2, b[j1:j2]))
+ delete a[0:1] (q) b[0:0] ()
+ equal a[1:3] (ab) b[0:2] (ab)
+ replace a[3:4] (x) b[2:3] (y)
+ equal a[4:6] (cd) b[3:5] (cd)
+ insert a[6:6] () b[5:6] (f)
+ """
+
+ if self.opcodes is not None:
+ return self.opcodes
+ i = j = 0
+ self.opcodes = answer = []
+ for ai, bj, size in self.get_matching_blocks():
+ # invariant: we've pumped out correct diffs to change
+ # a[:i] into b[:j], and the next matching block is
+ # a[ai:ai+size] == b[bj:bj+size]. So we need to pump
+ # out a diff to change a[i:ai] into b[j:bj], pump out
+ # the matching block, and move (i,j) beyond the match
+ tag = ''
+ if i < ai and j < bj:
+ tag = 'replace'
+ elif i < ai:
+ tag = 'delete'
+ elif j < bj:
+ tag = 'insert'
+ if tag:
+ answer.append( (tag, i, ai, j, bj) )
+ i, j = ai+size, bj+size
+ # the list of matching blocks is terminated by a
+ # sentinel with size 0
+ if size:
+ answer.append( ('equal', ai, i, bj, j) )
+ return answer
+
+ def ratio(self):
+ """Return a measure of the sequences' similarity (float in [0,1]).
+
+ Where T is the total number of elements in both sequences, and
+ M is the number of matches, this is 2,0*M / T.
+ Note that this is 1 if the sequences are identical, and 0 if
+ they have nothing in common.
+
+ .ratio() is expensive to compute if you haven't already computed
+ .get_matching_blocks() or .get_opcodes(), in which case you may
+ want to try .quick_ratio() or .real_quick_ratio() first to get an
+ upper bound.
+
+ >>> s = SequenceMatcher(None, "abcd", "bcde")
+ >>> s.ratio()
+ 0.75
+ >>> s.quick_ratio()
+ 0.75
+ >>> s.real_quick_ratio()
+ 1.0
+ """
+
+ matches = reduce(lambda sum, triple: sum + triple[-1],
+ self.get_matching_blocks(), 0)
+ return 2.0 * matches / (len(self.a) + len(self.b))
+
+ def quick_ratio(self):
+ """Return an upper bound on ratio() relatively quickly.
+
+ This isn't defined beyond that it is an upper bound on .ratio(), and
+ is faster to compute.
+ """
+
+ # viewing a and b as multisets, set matches to the cardinality
+ # of their intersection; this counts the number of matches
+ # without regard to order, so is clearly an upper bound
+ if self.fullbcount is None:
+ self.fullbcount = fullbcount = {}
+ for elt in self.b:
+ fullbcount[elt] = fullbcount.get(elt, 0) + 1
+ fullbcount = self.fullbcount
+ # avail[x] is the number of times x appears in 'b' less the
+ # number of times we've seen it in 'a' so far ... kinda
+ avail = {}
+ availhas, matches = avail.has_key, 0
+ for elt in self.a:
+ if availhas(elt):
+ numb = avail[elt]
+ else:
+ numb = fullbcount.get(elt, 0)
+ avail[elt] = numb - 1
+ if numb > 0:
+ matches = matches + 1
+ return 2.0 * matches / (len(self.a) + len(self.b))
+
+ def real_quick_ratio(self):
+ """Return an upper bound on ratio() very quickly.
+
+ This isn't defined beyond that it is an upper bound on .ratio(), and
+ is faster to compute than either .ratio() or .quick_ratio().
+ """
+
+ la, lb = len(self.a), len(self.b)
+ # can't have more matches than the number of elements in the
+ # shorter sequence
+ return 2.0 * min(la, lb) / (la + lb)
+
+def get_close_matches(word, possibilities, n=3, cutoff=0.6):
+ """Use SequenceMatcher to return list of the best "good enough" matches.
+
+ word is a sequence for which close matches are desired (typically a
+ string).
+
+ possibilities is a list of sequences against which to match word
+ (typically a list of strings).
+
+ Optional arg n (default 3) is the maximum number of close matches to
+ return. n must be > 0.
+
+ Optional arg cutoff (default 0.6) is a float in [0, 1]. Possibilities
+ that don't score at least that similar to word are ignored.
+
+ The best (no more than n) matches among the possibilities are returned
+ in a list, sorted by similarity score, most similar first.
+
+ >>> get_close_matches("appel", ["ape", "apple", "peach", "puppy"])
+ ['apple', 'ape']
+ >>> import keyword
+ >>> get_close_matches("wheel", keyword.kwlist)
+ ['while']
+ >>> get_close_matches("apple", keyword.kwlist)
+ []
+ >>> get_close_matches("accept", keyword.kwlist)
+ ['except']
+ """
+
+ if not n > 0:
+ raise ValueError("n must be > 0: " + `n`)
+ if not 0.0 <= cutoff <= 1.0:
+ raise ValueError("cutoff must be in [0.0, 1.0]: " + `cutoff`)
+ result = []
+ s = SequenceMatcher()
+ s.set_seq2(word)
+ for x in possibilities:
+ s.set_seq1(x)
+ if s.real_quick_ratio() >= cutoff and \
+ s.quick_ratio() >= cutoff and \
+ s.ratio() >= cutoff:
+ result.append((s.ratio(), x))
+ # Sort by score.
+ result.sort()
+ # Retain only the best n.
+ result = result[-n:]
+ # Move best-scorer to head of list.
+ result.reverse()
+ # Strip scores.
+ # Python 2.x list comprehensions: return [x for score, x in result]
+ return_result = []
+ for score, x in result:
+ return_result.append(x)
+ return return_result
+
+def _test():
+ import doctest, difflib
+ return doctest.testmod(difflib)
+
+if __name__ == "__main__":
+ _test()
--- /dev/null
+#! /usr/bin/env python
+
+# Module ndiff version 1.6.0
+# Released to the public domain 08-Dec-2000,
+# by Tim Peters (tim.one@home.com).
+
+# Backported to Python 1.5.2 for ViewCVS by pf@artcom-gmbh.de, 24-Dec-2001
+
+# Provided as-is; use at your own risk; no warranty; no promises; enjoy!
+
+"""ndiff [-q] file1 file2
+ or
+ndiff (-r1 | -r2) < ndiff_output > file1_or_file2
+
+Print a human-friendly file difference report to stdout. Both inter-
+and intra-line differences are noted. In the second form, recreate file1
+(-r1) or file2 (-r2) on stdout, from an ndiff report on stdin.
+
+In the first form, if -q ("quiet") is not specified, the first two lines
+of output are
+
+-: file1
++: file2
+
+Each remaining line begins with a two-letter code:
+
+ "- " line unique to file1
+ "+ " line unique to file2
+ " " line common to both files
+ "? " line not present in either input file
+
+Lines beginning with "? " attempt to guide the eye to intraline
+differences, and were not present in either input file. These lines can be
+confusing if the source files contain tab characters.
+
+The first file can be recovered by retaining only lines that begin with
+" " or "- ", and deleting those 2-character prefixes; use ndiff with -r1.
+
+The second file can be recovered similarly, but by retaining only " " and
+"+ " lines; use ndiff with -r2; or, on Unix, the second file can be
+recovered by piping the output through
+
+ sed -n '/^[+ ] /s/^..//p'
+
+See module comments for details and programmatic interface.
+"""
+
+__version__ = 1, 6, 1
+
+# SequenceMatcher tries to compute a "human-friendly diff" between
+# two sequences (chiefly picturing a file as a sequence of lines,
+# and a line as a sequence of characters, here). Unlike e.g. UNIX(tm)
+# diff, the fundamental notion is the longest *contiguous* & junk-free
+# matching subsequence. That's what catches peoples' eyes. The
+# Windows(tm) windiff has another interesting notion, pairing up elements
+# that appear uniquely in each sequence. That, and the method here,
+# appear to yield more intuitive difference reports than does diff. This
+# method appears to be the least vulnerable to synching up on blocks
+# of "junk lines", though (like blank lines in ordinary text files,
+# or maybe "<P>" lines in HTML files). That may be because this is
+# the only method of the 3 that has a *concept* of "junk" <wink>.
+#
+# Note that ndiff makes no claim to produce a *minimal* diff. To the
+# contrary, minimal diffs are often counter-intuitive, because they
+# synch up anywhere possible, sometimes accidental matches 100 pages
+# apart. Restricting synch points to contiguous matches preserves some
+# notion of locality, at the occasional cost of producing a longer diff.
+#
+# With respect to junk, an earlier version of ndiff simply refused to
+# *start* a match with a junk element. The result was cases like this:
+# before: private Thread currentThread;
+# after: private volatile Thread currentThread;
+# If you consider whitespace to be junk, the longest contiguous match
+# not starting with junk is "e Thread currentThread". So ndiff reported
+# that "e volatil" was inserted between the 't' and the 'e' in "private".
+# While an accurate view, to people that's absurd. The current version
+# looks for matching blocks that are entirely junk-free, then extends the
+# longest one of those as far as possible but only with matching junk.
+# So now "currentThread" is matched, then extended to suck up the
+# preceding blank; then "private" is matched, and extended to suck up the
+# following blank; then "Thread" is matched; and finally ndiff reports
+# that "volatile " was inserted before "Thread". The only quibble
+# remaining is that perhaps it was really the case that " volatile"
+# was inserted after "private". I can live with that <wink>.
+#
+# NOTE on junk: the module-level names
+# IS_LINE_JUNK
+# IS_CHARACTER_JUNK
+# can be set to any functions you like. The first one should accept
+# a single string argument, and return true iff the string is junk.
+# The default is whether the regexp r"\s*#?\s*$" matches (i.e., a
+# line without visible characters, except for at most one splat).
+# The second should accept a string of length 1 etc. The default is
+# whether the character is a blank or tab (note: bad idea to include
+# newline in this!).
+#
+# After setting those, you can call fcompare(f1name, f2name) with the
+# names of the files you want to compare. The difference report
+# is sent to stdout. Or you can call main(args), passing what would
+# have been in sys.argv[1:] had the cmd-line form been used.
+
+from compat_difflib import SequenceMatcher
+
+TRACE = 0
+
+# define what "junk" means
+import re
+
+def IS_LINE_JUNK(line, pat=re.compile(r"\s*#?\s*$").match):
+ return pat(line) is not None
+
+def IS_CHARACTER_JUNK(ch, ws=" \t"):
+ return ch in ws
+
+del re
+
+# meant for dumping lines
+def dump(tag, x, lo, hi):
+ for i in xrange(lo, hi):
+ print tag, x[i],
+
+def plain_replace(a, alo, ahi, b, blo, bhi):
+ assert alo < ahi and blo < bhi
+ # dump the shorter block first -- reduces the burden on short-term
+ # memory if the blocks are of very different sizes
+ if bhi - blo < ahi - alo:
+ dump('+', b, blo, bhi)
+ dump('-', a, alo, ahi)
+ else:
+ dump('-', a, alo, ahi)
+ dump('+', b, blo, bhi)
+
+# When replacing one block of lines with another, this guy searches
+# the blocks for *similar* lines; the best-matching pair (if any) is
+# used as a synch point, and intraline difference marking is done on
+# the similar pair. Lots of work, but often worth it.
+
+def fancy_replace(a, alo, ahi, b, blo, bhi):
+ if TRACE:
+ print '*** fancy_replace', alo, ahi, blo, bhi
+ dump('>', a, alo, ahi)
+ dump('<', b, blo, bhi)
+
+ # don't synch up unless the lines have a similarity score of at
+ # least cutoff; best_ratio tracks the best score seen so far
+ best_ratio, cutoff = 0.74, 0.75
+ cruncher = SequenceMatcher(IS_CHARACTER_JUNK)
+ eqi, eqj = None, None # 1st indices of equal lines (if any)
+
+ # search for the pair that matches best without being identical
+ # (identical lines must be junk lines, & we don't want to synch up
+ # on junk -- unless we have to)
+ for j in xrange(blo, bhi):
+ bj = b[j]
+ cruncher.set_seq2(bj)
+ for i in xrange(alo, ahi):
+ ai = a[i]
+ if ai == bj:
+ if eqi is None:
+ eqi, eqj = i, j
+ continue
+ cruncher.set_seq1(ai)
+ # computing similarity is expensive, so use the quick
+ # upper bounds first -- have seen this speed up messy
+ # compares by a factor of 3.
+ # note that ratio() is only expensive to compute the first
+ # time it's called on a sequence pair; the expensive part
+ # of the computation is cached by cruncher
+ if cruncher.real_quick_ratio() > best_ratio and \
+ cruncher.quick_ratio() > best_ratio and \
+ cruncher.ratio() > best_ratio:
+ best_ratio, best_i, best_j = cruncher.ratio(), i, j
+ if best_ratio < cutoff:
+ # no non-identical "pretty close" pair
+ if eqi is None:
+ # no identical pair either -- treat it as a straight replace
+ plain_replace(a, alo, ahi, b, blo, bhi)
+ return
+ # no close pair, but an identical pair -- synch up on that
+ best_i, best_j, best_ratio = eqi, eqj, 1.0
+ else:
+ # there's a close pair, so forget the identical pair (if any)
+ eqi = None
+
+ # a[best_i] very similar to b[best_j]; eqi is None iff they're not
+ # identical
+ if TRACE:
+ print '*** best_ratio', best_ratio, best_i, best_j
+ dump('>', a, best_i, best_i+1)
+ dump('<', b, best_j, best_j+1)
+
+ # pump out diffs from before the synch point
+ fancy_helper(a, alo, best_i, b, blo, best_j)
+
+ # do intraline marking on the synch pair
+ aelt, belt = a[best_i], b[best_j]
+ if eqi is None:
+ # pump out a '-', '?', '+', '?' quad for the synched lines
+ atags = btags = ""
+ cruncher.set_seqs(aelt, belt)
+ for tag, ai1, ai2, bj1, bj2 in cruncher.get_opcodes():
+ la, lb = ai2 - ai1, bj2 - bj1
+ if tag == 'replace':
+ atags = atags + '^' * la
+ btags = btags + '^' * lb
+ elif tag == 'delete':
+ atags = atags + '-' * la
+ elif tag == 'insert':
+ btags = btags + '+' * lb
+ elif tag == 'equal':
+ atags = atags + ' ' * la
+ btags = btags + ' ' * lb
+ else:
+ raise ValueError, 'unknown tag ' + `tag`
+ printq(aelt, belt, atags, btags)
+ else:
+ # the synch pair is identical
+ print ' ', aelt,
+
+ # pump out diffs from after the synch point
+ fancy_helper(a, best_i+1, ahi, b, best_j+1, bhi)
+
+def fancy_helper(a, alo, ahi, b, blo, bhi):
+ if alo < ahi:
+ if blo < bhi:
+ fancy_replace(a, alo, ahi, b, blo, bhi)
+ else:
+ dump('-', a, alo, ahi)
+ elif blo < bhi:
+ dump('+', b, blo, bhi)
+
+# Crap to deal with leading tabs in "?" output. Can hurt, but will
+# probably help most of the time.
+
+def printq(aline, bline, atags, btags):
+ common = min(count_leading(aline, "\t"),
+ count_leading(bline, "\t"))
+ common = min(common, count_leading(atags[:common], " "))
+ print "-", aline,
+ if count_leading(atags, " ") < len(atags):
+ print "?", "\t" * common + atags[common:]
+ print "+", bline,
+ if count_leading(btags, " ") < len(btags):
+ print "?", "\t" * common + btags[common:]
+
+def count_leading(line, ch):
+ i, n = 0, len(line)
+ while i < n and line[i] == ch:
+ i = i+1
+ return i
+
+def fail(msg):
+ import sys
+ out = sys.stderr.write
+ out(msg + "\n\n")
+ out(__doc__)
+ return 0
+
+# open a file & return the file object; gripe and return 0 if it
+# couldn't be opened
+def fopen(fname):
+ try:
+ return open(fname, 'r')
+ except IOError, detail:
+ return fail("couldn't open " + fname + ": " + str(detail))
+
+# open two files & spray the diff to stdout; return false iff a problem
+def fcompare(f1name, f2name):
+ f1 = fopen(f1name)
+ f2 = fopen(f2name)
+ if not f1 or not f2:
+ return 0
+
+ a = f1.readlines(); f1.close()
+ b = f2.readlines(); f2.close()
+
+ cruncher = SequenceMatcher(IS_LINE_JUNK, a, b)
+ for tag, alo, ahi, blo, bhi in cruncher.get_opcodes():
+ if tag == 'replace':
+ fancy_replace(a, alo, ahi, b, blo, bhi)
+ elif tag == 'delete':
+ dump('-', a, alo, ahi)
+ elif tag == 'insert':
+ dump('+', b, blo, bhi)
+ elif tag == 'equal':
+ dump(' ', a, alo, ahi)
+ else:
+ raise ValueError, 'unknown tag ' + `tag`
+
+ return 1
+
+# crack args (sys.argv[1:] is normal) & compare;
+# return false iff a problem
+
+def main(args):
+ import getopt
+ try:
+ opts, args = getopt.getopt(args, "qr:")
+ except getopt.error, detail:
+ return fail(str(detail))
+ noisy = 1
+ qseen = rseen = 0
+ for opt, val in opts:
+ if opt == "-q":
+ qseen = 1
+ noisy = 0
+ elif opt == "-r":
+ rseen = 1
+ whichfile = val
+ if qseen and rseen:
+ return fail("can't specify both -q and -r")
+ if rseen:
+ if args:
+ return fail("no args allowed with -r option")
+ if whichfile in "12":
+ restore(whichfile)
+ return 1
+ return fail("-r value must be 1 or 2")
+ if len(args) != 2:
+ return fail("need 2 filename args")
+ f1name, f2name = args
+ if noisy:
+ print '-:', f1name
+ print '+:', f2name
+ return fcompare(f1name, f2name)
+
+def restore(which):
+ import sys
+ tag = {"1": "- ", "2": "+ "}[which]
+ prefixes = (" ", tag)
+ for line in sys.stdin.readlines():
+ if line[:2] in prefixes:
+ print line[2:],
+
+if __name__ == '__main__':
+ import sys
+ args = sys.argv[1:]
+ if "-profile" in args:
+ import profile, pstats
+ args.remove("-profile")
+ statf = "ndiff.pro"
+ profile.run("main(args)", statf)
+ stats = pstats.Stats(statf)
+ stats.strip_dirs().sort_stats('time').print_stats()
+ else:
+ main(args)
--- /dev/null
+# -*-python-*-
+#
+# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewVC
+# distribution or at http://viewvc.org/license-1.html.
+#
+# For more information, visit http://viewvc.org/
+#
+# -----------------------------------------------------------------------
+#
+# config.py: configuration utilities
+#
+# -----------------------------------------------------------------------
+
+import sys
+import os
+import string
+import ConfigParser
+import fnmatch
+
+
+#########################################################################
+#
+# CONFIGURATION
+#
+# There are three forms of configuration:
+#
+# 1) edit the viewvc.conf created by the viewvc-install(er)
+# 2) as (1), but delete all unchanged entries from viewvc.conf
+# 3) do not use viewvc.conf and just edit the defaults in this file
+#
+# Most users will want to use (1), but there are slight speed advantages
+# to the other two options. Note that viewvc.conf values are a bit easier
+# to work with since it is raw text, rather than python literal values.
+#
+#########################################################################
+
+class Config:
+ _sections = ('general', 'options', 'cvsdb', 'templates')
+ _force_multi_value = ('cvs_roots', 'forbidden',
+ 'svn_roots', 'languages', 'kv_files',
+ 'root_parents')
+
+ def __init__(self):
+ for section in self._sections:
+ setattr(self, section, _sub_config())
+
+ def load_config(self, pathname, vhost=None):
+ self.conf_path = os.path.isfile(pathname) and pathname or None
+ self.base = os.path.dirname(pathname)
+
+ parser = ConfigParser.ConfigParser()
+ parser.read(pathname)
+
+ for section in self._sections:
+ if parser.has_section(section):
+ self._process_section(parser, section, section)
+
+ if vhost and parser.has_section('vhosts'):
+ self._process_vhost(parser, vhost)
+
+ def load_kv_files(self, language):
+ kv = _sub_config()
+
+ for fname in self.general.kv_files:
+ if fname[0] == '[':
+ idx = string.index(fname, ']')
+ parts = string.split(fname[1:idx], '.')
+ fname = string.strip(fname[idx+1:])
+ else:
+ parts = [ ]
+ fname = string.replace(fname, '%lang%', language)
+
+ parser = ConfigParser.ConfigParser()
+ parser.read(os.path.join(self.base, fname))
+ for section in parser.sections():
+ for option in parser.options(section):
+ full_name = parts + [section]
+ ob = kv
+ for name in full_name:
+ try:
+ ob = getattr(ob, name)
+ except AttributeError:
+ c = _sub_config()
+ setattr(ob, name, c)
+ ob = c
+ setattr(ob, option, parser.get(section, option))
+
+ return kv
+
+ def _process_section(self, parser, section, subcfg_name):
+ sc = getattr(self, subcfg_name)
+
+ for opt in parser.options(section):
+ value = parser.get(section, opt)
+ if opt in self._force_multi_value:
+ value = map(string.strip, filter(None, string.split(value, ',')))
+ else:
+ try:
+ value = int(value)
+ except ValueError:
+ pass
+
+ if opt == 'cvs_roots' or opt == 'svn_roots':
+ value = _parse_roots(opt, value)
+
+ setattr(sc, opt, value)
+
+ def _process_vhost(self, parser, vhost):
+ canon_vhost = self._find_canon_vhost(parser, vhost)
+ if not canon_vhost:
+ # none of the vhost sections matched
+ return
+
+ cv = canon_vhost + '-'
+ lcv = len(cv)
+ for section in parser.sections():
+ if section[:lcv] == cv:
+ self._process_section(parser, section, section[lcv:])
+
+ def _find_canon_vhost(self, parser, vhost):
+ vhost = string.lower(vhost)
+ # Strip (ignore) port number:
+ vhost = string.split(vhost, ':')[0]
+
+ for canon_vhost in parser.options('vhosts'):
+ value = parser.get('vhosts', canon_vhost)
+ patterns = map(string.lower, map(string.strip,
+ filter(None, string.split(value, ','))))
+ for pat in patterns:
+ if fnmatch.fnmatchcase(vhost, pat):
+ return canon_vhost
+
+ return None
+
+ def set_defaults(self):
+ "Set some default values in the configuration."
+
+ self.general.cvs_roots = { }
+ self.general.svn_roots = { }
+ self.general.root_parents = []
+ self.general.default_root = ''
+ self.general.rcs_path = ''
+ if sys.platform == "win32":
+ self.general.cvsnt_exe_path = 'cvs'
+ else:
+ self.general.cvsnt_exe_path = None
+ self.general.use_rcsparse = 0
+ self.general.svn_path = ''
+ self.general.mime_types_file = ''
+ self.general.address = '<a href="mailto:user@insert.your.domain.here">No admin address has been configured</a>'
+ self.general.forbidden = ()
+ self.general.kv_files = [ ]
+ self.general.languages = ['en-us']
+
+ self.templates.directory = None
+ self.templates.log = None
+ self.templates.query = None
+ self.templates.diff = None
+ self.templates.graph = None
+ self.templates.annotate = None
+ self.templates.markup = None
+ self.templates.error = None
+ self.templates.query_form = None
+ self.templates.query_results = None
+ self.templates.roots = None
+
+ self.cvsdb.enabled = 0
+ self.cvsdb.host = ''
+ self.cvsdb.port = 3306
+ self.cvsdb.database_name = ''
+ self.cvsdb.user = ''
+ self.cvsdb.passwd = ''
+ self.cvsdb.readonly_user = ''
+ self.cvsdb.readonly_passwd = ''
+ self.cvsdb.row_limit = 1000
+ self.cvsdb.rss_row_limit = 100
+
+ self.options.root_as_url_component = 0
+ self.options.default_file_view = "log"
+ self.options.checkout_magic = 0
+ self.options.sort_by = 'file'
+ self.options.sort_group_dirs = 1
+ self.options.hide_attic = 1
+ self.options.log_sort = 'date'
+ self.options.diff_format = 'h'
+ self.options.hide_cvsroot = 1
+ self.options.hr_breakable = 1
+ self.options.hr_funout = 1
+ self.options.hr_ignore_white = 1
+ self.options.hr_ignore_keyword_subst = 1
+ self.options.hr_intraline = 0
+ self.options.allow_annotate = 1
+ self.options.allow_markup = 1
+ self.options.allow_compress = 1
+ self.options.template_dir = "templates"
+ self.options.docroot = None
+ self.options.show_subdir_lastmod = 0
+ self.options.show_logs = 1
+ self.options.show_log_in_markup = 1
+ self.options.cross_copies = 0
+ self.options.py2html_path = '.'
+ self.options.short_log_len = 80
+ self.options.use_enscript = 0
+ self.options.enscript_path = ''
+ self.options.use_highlight = 0
+ self.options.highlight_path = ''
+ self.options.highlight_line_numbers = 1
+ self.options.highlight_convert_tabs = 2
+ self.options.use_php = 0
+ self.options.php_exe_path = 'php'
+ self.options.allow_tar = 0
+ self.options.use_cvsgraph = 0
+ self.options.cvsgraph_path = ''
+ self.options.cvsgraph_conf = "cvsgraph.conf"
+ self.options.use_re_search = 0
+ self.options.use_pagesize = 0
+ self.options.limit_changes = 100
+ self.options.use_localtime = 0
+ self.options.http_expiration_time = 600
+ self.options.generate_etags = 1
+
+ def is_forbidden(self, module):
+ if not module:
+ return 0
+ default = 0
+ for pat in self.general.forbidden:
+ if pat[0] == '!':
+ default = 1
+ if fnmatch.fnmatchcase(module, pat[1:]):
+ return 0
+ elif fnmatch.fnmatchcase(module, pat):
+ return 1
+ return default
+
+
+def _parse_roots(config_name, config_value):
+ roots = { }
+ for root in config_value:
+ pos = string.find(root, ':')
+ if pos < 0:
+ raise MalformedRoot(config_name, root)
+ name, path = map(string.strip, (root[:pos], root[pos+1:]))
+ roots[name] = path
+ return roots
+
+
+class MalformedRoot(Exception):
+ def __init__(self, config_name, value_given):
+ Exception.__init__(self, config_name, value_given)
+ self.config_name = config_name
+ self.value_given = value_given
+ def __str__(self):
+ return "malformed configuration: '%s' uses invalid syntax: %s" \
+ % (self.config_name, self.value_given)
+
+
+class _sub_config:
+ pass
+
+if not hasattr(sys, 'hexversion'):
+ # Python 1.5 or 1.5.1. fix the syntax for ConfigParser options.
+ import regex
+ ConfigParser.option_cre = regex.compile('^\([-A-Za-z0-9._]+\)\(:\|['
+ + string.whitespace
+ + ']*=\)\(.*\)$')
--- /dev/null
+# -*-python-*-
+#
+# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewVC
+# distribution or at http://viewvc.org/license-1.html.
+#
+# For more information, visit http://viewvc.org/
+#
+# -----------------------------------------------------------------------
+
+import os
+import sys
+import string
+import time
+import fnmatch
+import re
+
+import dbi
+
+
+## error
+error = "cvsdb error"
+
+## cached (active) database connections
+gCheckinDatabase = None
+gCheckinDatabaseReadOnly = None
+
+
+## CheckinDatabase provides all interfaces needed to the SQL database
+## back-end; it needs to be subclassed, and have its "Connect" method
+## defined to actually be complete; it should run well off of any DBI 2.0
+## complient database interface
+
+class CheckinDatabase:
+ def __init__(self, host, port, user, passwd, database, row_limit):
+ self._host = host
+ self._port = port
+ self._user = user
+ self._passwd = passwd
+ self._database = database
+ self._row_limit = row_limit
+
+ ## database lookup caches
+ self._get_cache = {}
+ self._get_id_cache = {}
+ self._desc_id_cache = {}
+
+ def Connect(self):
+ self.db = dbi.connect(
+ self._host, self._port, self._user, self._passwd, self._database)
+
+ def sql_get_id(self, table, column, value, auto_set):
+ sql = "SELECT id FROM %s WHERE %s=%%s" % (table, column)
+ sql_args = (value, )
+
+ cursor = self.db.cursor()
+ cursor.execute(sql, sql_args)
+ try:
+ (id, ) = cursor.fetchone()
+ except TypeError:
+ if not auto_set:
+ return None
+ else:
+ return str(int(id))
+
+ ## insert the new identifier
+ sql = "INSERT INTO %s(%s) VALUES(%%s)" % (table, column)
+ sql_args = (value, )
+ cursor.execute(sql, sql_args)
+
+ return self.sql_get_id(table, column, value, 0)
+
+ def get_id(self, table, column, value, auto_set):
+ ## attempt to retrieve from cache
+ try:
+ return self._get_id_cache[table][column][value]
+ except KeyError:
+ pass
+
+ id = self.sql_get_id(table, column, value, auto_set)
+ if id == None:
+ return None
+
+ ## add to cache
+ try:
+ temp = self._get_id_cache[table]
+ except KeyError:
+ temp = self._get_id_cache[table] = {}
+
+ try:
+ temp2 = temp[column]
+ except KeyError:
+ temp2 = temp[column] = {}
+
+ temp2[value] = id
+ return id
+
+ def sql_get(self, table, column, id):
+ sql = "SELECT %s FROM %s WHERE id=%%s" % (column, table)
+ sql_args = (id, )
+
+ cursor = self.db.cursor()
+ cursor.execute(sql, sql_args)
+ try:
+ (value, ) = cursor.fetchone()
+ except TypeError:
+ return None
+
+ return value
+
+ def get(self, table, column, id):
+ ## attempt to retrieve from cache
+ try:
+ return self._get_cache[table][column][id]
+ except KeyError:
+ pass
+
+ value = self.sql_get(table, column, id)
+ if value == None:
+ return None
+
+ ## add to cache
+ try:
+ temp = self._get_cache[table]
+ except KeyError:
+ temp = self._get_cache[table] = {}
+
+ try:
+ temp2 = temp[column]
+ except KeyError:
+ temp2 = temp[column] = {}
+
+ temp2[id] = value
+ return value
+
+ def get_list(self, table, field_index):
+ sql = "SELECT * FROM %s" % (table)
+ cursor = self.db.cursor()
+ cursor.execute(sql)
+
+ list = []
+ while 1:
+ row = cursor.fetchone()
+ if row == None:
+ break
+ list.append(row[field_index])
+
+ return list
+
+ def GetBranchID(self, branch, auto_set = 1):
+ return self.get_id("branches", "branch", branch, auto_set)
+
+ def GetBranch(self, id):
+ return self.get("branches", "branch", id)
+
+ def GetDirectoryID(self, dir, auto_set = 1):
+ return self.get_id("dirs", "dir", dir, auto_set)
+
+ def GetDirectory(self, id):
+ return self.get("dirs", "dir", id)
+
+ def GetFileID(self, file, auto_set = 1):
+ return self.get_id("files", "file", file, auto_set)
+
+ def GetFile(self, id):
+ return self.get("files", "file", id)
+
+ def GetAuthorID(self, author, auto_set = 1):
+ return self.get_id("people", "who", author, auto_set)
+
+ def GetAuthor(self, id):
+ return self.get("people", "who", id)
+
+ def GetRepositoryID(self, repository, auto_set = 1):
+ return self.get_id("repositories", "repository", repository, auto_set)
+
+ def GetRepository(self, id):
+ return self.get("repositories", "repository", id)
+
+ def SQLGetDescriptionID(self, description, auto_set = 1):
+ ## lame string hash, blame Netscape -JMP
+ hash = len(description)
+
+ sql = "SELECT id FROM descs WHERE hash=%s AND description=%s"
+ sql_args = (hash, description)
+
+ cursor = self.db.cursor()
+ cursor.execute(sql, sql_args)
+ try:
+ (id, ) = cursor.fetchone()
+ except TypeError:
+ if not auto_set:
+ return None
+ else:
+ return str(int(id))
+
+ sql = "INSERT INTO descs (hash,description) values (%s,%s)"
+ sql_args = (hash, description)
+ cursor.execute(sql, sql_args)
+
+ return self.GetDescriptionID(description, 0)
+
+ def GetDescriptionID(self, description, auto_set = 1):
+ ## attempt to retrieve from cache
+ hash = len(description)
+ try:
+ return self._desc_id_cache[hash][description]
+ except KeyError:
+ pass
+
+ id = self.SQLGetDescriptionID(description, auto_set)
+ if id == None:
+ return None
+
+ ## add to cache
+ try:
+ temp = self._desc_id_cache[hash]
+ except KeyError:
+ temp = self._desc_id_cache[hash] = {}
+
+ temp[description] = id
+ return id
+
+ def GetDescription(self, id):
+ return self.get("descs", "description", id)
+
+ def GetRepositoryList(self):
+ return self.get_list("repositories", 1)
+
+ def GetBranchList(self):
+ return self.get_list("branches", 1)
+
+ def GetAuthorList(self):
+ return self.get_list("people", 1)
+
+ def AddCommitList(self, commit_list):
+ for commit in commit_list:
+ self.AddCommit(commit)
+
+ def AddCommit(self, commit):
+ ci_when = dbi.DateTimeFromTicks(commit.GetTime())
+ ci_type = commit.GetTypeString()
+ who_id = self.GetAuthorID(commit.GetAuthor())
+ repository_id = self.GetRepositoryID(commit.GetRepository())
+ directory_id = self.GetDirectoryID(commit.GetDirectory())
+ file_id = self.GetFileID(commit.GetFile())
+ revision = commit.GetRevision()
+ sticky_tag = "NULL"
+ branch_id = self.GetBranchID(commit.GetBranch())
+ plus_count = commit.GetPlusCount()
+ minus_count = commit.GetMinusCount()
+ description_id = self.GetDescriptionID(commit.GetDescription())
+
+ sql = "REPLACE INTO checkins"\
+ " (type,ci_when,whoid,repositoryid,dirid,fileid,revision,"\
+ " stickytag,branchid,addedlines,removedlines,descid)"\
+ "VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"
+ sql_args = (ci_type, ci_when, who_id, repository_id,
+ directory_id, file_id, revision, sticky_tag, branch_id,
+ plus_count, minus_count, description_id)
+
+ cursor = self.db.cursor()
+ cursor.execute(sql, sql_args)
+
+ def SQLQueryListString(self, field, query_entry_list):
+ sqlList = []
+
+ for query_entry in query_entry_list:
+ data = query_entry.data
+ ## figure out the correct match type
+ if query_entry.match == "exact":
+ match = "="
+ elif query_entry.match == "like":
+ match = " LIKE "
+ elif query_entry.match == "glob":
+ match = " REGEXP "
+ # use fnmatch to translate the glob into a regexp
+ data = fnmatch.translate(data)
+ if data[0] != '^': data = '^' + data
+ elif query_entry.match == "regex":
+ match = " REGEXP "
+ elif query_entry.match == "notregex":
+ match = " NOT REGEXP "
+
+ sqlList.append("%s%s%s" % (field, match, self.db.literal(data)))
+
+ return "(%s)" % (string.join(sqlList, " OR "))
+
+ def CreateSQLQueryString(self, query):
+ tableList = [("checkins", None)]
+ condList = []
+
+ if len(query.repository_list):
+ tableList.append(("repositories",
+ "(checkins.repositoryid=repositories.id)"))
+ temp = self.SQLQueryListString("repositories.repository",
+ query.repository_list)
+ condList.append(temp)
+
+ if len(query.branch_list):
+ tableList.append(("branches", "(checkins.branchid=branches.id)"))
+ temp = self.SQLQueryListString("branches.branch",
+ query.branch_list)
+ condList.append(temp)
+
+ if len(query.directory_list):
+ tableList.append(("dirs", "(checkins.dirid=dirs.id)"))
+ temp = self.SQLQueryListString("dirs.dir", query.directory_list)
+ condList.append(temp)
+
+ if len(query.file_list):
+ tableList.append(("files", "(checkins.fileid=files.id)"))
+ temp = self.SQLQueryListString("files.file", query.file_list)
+ condList.append(temp)
+
+ if len(query.author_list):
+ tableList.append(("people", "(checkins.whoid=people.id)"))
+ temp = self.SQLQueryListString("people.who", query.author_list)
+ condList.append(temp)
+
+ if query.from_date:
+ temp = "(checkins.ci_when>=\"%s\")" % (str(query.from_date))
+ condList.append(temp)
+
+ if query.to_date:
+ temp = "(checkins.ci_when<=\"%s\")" % (str(query.to_date))
+ condList.append(temp)
+
+ if query.sort == "date":
+ order_by = "ORDER BY checkins.ci_when DESC,descid"
+ elif query.sort == "author":
+ tableList.append(("people", "(checkins.whoid=people.id)"))
+ order_by = "ORDER BY people.who,descid"
+ elif query.sort == "file":
+ tableList.append(("files", "(checkins.fileid=files.id)"))
+ order_by = "ORDER BY files.file,descid"
+
+ ## exclude duplicates from the table list, and split out join
+ ## conditions from table names. In future, the join conditions
+ ## might be handled by INNER JOIN statements instead of WHERE
+ ## clauses, but MySQL 3.22 apparently doesn't support them well.
+ tables = []
+ joinConds = []
+ for (table, cond) in tableList:
+ if table not in tables:
+ tables.append(table)
+ if cond is not None: joinConds.append(cond)
+
+ tables = string.join(tables, ",")
+ conditions = string.join(joinConds + condList, " AND ")
+ conditions = conditions and "WHERE %s" % conditions
+
+ ## limit the number of rows requested or we could really slam
+ ## a server with a large database
+ limit = ""
+ if query.limit:
+ limit = "LIMIT %s" % (str(query.limit))
+ elif self._row_limit:
+ limit = "LIMIT %s" % (str(self._row_limit))
+
+ sql = "SELECT checkins.* FROM %s %s %s %s" % (
+ tables, conditions, order_by, limit)
+
+ return sql
+
+ def RunQuery(self, query):
+ sql = self.CreateSQLQueryString(query)
+ cursor = self.db.cursor()
+ cursor.execute(sql)
+
+ while 1:
+ row = cursor.fetchone()
+ if not row:
+ break
+
+ (dbType, dbCI_When, dbAuthorID, dbRepositoryID, dbDirID,
+ dbFileID, dbRevision, dbStickyTag, dbBranchID, dbAddedLines,
+ dbRemovedLines, dbDescID) = row
+
+ commit = LazyCommit(self)
+ if dbType == 'Add':
+ commit.SetTypeAdd()
+ elif dbType == 'Remove':
+ commit.SetTypeRemove()
+ else:
+ commit.SetTypeChange()
+ commit.SetTime(dbi.TicksFromDateTime(dbCI_When))
+ commit.SetFileID(dbFileID)
+ commit.SetDirectoryID(dbDirID)
+ commit.SetRevision(dbRevision)
+ commit.SetRepositoryID(dbRepositoryID)
+ commit.SetAuthorID(dbAuthorID)
+ commit.SetBranchID(dbBranchID)
+ commit.SetPlusCount(dbAddedLines)
+ commit.SetMinusCount(dbRemovedLines)
+ commit.SetDescriptionID(dbDescID)
+
+ query.AddCommit(commit)
+
+ def CheckCommit(self, commit):
+ repository_id = self.GetRepositoryID(commit.GetRepository(), 0)
+ if repository_id == None:
+ return None
+
+ dir_id = self.GetDirectoryID(commit.GetDirectory(), 0)
+ if dir_id == None:
+ return None
+
+ file_id = self.GetFileID(commit.GetFile(), 0)
+ if file_id == None:
+ return None
+
+ sql = "SELECT * FROM checkins WHERE "\
+ " repositoryid=%s AND dirid=%s AND fileid=%s AND revision=%s"
+ sql_args = (repository_id, dir_id, file_id, commit.GetRevision())
+
+ cursor = self.db.cursor()
+ cursor.execute(sql, sql_args)
+ try:
+ (ci_type, ci_when, who_id, repository_id,
+ dir_id, file_id, revision, sticky_tag, branch_id,
+ plus_count, minus_count, description_id) = cursor.fetchone()
+ except TypeError:
+ return None
+
+ return commit
+
+## the Commit class holds data on one commit, the representation is as
+## close as possible to how it should be committed and retrieved to the
+## database engine
+class Commit:
+ ## static constants for type of commit
+ CHANGE = 0
+ ADD = 1
+ REMOVE = 2
+
+ def __init__(self):
+ self.__directory = ''
+ self.__file = ''
+ self.__repository = ''
+ self.__revision = ''
+ self.__author = ''
+ self.__branch = ''
+ self.__pluscount = ''
+ self.__minuscount = ''
+ self.__description = ''
+ self.__gmt_time = 0.0
+ self.__type = Commit.CHANGE
+
+ def SetRepository(self, repository):
+ self.__repository = repository
+
+ def GetRepository(self):
+ return self.__repository
+
+ def SetDirectory(self, dir):
+ self.__directory = dir
+
+ def GetDirectory(self):
+ return self.__directory
+
+ def SetFile(self, file):
+ self.__file = file
+
+ def GetFile(self):
+ return self.__file
+
+ def SetRevision(self, revision):
+ self.__revision = revision
+
+ def GetRevision(self):
+ return self.__revision
+
+ def SetTime(self, gmt_time):
+ self.__gmt_time = float(gmt_time)
+
+ def GetTime(self):
+ return self.__gmt_time
+
+ def SetAuthor(self, author):
+ self.__author = author
+
+ def GetAuthor(self):
+ return self.__author
+
+ def SetBranch(self, branch):
+ self.__branch = branch or ''
+
+ def GetBranch(self):
+ return self.__branch
+
+ def SetPlusCount(self, pluscount):
+ self.__pluscount = pluscount
+
+ def GetPlusCount(self):
+ return self.__pluscount
+
+ def SetMinusCount(self, minuscount):
+ self.__minuscount = minuscount
+
+ def GetMinusCount(self):
+ return self.__minuscount
+
+ def SetDescription(self, description):
+ self.__description = description
+
+ def GetDescription(self):
+ return self.__description
+
+ def SetTypeChange(self):
+ self.__type = Commit.CHANGE
+
+ def SetTypeAdd(self):
+ self.__type = Commit.ADD
+
+ def SetTypeRemove(self):
+ self.__type = Commit.REMOVE
+
+ def GetType(self):
+ return self.__type
+
+ def GetTypeString(self):
+ if self.__type == Commit.CHANGE:
+ return 'Change'
+ elif self.__type == Commit.ADD:
+ return 'Add'
+ elif self.__type == Commit.REMOVE:
+ return 'Remove'
+
+## LazyCommit overrides a few methods of Commit to only retrieve
+## it's properties as they are needed
+class LazyCommit(Commit):
+ def __init__(self, db):
+ Commit.__init__(self)
+ self.__db = db
+
+ def SetFileID(self, dbFileID):
+ self.__dbFileID = dbFileID
+
+ def GetFileID(self):
+ return self.__dbFileID
+
+ def GetFile(self):
+ return self.__db.GetFile(self.__dbFileID)
+
+ def SetDirectoryID(self, dbDirID):
+ self.__dbDirID = dbDirID
+
+ def GetDirectoryID(self):
+ return self.__dbDirID
+
+ def GetDirectory(self):
+ return self.__db.GetDirectory(self.__dbDirID)
+
+ def SetRepositoryID(self, dbRepositoryID):
+ self.__dbRepositoryID = dbRepositoryID
+
+ def GetRepositoryID(self):
+ return self.__dbRepositoryID
+
+ def GetRepository(self):
+ return self.__db.GetRepository(self.__dbRepositoryID)
+
+ def SetAuthorID(self, dbAuthorID):
+ self.__dbAuthorID = dbAuthorID
+
+ def GetAuthorID(self):
+ return self.__dbAuthorID
+
+ def GetAuthor(self):
+ return self.__db.GetAuthor(self.__dbAuthorID)
+
+ def SetBranchID(self, dbBranchID):
+ self.__dbBranchID = dbBranchID
+
+ def GetBranchID(self):
+ return self.__dbBranchID
+
+ def GetBranch(self):
+ return self.__db.GetBranch(self.__dbBranchID)
+
+ def SetDescriptionID(self, dbDescID):
+ self.__dbDescID = dbDescID
+
+ def GetDescriptionID(self):
+ return self.__dbDescID
+
+ def GetDescription(self):
+ return self.__db.GetDescription(self.__dbDescID)
+
+## QueryEntry holds data on one match-type in the SQL database
+## match is: "exact", "like", or "regex"
+class QueryEntry:
+ def __init__(self, data, match):
+ self.data = data
+ self.match = match
+
+## CheckinDatabaseQueryData is a object which contains the search parameters
+## for a query to the CheckinDatabase
+class CheckinDatabaseQuery:
+ def __init__(self):
+ ## sorting
+ self.sort = "date"
+
+ ## repository to query
+ self.repository_list = []
+ self.branch_list = []
+ self.directory_list = []
+ self.file_list = []
+ self.author_list = []
+
+ ## date range in DBI 2.0 timedate objects
+ self.from_date = None
+ self.to_date = None
+
+ ## limit on number of rows to return
+ self.limit = None
+
+ ## list of commits -- filled in by CVS query
+ self.commit_list = []
+
+ ## commit_cb provides a callback for commits as they
+ ## are added
+ self.commit_cb = None
+
+ def SetRepository(self, repository, match = "exact"):
+ self.repository_list.append(QueryEntry(repository, match))
+
+ def SetBranch(self, branch, match = "exact"):
+ self.branch_list.append(QueryEntry(branch, match))
+
+ def SetDirectory(self, directory, match = "exact"):
+ self.directory_list.append(QueryEntry(directory, match))
+
+ def SetFile(self, file, match = "exact"):
+ self.file_list.append(QueryEntry(file, match))
+
+ def SetAuthor(self, author, match = "exact"):
+ self.author_list.append(QueryEntry(author, match))
+
+ def SetSortMethod(self, sort):
+ self.sort = sort
+
+ def SetFromDateObject(self, ticks):
+ self.from_date = dbi.DateTimeFromTicks(ticks)
+
+ def SetToDateObject(self, ticks):
+ self.to_date = dbi.DateTimeFromTicks(ticks)
+
+ def SetFromDateHoursAgo(self, hours_ago):
+ ticks = time.time() - (3600 * hours_ago)
+ self.from_date = dbi.DateTimeFromTicks(ticks)
+
+ def SetFromDateDaysAgo(self, days_ago):
+ ticks = time.time() - (86400 * days_ago)
+ self.from_date = dbi.DateTimeFromTicks(ticks)
+
+ def SetToDateDaysAgo(self, days_ago):
+ ticks = time.time() - (86400 * days_ago)
+ self.to_date = dbi.DateTimeFromTicks(ticks)
+
+ def SetLimit(self, limit):
+ self.limit = limit;
+
+ def AddCommit(self, commit):
+ self.commit_list.append(commit)
+
+
+##
+## entrypoints
+##
+def CreateCommit():
+ return Commit()
+
+def CreateCheckinQuery():
+ return CheckinDatabaseQuery()
+
+def ConnectDatabaseReadOnly(cfg):
+ global gCheckinDatabaseReadOnly
+
+ if gCheckinDatabaseReadOnly:
+ return gCheckinDatabaseReadOnly
+
+ gCheckinDatabaseReadOnly = CheckinDatabase(
+ cfg.cvsdb.host,
+ cfg.cvsdb.port,
+ cfg.cvsdb.readonly_user,
+ cfg.cvsdb.readonly_passwd,
+ cfg.cvsdb.database_name,
+ cfg.cvsdb.row_limit)
+
+ gCheckinDatabaseReadOnly.Connect()
+ return gCheckinDatabaseReadOnly
+
+def ConnectDatabase(cfg):
+ global gCheckinDatabase
+
+ if gCheckinDatabase:
+ return gCheckinDatabase
+
+ gCheckinDatabase = CheckinDatabase(
+ cfg.cvsdb.host,
+ cfg.cvsdb.port,
+ cfg.cvsdb.user,
+ cfg.cvsdb.passwd,
+ cfg.cvsdb.database_name,
+ cfg.cvsdb.row_limit)
+
+ gCheckinDatabase.Connect()
+ return gCheckinDatabase
+
+def GetCommitListFromRCSFile(repository, path_parts, revision=None):
+ commit_list = []
+
+ directory = string.join(path_parts[:-1], "/")
+ file = path_parts[-1]
+
+ revs = repository.itemlog(path_parts, revision, {"cvs_pass_rev": 1})
+ for rev in revs:
+ commit = CreateCommit()
+ commit.SetRepository(repository.rootpath)
+ commit.SetDirectory(directory)
+ commit.SetFile(file)
+ commit.SetRevision(rev.string)
+ commit.SetAuthor(rev.author)
+ commit.SetDescription(rev.log)
+ commit.SetTime(rev.date)
+
+ if rev.changed:
+ # extract the plus/minus and drop the sign
+ plus, minus = string.split(rev.changed)
+ commit.SetPlusCount(plus[1:])
+ commit.SetMinusCount(minus[1:])
+
+ if rev.dead:
+ commit.SetTypeRemove()
+ else:
+ commit.SetTypeChange()
+ else:
+ commit.SetTypeAdd()
+
+ commit_list.append(commit)
+
+ # if revision is on a branch which has at least one tag
+ if len(rev.number) > 2 and rev.branches:
+ commit.SetBranch(rev.branches[0].name)
+
+ return commit_list
+
+def GetUnrecordedCommitList(repository, path_parts, db):
+ commit_list = GetCommitListFromRCSFile(repository, path_parts)
+
+ unrecorded_commit_list = []
+ for commit in commit_list:
+ result = db.CheckCommit(commit)
+ if not result:
+ unrecorded_commit_list.append(commit)
+
+ return unrecorded_commit_list
+
+_re_likechars = re.compile(r"([_%\\])")
+
+def EscapeLike(literal):
+ """Escape literal string for use in a MySQL LIKE pattern"""
+ return re.sub(_re_likechars, r"\\\1", literal)
+
+def FindRepository(db, path):
+ """Find repository path in database given path to subdirectory
+ Returns normalized repository path and relative directory path"""
+ path = os.path.normpath(path)
+ dirs = []
+ while path:
+ rep = os.path.normcase(path)
+ if db.GetRepositoryID(rep, 0) is None:
+ path, pdir = os.path.split(path)
+ if not pdir:
+ return None, None
+ dirs.append(pdir)
+ else:
+ break
+ dirs.reverse()
+ return rep, dirs
+
+def CleanRepository(path):
+ """Return normalized top-level repository path"""
+ return os.path.normcase(os.path.normpath(path))
+
--- /dev/null
+# -*-python-*-
+#
+# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewVC
+# distribution or at http://viewvc.org/license-1.html.
+#
+# For more information, visit http://viewvc.org/
+#
+# -----------------------------------------------------------------------
+
+import sys
+import time
+import types
+import re
+import compat
+import MySQLdb
+
+# set to 1 to store commit times in UTC, or 0 to use the ViewVC machine's
+# local timezone. Using UTC is recommended because it ensures that the
+# database will remain valid even if it is moved to another machine or the host
+# computer's time zone is changed. UTC also avoids the ambiguity associated
+# with daylight saving time (for example if a computer in New York recorded the
+# local time 2002/10/27 1:30 am, there would be no way to tell whether the
+# actual time was recorded before or after clocks were rolled back). Use local
+# times for compatibility with databases used by ViewCVS 0.92 and earlier
+# versions.
+utc_time = 1
+
+def DateTimeFromTicks(ticks):
+ """Return a MySQL DATETIME value from a unix timestamp"""
+
+ if utc_time:
+ t = time.gmtime(ticks)
+ else:
+ t = time.localtime(ticks)
+ return "%04d-%02d-%02d %02d:%02d:%02d" % t[:6]
+
+_re_datetime = re.compile('([0-9]{4})-([0-9][0-9])-([0-9][0-9]) '
+ '([0-9][0-9]):([0-9][0-9]):([0-9][0-9])')
+
+def TicksFromDateTime(datetime):
+ """Return a unix timestamp from a MySQL DATETIME value"""
+
+ if type(datetime) == types.StringType:
+ # datetime is a MySQL DATETIME string
+ matches = _re_datetime.match(datetime).groups()
+ t = tuple(map(int, matches)) + (0, 0, 0)
+ elif hasattr(datetime, "timetuple"):
+ # datetime is a Python >=2.3 datetime.DateTime object
+ t = datetime.timetuple()
+ else:
+ # datetime is an eGenix mx.DateTime object
+ t = datetime.tuple()
+
+ if utc_time:
+ return compat.timegm(t)
+ else:
+ return time.mktime(t[:8] + (-1,))
+
+def connect(host, port, user, passwd, db):
+ return MySQLdb.connect(host=host, port=port, user=user, passwd=passwd, db=db)
--- /dev/null
+# -*-python-*-
+#
+# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewVC
+# distribution or at http://viewvc.org/license-1.html.
+#
+# For more information, visit http://viewvc.org/
+#
+# -----------------------------------------------------------------------
+
+#
+# Note: a t_start/t_end pair consumes about 0.00005 seconds on a P3/700.
+# the lambda form (when debugging is disabled) should be even faster.
+#
+
+import sys
+
+
+SHOW_TIMES = 0
+SHOW_CHILD_PROCESSES = 0
+
+if SHOW_TIMES:
+
+ import time
+
+ _timers = { }
+ _times = { }
+
+ def t_start(which):
+ _timers[which] = time.time()
+
+ def t_end(which):
+ t = time.time() - _timers[which]
+ if _times.has_key(which):
+ _times[which] = _times[which] + t
+ else:
+ _times[which] = t
+
+ def dump():
+ for name, value in _times.items():
+ print '%s: %.6f<br />' % (name, value)
+
+else:
+
+ t_start = t_end = dump = lambda *args: None
+
+
+class ViewVCException:
+ def __init__(self, msg, status=None):
+ self.msg = msg
+ self.status = status
+
+ def __str__(self):
+ if self.status:
+ return '%s: %s' % (self.status, self.msg)
+ return "ViewVC Unrecoverable Error: %s" % self.msg
+
+
+def PrintException(server, exc_data):
+ status = exc_data['status']
+ msg = exc_data['msg']
+ tb = exc_data['stacktrace']
+
+ server.header(status=status)
+ server.write("<h3>An Exception Has Occurred</h3>\n")
+
+ s = ''
+ if msg:
+ s = '<p><pre>%s</pre></p>' % server.escape(msg)
+ if status:
+ s = s + ('<h4>HTTP Response Status</h4>\n<p><pre>\n%s</pre></p><hr />\n'
+ % status)
+ server.write(s)
+
+ server.write("<h4>Python Traceback</h4>\n<p><pre>")
+ server.write(server.escape(tb))
+ server.write("</pre></p>\n")
+
+
+def GetExceptionData():
+ # capture the exception before doing anything else
+ exc_type, exc, exc_tb = sys.exc_info()
+
+ exc_dict = {
+ 'status' : None,
+ 'msg' : None,
+ 'stacktrace' : None,
+ }
+
+ try:
+ import traceback, string
+
+ if isinstance(exc, ViewVCException):
+ exc_dict['msg'] = exc.msg
+ exc_dict['status'] = exc.status
+
+ tb = string.join(traceback.format_exception(exc_type, exc, exc_tb), '')
+ exc_dict['stacktrace'] = tb
+
+ finally:
+ # prevent circular reference. sys.exc_info documentation warns
+ # "Assigning the traceback return value to a local variable in a function
+ # that is handling an exception will cause a circular reference..."
+ # This is all based on 'exc_tb', and we're now done with it. Toss it.
+ del exc_tb
+
+ return exc_dict
+
+
+if SHOW_CHILD_PROCESSES:
+ class Process:
+ def __init__(self, command, inStream, outStream, errStream):
+ self.command = command
+ self.debugIn = inStream
+ self.debugOut = outStream
+ self.debugErr = errStream
+
+ import sapi
+ if not sapi.server is None:
+ if not sapi.server.pageGlobals.has_key('processes'):
+ sapi.server.pageGlobals['processes'] = [self]
+ else:
+ sapi.server.pageGlobals['processes'].append(self)
+
+ def DumpChildren(server):
+ import os
+
+ if not server.pageGlobals.has_key('processes'):
+ return
+
+ server.header()
+ lastOut = None
+ i = 0
+
+ for k in server.pageGlobals['processes']:
+ i = i + 1
+ server.write("<table>\n")
+ server.write("<tr><td colspan=\"2\">Child Process%i</td></tr>" % i)
+ server.write("<tr>\n <td style=\"vertical-align:top\">Command Line</td> <td><pre>")
+ server.write(server.escape(k.command))
+ server.write("</pre></td>\n</tr>\n")
+ server.write("<tr>\n <td style=\"vertical-align:top\">Standard In:</td> <td>")
+
+ if k.debugIn is lastOut and not lastOut is None:
+ server.write("<em>Output from process %i</em>" % (i - 1))
+ elif k.debugIn:
+ server.write("<pre>")
+ server.write(server.escape(k.debugIn.getvalue()))
+ server.write("</pre>")
+
+ server.write("</td>\n</tr>\n")
+
+ if k.debugOut is k.debugErr:
+ server.write("<tr>\n <td style=\"vertical-align:top\">Standard Out & Error:</td> <td><pre>")
+ if k.debugOut:
+ server.write(server.escape(k.debugOut.getvalue()))
+ server.write("</pre></td>\n</tr>\n")
+
+ else:
+ server.write("<tr>\n <td style=\"vertical-align:top\">Standard Out:</td> <td><pre>")
+ if k.debugOut:
+ server.write(server.escape(k.debugOut.getvalue()))
+ server.write("</pre></td>\n</tr>\n")
+ server.write("<tr>\n <td style=\"vertical-align:top\">Standard Error:</td> <td><pre>")
+ if k.debugErr:
+ server.write(server.escape(k.debugErr.getvalue()))
+ server.write("</pre></td>\n</tr>\n")
+
+ server.write("</table>\n")
+ server.flush()
+ lastOut = k.debugOut
+
+ server.write("<table>\n")
+ server.write("<tr><td colspan=\"2\">Environment Variables</td></tr>")
+ for k, v in os.environ.items():
+ server.write("<tr>\n <td style=\"vertical-align:top\"><pre>")
+ server.write(server.escape(k))
+ server.write("</pre></td>\n <td style=\"vertical-align:top\"><pre>")
+ server.write(server.escape(v))
+ server.write("</pre></td>\n</tr>")
+ server.write("</table>")
+
+else:
+
+ def DumpChildren(server):
+ pass
+
--- /dev/null
+#!/usr/bin/env python
+"""ezt.py -- easy templating
+
+ezt templates are simply text files in whatever format you so desire
+(such as XML, HTML, etc.) which contain directives sprinkled
+throughout. With these directives it is possible to generate the
+dynamic content from the ezt templates.
+
+These directives are enclosed in square brackets. If you are a
+C-programmer, you might be familar with the #ifdef directives of the C
+preprocessor 'cpp'. ezt provides a similar concept. Additionally EZT
+has a 'for' directive, which allows it to iterate (repeat) certain
+subsections of the template according to sequence of data items
+provided by the application.
+
+The final rendering is performed by the method generate() of the Template
+class. Building template instances can either be done using external
+EZT files (convention: use the suffix .ezt for such files):
+
+ >>> template = Template("../templates/log.ezt")
+
+or by calling the parse() method of a template instance directly with
+a EZT template string:
+
+ >>> template = Template()
+ >>> template.parse('''<html><head>
+ ... <title>[title_string]</title></head>
+ ... <body><h1>[title_string]</h1>
+ ... [for a_sequence] <p>[a_sequence]</p>
+ ... [end] <hr />
+ ... The [person] is [if-any state]in[else]out[end].
+ ... </body>
+ ... </html>
+ ... ''')
+
+The application should build a dictionary 'data' and pass it together
+with the output fileobject to the templates generate method:
+
+ >>> data = {'title_string' : "A Dummy Page",
+ ... 'a_sequence' : ['list item 1', 'list item 2', 'another element'],
+ ... 'person': "doctor",
+ ... 'state' : None }
+ >>> import sys
+ >>> template.generate(sys.stdout, data)
+ <html><head>
+ <title>A Dummy Page</title></head>
+ <body><h1>A Dummy Page</h1>
+ <p>list item 1</p>
+ <p>list item 2</p>
+ <p>another element</p>
+ <hr />
+ The doctor is out.
+ </body>
+ </html>
+
+Template syntax error reporting should be improved. Currently it is
+very sparse (template line numbers would be nice):
+
+ >>> Template().parse("[if-any where] foo [else] bar [end unexpected args]")
+ Traceback (innermost last):
+ File "<stdin>", line 1, in ?
+ File "ezt.py", line 220, in parse
+ self.program = self._parse(text)
+ File "ezt.py", line 275, in _parse
+ raise ArgCountSyntaxError(str(args[1:]))
+ ArgCountSyntaxError: ['unexpected', 'args']
+ >>> Template().parse("[if unmatched_end]foo[end]")
+ Traceback (innermost last):
+ File "<stdin>", line 1, in ?
+ File "ezt.py", line 206, in parse
+ self.program = self._parse(text)
+ File "ezt.py", line 266, in _parse
+ raise UnmatchedEndError()
+ UnmatchedEndError
+
+
+Directives
+==========
+
+ Several directives allow the use of dotted qualified names refering to objects
+ or attributes of objects contained in the data dictionary given to the
+ .generate() method.
+
+ Qualified names
+ ---------------
+
+ Qualified names have two basic forms: a variable reference, or a string
+ constant. References are a name from the data dictionary with optional
+ dotted attributes (where each intermediary is an object with attributes,
+ of course).
+
+ Examples:
+
+ [varname]
+
+ [ob.attr]
+
+ ["string"]
+
+ Simple directives
+ -----------------
+
+ [QUAL_NAME]
+
+ This directive is simply replaced by the value of the qualified name.
+ If the value is a number it's converted to a string before being
+ outputted. If it is None, nothing is outputted. If it is a python file
+ object (i.e. any object with a "read" method), it's contents are
+ outputted. If it is a callback function (any callable python object
+ is assumed to be a callback function), it is invoked and passed an EZT
+ Context object as an argument.
+
+ [QUAL_NAME QUAL_NAME ...]
+
+ If the first value is a callback function, it is invoked with an EZT
+ Context object as a first argument, and the rest of the values as
+ additional arguments.
+
+ Otherwise, the first value defines a substitution format, specifying
+ constant text and indices of the additional arguments. The arguments
+ are substituted and the result is inserted into the output stream.
+
+ Example:
+ ["abc %0 def %1 ghi %0" foo bar.baz]
+
+ Note that the first value can be any type of qualified name -- a string
+ constant or a variable reference. Use %% to substitute a percent sign.
+ Argument indices are 0-based.
+
+ [include "filename"] or [include QUAL_NAME]
+
+ This directive is replaced by content of the named include file. Note
+ that a string constant is more efficient -- the target file is compiled
+ inline. In the variable form, the target file is compiled and executed
+ at runtime.
+
+ Block directives
+ ----------------
+
+ [for QUAL_NAME] ... [end]
+
+ The text within the [for ...] directive and the corresponding [end]
+ is repeated for each element in the sequence referred to by the
+ qualified name in the for directive. Within the for block this
+ identifiers now refers to the actual item indexed by this loop
+ iteration.
+
+ [if-any QUAL_NAME [QUAL_NAME2 ...]] ... [else] ... [end]
+
+ Test if any QUAL_NAME value is not None or an empty string or list.
+ The [else] clause is optional. CAUTION: Numeric values are
+ converted to string, so if QUAL_NAME refers to a numeric value 0,
+ the then-clause is substituted!
+
+ [if-index INDEX_FROM_FOR odd] ... [else] ... [end]
+ [if-index INDEX_FROM_FOR even] ... [else] ... [end]
+ [if-index INDEX_FROM_FOR first] ... [else] ... [end]
+ [if-index INDEX_FROM_FOR last] ... [else] ... [end]
+ [if-index INDEX_FROM_FOR NUMBER] ... [else] ... [end]
+
+ These five directives work similar to [if-any], but are only useful
+ within a [for ...]-block (see above). The odd/even directives are
+ for example useful to choose different background colors for
+ adjacent rows in a table. Similar the first/last directives might
+ be used to remove certain parts (for example "Diff to previous"
+ doesn't make sense, if there is no previous).
+
+ [is QUAL_NAME STRING] ... [else] ... [end]
+ [is QUAL_NAME QUAL_NAME] ... [else] ... [end]
+
+ The [is ...] directive is similar to the other conditional
+ directives above. But it allows to compare two value references or
+ a value reference with some constant string.
+
+ [define VARIABLE] ... [end]
+
+ The [define ...] directive allows you to create and modify template
+ variables from within the template itself. Essentially, any data
+ between inside the [define ...] and its matching [end] will be
+ expanded using the other template parsing and output generation
+ rules, and then stored as a string value assigned to the variable
+ VARIABLE. The new (or changed) variable is then available for use
+ with other mechanisms such as [is ...] or [if-any ...], as long as
+ they appear later in the template.
+
+ [format STRING] ... [end]
+
+ The format directive controls how the values substituted into
+ templates are escaped before they are put into the output stream. It
+ has no effect on the literal text of the templates, only the output
+ from [QUAL_NAME ...] directives. STRING can be one of "raw" "html"
+ or "xml". The "raw" mode leaves the output unaltered. The "html" and
+ "xml" modes escape special characters using entity escapes (like
+ " and >)
+
+ [format CALLBACK]
+
+ Python applications using EZT can provide custom formatters as callback
+ variables. "[format CALLBACK][QUAL_NAME][end]" is in most cases
+ equivalent to "[CALLBACK QUAL_NAME]"
+"""
+#
+# Copyright (C) 2001-2005 Greg Stein. All Rights Reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+#
+# This software is maintained by Greg and is available at:
+# http://svn.webdav.org/repos/projects/ezt/trunk/
+#
+
+import string
+import re
+from types import StringType, IntType, FloatType, LongType, TupleType
+import os
+import cgi
+try:
+ import cStringIO
+except ImportError:
+ import StringIO
+ cStringIO = StringIO
+
+#
+# Formatting types
+#
+FORMAT_RAW = 'raw'
+FORMAT_HTML = 'html'
+FORMAT_XML = 'xml'
+
+#
+# This regular expression matches three alternatives:
+# expr: DIRECTIVE | BRACKET | COMMENT
+# DIRECTIVE: '[' ITEM (whitespace ITEM)* ']
+# ITEM: STRING | NAME
+# STRING: '"' (not-slash-or-dquote | '\' anychar)* '"'
+# NAME: (alphanum | '_' | '-' | '.')+
+# BRACKET: '[[]'
+# COMMENT: '[#' not-rbracket* ']'
+#
+# When used with the split() method, the return value will be composed of
+# non-matching text and the two paren groups (DIRECTIVE and BRACKET). Since
+# the COMMENT matches are not placed into a group, they are considered a
+# "splitting" value and simply dropped.
+#
+_item = r'(?:"(?:[^\\"]|\\.)*"|[-\w.]+)'
+_re_parse = re.compile(r'\[(%s(?: +%s)*)\]|(\[\[\])|\[#[^\]]*\]' % (_item, _item))
+
+_re_args = re.compile(r'"(?:[^\\"]|\\.)*"|[-\w.]+')
+
+# block commands and their argument counts
+_block_cmd_specs = { 'if-index':2, 'for':1, 'is':2, 'define':1, 'format':1 }
+_block_cmds = _block_cmd_specs.keys()
+
+# two regular expresssions for compressing whitespace. the first is used to
+# compress any whitespace including a newline into a single newline. the
+# second regex is used to compress runs of whitespace into a single space.
+_re_newline = re.compile('[ \t\r\f\v]*\n\\s*')
+_re_whitespace = re.compile(r'\s\s+')
+
+# this regex is used to substitute arguments into a value. we split the value,
+# replace the relevant pieces, and then put it all back together. splitting
+# will produce a list of: TEXT ( splitter TEXT )*. splitter will be '%' or
+# an integer.
+_re_subst = re.compile('%(%|[0-9]+)')
+
+class Template:
+
+ def __init__(self, fname=None, compress_whitespace=1,
+ base_format=FORMAT_RAW):
+ self.compress_whitespace = compress_whitespace
+ if fname:
+ self.parse_file(fname, base_format)
+
+ def parse_file(self, fname, base_format=FORMAT_RAW):
+ "fname -> a string object with pathname of file containg an EZT template."
+
+ self.parse(_FileReader(fname), base_format)
+
+ def parse(self, text_or_reader, base_format=FORMAT_RAW):
+ """Parse the template specified by text_or_reader.
+
+ The argument should be a string containing the template, or it should
+ specify a subclass of ezt.Reader which can read templates. The base
+ format for printing values is given by base_format.
+ """
+ if not isinstance(text_or_reader, Reader):
+ # assume the argument is a plain text string
+ text_or_reader = _TextReader(text_or_reader)
+
+ self.program = self._parse(text_or_reader, base_format=base_format)
+
+ def generate(self, fp, data):
+ if hasattr(data, '__getitem__') or callable(getattr(data, 'keys', None)):
+ # a dictionary-like object was passed. convert it to an
+ # attribute-based object.
+ class _data_ob:
+ def __init__(self, d):
+ vars(self).update(d)
+ data = _data_ob(data)
+
+ ctx = Context(fp)
+ ctx.data = data
+ ctx.for_iterators = { }
+ ctx.defines = { }
+ self._execute(self.program, ctx)
+
+ def _parse(self, reader, for_names=None, file_args=(), base_format=None):
+ """text -> string object containing the template.
+
+ This is a private helper function doing the real work for method parse.
+ It returns the parsed template as a 'program'. This program is a sequence
+ made out of strings or (function, argument) 2-tuples.
+
+ Note: comment directives [# ...] are automatically dropped by _re_parse.
+ """
+
+ # parse the template program into: (TEXT DIRECTIVE BRACKET)* TEXT
+ parts = _re_parse.split(reader.text)
+
+ program = [ ]
+ stack = [ ]
+ if not for_names:
+ for_names = [ ]
+
+ if base_format:
+ program.append((self._cmd_format, _printers[base_format]))
+
+ for i in range(len(parts)):
+ piece = parts[i]
+ which = i % 3 # discriminate between: TEXT DIRECTIVE BRACKET
+ if which == 0:
+ # TEXT. append if non-empty.
+ if piece:
+ if self.compress_whitespace:
+ piece = _re_whitespace.sub(' ', _re_newline.sub('\n', piece))
+ program.append(piece)
+ elif which == 2:
+ # BRACKET directive. append '[' if present.
+ if piece:
+ program.append('[')
+ elif piece:
+ # DIRECTIVE is present.
+ args = _re_args.findall(piece)
+ cmd = args[0]
+ if cmd == 'else':
+ if len(args) > 1:
+ raise ArgCountSyntaxError(str(args[1:]))
+ ### check: don't allow for 'for' cmd
+ idx = stack[-1][1]
+ true_section = program[idx:]
+ del program[idx:]
+ stack[-1][3] = true_section
+ elif cmd == 'end':
+ if len(args) > 1:
+ raise ArgCountSyntaxError(str(args[1:]))
+ # note: true-section may be None
+ try:
+ cmd, idx, args, true_section = stack.pop()
+ except IndexError:
+ raise UnmatchedEndError()
+ else_section = program[idx:]
+ if cmd == 'format':
+ program.append((self._cmd_end_format, None))
+ else:
+ func = getattr(self, '_cmd_' + re.sub('-', '_', cmd))
+ program[idx:] = [ (func, (args, true_section, else_section)) ]
+ if cmd == 'for':
+ for_names.pop()
+ elif cmd in _block_cmds:
+ if len(args) > _block_cmd_specs[cmd] + 1:
+ raise ArgCountSyntaxError(str(args[1:]))
+ ### this assumes arg1 is always a ref unless cmd is 'define'
+ if cmd != 'define':
+ args[1] = _prepare_ref(args[1], for_names, file_args)
+
+ # handle arg2 for the 'is' command
+ if cmd == 'is':
+ args[2] = _prepare_ref(args[2], for_names, file_args)
+ elif cmd == 'for':
+ for_names.append(args[1][0]) # append the refname
+ elif cmd == 'format':
+ if args[1][0]:
+ # argument is a variable reference
+ printer = args[1]
+ else:
+ # argument is a string constant referring to built-in printer
+ printer = _printers.get(args[1][1])
+ if not printer:
+ raise UnknownFormatConstantError(str(args[1:]))
+ program.append((self._cmd_format, printer))
+
+ # remember the cmd, current pos, args, and a section placeholder
+ stack.append([cmd, len(program), args[1:], None])
+ elif cmd == 'include':
+ if args[1][0] == '"':
+ include_filename = args[1][1:-1]
+ f_args = [ ]
+ for arg in args[2:]:
+ f_args.append(_prepare_ref(arg, for_names, file_args))
+ program.extend(self._parse(reader.read_other(include_filename),
+ for_names, f_args))
+ else:
+ if len(args) != 2:
+ raise ArgCountSyntaxError(str(args))
+ program.append((self._cmd_include,
+ (_prepare_ref(args[1], for_names, file_args),
+ reader)))
+ elif cmd == 'if-any':
+ f_args = [ ]
+ for arg in args[1:]:
+ f_args.append(_prepare_ref(arg, for_names, file_args))
+ stack.append(['if-any', len(program), f_args, None])
+ else:
+ # implied PRINT command
+ f_args = [ ]
+ for arg in args:
+ f_args.append(_prepare_ref(arg, for_names, file_args))
+ program.append((self._cmd_print, f_args))
+
+ if stack:
+ ### would be nice to say which blocks...
+ raise UnclosedBlocksError()
+ return program
+
+ def _execute(self, program, ctx):
+ """This private helper function takes a 'program' sequence as created
+ by the method '_parse' and executes it step by step. strings are written
+ to the file object 'fp' and functions are called.
+ """
+ for step in program:
+ if isinstance(step, StringType):
+ ctx.fp.write(step)
+ else:
+ step[0](step[1], ctx)
+
+ def _cmd_print(self, valrefs, ctx):
+ value = _get_value(valrefs[0], ctx)
+ args = map(lambda valref, ctx=ctx: _get_value(valref, ctx), valrefs[1:])
+ _write_value(value, args, ctx)
+
+ def _cmd_format(self, printer, ctx):
+ if type(printer) is TupleType:
+ printer = _get_value(printer, ctx)
+ ctx.printers.append(printer)
+
+ def _cmd_end_format(self, valref, ctx):
+ ctx.printers.pop()
+
+ def _cmd_include(self, (valref, reader), ctx):
+ fname = _get_value(valref, ctx)
+ ### note: we don't have the set of for_names to pass into this parse.
+ ### I don't think there is anything to do but document it.
+ self._execute(self._parse(reader.read_other(fname)), ctx)
+
+ def _cmd_if_any(self, args, ctx):
+ "If any value is a non-empty string or non-empty list, then T else F."
+ (valrefs, t_section, f_section) = args
+ value = 0
+ for valref in valrefs:
+ if _get_value(valref, ctx):
+ value = 1
+ break
+ self._do_if(value, t_section, f_section, ctx)
+
+ def _cmd_if_index(self, args, ctx):
+ ((valref, value), t_section, f_section) = args
+ iterator = ctx.for_iterators[valref[0]]
+ if value == 'even':
+ value = iterator.index % 2 == 0
+ elif value == 'odd':
+ value = iterator.index % 2 == 1
+ elif value == 'first':
+ value = iterator.index == 0
+ elif value == 'last':
+ value = iterator.is_last()
+ else:
+ value = iterator.index == int(value)
+ self._do_if(value, t_section, f_section, ctx)
+
+ def _cmd_is(self, args, ctx):
+ ((left_ref, right_ref), t_section, f_section) = args
+ value = _get_value(right_ref, ctx)
+ value = string.lower(_get_value(left_ref, ctx)) == string.lower(value)
+ self._do_if(value, t_section, f_section, ctx)
+
+ def _do_if(self, value, t_section, f_section, ctx):
+ if t_section is None:
+ t_section = f_section
+ f_section = None
+ if value:
+ section = t_section
+ else:
+ section = f_section
+ if section is not None:
+ self._execute(section, ctx)
+
+ def _cmd_for(self, args, ctx):
+ ((valref,), unused, section) = args
+ list = _get_value(valref, ctx)
+ if isinstance(list, StringType):
+ raise NeedSequenceError()
+ refname = valref[0]
+ ctx.for_iterators[refname] = iterator = _iter(list)
+ for unused in iterator:
+ self._execute(section, ctx)
+ del ctx.for_iterators[refname]
+
+ def _cmd_define(self, args, ctx):
+ ((name,), unused, section) = args
+ origfp = ctx.fp
+ ctx.fp = cStringIO.StringIO()
+ if section is not None:
+ self._execute(section, ctx)
+ ctx.defines[name] = ctx.fp.getvalue()
+ ctx.fp = origfp
+
+def boolean(value):
+ "Return a value suitable for [if-any bool_var] usage in a template."
+ if value:
+ return 'yes'
+ return None
+
+
+def _prepare_ref(refname, for_names, file_args):
+ """refname -> a string containing a dotted identifier. example:"foo.bar.bang"
+ for_names -> a list of active for sequences.
+
+ Returns a `value reference', a 3-tuple made out of (refname, start, rest),
+ for fast access later.
+ """
+ # is the reference a string constant?
+ if refname[0] == '"':
+ return None, refname[1:-1], None
+
+ parts = string.split(refname, '.')
+ start = parts[0]
+ rest = parts[1:]
+
+ # if this is an include-argument, then just return the prepared ref
+ if start[:3] == 'arg':
+ try:
+ idx = int(start[3:])
+ except ValueError:
+ pass
+ else:
+ if idx < len(file_args):
+ orig_refname, start, more_rest = file_args[idx]
+ if more_rest is None:
+ # the include-argument was a string constant
+ return None, start, None
+
+ # prepend the argument's "rest" for our further processing
+ rest[:0] = more_rest
+
+ # rewrite the refname to ensure that any potential 'for' processing
+ # has the correct name
+ ### this can make it hard for debugging include files since we lose
+ ### the 'argNNN' names
+ if not rest:
+ return start, start, [ ]
+ refname = start + '.' + string.join(rest, '.')
+
+ if for_names:
+ # From last to first part, check if this reference is part of a for loop
+ for i in range(len(parts), 0, -1):
+ name = string.join(parts[:i], '.')
+ if name in for_names:
+ return refname, name, parts[i:]
+
+ return refname, start, rest
+
+def _get_value((refname, start, rest), ctx):
+ """(refname, start, rest) -> a prepared `value reference' (see above).
+ ctx -> an execution context instance.
+
+ Does a name space lookup within the template name space. Active
+ for blocks take precedence over data dictionary members with the
+ same name.
+ """
+ if rest is None:
+ # it was a string constant
+ return start
+
+ # get the starting object
+ if ctx.for_iterators.has_key(start):
+ ob = ctx.for_iterators[start].last_item
+ elif ctx.defines.has_key(start):
+ ob = ctx.defines[start]
+ elif hasattr(ctx.data, start):
+ ob = getattr(ctx.data, start)
+ else:
+ raise UnknownReference(refname)
+
+ # walk the rest of the dotted reference
+ for attr in rest:
+ try:
+ ob = getattr(ob, attr)
+ except AttributeError:
+ raise UnknownReference(refname)
+
+ # make sure we return a string instead of some various Python types
+ if isinstance(ob, IntType) \
+ or isinstance(ob, LongType) \
+ or isinstance(ob, FloatType):
+ return str(ob)
+ if ob is None:
+ return ''
+
+ # string or a sequence
+ return ob
+
+def _write_value(value, args, ctx):
+ # value is a callback function, generates its own output
+ if callable(value):
+ apply(value, [ctx] + list(args))
+ return
+
+ # pop printer in case it recursively calls _write_value
+ printer = ctx.printers.pop()
+
+ try:
+ # if the value has a 'read' attribute, then it is a stream: copy it
+ if hasattr(value, 'read'):
+ while 1:
+ chunk = value.read(16384)
+ if not chunk:
+ break
+ printer(ctx, chunk)
+
+ # value is a substitution pattern
+ elif args:
+ parts = _re_subst.split(value)
+ for i in range(len(parts)):
+ piece = parts[i]
+ if i%2 == 1 and piece != '%':
+ idx = int(piece)
+ if idx < len(args):
+ piece = args[idx]
+ else:
+ piece = '<undef>'
+ printer(ctx, piece)
+
+ # plain old value, write to output
+ else:
+ printer(ctx, value)
+
+ finally:
+ ctx.printers.append(printer)
+
+
+class Context:
+ """A container for the execution context"""
+ def __init__(self, fp):
+ self.fp = fp
+ self.printers = []
+ def write(self, value, args=()):
+ _write_value(value, args, self)
+
+class Reader:
+ "Abstract class which allows EZT to detect Reader objects."
+
+class _FileReader(Reader):
+ """Reads templates from the filesystem."""
+ def __init__(self, fname):
+ self.text = open(fname, 'rb').read()
+ self._dir = os.path.dirname(fname)
+ def read_other(self, relative):
+ return _FileReader(os.path.join(self._dir, relative))
+
+class _TextReader(Reader):
+ """'Reads' a template from provided text."""
+ def __init__(self, text):
+ self.text = text
+ def read_other(self, relative):
+ raise BaseUnavailableError()
+
+class _Iterator:
+ """Specialized iterator for EZT that counts items and can look ahead
+
+ Implements standard iterator interface and provides an is_last() method
+ and two public members:
+
+ index - integer index of the current item
+ last_item - last item returned by next()"""
+
+ def __init__(self, sequence):
+ self._iter = iter(sequence)
+
+ def next(self):
+ if hasattr(self, '_next_item'):
+ self.last_item = self._next_item
+ del self._next_item
+ else:
+ self.last_item = self._iter.next() # may raise StopIteration
+
+ if hasattr(self, 'index'):
+ self.index = self.index + 1
+ else:
+ self.index = 0
+
+ return self.last_item
+
+ def is_last(self):
+ """Return true if the current item is the last in the sequence"""
+ # the only way we can tell if the current item is last is to call next()
+ # and store the return value so it doesn't get lost
+ if not hasattr(self, '_next_item'):
+ try:
+ self._next_item = self._iter.next()
+ except StopIteration:
+ return 1
+ return 0
+
+ def __iter__(self):
+ return self
+
+class _OldIterator:
+ """Alternate implemention of _Iterator for old Pythons without iterators
+
+ This class implements the sequence protocol, instead of the iterator
+ interface, so it's really not an iterator at all. But it can be used in
+ python "for" loops as a drop-in replacement for _Iterator. It also provides
+ the is_last() method and "last_item" and "index" members described in the
+ _Iterator docstring."""
+
+ def __init__(self, sequence):
+ self._seq = sequence
+
+ def __getitem__(self, index):
+ self.last_item = self._seq[index] # may raise IndexError
+ self.index = index
+ return self.last_item
+
+ def is_last(self):
+ return self.index + 1 >= len(self._seq)
+
+try:
+ iter
+except NameError:
+ _iter = _OldIterator
+else:
+ _iter = _Iterator
+
+class EZTException(Exception):
+ """Parent class of all EZT exceptions."""
+
+class ArgCountSyntaxError(EZTException):
+ """A bracket directive got the wrong number of arguments."""
+
+class UnknownReference(EZTException):
+ """The template references an object not contained in the data dictionary."""
+
+class NeedSequenceError(EZTException):
+ """The object dereferenced by the template is no sequence (tuple or list)."""
+
+class UnclosedBlocksError(EZTException):
+ """This error may be simply a missing [end]."""
+
+class UnmatchedEndError(EZTException):
+ """This error may be caused by a misspelled if directive."""
+
+class BaseUnavailableError(EZTException):
+ """Base location is unavailable, which disables includes."""
+
+class UnknownFormatConstantError(EZTException):
+ """The format specifier is an unknown value."""
+
+def _raw_printer(ctx, s):
+ ctx.fp.write(s)
+
+def _html_printer(ctx, s):
+ ctx.fp.write(cgi.escape(s))
+
+_printers = {
+ FORMAT_RAW : _raw_printer,
+ FORMAT_HTML : _html_printer,
+ FORMAT_XML : _html_printer,
+}
+
+# --- standard test environment ---
+def test_parse():
+ assert _re_parse.split('[a]') == ['', '[a]', None, '']
+ assert _re_parse.split('[a] [b]') == \
+ ['', '[a]', None, ' ', '[b]', None, '']
+ assert _re_parse.split('[a c] [b]') == \
+ ['', '[a c]', None, ' ', '[b]', None, '']
+ assert _re_parse.split('x [a] y [b] z') == \
+ ['x ', '[a]', None, ' y ', '[b]', None, ' z']
+ assert _re_parse.split('[a "b" c "d"]') == \
+ ['', '[a "b" c "d"]', None, '']
+ assert _re_parse.split(r'["a \"b[foo]" c.d f]') == \
+ ['', '["a \\"b[foo]" c.d f]', None, '']
+
+def _test(argv):
+ import doctest, ezt
+ verbose = "-v" in argv
+ return doctest.testmod(ezt, verbose=verbose)
+
+if __name__ == "__main__":
+ # invoke unit test for this module:
+ import sys
+ sys.exit(_test(sys.argv)[0])
--- /dev/null
+# -*-python-*-
+#
+# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewVC
+# distribution or at http://viewvc.org/license-1.html.
+#
+# For more information, visit http://viewvc.org/
+#
+# -----------------------------------------------------------------------
+#
+# idiff: display differences between files highlighting intraline changes
+#
+# -----------------------------------------------------------------------
+
+from __future__ import generators
+
+import difflib
+import sys
+import re
+import ezt
+import cgi
+
+def sidebyside(fromlines, tolines, context):
+ """Generate side by side diff"""
+
+ ### for some reason mdiff chokes on \n's in input lines
+ line_strip = lambda line: line.rstrip("\n")
+ fromlines = map(line_strip, fromlines)
+ tolines = map(line_strip, tolines)
+
+ gap = False
+ for fromdata, todata, flag in difflib._mdiff(fromlines, tolines, context):
+ if fromdata is None and todata is None and flag is None:
+ gap = True
+ else:
+ from_item = _mdiff_split(flag, fromdata)
+ to_item = _mdiff_split(flag, todata)
+ yield _item(gap=ezt.boolean(gap), columns=(from_item, to_item))
+ gap = False
+
+_re_mdiff = re.compile("\0([+-^])(.*?)\1")
+
+def _mdiff_split(flag, (line_number, text)):
+ """Break up row from mdiff output into segments"""
+ segments = []
+ pos = 0
+ while True:
+ m = _re_mdiff.search(text, pos)
+ if not m:
+ segments.append(_item(text=cgi.escape(text[pos:]), type=None))
+ break
+
+ if m.start() > pos:
+ segments.append(_item(text=cgi.escape(text[pos:m.start()]), type=None))
+
+ if m.group(1) == "+":
+ segments.append(_item(text=cgi.escape(m.group(2)), type="add"))
+ elif m.group(1) == "-":
+ segments.append(_item(text=cgi.escape(m.group(2)), type="remove"))
+ elif m.group(1) == "^":
+ segments.append(_item(text=cgi.escape(m.group(2)), type="change"))
+
+ pos = m.end()
+
+ return _item(segments=segments, line_number=line_number)
+
+def unified(fromlines, tolines, context):
+ """Generate unified diff"""
+
+ diff = difflib.Differ().compare(fromlines, tolines)
+ lastrow = None
+
+ for row in _trim_context(diff, context):
+ if row[0].startswith("? "):
+ yield _differ_split(lastrow, row[0])
+ lastrow = None
+ else:
+ if lastrow:
+ yield _differ_split(lastrow, None)
+ lastrow = row
+
+ if lastrow:
+ yield _differ_split(lastrow, None)
+
+def _trim_context(lines, context_size):
+ """Trim context lines that don't surround changes from Differ results
+
+ yields (line, leftnum, rightnum, gap) tuples"""
+
+ # circular buffer to hold context lines
+ context_buffer = [None] * (context_size or 0)
+ context_start = context_len = 0
+
+ # number of context lines left to print after encountering a change
+ context_owed = 0
+
+ # current line numbers
+ leftnum = rightnum = 0
+
+ # whether context lines have been dropped
+ gap = False
+
+ for line in lines:
+ row = save = None
+
+ if line.startswith("- "):
+ leftnum = leftnum + 1
+ row = line, leftnum, None
+ context_owed = context_size
+
+ elif line.startswith("+ "):
+ rightnum = rightnum + 1
+ row = line, None, rightnum
+ context_owed = context_size
+
+ else:
+ if line.startswith(" "):
+ leftnum = leftnum = leftnum + 1
+ rightnum = rightnum = rightnum + 1
+ if context_owed > 0:
+ context_owed = context_owed - 1
+ elif context_size is not None:
+ save = True
+
+ row = line, leftnum, rightnum
+
+ if save:
+ # don't yield row right away, store it in buffer
+ context_buffer[(context_start + context_len) % context_size] = row
+ if context_len == context_size:
+ context_start = (context_start + 1) % context_size
+ gap = True
+ else:
+ context_len = context_len + 1
+ else:
+ # yield row, but first drain stuff in buffer
+ context_len == context_size
+ while context_len:
+ yield context_buffer[context_start] + (gap,)
+ gap = False
+ context_start = (context_start + 1) % context_size
+ context_len = context_len - 1
+ yield row + (gap,)
+ gap = False
+
+_re_differ = re.compile(r"[+-^]+")
+
+def _differ_split(row, guide):
+ """Break row into segments using guide line"""
+ line, left_number, right_number, gap = row
+
+ if left_number and right_number:
+ type = ""
+ elif left_number:
+ type = "remove"
+ elif right_number:
+ type = "add"
+
+ segments = []
+ pos = 2
+
+ if guide:
+ assert guide.startswith("? ")
+
+ for m in _re_differ.finditer(guide, pos):
+ if m.start() > pos:
+ segments.append(_item(text=cgi.escape(line[pos:m.start()]), type=None))
+ segments.append(_item(text=cgi.escape(line[m.start():m.end()]),
+ type="change"))
+ pos = m.end()
+
+ segments.append(_item(text=cgi.escape(line[pos:]), type=None))
+
+ return _item(gap=ezt.boolean(gap), type=type, segments=segments,
+ left_number=left_number, right_number=right_number)
+
+class _item:
+ def __init__(self, **kw):
+ vars(self).update(kw)
+
+try:
+ ### Using difflib._mdiff function here was the easiest way of obtaining
+ ### intraline diffs for use in ViewVC, but it doesn't exist prior to
+ ### Python 2.4 and is not part of the public difflib API, so for now
+ ### fall back if it doesn't exist.
+ difflib._mdiff
+except AttributeError:
+ sidebyside = None
--- /dev/null
+# -*-python-*-
+#
+# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
+#
+# By using this file, you agree to the terms and conditions set forth in
+# the LICENSE.html file which can be found at the top level of the ViewVC
+# distribution or at http://viewvc.org/license-1.html.
+#
+# For more information,