File viewvc-buglink.patch of Package viewvc

Index: conf/viewvc.conf.dist
===================================================================
--- conf/viewvc.conf.dist.orig
+++ conf/viewvc.conf.dist
@@ -343,6 +343,21 @@
 ##---------------------------------------------------------------------------
 [options]
 
+## The 'buglink_base' value is a string that can be used to form a URL
+## by appending a bug number.  If viewvc sees something that looks
+## like a bug number in a log message (eg. "bug 12345" or "#12345"), it
+## will be displayed as a link to the bug in your bug tracking system.
+##
+## For a Bugzilla installation, you probably want to set this to
+## something like "http://hostname/show_bug.cgi?id=".  For the Debian
+## bug tracker, you might use
+## "http://hostname/cgi-bin/bugreport.cgi?bug=".
+##
+## If 'buglink_base' is not set, then bug tracker links won't be
+## generated.
+##
+#buglink_base = http://example.com/show_bug.cgi?id=
+
 ## root_as_url_component: Interpret the first path component in the URL
 ## after the script location as the root to use.  This is an
 ## alternative to using the "root=" query key. If ViewVC is configured
Index: lib/config.py
===================================================================
--- lib/config.py.orig
+++ lib/config.py
@@ -52,7 +52,7 @@ import fnmatch
 # Here's a diagram of the valid overlays/overrides:
 #
 #         PER-ROOT          PER-VHOST            BASE
-#       
+#
 #                         ,-----------.     ,-----------.
 #                         | vhost-*/  |     |           |
 #                         |  general  | --> |  general  |
@@ -150,13 +150,13 @@ class Config:
     settings there as overrides to the built-in default values.  If
     VHOST is provided, also process the configuration overrides
     specific to that virtual host."""
-    
+
     self.conf_path = os.path.isfile(pathname) and pathname or None
     self.base = os.path.dirname(pathname)
     self.parser = ConfigParser.ConfigParser()
     self.parser.optionxform = lambda x: x # don't case-normalize option names.
     self.parser.read(self.conf_path or [])
-    
+
     for section in self.parser.sections():
       if self._is_allowed_section(section, self._base_sections):
         self._process_section(self.parser, section, section)
@@ -168,7 +168,7 @@ class Config:
     """Process the key/value (kv) files specified in the
     configuration, merging their values into the configuration as
     dotted heirarchical items."""
-    
+
     kv = _sub_config()
 
     for fname in self.general.kv_files:
@@ -228,7 +228,7 @@ class Config:
     """Return 1 iff SECTION is an allowed section, defined as being
     explicitly present in the ALLOWED_SECTIONS list or present in the
     form 'someprefix-*' in that list."""
-    
+
     for allowed_section in allowed_sections:
       if allowed_section[-1] == '*':
         if _startswith(section, allowed_section[:-1]):
@@ -284,7 +284,7 @@ class Config:
     set.  This is a destructive change to the configuration."""
 
     did_overlay = 0
-    
+
     if not self.conf_path:
       return
 
@@ -374,7 +374,7 @@ class Config:
         for attr in dir(sub_config):
           params[attr] = getattr(sub_config, attr)
     return params
-  
+
   def set_defaults(self):
     "Set some default values in the configuration."
 
@@ -445,6 +445,7 @@ class Config:
     self.options.log_pagesextra = 3
     self.options.limit_changes = 100
     self.options.stacktraces = 0
+    self.options.buglink_base = None
 
     self.templates.diff = None
     self.templates.directory = None
@@ -464,13 +465,13 @@ class Config:
     self.cvsdb.user = ''
     self.cvsdb.passwd = ''
     self.cvsdb.readonly_user = ''
-    self.cvsdb.readonly_passwd = '' 
+    self.cvsdb.readonly_passwd = ''
     self.cvsdb.row_limit = 1000
     self.cvsdb.rss_row_limit = 100
     self.cvsdb.check_database_for_root = 0
 
     self.query.viewvc_base_url = None
-    
+
 def _startswith(somestr, substr):
   return somestr[:len(substr)] == substr
 
