mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-07 20:17:37 +00:00
826 lines
28 KiB
Perl
Executable File
826 lines
28 KiB
Perl
Executable File
#!/usr/bonsaitools/bin/perl --
|
|
# -*- Mode: perl; indent-tabs-mode: nil -*-
|
|
#
|
|
# The contents of this file are subject to the Netscape Public License
|
|
# Version 1.0 (the "License"); you may not use this file except in
|
|
# compliance with the License. You may obtain a copy of the License at
|
|
# http://www.mozilla.org/NPL/
|
|
#
|
|
# Software distributed under the License is distributed on an "AS IS"
|
|
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing rights and limitations
|
|
# under the License.
|
|
#
|
|
# The Original Code is the Bonsai CVS tool.
|
|
#
|
|
# The Initial Developer of the Original Code is Netscape Communications
|
|
# Corporation. Portions created by Netscape are Copyright (C) 1998
|
|
# Netscape Communications Corporation. All Rights Reserved.
|
|
|
|
##############################################################################
|
|
#
|
|
# cvsblame.pl - Shamelessly adapted from Scott Furman's cvsblame script
|
|
# by Steve Lamm (slamm@netscape.com)
|
|
# - Annotate each line of a CVS file with its author,
|
|
# revision #, date, etc.
|
|
#
|
|
# Report problems to Steve Lamm (slamm@netscape.com)
|
|
#
|
|
##############################################################################
|
|
|
|
require 'timelocal.pl'; # timestamps
|
|
use Date::Format; # human-readable dates
|
|
|
|
$debug = 0;
|
|
|
|
# Extract base part of this script's name
|
|
($progname) = $0 =~ /([^\/]+)$/;
|
|
|
|
&cvsblame_init;
|
|
|
|
1;
|
|
|
|
|
|
sub cvsblame_init {
|
|
# Use default formatting options if none supplied
|
|
if (!$opt_A && !$opt_a && !$opt_d && !$opt_v) {
|
|
$opt_a = 1;
|
|
$opt_v = 1;
|
|
}
|
|
|
|
$time = time;
|
|
$SECS_PER_DAY = 60 * 60 * 24;
|
|
|
|
# Timestamp threshold at which annotations begin to occur, if -m option present.
|
|
$opt_m_timestamp = $time - $opt_m * $SECS_PER_DAY;
|
|
}
|
|
|
|
# Generic traversal of a CVS tree. Invoke callback function for
|
|
# individual directories that contain CVS files.
|
|
sub traverse_cvs_tree {
|
|
local ($dir, *callback, $nlink) = @_;
|
|
local ($dev, $ino, $mode, $subcount);
|
|
|
|
# Get $nlink for top-level directory
|
|
($dev, $ino, $mode, $nlink) = stat($dir) unless $nlink;
|
|
|
|
# Read directory
|
|
opendir(DIR, $dir) || die "Can't open $dir\n";
|
|
local(@filenames) = readdir(DIR);
|
|
closedir(DIR);
|
|
|
|
return if ! -d "$dir/CVS";
|
|
|
|
&callback($dir);
|
|
|
|
# This dir has subdirs
|
|
if ($nlink != 2) {
|
|
$subcount = $nlink - 2; # Number of subdirectories
|
|
for (@filenames) {
|
|
last if $subcount == 0;
|
|
next if $_ eq '.';
|
|
next if $_ eq '..';
|
|
next if $_ eq 'CVS';
|
|
$name = "$dir/$_";
|
|
|
|
($dev, $ino, $mode, $nlink) = lstat($name);
|
|
next unless -d _;
|
|
if (-x _ && -r _) {
|
|
print STDERR "$progname: Entering $name\n";
|
|
&traverse_cvs_tree($name, *callback, $nlink);
|
|
} else {
|
|
warn("Couldn't chdir to $name");
|
|
}
|
|
--$subcount;
|
|
}
|
|
}
|
|
}
|
|
|
|
# Consume one token from the already opened RCSFILE filehandle.
|
|
# Unescape string tokens, if necessary.
|
|
sub get_token {
|
|
# Erase all-whitespace lines.
|
|
while ($line_buffer =~ /^$/) {
|
|
die ("Unexpected EOF") if eof(RCSFILE);
|
|
$line_buffer = <RCSFILE>;
|
|
$line_buffer =~ s/^\s+//; # Erase leading whitespace
|
|
}
|
|
|
|
# A string of non-whitespace characters is a token ...
|
|
return $1 if ($line_buffer =~ s/^([^;@][^;\s]*)\s*//o);
|
|
|
|
# ...and so is a single semicolon ...
|
|
return ';' if ($line_buffer =~ s/^;\s*//o);
|
|
|
|
# ...or an RCS-encoded string that starts with an @ character.
|
|
$line_buffer =~ s/^@([^@]*)//o;
|
|
$token = $1;
|
|
|
|
# Detect single @ character used to close RCS-encoded string.
|
|
while ($line_buffer !~ /@/o || # Short-circuit optimization
|
|
$line_buffer !~ /([^@]|^)@([^@]|$)/o) {
|
|
$token .= $line_buffer;
|
|
die ("Unexpected EOF") if eof(RCSFILE);
|
|
$line_buffer = <RCSFILE>;
|
|
}
|
|
|
|
# Retain the remainder of the line after the terminating @ character.
|
|
$i = rindex($line_buffer, '@');
|
|
$token .= substr($line_buffer, 0, $i);
|
|
$line_buffer = substr($line_buffer, $i + 1);
|
|
|
|
# Undo escape-coding of @ characters.
|
|
$token =~ s/@@/@/og;
|
|
|
|
# Digest any extra blank lines.
|
|
while (($line_buffer =~ /^$/) && !eof(RCSFILE)) {
|
|
$line_buffer = <RCSFILE>;
|
|
}
|
|
return $token;
|
|
}
|
|
|
|
# Consume a token from RCS filehandle and ensure that it matches
|
|
# the given string constant.
|
|
sub match_token {
|
|
local ($match) = @_;
|
|
|
|
local ($token) = &get_token;
|
|
die "Unexpected parsing error in RCS file $rcs_pathname.\n",
|
|
"Expected token: $match, but saw: $token\n"
|
|
if ($token ne $match);
|
|
}
|
|
|
|
# Push RCS token back into the input buffer.
|
|
sub unget_token {
|
|
local ($token) = @_;
|
|
$line_buffer = $token . " " . $line_buffer;
|
|
}
|
|
|
|
# Parses "administrative" header of RCS files, setting these globals:
|
|
#
|
|
# $head_revision -- Revision for which cleartext is stored
|
|
# $principal_branch
|
|
# $file_description
|
|
# %revision_symbolic_name -- mapping from numerical revision # to symbolic tag
|
|
# %tag_revision -- mapping from symbolic tag to numerical revision #
|
|
#
|
|
sub parse_rcs_admin {
|
|
local ($token, $tag, $tag_name, $tag_revision);
|
|
|
|
# Undefine variables, because we may have already read another RCS file
|
|
undef %tag_revision;
|
|
undef %revision_symbolic_name;
|
|
|
|
while (1) {
|
|
# Read initial token at beginning of line
|
|
$token = &get_token(RCSFILE);
|
|
|
|
# We're done once we reach the description of the RCS tree
|
|
if ($token =~ /^\d/o) {
|
|
&unget_token($token);
|
|
return;
|
|
}
|
|
|
|
# print "token: $token\n";
|
|
|
|
if ($token eq "head") {
|
|
$head_revision = &get_token;
|
|
&get_token; # Eat semicolon
|
|
} elsif ($token eq "branch") {
|
|
$principal_branch = &get_token;
|
|
&get_token; # Eat semicolon
|
|
} elsif ($token eq "symbols") {
|
|
|
|
# Create an associate array that maps from tag name to
|
|
# revision number and vice-versa.
|
|
while (($tag = &get_token) ne ';') {
|
|
($tag_name, $tag_revision) = split(':', $tag);
|
|
|
|
$tag_revision{$tag_name} = $tag_revision;
|
|
$revision_symbolic_name{$tag_revision} = $tag_name;
|
|
}
|
|
} elsif ($token eq "comment") {
|
|
$file_description = &get_token;
|
|
&get_token; # Eat semicolon
|
|
|
|
# Ignore all these other fields - We don't care about them.
|
|
} elsif (($token eq "locks") ||
|
|
($token eq "strict") ||
|
|
($token eq "expand") ||
|
|
($token eq "access")) {
|
|
(1) while (&get_token ne ';');
|
|
} else {
|
|
warn ("Unexpected RCS token: $token\n");
|
|
}
|
|
}
|
|
|
|
die "Unexpected EOF";
|
|
}
|
|
|
|
# Construct associative arrays that represent the topology of the RCS tree
|
|
# and other arrays that contain info about individual revisions.
|
|
#
|
|
# The following associative arrays are created, keyed by revision number:
|
|
# %revision_date -- e.g. "96.02.23.00.21.52"
|
|
# %timestamp -- seconds since 12:00 AM, Jan 1, 1970 GMT
|
|
# %revision_author -- e.g. "tom"
|
|
# %revision_branches -- descendant branch revisions, separated by spaces,
|
|
# e.g. "1.21.4.1 1.21.2.6.1"
|
|
# %prev_revision -- revision number of previous *ancestor* in RCS tree.
|
|
# Traversal of this array occurs in the direction
|
|
# of the primordial (1.1) revision.
|
|
# %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.
|
|
# %next_delta -- revision number of next "delta". Inverts %prev_delta.
|
|
#
|
|
# Also creates %last_revision, keyed by a branch revision number, which
|
|
# indicates the latest revision on a given branch,
|
|
# e.g. $last_revision{"1.2.8"} == 1.2.8.5
|
|
#
|
|
sub parse_rcs_tree {
|
|
local($revision, $date, $author, $branches, $next);
|
|
local($branch, $is_trunk_revision);
|
|
local($mon,$day,$hhmm,$year);
|
|
# Undefine variables, because we may have already read another RCS file
|
|
undef %timestamp;
|
|
undef %revision_age;
|
|
undef %revision_author;
|
|
undef %revision_branches;
|
|
undef %revision_ctime;
|
|
undef %revision_date;
|
|
undef %prev_revision;
|
|
undef %prev_delta;
|
|
undef %next_delta;
|
|
undef %last_revision;
|
|
|
|
while (1) {
|
|
$revision = &get_token;
|
|
|
|
# End of RCS tree description ?
|
|
if ($revision eq 'desc') {
|
|
&unget_token($revision);
|
|
return;
|
|
}
|
|
|
|
$is_trunk_revision = ($revision =~ /^[0-9]+\.[0-9]+$/);
|
|
|
|
$tag_revision{$revision} = $revision;
|
|
($branch) = $revision =~ /(.*)\.[0-9]+/o;
|
|
$last_revision{$branch} = $revision;
|
|
|
|
# Parse date
|
|
&match_token('date');
|
|
$date = &get_token;
|
|
$revision_date{$revision} = $date;
|
|
&match_token(';');
|
|
|
|
# Convert date into timestamp
|
|
@date_fields = reverse(split(/\./, $date));
|
|
$date_fields[4]--; # Month ranges from 0-11, not 1-12
|
|
$timestamp{$revision} = &timegm(@date_fields);
|
|
|
|
# Compute date string; Format it the way I like.
|
|
|
|
($mon, $day, $hhmm, $year) = split(/#/,
|
|
time2str("%b#%e#%H:%M#%Y",
|
|
$timestamp{$revision}));
|
|
$revision_ctime{$revision} = "$day $mon $year $hhmm";
|
|
|
|
# Save age
|
|
$revision_age{$revision} =
|
|
($time - $timestamp{$revision}) / $SECS_PER_DAY;
|
|
|
|
# Parse author
|
|
&match_token('author');
|
|
$author = &get_token;
|
|
$revision_author{$revision} = $author;
|
|
&match_token(';');
|
|
|
|
# Parse state;
|
|
&match_token('state');
|
|
(1) while (&get_token ne ';');
|
|
|
|
# Parse branches
|
|
&match_token('branches');
|
|
$branches = '';
|
|
while (($token = &get_token) ne ';') {
|
|
$prev_revision{$token} = $revision;
|
|
$prev_delta{$token} = $revision;
|
|
$branches .= "$token ";
|
|
}
|
|
$revision_branches{$revision} = $branches;
|
|
|
|
# Parse revision of next delta in chain
|
|
&match_token('next');
|
|
$next = '';
|
|
if (($token = &get_token) ne ';') {
|
|
$next = $token;
|
|
&get_token; # Eat semicolon
|
|
$next_delta{$revision} = $next;
|
|
$prev_delta{$next} = $revision;
|
|
if ($is_trunk_revision) {
|
|
$prev_revision{$revision} = $next;
|
|
} else {
|
|
$prev_revision{$next} = $revision;
|
|
}
|
|
}
|
|
|
|
if ($debug >= 3) {
|
|
print "<pre>revision = $revision\n";
|
|
print "date = $date\n";
|
|
print "author = $author\n";
|
|
print "branches = $branches\n";
|
|
print "next = $next</pre>\n\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
sub parse_rcs_description {
|
|
&match_token('desc');
|
|
$rcs_file_description = &get_token;
|
|
}
|
|
|
|
# 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.
|
|
sub parse_rcs_deltatext {
|
|
undef %revision_log;
|
|
undef %revision_deltatext;
|
|
|
|
while (!eof(RCSFILE)) {
|
|
$revision = &get_token;
|
|
print "Reading delta for revision: $revision\n" if ($debug >= 3);
|
|
&match_token('log');
|
|
$revision_log{$revision} = &get_token;
|
|
&match_token('text');
|
|
$revision_deltatext{$revision} = &get_token;
|
|
}
|
|
}
|
|
|
|
# Reads and parses complete RCS file from already-opened RCSFILE descriptor.
|
|
sub parse_rcs_file {
|
|
print "Reading RCS admin...\n" if ($debug >= 2);
|
|
&parse_rcs_admin();
|
|
print "Reading RCS revision tree topology...\n" if ($debug >= 2);
|
|
&parse_rcs_tree();
|
|
|
|
if( $debug >= 3 ){
|
|
print "<pre>Keys:\n\n";
|
|
for $i (keys %tag_revision ){
|
|
$k = $tag_revision{$i};
|
|
print "yoyuo $i: $k\n";
|
|
}
|
|
print "</pre>\n";
|
|
}
|
|
|
|
&parse_rcs_description();
|
|
print "Reading RCS revision deltas...\n" if ($debug >= 2);
|
|
&parse_rcs_deltatext();
|
|
print "Done reading RCS file...\n" if ($debug >= 2);
|
|
}
|
|
|
|
# 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.
|
|
sub map_tag_to_revision {
|
|
local($tag_or_revision) = @_;
|
|
|
|
local ($revision) = $tag_revision{$tag_or_revision};
|
|
|
|
# Is this a branch tag, e.g. xxx.yyy.0.zzz
|
|
if ($revision =~ /(.*)\.0\.([0-9]+)/o) {
|
|
$branch = $1 . '.' . $2;
|
|
# Return latest revision on the branch, if any.
|
|
return $last_revision{$branch} if (defined($last_revision{$branch}));
|
|
return $1; # No revisions on branch - return branch point
|
|
} else {
|
|
return $revision;
|
|
}
|
|
}
|
|
|
|
# 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.
|
|
|
|
sub ancestor_revisions {
|
|
local ($revision) = @_;
|
|
local (@ancestors);
|
|
|
|
$revision = $prev_revision{$revision};
|
|
while ($revision) {
|
|
push(@ancestors, $revision);
|
|
$revision = $prev_revision{$revision};
|
|
}
|
|
|
|
return @ancestors;
|
|
}
|
|
|
|
# Extract the given revision from the digested RCS file.
|
|
# (Essentially the equivalent of cvs up -rXXX)
|
|
sub extract_revision {
|
|
local ($revision) = @_;
|
|
local (@path);
|
|
|
|
# Compute path through tree of revision deltas to most recent trunk revision
|
|
while ($revision) {
|
|
push(@path, $revision);
|
|
$revision = $prev_delta{$revision};
|
|
}
|
|
@path = reverse(@path);
|
|
shift @path; # Get rid of head revision
|
|
|
|
# Get complete contents of head revision
|
|
local (@text) = split(/^/, $revision_deltatext{$head_revision});
|
|
|
|
# Iterate, applying deltas to previous revision
|
|
foreach $revision (@path) {
|
|
$adjust = 0;
|
|
@diffs = split(/^/, $revision_deltatext{$revision});
|
|
|
|
local ($lines_added) = 0;
|
|
local ($lines_removed) = 0;
|
|
|
|
foreach $command (@diffs) {
|
|
if ($add_lines_remaining > 0) {
|
|
# Insertion lines from a prior "a" command.
|
|
splice(@text, $start_line + $adjust,
|
|
0, $command);
|
|
$add_lines_remaining--;
|
|
$adjust++;
|
|
} elsif ($command =~ /^d(\d+)\s(\d+)/) {
|
|
# "d" - Delete command
|
|
($start_line, $count) = ($1, $2);
|
|
splice(@text, $start_line + $adjust - 1, $count);
|
|
$adjust -= $count;
|
|
$lines_removed += $count;
|
|
} elsif ($command =~ /^a(\d+)\s(\d+)/) {
|
|
# "a" - Add command
|
|
($start_line, $count) = ($1, $2);
|
|
$add_lines_remaining = $count;
|
|
$lines_added += $lines_added;
|
|
} else {
|
|
die "Error parsing diff commands";
|
|
}
|
|
}
|
|
$lines_removed{$revision} += $lines_removed;
|
|
$lines_added{$revision} += $lines_added;
|
|
}
|
|
|
|
return @text;
|
|
}
|
|
|
|
sub parse_cvs_file {
|
|
local($rcs_pathname) = @_;
|
|
|
|
# Args in: $opt_rev - requested revision
|
|
# $opt_m - time since modified
|
|
# Args out: @revision_map
|
|
# $revision
|
|
# %timestamp
|
|
# (%revision_deltatext)
|
|
|
|
@revision_map = ();
|
|
CheckHidden($rcs_pathname);
|
|
|
|
die "$progname: error: This file appeared to be under CVS control, " .
|
|
"but the RCS file is inaccessible.\n(Couldn't open '$rcs_pathname')\n"
|
|
if !open (RCSFILE, "< $rcs_pathname");
|
|
&parse_rcs_file();
|
|
close(RCSFILE);
|
|
|
|
if (!defined($opt_rev) || $opt_rev eq '' || $opt_rev eq 'HEAD') {
|
|
# Explicitly specified topmost revision in tree
|
|
$revision = $head_revision;
|
|
} else {
|
|
# Symbolic tag or specific revision number specified.
|
|
$revision = &map_tag_to_revision($opt_rev);
|
|
die "$progname: error: -r: No such revision: $opt_rev\n"
|
|
if ($revision eq '');
|
|
}
|
|
|
|
# The primordial revision is not always 1.1! Go find it.
|
|
my $primordial = $revision;
|
|
while ($prev_revision{$primordial} != "") {
|
|
$primordial = $prev_revision{$primordial};
|
|
}
|
|
|
|
# Don't display file at all, if -m option is specified and no
|
|
# changes have been made in the specified file.
|
|
return if ($opt_m && $timestamp{$revision} < $opt_m_timestamp);
|
|
|
|
# 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 = split(/^/, $revision_deltatext{$head_revision});
|
|
for ($rev = $prev_revision{$head_revision}; $rev;
|
|
$rev = $prev_revision{$rev}) {
|
|
@diffs = split(/^/, $revision_deltatext{$rev});
|
|
foreach $command (@diffs) {
|
|
if ($skip > 0) {
|
|
# Skip insertion lines from a prior "a" command.
|
|
$skip--;
|
|
} elsif ($command =~ /^d(\d+)\s(\d+)/) {
|
|
# "d" - Delete command
|
|
($start_line, $count) = ($1, $2);
|
|
$line_count -= $count;
|
|
} elsif ($command =~ /^a(\d+)\s(\d+)/) {
|
|
# "a" - Add command
|
|
($start_line, $count) = ($1, $2);
|
|
$skip = $count;
|
|
$line_count += $count;
|
|
} else {
|
|
die "$progname: error: illegal RCS file $rcs_pathname\n",
|
|
" error appears in revision $rev\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
# 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.
|
|
while ($line_count--) {
|
|
push(@revision_map, $primordial);
|
|
}
|
|
|
|
@ancestors = &ancestor_revisions($revision);
|
|
unshift (@ancestors, $revision); #
|
|
pop @ancestors; # Remove "1.1"
|
|
$last_revision = $primordial;
|
|
foreach $revision (reverse @ancestors) {
|
|
$is_trunk_revision = ($revision =~ /^[0-9]+\.[0-9]+$/);
|
|
|
|
if ($is_trunk_revision) {
|
|
@diffs = split(/^/, $revision_deltatext{$last_revision});
|
|
|
|
# Revisions on the trunk specify deltas that transform a
|
|
# revision into an earlier revision, so invert the translation
|
|
# of the 'diff' commands.
|
|
foreach $command (@diffs) {
|
|
if ($skip > 0) {
|
|
$skip--;
|
|
} else {
|
|
if ($command =~ /^d(\d+)\s(\d+)$/) { # Delete command
|
|
($start_line, $count) = ($1, $2);
|
|
|
|
$#temp = -1;
|
|
while ($count--) {
|
|
push(@temp, $revision);
|
|
}
|
|
splice(@revision_map, $start_line - 1, 0, @temp);
|
|
} elsif ($command =~ /^a(\d+)\s(\d+)$/) { # Add command
|
|
($start_line, $count) = ($1, $2);
|
|
splice(@revision_map, $start_line, $count);
|
|
$skip = $count;
|
|
} else {
|
|
die "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;
|
|
@diffs = split(/^/, $revision_deltatext{$revision});
|
|
foreach $command (@diffs) {
|
|
if ($skip > 0) {
|
|
$skip--;
|
|
} else {
|
|
if ($command =~ /^d(\d+)\s(\d+)$/) { # Delete command
|
|
($start_line, $count) = ($1, $2);
|
|
splice(@revision_map, $start_line + $adjust - 1, $count);
|
|
$adjust -= $count;
|
|
} elsif ($command =~ /^a(\d+)\s(\d+)$/) { # Add command
|
|
($start_line, $count) = ($1, $2);
|
|
|
|
$skip = $count;
|
|
$#temp = -1;
|
|
while ($count--) {
|
|
push(@temp, $revision);
|
|
}
|
|
splice(@revision_map, $start_line + $adjust, 0, @temp);
|
|
$adjust += $skip;
|
|
} else {
|
|
die "Error parsing diff commands";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$last_revision = $revision;
|
|
}
|
|
$revision;
|
|
}
|
|
|
|
__END__
|
|
#
|
|
# The following are parts of the original cvsblame script that are not
|
|
# used for cvsblame.pl
|
|
#
|
|
|
|
# Read CVS/Entries and CVS/Repository files.
|
|
#
|
|
# Creates these associative arrays, keyed by the CVS file pathname
|
|
#
|
|
# %cvs_revision -- Revision # present in working directory
|
|
# %cvs_date
|
|
# %cvs_sticky_revision -- Sticky tag, if any
|
|
#
|
|
# Also, creates %cvs_files, keyed by the directory path, which contains
|
|
# a space-separated list of the files under CVS control in the directory
|
|
sub read_cvs_entries
|
|
{
|
|
local ($directory) = @_;
|
|
local ($filename, $rev, $date, $idunno, $sticky, $pathname);
|
|
|
|
$cvsdir = $directory . '/CVS';
|
|
|
|
CheckHidden($cvsdir);
|
|
|
|
return if (! -d $cvsdir);
|
|
|
|
return if !open(ENTRIES, "< $cvsdir/Entries");
|
|
|
|
while(<ENTRIES>) {
|
|
chop;
|
|
($filename, $rev, $date, $idunno, $sticky) = split("/", substr($_, 1));
|
|
($pathname) = $directory . "/" . $filename;
|
|
$cvs_revision{$pathname} = $rev;
|
|
$cvs_date{$pathname} = $date;
|
|
$cvs_sticky_revision{$pathname} = $sticky;
|
|
$cvs_files{$directory} .= "$filename\\";
|
|
}
|
|
close(ENTRIES);
|
|
|
|
return if !open(REPOSITORY, "< $cvsdir/Repository");
|
|
$repository = <REPOSITORY>;
|
|
chop($repository);
|
|
close(REPOSITORY);
|
|
$repository{$directory} = $repository;
|
|
}
|
|
|
|
# Given path to file in CVS working directory, compute path to RCS
|
|
# repository file. Cache that info for future use.
|
|
|
|
|
|
sub rcs_pathname {
|
|
($pathname) = @_;
|
|
|
|
if ($pathname =~ m@/@) {
|
|
($directory,$filename) = $pathname =~ m@(.*)/([^/]+)$@;
|
|
} else {
|
|
($directory,$filename) = ('.',$pathname);
|
|
$pathname = "./" . $pathname;
|
|
}
|
|
if (!defined($repository{$directory})) {
|
|
&read_cvs_entries($directory);
|
|
}
|
|
|
|
if (!defined($cvs_revision{$pathname})) {
|
|
die "$progname: error: File '$pathname' does not appear to be under" .
|
|
" CVS control.\n"
|
|
}
|
|
|
|
print STDERR "file: $filename\n" if $debug;
|
|
local ($rcs_path) = $repository{$directory} . '/' . $filename . ',v';
|
|
return $rcs_path if (-r $rcs_path);
|
|
|
|
# A file that exists only on the branch, not on the trunk, is found
|
|
# in the Attic subdir.
|
|
return $repository{$directory} . '/Attic/' . $filename . ',v';
|
|
}
|
|
|
|
sub show_annotated_cvs_file {
|
|
local($pathname) = @_;
|
|
local(@output) = ();
|
|
|
|
$revision = &parse_cvs_file($pathname);
|
|
|
|
@text = &extract_revision($revision);
|
|
die "$progname: Internal consistency error" if ($#text != $#revision_map);
|
|
|
|
# Set total width of line annotation.
|
|
# Warning: field widths here must match format strings below.
|
|
$annotation_width = 0;
|
|
$annotation_width += 8 if $opt_a; # author
|
|
$annotation_width += 7 if $opt_v; # revision
|
|
$annotation_width += 6 if $opt_A; # age
|
|
$annotation_width += 12 if $opt_d; # date
|
|
$blank_annotation = ' ' x $annotation_width;
|
|
|
|
if ($multiple_files_on_command_line) {
|
|
print "\n", "=" x (83 + $annotation_width);
|
|
print "\n$progname: Listing file: $pathname\n"
|
|
}
|
|
|
|
# Print each line of the revision, preceded by its annotation.
|
|
$line = 0;
|
|
foreach $revision (@revision_map) {
|
|
$text = $text[$line++];
|
|
$annotation = '';
|
|
|
|
# Annotate with revision author
|
|
$annotation .= sprintf("%-8s", $revision_author{$revision}) if $opt_a;
|
|
|
|
# Annotate with revision number
|
|
$annotation .= sprintf(" %-6s", $revision) if $opt_v;
|
|
|
|
# Date annotation
|
|
$annotation .= " $revision_ctime{$revision}" if $opt_d;
|
|
|
|
# Age annotation ?
|
|
$annotation .= sprintf(" (%3s)",
|
|
int($revision_age{$revision})) if $opt_A;
|
|
|
|
# -m (if-modified-since) annotion ?
|
|
if ($opt_m && ($timestamp{$revision} < $opt_m_timestamp)) {
|
|
$annotation = $blank_annotation;
|
|
}
|
|
|
|
# Suppress annotation of whitespace lines, if requested;
|
|
$annotation = $blank_annotation if $opt_w && ($text =~ /^\s*$/);
|
|
|
|
# printf "%4d ", $line if $opt_l;
|
|
# print "$annotation - $text";
|
|
push(@output, sprintf("%4d ", $line)) if $opt_l;
|
|
push(@output, "$annotation - $text");
|
|
}
|
|
@output;
|
|
}
|
|
|
|
sub usage {
|
|
die
|
|
"$progname: usage: [options] [file|dir]...\n",
|
|
" Options:\n",
|
|
" -r <revision> Specify CVS revision of file to display\n",
|
|
" <revision> can be any of:\n",
|
|
" + numeric tag, e.g. 1.23,\n",
|
|
" + symbolic branch or revision tag, e.g. CHEDDAR,\n",
|
|
" + HEAD keyword (most recent revision on trunk)\n",
|
|
" -a Annotate lines with author (username)\n",
|
|
" -A Annotate lines with age, in days\n",
|
|
" -v Annotate lines with revision number\n",
|
|
" -d Annotate lines with date, in local time zone\n",
|
|
" -l Annotate lines with line number\n",
|
|
" -w Don't annotate all-whitespace lines\n",
|
|
" -m <# days> Only annotate lines modified within last <# days>\n",
|
|
" -h Print help (this message)\n\n",
|
|
" (-a -v assumed, if none of -a, -v, -A, -d supplied)\n"
|
|
;
|
|
}
|
|
|
|
&usage if (!&Getopts('r:m:Aadhlvw'));
|
|
&usage if ($opt_h); # help option
|
|
|
|
$multiple_files_on_command_line = 1 if ($#ARGV != 0);
|
|
|
|
&cvsblame_init;
|
|
|
|
sub annotate_cvs_directory
|
|
{
|
|
local($dir) = @_;
|
|
&read_cvs_entries($dir);
|
|
foreach $file (split(/\\/, $cvs_files{$dir})) {
|
|
&show_annotated_cvs_file("$dir/$file");
|
|
}
|
|
}
|
|
|
|
# No files on command-line ? Use current directory.
|
|
push(@ARGV, '.') if ($#ARGV == -1);
|
|
|
|
# Iterate over files/directories on command-line
|
|
while ($#ARGV >= 0) {
|
|
$pathname = shift @ARGV;
|
|
# Is it a directory ?
|
|
if (-d $pathname) {
|
|
&traverse_cvs_tree($pathname, *annotate_cvs_directory);
|
|
|
|
# No, it must be a file.
|
|
} else {
|
|
&show_annotated_cvs_file($pathname);
|
|
}
|
|
}
|