diff --git a/cgi/viewcvs.cgi b/cgi/viewcvs.cgi index a95484bf..2bd4fd7e 100755 --- a/cgi/viewcvs.cgi +++ b/cgi/viewcvs.cgi @@ -69,2446 +69,3 @@ import popen ######################################################################### - -checkout_magic_path = '~checkout~' -viewcvs_mime_type = 'text/vnd.viewcvs-markup' - -# put here the variables we need in order to hold our state - they will be -# added (with their current value) to any link/query string you construct -_sticky_vars = ( - 'cvsroot', - 'hideattic', - 'sortby', - 'logsort', - 'diff_format', - 'only_with_tag' - ) - -# regex used to move from a file to a directory -_re_up_path = re.compile('(Attic/)?[^/]+$') - -_EOF_FILE = 'end of file entries' # no more entries for this RCS file -_EOF_LOG = 'end of log' # hit the true EOF on the pipe -_EOF_ERROR = 'error message found' # rlog issued an error - -_FILE_HAD_ERROR = 'could not read file' - -_UNREADABLE_MARKER = '//UNREADABLE-MARKER//' - -header_comment = '''\ - -''' - -# for reading/writing between a couple descriptors -CHUNK_SIZE = 8192 - -# if your rlog doesn't use 77 '=' characters, then this must change -LOG_END_MARKER = '=' * 77 + '\n' -ENTRY_END_MARKER = '-' * 28 + '\n' - - -class Request: - def __init__(self): - where = os.environ.get('PATH_INFO', '') - - # clean it up. this removes duplicate '/' characters and any that may - # exist at the front or end of the path. - parts = filter(None, string.split(where, '/')) - - # does it have the magic checkout prefix? - if parts and parts[0] == checkout_magic_path: - self.has_checkout_magic = 1 - del parts[0] - else: - self.has_checkout_magic = 0 - - # put it back together - where = string.join(parts, '/') - - script_name = os.environ['SCRIPT_NAME'] ### clean this up? - if where: - url = script_name + '/' + urllib.quote(where) - else: - url = script_name - - self.where = where - self.script_name = script_name - self.url = url - if parts: - self.module = parts[0] - else: - self.module = None - - self.browser = os.environ.get('HTTP_USER_AGENT', 'unknown') - - # in lynx, it it very annoying to have two links - # per file, so disable the link at the icon - # in this case: - self.no_file_links = string.find(self.browser, 'Lynx') != -1 - - # newer browsers accept gzip content encoding - # and state this in a header - # (netscape did always but didn't state it) - # It has been reported that these - # braindamaged MS-Internet Explorers claim that they - # accept gzip .. but don't in fact and - # display garbage then :-/ - self.may_compress = ( - ( string.find(os.environ.get('HTTP_ACCEPT_ENCODING', ''), 'gzip') != -1 - or string.find(self.browser, 'Mozilla/3') != -1) - and string.find(self.browser, 'MSIE') == -1 - ) - - # parse the query params into a dictionary (and use defaults) - query_dict = default_settings.copy() - for name, values in cgi.parse().items(): - query_dict[name] = values[0] - - # set up query strings, prefixed by question marks and ampersands - query = sticky_query(query_dict) - if query: - self.qmark_query = '?' + query - self.amp_query = '&' + query - else: - self.qmark_query = '' - self.amp_query = '' - - self.query_dict = query_dict - - # set up the CVS repository to use - self.cvsrep = query_dict.get('cvsroot', cfg.general.default_root) - self.cvsroot = cfg.general.cvs_roots[self.cvsrep] - - self.full_name = self.cvsroot + '/' + where - - def setup_mime_type_info(self): - if cfg.general.mime_types_file: - mimetypes.init([cfg.general.mime_types_file]) - self.mime_type, self.encoding = mimetypes.guess_type(self.where) - if not self.mime_type: - self.mime_type = 'text/plain' - self.default_text_plain = self.mime_type == 'text/plain' - self.default_viewable = cfg.options.allow_markup and \ - is_viewable(self.mime_type) - - -class LogHeader: - "Hold state from the header portion of an 'rlog' output." - def __init__(self, filename, head=None, branch=None, taginfo=None): - self.filename = filename - self.head = head - self.branch = branch - self.taginfo = taginfo - - -class LogEntry: - "Hold state for each revision entry in an 'rlog' output." - def __init__(self, rev, date, author, state, changed, log): - self.rev = rev - self.date = date - self.author = author - self.state = state - self.changed = changed - self.log = log - - -def redirect(location): - print 'Status: 301 Moved' - print 'Location:', location - print - print 'This document is located here.' % location - sys.exit(0) - -def error(msg, status='500 Internal Server Error'): - print 'Status:', status - print - print msg - sys.exit(0) - -_header_sent = 0 -def http_header(content_type='text/html'): - global _header_sent - if _header_sent: - return - print 'Content-Type:', content_type - print - _header_sent = 1 - -def html_header(title): - http_header() - logo = cfg.images.get_image('logo') - print '''\ - - -%s -%s - - - -

%s

