2000-04-28 18:01:48 +04:00
|
|
|
#!/usr/local/bin/python
|
|
|
|
# -*-python-*-
|
|
|
|
#
|
2002-09-05 11:34:45 +04:00
|
|
|
# Copyright (C) 2000-2002 The ViewCVS Group. All Rights Reserved.
|
2000-04-28 18:01:48 +04:00
|
|
|
# Copyright (C) 2000 Curt Hagenlocher <curt@hagenlocher.org>
|
|
|
|
#
|
2000-05-09 12:36:29 +04:00
|
|
|
# By using this file, you agree to the terms and conditions set forth in
|
|
|
|
# the LICENSE.html file which can be found at the top level of the ViewCVS
|
2001-05-30 12:49:19 +04:00
|
|
|
# distribution or at http://viewcvs.sourceforge.net/license-1.html.
|
2000-04-28 18:01:48 +04:00
|
|
|
#
|
2000-05-09 12:36:29 +04:00
|
|
|
# Contact information:
|
|
|
|
# Greg Stein, PO Box 760, Palo Alto, CA, 94302
|
2001-05-30 12:49:19 +04:00
|
|
|
# gstein@lyra.org, http://viewcvs.sourceforge.net/
|
2000-04-28 18:01:48 +04:00
|
|
|
#
|
2000-05-09 12:36:29 +04:00
|
|
|
# -----------------------------------------------------------------------
|
|
|
|
#
|
|
|
|
# blame.py: Annotate each line of a CVS file with its author,
|
|
|
|
# revision #, date, etc.
|
2000-04-28 18:01:48 +04:00
|
|
|
#
|
|
|
|
# -----------------------------------------------------------------------
|
|
|
|
#
|
|
|
|
# This software is being maintained as part of the ViewCVS project.
|
|
|
|
# Information is available at:
|
2001-05-30 12:49:19 +04:00
|
|
|
# http://viewcvs.sourceforge.net/
|
2000-04-28 18:01:48 +04:00
|
|
|
#
|
|
|
|
# This file is based on the cvsblame.pl portion of the Bonsai CVS tool,
|
|
|
|
# developed by Steve Lamm for Netscape Communications Corporation. More
|
|
|
|
# information about Bonsai can be found at
|
|
|
|
# http://www.mozilla.org/bonsai.html
|
|
|
|
#
|
|
|
|
# cvsblame.pl, in turn, was based on Scott Furman's cvsblame script
|
|
|
|
#
|
|
|
|
# -----------------------------------------------------------------------
|
|
|
|
#
|
|
|
|
|
|
|
|
import string
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import time
|
|
|
|
import math
|
|
|
|
import cgi
|
2005-02-03 16:04:03 +03:00
|
|
|
import vclib
|
2002-09-05 11:11:42 +04:00
|
|
|
from vclib.ccvs import rcsparse
|
2000-04-28 18:01:48 +04:00
|
|
|
|
2001-05-08 23:34:58 +04:00
|
|
|
class CVSParser(rcsparse.Sink):
|
2000-05-09 15:21:30 +04:00
|
|
|
# Precompiled regular expressions
|
|
|
|
trunk_rev = re.compile('^[0-9]+\\.[0-9]+$')
|
|
|
|
last_branch = re.compile('(.*)\\.[0-9]+')
|
2005-04-09 02:55:58 +04:00
|
|
|
is_branch = re.compile('^(.*)\\.0\\.([0-9]+)$')
|
2000-05-09 15:21:30 +04:00
|
|
|
d_command = re.compile('^d(\d+)\\s(\\d+)')
|
|
|
|
a_command = re.compile('^a(\d+)\\s(\\d+)')
|
|
|
|
|
|
|
|
SECONDS_PER_DAY = 86400
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.Reset()
|
|
|
|
|
|
|
|
def Reset(self):
|
|
|
|
self.last_revision = {}
|
|
|
|
self.prev_revision = {}
|
|
|
|
self.revision_date = {}
|
|
|
|
self.revision_author = {}
|
|
|
|
self.revision_branches = {}
|
|
|
|
self.next_delta = {}
|
|
|
|
self.prev_delta = {}
|
|
|
|
self.tag_revision = {}
|
|
|
|
self.timestamp = {}
|
|
|
|
self.revision_ctime = {}
|
|
|
|
self.revision_age = {}
|
|
|
|
self.revision_log = {}
|
|
|
|
self.revision_deltatext = {}
|
2002-10-10 04:53:14 +04:00
|
|
|
self.revision_map = [] # map line numbers to revisions
|
2000-05-09 15:21:30 +04:00
|
|
|
self.lines_added = {}
|
|
|
|
self.lines_removed = {}
|
|
|
|
|
|
|
|
# Map a tag to a numerical revision number. The tag can be a symbolic
|
|
|
|
# branch tag, a symbolic revision tag, or an ordinary numerical
|
|
|
|
# revision number.
|
|
|
|
def map_tag_to_revision(self, tag_or_revision):
|
|
|
|
try:
|
|
|
|
revision = self.tag_revision[tag_or_revision]
|
|
|
|
match = self.is_branch.match(revision)
|
|
|
|
if match:
|
|
|
|
branch = match.group(1) + '.' + match.group(2)
|
2000-05-12 02:34:14 +04:00
|
|
|
if self.last_revision.get(branch):
|
2000-05-09 15:21:30 +04:00
|
|
|
return self.last_revision[branch]
|
|
|
|
else:
|
|
|
|
return match.group(1)
|
|
|
|
else:
|
|
|
|
return revision
|
|
|
|
except:
|
|
|
|
return ''
|
|
|
|
|
|
|
|
# Construct an ordered list of ancestor revisions to the given
|
|
|
|
# revision, starting with the immediate ancestor and going back
|
|
|
|
# to the primordial revision (1.1).
|
|
|
|
#
|
|
|
|
# Note: The generated path does not traverse the tree the same way
|
|
|
|
# that the individual revision deltas do. In particular,
|
|
|
|
# the path traverses the tree "backwards" on branches.
|
|
|
|
def ancestor_revisions(self, revision):
|
|
|
|
ancestors = []
|
2000-05-11 15:15:54 +04:00
|
|
|
revision = self.prev_revision.get(revision)
|
2000-05-09 15:21:30 +04:00
|
|
|
while revision:
|
|
|
|
ancestors.append(revision)
|
2000-05-11 15:15:54 +04:00
|
|
|
revision = self.prev_revision.get(revision)
|
2000-05-09 15:21:30 +04:00
|
|
|
|
|
|
|
return ancestors
|
|
|
|
|
2001-04-27 11:05:49 +04:00
|
|
|
# Split deltatext specified by rev to each line.
|
|
|
|
def deltatext_split(self, rev):
|
|
|
|
lines = string.split(self.revision_deltatext[rev], '\n')
|
|
|
|
if lines[-1] == '':
|
|
|
|
del lines[-1]
|
|
|
|
return lines
|
|
|
|
|
2000-05-09 15:21:30 +04:00
|
|
|
# Extract the given revision from the digested RCS file.
|
|
|
|
# (Essentially the equivalent of cvs up -rXXX)
|
|
|
|
def extract_revision(self, revision):
|
|
|
|
path = []
|
|
|
|
add_lines_remaining = 0
|
|
|
|
start_line = 0
|
|
|
|
count = 0
|
|
|
|
while revision:
|
|
|
|
path.append(revision)
|
2000-05-12 02:34:14 +04:00
|
|
|
revision = self.prev_delta.get(revision)
|
2000-05-09 15:21:30 +04:00
|
|
|
path.reverse()
|
|
|
|
path = path[1:] # Get rid of head revision
|
|
|
|
|
2001-04-27 11:05:49 +04:00
|
|
|
text = self.deltatext_split(self.head_revision)
|
2000-05-09 15:21:30 +04:00
|
|
|
|
|
|
|
# Iterate, applying deltas to previous revision
|
|
|
|
for revision in path:
|
|
|
|
adjust = 0
|
2001-04-27 11:05:49 +04:00
|
|
|
diffs = self.deltatext_split(revision)
|
2000-05-09 15:21:30 +04:00
|
|
|
self.lines_added[revision] = 0
|
|
|
|
self.lines_removed[revision] = 0
|
|
|
|
lines_added_now = 0
|
|
|
|
lines_removed_now = 0
|
|
|
|
|
|
|
|
for command in diffs:
|
|
|
|
dmatch = self.d_command.match(command)
|
|
|
|
amatch = self.a_command.match(command)
|
|
|
|
if add_lines_remaining > 0:
|
|
|
|
# Insertion lines from a prior "a" command
|
|
|
|
text.insert(start_line + adjust, command)
|
|
|
|
add_lines_remaining = add_lines_remaining - 1
|
|
|
|
adjust = adjust + 1
|
|
|
|
elif dmatch:
|
|
|
|
# "d" - Delete command
|
|
|
|
start_line = string.atoi(dmatch.group(1))
|
|
|
|
count = string.atoi(dmatch.group(2))
|
|
|
|
begin = start_line + adjust - 1
|
|
|
|
del text[begin:begin + count]
|
|
|
|
adjust = adjust - count
|
|
|
|
lines_removed_now = lines_removed_now + count
|
|
|
|
elif amatch:
|
|
|
|
# "a" - Add command
|
|
|
|
start_line = string.atoi(amatch.group(1))
|
|
|
|
count = string.atoi(amatch.group(2))
|
|
|
|
add_lines_remaining = count
|
|
|
|
lines_added_now = lines_added_now + count
|
|
|
|
else:
|
|
|
|
raise RuntimeError, 'Error parsing diff commands'
|
|
|
|
|
|
|
|
self.lines_added[revision] = self.lines_added[revision] + lines_added_now
|
|
|
|
self.lines_removed[revision] = self.lines_removed[revision] + lines_removed_now
|
|
|
|
return text
|
|
|
|
|
2001-05-08 23:34:58 +04:00
|
|
|
def set_head_revision(self, revision):
|
|
|
|
self.head_revision = revision
|
|
|
|
|
|
|
|
def set_principal_branch(self, branch_name):
|
|
|
|
self.principal_branch = branch_name
|
2000-05-09 15:21:30 +04:00
|
|
|
|
2001-05-08 23:34:58 +04:00
|
|
|
def define_tag(self, name, revision):
|
|
|
|
# Create an associate array that maps from tag name to
|
|
|
|
# revision number and vice-versa.
|
|
|
|
self.tag_revision[name] = revision
|
|
|
|
|
|
|
|
def set_comment(self, comment):
|
|
|
|
self.file_description = comment
|
|
|
|
|
|
|
|
def set_description(self, description):
|
|
|
|
self.rcs_file_description = description
|
2000-05-09 15:21:30 +04:00
|
|
|
|
|
|
|
# Construct dicts that represent the topology of the RCS tree
|
|
|
|
# and other arrays that contain info about individual revisions.
|
|
|
|
#
|
|
|
|
# The following dicts are created, keyed by revision number:
|
|
|
|
# self.revision_date -- e.g. "96.02.23.00.21.52"
|
|
|
|
# self.timestamp -- seconds since 12:00 AM, Jan 1, 1970 GMT
|
|
|
|
# self.revision_author -- e.g. "tom"
|
|
|
|
# self.revision_branches -- descendant branch revisions, separated by spaces,
|
|
|
|
# e.g. "1.21.4.1 1.21.2.6.1"
|
|
|
|
# self.prev_revision -- revision number of previous *ancestor* in RCS tree.
|
|
|
|
# Traversal of this array occurs in the direction
|
|
|
|
# of the primordial (1.1) revision.
|
|
|
|
# self.prev_delta -- revision number of previous revision which forms
|
|
|
|
# the basis for the edit commands in this revision.
|
|
|
|
# This causes the tree to be traversed towards the
|
|
|
|
# trunk when on a branch, and towards the latest trunk
|
|
|
|
# revision when on the trunk.
|
|
|
|
# self.next_delta -- revision number of next "delta". Inverts prev_delta.
|
|
|
|
#
|
|
|
|
# Also creates self.last_revision, keyed by a branch revision number, which
|
|
|
|
# indicates the latest revision on a given branch,
|
|
|
|
# e.g. self.last_revision{"1.2.8"} == 1.2.8.5
|
2001-05-08 23:34:58 +04:00
|
|
|
def define_revision(self, revision, timestamp, author, state,
|
|
|
|
branches, next):
|
|
|
|
self.tag_revision[revision] = revision
|
|
|
|
branch = self.last_branch.match(revision).group(1)
|
|
|
|
self.last_revision[branch] = revision
|
|
|
|
|
|
|
|
#self.revision_date[revision] = date
|
|
|
|
self.timestamp[revision] = timestamp
|
|
|
|
|
|
|
|
# Pretty print the date string
|
|
|
|
ltime = time.localtime(self.timestamp[revision])
|
|
|
|
formatted_date = time.strftime("%d %b %Y %H:%M", ltime)
|
|
|
|
self.revision_ctime[revision] = formatted_date
|
|
|
|
|
|
|
|
# Save age
|
|
|
|
self.revision_age[revision] = ((time.time() - self.timestamp[revision])
|
|
|
|
/ self.SECONDS_PER_DAY)
|
|
|
|
|
|
|
|
# save author
|
|
|
|
self.revision_author[revision] = author
|
|
|
|
|
|
|
|
# ignore the state
|
|
|
|
|
|
|
|
# process the branch information
|
|
|
|
branch_text = ''
|
|
|
|
for branch in branches:
|
|
|
|
self.prev_revision[branch] = revision
|
|
|
|
self.next_delta[revision] = branch
|
|
|
|
self.prev_delta[branch] = revision
|
|
|
|
branch_text = branch_text + branch + ''
|
|
|
|
self.revision_branches[revision] = branch_text
|
|
|
|
|
|
|
|
# process the "next revision" information
|
|
|
|
if next:
|
|
|
|
self.next_delta[revision] = next
|
|
|
|
self.prev_delta[next] = revision
|
2000-05-09 15:21:30 +04:00
|
|
|
is_trunk_revision = self.trunk_rev.match(revision) is not None
|
2001-05-08 23:34:58 +04:00
|
|
|
if is_trunk_revision:
|
|
|
|
self.prev_revision[revision] = next
|
|
|
|
else:
|
|
|
|
self.prev_revision[next] = revision
|
2000-05-09 15:21:30 +04:00
|
|
|
|
|
|
|
# Construct associative arrays containing info about individual revisions.
|
|
|
|
#
|
|
|
|
# The following associative arrays are created, keyed by revision number:
|
|
|
|
# revision_log -- log message
|
|
|
|
# revision_deltatext -- Either the complete text of the revision,
|
|
|
|
# in the case of the head revision, or the
|
|
|
|
# encoded delta between this revision and another.
|
|
|
|
# The delta is either with respect to the successor
|
|
|
|
# revision if this revision is on the trunk or
|
|
|
|
# relative to its immediate predecessor if this
|
|
|
|
# revision is on a branch.
|
2001-05-08 23:34:58 +04:00
|
|
|
def set_revision_info(self, revision, log, text):
|
|
|
|
self.revision_log[revision] = log
|
|
|
|
self.revision_deltatext[revision] = text
|
2000-05-09 15:21:30 +04:00
|
|
|
|
|
|
|
def parse_cvs_file(self, rcs_pathname, opt_rev = None, opt_m_timestamp = None):
|
|
|
|
# Args in: opt_rev - requested revision
|
|
|
|
# opt_m - time since modified
|
|
|
|
# Args out: revision_map
|
|
|
|
# timestamp
|
|
|
|
# revision_deltatext
|
|
|
|
|
2003-10-14 19:53:42 +04:00
|
|
|
# CheckHidden(rcs_pathname)
|
2000-05-09 15:21:30 +04:00
|
|
|
try:
|
2003-02-26 05:31:31 +03:00
|
|
|
rcsfile = open(rcs_pathname, 'rb')
|
2000-05-09 15:21:30 +04:00
|
|
|
except:
|
|
|
|
raise RuntimeError, ('error: %s appeared to be under CVS control, ' +
|
|
|
|
'but the RCS file is inaccessible.') % rcs_pathname
|
|
|
|
|
2001-05-08 23:34:58 +04:00
|
|
|
rcsparse.Parser().parse(rcsfile, self)
|
|
|
|
rcsfile.close()
|
2000-05-09 15:21:30 +04:00
|
|
|
|
|
|
|
if opt_rev in [None, '', 'HEAD']:
|
|
|
|
# Explicitly specified topmost revision in tree
|
|
|
|
revision = self.head_revision
|
|
|
|
else:
|
|
|
|
# Symbolic tag or specific revision number specified.
|
|
|
|
revision = self.map_tag_to_revision(opt_rev)
|
|
|
|
if revision == '':
|
|
|
|
raise RuntimeError, 'error: -r: No such revision: ' + opt_rev
|
|
|
|
|
|
|
|
# The primordial revision is not always 1.1! Go find it.
|
|
|
|
primordial = revision
|
2000-05-12 02:34:14 +04:00
|
|
|
while self.prev_revision.get(primordial):
|
2000-05-09 15:21:30 +04:00
|
|
|
primordial = self.prev_revision[primordial]
|
|
|
|
|
|
|
|
# Don't display file at all, if -m option is specified and no
|
|
|
|
# changes have been made in the specified file.
|
|
|
|
if opt_m_timestamp and self.timestamp[revision] < opt_m_timestamp:
|
|
|
|
return ''
|
|
|
|
|
|
|
|
# Figure out how many lines were in the primordial, i.e. version 1.1,
|
|
|
|
# check-in by moving backward in time from the head revision to the
|
|
|
|
# first revision.
|
|
|
|
line_count = 0
|
2000-05-12 02:34:14 +04:00
|
|
|
if self.revision_deltatext.get(self.head_revision):
|
2001-04-27 11:05:49 +04:00
|
|
|
tmp_array = self.deltatext_split(self.head_revision)
|
2000-05-09 15:21:30 +04:00
|
|
|
line_count = len(tmp_array)
|
|
|
|
|
|
|
|
skip = 0
|
|
|
|
|
2000-05-11 15:15:54 +04:00
|
|
|
rev = self.prev_revision.get(self.head_revision)
|
2000-05-09 15:21:30 +04:00
|
|
|
while rev:
|
2001-04-27 11:05:49 +04:00
|
|
|
diffs = self.deltatext_split(rev)
|
2000-05-09 15:21:30 +04:00
|
|
|
for command in diffs:
|
|
|
|
dmatch = self.d_command.match(command)
|
|
|
|
amatch = self.a_command.match(command)
|
|
|
|
if skip > 0:
|
|
|
|
# Skip insertion lines from a prior "a" command
|
|
|
|
skip = skip - 1
|
|
|
|
elif dmatch:
|
|
|
|
# "d" - Delete command
|
|
|
|
start_line = string.atoi(dmatch.group(1))
|
|
|
|
count = string.atoi(dmatch.group(2))
|
|
|
|
line_count = line_count - count
|
|
|
|
elif amatch:
|
|
|
|
# "a" - Add command
|
|
|
|
start_line = string.atoi(amatch.group(1))
|
|
|
|
count = string.atoi(amatch.group(2))
|
2003-10-14 19:53:42 +04:00
|
|
|
skip = count
|
2000-05-09 15:21:30 +04:00
|
|
|
line_count = line_count + count
|
|
|
|
else:
|
|
|
|
raise RuntimeError, 'error: illegal RCS file'
|
2000-05-11 15:15:54 +04:00
|
|
|
|
|
|
|
rev = self.prev_revision.get(rev)
|
2000-05-09 15:21:30 +04:00
|
|
|
|
|
|
|
# Now, play the delta edit commands *backwards* from the primordial
|
|
|
|
# revision forward, but rather than applying the deltas to the text of
|
|
|
|
# each revision, apply the changes to an array of revision numbers.
|
|
|
|
# This creates a "revision map" -- an array where each element
|
|
|
|
# represents a line of text in the given revision but contains only
|
|
|
|
# the revision number in which the line was introduced rather than
|
|
|
|
# the line text itself.
|
|
|
|
#
|
|
|
|
# Note: These are backward deltas for revisions on the trunk and
|
|
|
|
# forward deltas for branch revisions.
|
|
|
|
|
|
|
|
# Create initial revision map for primordial version.
|
|
|
|
self.revision_map = [primordial] * line_count
|
|
|
|
|
|
|
|
ancestors = [revision, ] + self.ancestor_revisions(revision)
|
|
|
|
ancestors = ancestors[:-1] # Remove "1.1"
|
|
|
|
last_revision = primordial
|
|
|
|
ancestors.reverse()
|
|
|
|
for revision in ancestors:
|
|
|
|
is_trunk_revision = self.trunk_rev.match(revision) is not None
|
|
|
|
|
|
|
|
if is_trunk_revision:
|
2001-04-27 11:05:49 +04:00
|
|
|
diffs = self.deltatext_split(last_revision)
|
2000-05-09 15:21:30 +04:00
|
|
|
|
|
|
|
# Revisions on the trunk specify deltas that transform a
|
|
|
|
# revision into an earlier revision, so invert the translation
|
|
|
|
# of the 'diff' commands.
|
|
|
|
for command in diffs:
|
|
|
|
if skip > 0:
|
|
|
|
skip = skip - 1
|
|
|
|
else:
|
|
|
|
dmatch = self.d_command.match(command)
|
|
|
|
amatch = self.a_command.match(command)
|
|
|
|
if dmatch:
|
|
|
|
start_line = string.atoi(dmatch.group(1))
|
|
|
|
count = string.atoi(dmatch.group(2))
|
|
|
|
temp = []
|
|
|
|
while count > 0:
|
|
|
|
temp.append(revision)
|
|
|
|
count = count - 1
|
|
|
|
self.revision_map = (self.revision_map[:start_line - 1] +
|
|
|
|
temp + self.revision_map[start_line - 1:])
|
|
|
|
elif amatch:
|
|
|
|
start_line = string.atoi(amatch.group(1))
|
|
|
|
count = string.atoi(amatch.group(2))
|
|
|
|
del self.revision_map[start_line:start_line + count]
|
|
|
|
skip = count
|
|
|
|
else:
|
|
|
|
raise RuntimeError, 'Error parsing diff commands'
|
|
|
|
|
|
|
|
else:
|
|
|
|
# Revisions on a branch are arranged backwards from those on
|
|
|
|
# the trunk. They specify deltas that transform a revision
|
|
|
|
# into a later revision.
|
|
|
|
adjust = 0
|
2001-04-27 11:05:49 +04:00
|
|
|
diffs = self.deltatext_split(revision)
|
2000-05-09 15:21:30 +04:00
|
|
|
for command in diffs:
|
|
|
|
if skip > 0:
|
|
|
|
skip = skip - 1
|
|
|
|
else:
|
|
|
|
dmatch = self.d_command.match(command)
|
|
|
|
amatch = self.a_command.match(command)
|
|
|
|
if dmatch:
|
|
|
|
start_line = string.atoi(dmatch.group(1))
|
|
|
|
count = string.atoi(dmatch.group(2))
|
|
|
|
del self.revision_map[start_line + adjust - 1:start_line + adjust - 1 + count]
|
|
|
|
adjust = adjust - count
|
|
|
|
elif amatch:
|
|
|
|
start_line = string.atoi(amatch.group(1))
|
|
|
|
count = string.atoi(amatch.group(2))
|
|
|
|
skip = count
|
|
|
|
temp = []
|
|
|
|
while count > 0:
|
|
|
|
temp.append(revision)
|
|
|
|
count = count - 1
|
|
|
|
self.revision_map = (self.revision_map[:start_line + adjust] +
|
|
|
|
temp + self.revision_map[start_line + adjust:])
|
|
|
|
adjust = adjust + skip
|
|
|
|
else:
|
|
|
|
raise RuntimeError, 'Error parsing diff commands'
|
|
|
|
|
|
|
|
last_revision = revision
|
|
|
|
|
|
|
|
return revision
|
2000-04-28 18:01:48 +04:00
|
|
|
|
|
|
|
|
|
|
|
re_includes = re.compile('\\#(\\s*)include(\\s*)"(.*?)"')
|
|
|
|
|
2005-02-03 16:04:03 +03:00
|
|
|
def link_includes(text, repos, path_parts, include_url):
|
2000-05-09 15:21:30 +04:00
|
|
|
match = re_includes.match(text)
|
|
|
|
if match:
|
|
|
|
incfile = match.group(3)
|
2005-02-03 16:04:03 +03:00
|
|
|
|
|
|
|
# check current directory and parent directory for file
|
|
|
|
for depth in (-1, -2):
|
|
|
|
include_path = path_parts[:depth] + [incfile]
|
|
|
|
try:
|
|
|
|
repos.rcsfile(include_path) # will throw if path doesn't exist
|
|
|
|
break
|
|
|
|
except vclib.ItemNotFound:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
include_path = None
|
|
|
|
|
|
|
|
if include_path:
|
|
|
|
url = string.replace(include_url, '/WHERE/',
|
|
|
|
string.join(include_path, '/'))
|
2000-05-12 11:24:13 +04:00
|
|
|
return '#%sinclude%s"<a href="%s">%s</a>"' % \
|
2001-05-10 13:12:45 +04:00
|
|
|
(match.group(1), match.group(2), url, incfile)
|
2005-02-03 16:04:03 +03:00
|
|
|
|
2000-05-09 15:21:30 +04:00
|
|
|
return text
|
2000-04-28 18:01:48 +04:00
|
|
|
|
2004-04-21 09:08:12 +04:00
|
|
|
class BlameSource:
|
2005-02-03 16:04:03 +03:00
|
|
|
def __init__(self, repos, path_parts, diff_url, include_url, opt_rev = None):
|
2004-04-21 09:08:12 +04:00
|
|
|
# Parse the CVS file
|
|
|
|
parser = CVSParser()
|
2005-02-03 16:04:03 +03:00
|
|
|
revision = parser.parse_cvs_file(repos.rcsfile(path_parts, 1), opt_rev)
|
2004-04-21 09:08:12 +04:00
|
|
|
count = len(parser.revision_map)
|
|
|
|
lines = parser.extract_revision(revision)
|
|
|
|
if len(lines) != count:
|
|
|
|
raise RuntimeError, 'Internal consistency error'
|
|
|
|
|
|
|
|
# set up some state variables
|
2005-02-03 16:04:03 +03:00
|
|
|
self.repos = repos
|
|
|
|
self.path_parts = path_parts
|
|
|
|
self.diff_url = diff_url
|
|
|
|
self.include_url = include_url
|
2005-02-04 21:28:22 +03:00
|
|
|
self.revision = revision
|
2004-04-21 09:08:12 +04:00
|
|
|
self.lines = lines
|
|
|
|
self.num_lines = count
|
|
|
|
self.parser = parser
|
|
|
|
|
|
|
|
# keep track of where we are during an iteration
|
|
|
|
self.idx = -1
|
|
|
|
self.last = None
|
|
|
|
|
|
|
|
def __getitem__(self, idx):
|
|
|
|
if idx == self.idx:
|
|
|
|
return self.last
|
|
|
|
if idx >= self.num_lines:
|
|
|
|
raise IndexError("No more annotations")
|
|
|
|
if idx != self.idx + 1:
|
|
|
|
raise BlameSequencingError()
|
|
|
|
|
|
|
|
# Get the line, escape HTML, and (maybe) add include links.
|
|
|
|
rev = self.parser.revision_map[idx]
|
|
|
|
prev_rev = self.parser.prev_revision.get(rev)
|
|
|
|
line_number = idx + 1
|
|
|
|
author = self.parser.revision_author[rev]
|
|
|
|
diff_url = None
|
|
|
|
if prev_rev:
|
2005-02-03 16:04:03 +03:00
|
|
|
diff_url = '%sr1=%s&r2=%s' % (self.diff_url, prev_rev, rev)
|
|
|
|
|
2004-04-21 09:08:12 +04:00
|
|
|
thisline = self.lines[idx]
|
|
|
|
thisline = cgi.escape(thisline)
|
|
|
|
if 1: #cfg.options.blame_link_includes:
|
2005-02-03 16:04:03 +03:00
|
|
|
thisline = link_includes(thisline, self.repos, self.path_parts,
|
|
|
|
self.include_url)
|
2000-05-09 15:21:30 +04:00
|
|
|
|
2004-04-21 09:08:12 +04:00
|
|
|
item = _item(text=thisline, line_number=line_number, rev=rev,
|
|
|
|
prev_rev=prev_rev, diff_url=diff_url, author=author)
|
|
|
|
self.last = item
|
|
|
|
self.idx = idx
|
|
|
|
return item
|
2000-05-09 15:21:30 +04:00
|
|
|
|
2004-04-21 09:08:12 +04:00
|
|
|
class BlameSequencingError(Exception):
|
|
|
|
pass
|
2000-05-09 15:21:30 +04:00
|
|
|
|
2004-04-21 09:08:12 +04:00
|
|
|
class _item:
|
|
|
|
def __init__(self, **kw):
|
|
|
|
vars(self).update(kw)
|
|
|
|
|
|
|
|
def make_html(root, rcs_path, opt_rev = None, sticky = None):
|
|
|
|
bs = BlameSource(root, rcs_path, opt_rev, sticky)
|
|
|
|
|
|
|
|
count = bs.num_lines
|
2000-05-09 15:21:30 +04:00
|
|
|
if count == 0:
|
|
|
|
count = 1
|
|
|
|
|
2000-05-11 05:12:33 +04:00
|
|
|
line_num_width = int(math.log10(count)) + 1
|
2000-05-09 15:21:30 +04:00
|
|
|
revision_width = 3
|
|
|
|
author_width = 5
|
|
|
|
line = 0
|
|
|
|
old_revision = 0
|
|
|
|
row_color = ''
|
|
|
|
inMark = 0
|
|
|
|
rev_count = 0
|
|
|
|
|
2004-04-21 09:08:12 +04:00
|
|
|
open_table_tag = '<table border="0" cellpadding="0" cellspacing="0" width="100%">'
|
|
|
|
startOfRow = '<tr><td colspan="3"%s><pre>'
|
|
|
|
endOfRow = '</td></tr>'
|
2000-05-09 15:21:30 +04:00
|
|
|
|
2004-04-21 09:08:12 +04:00
|
|
|
print open_table_tag + (startOfRow % '')
|
2000-05-09 15:21:30 +04:00
|
|
|
|
2004-04-21 09:08:12 +04:00
|
|
|
for line_data in bs:
|
|
|
|
revision = line_data.rev
|
|
|
|
thisline = line_data.text
|
|
|
|
line = line_data.line_number
|
|
|
|
author = line_data.author
|
|
|
|
prev_rev = line_data.prev_rev
|
|
|
|
diff_url = line_data.diff_url
|
|
|
|
|
2000-05-09 15:21:30 +04:00
|
|
|
output = ''
|
|
|
|
|
|
|
|
if old_revision != revision and line != 1:
|
|
|
|
if row_color == '':
|
|
|
|
row_color = ' bgcolor="#e7e7e7"'
|
|
|
|
else:
|
|
|
|
row_color = ''
|
|
|
|
|
|
|
|
if not inMark:
|
2004-04-21 09:08:12 +04:00
|
|
|
output = output + endOfRow + (startOfRow % row_color)
|
2000-05-09 15:21:30 +04:00
|
|
|
|
2002-10-10 04:53:14 +04:00
|
|
|
output = output + '<a name="%d">%*d</a>' % (line, line_num_width, line)
|
2000-05-09 15:21:30 +04:00
|
|
|
|
|
|
|
if old_revision != revision or rev_count > 20:
|
|
|
|
revision_width = max(revision_width, len(revision))
|
|
|
|
|
2004-04-21 09:08:12 +04:00
|
|
|
output = output + ' '
|
|
|
|
if diff_url:
|
|
|
|
output = output + '<a href="%s">' % diff_url
|
|
|
|
|
2000-05-09 15:21:30 +04:00
|
|
|
author_width = max(author_width, len(author))
|
2002-10-10 04:53:14 +04:00
|
|
|
output = output + ('%-*s ' % (author_width, author))
|
2000-05-12 11:24:13 +04:00
|
|
|
output = output + revision
|
2004-04-21 09:08:12 +04:00
|
|
|
if prev_rev:
|
2000-05-12 11:24:13 +04:00
|
|
|
output = output + '</a>'
|
|
|
|
output = output + (' ' * (revision_width - len(revision) + 1))
|
2000-05-09 15:21:30 +04:00
|
|
|
|
|
|
|
old_revision = revision
|
|
|
|
rev_count = 0
|
|
|
|
else:
|
|
|
|
output = output + ' ' + (' ' * (author_width + revision_width))
|
|
|
|
rev_count = rev_count + 1
|
|
|
|
|
|
|
|
output = output + thisline
|
|
|
|
|
|
|
|
# Close the highlighted section
|
|
|
|
#if (defined $mark_cmd and mark_cmd != 'begin'):
|
|
|
|
# chop($output)
|
|
|
|
# output = output + endOfRow + (startOfRow % row_color)
|
|
|
|
# inMark = 0
|
|
|
|
|
|
|
|
print output
|
2000-05-11 15:15:54 +04:00
|
|
|
print endOfRow + '</table>'
|
2000-04-28 18:01:48 +04:00
|
|
|
|
|
|
|
|
2000-05-11 05:12:33 +04:00
|
|
|
def main():
|
2002-10-10 04:53:14 +04:00
|
|
|
import sys
|
2000-05-11 05:12:33 +04:00
|
|
|
if len(sys.argv) != 3:
|
|
|
|
print 'USAGE: %s cvsroot rcs-file' % sys.argv[0]
|
|
|
|
sys.exit(1)
|
|
|
|
make_html(sys.argv[1], sys.argv[2])
|
|
|
|
|
2000-04-28 18:01:48 +04:00
|
|
|
if __name__ == '__main__':
|
2000-05-11 05:12:33 +04:00
|
|
|
main()
|