@@ -494,7 +495,7 @@ class IllegalOverrideSection(ViewVCConfi
   def __str__(self):
     return "malformed configuration: illegal %s override section: %s" \
            % (self.override_type, self.section_name)
-  
+
 class MalformedRoot(ViewVCConfigurationError):
   def __init__(self, config_name, value_given):
     Exception.__init__(self, config_name, value_given)
Index: lib/viewvc.py
===================================================================
--- lib/viewvc.py.orig
+++ lib/viewvc.py
@@ -154,7 +154,7 @@ class Request:
     for name, values in self.server.params().items():
       # we only care about the first value
       value = values[0]
-      
+
       # patch up old queries that use 'cvsroot' to look like they used 'root'
       if name == 'cvsroot':
         name = 'root'
@@ -172,12 +172,12 @@ class Request:
 
       # validate the parameter
       _validate_param(name, value)
-      
+
       # if we're here, then the parameter is okay
       self.query_dict[name] = value
 
     # Resolve the view parameter into a handler function.
-    self.view_func = _views.get(self.query_dict.get('view', None), 
+    self.view_func = _views.get(self.query_dict.get('view', None),
                                 self.view_func)
 
     # Process PATH_INFO component of query string
@@ -239,7 +239,7 @@ class Request:
       if roottype:
         # Overlay root-specific options.
         cfg.overlay_root_options(self.rootname)
-        
+
         # Setup an Authorizer for this rootname and username
         debug.t_start('setup-authorizer')
         self.auth = setup_authorizer(cfg, self.username)
@@ -290,7 +290,7 @@ class Request:
           'The root "%s" has an unknown type ("%s").  Expected "cvs" or "svn".'
           % (self.rootname, type),
           "500 Internal Server Error")
-      
+
     # If this is using an old-style 'rev' parameter, redirect to new hotness.
     # Subversion URLs will now use 'pathrev'; CVS ones use 'revision'.
     if self.repos and self.query_dict.has_key('rev'):
@@ -356,7 +356,7 @@ class Request:
           needs_redirect = 1
 
     if self.view_func is None:
-      # view parameter is not set, try looking at pathtype and the 
+      # view parameter is not set, try looking at pathtype and the
       # other parameters
       if not self.rootname:
         self.view_func = view_roots
@@ -374,7 +374,7 @@ class Request:
         elif self.query_dict.has_key('graph'):
           if not self.query_dict.has_key('makeimage'):
             self.view_func = view_cvsgraph
-          else: 
+          else:
             self.view_func = view_cvsgraph_image
         elif self.query_dict.has_key('revision') \
                  or cfg.options.default_file_view != "log":
@@ -394,7 +394,7 @@ class Request:
       self.where = ''
       self.path_parts = []
       self.pathtype = None
-      
+
     # if we have a directory and the request didn't end in "/", then redirect
     # so that it does.
     if (self.pathtype == vclib.DIR and path_info[-1:] != '/'
@@ -454,7 +454,7 @@ class Request:
 
   def get_link(self, view_func=None, where=None, pathtype=None, params=None):
     """Constructs a link pointing to another ViewVC page. All arguments
-    correspond to members of the Request object. If they are set to 
+    correspond to members of the Request object. If they are set to
     None they take values from the current page. Return value is a base
     URL and a dictionary of parameters"""
 
@@ -467,7 +467,7 @@ class Request:
       params = self.query_dict.copy()
     else:
       params = params.copy()
-      
+
     # must specify both where and pathtype or neither
     assert (where is None) == (pathtype is None)
 
@@ -481,7 +481,7 @@ class Request:
       pathtype = self.pathtype
 
     # no need to add sticky variables for views with no links
-    sticky_vars = not (view_func is view_checkout 
+    sticky_vars = not (view_func is view_checkout
                        or view_func is download_tarball)
 
     # The logic used to construct the URL is an inverse of the
@@ -518,7 +518,7 @@ class Request:
 
         # no need to specify default root
         if rootname == cfg.general.default_root:
-          del params['root']   
+          del params['root']
 
     # add 'pathrev' value to parameter list
     if (self.pathrev is not None
@@ -606,7 +606,7 @@ def _normalize_path(path):
   because we output the script name in links and web browsers
   interpret //viewvc.cgi/ as http://viewvc.cgi/
   """
-  
+
   i = 0
   for c in path:
     if c != '/':
@@ -687,7 +687,7 @@ _legal_params = {
   'search'        : _validate_regex,
   'p1'            : None,
   'p2'            : None,
-  
+
   'hideattic'     : _re_validate_boolint,
   'limit_changes' : _re_validate_number,
   'sortby'        : _re_validate_alpha,
@@ -787,7 +787,7 @@ def _orig_path(request, rev_param='revis
   #     *checkout*/circle.jpg?pathrev=3
   #     *checkout*/square.jpg?revision=3
   #     *checkout*/square.jpg?revision=3&pathrev=4
-  # 
+  #
   # Note that the following:
   #
   #     *checkout*/circle.jpg?rev=3
@@ -799,7 +799,7 @@ def _orig_path(request, rev_param='revis
   #
   rev = request.query_dict.get(rev_param, request.pathrev)
   path = request.query_dict.get(path_param, request.where)
-  
+
   if rev is not None and hasattr(request.repos, '_getrev'):
     try:
       pathrev = request.repos._getrev(request.pathrev)
@@ -813,7 +813,7 @@ def setup_authorizer(cfg, username, root
   """Setup the authorizer.  If ROOTNAME is provided, assume that
   per-root options have not been overlayed.  Otherwise, assume they
   have (and fetch the authorizer for the configured root)."""
-  
+
   if rootname is None:
     authorizer = cfg.options.authorizer
     params = cfg.get_authorizer_params()
@@ -853,7 +853,7 @@ def check_freshness(request, mtime=None,
   # See if we are supposed to disable etags (for debugging, usually)
   if not cfg.options.generate_etags:
     return 0
-  
+
   request_etag = request_mtime = None
   if etag is not None:
     if weak:
@@ -937,7 +937,7 @@ def get_writeready_server_file(request,
     request.server.addheader('Content-Encoding', 'gzip')
   elif content_length is not None:
     request.server.addheader('Content-Length', content_length)
-  
+
   if content_type and encoding:
     request.server.header("%s; charset=%s" % (content_type, encoding))
   elif content_type:
@@ -950,9 +950,9 @@ def get_writeready_server_file(request,
                        request.server.file())
   else:
     fp = request.server.file()
-  
+
   return fp
-  
+
 def generate_page(request, view_name, data, content_type=None):
   server_fp = get_writeready_server_file(request, content_type)
   template = get_view_template(request.cfg, view_name, request.language)
@@ -1040,7 +1040,7 @@ def default_view(mime_type, cfg):
   # very useful marked up. If the mime type is totally unknown (happens when
   # we encounter an unrecognized file extension) we also view it through
   # the markup page since that's better than sending it text/plain.
-  if ('markup' in cfg.options.allowed_views and 
+  if ('markup' in cfg.options.allowed_views and
       (is_viewable_image(mime_type) or is_text(mime_type))):
     return view_markup
   return view_checkout
@@ -1053,7 +1053,7 @@ def is_binary_file_mime_type(mime_type,
       if fnmatch.fnmatch(mime_type, pattern):
         return True
   return False
-  
+
 def get_file_view_info(request, where, rev=None, mime_type=None, pathrev=-1):
   """Return an object holding common hrefs and a viewability flag used
   for various views of FILENAME at revision REV whose MIME type is
@@ -1068,9 +1068,9 @@ def get_file_view_info(request, where, r
      prefer_markup
      is_viewable_image
      is_binary
-     
+
   """
-  
+
   rev = rev and str(rev) or None
   mime_type = mime_type or guess_mime(where)
   if pathrev == -1: # cheesy default value, since we need to preserve None
@@ -1167,7 +1167,11 @@ class ViewVCHtmlFormatterTokens:
           return out, out_len, 1
     return out, out_len, 0
 
-    
+
+# Matches bug numbers
+_re_rewrite_bug = re.compile(r'((?:\bbug[\s:#+]|[^&]#|^#)\s*(\d\d+))', re.I)
+_re_buglink_prefix = ""
+
 class ViewVCHtmlFormatter:
   """Format a string as HTML-encoded output with customizable markup
   rules, for example turning strings that look like URLs into anchor links.
@@ -1176,10 +1180,23 @@ class ViewVCHtmlFormatter:
   interface, there is a good chance that there are consumers outside
   of ViewVC itself that make use of these things.
   """
-  
+
   def __init__(self):
     self._formatters = []
 
+  def format_bugzilla(self, mobj, userdata, maxlen=0):
+    """Return a 2-tuple containing:
+         - the text represented by MatchObject MOBJ, formatted as
+           linkified URL, with no more than MAXLEN characters in the
+           non-HTML-tag bits.  If MAXLEN is 0, there is no maximum.
+         - the number of non-HTML-tag characters returned.
+    """
+    s = mobj.group(0)
+    trunc_s = maxlen and s[:maxlen] or s
+    return '<a href="%s%s">%s</a>' % (_re_buglink_prefix, urllib.quote(mobj.group(2)),
+                    sapi.escape(trunc_s)), \
+                    len(trunc_s)
+
   def format_url(self, mobj, userdata, maxlen=0):
     """Return a 2-tuple containing:
          - the text represented by MatchObject MOBJ, formatted as
@@ -1212,7 +1229,7 @@ class ViewVCHtmlFormatter:
            entity-encoded email address, with no more than MAXLEN characters
            in the non-HTML-tag bits.  If MAXLEN is 0, there is no maximum.
          - the number of non-HTML-tag characters returned.
-    """    
+    """
     s = mobj.group(0)
     trunc_s = maxlen and s[:maxlen] or s
     return self._entity_encode(trunc_s), len(trunc_s)
@@ -1282,10 +1299,10 @@ class ViewVCHtmlFormatter:
          - the text S, HTML-escaped, containing no more than MAXLEN
            characters.  If MAXLEN is 0, there is no maximum.
          - the number of characters returned.
-    """   
+    """
     trunc_s = maxlen and s[:maxlen] or s
     return sapi.escape(trunc_s), len(trunc_s)
-  
+
   def add_formatter(self, regexp, conv, userdata=None):
     """Register a formatter which finds instances of strings matching
     REGEXP, and using the function CONV and USERDATA to format them.
@@ -1375,11 +1392,11 @@ class LogFormatter:
 
   def get(self, maxlen=0, htmlize=1):
     cfg = self.request.cfg
-    
+
     # Prefer the cache.
     if self.cache.has_key((maxlen, htmlize)):
       return self.cache[(maxlen, htmlize)]
-    
+
     # If we are HTML-izing...
     if htmlize:
       # ...and we don't yet have ViewVCHtmlFormatter() object tokens...
@@ -1399,6 +1416,11 @@ class LogFormatter:
           lf.add_formatter(_re_rewrite_svnrevref, lf.format_svnrevref,
                            revision_to_url)
 
+        if cfg.options.buglink_base is not None:
+          global _re_buglink_prefix
+          _re_buglink_prefix = cfg.options.buglink_base
+          lf.add_formatter(_re_rewrite_bug, lf.format_bugzilla)
+
         # Rewrite email addresses.
         if cfg.options.mangle_email_addresses == 2:
           lf.add_formatter(_re_rewrite_email, lf.format_email_truncated)
@@ -1409,7 +1431,7 @@ class LogFormatter:
 
         # Add custom rewrite handling per configuration.
         for rule in cfg.options.custom_log_formatting:
-          rule = rule.replace('\\:', '\x01')          
+          rule = rule.replace('\\:', '\x01')
           regexp, format = map(lambda x: x.strip(), rule.split(':', 1))
           regexp = regexp.replace('\x01', ':')
           format = format.replace('\x01', ':')
@@ -1496,7 +1518,7 @@ def html_time(request, secs, extended=0)
 def common_template_data(request, revision=None, mime_type=None):
   """Return a ezt.TemplateData instance with data dictionary items
   common to most ViewVC views."""
-  
+
   cfg = request.cfg
 
   # Initialize data dictionary members (sorted alphanumerically)
@@ -1587,7 +1609,7 @@ def common_template_data(request, revisi
     data['view_href'] = request.get_url(view_func=view_directory,
                                        params={}, escape=1)
     if 'tar' in cfg.options.allowed_views:
-      data['tarball_href'] = request.get_url(view_func=download_tarball, 
+      data['tarball_href'] = request.get_url(view_func=download_tarball,
                                              params={},
                                              escape=1)
     if request.roottype == 'svn':
@@ -1633,7 +1655,7 @@ def retry_read(src, reqlen=CHUNK_SIZE):
         time.sleep(1)
         continue
     return chunk
-  
+
 def copy_stream(src, dst, htmlize=0):
   while 1:
     chunk = retry_read(src)
@@ -1682,7 +1704,7 @@ def detect_encoding(text_block):
   Python module.  (Currently, this is used only when syntax
   highlighting is not enabled/available; otherwise, Pygments does this
   work for us.)"""
-  
+
   # Does the TEXT_BLOCK start with a BOM?
   for bom, encoding in [('\xef\xbb\xbf', 'utf-8'),
                         ('\xff\xfe', 'utf-16'),
@@ -1711,7 +1733,7 @@ def detect_encoding(text_block):
 
   # By default ... we have no idea.
   return None
-  
+
 def transcode_text(text, encoding=None):
   """If ENCODING is provided and not 'utf-8', transcode TEXT from
   ENCODING to UTF-8."""
@@ -1735,7 +1757,7 @@ def markup_stream(request, cfg, blame_da
   apply syntax coloration to the file contents, and use the
   HTML-marked-up results as the text in the return vclib.Annotation
   objects."""
-  
+
   # Nothing to mark up?  So be it.
   if not file_lines:
     return []
@@ -1792,7 +1814,7 @@ def markup_stream(request, cfg, blame_da
                                      stripnl=False)
       except ClassNotFound:
         pygments_lexer = None
-        
+
   # If we aren't highlighting, just return an amalgamation of the
   # BLAME_DATA (if any) and the FILE_LINES.
   if not pygments_lexer:
@@ -1933,7 +1955,7 @@ def parse_mime_type(mime_type):
     name, value = string.split(part, '=', 1)
     parameters[name] = value
   return type_subtype, parameters
-  
+
 def calculate_mime_type(request, path_parts, rev):
   """Return a 2-tuple carrying the MIME content type and character
   encoding for the file represented by PATH_PARTS in REV.  Use REQUEST
@@ -1961,7 +1983,7 @@ def assert_viewable_filesize(cfg, filesi
                                 'disallowed by configuration'
                                 % (cfg.options.max_filesize_kbytes),
                                 '403 Forbidden')
-  
+
 def markup_or_annotate(request, is_annotate):
   cfg = request.cfg
   path, rev = _orig_path(request, is_annotate and 'annotate' or 'revision')
@@ -1974,7 +1996,7 @@ def markup_or_annotate(request, is_annot
   if is_binary_file_mime_type(mime_type, cfg):
     raise debug.ViewVCException('Display of binary file content disabled '
                                 'by configuration', '403 Forbidden')
-    
+
   # Is this a viewable image type?
   if is_viewable_image(mime_type) \
      and 'co' in cfg.options.allowed_views:
@@ -2128,9 +2150,9 @@ def markup_or_annotate(request, is_annot
                                         pathtype=vclib.FILE,
                                         params={'pathrev': revision},
                                         escape=1)
-    
+
   generate_page(request, "file", data)
-  
+
 def view_markup(request):
   if 'markup' not in request.cfg.options.allowed_views:
     raise debug.ViewVCException('Markup view is disabled',
@@ -2213,7 +2235,7 @@ def view_roots(request):
   if 'roots' not in request.cfg.options.allowed_views:
     raise debug.ViewVCException('Root listing view is disabled',
                                 '403 Forbidden')
-  
+
   # add in the roots for the selection
   roots = []
   expand_root_parents(request.cfg)
@@ -2247,7 +2269,7 @@ def view_roots(request):
   data = common_template_data(request)
   data.merge(ezt.TemplateData({
     'roots' : roots,
-    'roots_shown' : len(roots), 
+    'roots_shown' : len(roots),
     }))
   generate_page(request, "roots", data)
 
@@ -2269,7 +2291,7 @@ def view_directory(request):
   # List current directory
   options = {}
   if request.roottype == 'cvs':
-    hideattic = int(request.query_dict.get('hideattic', 
+    hideattic = int(request.query_dict.get('hideattic',
                                            cfg.options.hide_attic))
     options["cvs_subdirs"] = (cfg.options.show_subdir_lastmod and
                               cfg.options.show_logs)
@@ -2321,7 +2343,7 @@ def view_directory(request):
   rows = [ ]
   dirs_displayed = files_displayed = 0
   num_dead = 0
-  
+
   # set some values to be used inside loop
   where = request.where
   where_prefix = where and where + '/'
@@ -2361,7 +2383,7 @@ def view_directory(request):
          and is_cvsroot_path(request.roottype,
                              request.path_parts + [file.name]):
         continue
-    
+
       dirs_displayed += 1
 
       row.view_href = request.get_url(view_func=view_directory,
@@ -2387,7 +2409,7 @@ def view_directory(request):
                                        pathtype=vclib.DIR,
                                        params={},
                                        escape=1)
-      
+
     elif file.kind == vclib.FILE:
       if searchstr is not None:
         if request.roottype == 'cvs' and (file.errors or file.dead):
@@ -2399,11 +2421,11 @@ def view_directory(request):
         num_dead = num_dead + 1
         if hideattic:
           continue
-        
+
       files_displayed += 1
 
       file_where = where_prefix + file.name
-      if request.roottype == 'svn': 
+      if request.roottype == 'svn':
         row.size = file.size
 
       row.mime_type, encoding = calculate_mime_type(request,
@@ -2512,7 +2534,7 @@ def view_directory(request):
     branch_tags.sort(icmp)
     branch_tags.reverse()
     data['branch_tags']= branch_tags
-    
+
     data['attic_showing'] = ezt.boolean(not hideattic)
     data['show_attic_href'] = request.get_url(params={'hideattic': 0},
                                               escape=1)
@@ -2590,7 +2612,7 @@ def paging_sws(data, key, pagestart, loc
     try:
       pick.end = getattr(data[key][i+pagesize-1], local_name)
     except IndexError:
-      pick.end = getattr(data[key][-1], local_name)   
+      pick.end = getattr(data[key][-1], local_name)
     picklist.append(pick)
     if pick.count >= last_requested:
       pick.more = ezt.boolean(1)
@@ -2645,9 +2667,9 @@ def redirect_pathrev(request):
   new_pathrev = request.query_dict.get('pathrev') or None
   path = request.query_dict.get('orig_path', '')
   pathtype = request.query_dict.get('orig_pathtype')
-  pathrev = request.query_dict.get('orig_pathrev') 
+  pathrev = request.query_dict.get('orig_pathrev')
   view = _views.get(request.query_dict.get('orig_view'))
-  
+
   youngest = request.repos.get_youngest_revision()
 
   # go out of the way to allow revision numbers higher than youngest
@@ -2669,7 +2691,7 @@ def redirect_pathrev(request):
     if new_pathrev is None and pathrev == youngest:
       pathrev = None
 
-  request.server.redirect(request.get_url(view_func=view, 
+  request.server.redirect(request.get_url(view_func=view,
                                           where=path,
                                           pathtype=pathtype,
                                           params={'pathrev': pathrev}))
@@ -2751,7 +2773,7 @@ def view_log(request):
     entry.diff_to_prev_href = None
     entry.diff_to_branch_href = None
     entry.diff_to_main_href = None
-        
+
     if request.roottype == 'cvs':
       prev = rev.prev or rev.parent
       entry.prev = prev and prev.string
@@ -2944,7 +2966,7 @@ def view_log(request):
       data['tag_annotate_href']= fvi.annotate_href
       data['tag_prefer_markup']= fvi.prefer_markup
   else:
-    data['head_view_href'] = request.get_url(view_func=view_directory, 
+    data['head_view_href'] = request.get_url(view_func=view_directory,
                                              params={}, escape=1)
 
   taginfo = options.get('cvs_tags', {})
@@ -2985,7 +3007,7 @@ def view_log(request):
 def view_checkout(request):
 
   cfg = request.cfg
-  
+
   if 'co' not in cfg.options.allowed_views:
     raise debug.ViewVCException('Checkout view is disabled',
                                  '403 Forbidden')
@@ -3024,7 +3046,7 @@ def view_cvsgraph_image(request):
                    ("-c", cfg.path(cfg.options.cvsgraph_conf),
                     "-r", request.repos.rootpath,
                     rcsfile), 'rb', 0)
-  
+
   copy_stream(fp, get_writeready_server_file(request, 'image/png'))
   fp.close()
 
@@ -3054,7 +3076,7 @@ def view_cvsgraph(request):
                     "-x", "x",
                     "-3", request.get_url(view_func=view_log, params={},
                                           escape=1),
-                    "-4", request.get_url(view_func=view, 
+                    "-4", request.get_url(view_func=view,
                                           params={'revision': None},
                                           escape=1, partial=1),
                     "-5", request.get_url(view_func=view_diff,
@@ -3155,7 +3177,7 @@ class DiffSource:
     self.save_line = None
     self.line_number = None
     self.prev_line_number = None
-    
+
     # keep track of where we are during an iteration
     self.idx = -1
     self.last = None
@@ -3186,11 +3208,11 @@ class DiffSource:
     if self.cfg.options.tabsize > 0:
       text = string.expandtabs(text, self.cfg.options.tabsize)
     hr_breakable = self.cfg.options.hr_breakable
-    
+
     # in the code below, "\x01" will be our stand-in for "&". We don't want
     # to insert "&" because it would get escaped by sapi.escape().  Similarly,
     # we use "\x02" as a stand-in for "<br>"
-  
+
     if hr_breakable > 1 and len(text) > hr_breakable:
       text = re.sub('(' + ('.' * hr_breakable) + ')', '\\1\x02', text)
     if hr_breakable:
@@ -3203,7 +3225,7 @@ class DiffSource:
     text = string.replace(text, '\x02',
                           '<span style="color:red">\</span><br />')
     return text
-    
+
   def _get_row(self):
     if self.state[:5] == 'flush':
       item = self._flush_row()
@@ -3243,7 +3265,7 @@ class DiffSource:
                    line_info_left=match.group(1),
                    line_info_right=match.group(2),
                    line_info_extra=self._format_text(match.group(3)))
-    
+
     if line[0] == '\\':
       # \ No newline at end of file
 
@@ -3254,7 +3276,7 @@ class DiffSource:
 
     diff_code = line[0]
     output = self._format_text(line[1:])
-    
+
     if diff_code == '+':
       if self.state == 'dump':
         self.line_number = self.line_number + 1
@@ -3353,7 +3375,7 @@ def diff_parse_headers(fp, diff_type, pa
       elif line[:3] == 'Bin':
         flag = _RCSDIFF_IS_BINARY
         parsing = 0
-      elif (string.find(line, 'not found') != -1 or 
+      elif (string.find(line, 'not found') != -1 or
             string.find(line, 'illegal option') != -1):
         flag = _RCSDIFF_ERROR
         parsing = 0
@@ -3411,7 +3433,7 @@ def setup_diff(request):
     else:
       rev1 = r1[:idx]
       sym1 = r1[idx+1:]
-      
+
   if r2 == 'text':
     rev2 = query_dict.get('tr2', None)
     if not rev2:
@@ -3433,7 +3455,7 @@ def setup_diff(request):
     except vclib.InvalidRevision:
       raise debug.ViewVCException('Invalid revision(s) passed to diff',
                                   '400 Bad Request')
-    
+
   p1 = _get_diff_path_parts(request, 'p1', rev1, request.pathrev)
   p2 = _get_diff_path_parts(request, 'p2', rev2, request.pathrev)
 
@@ -3476,7 +3498,7 @@ def view_patch(request):
   else:
     raise debug.ViewVCException('Diff format %s not understood'
                                  % format, '400 Bad Request')
-  
+
   try:
     fp = request.repos.rawdiff(p1, rev1, p2, rev2, diff_type)
   except vclib.InvalidRevision:
@@ -3503,7 +3525,7 @@ def view_diff(request):
   cfg = request.cfg
   query_dict = request.query_dict
   p1, p2, rev1, rev2, sym1, sym2 = setup_diff(request)
-  
+
   mime_type1, encoding1 = calculate_mime_type(request, p1, rev1)
   mime_type2, encoding2 = calculate_mime_type(request, p2, rev2)
   if is_binary_file_mime_type(mime_type1, cfg) or \
@@ -3524,7 +3546,7 @@ def view_diff(request):
          and html_time(request, log_entry1.date, 1) or None
   ago2 = log_entry2.date is not None \
          and html_time(request, log_entry2.date, 2) or None
-  
+
   diff_type = None
   diff_options = {}
   human_readable = 0
@@ -3579,7 +3601,7 @@ def view_diff(request):
       else:
         unified = idiff.unified(lines_left, lines_right,
                                 diff_options.get("context", 2))
-    else: 
+    else:
       fp = request.repos.rawdiff(p1, rev1, p2, rev2, diff_type, diff_options)
   except vclib.InvalidRevision:
     raise debug.ViewVCException('Invalid path(s) or revision(s) passed '
@@ -3622,7 +3644,7 @@ def view_diff(request):
                annotate_href=fvi.annotate_href,
                revision_href=fvi.revision_href,
                prefer_markup=fvi.prefer_markup)
-    
+
   fvi = get_file_view_info(request, path_right, rev2)
   right = _item(date=make_time_string(log_entry2.date, cfg),
                 author=log_entry2.author,
@@ -3744,7 +3766,7 @@ def generate_tarball(out, request, reldi
     tar_dir = tar_dir + _path_join(reldir) + '/'
 
   cvs = request.roottype == 'cvs'
-  
+
   # If our caller doesn't dictate a datestamp to use for the current
   # directory, its datestamps will be the youngest of the datestamps
   # of versioned items in that subdirectory.  We'll be ignoring dead
@@ -3822,7 +3844,7 @@ def generate_tarball(out, request, reldi
       # Write the tarball header...
       generate_tarball_header(out, tar_dir + file.name, filesize, mode,
                               file.date is not None and file.date or 0)
-      
+
       # ...the file's contents ...
       fp = request.repos.openfile(rep_path + [file.name], request.pathrev, {})[0]
       while 1:
@@ -3852,7 +3874,7 @@ def generate_tarball(out, request, reldi
 
 def download_tarball(request):
   cfg = request.cfg
-  
+
   if 'tar' not in request.cfg.options.allowed_views:
     raise debug.ViewVCException('Tarball generation is disabled',
                                  '403 Forbidden')
@@ -3863,7 +3885,7 @@ def download_tarball(request):
   # our own gzip stream wrapper.
   if debug.TARFILE_PATH:
     fp = open(debug.TARFILE_PATH, 'w')
-  else:    
+  else:
     tarfile = request.rootname
     if request.path_parts:
       tarfile = "%s-%s" % (tarfile, request.path_parts[-1])
@@ -3904,7 +3926,7 @@ def view_revision(request):
   except vclib.InvalidRevision:
     raise debug.ViewVCException('Invalid revision', '404 Not Found')
   youngest_rev = request.repos.get_youngest_revision()
-  
+
   # The revision number acts as a weak validator (but we tell browsers
   # not to cache the youngest revision).
   if rev != youngest_rev and check_freshness(request, None, str(rev), weak=1):
@@ -3934,7 +3956,7 @@ def view_revision(request):
       value = None
       undisplayable = ezt.boolean(1)
     props.append(_item(name=name, value=value, undisplayable=undisplayable))
-  
+
   # Sort the changes list by path.
   def changes_sort_by_path(a, b):
     return cmp(a.path_parts, b.path_parts)
@@ -4002,14 +4024,14 @@ def view_revision(request):
 
       if change.pathtype is vclib.FILE and change.text_changed:
         change.diff_href = request.get_url(view_func=view_diff,
-                                           where=path, 
+                                           where=path,
                                            pathtype=change.pathtype,
                                            params={'pathrev' : str(rev),
                                                    'r1' : str(rev),
                                                    'r2' : str(change.base_rev),
                                                    },
                                            escape=1)
-    
+
 
     # use same variable names as the log template
     change.path = _path_join(change.path_parts)
@@ -4099,7 +4121,7 @@ def is_querydb_nonempty_for_root(request
 def validate_query_args(request):
   # Do some additional input validation of query form arguments beyond
   # what is offered by the CGI param validation loop in Request.run_viewvc().
-  
+
   for arg_base in ['branch', 'file', 'comment', 'who']:
     # First, make sure the the XXX_match args have valid values:
     arg_match = arg_base + '_match'
@@ -4122,7 +4144,7 @@ def validate_query_args(request):
             'An illegal value was provided for the "%s" parameter.'
             % (arg_base),
             '400 Bad Request')
-  
+
 def view_queryform(request):
   if not is_query_supported(request):
     raise debug.ViewVCException('Can not query project root "%s" at "%s".'
@@ -4131,7 +4153,7 @@ def view_queryform(request):
 
   # Do some more precise input validation.
   validate_query_args(request)
-  
+
   query_action, query_hidden_values = \
     request.get_form(view_func=view_query, params={'limit_changes': None})
   limit_changes = \
@@ -4140,7 +4162,7 @@ def view_queryform(request):
 
   def escaped_query_dict_get(itemname, itemdefault=''):
     return request.server.escape(request.query_dict.get(itemname, itemdefault))
-    
+
   data = common_template_data(request)
   data.merge(ezt.TemplateData({
     'branch' : escaped_query_dict_get('branch', ''),
@@ -4167,7 +4189,7 @@ def view_queryform(request):
 
 def parse_date(datestr):
   """Parse a date string from the query form."""
-  
+
   match = re.match(r'^(\d\d\d\d)-(\d\d)-(\d\d)(?:\ +'
                    '(\d\d):(\d\d)(?::(\d\d))?)?$', datestr)
   if match:
@@ -4279,7 +4301,7 @@ def build_commit(request, files, max_fil
   plus_count = 0
   minus_count = 0
   found_unreadable = 0
-  
+
   for f in files:
     dirname = f.GetDirectory()
     filename = f.GetFile()
@@ -4310,7 +4332,7 @@ def build_commit(request, files, max_fil
          and is_cvsroot_path(request.roottype, path_parts):
         found_unreadable = 1
         continue
-      
+
       # We have to do a rare authz check here because this data comes
       # from the CVSdb, not from the vclib providers.
       #
@@ -4328,12 +4350,12 @@ def build_commit(request, files, max_fil
       if not readable:
         found_unreadable = 1
         continue
-         
+
     if request.roottype == 'svn':
       params = { 'pathrev': exam_rev }
     else:
-      params = { 'revision': exam_rev, 'pathrev': f.GetBranch() or None }  
-    
+      params = { 'revision': exam_rev, 'pathrev': f.GetBranch() or None }
+
     dir_href = request.get_url(view_func=view_directory,
                                where=dirname, pathtype=vclib.DIR,
                                params=params, escape=1)
@@ -4367,7 +4389,7 @@ def build_commit(request, files, max_fil
     minus = int(f.GetMinusCount())
     plus_count = plus_count + plus
     minus_count = minus_count + minus
-    
+
     num_allowed = num_allowed + 1
     if max_files and num_allowed > max_files:
       continue
@@ -4560,7 +4582,7 @@ def view_query(request):
   db.RunQuery(query)
   commit_list = query.GetCommitList()
   row_limit_reached = query.GetLimitReached()
-  
+
   # gather commits
   commits = []
   plus_count = 0
@@ -4580,7 +4602,7 @@ def view_query(request):
       # base modification time on the newest commit
       if commit.GetTime() > mod_time:
         mod_time = commit.GetTime()
-        
+
       # For CVS, group commits with the same commit message.
       # For Subversion, group them only if they have the same revision number
       if request.roottype == 'cvs':
@@ -4605,7 +4627,7 @@ def view_query(request):
       limited_files = 0
       current_desc = commit_desc
       current_rev = commit_rev
-      
+
     # we need to tack on our last commit grouping, if any
     commit_item = build_commit(request, files, limit_changes,
                                dir_strip, format)
@@ -4614,7 +4636,7 @@ def view_query(request):
       plus_count = plus_count + commit_item.plus
       minus_count = minus_count + commit_item.minus
       commits.append(commit_item)
-  
+
   # only show the branch column if we are querying all branches
   # or doing a non-exact branch match on a CVS repository.
   show_branch = ezt.boolean(request.roottype == 'cvs' and
@@ -4689,7 +4711,7 @@ for code, view in _views.items():
 def list_roots(request):
   cfg = request.cfg
   allroots = { }
-  
+
   # Add the viewable Subversion roots
   for root in cfg.general.svn_roots.keys():
     auth = setup_authorizer(cfg, request.username, root)
@@ -4725,12 +4747,12 @@ def list_roots(request):
     except vclib.ReposNotFound:
       continue
     allroots[root] = [cfg.general.cvs_roots[root], 'cvs', None]
-    
+
   return allroots
 
 def expand_root_parents(cfg):
   """Expand the configured root parents into individual roots."""
-  
+
   # Each item in root_parents is a "directory : repo_type" string.
   for pp in cfg.general.root_parents:
     pos = string.rfind(pp, ':')
@@ -4762,7 +4784,7 @@ def find_root_in_parents(cfg, rootname,
   # Easy out:  caller wants rootname "CVSROOT", and we're hiding those.
   if rootname == 'CVSROOT' and cfg.options.hide_cvsroot:
     return None
-  
+
   for pp in cfg.general.root_parents:
     pos = string.rfind(pp, ':')
     if pos < 0:
@@ -4771,7 +4793,7 @@ def find_root_in_parents(cfg, rootname,
     if repo_type != roottype:
       continue
     pp = os.path.normpath(string.strip(pp[:pos]))
-    
+
     rootpath = None
     if roottype == 'cvs':
       rootpath = vclib.ccvs.find_root_in_parent(pp, rootname)
@@ -4797,7 +4819,7 @@ def locate_root(cfg, rootname):
     cfg.general.svn_roots[rootname] = path_in_parent
     return 'svn', path_in_parent
   return None, None
-  
+
 def load_config(pathname=None, server=None):
   """Load the ViewVC configuration file.  SERVER is the server object
   that will be using this configuration.  Consult the environment for
@@ -4805,7 +4827,7 @@ def load_config(pathname=None, server=No
   legacy name) and, if set, use its value as the path of the
   configuration file; otherwise, use PATHNAME (if provided).  Failing
   all else, use a hardcoded default configuration path."""
-  
+
   debug.t_start('load-config')
 
   # See if the environment contains overrides to the configuration
openSUSE Build Service is sponsored by