%s
-''' % (header_comment, title, cfg.colors.text, cfg.colors.background, - title, logo) - -def html_footer(): - print '
' - print '' % cfg.general.address - print '
%s
ViewCVS %s
' % __version__ - print 'by Greg Stein' - print '
' - print '' - -def sticky_query(dict): - sticky_dict = { } - for varname in _sticky_vars: - value = dict.get(varname) - if value is not None and value != default_settings.get(varname, ''): - sticky_dict[varname] = value - return compat.urlencode(sticky_dict) - -def toggle_query(query_dict, which, value=None): - dict = query_dict.copy() - if value is None: - dict[which] = not dict[which] - else: - dict[which] = value - query = sticky_query(dict) - if query: - return '?' + query - return '' - -def clickable_path(request, path, leaf_is_link, leaf_is_file, drop_leaf): - if path == '/': - # this should never happen - chooseCVSRoot() is - # intended to do this - return '[%s]' % cvsrep - - s = '[%s]' % \ - (request.script_name, request.qmark_query, request.cvsrep) - parts = filter(None, string.split(path, '/')) - if drop_leaf: - del parts[-1] - leaf_is_link = 1 - leaf_is_file = 0 - where = '' - for i in range(len(parts)): - where = where + '/' + parts[i] - is_leaf = i == len(parts) - 1 - if not is_leaf or leaf_is_link: - if is_leaf and leaf_is_file: - slash = '' - else: - slash = '/' - ### should we be encoding/quoting the URL stuff? (probably...) - s = s + ' / %s' % \ - (request.script_name, where, slash, request.qmark_query, parts[i]) - else: - s = s + ' / ' + parts[i] - - return s - -def html_link(contents, link): - return '%s' % (link, contents) - -def link_tags(query_dict, where, text, add_links): - if not add_links: - return text - - filename = os.path.basename(where) - file_url = urllib.quote(filename) - links = [ ] - for tag in string.split(text, ', '): - links.append('%s' % - (file_url, - toggle_query(query_dict, 'only_with_tag', tag), - tag)) - return string.join(links, ', ') - -def is_viewable(mime_type): - return mime_type[:5] == 'text/' or mime_type[:6] == 'image/' - -_re_rewrite_url = re.compile('((http|ftp)(://[-a-zA-Z0-9%.~:_/]+)([?&]([-a-zA-Z0-9%.~:_]+)=([-a-zA-Z0-9%.~:_])+)*(#([-a-zA-Z0-9%.~:_]+)?)?)') -_re_rewrite_email = re.compile('([-a-zA-Z0-9_.]+@([-a-zA-Z0-9]+\.)+[A-Za-z]{2,4})') -def htmlify(html): - html = cgi.escape(html) - html = re.sub(_re_rewrite_url, r'\1', html) - html = re.sub(_re_rewrite_email, r'\1', html) - return html - -def html_log(log): - print ' ' + htmlify(log[:cfg.options.short_log_len]) - if len(log) > cfg.options.short_log_len: - print '...' - print '' - -def download_url(request, url, revision, mime_type): - if cfg.options.checkout_magic and mime_type != viewcvs_mime_type: - url = '%s/%s/%s/%s' % \ - (request.script_name, checkout_magic_path, - os.path.dirname(request.where), url) - - url = url + '?rev=' + revision - if mime_type: - return url + '&content-type=' + mime_type - return url - -def download_link(request, url, revision, text, mime_type=None): - full_url = download_url(request, url, revision, mime_type) - paren = text[0] == '(' - - lparen = rparen = '' - if paren: - lparen = '(' - rparen = ')' - text = text[1:-1] - - print '%s%s%s' % (text, rparen) - -def html_icon(which): - return cfg.images.get_image(which + '_icon') - -def plural(num, text): - if num == 1: - return '1 ' + text - if num: - return '%d %ss' % (num, text) - return '' - -_time_desc = { - 1 : 'second', - 60 : 'minute', - 3600 : 'hour', - 86400 : 'day', - 604800 : 'week', - 2628000 : 'month', - 31536000 : 'year', - } -def html_time(secs, extended=0): - secs = long(time.time()) - secs - if secs < 2: - return 'very little time' - breaks = _time_desc.keys() - breaks.sort() - i = 0 - while i < len(breaks): - if secs < 2 * breaks[i]: - break - i = i + 1 - value = breaks[i - 1] - s = plural(secs / value, _time_desc[value]) - - if extended and i > 1: - secs = secs % value - value = breaks[i - 2] - ext = plural(secs / value, _time_desc[value]) - if ext: - s = s + ', ' + ext - return s - -def html_option(value, cur_value, text=None): - if text is None: - attr = '' - text = value - else: - attr = 'value="%s"' % value - - if value == cur_value: - print '' % (attr, text) - else: - print '' % (attr, text) - -def print_diff_select(query_dict): - print '' - -def navigate_header(request, swhere, path, filename, rev, title): - if swhere == request.url: - swhere = urllib.quote(filename) - - print '' - print header_comment - print '%s/%s - %s - %s' % (path, filename, title, rev) - print '' % cfg.colors.alt_background - print '' % cfg.colors.nav_header - print '' % \ - (html_link(filename, - '%s%s#rev%s' % (swhere, request.qmark_query, rev)), - html_icon('file')) - print '' % \ - (html_icon('dir'), clickable_path(request, path, 1, 0, 0)) - print '
' - print '%s' % \ - (swhere, request.qmark_query, rev, html_icon('back')) - print 'Return to %s CVS log %s%s Up to %s
' - -def markup_stream_default(fp): - print '
'
-  while 1:
-    ### technically, the htmlify() could fail if something falls across
-    ### the chunk boundary. TFB.
-    chunk = fp.read(CHUNK_SIZE)
-    if not chunk:
-      break
-    sys.stdout.write(htmlify(chunk))
-  print '
' - -def markup_stream_python(fp): - try: - # see if Marc-Andre Lemburg's py2html stuff is around - # http://starship.python.net/crew/lemburg/SoftwareDescriptions.html#py2html.py - ### maybe restrict the import to *only* this directory? - sys.path.insert(0, cfg.options.py2html_path) - import py2html - import PyFontify - except ImportError: - # fall back to the default streamer - markup_stream_default(fp) - else: - ### it doesn't escape stuff quite right, nor does it munge URLs and - ### mailtos as well as we do. - html = cgi.escape(fp.read()) - pp = py2html.PrettyPrint(PyFontify.fontify, "rawhtml", "color") - html = pp.fontify(html) - html = re.sub(_re_rewrite_url, r'\1', html) - html = re.sub(_re_rewrite_email, r'\1', html) - sys.stdout.write(html) - -def markup_stream_enscript(lang, fp): - sys.stdout.flush() - enscript = popen.pipe_cmds([(cfg.options.enscript_path + 'enscript', - '--color', '-W', 'html', '-E' + lang, '-o', - '-', '-'), - ('sed', '-n', '/^
$/,/<\\/PRE>$/p')])
-
-  while 1:
-    chunk = fp.read(CHUNK_SIZE)
-    if not chunk:
-      if fp.eof() is None:
-        time.sleep(1)
-        continue
-      break
-    enscript.write(chunk)
-
-  enscript.close()
-
-markup_streamers = {
-#  '.py' : markup_stream_python,
-  }
-
-### this sucks... we have to duplicate the extensions defined by enscript
-enscript_extensions = {
-  '.c' : 'c',
-  '.h' : 'c',
-  '.c++' : 'cpp',
-  '.C' : 'cpp',
-  '.H' : 'cpp',
-  '.cpp' : 'cpp',
-  '.cc' : 'cpp',
-  '.cxx' : 'cpp',
-  '.m' : 'objc',
-  '.scm' : 'scheme',
-  '.scheme' : 'scheme',
-  '.el' : 'elisp',
-  '.ada' : 'ada',
-  '.adb' : 'ada',
-  '.ads' : 'ada',
-  '.s' : 'asm',
-  '.S' : 'asm',
-  '.st' : 'states',
-  '.tcl' : 'tcl',
-  '.v' : 'verilog',
-  '.vh' : 'verilog',
-  '.htm' : 'html',
-  '.html' : 'html',
-  '.shtml' : 'html',
-  '.vhd' : 'vhdl',
-  '.vhdl' : 'vhdl',
-  '.scr' : 'synopsys',
-  '.syn' : 'synopsys',
-  '.synth' : 'synopsys',
-  '.idl' : 'idl',
-  '.hs' : 'haskell',
-  '.lhs' : 'haskell',
-  '.gs' : 'haskell',
-  '.lgs' : 'haskell',
-  '.pm' : 'perl',
-  '.pl' : 'perl',
-  '.eps' : 'postscript',
-  '.EPS' : 'postscript',
-  '.ps' : 'postscript',
-  '.PS' : 'postscript',
-  '.js' : 'javascript',
-  '.java' : 'java',
-  '.pas' : 'pascal',
-  '.pp' : 'pascal',
-  '.p' : 'pascal',
-  '.f' : 'fortran',
-  '.F' : 'fortran',
-  '.awk' : 'awk',
-  '.sh' : 'sh',
-  '.vba' : 'vba',
-
-  ### use enscript or py2html?
-  '.py' : 'python',
-  }
-enscript_filenames = {
-  '.emacs' : 'elisp',
-  'Makefile' : 'makefile',
-  'makefile' : 'makefile',
-  }
-
-def markup_stream(request, fp, revision, mime_type):
-  full_name = request.full_name
-  where = request.where
-  query_dict = request.query_dict
-
-  pathname, filename = os.path.split(where)
-  if pathname[-6:] == '/Attic':
-    pathname = pathname[:-6]
-  file_url = urllib.quote(filename)
-
-  http_header()
-  navigate_header(request, request.url, pathname, filename, revision, 'view')
-  print '
' - print '
' % cfg.colors.markup_log - print 'File:', clickable_path(request, where, 1, 1, 0), '' - download_link(request, file_url, revision, '(download)') - if not request.default_text_plain: - download_link(request, file_url, revision, '(as text)', 'text/plain') - print '
' - if cfg.options.show_log_in_markup: - show_revs, rev_map, rev_order, taginfo, rev2tag, \ - cur_branch, branch_points, branch_names = read_log(full_name) - - print_log(request, rev_map, rev_order, rev_map[revision], rev2tag, - branch_points, 0) - else: - print 'Version: %s
' % revision - tag = query_dict.get('only_with_tag') - if tag: - print 'Tag: %s
' % tag - print '
' - - print '
' - if mime_type[:6] == 'image/': - url = download_url(request, file_url, revision, mime_type) - print '
' % (url, request.amp_query) - else: - basename, ext = os.path.splitext(filename) - streamer = markup_streamers.get(ext) - if streamer: - streamer(fp) - elif not cfg.options.use_enscript: - markup_stream_default(fp) - else: - lang = enscript_extensions.get(ext) - if not lang: - lang = enscript_filenames.get(basename) - if lang and lang not in cfg.options.disable_enscript_lang: - markup_stream_enscript(lang, fp) - else: - markup_stream_default(fp) - status = fp.close() - if status: - raise error, 'pipe error status: %d' % status - html_footer() - -def get_file_data(full_name): - """Return a sequence of tuples containing various data about the files. - - data[0] = (relative) filename - data[1] = full pathname - data[2] = is_directory (0/1) - - Only RCS files (*,v) and subdirs are returned. - """ - - files = os.listdir(full_name) - data = [ ] - - uid = os.getuid() - gid = os.getgid() - - for file in files: - pathname = full_name + '/' + file - try: - info = os.stat(pathname) - except os.error: - data.append((file, _UNREADABLE_MARKER, None)) - continue - mode = info[stat.ST_MODE] - isdir = stat.S_ISDIR(mode) - isreg = stat.S_ISREG(mode) - if (isreg and file[-2:] == ',v') or isdir: - # - # Quick version of access() where we use existing stat() data. - # - # This might not be perfect -- the OS may return slightly different - # results for some bizarre reason. However, we make a good show of - # "can I read this file/dir?" by checking the various perm bits. - # - # NOTE: if the UID matches, then we must match the user bits -- we - # cannot defer to group or other bits. Similarly, if the GID matches, - # then we must have read access in the group bits. - # - if isdir: - mask = stat.S_IROTH | stat.S_IXOTH - else: - mask = stat.S_IROTH - - valid = 1 - if info[stat.ST_UID] == uid: - if ((mode >> 6) & mask) != mask: - valid = 0 - elif info[stat.ST_GID] == gid: - if ((mode >> 3) & mask) != mask: - valid = 0 - elif (mode & mask) != mask: - valid = 0 - - if valid: - data.append((file, pathname, isdir)) - else: - data.append((file, _UNREADABLE_MARKER, isdir)) - - return data - -def get_last_modified(file_data): - """Return mapping of subdir to info about the most recently modified subfile. - - key = subdir - data[0] = "subdir/subfile" of the most recently modified subfile - data[1] = the mod time of that file (time_t) - """ - - lastmod = { } - for file, pathname, isdir in file_data: - if not isdir or pathname == _UNREADABLE_MARKER: - continue - if file == 'Attic': - continue - - subfiles = os.listdir(pathname) - latest = ('', 0) - for subfile in subfiles: - ### filter CVS locks? stale NFS handles? - if subfile[-2:] != ',v': - continue - subpath = pathname + '/' + subfile - info = os.stat(subpath) - if not stat.S_ISREG(info[stat.ST_MODE]): - continue - if info[stat.ST_MTIME] > latest[1]: - latest = (file + '/' + subfile, info[stat.ST_MTIME]) - if latest[0]: - lastmod[file] = latest - return lastmod - -def parse_log_header(fp): - """Parse and RCS/CVS log header. - - fp is a file (pipe) opened for reading the log information. - - On entry, fp should point to the start of a log entry. - On exit, fp will have consumed the separator line between the header and - the first revision log. - - If there is no revision information (e.g. the "-h" switch was passed to - rlog), then fp will consumed the file separator line on exit. - """ - filename = head = branch = None - taginfo = { } # tag name => revision - - parsing_tags = 0 - eof = None - - while 1: - line = fp.readline() - if not line: - # the true end-of-file - eof = _EOF_LOG - break - - if parsing_tags: - if line[0] == '\t': - [ tag, rev ] = map(string.strip, string.split(line, ':')) - taginfo[tag] = rev - else: - # oops. this line isn't tag info. stop parsing tags. - parsing_tags = 0 - - if not parsing_tags: - if line[:9] == 'RCS file:': - # remove the trailing ,v - filename = line[10:-3] - elif line[:5] == 'head:': - head = line[6:-1] - elif line[:7] == 'branch:': - branch = line[8:-1] - elif line[:14] == 'symbolic names': - # start parsing the tag information - parsing_tags = 1 - elif line == ENTRY_END_MARKER: - # end of the headers - break - elif line == LOG_END_MARKER: - # end of this file's log information - eof = _EOF_FILE - break - elif line[:6] == 'rlog: ': - # rlog: filename/goes/here,v: error message - idx = string.find(line, ':', 6) - if idx != -1: - if line[idx:idx+32] == ': warning: Unknown phrases like ': - # don't worry about this warning. it can happen with some RCS - # files that have unknown fields in them (e.g. "permissions 644;" - continue - - # looks like a filename - filename = line[6:idx] - if filename[-2:] == ',v': - filename = filename[:-2] - return LogHeader(filename), _EOF_ERROR - # dunno what this is - - return LogHeader(filename, head, branch, taginfo), eof - -_re_date_author = re.compile(r'^date:\s+([^;]+);\s+author:\s+([^;]+);.*') -_re_log_info = re.compile(r'^date:\s+([^;]+);' - r'\s+author:\s+([^;]+);' - r'\s+state:\s+([^;]+);' - r'(\s+lines:\s+([0-9\s+-]+))?\n$') -### _re_rev should be updated to extract the "locked" flag -_re_rev = re.compile(r'^revision\s+([0-9.]+).*') -def parse_log_entry(fp): - """Parse a single log entry. - - On entry, fp should point to the first line of the entry (the "revision" - line). - On exit, fp will have consumed the log separator line (dashes) or the - end-of-file marker (equals). - - Returns: revision, date (time_t secs), author, state, lines changed, - the log text, and eof flag (see _EOF_*) - """ - rev = None - line = fp.readline() - if not line: - return None, _EOF_LOG - if line[:8] == 'revision': - match = _re_rev.match(line) - if not match: - return None, _EOF_LOG - rev = match.group(1) - - line = fp.readline() - if not line: - return None, _EOF_LOG - match = _re_log_info.match(line) - - eof = None - log = '' - while 1: - line = fp.readline() - if not line: - # true end-of-file - eof = _EOF_LOG - break - if line[:9] == 'branches:': - continue - if line == ENTRY_END_MARKER: - break - if line == LOG_END_MARKER: - # end of this file's log information - eof = _EOF_FILE - break - - log = log + line - - if not rev or not match: - # there was a parsing error - return None, eof - - date = int(time.mktime(compat.cvs_strptime(match.group(1)))) - time.timezone - - return LogEntry(rev, date, - # author, state, lines changed - match.group(2), match.group(3), match.group(5), - log), eof - -def skip_file(fp): - "Skip the rest of a file's log information." - while 1: - line = fp.readline() - if not line: - break - if line == LOG_END_MARKER: - break - -def process_rlog_output(rlog, full_name, view_tag, fileinfo, alltags): - "Fill in fileinfo and alltags with info from the rlog output." - - # consume each file found in the resulting log - while 1: - - revwanted = None - branch = None - branchpoint = None - - header, eof = parse_log_header(rlog) - filename = header.filename - head = header.head - branch = header.branch - symrev = header.taginfo - - # the rlog output is done - if eof == _EOF_LOG: - break - - if filename: - # convert from absolute to relative - if filename[:len(full_name)] == full_name: - filename = filename[len(full_name)+1:] - - # for a subdir (not Attic files!), use the subdir for a key - idx = string.find(filename, '/') - if idx != -1 and filename[:6] != 'Attic/': - info_key = filename[:idx] - else: - info_key = filename - - # an error was found regarding this file - if eof == _EOF_ERROR: - fileinfo[info_key] = _FILE_HAD_ERROR - continue - - # if we hit the end of the log information (already!), then there is - # nothing we can do with this file - if eof: - continue - - if not filename or not head: - # parsing error. skip the rest of this file. - skip_file(rlog) - continue - - if not branch: - idx = string.rfind(head, '.') - branch = head[:idx] - idx = string.rfind(branch, '.') - if idx == -1: - branch = '0.' + branch - else: - branch = branch[:idx] + '.0' + branch[idx:] - - symrev['MAIN'] = symrev['HEAD'] = branch - - if symrev.has_key(view_tag): - revwanted = symrev[view_tag] - if revwanted[:2] == '0.': ### possible? - branch = revwanted[2:] - else: - idx = string.find(revwanted, '.0.') - if idx == -1: - branch = revwanted - else: - branch = revwanted[:idx] + revwanted[idx+2:] - if revwanted != branch: - revwanted = None - - idx = string.rfind(branch, '.') - if idx == -1: - branchpoint = '' - else: - branchpoint = branch[:idx] - - elif view_tag: - # the tag wasn't found, so skip this file - skip_file(rlog) - continue - - # we don't care about the values -- just the keys. this the fastest - # way to merge the set of keys - alltags.update(symrev) - - # read all of the log entries until we find the revision we want - while 1: - - # fetch one of the log entries - entry, eof = parse_log_entry(rlog) - - if not entry: - # parsing error - if not eof: - skip_file(rlog) - break - - rev = entry.rev - - idx = string.rfind(rev, '.') - revbranch = rev[:idx] - - if not view_tag or (not revwanted and branch == revbranch): - revwanted = rev - - if rev == revwanted or rev == branchpoint: - fileinfo[info_key] = (rev, entry.date, entry.log, entry.author, - filename, entry.state) - - if rev == revwanted: - # done with this file now - if not eof: - skip_file(rlog) - break - - # if we hit the true EOF, or just this file's end-of-info, then we are - # done collecting log entries. - if eof: - break - -def get_logs(full_name, files, view_tag): - - if len(files) == 0: - return { }, [ ] - - fileinfo = { } - alltags = { # all the tags seen in the files of this dir - 'MAIN' : '1', - 'HEAD' : '1', - } - - chunk_size = 100 - while files: - chunk = files[:chunk_size] - del files[:chunk_size] - - # prepend the full pathname for each file - for i in range(len(chunk)): - chunk[i] = full_name + '/' + chunk[i] - - if view_tag: - # NOTE: can't pass tag on command line since a tag may contain "-" - # we'll search the output for the appropriate revision - rlog = popen.popen(cfg.general.rcs_path + 'rlog', chunk, 'r') - else: - # fetch the latest revision on the default branch - chunk = ('-r',) + tuple(chunk) - rlog = popen.popen(cfg.general.rcs_path + 'rlog', chunk, 'r') - - process_rlog_output(rlog, full_name, view_tag, fileinfo, alltags) - - ### it would be nice to verify that we got SOMETHING from rlog about - ### each file. if we didn't, then it could be that the chunk is still - ### too large, so we want to cut the chunk_size in half and try again. - ### - ### BUT: if we didn't get feedback for some *other* reason, then halving - ### the chunk size could merely send us into a needless retry loop. - ### - ### more work for later... - - status = rlog.close() - if status: - raise 'error during rlog: '+hex(status) - - return fileinfo, alltags - -def revcmp(rev1, rev2): - rev1 = map(int, string.split(rev1, '.')) - rev2 = map(int, string.split(rev2, '.')) - return cmp(rev1, rev2) - -def print_roots(current_root): - if len(cfg.general.cvs_roots) < 2: - return - print '

Project Root

' - print '
' - print '
' - -def view_directory(request): - full_name = request.full_name - where = request.where - query_dict = request.query_dict - - view_tag = query_dict.get('only_with_tag') - hideattic = int(query_dict.get('hideattic')) ### watch for errors in int()? - sortby = query_dict.get('sortby', 'file') - - file_data = get_file_data(full_name) - - if cfg.options.show_subdir_lastmod: - lastmod = get_last_modified(file_data) - else: - lastmod = { } - if cfg.options.show_logs: - subfiles = map(lambda (subfile, mtime): subfile, lastmod.values()) - else: - subfiles = [ ] - - attic_files = [ ] - if not hideattic or view_tag: - try: - attic_files = os.listdir(full_name + '/Attic') - except os.error: - pass - else: - ### filter for just RCS files? - attic_files = map(lambda file: 'Attic/' + file, attic_files) - - # get all the required info - rcs_files = subfiles + attic_files - for file, pathname, isdir in file_data: - if not isdir and pathname != _UNREADABLE_MARKER: - rcs_files.append(file) - fileinfo, alltags = get_logs(full_name, rcs_files, view_tag) - - # append the Attic files into the file_data now - # NOTE: we only insert the filename and isdir==0 - for file in attic_files: - file_data.append((file, None, 0)) - - if where == '': - html_header(cfg.general.main_title) - - # these may be commented out or altered in the configuration section - print cfg.text.long_intro - print cfg.text.doc_info - print cfg.text.repository_info - - print_roots(request.cvsrep) - else: - html_header(where) - print cfg.text.short_intro - - print '

' - - if where == '': - #choose_mirror() - #choose_cvsroot() - pass - else: - print '

Current directory: ', \ - clickable_path(request, where, 0, 0, 0), '' - if view_tag: - print '

Current tag: ', view_tag, '' - - print '


' - - num_cols = 0 - - if cfg.colors.table_border: - print '' \ - '
' % cfg.colors.table_border - print '' % cfg.options.table_padding - - def print_header(title, which, sortby=sortby, query_dict=query_dict): - if sortby == which: - print '' % \ - (cfg.colors.column_header_sorted, title) - else: - query = toggle_query(query_dict, 'sortby', which) - print '' % \ - (cfg.colors.column_header_normal, query, title) - - print '' - num_cols = 1 - print_header('File', 'file') - - # fileinfo will be len==0 if we only have dirs and !show_subdir_lastmod - # in that case, we don't need the extra columns - if len(fileinfo): - num_cols = 3 - print_header('Rev.', 'rev') - print_header('Age', 'date') - if cfg.options.show_author: - num_cols = 4 - print_header('Author', 'author') - if cfg.options.show_logs: - num_cols = num_cols + 1 - print_header('Last log entry', 'log') - print '' - - def file_sort_cmp(data1, data2, sortby=sortby, fileinfo=fileinfo): - if data1[2]: # is_directory - if data2[2]: - # both are directories. sort on name. - return cmp(data1[0], data2[0]) - # data1 is a directory, it sorts first. - return -1 - if data2[2]: - # data2 is a directory, it sorts first. - return 1 - - # the two files should be RCS files. drop the ",v" from the end. - file1 = data1[0][:-2] - file2 = data2[0][:-2] - - # we should have data on these. if not, then it is because we requested - # a specific tag and that tag is not present on the file. - info1 = fileinfo.get(file1) - if info1 == _FILE_HAD_ERROR: - info1 = None - info2 = fileinfo.get(file2) - if info2 == _FILE_HAD_ERROR: - info2 = None - if info1 and info2: - if sortby == 'rev': - result = revcmp(info1[0], info2[0]) - elif sortby == 'date': - result = cmp(info2[1], info1[1]) # latest date is first - elif sortby == 'log': - result = cmp(info1[2], info2[2]) - elif sortby == 'author': - result = cmp(info1[3], info2[3]) - else: - # sortby == 'file' ... fall thru - result = 0 - - # return for unequal values; or fall thru for secondary-sort on name - if result: - return result - - # sort by file name - if file1[:6] == 'Attic/': - file1 = file1[6:] - if file2[:6] == 'Attic/': - file2 = file2[6:] - return cmp(file1, file2) - - # sort with directories first, and using the "sortby" criteria - file_data.sort(file_sort_cmp) - - cur_row = 0 - num_files = 0 - num_displayed = 0 - unreadable = 0 - - attic_toggle_link = '[Hide]' % \ - toggle_query(query_dict, 'hideattic', 1) - - ### display a row for ".." ? - - for file, pathname, isdir in file_data: - - if pathname == _UNREADABLE_MARKER: - if isdir is None: - # We couldn't even stat() the file to figure out what it is. - slash = '' - elif isdir: - slash = '/' - else: - slash = '' - file = file[:-2] # strip the ,v - num_displayed = num_displayed + 1 - print '' % \ - (cfg.colors.even_odd[cur_row % 2], file, file, slash) - print '' % \ - (num_cols - 1) - print '' - cur_row = cur_row + 1 - unreadable = 1 - continue - - if isdir: - if not hideattic and file == 'Attic': - continue - if where == '' and ((file == 'CVSROOT' and cfg.options.hide_cvsroot) - or cfg.is_forbidden(file)): - continue - if file == 'CVS': # CVS directory in a repository is used for fileattr. - continue - - print '' - - else: - # remove the ",v" - file = file[:-2] - - num_files = num_files + 1 - info = fileinfo.get(file) - if info == _FILE_HAD_ERROR: - print '' % \ - (cfg.colors.even_odd[cur_row % 2], file, file) - print '' % \ - (num_cols - 1) - print '' - cur_row = cur_row + 1 - num_displayed = num_displayed + 1 - unreadable = 1 - continue - elif not info: - continue - elif hideattic and view_tag and info[5] == 'dead': - continue - num_displayed = num_displayed + 1 - - file_url = urllib.quote(file) - url = file_url + request.qmark_query - - if view_tag: - if info[5] == 'dead': - attic = ' (not exist) ' + attic_toggle_link - else: - attic = '' - else: - if file[:6] == 'Attic/': - attic = ' (in the Attic) ' + attic_toggle_link - else: - attic = '' - - if file[:6] == 'Attic/': - file = file[6:] - - print '' - - cur_row = cur_row + 1 - - if cfg.colors.table_border: - print '
%s' \ - '%s' \ - '
%s%sunreadable
' % cfg.colors.even_odd[cur_row % 2] - url = urllib.quote(file) + '/' + request.qmark_query - print '' % file - if request.no_file_links: - print html_icon('dir') - else: - print html_link(html_icon('dir'), url) - print html_link(file + '/', url) - if file == 'Attic': - print '  [Don\'t hide]' % \ - toggle_query(query_dict, 'hideattic', 0) - - info = fileinfo.get(file) - if info == _FILE_HAD_ERROR: - print 'CVS information is unreadable' % \ - (num_cols - 1) - cur_row = cur_row + 1 - unreadable = 1 - elif info: - print '  ' - print html_time(info[1]) - if cfg.options.show_author: - print ' ' - print info[3] - if cfg.options.show_logs: - print ' ' - subfile = info[4] - idx = string.find(subfile, '/') - print '%s/%s' % (subfile[idx+1:], info[0]) - print '
' - if info[2]: - html_log(info[2]) - else: - for i in range(1, num_cols): - print '
 ' - - print '
%sCVS information is unreadable
' % cfg.colors.even_odd[cur_row % 2] - print '' % file - - if request.no_file_links: - print html_icon('file') - else: - print html_link(html_icon('file'), url) - print html_link(file, url), attic - - print ' ' - if cfg.options.allow_markup: - download_link(request, file_url, info[0], info[0], viewcvs_mime_type) - else: - download_link(request, file_url, info[0], info[0]) - print ' ' - print html_time(info[1]) - if cfg.options.show_author: - print ' ' - print info[3] - if cfg.options.show_logs: - print '' - html_log(info[2]) - - print '
' - print '
' - - if num_files and not num_displayed: - print '

NOTE: There are %d files, but none match the current ' \ - 'tag (%s)' % (num_files, view_tag) - if unreadable: - print '


NOTE: One or more files were ' \ - 'unreadable. The files in the CVS repository should be readable ' \ - 'by the web server process. Please report this condition to the ' \ - 'administrator of this CVS repository.' - - if alltags or view_tag: - print '
' - print '
' - for varname in _sticky_vars: - value = query_dict.get(varname, '') - if value != '' and value != default_settings.get(varname, '') and \ - varname != 'only_with_tag': - print '' % \ - (varname, query_dict[varname]) - - alltagnames = alltags.keys() - alltagnames.sort(lambda t1, t2: cmp(string.lower(t1), string.lower(t2))) - alltagnames.reverse() - branchtags = [] - nonbranchtags = [] - for tag in alltagnames: - rev = alltags[tag] - if string.find(rev, '.0.') == -1: - nonbranchtags.append(tag) - else: - branchtags.append(tag) - - print 'Show only files with tag:' - print '
' - - if cfg.options.allow_tar: - url = os.path.basename(where) + '.tar.gz?tarball=1' - query = sticky_query(query_dict) - if query: - url = url + '&' + query - print html_link("Download tarball", url) - - html_footer() - -def fetch_log(full_name, which_rev=None): - if which_rev: - args = ('-r' + which_rev, full_name) - else: - args = (full_name,) - rlog = popen.popen(cfg.general.rcs_path + 'rlog', args, 'r') - - header, eof = parse_log_header(rlog) - filename = header.filename - head = header.head - branch = header.branch - taginfo = header.taginfo - - if eof: - # no log entries or a parsing failure - return head, branch, taginfo, { } - - revs = [ ] - while 1: - entry, eof = parse_log_entry(rlog) - if entry: - # valid revision info - revs.append(entry) - if eof: - break - - return head, branch, taginfo, revs - -def logsort_date_cmp(rev1, rev2): - # sort on date; secondary on revision number - return -cmp(rev1.date, rev2.date) or -revcmp(rev1.rev, rev2.rev) - -def logsort_rev_cmp(rev1, rev2): - # sort highest revision first - return -revcmp(rev1.rev, rev2.rev) - -_re_is_branch = re.compile(r'^((.*)\.)?\b0\.(\d+)$') -def read_log(full_name, which_rev=None, view_tag=None, logsort='cvs'): - head, cur_branch, taginfo, revs = fetch_log(full_name, which_rev) - - if not cur_branch: - idx = string.rfind(head, '.') - cur_branch = head[:idx] - - rev_order = map(lambda entry: entry.rev, revs) - rev_order.sort(revcmp) - rev_order.reverse() - - # HEAD is an artificial tag which is simply the highest tag number on the - # main branch, unless there is a branch tag in the RCS file in which case - # it's the highest revision on that branch. Find it by looking through - # rev_order; it is the first commit listed on the appropriate branch. - # This is not neccesary the same revision as marked as head in the RCS file. - idx = string.rfind(cur_branch, '.') - if idx == -1: - taginfo['MAIN'] = '0.' + cur_branch - else: - taginfo['MAIN'] = cur_branch[:idx] + '.0' + cur_branch[idx:] - - for rev in rev_order: - idx = string.rfind(rev, '.') - if idx != -1 and cur_branch == rev[:idx]: - taginfo['HEAD'] = rev - break - else: - idx = string.rfind(cur_branch, '.') - taginfo['HEAD'] = cur_branch[:idx] - - # map revision numbers to tag names - rev2tag = { } - - # names of symbols at each branch point - branch_points = { } - - branch_names = [ ] - - # Now that we know all of the revision numbers, we can associate - # absolute revision numbers with all of the symbolic names, and - # pass them to the form so that the same association doesn't have - # to be built then. - - items = taginfo.items() - items.sort() - items.reverse() - for tag, rev in items: - match = _re_is_branch.match(rev) - if match: - branch_names.append(tag) - - # - # A revision number of A.B.0.D really translates into - # "the highest current revision on branch A.B.D". - # - # If there is no branch A.B.D, then it translates into - # the head A.B . - # - # This reasoning also applies to the main branch A.B, - # with the branch number 0.A, with the exception that - # it has no head to translate to if there is nothing on - # the branch, but I guess this can never happen? - # (the code below gracefully forgets about the branch - # if it should happen) - # - head = match.group(2) or '' - branch = match.group(3) - if head: - branch_rev = head + '.' + branch - else: - branch_rev = branch - rev = head - for r in rev_order: - if r == branch_rev or r[:len(branch_rev)+1] == branch_rev + '.': - rev = branch_rev - break - if rev == '': - continue - if rev != head and head != '': - if branch_points.has_key(head): - branch_points[head] = branch_points[head] + ', ' + tag - else: - branch_points[head] = tag - - if rev2tag.has_key(rev): - rev2tag[rev] = rev2tag[rev] + ', ' + tag - else: - rev2tag[rev] = tag - - if view_tag: - view_rev = taginfo.get(view_tag) - if not view_rev: - error('Tag %s not defined.' % view_tag, '404 Tag not found') - - if view_rev[:2] == '0.': - view_rev = view_rev[2:] - idx = string.rfind(view_rev, '.') - branch_point = view_rev[:idx] - else: - idx = string.find(view_rev, '.0.') - if idx == -1: - branch_point = view_rev - else: - view_rev = view_rev[:idx] + view_rev[idx+2:] - idx = string.rfind(view_rev, '.') - branch_point = view_rev[:idx] - - show_revs = [ ] - for entry in revs: - rev = entry.rev - idx = string.rfind(rev, '.') - branch = rev[:idx] - if branch == view_rev or rev == branch_point: - show_revs.append(entry) - else: - show_revs = revs - - if logsort == 'date': - show_revs.sort(logsort_date_cmp) - elif logsort == 'rev': - show_revs.sort(logsort_rev_cmp) - else: - # no sorting - pass - - # build a map of revision number to entry information - rev_map = { } - for entry in revs: - rev_map[entry.rev] = entry - - ### some of this return stuff doesn't make a lot of sense... - return show_revs, rev_map, rev_order, taginfo, rev2tag, \ - cur_branch, branch_points, branch_names - -_re_is_vendor_branch = re.compile(r'^1\.1\.1\.\d+$') -g_name_printed = { } ### gawd, what a hack... -def print_log(request, rev_map, rev_order, entry, rev2tag, branch_points, - add_links): - query_dict = request.query_dict - where = request.where - - ### torch this. some old grandfathering stuff... - # revinfo = (rev, date, author, state, lines changed, log) - revinfo = entry.rev, entry.date, entry.author, entry.state, \ - entry.changed, entry.log - - rev = revinfo[0] - - idx = string.rfind(rev, '.') - branch = rev[:idx] - idx = string.rfind(branch, '.') - if idx == -1: - branch_point = '' - else: - branch_point = branch[:idx] - - is_dead = revinfo[3] == 'dead' - - if add_links and not is_dead: - filename = os.path.basename(where) - file_url = urllib.quote(filename) - print '
' % rev - if rev2tag.has_key(rev): - for tag in string.split(rev2tag[rev], ', '): - print '' % tag - if rev2tag.has_key(branch) and not g_name_printed.has_key(branch): - for tag in string.split(rev2tag[branch], ', '): - print '' % tag - g_name_printed[branch] = 1 - print 'Revision' - if request.default_viewable: - download_link(request, file_url, rev, rev, viewcvs_mime_type) - print '/' - download_link(request, file_url, rev, '(download)', request.mime_type) - else: - download_link(request, file_url, rev, rev) - if not request.default_text_plain: - print '/' - download_link(request, file_url, rev, '(as text)', 'text/plain') - if not request.default_viewable: - print '/' - download_link(request, file_url, rev, '(view)', viewcvs_mime_type) - if cfg.options.allow_annotate: - print '- annotate' % \ - (request.url, rev, request.amp_query) - if cfg.options.allow_version_select: - if query_dict.get('r1') != rev: - print '- [select for diffs]' % \ - (request.url, rev, request.amp_query) - else: - print '- [selected]' - - else: - print 'Revision %s' % rev - - is_vendor_branch = _re_is_vendor_branch.match(rev) - if is_vendor_branch: - print '(vendor branch)' - - print ', %s UTC (%s ago) by %s' % \ - (time.asctime(time.gmtime(revinfo[1])), - html_time(revinfo[1], 1), - revinfo[2]) - - if rev2tag.has_key(branch): - print '
Branch: %s' % \ - link_tags(query_dict, where, rev2tag[branch], add_links) - if rev2tag.has_key(rev): - print '
CVS Tags: %s' % \ - link_tags(query_dict, where, rev2tag[rev], add_links) - if branch_points.has_key(rev): - print '
Branch point for: %s' % \ - link_tags(query_dict, where, branch_points[rev], add_links) - - prev_rev = string.split(rev, '.') - while 1: - if prev_rev[-1] == '0': # .0 can be caused by 'commit -r X.Y.Z.0' - prev_rev = prev_rev[:-2] # X.Y.Z.0 becomes X.Y.Z - else: - prev_rev[-1] = str(int(prev_rev[-1]) - 1) - prev = string.join(prev_rev, '.') - if rev_map.has_key(prev) or prev == '': - break - if prev and rev_map[rev].changed: - print '
Changes since %s: %s lines' % \ - (prev, rev_map[rev].changed) - - if is_dead: - print '
FILE REMOVED' - elif add_links: - diff_rev = { rev : 1, '' : 1 } - print '
Diff' - - # is the (current) diff format human readable? - human_readable = query_dict['diff_format'] == 'h' - - # diff against previous version - if prev: - diff_rev[prev] = 1 - print 'to previous %s' % \ - (request.url, prev, rev, request.amp_query, prev) - if not human_readable: - print '(colored)' % \ - (request.url, prev, rev, request.amp_query) - - # diff against branch point (if not a vendor branch) - if rev2tag.has_key(branch_point) and \ - not is_vendor_branch and \ - not diff_rev.has_key(branch_point): - diff_rev[branch_point] = 1 - print 'to a branchpoint %s' % \ - (request.url, branch_point, rev, request.amp_query, - branch_point) - if not human_readable: - print '(colored)' % \ - (request.url, branch_point, rev, request.amp_query) - - # if it's on a branch (and not a vendor branch), then diff against the - # next revision of the higher branch (e.g. change is committed and - # brought over to -stable) - if string.count(rev, '.') > 1 and not is_vendor_branch: - # locate this rev in the ordered list of revisions - for i in range(len(rev_order)): - if rev_order[i] == rev: - break - - # create a rev that can be compared - c_rev = map(int, string.split(rev, '.')) - - next_main = '' - while i: - next = rev_order[i - 1] - c_work = string.split(rev, '.') - if len(c_work) < len(c_rev): - # found something not on the branch - next_main = next - break - - # this is a higher version on the same branch; the lower one (rev) - # shouldn't have a diff against the "next main branch" - if c_work[:-1] == c_rev[:len(c_work) - 1]: - break - - i = i - 1 - - if not diff_rev.has_key(next_main): - diff_rev[next_main] = 1 - print 'next main %s' % \ - (request.url, next_main, rev, request.amp_query, next_main) - if not human_readable: - print '(colored)' \ - % (request.url, next_main, rev, request.amp_query) - - # if they have selected r1, then diff against that - r1 = query_dict.get('r1') - if r1 and not diff_rev.has_key(r1): - diff_rev[r1] = 1 - print 'to selected %s' % \ - (request.url, r1, rev, request.amp_query, r1) - if not human_readable: - print '(colored)' \ - % (request.url, r1, rev, request.amp_query) - - print '
' + htmlify(revinfo[5]) + '
' - -def view_log(request): - full_name = request.full_name - where = request.where - query_dict = request.query_dict - - view_tag = query_dict.get('only_with_tag') - - show_revs, rev_map, rev_order, taginfo, rev2tag, \ - cur_branch, branch_points, branch_names = \ - read_log(full_name, None, view_tag, query_dict['logsort']) - - html_header('CVS log for %s' % where) - - up_where = re.sub(_re_up_path, '', where) - - ### whoops. this sometimes/always? does not have the ",v" - assert full_name[-2:] != ',v', 'please report this error to viewcvs@lyra.org' - #filename = os.path.basename(full_name[:-2]) # drop the ",v" - filename = os.path.basename(full_name) - - ### try: "./" + query + "#" + filename - back_url = request.script_name + '/' + urllib.quote(up_where) + \ - request.qmark_query - - print html_link(html_icon('back'), back_url + '#' + filename) - - ### use drop_leaf - print 'Up to %s

' % clickable_path(request, up_where, 1, 0, 0) - print 'Request diff between arbitrary revisions' - print '


' - - if cur_branch: - print 'Default branch:', rev2tag.get(cur_branch, cur_branch) - else: - print 'No default branch' - print '
' - if view_tag: - print 'Current tag:', view_tag, '
' - - for entry in show_revs: - print '
' - print_log(request, rev_map, rev_order, entry, rev2tag, branch_points, 1) - - sel = [ ] - tagitems = taginfo.items() - tagitems.sort() - tagitems.reverse() - for tag, rev in tagitems: - sel.append('' % (rev, tag, tag)) - sel = string.join(sel, '\n') - - print '
' - print 'This form allows you to request diffs between any two revisions of' - print 'a file. You may select a symbolic revision name using the selection' - print 'box or you may type in a numeric name using the type-in text box.' - print '

' - - print '

' % \ - request.url - for varname in _sticky_vars: - value = query_dict.get(varname, '') - if value != '' and value != default_settings.get(varname): - print '' % (varname, value) - - print 'Diffs between' - print '' - - if query_dict.has_key('r1'): - diff_rev = query_dict['r1'] - else: - diff_rev = show_revs[-1].rev - print '' % \ - (cfg.options.input_text_size, diff_rev) - - print 'and' - print '' - - if query_dict.has_key('r2'): - diff_rev = query_dict['r2'] - else: - diff_rev = show_revs[0].rev - print '' % \ - (cfg.options.input_text_size, diff_rev) - - print '
Type of Diff should be a' - print_diff_select(query_dict) - - print '
' - print '
' - - hidden_values = '' - for varname in _sticky_vars: - if varname != 'only_with_tag' and varname != 'logsort': - value = query_dict.get(varname, '') - if value != '' and value != default_settings.get(varname): - hidden_values = hidden_values + \ - '' % \ - (varname, value) - - if branch_names: - print '
' % request.url - print hidden_values - - print 'View only Branch:' - print '' - print '
' - - print '' - print '
' % request.url - print hidden_values - print 'Sort log by:' - print '
' - - html_footer() - -### suck up other warnings in _re_co_warning? -_re_co_filename = re.compile(r'^(.*),v\s+-->\s+standard output\s*\n$') -_re_co_warning = re.compile(r'^.*co: .*,v: warning: Unknown phrases like .*\n$') -_re_co_revision = re.compile(r'^revision\s+([\d\.]+)\s*\n$') -def view_checkout(request): - full_name = request.full_name - where = request.where - query_dict = request.query_dict - - rev = query_dict.get('rev') - - ### validate the revision? - - mime_type = query_dict.get('content-type') - if mime_type: - ### validate it? - pass - else: - mime_type = request.mime_type - - if rev: - rev_flag = '-p' + rev - else: - rev_flag = '-p' - - fp = popen.popen(cfg.general.rcs_path + 'co', (rev_flag, full_name), 'r') - - # header from co: - # - #/home/cvsroot/mod_dav/dav_shared_stub.c,v --> standard output - #revision 1.1 - # - # Sometimes, the following line might occur at line 2: - #co: INSTALL,v: warning: Unknown phrases like `permissions ...;' are present. - - # parse the output header - filename = revision = None - - line = fp.readline() - if not line: - error('Missing output from co.
' - 'fname="%s". url="%s"' % (filename, where)) - - match = _re_co_filename.match(line) - if not match: - error('First line of co output is not the filename.
' - 'Line was: %s
' - 'fname="%s". url="%s"' % (line, filename, where)) - filename = match.group(1) - - line = fp.readline() - if not line: - error('Missing second line of output from co.
' - 'fname="%s". url="%s"' % (filename, where)) - match = _re_co_revision.match(line) - if not match: - match = _re_co_warning.match(line) - if not match: - error('Second line of co output is not the revision.
' - 'Line was: %s
' - 'fname="%s". url="%s"' % (line, filename, where)) - - # second line was a warning. ignore it and move along. - line = fp.readline() - if not line: - error('Missing third line of output from co (after a warning).
' - 'fname="%s". url="%s"' % (filename, where)) - match = _re_co_revision.match(line) - if not match: - error('Third line of co output is not the revision.
' - 'Line was: %s
' - 'fname="%s". url="%s"' % (line, filename, where)) - - # one of the above cases matches the revision. grab it. - revision = match.group(1) - - if filename != full_name: - error('The filename from co did not match. Found "%s". Wanted "%s"
' - 'url="%s"' % (filename, full_name, where)) - - if mime_type == viewcvs_mime_type: - # use the "real" MIME type - markup_stream(request, fp, revision, request.mime_type) - else: - http_header(mime_type) - while 1: - chunk = fp.read(8192) - if not chunk: - break - sys.stdout.write(chunk) - -def view_annotate(request): - rev = request.query_dict['annotate'] - - pathname, filename = os.path.split(request.where) - if pathname[-6:] == '/Attic': - pathname = pathname[:-6] - - http_header() - navigate_header(request, request.url, pathname, filename, rev, 'view') - print '
' - - import blame - blame.make_html(request.cvsroot, request.where + ',v', rev, - sticky_query(request.query_dict)) - - html_footer() - - -_re_extract_rev = re.compile(r'^[-+]+ [^\t]+\t([^\t]+)\t((\d+\.)+\d+)$') -_re_extract_info = re.compile(r'@@ \-([0-9]+).*\+([0-9]+).*@@(.*)') -_re_extract_diff = re.compile(r'^([-+ ])(.*)') -def human_readable_diff(request, fp, rev1, rev2, sym1, sym2): - query_dict = request.query_dict - - where_nd = request.where[:-5] # remove the ".diff" - pathname, filename = os.path.split(where_nd) - - navigate_header(request, request.script_name + '/' + where_nd, pathname, - filename, rev2, 'diff') - - log_rev1 = log_rev2 = None - date1 = date2 = '' - r1r = r2r = '' - while 1: - line = fp.readline() - if not line: - break - - # Use regex matching to extract the data and to ensure that we are - # extracting it from a properly formatted line. There are rcsdiff - # programs out there that don't supply the correct format; we'll be - # flexible in case we run into one of those. - if line[:4] == '--- ': - match = _re_extract_rev.match(line) - if match: - date1 = ', ' + match.group(1) - log_rev1 = match.group(2) - elif line[:4] == '+++ ': - match = _re_extract_rev.match(line) - if match: - date2 = ', ' + match.group(1) - log_rev2 = match.group(2) - break - - if (log_rev1 and log_rev1 != rev1) or (log_rev2 and log_rev2 != rev2): - print 'ERROR: rcsdiff did not return the correct' - print 'version number in its output.' - print '(got "%s" / "%s", expected "%s" / "%s")' % \ - (log_rev1, log_rev2, rev1, rev2) - print '

Aborting operation.' - sys.exit(0) - - print '

Diff for /%s between version %s and %s

' % \ - (where_nd, rev1, rev2) - print '' - print '' - print '' - print '' - - fs = '' % \ - (cfg.options.diff_font_face, cfg.options.diff_font_size) - left_row = right_row = 0 - - # this will be set to true if any changes are found - changes_seen = 0 - - while 1: - line = fp.readline() - if not line: - break - - # we've seen some kind of change - changes_seen = 1 - - if line[:2] == '@@': - match = _re_extract_info.match(line) - print '' - - state = 'dump' - left_col = [ ] - right_col = [ ] - elif line[0] == '\\': - # \ No newline at end of file - flush_diff_rows(state, left_col, right_col) - left_col = [ ] - right_col = [ ] - else: - match = _re_extract_diff.match(line) - line = spaced_html_text(match.group(2)) - - # add font stuff - line = fs + ' ' + line + '' - - diff_code = match.group(1) - if diff_code == '+': - if state == 'dump': - print '' \ - '' % \ - (cfg.colors.diff_empty, cfg.colors.diff_add, line) - else: - state = 'pre-change-add' - right_col.append(line) - elif diff_code == '-': - state = 'pre-change-remove' - left_col.append(line) - else: - flush_diff_rows(state, left_col, right_col) - print '' % (line, line) - state = 'dump' - left_col = [ ] - right_col = [ ] - - if changes_seen: - flush_diff_rows(state, left_col, right_col) - else: - print '' - print '' % (cfg.colors.diff_empty) - - print '
' - print 'version %s%s' % (rev1, date1) - if sym1: - print '
Tag:', sym1 - print '
' - print 'version %s%s' % (rev2, date2) - if sym2: - print '
Tag:', sym2 - print '
' % cfg.colors.diff_heading - print '' - print '' % \ - (match.group(1), match.group(3)) - print '
Line %s %s
' - print '' - print '' % \ - (match.group(2), match.group(3)) - print '
Line %s %s
 %s
%s%s
 

- No changes -
 


' - print '' - - # format selector - print '' - print '
' - - # print the legend - print '
Legend:
' - print '' - print '' % (cfg.colors.diff_remove, rev1, cfg.colors.diff_empty) - print '' % cfg.colors.diff_change - print '' % (cfg.colors.diff_empty, cfg.colors.diff_add, rev2) - print '
Removed from v.%s 
changed lines
 Added in v.%s
' % request.url - for varname, value in query_dict.items(): - if varname != 'diff_format' and value != default_settings.get(varname): - print '' % \ - (varname, cgi.escape(value)) - print_diff_select(query_dict) - print '
' - html_footer() - - -def flush_diff_rows(state, left_col, right_col): - if state == 'pre-change-remove': - for row in left_col: - print '%s ' % \ - (cfg.colors.diff_remove, row, cfg.colors.diff_empty) - elif state == 'pre-change-add': - for i in range(max(len(left_col), len(right_col))): - if i < len(left_col): - left = '%s' % (cfg.colors.diff_change, left_col[i]) - else: - left = ' ' % cfg.colors.diff_dark_change - if i < len(right_col): - right = '%s' % (cfg.colors.diff_change, right_col[i]) - else: - right = ' ' % cfg.colors.diff_dark_change - print '%s%s' % (left, right) - -def spaced_html_text(text): - text = string.expandtabs(string.rstrip(text)) - - # in the code below, "\x01" will be our stand-in for "&". We don't want - # to insert "&" because it would get escaped by htmlify(). Similarly, - # we use "\x02" as a stand-in for "
" - - if cfg.options.hr_breakable > 1 and len(text) > cfg.options.hr_breakable: - text = re.sub('(' + ('.' * cfg.options.hr_breakable) + ')', - '\\1\x02', - text) - if cfg.options.hr_breakable: - # make every other space "breakable" - text = string.replace(text, ' ', ' \x01nbsp;') - else: - text = string.replace(text, ' ', '\x01nbsp;') - text = htmlify(text) - text = string.replace(text, '\x01', '&') - text = string.replace(text, '\x02', '\
') - return text - -def view_diff(request, cvs_filename): - query_dict = request.query_dict - cvsroot = request.cvsroot - - r1 = query_dict['r1'] - r2 = query_dict['r2'] - - sym1 = sym2 = '' - - if r1 == 'text': - rev1 = query_dict['tr1'] - else: - idx = string.find(r1, ':') - if idx == -1: - rev1 = r1 - else: - rev1 = r1[:idx] - sym1 = r1[idx+1:] - - if r2 == 'text': - rev2 = query_dict['tr2'] - sym2 = '' - else: - idx = string.find(r2, ':') - if idx == -1: - rev2 = r2 - else: - rev2 = r2[:idx] - sym2 = r2[idx+1:] - - if revcmp(rev1, rev2) > 0: - rev1, rev2 = rev2, rev1 - sym1, sym2 = sym2, sym1 - - human_readable = 0 - unified = 0 - - args = [ ] - - format = query_dict['diff_format'] - if format == 'c': - args.append('-c') - diff_name = 'Context diff' - elif format == 's': - args.append('--side-by-side') - args.append('--width=164') - diff_name = 'Side by Side' - elif format == 'H': - args.append('--unified=15') - diff_name = 'Long Human readable' - human_readable = 1 - unified = 1 - elif format == 'h': - args.append('-u') - diff_name = 'Human readable' - human_readable = 1 - unified = 1 - elif format == 'u': - args.append('-u') - diff_name = 'Unidiff' - unified = 1 - else: - error('Diff format %s not understood' % format, '400 Bad arguments') - - if human_readable: - if cfg.options.hr_funout: - args.append('-p') - if cfg.options.hr_ignore_white: - args.append('-w') - if cfg.options.hr_ignore_keyword_subst: - args.append('-kk') - - args[len(args):] = ['-r' + rev1, '-r' + rev2, cvs_filename] - fp = popen.popen(cfg.general.rcs_path + 'rcsdiff', args, 'r') - - if human_readable: - http_header() - human_readable_diff(request, fp, rev1, rev2, sym1, sym2) - sys.exit(0) - - http_header('text/plain') - - if unified: - f1 = '--- ' + cvsroot - f2 = '+++ ' + cvsroot - else: - f1 = '*** ' + cvsroot - f2 = '--- ' + cvsroot - - while 1: - line = fp.readline() - if not line: - break - - if line[:len(f1)] == f1: - line = string.replace(line, cvsroot + '/', '') - if sym1: - line = line[:-1] + ' %s\n' % sym1 - elif line[:len(f2)] == f2: - line = string.replace(line, cvsroot + '/', '') - if sym2: - line = line[:-1] + ' %s\n' % sym2 - - print line[:-1] - -def generate_tarball_header(out, name, size=0, mode=None, mtime=0, uid=0, gid=0, typefrag=None, linkname='', uname='viewcvs', gname='viewcvs', devmajor=1, devminor=0, prefix=None, magic='ustar', version='', chksum=None): - if not mode: - if name[-1:] == '/': - mode = 0755 - else: - mode = 0644 - - if not typefrag: - if name[-1:] == '/': - typefrag = '5' # directory - else: - typefrag = '0' # regular file - - if not prefix: - prefix = '' - - block1 = struct.pack('100s 8s 8s 8s 12s 12s', - name, - '%07o' % mode, - '%07o' % uid, - '%07o' % gid, - '%011o' % size, - '%011o' % mtime) - - block2 = struct.pack('c 100s 6s 2s 32s 32s 8s 8s 155s', - typefrag, - linkname, - magic, - version, - uname, - gname, - '%07o' % devmajor, - '%07o' % devminor, - prefix) - - if not chksum: - dummy_chksum = ' ' - block = block1 + dummy_chksum + block2 - chksum = 0 - for i in range(len(block)): - chksum = chksum + ord(block[i]) - - block = block1 + struct.pack('8s', '%07o' % chksum) + block2 - block = block + '\0' * (512 - len(block)) - - out.write(block) - -def generate_tarball(out, relative, directory, tag, stack=[]): - subdirs = [ ] - rcs_files = [ ] - for file, pathname, isdir in get_file_data(directory): - if pathname == _UNREADABLE_MARKER: - continue - if isdir: - subdirs.append(file) - else: - rcs_files.append(file) - if tag: - for file, pathname, isdir in get_file_data(directory + '/Attic'): - if not isdir and pathname != _UNREADABLE_MARKER: - rcs_files.append('Attic/' + file) - - stack.append(relative + '/') - - fileinfo, alltags = get_logs(directory, rcs_files, tag) - - files = fileinfo.keys() - files.sort(lambda a, b: cmp(os.path.basename(a), os.path.basename(b))) - - for file in files: - info = fileinfo.get(file) - rev = info[0] - date = info[1] - filename = info[4] - state = info[5] - if state == 'dead': - continue - - for dir in stack: - generate_tarball_header(out, dir) - del stack[0:] - - info = os.stat(directory + '/' + file + ',v') - mode = (info[stat.ST_MODE] & 0555) | 0200 - - rev_flag = '-p' + rev - full_name = directory + '/' + file + ',v' - fp = popen.popen(cfg.general.rcs_path + 'co', (rev_flag, full_name), 'r', 0) - contents = fp.read() - status = fp.close() - - generate_tarball_header(out, relative + '/' + os.path.basename(filename), len(contents), mode, date) - out.write(contents) - out.write('\0' * (511 - ((len(contents) + 511) % 512))) - - subdirs.sort() - for subdir in subdirs: - if subdir != 'Attic': - generate_tarball(out, relative + '/' + subdir, directory + '/' + subdir, tag, stack) - - if len(stack): - del stack[-1:] - -def download_tarball(request): - query_dict = request.query_dict - full_name = request.full_name - - directory = re.sub(_re_up_path, '', full_name)[0:-1] - filename = os.path.basename(full_name) - - tag = query_dict.get('only_with_tag') - - http_header('application/octet-stream') - fp = popen.pipe_cmds([('gzip', '-c', '-n')]) - generate_tarball(fp, os.path.basename(directory), directory, tag) - fp.write('\0' * 1024) - fp.close - -def handle_config(): - global cfg - cfg = config.Config() - cfg.set_defaults() - - # load in configuration information from the config file - pathname = CONF_PATHNAME or 'viewcvs.conf' - cfg.load_config(pathname, os.environ.get('HTTP_HOST')) - - global default_settings - default_settings = { - "sortby" : cfg.options.sort_by, - "hideattic" : cfg.options.hide_attic, - "logsort" : cfg.options.log_sort, - "diff_format" : cfg.options.diff_format, - "hidecvsroot" : cfg.options.hide_cvsroot, - "hidenonreadable" : cfg.options.hide_non_readable, - } - - -def main(): - # handle the configuration stuff - handle_config() - - # build a Request object, which contains info about the HTTP request - request = Request() - - # is the CVS root really there? - if not os.path.isdir(request.cvsroot): - error('%s not found!

The server on which the CVS tree lives is ' - 'probably down. Please try again in a few minutes.' % - request.cvsroot) - - full_name = request.full_name - isdir = os.path.isdir(full_name) - - url = request.url - where = request.where - - ### look for GZIP binary - - # if we have a directory and the request didn't end in "/", then redirect - # so that it does. (so that relative URLs in our output work right) - if isdir and os.environ.get('PATH_INFO', '')[-1:] != '/': - redirect(url + '/' + request.qmark_query) - - # check the forbidden list - if cfg.is_forbidden(request.module): - error('Access to "%s" is forbidden.' % request.module, '403 Forbidden') - - if isdir: - view_directory(request) - return - - # since we aren't talking about a directory, set up the mime type info - # for the file. - request.setup_mime_type_info() - - query_dict = request.query_dict - - if os.path.isfile(full_name + ',v'): - if query_dict.has_key('rev') or request.has_checkout_magic: - view_checkout(request) - elif query_dict.has_key('annotate') and cfg.options.allow_annotate: - view_annotate(request) - elif query_dict.has_key('r1') and query_dict.has_key('r2'): - view_diff(request, full_name) - else: - view_log(request) - elif full_name[-5:] == '.diff' and os.path.isfile(full_name[:-5] + ',v') \ - and query_dict.has_key('r1') and query_dict.has_key('r2'): - view_diff(request, full_name[:-5]) - elif cfg.options.allow_tar \ - and full_name[-7:] == '.tar.gz' and query_dict.has_key('tarball'): - download_tarball(request) - else: - # if the file is in the Attic, then redirect - idx = string.rfind(full_name, '/') - attic_name = full_name[:idx] + '/Attic' + full_name[idx:] + ',v' - if os.path.isfile(attic_name): - idx = string.rfind(url, '/') - redirect(url[:idx] + '/Attic' + url[idx:]) - - error('%s: unknown location' % request.url, '404 Not Found') - - -try: - main() -except SystemExit, e: - # don't catch SystemExit (caused by sys.exit()). propagate the exit code - sys.exit(e[0]) -except: - info = sys.exc_info() - html_header('Python Exception Occurred') - import traceback - lines = apply(traceback.format_exception, info) - print '

'
-  print cgi.escape(string.join(lines, ''))
-  print '
' - html_footer() diff --git a/lib/viewcvs.py b/lib/viewcvs.py new file mode 100644 index 00000000..09eb04a6 --- /dev/null +++ b/lib/viewcvs.py @@ -0,0 +1,2443 @@ + +checkout_magic_path = '~checkout~' +viewcvs_mime_type = 'text/vnd.viewcvs-markup' + +# put here the variables we need in order to hold our state - they will be +# added (with their current value) to any link/query string you construct +_sticky_vars = ( + 'cvsroot', + 'hideattic', + 'sortby', + 'logsort', + 'diff_format', + 'only_with_tag' + ) + +# regex used to move from a file to a directory +_re_up_path = re.compile('(Attic/)?[^/]+$') + +_EOF_FILE = 'end of file entries' # no more entries for this RCS file +_EOF_LOG = 'end of log' # hit the true EOF on the pipe +_EOF_ERROR = 'error message found' # rlog issued an error + +_FILE_HAD_ERROR = 'could not read file' + +_UNREADABLE_MARKER = '//UNREADABLE-MARKER//' + +header_comment = '''\ + +''' + +# for reading/writing between a couple descriptors +CHUNK_SIZE = 8192 + +# if your rlog doesn't use 77 '=' characters, then this must change +LOG_END_MARKER = '=' * 77 + '\n' +ENTRY_END_MARKER = '-' * 28 + '\n' + + +class Request: + def __init__(self): + where = os.environ.get('PATH_INFO', '') + + # clean it up. this removes duplicate '/' characters and any that may + # exist at the front or end of the path. + parts = filter(None, string.split(where, '/')) + + # does it have the magic checkout prefix? + if parts and parts[0] == checkout_magic_path: + self.has_checkout_magic = 1 + del parts[0] + else: + self.has_checkout_magic = 0 + + # put it back together + where = string.join(parts, '/') + + script_name = os.environ['SCRIPT_NAME'] ### clean this up? + if where: + url = script_name + '/' + urllib.quote(where) + else: + url = script_name + + self.where = where + self.script_name = script_name + self.url = url + if parts: + self.module = parts[0] + else: + self.module = None + + self.browser = os.environ.get('HTTP_USER_AGENT', 'unknown') + + # in lynx, it it very annoying to have two links + # per file, so disable the link at the icon + # in this case: + self.no_file_links = string.find(self.browser, 'Lynx') != -1 + + # newer browsers accept gzip content encoding + # and state this in a header + # (netscape did always but didn't state it) + # It has been reported that these + # braindamaged MS-Internet Explorers claim that they + # accept gzip .. but don't in fact and + # display garbage then :-/ + self.may_compress = ( + ( string.find(os.environ.get('HTTP_ACCEPT_ENCODING', ''), 'gzip') != -1 + or string.find(self.browser, 'Mozilla/3') != -1) + and string.find(self.browser, 'MSIE') == -1 + ) + + # parse the query params into a dictionary (and use defaults) + query_dict = default_settings.copy() + for name, values in cgi.parse().items(): + query_dict[name] = values[0] + + # set up query strings, prefixed by question marks and ampersands + query = sticky_query(query_dict) + if query: + self.qmark_query = '?' + query + self.amp_query = '&' + query + else: + self.qmark_query = '' + self.amp_query = '' + + self.query_dict = query_dict + + # set up the CVS repository to use + self.cvsrep = query_dict.get('cvsroot', cfg.general.default_root) + self.cvsroot = cfg.general.cvs_roots[self.cvsrep] + + self.full_name = self.cvsroot + '/' + where + + def setup_mime_type_info(self): + if cfg.general.mime_types_file: + mimetypes.init([cfg.general.mime_types_file]) + self.mime_type, self.encoding = mimetypes.guess_type(self.where) + if not self.mime_type: + self.mime_type = 'text/plain' + self.default_text_plain = self.mime_type == 'text/plain' + self.default_viewable = cfg.options.allow_markup and \ + is_viewable(self.mime_type) + + +class LogHeader: + "Hold state from the header portion of an 'rlog' output." + def __init__(self, filename, head=None, branch=None, taginfo=None): + self.filename = filename + self.head = head + self.branch = branch + self.taginfo = taginfo + + +class LogEntry: + "Hold state for each revision entry in an 'rlog' output." + def __init__(self, rev, date, author, state, changed, log): + self.rev = rev + self.date = date + self.author = author + self.state = state + self.changed = changed + self.log = log + + +def redirect(location): + print 'Status: 301 Moved' + print 'Location:', location + print + print 'This document is located here.' % location + sys.exit(0) + +def error(msg, status='500 Internal Server Error'): + print 'Status:', status + print + print msg + sys.exit(0) + +_header_sent = 0 +def http_header(content_type='text/html'): + global _header_sent + if _header_sent: + return + print 'Content-Type:', content_type + print + _header_sent = 1 + +def html_header(title): + http_header() + logo = cfg.images.get_image('logo') + print '''\ + + +%s +%s + + + +

%s

%s
+''' % (header_comment, title, cfg.colors.text, cfg.colors.background, + title, logo) + +def html_footer(): + print '
' + print '' % cfg.general.address + print '
%s
ViewCVS %s
' % __version__ + print 'by Greg Stein' + print '
' + print '' + +def sticky_query(dict): + sticky_dict = { } + for varname in _sticky_vars: + value = dict.get(varname) + if value is not None and value != default_settings.get(varname, ''): + sticky_dict[varname] = value + return compat.urlencode(sticky_dict) + +def toggle_query(query_dict, which, value=None): + dict = query_dict.copy() + if value is None: + dict[which] = not dict[which] + else: + dict[which] = value + query = sticky_query(dict) + if query: + return '?' + query + return '' + +def clickable_path(request, path, leaf_is_link, leaf_is_file, drop_leaf): + if path == '/': + # this should never happen - chooseCVSRoot() is + # intended to do this + return '[%s]' % cvsrep + + s = '[%s]' % \ + (request.script_name, request.qmark_query, request.cvsrep) + parts = filter(None, string.split(path, '/')) + if drop_leaf: + del parts[-1] + leaf_is_link = 1 + leaf_is_file = 0 + where = '' + for i in range(len(parts)): + where = where + '/' + parts[i] + is_leaf = i == len(parts) - 1 + if not is_leaf or leaf_is_link: + if is_leaf and leaf_is_file: + slash = '' + else: + slash = '/' + ### should we be encoding/quoting the URL stuff? (probably...) + s = s + ' / %s' % \ + (request.script_name, where, slash, request.qmark_query, parts[i]) + else: + s = s + ' / ' + parts[i] + + return s + +def html_link(contents, link): + return '%s' % (link, contents) + +def link_tags(query_dict, where, text, add_links): + if not add_links: + return text + + filename = os.path.basename(where) + file_url = urllib.quote(filename) + links = [ ] + for tag in string.split(text, ', '): + links.append('%s' % + (file_url, + toggle_query(query_dict, 'only_with_tag', tag), + tag)) + return string.join(links, ', ') + +def is_viewable(mime_type): + return mime_type[:5] == 'text/' or mime_type[:6] == 'image/' + +_re_rewrite_url = re.compile('((http|ftp)(://[-a-zA-Z0-9%.~:_/]+)([?&]([-a-zA-Z0-9%.~:_]+)=([-a-zA-Z0-9%.~:_])+)*(#([-a-zA-Z0-9%.~:_]+)?)?)') +_re_rewrite_email = re.compile('([-a-zA-Z0-9_.]+@([-a-zA-Z0-9]+\.)+[A-Za-z]{2,4})') +def htmlify(html): + html = cgi.escape(html) + html = re.sub(_re_rewrite_url, r'\1', html) + html = re.sub(_re_rewrite_email, r'\1', html) + return html + +def html_log(log): + print ' ' + htmlify(log[:cfg.options.short_log_len]) + if len(log) > cfg.options.short_log_len: + print '...' + print '' + +def download_url(request, url, revision, mime_type): + if cfg.options.checkout_magic and mime_type != viewcvs_mime_type: + url = '%s/%s/%s/%s' % \ + (request.script_name, checkout_magic_path, + os.path.dirname(request.where), url) + + url = url + '?rev=' + revision + if mime_type: + return url + '&content-type=' + mime_type + return url + +def download_link(request, url, revision, text, mime_type=None): + full_url = download_url(request, url, revision, mime_type) + paren = text[0] == '(' + + lparen = rparen = '' + if paren: + lparen = '(' + rparen = ')' + text = text[1:-1] + + print '%s%s%s' % (text, rparen) + +def html_icon(which): + return cfg.images.get_image(which + '_icon') + +def plural(num, text): + if num == 1: + return '1 ' + text + if num: + return '%d %ss' % (num, text) + return '' + +_time_desc = { + 1 : 'second', + 60 : 'minute', + 3600 : 'hour', + 86400 : 'day', + 604800 : 'week', + 2628000 : 'month', + 31536000 : 'year', + } +def html_time(secs, extended=0): + secs = long(time.time()) - secs + if secs < 2: + return 'very little time' + breaks = _time_desc.keys() + breaks.sort() + i = 0 + while i < len(breaks): + if secs < 2 * breaks[i]: + break + i = i + 1 + value = breaks[i - 1] + s = plural(secs / value, _time_desc[value]) + + if extended and i > 1: + secs = secs % value + value = breaks[i - 2] + ext = plural(secs / value, _time_desc[value]) + if ext: + s = s + ', ' + ext + return s + +def html_option(value, cur_value, text=None): + if text is None: + attr = '' + text = value + else: + attr = 'value="%s"' % value + + if value == cur_value: + print '' % (attr, text) + else: + print '' % (attr, text) + +def print_diff_select(query_dict): + print '' + +def navigate_header(request, swhere, path, filename, rev, title): + if swhere == request.url: + swhere = urllib.quote(filename) + + print '' + print header_comment + print '%s/%s - %s - %s' % (path, filename, title, rev) + print '' % cfg.colors.alt_background + print '' % cfg.colors.nav_header + print '' % \ + (html_link(filename, + '%s%s#rev%s' % (swhere, request.qmark_query, rev)), + html_icon('file')) + print '' % \ + (html_icon('dir'), clickable_path(request, path, 1, 0, 0)) + print '
' + print '%s' % \ + (swhere, request.qmark_query, rev, html_icon('back')) + print 'Return to %s CVS log %s%s Up to %s
' + +def markup_stream_default(fp): + print '
'
+  while 1:
+    ### technically, the htmlify() could fail if something falls across
+    ### the chunk boundary. TFB.
+    chunk = fp.read(CHUNK_SIZE)
+    if not chunk:
+      break
+    sys.stdout.write(htmlify(chunk))
+  print '
' + +def markup_stream_python(fp): + try: + # see if Marc-Andre Lemburg's py2html stuff is around + # http://starship.python.net/crew/lemburg/SoftwareDescriptions.html#py2html.py + ### maybe restrict the import to *only* this directory? + sys.path.insert(0, cfg.options.py2html_path) + import py2html + import PyFontify + except ImportError: + # fall back to the default streamer + markup_stream_default(fp) + else: + ### it doesn't escape stuff quite right, nor does it munge URLs and + ### mailtos as well as we do. + html = cgi.escape(fp.read()) + pp = py2html.PrettyPrint(PyFontify.fontify, "rawhtml", "color") + html = pp.fontify(html) + html = re.sub(_re_rewrite_url, r'\1', html) + html = re.sub(_re_rewrite_email, r'\1', html) + sys.stdout.write(html) + +def markup_stream_enscript(lang, fp): + sys.stdout.flush() + enscript = popen.pipe_cmds([(cfg.options.enscript_path + 'enscript', + '--color', '-W', 'html', '-E' + lang, '-o', + '-', '-'), + ('sed', '-n', '/^
$/,/<\\/PRE>$/p')])
+
+  while 1:
+    chunk = fp.read(CHUNK_SIZE)
+    if not chunk:
+      if fp.eof() is None:
+        time.sleep(1)
+        continue
+      break
+    enscript.write(chunk)
+
+  enscript.close()
+
+markup_streamers = {
+#  '.py' : markup_stream_python,
+  }
+
+### this sucks... we have to duplicate the extensions defined by enscript
+enscript_extensions = {
+  '.c' : 'c',
+  '.h' : 'c',
+  '.c++' : 'cpp',
+  '.C' : 'cpp',
+  '.H' : 'cpp',
+  '.cpp' : 'cpp',
+  '.cc' : 'cpp',
+  '.cxx' : 'cpp',
+  '.m' : 'objc',
+  '.scm' : 'scheme',
+  '.scheme' : 'scheme',
+  '.el' : 'elisp',
+  '.ada' : 'ada',
+  '.adb' : 'ada',
+  '.ads' : 'ada',
+  '.s' : 'asm',
+  '.S' : 'asm',
+  '.st' : 'states',
+  '.tcl' : 'tcl',
+  '.v' : 'verilog',
+  '.vh' : 'verilog',
+  '.htm' : 'html',
+  '.html' : 'html',
+  '.shtml' : 'html',
+  '.vhd' : 'vhdl',
+  '.vhdl' : 'vhdl',
+  '.scr' : 'synopsys',
+  '.syn' : 'synopsys',
+  '.synth' : 'synopsys',
+  '.idl' : 'idl',
+  '.hs' : 'haskell',
+  '.lhs' : 'haskell',
+  '.gs' : 'haskell',
+  '.lgs' : 'haskell',
+  '.pm' : 'perl',
+  '.pl' : 'perl',
+  '.eps' : 'postscript',
+  '.EPS' : 'postscript',
+  '.ps' : 'postscript',
+  '.PS' : 'postscript',
+  '.js' : 'javascript',
+  '.java' : 'java',
+  '.pas' : 'pascal',
+  '.pp' : 'pascal',
+  '.p' : 'pascal',
+  '.f' : 'fortran',
+  '.F' : 'fortran',
+  '.awk' : 'awk',
+  '.sh' : 'sh',
+  '.vba' : 'vba',
+
+  ### use enscript or py2html?
+  '.py' : 'python',
+  }
+enscript_filenames = {
+  '.emacs' : 'elisp',
+  'Makefile' : 'makefile',
+  'makefile' : 'makefile',
+  }
+
+def markup_stream(request, fp, revision, mime_type):
+  full_name = request.full_name
+  where = request.where
+  query_dict = request.query_dict
+
+  pathname, filename = os.path.split(where)
+  if pathname[-6:] == '/Attic':
+    pathname = pathname[:-6]
+  file_url = urllib.quote(filename)
+
+  http_header()
+  navigate_header(request, request.url, pathname, filename, revision, 'view')
+  print '
' + print '
' % cfg.colors.markup_log + print 'File:', clickable_path(request, where, 1, 1, 0), '' + download_link(request, file_url, revision, '(download)') + if not request.default_text_plain: + download_link(request, file_url, revision, '(as text)', 'text/plain') + print '
' + if cfg.options.show_log_in_markup: + show_revs, rev_map, rev_order, taginfo, rev2tag, \ + cur_branch, branch_points, branch_names = read_log(full_name) + + print_log(request, rev_map, rev_order, rev_map[revision], rev2tag, + branch_points, 0) + else: + print 'Version: %s
' % revision + tag = query_dict.get('only_with_tag') + if tag: + print 'Tag: %s
' % tag + print '
' + + print '
' + if mime_type[:6] == 'image/': + url = download_url(request, file_url, revision, mime_type) + print '
' % (url, request.amp_query) + else: + basename, ext = os.path.splitext(filename) + streamer = markup_streamers.get(ext) + if streamer: + streamer(fp) + elif not cfg.options.use_enscript: + markup_stream_default(fp) + else: + lang = enscript_extensions.get(ext) + if not lang: + lang = enscript_filenames.get(basename) + if lang and lang not in cfg.options.disable_enscript_lang: + markup_stream_enscript(lang, fp) + else: + markup_stream_default(fp) + status = fp.close() + if status: + raise error, 'pipe error status: %d' % status + html_footer() + +def get_file_data(full_name): + """Return a sequence of tuples containing various data about the files. + + data[0] = (relative) filename + data[1] = full pathname + data[2] = is_directory (0/1) + + Only RCS files (*,v) and subdirs are returned. + """ + + files = os.listdir(full_name) + data = [ ] + + uid = os.getuid() + gid = os.getgid() + + for file in files: + pathname = full_name + '/' + file + try: + info = os.stat(pathname) + except os.error: + data.append((file, _UNREADABLE_MARKER, None)) + continue + mode = info[stat.ST_MODE] + isdir = stat.S_ISDIR(mode) + isreg = stat.S_ISREG(mode) + if (isreg and file[-2:] == ',v') or isdir: + # + # Quick version of access() where we use existing stat() data. + # + # This might not be perfect -- the OS may return slightly different + # results for some bizarre reason. However, we make a good show of + # "can I read this file/dir?" by checking the various perm bits. + # + # NOTE: if the UID matches, then we must match the user bits -- we + # cannot defer to group or other bits. Similarly, if the GID matches, + # then we must have read access in the group bits. + # + if isdir: + mask = stat.S_IROTH | stat.S_IXOTH + else: + mask = stat.S_IROTH + + valid = 1 + if info[stat.ST_UID] == uid: + if ((mode >> 6) & mask) != mask: + valid = 0 + elif info[stat.ST_GID] == gid: + if ((mode >> 3) & mask) != mask: + valid = 0 + elif (mode & mask) != mask: + valid = 0 + + if valid: + data.append((file, pathname, isdir)) + else: + data.append((file, _UNREADABLE_MARKER, isdir)) + + return data + +def get_last_modified(file_data): + """Return mapping of subdir to info about the most recently modified subfile. + + key = subdir + data[0] = "subdir/subfile" of the most recently modified subfile + data[1] = the mod time of that file (time_t) + """ + + lastmod = { } + for file, pathname, isdir in file_data: + if not isdir or pathname == _UNREADABLE_MARKER: + continue + if file == 'Attic': + continue + + subfiles = os.listdir(pathname) + latest = ('', 0) + for subfile in subfiles: + ### filter CVS locks? stale NFS handles? + if subfile[-2:] != ',v': + continue + subpath = pathname + '/' + subfile + info = os.stat(subpath) + if not stat.S_ISREG(info[stat.ST_MODE]): + continue + if info[stat.ST_MTIME] > latest[1]: + latest = (file + '/' + subfile, info[stat.ST_MTIME]) + if latest[0]: + lastmod[file] = latest + return lastmod + +def parse_log_header(fp): + """Parse and RCS/CVS log header. + + fp is a file (pipe) opened for reading the log information. + + On entry, fp should point to the start of a log entry. + On exit, fp will have consumed the separator line between the header and + the first revision log. + + If there is no revision information (e.g. the "-h" switch was passed to + rlog), then fp will consumed the file separator line on exit. + """ + filename = head = branch = None + taginfo = { } # tag name => revision + + parsing_tags = 0 + eof = None + + while 1: + line = fp.readline() + if not line: + # the true end-of-file + eof = _EOF_LOG + break + + if parsing_tags: + if line[0] == '\t': + [ tag, rev ] = map(string.strip, string.split(line, ':')) + taginfo[tag] = rev + else: + # oops. this line isn't tag info. stop parsing tags. + parsing_tags = 0 + + if not parsing_tags: + if line[:9] == 'RCS file:': + # remove the trailing ,v + filename = line[10:-3] + elif line[:5] == 'head:': + head = line[6:-1] + elif line[:7] == 'branch:': + branch = line[8:-1] + elif line[:14] == 'symbolic names': + # start parsing the tag information + parsing_tags = 1 + elif line == ENTRY_END_MARKER: + # end of the headers + break + elif line == LOG_END_MARKER: + # end of this file's log information + eof = _EOF_FILE + break + elif line[:6] == 'rlog: ': + # rlog: filename/goes/here,v: error message + idx = string.find(line, ':', 6) + if idx != -1: + if line[idx:idx+32] == ': warning: Unknown phrases like ': + # don't worry about this warning. it can happen with some RCS + # files that have unknown fields in them (e.g. "permissions 644;" + continue + + # looks like a filename + filename = line[6:idx] + if filename[-2:] == ',v': + filename = filename[:-2] + return LogHeader(filename), _EOF_ERROR + # dunno what this is + + return LogHeader(filename, head, branch, taginfo), eof + +_re_date_author = re.compile(r'^date:\s+([^;]+);\s+author:\s+([^;]+);.*') +_re_log_info = re.compile(r'^date:\s+([^;]+);' + r'\s+author:\s+([^;]+);' + r'\s+state:\s+([^;]+);' + r'(\s+lines:\s+([0-9\s+-]+))?\n$') +### _re_rev should be updated to extract the "locked" flag +_re_rev = re.compile(r'^revision\s+([0-9.]+).*') +def parse_log_entry(fp): + """Parse a single log entry. + + On entry, fp should point to the first line of the entry (the "revision" + line). + On exit, fp will have consumed the log separator line (dashes) or the + end-of-file marker (equals). + + Returns: revision, date (time_t secs), author, state, lines changed, + the log text, and eof flag (see _EOF_*) + """ + rev = None + line = fp.readline() + if not line: + return None, _EOF_LOG + if line[:8] == 'revision': + match = _re_rev.match(line) + if not match: + return None, _EOF_LOG + rev = match.group(1) + + line = fp.readline() + if not line: + return None, _EOF_LOG + match = _re_log_info.match(line) + + eof = None + log = '' + while 1: + line = fp.readline() + if not line: + # true end-of-file + eof = _EOF_LOG + break + if line[:9] == 'branches:': + continue + if line == ENTRY_END_MARKER: + break + if line == LOG_END_MARKER: + # end of this file's log information + eof = _EOF_FILE + break + + log = log + line + + if not rev or not match: + # there was a parsing error + return None, eof + + date = int(time.mktime(compat.cvs_strptime(match.group(1)))) - time.timezone + + return LogEntry(rev, date, + # author, state, lines changed + match.group(2), match.group(3), match.group(5), + log), eof + +def skip_file(fp): + "Skip the rest of a file's log information." + while 1: + line = fp.readline() + if not line: + break + if line == LOG_END_MARKER: + break + +def process_rlog_output(rlog, full_name, view_tag, fileinfo, alltags): + "Fill in fileinfo and alltags with info from the rlog output." + + # consume each file found in the resulting log + while 1: + + revwanted = None + branch = None + branchpoint = None + + header, eof = parse_log_header(rlog) + filename = header.filename + head = header.head + branch = header.branch + symrev = header.taginfo + + # the rlog output is done + if eof == _EOF_LOG: + break + + if filename: + # convert from absolute to relative + if filename[:len(full_name)] == full_name: + filename = filename[len(full_name)+1:] + + # for a subdir (not Attic files!), use the subdir for a key + idx = string.find(filename, '/') + if idx != -1 and filename[:6] != 'Attic/': + info_key = filename[:idx] + else: + info_key = filename + + # an error was found regarding this file + if eof == _EOF_ERROR: + fileinfo[info_key] = _FILE_HAD_ERROR + continue + + # if we hit the end of the log information (already!), then there is + # nothing we can do with this file + if eof: + continue + + if not filename or not head: + # parsing error. skip the rest of this file. + skip_file(rlog) + continue + + if not branch: + idx = string.rfind(head, '.') + branch = head[:idx] + idx = string.rfind(branch, '.') + if idx == -1: + branch = '0.' + branch + else: + branch = branch[:idx] + '.0' + branch[idx:] + + symrev['MAIN'] = symrev['HEAD'] = branch + + if symrev.has_key(view_tag): + revwanted = symrev[view_tag] + if revwanted[:2] == '0.': ### possible? + branch = revwanted[2:] + else: + idx = string.find(revwanted, '.0.') + if idx == -1: + branch = revwanted + else: + branch = revwanted[:idx] + revwanted[idx+2:] + if revwanted != branch: + revwanted = None + + idx = string.rfind(branch, '.') + if idx == -1: + branchpoint = '' + else: + branchpoint = branch[:idx] + + elif view_tag: + # the tag wasn't found, so skip this file + skip_file(rlog) + continue + + # we don't care about the values -- just the keys. this the fastest + # way to merge the set of keys + alltags.update(symrev) + + # read all of the log entries until we find the revision we want + while 1: + + # fetch one of the log entries + entry, eof = parse_log_entry(rlog) + + if not entry: + # parsing error + if not eof: + skip_file(rlog) + break + + rev = entry.rev + + idx = string.rfind(rev, '.') + revbranch = rev[:idx] + + if not view_tag or (not revwanted and branch == revbranch): + revwanted = rev + + if rev == revwanted or rev == branchpoint: + fileinfo[info_key] = (rev, entry.date, entry.log, entry.author, + filename, entry.state) + + if rev == revwanted: + # done with this file now + if not eof: + skip_file(rlog) + break + + # if we hit the true EOF, or just this file's end-of-info, then we are + # done collecting log entries. + if eof: + break + +def get_logs(full_name, files, view_tag): + + if len(files) == 0: + return { }, [ ] + + fileinfo = { } + alltags = { # all the tags seen in the files of this dir + 'MAIN' : '1', + 'HEAD' : '1', + } + + chunk_size = 100 + while files: + chunk = files[:chunk_size] + del files[:chunk_size] + + # prepend the full pathname for each file + for i in range(len(chunk)): + chunk[i] = full_name + '/' + chunk[i] + + if view_tag: + # NOTE: can't pass tag on command line since a tag may contain "-" + # we'll search the output for the appropriate revision + rlog = popen.popen(cfg.general.rcs_path + 'rlog', chunk, 'r') + else: + # fetch the latest revision on the default branch + chunk = ('-r',) + tuple(chunk) + rlog = popen.popen(cfg.general.rcs_path + 'rlog', chunk, 'r') + + process_rlog_output(rlog, full_name, view_tag, fileinfo, alltags) + + ### it would be nice to verify that we got SOMETHING from rlog about + ### each file. if we didn't, then it could be that the chunk is still + ### too large, so we want to cut the chunk_size in half and try again. + ### + ### BUT: if we didn't get feedback for some *other* reason, then halving + ### the chunk size could merely send us into a needless retry loop. + ### + ### more work for later... + + status = rlog.close() + if status: + raise 'error during rlog: '+hex(status) + + return fileinfo, alltags + +def revcmp(rev1, rev2): + rev1 = map(int, string.split(rev1, '.')) + rev2 = map(int, string.split(rev2, '.')) + return cmp(rev1, rev2) + +def print_roots(current_root): + if len(cfg.general.cvs_roots) < 2: + return + print '

Project Root

' + print '
' + print '
' + +def view_directory(request): + full_name = request.full_name + where = request.where + query_dict = request.query_dict + + view_tag = query_dict.get('only_with_tag') + hideattic = int(query_dict.get('hideattic')) ### watch for errors in int()? + sortby = query_dict.get('sortby', 'file') + + file_data = get_file_data(full_name) + + if cfg.options.show_subdir_lastmod: + lastmod = get_last_modified(file_data) + else: + lastmod = { } + if cfg.options.show_logs: + subfiles = map(lambda (subfile, mtime): subfile, lastmod.values()) + else: + subfiles = [ ] + + attic_files = [ ] + if not hideattic or view_tag: + try: + attic_files = os.listdir(full_name + '/Attic') + except os.error: + pass + else: + ### filter for just RCS files? + attic_files = map(lambda file: 'Attic/' + file, attic_files) + + # get all the required info + rcs_files = subfiles + attic_files + for file, pathname, isdir in file_data: + if not isdir and pathname != _UNREADABLE_MARKER: + rcs_files.append(file) + fileinfo, alltags = get_logs(full_name, rcs_files, view_tag) + + # append the Attic files into the file_data now + # NOTE: we only insert the filename and isdir==0 + for file in attic_files: + file_data.append((file, None, 0)) + + if where == '': + html_header(cfg.general.main_title) + + # these may be commented out or altered in the configuration section + print cfg.text.long_intro + print cfg.text.doc_info + print cfg.text.repository_info + + print_roots(request.cvsrep) + else: + html_header(where) + print cfg.text.short_intro + + print '

' + + if where == '': + #choose_mirror() + #choose_cvsroot() + pass + else: + print '

Current directory: ', \ + clickable_path(request, where, 0, 0, 0), '' + if view_tag: + print '

Current tag: ', view_tag, '' + + print '


' + + num_cols = 0 + + if cfg.colors.table_border: + print '' \ + '
' % cfg.colors.table_border + print '' % cfg.options.table_padding + + def print_header(title, which, sortby=sortby, query_dict=query_dict): + if sortby == which: + print '' % \ + (cfg.colors.column_header_sorted, title) + else: + query = toggle_query(query_dict, 'sortby', which) + print '' % \ + (cfg.colors.column_header_normal, query, title) + + print '' + num_cols = 1 + print_header('File', 'file') + + # fileinfo will be len==0 if we only have dirs and !show_subdir_lastmod + # in that case, we don't need the extra columns + if len(fileinfo): + num_cols = 3 + print_header('Rev.', 'rev') + print_header('Age', 'date') + if cfg.options.show_author: + num_cols = 4 + print_header('Author', 'author') + if cfg.options.show_logs: + num_cols = num_cols + 1 + print_header('Last log entry', 'log') + print '' + + def file_sort_cmp(data1, data2, sortby=sortby, fileinfo=fileinfo): + if data1[2]: # is_directory + if data2[2]: + # both are directories. sort on name. + return cmp(data1[0], data2[0]) + # data1 is a directory, it sorts first. + return -1 + if data2[2]: + # data2 is a directory, it sorts first. + return 1 + + # the two files should be RCS files. drop the ",v" from the end. + file1 = data1[0][:-2] + file2 = data2[0][:-2] + + # we should have data on these. if not, then it is because we requested + # a specific tag and that tag is not present on the file. + info1 = fileinfo.get(file1) + if info1 == _FILE_HAD_ERROR: + info1 = None + info2 = fileinfo.get(file2) + if info2 == _FILE_HAD_ERROR: + info2 = None + if info1 and info2: + if sortby == 'rev': + result = revcmp(info1[0], info2[0]) + elif sortby == 'date': + result = cmp(info2[1], info1[1]) # latest date is first + elif sortby == 'log': + result = cmp(info1[2], info2[2]) + elif sortby == 'author': + result = cmp(info1[3], info2[3]) + else: + # sortby == 'file' ... fall thru + result = 0 + + # return for unequal values; or fall thru for secondary-sort on name + if result: + return result + + # sort by file name + if file1[:6] == 'Attic/': + file1 = file1[6:] + if file2[:6] == 'Attic/': + file2 = file2[6:] + return cmp(file1, file2) + + # sort with directories first, and using the "sortby" criteria + file_data.sort(file_sort_cmp) + + cur_row = 0 + num_files = 0 + num_displayed = 0 + unreadable = 0 + + attic_toggle_link = '[Hide]' % \ + toggle_query(query_dict, 'hideattic', 1) + + ### display a row for ".." ? + + for file, pathname, isdir in file_data: + + if pathname == _UNREADABLE_MARKER: + if isdir is None: + # We couldn't even stat() the file to figure out what it is. + slash = '' + elif isdir: + slash = '/' + else: + slash = '' + file = file[:-2] # strip the ,v + num_displayed = num_displayed + 1 + print '' % \ + (cfg.colors.even_odd[cur_row % 2], file, file, slash) + print '' % \ + (num_cols - 1) + print '' + cur_row = cur_row + 1 + unreadable = 1 + continue + + if isdir: + if not hideattic and file == 'Attic': + continue + if where == '' and ((file == 'CVSROOT' and cfg.options.hide_cvsroot) + or cfg.is_forbidden(file)): + continue + if file == 'CVS': # CVS directory in a repository is used for fileattr. + continue + + print '' + + else: + # remove the ",v" + file = file[:-2] + + num_files = num_files + 1 + info = fileinfo.get(file) + if info == _FILE_HAD_ERROR: + print '' % \ + (cfg.colors.even_odd[cur_row % 2], file, file) + print '' % \ + (num_cols - 1) + print '' + cur_row = cur_row + 1 + num_displayed = num_displayed + 1 + unreadable = 1 + continue + elif not info: + continue + elif hideattic and view_tag and info[5] == 'dead': + continue + num_displayed = num_displayed + 1 + + file_url = urllib.quote(file) + url = file_url + request.qmark_query + + if view_tag: + if info[5] == 'dead': + attic = ' (not exist) ' + attic_toggle_link + else: + attic = '' + else: + if file[:6] == 'Attic/': + attic = ' (in the Attic) ' + attic_toggle_link + else: + attic = '' + + if file[:6] == 'Attic/': + file = file[6:] + + print '' + + cur_row = cur_row + 1 + + if cfg.colors.table_border: + print '
%s' \ + '%s' \ + '
%s%sunreadable
' % cfg.colors.even_odd[cur_row % 2] + url = urllib.quote(file) + '/' + request.qmark_query + print '' % file + if request.no_file_links: + print html_icon('dir') + else: + print html_link(html_icon('dir'), url) + print html_link(file + '/', url) + if file == 'Attic': + print '  [Don\'t hide]' % \ + toggle_query(query_dict, 'hideattic', 0) + + info = fileinfo.get(file) + if info == _FILE_HAD_ERROR: + print 'CVS information is unreadable' % \ + (num_cols - 1) + cur_row = cur_row + 1 + unreadable = 1 + elif info: + print '  ' + print html_time(info[1]) + if cfg.options.show_author: + print ' ' + print info[3] + if cfg.options.show_logs: + print ' ' + subfile = info[4] + idx = string.find(subfile, '/') + print '%s/%s' % (subfile[idx+1:], info[0]) + print '
' + if info[2]: + html_log(info[2]) + else: + for i in range(1, num_cols): + print '
 ' + + print '
%sCVS information is unreadable
' % cfg.colors.even_odd[cur_row % 2] + print '' % file + + if request.no_file_links: + print html_icon('file') + else: + print html_link(html_icon('file'), url) + print html_link(file, url), attic + + print ' ' + if cfg.options.allow_markup: + download_link(request, file_url, info[0], info[0], viewcvs_mime_type) + else: + download_link(request, file_url, info[0], info[0]) + print ' ' + print html_time(info[1]) + if cfg.options.show_author: + print ' ' + print info[3] + if cfg.options.show_logs: + print '' + html_log(info[2]) + + print '
' + print '
' + + if num_files and not num_displayed: + print '

NOTE: There are %d files, but none match the current ' \ + 'tag (%s)' % (num_files, view_tag) + if unreadable: + print '


NOTE: One or more files were ' \ + 'unreadable. The files in the CVS repository should be readable ' \ + 'by the web server process. Please report this condition to the ' \ + 'administrator of this CVS repository.' + + if alltags or view_tag: + print '
' + print '
' + for varname in _sticky_vars: + value = query_dict.get(varname, '') + if value != '' and value != default_settings.get(varname, '') and \ + varname != 'only_with_tag': + print '' % \ + (varname, query_dict[varname]) + + alltagnames = alltags.keys() + alltagnames.sort(lambda t1, t2: cmp(string.lower(t1), string.lower(t2))) + alltagnames.reverse() + branchtags = [] + nonbranchtags = [] + for tag in alltagnames: + rev = alltags[tag] + if string.find(rev, '.0.') == -1: + nonbranchtags.append(tag) + else: + branchtags.append(tag) + + print 'Show only files with tag:' + print '
' + + if cfg.options.allow_tar: + url = os.path.basename(where) + '.tar.gz?tarball=1' + query = sticky_query(query_dict) + if query: + url = url + '&' + query + print html_link("Download tarball", url) + + html_footer() + +def fetch_log(full_name, which_rev=None): + if which_rev: + args = ('-r' + which_rev, full_name) + else: + args = (full_name,) + rlog = popen.popen(cfg.general.rcs_path + 'rlog', args, 'r') + + header, eof = parse_log_header(rlog) + filename = header.filename + head = header.head + branch = header.branch + taginfo = header.taginfo + + if eof: + # no log entries or a parsing failure + return head, branch, taginfo, { } + + revs = [ ] + while 1: + entry, eof = parse_log_entry(rlog) + if entry: + # valid revision info + revs.append(entry) + if eof: + break + + return head, branch, taginfo, revs + +def logsort_date_cmp(rev1, rev2): + # sort on date; secondary on revision number + return -cmp(rev1.date, rev2.date) or -revcmp(rev1.rev, rev2.rev) + +def logsort_rev_cmp(rev1, rev2): + # sort highest revision first + return -revcmp(rev1.rev, rev2.rev) + +_re_is_branch = re.compile(r'^((.*)\.)?\b0\.(\d+)$') +def read_log(full_name, which_rev=None, view_tag=None, logsort='cvs'): + head, cur_branch, taginfo, revs = fetch_log(full_name, which_rev) + + if not cur_branch: + idx = string.rfind(head, '.') + cur_branch = head[:idx] + + rev_order = map(lambda entry: entry.rev, revs) + rev_order.sort(revcmp) + rev_order.reverse() + + # HEAD is an artificial tag which is simply the highest tag number on the + # main branch, unless there is a branch tag in the RCS file in which case + # it's the highest revision on that branch. Find it by looking through + # rev_order; it is the first commit listed on the appropriate branch. + # This is not neccesary the same revision as marked as head in the RCS file. + idx = string.rfind(cur_branch, '.') + if idx == -1: + taginfo['MAIN'] = '0.' + cur_branch + else: + taginfo['MAIN'] = cur_branch[:idx] + '.0' + cur_branch[idx:] + + for rev in rev_order: + idx = string.rfind(rev, '.') + if idx != -1 and cur_branch == rev[:idx]: + taginfo['HEAD'] = rev + break + else: + idx = string.rfind(cur_branch, '.') + taginfo['HEAD'] = cur_branch[:idx] + + # map revision numbers to tag names + rev2tag = { } + + # names of symbols at each branch point + branch_points = { } + + branch_names = [ ] + + # Now that we know all of the revision numbers, we can associate + # absolute revision numbers with all of the symbolic names, and + # pass them to the form so that the same association doesn't have + # to be built then. + + items = taginfo.items() + items.sort() + items.reverse() + for tag, rev in items: + match = _re_is_branch.match(rev) + if match: + branch_names.append(tag) + + # + # A revision number of A.B.0.D really translates into + # "the highest current revision on branch A.B.D". + # + # If there is no branch A.B.D, then it translates into + # the head A.B . + # + # This reasoning also applies to the main branch A.B, + # with the branch number 0.A, with the exception that + # it has no head to translate to if there is nothing on + # the branch, but I guess this can never happen? + # (the code below gracefully forgets about the branch + # if it should happen) + # + head = match.group(2) or '' + branch = match.group(3) + if head: + branch_rev = head + '.' + branch + else: + branch_rev = branch + rev = head + for r in rev_order: + if r == branch_rev or r[:len(branch_rev)+1] == branch_rev + '.': + rev = branch_rev + break + if rev == '': + continue + if rev != head and head != '': + if branch_points.has_key(head): + branch_points[head] = branch_points[head] + ', ' + tag + else: + branch_points[head] = tag + + if rev2tag.has_key(rev): + rev2tag[rev] = rev2tag[rev] + ', ' + tag + else: + rev2tag[rev] = tag + + if view_tag: + view_rev = taginfo.get(view_tag) + if not view_rev: + error('Tag %s not defined.' % view_tag, '404 Tag not found') + + if view_rev[:2] == '0.': + view_rev = view_rev[2:] + idx = string.rfind(view_rev, '.') + branch_point = view_rev[:idx] + else: + idx = string.find(view_rev, '.0.') + if idx == -1: + branch_point = view_rev + else: + view_rev = view_rev[:idx] + view_rev[idx+2:] + idx = string.rfind(view_rev, '.') + branch_point = view_rev[:idx] + + show_revs = [ ] + for entry in revs: + rev = entry.rev + idx = string.rfind(rev, '.') + branch = rev[:idx] + if branch == view_rev or rev == branch_point: + show_revs.append(entry) + else: + show_revs = revs + + if logsort == 'date': + show_revs.sort(logsort_date_cmp) + elif logsort == 'rev': + show_revs.sort(logsort_rev_cmp) + else: + # no sorting + pass + + # build a map of revision number to entry information + rev_map = { } + for entry in revs: + rev_map[entry.rev] = entry + + ### some of this return stuff doesn't make a lot of sense... + return show_revs, rev_map, rev_order, taginfo, rev2tag, \ + cur_branch, branch_points, branch_names + +_re_is_vendor_branch = re.compile(r'^1\.1\.1\.\d+$') +g_name_printed = { } ### gawd, what a hack... +def print_log(request, rev_map, rev_order, entry, rev2tag, branch_points, + add_links): + query_dict = request.query_dict + where = request.where + + ### torch this. some old grandfathering stuff... + # revinfo = (rev, date, author, state, lines changed, log) + revinfo = entry.rev, entry.date, entry.author, entry.state, \ + entry.changed, entry.log + + rev = revinfo[0] + + idx = string.rfind(rev, '.') + branch = rev[:idx] + idx = string.rfind(branch, '.') + if idx == -1: + branch_point = '' + else: + branch_point = branch[:idx] + + is_dead = revinfo[3] == 'dead' + + if add_links and not is_dead: + filename = os.path.basename(where) + file_url = urllib.quote(filename) + print '
' % rev + if rev2tag.has_key(rev): + for tag in string.split(rev2tag[rev], ', '): + print '' % tag + if rev2tag.has_key(branch) and not g_name_printed.has_key(branch): + for tag in string.split(rev2tag[branch], ', '): + print '' % tag + g_name_printed[branch] = 1 + print 'Revision' + if request.default_viewable: + download_link(request, file_url, rev, rev, viewcvs_mime_type) + print '/' + download_link(request, file_url, rev, '(download)', request.mime_type) + else: + download_link(request, file_url, rev, rev) + if not request.default_text_plain: + print '/' + download_link(request, file_url, rev, '(as text)', 'text/plain') + if not request.default_viewable: + print '/' + download_link(request, file_url, rev, '(view)', viewcvs_mime_type) + if cfg.options.allow_annotate: + print '- annotate' % \ + (request.url, rev, request.amp_query) + if cfg.options.allow_version_select: + if query_dict.get('r1') != rev: + print '- [select for diffs]' % \ + (request.url, rev, request.amp_query) + else: + print '- [selected]' + + else: + print 'Revision %s' % rev + + is_vendor_branch = _re_is_vendor_branch.match(rev) + if is_vendor_branch: + print '(vendor branch)' + + print ', %s UTC (%s ago) by %s' % \ + (time.asctime(time.gmtime(revinfo[1])), + html_time(revinfo[1], 1), + revinfo[2]) + + if rev2tag.has_key(branch): + print '
Branch: %s' % \ + link_tags(query_dict, where, rev2tag[branch], add_links) + if rev2tag.has_key(rev): + print '
CVS Tags: %s' % \ + link_tags(query_dict, where, rev2tag[rev], add_links) + if branch_points.has_key(rev): + print '
Branch point for: %s' % \ + link_tags(query_dict, where, branch_points[rev], add_links) + + prev_rev = string.split(rev, '.') + while 1: + if prev_rev[-1] == '0': # .0 can be caused by 'commit -r X.Y.Z.0' + prev_rev = prev_rev[:-2] # X.Y.Z.0 becomes X.Y.Z + else: + prev_rev[-1] = str(int(prev_rev[-1]) - 1) + prev = string.join(prev_rev, '.') + if rev_map.has_key(prev) or prev == '': + break + if prev and rev_map[rev].changed: + print '
Changes since %s: %s lines' % \ + (prev, rev_map[rev].changed) + + if is_dead: + print '
FILE REMOVED' + elif add_links: + diff_rev = { rev : 1, '' : 1 } + print '
Diff' + + # is the (current) diff format human readable? + human_readable = query_dict['diff_format'] == 'h' + + # diff against previous version + if prev: + diff_rev[prev] = 1 + print 'to previous %s' % \ + (request.url, prev, rev, request.amp_query, prev) + if not human_readable: + print '(colored)' % \ + (request.url, prev, rev, request.amp_query) + + # diff against branch point (if not a vendor branch) + if rev2tag.has_key(branch_point) and \ + not is_vendor_branch and \ + not diff_rev.has_key(branch_point): + diff_rev[branch_point] = 1 + print 'to a branchpoint %s' % \ + (request.url, branch_point, rev, request.amp_query, + branch_point) + if not human_readable: + print '(colored)' % \ + (request.url, branch_point, rev, request.amp_query) + + # if it's on a branch (and not a vendor branch), then diff against the + # next revision of the higher branch (e.g. change is committed and + # brought over to -stable) + if string.count(rev, '.') > 1 and not is_vendor_branch: + # locate this rev in the ordered list of revisions + for i in range(len(rev_order)): + if rev_order[i] == rev: + break + + # create a rev that can be compared + c_rev = map(int, string.split(rev, '.')) + + next_main = '' + while i: + next = rev_order[i - 1] + c_work = string.split(rev, '.') + if len(c_work) < len(c_rev): + # found something not on the branch + next_main = next + break + + # this is a higher version on the same branch; the lower one (rev) + # shouldn't have a diff against the "next main branch" + if c_work[:-1] == c_rev[:len(c_work) - 1]: + break + + i = i - 1 + + if not diff_rev.has_key(next_main): + diff_rev[next_main] = 1 + print 'next main %s' % \ + (request.url, next_main, rev, request.amp_query, next_main) + if not human_readable: + print '(colored)' \ + % (request.url, next_main, rev, request.amp_query) + + # if they have selected r1, then diff against that + r1 = query_dict.get('r1') + if r1 and not diff_rev.has_key(r1): + diff_rev[r1] = 1 + print 'to selected %s' % \ + (request.url, r1, rev, request.amp_query, r1) + if not human_readable: + print '(colored)' \ + % (request.url, r1, rev, request.amp_query) + + print '
' + htmlify(revinfo[5]) + '
' + +def view_log(request): + full_name = request.full_name + where = request.where + query_dict = request.query_dict + + view_tag = query_dict.get('only_with_tag') + + show_revs, rev_map, rev_order, taginfo, rev2tag, \ + cur_branch, branch_points, branch_names = \ + read_log(full_name, None, view_tag, query_dict['logsort']) + + html_header('CVS log for %s' % where) + + up_where = re.sub(_re_up_path, '', where) + + ### whoops. this sometimes/always? does not have the ",v" + assert full_name[-2:] != ',v', 'please report this error to viewcvs@lyra.org' + #filename = os.path.basename(full_name[:-2]) # drop the ",v" + filename = os.path.basename(full_name) + + ### try: "./" + query + "#" + filename + back_url = request.script_name + '/' + urllib.quote(up_where) + \ + request.qmark_query + + print html_link(html_icon('back'), back_url + '#' + filename) + + ### use drop_leaf + print 'Up to %s

' % clickable_path(request, up_where, 1, 0, 0) + print 'Request diff between arbitrary revisions' + print '


' + + if cur_branch: + print 'Default branch:', rev2tag.get(cur_branch, cur_branch) + else: + print 'No default branch' + print '
' + if view_tag: + print 'Current tag:', view_tag, '
' + + for entry in show_revs: + print '
' + print_log(request, rev_map, rev_order, entry, rev2tag, branch_points, 1) + + sel = [ ] + tagitems = taginfo.items() + tagitems.sort() + tagitems.reverse() + for tag, rev in tagitems: + sel.append('' % (rev, tag, tag)) + sel = string.join(sel, '\n') + + print '
' + print 'This form allows you to request diffs between any two revisions of' + print 'a file. You may select a symbolic revision name using the selection' + print 'box or you may type in a numeric name using the type-in text box.' + print '

' + + print '

' % \ + request.url + for varname in _sticky_vars: + value = query_dict.get(varname, '') + if value != '' and value != default_settings.get(varname): + print '' % (varname, value) + + print 'Diffs between' + print '' + + if query_dict.has_key('r1'): + diff_rev = query_dict['r1'] + else: + diff_rev = show_revs[-1].rev + print '' % \ + (cfg.options.input_text_size, diff_rev) + + print 'and' + print '' + + if query_dict.has_key('r2'): + diff_rev = query_dict['r2'] + else: + diff_rev = show_revs[0].rev + print '' % \ + (cfg.options.input_text_size, diff_rev) + + print '
Type of Diff should be a' + print_diff_select(query_dict) + + print '
' + print '
' + + hidden_values = '' + for varname in _sticky_vars: + if varname != 'only_with_tag' and varname != 'logsort': + value = query_dict.get(varname, '') + if value != '' and value != default_settings.get(varname): + hidden_values = hidden_values + \ + '' % \ + (varname, value) + + if branch_names: + print '
' % request.url + print hidden_values + + print 'View only Branch:' + print '' + print '
' + + print '' + print '
' % request.url + print hidden_values + print 'Sort log by:' + print '
' + + html_footer() + +### suck up other warnings in _re_co_warning? +_re_co_filename = re.compile(r'^(.*),v\s+-->\s+standard output\s*\n$') +_re_co_warning = re.compile(r'^.*co: .*,v: warning: Unknown phrases like .*\n$') +_re_co_revision = re.compile(r'^revision\s+([\d\.]+)\s*\n$') +def view_checkout(request): + full_name = request.full_name + where = request.where + query_dict = request.query_dict + + rev = query_dict.get('rev') + + ### validate the revision? + + mime_type = query_dict.get('content-type') + if mime_type: + ### validate it? + pass + else: + mime_type = request.mime_type + + if rev: + rev_flag = '-p' + rev + else: + rev_flag = '-p' + + fp = popen.popen(cfg.general.rcs_path + 'co', (rev_flag, full_name), 'r') + + # header from co: + # + #/home/cvsroot/mod_dav/dav_shared_stub.c,v --> standard output + #revision 1.1 + # + # Sometimes, the following line might occur at line 2: + #co: INSTALL,v: warning: Unknown phrases like `permissions ...;' are present. + + # parse the output header + filename = revision = None + + line = fp.readline() + if not line: + error('Missing output from co.
' + 'fname="%s". url="%s"' % (filename, where)) + + match = _re_co_filename.match(line) + if not match: + error('First line of co output is not the filename.
' + 'Line was: %s
' + 'fname="%s". url="%s"' % (line, filename, where)) + filename = match.group(1) + + line = fp.readline() + if not line: + error('Missing second line of output from co.
' + 'fname="%s". url="%s"' % (filename, where)) + match = _re_co_revision.match(line) + if not match: + match = _re_co_warning.match(line) + if not match: + error('Second line of co output is not the revision.
' + 'Line was: %s
' + 'fname="%s". url="%s"' % (line, filename, where)) + + # second line was a warning. ignore it and move along. + line = fp.readline() + if not line: + error('Missing third line of output from co (after a warning).
' + 'fname="%s". url="%s"' % (filename, where)) + match = _re_co_revision.match(line) + if not match: + error('Third line of co output is not the revision.
' + 'Line was: %s
' + 'fname="%s". url="%s"' % (line, filename, where)) + + # one of the above cases matches the revision. grab it. + revision = match.group(1) + + if filename != full_name: + error('The filename from co did not match. Found "%s". Wanted "%s"
' + 'url="%s"' % (filename, full_name, where)) + + if mime_type == viewcvs_mime_type: + # use the "real" MIME type + markup_stream(request, fp, revision, request.mime_type) + else: + http_header(mime_type) + while 1: + chunk = fp.read(8192) + if not chunk: + break + sys.stdout.write(chunk) + +def view_annotate(request): + rev = request.query_dict['annotate'] + + pathname, filename = os.path.split(request.where) + if pathname[-6:] == '/Attic': + pathname = pathname[:-6] + + http_header() + navigate_header(request, request.url, pathname, filename, rev, 'view') + print '
' + + import blame + blame.make_html(request.cvsroot, request.where + ',v', rev, + sticky_query(request.query_dict)) + + html_footer() + + +_re_extract_rev = re.compile(r'^[-+]+ [^\t]+\t([^\t]+)\t((\d+\.)+\d+)$') +_re_extract_info = re.compile(r'@@ \-([0-9]+).*\+([0-9]+).*@@(.*)') +_re_extract_diff = re.compile(r'^([-+ ])(.*)') +def human_readable_diff(request, fp, rev1, rev2, sym1, sym2): + query_dict = request.query_dict + + where_nd = request.where[:-5] # remove the ".diff" + pathname, filename = os.path.split(where_nd) + + navigate_header(request, request.script_name + '/' + where_nd, pathname, + filename, rev2, 'diff') + + log_rev1 = log_rev2 = None + date1 = date2 = '' + r1r = r2r = '' + while 1: + line = fp.readline() + if not line: + break + + # Use regex matching to extract the data and to ensure that we are + # extracting it from a properly formatted line. There are rcsdiff + # programs out there that don't supply the correct format; we'll be + # flexible in case we run into one of those. + if line[:4] == '--- ': + match = _re_extract_rev.match(line) + if match: + date1 = ', ' + match.group(1) + log_rev1 = match.group(2) + elif line[:4] == '+++ ': + match = _re_extract_rev.match(line) + if match: + date2 = ', ' + match.group(1) + log_rev2 = match.group(2) + break + + if (log_rev1 and log_rev1 != rev1) or (log_rev2 and log_rev2 != rev2): + print 'ERROR: rcsdiff did not return the correct' + print 'version number in its output.' + print '(got "%s" / "%s", expected "%s" / "%s")' % \ + (log_rev1, log_rev2, rev1, rev2) + print '

Aborting operation.' + sys.exit(0) + + print '

Diff for /%s between version %s and %s

' % \ + (where_nd, rev1, rev2) + print '' + print '' + print '' + print '' + + fs = '' % \ + (cfg.options.diff_font_face, cfg.options.diff_font_size) + left_row = right_row = 0 + + # this will be set to true if any changes are found + changes_seen = 0 + + while 1: + line = fp.readline() + if not line: + break + + # we've seen some kind of change + changes_seen = 1 + + if line[:2] == '@@': + match = _re_extract_info.match(line) + print '' + + state = 'dump' + left_col = [ ] + right_col = [ ] + elif line[0] == '\\': + # \ No newline at end of file + flush_diff_rows(state, left_col, right_col) + left_col = [ ] + right_col = [ ] + else: + match = _re_extract_diff.match(line) + line = spaced_html_text(match.group(2)) + + # add font stuff + line = fs + ' ' + line + '' + + diff_code = match.group(1) + if diff_code == '+': + if state == 'dump': + print '' \ + '' % \ + (cfg.colors.diff_empty, cfg.colors.diff_add, line) + else: + state = 'pre-change-add' + right_col.append(line) + elif diff_code == '-': + state = 'pre-change-remove' + left_col.append(line) + else: + flush_diff_rows(state, left_col, right_col) + print '' % (line, line) + state = 'dump' + left_col = [ ] + right_col = [ ] + + if changes_seen: + flush_diff_rows(state, left_col, right_col) + else: + print '' + print '' % (cfg.colors.diff_empty) + + print '
' + print 'version %s%s' % (rev1, date1) + if sym1: + print '
Tag:', sym1 + print '
' + print 'version %s%s' % (rev2, date2) + if sym2: + print '
Tag:', sym2 + print '
' % cfg.colors.diff_heading + print '' + print '' % \ + (match.group(1), match.group(3)) + print '
Line %s %s
' + print '' + print '' % \ + (match.group(2), match.group(3)) + print '
Line %s %s
 %s
%s%s
 

- No changes -
 


' + print '' + + # format selector + print '' + print '
' + + # print the legend + print '
Legend:
' + print '' + print '' % (cfg.colors.diff_remove, rev1, cfg.colors.diff_empty) + print '' % cfg.colors.diff_change + print '' % (cfg.colors.diff_empty, cfg.colors.diff_add, rev2) + print '
Removed from v.%s 
changed lines
 Added in v.%s
' % request.url + for varname, value in query_dict.items(): + if varname != 'diff_format' and value != default_settings.get(varname): + print '' % \ + (varname, cgi.escape(value)) + print_diff_select(query_dict) + print '
' + html_footer() + + +def flush_diff_rows(state, left_col, right_col): + if state == 'pre-change-remove': + for row in left_col: + print '%s ' % \ + (cfg.colors.diff_remove, row, cfg.colors.diff_empty) + elif state == 'pre-change-add': + for i in range(max(len(left_col), len(right_col))): + if i < len(left_col): + left = '%s' % (cfg.colors.diff_change, left_col[i]) + else: + left = ' ' % cfg.colors.diff_dark_change + if i < len(right_col): + right = '%s' % (cfg.colors.diff_change, right_col[i]) + else: + right = ' ' % cfg.colors.diff_dark_change + print '%s%s' % (left, right) + +def spaced_html_text(text): + text = string.expandtabs(string.rstrip(text)) + + # in the code below, "\x01" will be our stand-in for "&". We don't want + # to insert "&" because it would get escaped by htmlify(). Similarly, + # we use "\x02" as a stand-in for "
" + + if cfg.options.hr_breakable > 1 and len(text) > cfg.options.hr_breakable: + text = re.sub('(' + ('.' * cfg.options.hr_breakable) + ')', + '\\1\x02', + text) + if cfg.options.hr_breakable: + # make every other space "breakable" + text = string.replace(text, ' ', ' \x01nbsp;') + else: + text = string.replace(text, ' ', '\x01nbsp;') + text = htmlify(text) + text = string.replace(text, '\x01', '&') + text = string.replace(text, '\x02', '\
') + return text + +def view_diff(request, cvs_filename): + query_dict = request.query_dict + cvsroot = request.cvsroot + + r1 = query_dict['r1'] + r2 = query_dict['r2'] + + sym1 = sym2 = '' + + if r1 == 'text': + rev1 = query_dict['tr1'] + else: + idx = string.find(r1, ':') + if idx == -1: + rev1 = r1 + else: + rev1 = r1[:idx] + sym1 = r1[idx+1:] + + if r2 == 'text': + rev2 = query_dict['tr2'] + sym2 = '' + else: + idx = string.find(r2, ':') + if idx == -1: + rev2 = r2 + else: + rev2 = r2[:idx] + sym2 = r2[idx+1:] + + if revcmp(rev1, rev2) > 0: + rev1, rev2 = rev2, rev1 + sym1, sym2 = sym2, sym1 + + human_readable = 0 + unified = 0 + + args = [ ] + + format = query_dict['diff_format'] + if format == 'c': + args.append('-c') + diff_name = 'Context diff' + elif format == 's': + args.append('--side-by-side') + args.append('--width=164') + diff_name = 'Side by Side' + elif format == 'H': + args.append('--unified=15') + diff_name = 'Long Human readable' + human_readable = 1 + unified = 1 + elif format == 'h': + args.append('-u') + diff_name = 'Human readable' + human_readable = 1 + unified = 1 + elif format == 'u': + args.append('-u') + diff_name = 'Unidiff' + unified = 1 + else: + error('Diff format %s not understood' % format, '400 Bad arguments') + + if human_readable: + if cfg.options.hr_funout: + args.append('-p') + if cfg.options.hr_ignore_white: + args.append('-w') + if cfg.options.hr_ignore_keyword_subst: + args.append('-kk') + + args[len(args):] = ['-r' + rev1, '-r' + rev2, cvs_filename] + fp = popen.popen(cfg.general.rcs_path + 'rcsdiff', args, 'r') + + if human_readable: + http_header() + human_readable_diff(request, fp, rev1, rev2, sym1, sym2) + sys.exit(0) + + http_header('text/plain') + + if unified: + f1 = '--- ' + cvsroot + f2 = '+++ ' + cvsroot + else: + f1 = '*** ' + cvsroot + f2 = '--- ' + cvsroot + + while 1: + line = fp.readline() + if not line: + break + + if line[:len(f1)] == f1: + line = string.replace(line, cvsroot + '/', '') + if sym1: + line = line[:-1] + ' %s\n' % sym1 + elif line[:len(f2)] == f2: + line = string.replace(line, cvsroot + '/', '') + if sym2: + line = line[:-1] + ' %s\n' % sym2 + + print line[:-1] + +def generate_tarball_header(out, name, size=0, mode=None, mtime=0, uid=0, gid=0, typefrag=None, linkname='', uname='viewcvs', gname='viewcvs', devmajor=1, devminor=0, prefix=None, magic='ustar', version='', chksum=None): + if not mode: + if name[-1:] == '/': + mode = 0755 + else: + mode = 0644 + + if not typefrag: + if name[-1:] == '/': + typefrag = '5' # directory + else: + typefrag = '0' # regular file + + if not prefix: + prefix = '' + + block1 = struct.pack('100s 8s 8s 8s 12s 12s', + name, + '%07o' % mode, + '%07o' % uid, + '%07o' % gid, + '%011o' % size, + '%011o' % mtime) + + block2 = struct.pack('c 100s 6s 2s 32s 32s 8s 8s 155s', + typefrag, + linkname, + magic, + version, + uname, + gname, + '%07o' % devmajor, + '%07o' % devminor, + prefix) + + if not chksum: + dummy_chksum = ' ' + block = block1 + dummy_chksum + block2 + chksum = 0 + for i in range(len(block)): + chksum = chksum + ord(block[i]) + + block = block1 + struct.pack('8s', '%07o' % chksum) + block2 + block = block + '\0' * (512 - len(block)) + + out.write(block) + +def generate_tarball(out, relative, directory, tag, stack=[]): + subdirs = [ ] + rcs_files = [ ] + for file, pathname, isdir in get_file_data(directory): + if pathname == _UNREADABLE_MARKER: + continue + if isdir: + subdirs.append(file) + else: + rcs_files.append(file) + if tag: + for file, pathname, isdir in get_file_data(directory + '/Attic'): + if not isdir and pathname != _UNREADABLE_MARKER: + rcs_files.append('Attic/' + file) + + stack.append(relative + '/') + + fileinfo, alltags = get_logs(directory, rcs_files, tag) + + files = fileinfo.keys() + files.sort(lambda a, b: cmp(os.path.basename(a), os.path.basename(b))) + + for file in files: + info = fileinfo.get(file) + rev = info[0] + date = info[1] + filename = info[4] + state = info[5] + if state == 'dead': + continue + + for dir in stack: + generate_tarball_header(out, dir) + del stack[0:] + + info = os.stat(directory + '/' + file + ',v') + mode = (info[stat.ST_MODE] & 0555) | 0200 + + rev_flag = '-p' + rev + full_name = directory + '/' + file + ',v' + fp = popen.popen(cfg.general.rcs_path + 'co', (rev_flag, full_name), 'r', 0) + contents = fp.read() + status = fp.close() + + generate_tarball_header(out, relative + '/' + os.path.basename(filename), len(contents), mode, date) + out.write(contents) + out.write('\0' * (511 - ((len(contents) + 511) % 512))) + + subdirs.sort() + for subdir in subdirs: + if subdir != 'Attic': + generate_tarball(out, relative + '/' + subdir, directory + '/' + subdir, tag, stack) + + if len(stack): + del stack[-1:] + +def download_tarball(request): + query_dict = request.query_dict + full_name = request.full_name + + directory = re.sub(_re_up_path, '', full_name)[0:-1] + filename = os.path.basename(full_name) + + tag = query_dict.get('only_with_tag') + + http_header('application/octet-stream') + fp = popen.pipe_cmds([('gzip', '-c', '-n')]) + generate_tarball(fp, os.path.basename(directory), directory, tag) + fp.write('\0' * 1024) + fp.close + +def handle_config(): + global cfg + cfg = config.Config() + cfg.set_defaults() + + # load in configuration information from the config file + pathname = CONF_PATHNAME or 'viewcvs.conf' + cfg.load_config(pathname, os.environ.get('HTTP_HOST')) + + global default_settings + default_settings = { + "sortby" : cfg.options.sort_by, + "hideattic" : cfg.options.hide_attic, + "logsort" : cfg.options.log_sort, + "diff_format" : cfg.options.diff_format, + "hidecvsroot" : cfg.options.hide_cvsroot, + "hidenonreadable" : cfg.options.hide_non_readable, + } + + +def main(): + # handle the configuration stuff + handle_config() + + # build a Request object, which contains info about the HTTP request + request = Request() + + # is the CVS root really there? + if not os.path.isdir(request.cvsroot): + error('%s not found!

The server on which the CVS tree lives is ' + 'probably down. Please try again in a few minutes.' % + request.cvsroot) + + full_name = request.full_name + isdir = os.path.isdir(full_name) + + url = request.url + where = request.where + + ### look for GZIP binary + + # if we have a directory and the request didn't end in "/", then redirect + # so that it does. (so that relative URLs in our output work right) + if isdir and os.environ.get('PATH_INFO', '')[-1:] != '/': + redirect(url + '/' + request.qmark_query) + + # check the forbidden list + if cfg.is_forbidden(request.module): + error('Access to "%s" is forbidden.' % request.module, '403 Forbidden') + + if isdir: + view_directory(request) + return + + # since we aren't talking about a directory, set up the mime type info + # for the file. + request.setup_mime_type_info() + + query_dict = request.query_dict + + if os.path.isfile(full_name + ',v'): + if query_dict.has_key('rev') or request.has_checkout_magic: + view_checkout(request) + elif query_dict.has_key('annotate') and cfg.options.allow_annotate: + view_annotate(request) + elif query_dict.has_key('r1') and query_dict.has_key('r2'): + view_diff(request, full_name) + else: + view_log(request) + elif full_name[-5:] == '.diff' and os.path.isfile(full_name[:-5] + ',v') \ + and query_dict.has_key('r1') and query_dict.has_key('r2'): + view_diff(request, full_name[:-5]) + elif cfg.options.allow_tar \ + and full_name[-7:] == '.tar.gz' and query_dict.has_key('tarball'): + download_tarball(request) + else: + # if the file is in the Attic, then redirect + idx = string.rfind(full_name, '/') + attic_name = full_name[:idx] + '/Attic' + full_name[idx:] + ',v' + if os.path.isfile(attic_name): + idx = string.rfind(url, '/') + redirect(url[:idx] + '/Attic' + url[idx:]) + + error('%s: unknown location' % request.url, '404 Not Found') + + +try: + main() +except SystemExit, e: + # don't catch SystemExit (caused by sys.exit()). propagate the exit code + sys.exit(e[0]) +except: + info = sys.exc_info() + html_header('Python Exception Occurred') + import traceback + lines = apply(traceback.format_exception, info) + print '

'
+  print cgi.escape(string.join(lines, ''))
+  print '
' + html_footer()