Cloned library of VTK-5.0.0 with extra build files for internal package management.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1052 lines
36 KiB

#!/usr/bin/env perl
# Time-stamp: <2002-11-01 15:33:04 barre>
#
# Get author and contributors.
#
# barre : Sebastien Barre <sebastien@barre.nom.fr>
#
# 0.8 (barre) :
# - Add --cvsweb and --cvsweb_suffix to report links to CVSweb logs
#
# 0.7 (barre) :
# - Fix empty cached log file pb (regenerated)
# - Add link from name to detailed description of contribution
#
# 0.6 (barre) :
# - Change --history_img in order to create any contribution graph(s)
#
# 0.5 (barre) :
# - Contribution graphs for the whole period (back to 1994).
#
# 0.4 (barre) :
# - D'oh ! Month returned by localtime start with 0 (=january).
#
# 0.3 (barre) :
# - add history feature + gnuplot fig
#
# 0.2 (barre) :
# - now handles most files
#
# 0.1 (barre) :
# - first release
use Carp;
use Cwd 'abs_path', 'cwd';
use Getopt::Long;
use Fcntl;
use File::Basename;
use File::Find;
use File::Path;
use POSIX;
use strict;
use FileHandle;
use Time::Local;
my ($VERSION, $PROGNAME, $AUTHOR) = (0.8, $0, "Sebastien Barre");
$PROGNAME =~ s/^.*[\\\/]//;
# -------------------------------------------------------------------------
# Defaults (add options as you want: "verbose" => 1 for default verbose mode)
my %default =
(
dirs => ["../.."],
cachedir => $ENV{TMP} . "/cache",
class_group => '^(vtk[A-Z0-9][A-Za-z0-9]+)\.(?:c|cpp|cxx|h|mm)$',
files_in => '(?:^hints|^README|\.(?:c|cmake|cpp|cxx|doc|h|html|in|java|mm|pl|py|tcl|txt))$',
files_out => '(?:^vtkVersion\.\w+|^pkgIndex\.tcl|^vtkParse\.tab\.c|\.yy\.c)$',
gnuplot_file => '../../../VTK-doxygen/contrib/history.plt',
history_dir => '../../../VTK-doxygen/contrib',
history_img => ['|lines|../../../VTK-doxygen/contrib/history.png',
'730|lines|../../../VTK-doxygen/contrib/history2y.png',
'180|linespoints|../../../VTK-doxygen/contrib/history6m.png'],
history_max_nb => 12,
lines_add => 1.0,
lines_rem => 0.5,
massive => 200,
max_class_nb => 10,
max_file_nb => 5,
min_class => 0.02,
min_file => 0.01,
min_contrib => 0.05,
min_gcontrib => 0.0001,
relativeto => "../..",
store => "doc_VTK_contributors.dox",
to => "../../../VTK-doxygen"
);
# -------------------------------------------------------------------------
# Parse options
my %args;
Getopt::Long::Configure("bundling");
GetOptions (\%args, "help", "verbose|v", "authors=s", "cachedir=s", "class_group=s", "cvsweb=s", "cvsweb_suffix=s", "files_in=s", "files_out=s", "gnuplot_file=s", "history_dir=s", "history_img=s@", "history_max_nb=i", "lines_add=f", "lines_rem=f", "massive=i", "max_class_nb=i", "max_file_nb=i", "min_class=f", "min_file=f", "min_contrib=f", "min_gcontrib=f", "relativeto=s", "store=s", "to=s");
print "$PROGNAME $VERSION, by $AUTHOR\n";
if (exists $args{"help"}) {
print <<"EOT";
Usage : $PROGNAME [--help] [--verbose|-v] [--authors file] [--cachedir path] [--files_in string] [--files_out string] [--gnuplot_file string] [--history_dir string] [--history_img string] [--history_max_nb number] [--lines_add number] [--lines_rem number] [--massive number] [--max_class_nb number] [--max_file_nb number] [--min_class number] [--min_file number] [--min_contrib number] [--min_gcontrib number] [--store file] [--relativeto path] [--to path] [files|directories...]
--help : this message
--verbose|-v : verbose (display filenames while processing)
--authors file : use 'file' to read authors list
--cachedir path : use 'path' as cache directory for CVS logs (default: $default{cachedir})
--files_in s : accept only file names (without path) matching 's' (default: $default{files_in})
--files_out s : (then) reject file names (without path) matching 's' (default: $default{files_out})
--gnuplot_file s : use 's' to store gnuplot command file (default: $default{gnuplot_file})
--history_dir s : history dir (default: $default{history_dir})
--history_img d|s : create history image 's' graphing contribs up to 'd' ago (default: $default{history_img})
--history_max_nb n : use at most 'n' authors in history pic (default: $default{history_max_nb})
--lines_add n : use 'n' as weight for added lines (default: $default{lines_add})
--lines_rem n : use 'n' as weight for removed lines (default: $default{lines_rem})
--massive n : use 'n' as minimum threshold for massive commits removal (default: $default{massive})
--max_class_nb n : do not display more than 'n' classes by contributor (default: $default{max_class_nb})
--max_file_nb n : do not display more than 'n' files by contributor (default: $default{max_file_nb})
--min_class n : display classes that represent more than 'n' % of author's contribution (default: $default{min_class})
--min_file n : display files that represent more than 'n' % of author's contribution (default: $default{min_file})
x --min_contrib n : display authors who represent more than 'n' % of classe's contribution (default: $default{min_contrib})
--min_gcontrib n : display authors who represent more than 'n' % of total contribution (default: $default{min_gcontrib})
--cvsweb s : use 's' as a base link to the CVSweb site
--cvsweb_suffix s : use 's' as a suffix to the link to the CVSweb site
--store file : use 'file' to store doc (default: $default{store})
--relativeto path: each file/directory to document is considered relative to 'path', where --to and --relativeto should be absolute (default: $default{relativeto})
--to path : use 'path' as destination directory (default: $default{to})
Example:
$PROGNAME --to ../vtk-doxygen
$PROGNAME contrib
EOT
exit;
}
foreach my $option (
"cachedir",
"class_group",
"files_in",
"files_out",
"gnuplot_file",
"history_dir",
"history_img",
"history_max_nb",
"lines_add",
"lines_rem",
"massive",
"min_contrib",
"min_gcontrib",
"min_class",
"min_file",
"max_class_nb",
"max_file_nb",
"relativeto",
"store",
"to") {
$args{$option} = $default{$option}
if ! exists $args{$option} && exists $default{$option};
}
$args{"verbose"} = 1 if exists $default{"verbose"};
$args{"cachedir"} =~ s/[\\\/]*$// if exists $args{"cachedir"};
$args{"history_dir"} =~ s/[\\\/]*$// if exists $args{"history_dir"};
$args{"relativeto"} =~ s/[\\\/]*$// if exists $args{"relativeto"};
$args{"to"} =~ s/[\\\/]*$// if exists $args{"to"};
my $os_is_win = ($^O =~ m/(MSWin32|Cygwin)/i);
my $open_file_as_text = $os_is_win ? O_TEXT : 0;
STDOUT->autoflush;
STDERR->autoflush;
my $start_time = time();
# -------------------------------------------------------------------------
# Read the authors list
# %authors is indexed by login name (i.e. $authors{$name})
# {'name'}: full name of the user
# {'email'}: email of the user
#
# %authors_aliases is indexed by aliased login name
# $authors_aliases{$name} is the real login name for $name
my %authors;
my %authors_aliases;
sub trim_spaces {
my @out = @_;
for (@out) {
s/^\s*//;
s/\s*$//;
}
return wantarray ? @out : $out[0];
}
# Grab each author, line by line, setup aliases
# Example:
# will, schroede: Schroeder, Will (will.schroeder@kitware.com)
my $authors_file_loaded = 0;
if (exists $args{"authors"}) {
if (!sysopen(AUTHORS_FILE,
$args{"authors"},
O_RDONLY|$open_file_as_text)) {
carp "$PROGNAME: unable to open authors list ". $args{"authors"}. "\n";
} else {
print "Reading the authors list from ". $args{"authors"} . "...\n";
my @authors_file = <AUTHORS_FILE>;
close(AUTHORS_FILE);
$authors_file_loaded = 1;
foreach my $line (@authors_file) {
if ($line && $line =~ m/^\s*(.+?)\s*:\s*(.+?)\s*\(\s*(.*?)\s*\)/) {
my @aliases = split(',', $1);
my $author = trim_spaces(shift @aliases);
$authors{$author}{'name'} = $2;
$authors{$author}{'email'} = $3;
foreach my $alias (@aliases) {
$authors_aliases{trim_spaces($alias)} = $author;
}
print " >> $author: ", $authors{$author}{'name'},
" (", $authors{$author}{'email'}, ")\n"
if exists $args{"verbose"};
}
}
print " => ", scalar keys %authors, " authors(s) read from ", basename($args{"authors"}), ".\n";
}
}
# -------------------------------------------------------------------------
# Parse a revision
#
# Example:
# 1.2
# date: 2001/06/05 15:34:17; author: barre; state: Exp; lines: +10 -3
# fix PrintSelf defects
sub parse_revision {
my $ref = shift;
$$ref =~ /([\d\.]*)\n(.*)\n(.*)/;
my ($revision, $fields_line, $message) = ($1, $2, $3);
my ($author, $date, $time, $lines_add, $lines_rem) =
(undef, undef, undef, undef, undef);
my @fields = split (';', $fields_line);
if ($fields[0] =~ m/date:\s+(\d+-\d+-\d+)\s+(\d+:\d+:\d+)\s+\+0000$/) {
($date, $time) = ($1, $2);
} else {
carp "Unable to find date of revision!\n";
}
if ($fields[1] =~ m/author:\s+(.+)$/) {
$author = $1;
} else {
carp "Unable to find author of revision!\n";
}
if (exists $fields[3] && $fields[3] =~ m/lines:\s+\+(\d+)\s+\-(\d+)$/) {
($lines_add, $lines_rem) = ($1, $2);
}
return ($revision, $date, $time, $author, $lines_add, $lines_rem, $message);
}
# -------------------------------------------------------------------------
# Collect all files and directories
# Avoid all CVS/ directories and third-party libs directories (identified
# by a .NoDartCoverage file)
push @ARGV, @{$default{dirs}} if !@ARGV;
print "Collecting...\n";
my %files;
foreach my $file (@ARGV) {
if (-f $file) {
$files{$file} = 1;
} elsif (-d $file) {
find sub {
if ($File::Find::dir =~ /\bCVS$/ ||
-e '.NoDartCoverage') {
$File::Find::prune = 1;
} else {
$files{$File::Find::name} = 1 if -f $_;
}
}, $file;
}
}
# -------------------------------------------------------------------------
# Process files and get logs
print "Processing files and getting logs...\n";
my $intermediate_time = time();
my $nb_revisions = 0;
# %log_by_file_revision is indexed by file and revision
# (i.e. $log_by_file_revision{$file_name}{$revision_number})
# {'date'}: revision's date
# {'author'}: revision's author
# {'lines_add'}: number of lines added for that revision
# {'lines_rem'}: number of lines removed for that revision
my %log_by_file_revision;
# %log_revision_by_signature_file is indexed by signature and file
# (i.e. $log_revision_by_signature_file{$signature}{$file_name})
# (hash) all revisions matching this signature and that file.
# The signature is made of the rev date, author and message.
my %log_revision_by_signature_file;
my @files_submitted = sort keys %files;
my $nb_file_submitted = scalar @files_submitted;
my $nb_file_fraction = ceil($nb_file_submitted / 10.0);
my %files_visited;
sub get_short_relative_name {
my ($file_name, $relative_to) = @_;
$file_name =~ s/^$relative_to//;
return $file_name;
}
foreach my $file_name (@files_submitted) {
# print " ? $file_name\n" if exists $args{"verbose"};
# Progress meter
if ($nb_file_submitted % $nb_file_fraction == 0) {
print " - Remaining files: $nb_file_submitted\n";
}
--$nb_file_submitted;
# Reject file if not matching pattern or already visited
my $file_name_basename = basename($file_name);
next if ($file_name_basename !~ m/$args{"files_in"}/ ||
$file_name_basename =~ m/$args{"files_out"}/ ||
exists $files_visited{$file_name});
print " $file_name\n" if exists $args{"verbose"};
# Get the CVS log for that file or grab it from the cache
my $cache_name = $args{"cachedir"} .
get_short_relative_name($file_name, $args{"relativeto"}) . '.log';
my $output;
my $old_slurp = $/;
undef $/; # slurp mode
# Use the cache if it exists, is not older than file, and not empty
if (-e $cache_name &&
(stat $cache_name)[9] >= (stat $file_name)[9] &&
(stat $cache_name)[7]) {
sysopen(CACHE_FILE,
$cache_name,
O_RDONLY|$open_file_as_text)
or croak "$PROGNAME: unable to open cache file $cache_name\n";
$output = <CACHE_FILE>;
close(CACHE_FILE);
} else {
my $file_name_dir = dirname($file_name);
print " >> $file_name_dir: cvs log -b $file_name_basename\n"
if exists $args{"verbose"};
my $current = cwd;
chdir($file_name_dir);
$output = qx/cvs log -b $file_name_basename/;
chdir($current);
mkpath(dirname($cache_name));
sysopen(CACHE_FILE,
$cache_name,
O_WRONLY|O_TRUNC|O_CREAT|$open_file_as_text)
or croak "$PROGNAME: unable to open cache file $cache_name\n";
print CACHE_FILE $output;
close(CACHE_FILE);
}
$/ = $old_slurp;
if (! $output) {
carp " >> Empty output from CVS log -b $file_name\n";
next;
}
$files_visited{$file_name} = 1;
# Process revisions
my @file_revisions =
split('----------------------------\nrevision ', $output);
shift @file_revisions;
# Store each revision
my $lines_added = 0;
foreach my $revision (@file_revisions) {
my ($revision, $date, $time, $author,
$lines_add, $lines_rem, $message) = parse_revision(\$revision);
# Resolve author aliases and use full name
$author = $authors_aliases{$author}
if exists $authors_aliases{$author};
$nb_revisions++;
# Store date/author
$log_by_file_revision{$file_name}{$revision}{'date'} = $date;
$log_by_file_revision{$file_name}{$revision}{'author'} = $author;
# Store lines added and removed. Load the file and count the
# lines for rev 1.1
if ($revision ne '1.1') {
$log_by_file_revision{$file_name}{$revision}{'lines_add'}
= $lines_add;
$log_by_file_revision{$file_name}{$revision}{'lines_rem'}
= $lines_rem;
$lines_added += $lines_add - $lines_rem;
$log_revision_by_signature_file
{"$date: $author: $message"}{$file_name}{$revision} = 1;
}
}
# Now get the number of lines for 1.1 (not stored in the log)
# Read the current file, add all changes
sysopen(FILE,
$file_name,
O_RDONLY|$open_file_as_text)
or croak "$PROGNAME: unable to open file $file_name\n";
my @lines = <FILE>;
close(FILE);
$log_by_file_revision{$file_name}{'1.1'}{'lines_add'}
= (scalar @lines) - $lines_added;
if ($log_by_file_revision{$file_name}{'1.1'}{'lines_add'} < 0) {
print " >> $file_name: ", $log_by_file_revision{$file_name}{'1.1'}{'lines_add'}, " lines !\n";
}
$log_by_file_revision{$file_name}{'1.1'}{'lines_rem'} = 0;
}
print " => $nb_revisions revision(s) stored from ", scalar keys %files_visited, " file(s) in ", time() - $intermediate_time, " s. \n";
# -------------------------------------------------------------------------
# Remove massive commits (copyright changes for example)
print "Removing massive commits (> " . $args{"massive"} . ") ...\n";
$intermediate_time = time();
my $nb_removed = 0;
# Remove these changes that have been commited to more than 'n' files
# in the same day by the same author with same log message (= signature)
foreach my $signature (sort { (scalar keys %{$log_revision_by_signature_file{$b}}) <=> (scalar keys %{$log_revision_by_signature_file{$a}}) }
keys %log_revision_by_signature_file) {
my @files = keys %{$log_revision_by_signature_file{$signature}};
if (scalar @files > $args{"massive"}) {
print " >> Removed: (" . scalar @files . ")\n " .
substr($signature, 0, 130) . "...\n\n" if exists $args{"verbose"};
foreach my $name (@files) {
foreach my $revision
(keys %{$log_revision_by_signature_file{$signature}{$name}}) {
++$nb_removed;
delete $log_by_file_revision{$name}{$revision};
}
}
}
}
print " => ", $nb_revisions - $nb_removed, " revision(s) kept ($nb_removed revision(s) removed) in ", time() - $intermediate_time, " s. \n";
# -------------------------------------------------------------------------
# Compute the contributions
print "Contributing contributions (sum(added * ", $args{"lines_add"}, " + removed * ", $args{"lines_rem"}, "))...\n";
# %contribution_by_author_file is indexed by author and file name
# (i.e $contribution_by_author_file{$author}{$file_name})
# stores the total contribution of this author for that specific file.
my %contribution_by_author_file;
# %contribution_by_author_date is indexed by author and date
# (i.e $contribution_by_author_date{$author}{$date})
# stores the total *accumulative* contribution of this author up to that date.
# (computed it 2 steps)
my %contribution_by_author_date;
# %contribution_by_author_class is indexed by author and class name
# (i.e $contribution_by_author_class{$author}{$class_name})
# stores the total contribution of this author for that specific class.
my %contribution_by_author_class;
# %classes is indexed by class name (i.e. $classes{$class_name})
# {'files'}: (hash) all files describing that class (header, implem, etc.)
# {'creators'}: (hash) all creators (authors of rev 1.1) for that class
# {'contributors'}: (hash) all contributors for that class
my %classes;
# %not_class_file_by_author is indexed by author
# (i.e $not_class_file_by_author{$author})
# (hash) files contributed by author but not part of a class group.
my %not_class_file_by_author;
$intermediate_time = time();
# Browse each file, each revision and use contribution
foreach my $file_name (keys %files_visited) {
# Check if file is part of a class group
my $class_name = undef;
if (basename($file_name) =~ m/$args{"class_group"}/) {
$class_name = $1;
print " >> $class_name: $file_name\n" if exists $args{"verbose"};
$classes{$class_name}{'files'}{$file_name} = 1;
$classes{$class_name}{'creators'}{$log_by_file_revision{$file_name}{'1.1'}{'author'}} = 1;
}
foreach my $revision (keys %{$log_by_file_revision{$file_name}}) {
my $author = $log_by_file_revision{$file_name}{$revision}{'author'};
# Store contribution
my $revision_contribution =
$log_by_file_revision{$file_name}{$revision}{'lines_add'} *
$args{"lines_add"} +
$log_by_file_revision{$file_name}{$revision}{'lines_rem'} *
$args{"lines_rem"};
$contribution_by_author_file{$author}{$file_name} +=
$revision_contribution;
$contribution_by_author_date{$author}{$log_by_file_revision{$file_name}{$revision}{'date'}} +=
$revision_contribution;
# File is part of a class, store the whole class contributors
# and contributions
if (defined($class_name)) {
$classes{$class_name}{'contributors'}{$author} = 1;
$contribution_by_author_class{$author}{$class_name} +=
$revision_contribution;
} else {
$not_class_file_by_author{$author}{$file_name} = 1;
}
}
}
print " => computed in ", time() - $intermediate_time, " s. \n";
# -------------------------------------------------------------------------
# Find contributors for each class, update header
print "Finding contributors > ", int($args{"min_contrib"} * 100.0), " and updating class headers...\n";
mkpath($args{"to"});
$intermediate_time = time();
my $nb_updated = 0;
my $class_name;
my @classes_names = sort { $b cmp $a } keys %classes;
while (@classes_names) {
$class_name = pop @classes_names;
# Sort the contributors for that class according to their
# respective contributions.
my @class_contributors_sorted =
sort {$contribution_by_author_class{$b}{$class_name} <=>
$contribution_by_author_class{$a}{$class_name}}
keys %{$classes{$class_name}{'contributors'}};
# Compute the total contribution for that class
my $total_class_contribution;
foreach my $class_contributor (@class_contributors_sorted) {
$total_class_contribution +=
$contribution_by_author_class{$class_contributor}{$class_name};
}
# Find the class header name
my $source_header_name = undef;
foreach my $file_name (keys %{$classes{$class_name}{'files'}}) {
if ($file_name =~ m/\.h$/) {
$source_header_name = $file_name;
last;
}
}
next if ! defined $source_header_name;
# Figure out the name of the already-converted-to-doxygen header
# file using --to and --relativeto destination file now
my $header;
# If source has absolute path, just use the basename (unless a
# relativeto path has been set) otherwise remove the ../ component
# before the source filename, so that it might be appended to the
# "to" directory.
if ($source_header_name =~ m/^(\/|[a-zA-W]\:[\/\\])/) {
if ($args{"relativeto"}) {
my ($dir, $absrel) = (abs_path(dirname($source_header_name)),
abs_path($args{"relativeto"}));
$dir =~ s/$absrel//;
$header = $args{"to"} . $dir . '/' . basename($source_header_name);
} else {
$header = $args{"to"} . '/' . basename($source_header_name);
}
} else {
$source_header_name =~ s/^(\.\.[\/\\])*//;
$header = $args{"to"} . '/' . $source_header_name;
}
next if ! -e $header;
# Read that header
my $old_slurp = $/;
undef $/; # slurp mode
if (!sysopen(HEADERFILE,
$header,
O_RDONLY|$open_file_as_text)) {
carp "$PROGNAME: unable to open $header\n";
next;
}
my $headerfile = <HEADERFILE>;
close(HEADERFILE);
$/ = $old_slurp;
# Search for the documentation block (@class ...)
if ($headerfile !~ /(.*\/\*\!\s+)(\@class\s.+?)(\*\/.*)/gms) {
carp "$PROGNAME: no documentation block in $header ! (skipping)\n";
next;
}
my ($pre, $block, $post) = ($1, $2, $3);
# Create new doc section, insert it into block
my $preamble = " \@par Created by:\n";
my $doc = $preamble . " - " .
join("\n - ",
sort map(exists $authors{$_}{'name'} ? $authors{$_}{'name'} : $_, keys %{$classes{$class_name}{'creators'}})) . "\n";
$doc .= "\n \@par CVS contributions (if > " . int(100.0 * $args{"min_contrib"}) . "%):\n";
foreach my $class_contributor (@class_contributors_sorted) {
my $ratio =
$contribution_by_author_class{$class_contributor}{$class_name} /
$total_class_contribution;
last if $ratio < $args{"min_contrib"};
$doc .= " - " . (exists $authors{$class_contributor}{'name'} ? $authors{$class_contributor}{'name'} : $class_contributor) . " (" . int($ratio * 100.0) . "%)\n";
}
if (exists $args{"cvsweb"}) {
$doc .= "\n \@par CVS logs (CVSweb):\n";
foreach my $file (keys %{$classes{$class_name}{'files'}}) {
my $shortname = get_short_relative_name($file, $args{"relativeto"});
my ($base, $dir, $ext) = fileparse($shortname, '\..*');
$doc .= ' - @htmlonly<A href="' . $args{"cvsweb"}
. $shortname;
$doc .= $args{"cvsweb_suffix"} if exists $args{"cvsweb_suffix"};
$doc .= '">@endhtmlonly @c ' . $ext
. ' @htmlonly</A>@endhtmlonly (@c ' . $shortname . ")\n";
}
}
if ($block !~ s/($preamble.+?)(\s*\@par|\z)/$doc$2/gms) {
$block .= "\n$doc";
}
# Write new header
if (!sysopen(HEADERFILE, $header,
O_WRONLY|O_TRUNC|O_CREAT|$open_file_as_text)) {
carp "$PROGNAME: unable to open $header\n";
next;
}
print HEADERFILE $pre . $block . $post;
close(HEADERFILE);
print " >> Updating $header\n" if exists $args{"verbose"};
$nb_updated++;
}
print " => $nb_updated header(s) updated in ", time() - $intermediate_time, " s. \n";
# -------------------------------------------------------------------------
# Build the page summary documentation
# $indent is the indentation string
my $indent = " ";
# $header is the Doxygen string summarizing what has been documented as well
# as the credits.
my $header;
my (@summary, @credits);
push @summary,
" - " . (scalar keys %contribution_by_author_file) . " authors(s) reported";
push @summary,
" - Files matching \@c " . $args{"files_in"} . " and not matching \@c ". $args{"files_out"} . " are taken into account";
push @summary,
" - $nb_revisions CVS revision(s) processed from " . (scalar keys %files_visited) . " matching file(s)";
push @summary,
" - " . ($nb_revisions - $nb_removed) . " revision(s) kept ($nb_removed revision(s) removed from 'massive commits' > " . $args{"massive"} . " changes per date/author/message)";
push @summary,
" - revision's contribution is " . $args{"lines_add"} . " * (number of lines added) + " . $args{"lines_rem"} . " * (number of lines removed)";
push @summary,
" - " . (scalar keys %classes) . " class(es) found (matching \@c " . $args{"class_group"} . ")";
push @credits,
"\@version $VERSION",
"\@author \@c $PROGNAME, by $AUTHOR";
$header = $indent . join("\n$indent", @summary) . "\n\n" .
$indent . join("\n$indent", @credits) . "\n\n";
# -------------------------------------------------------------------------
# Update contributions stats
# %contribution_by_author is indexed by author
# (i.e $contribution_by_author{$author})
# stores the total contribution of this author for all files processed so far.
my %contribution_by_author;
# %contribution_by_date is indexed by date
# (i.e $contribution_by_date{$date})
# stores the total *accumulative* contribution up to that date
# for all files processed so far.
my %contribution_by_date;
foreach my $contributor (keys %contribution_by_author_file) {
foreach my $file_name
(keys %{$contribution_by_author_file{$contributor}}) {
$contribution_by_author{$contributor} +=
$contribution_by_author_file{$contributor}{$file_name};
}
my $accumulate = 0;
foreach my $date
(sort {$a cmp $b} keys %{$contribution_by_author_date{$contributor}}) {
$contribution_by_date{$date} +=
$contribution_by_author_date{$contributor}{$date};
$accumulate += $contribution_by_author_date{$contributor}{$date};
$contribution_by_author_date{$contributor}{$date} = $accumulate;
}
}
my $accumulate = 0;
foreach my $date
(sort {$a cmp $b} keys %contribution_by_date) {
$accumulate += $contribution_by_date{$date};
$contribution_by_date{$date} = $accumulate;
}
my @all_contribution_dates = sort {$a cmp $b} keys %contribution_by_date;
my $last_contribution_date = $all_contribution_dates[$#all_contribution_dates];
my $first_contribution_date = $all_contribution_dates[0];
my $total_contribution = $contribution_by_date{$last_contribution_date};
# -------------------------------------------------------------------------
# Build documentation
my $destination_file = $args{"to"} . "/" . $args{"store"};
print "Building documentation to ", $destination_file, "\n";
sysopen(DEST_FILE,
$destination_file,
O_WRONLY|O_TRUNC|O_CREAT|$open_file_as_text)
or croak "$PROGNAME: unable to open destination file $destination_file\n";
print DEST_FILE
"/*! \@page contributors Contributors\n\n$header";
print DEST_FILE
"\n$indent\@section contributors_alphabetical Contributors (by alphabetical order)\n\n";
sub compare_by_author_name {
my $a_name = exists $authors{$a}{'name'} ? $authors{$a}{'name'} : $a;
my $b_name = exists $authors{$b}{'name'} ? $authors{$b}{'name'} : $b;
return lc $a_name cmp lc $b_name;
}
foreach my $contributor (sort compare_by_author_name
keys %contribution_by_author_file) {
my $c_ratio = $contribution_by_author{$contributor} / $total_contribution;
my $name = exists $authors{$contributor}{'name'}
? $authors{$contributor}{'name'} : $contributor;
if ($c_ratio < $args{"min_gcontrib"}) {
print DEST_FILE "$indent - $name\n";
} else {
print DEST_FILE "$indent - \@ref $contributor \"$name\"\n";
}
}
print DEST_FILE
"\n$indent\@section contributors_decreasing Contributors (by decreasing order of global contribution)\n";
print DEST_FILE
"\n$indent\@note Contributions lower than " . int($args{"min_gcontrib"} * 10000.0) / 100.0 . "% are not listed\n\n";
my @contributors_sorted =
sort {$contribution_by_author{$b} <=> $contribution_by_author{$a}}
keys %contribution_by_author;
foreach my $contributor (@contributors_sorted) {
my $c_ratio = $contribution_by_author{$contributor} / $total_contribution;
last if $c_ratio < $args{"min_gcontrib"};
my $name = exists $authors{$contributor}{'name'}
? $authors{$contributor}{'name'} : $contributor;
print DEST_FILE "$indent -# \@ref $contributor \"$name\"\n";
}
foreach my $history (@{$args{"history_img"}}) {
my ($delta, $style, $filename) = split(/\|/, $history);
print DEST_FILE
"\n$indent\@image html " . basename($filename) . "\n";
}
print DEST_FILE
"\n$indent\@section contributors_detailed Detailed contributions (by decreasing order)\n";
print DEST_FILE
"\n$indent\@note Contributions lower than " . int($args{"min_gcontrib"} * 10000.0) / 100.0 . "% are not listed\n";
print DEST_FILE
"\n$indent\@note Details are:",
"\n$indent - % of global contribution at $last_contribution_date, % at date of last contribution, % at date of first contribution",
"\n$indent - first ", $args{"max_class_nb"}, " contributed classes > ", int($args{"min_class"} * 100.0), "% of author's total contribution",
"\n$indent - first ", $args{"max_file_nb"}, " contributed (non-class) files > ", int($args{"min_file"} * 100.0), "% of author's total contribution",
"\n\n";
foreach my $contributor (@contributors_sorted) {
# Yes, you can have 0 contrib:
# vtkPiecewiseFunction.cxx:date: 1999/11/03 18:11:39;
# author: tpan; state: Exp; lines: +0 -0
next if $contribution_by_author{$contributor} == 0;
my $c_ratio = $contribution_by_author{$contributor} / $total_contribution;
last if $c_ratio < $args{"min_gcontrib"};
# Classes
my @ok_classes;
my @classes_sorted =
sort {$contribution_by_author_class{$contributor}{$b} <=>
$contribution_by_author_class{$contributor}{$a}}
keys %{$contribution_by_author_class{$contributor}};
foreach my $class_name (@classes_sorted) {
last if scalar @ok_classes > $args{"max_class_nb"};
my $ratio =
$contribution_by_author_class{$contributor}{$class_name} /
$contribution_by_author{$contributor};
last if $ratio < $args{"min_class"};
push @ok_classes, "$class_name (" . int($ratio * 100.0) . "%)";
}
# Files
my @ok_files;
my @files_sorted =
sort {$contribution_by_author_file{$contributor}{$b} <=>
$contribution_by_author_file{$contributor}{$a}}
keys %{$not_class_file_by_author{$contributor}};
foreach my $file_name (@files_sorted) {
next if basename($file_name) =~ m/$args{"class_group"}/;
last if scalar @ok_files > $args{"max_file_nb"};
my $ratio =
$contribution_by_author_file{$contributor}{$file_name} /
$contribution_by_author{$contributor};
last if $ratio < $args{"min_file"};
push @ok_files, get_short_relative_name($file_name, $args{"relativeto"}) . " (" . int($ratio * 100.0) . "%)";
}
my $name = exists $authors{$contributor}{'name'}
? $authors{$contributor}{'name'} . " ($contributor)" : $contributor;
print DEST_FILE "$indent -# \@anchor $contributor \@b $name:\n";
my @contribution_dates = sort { $a cmp $b }
keys %{$contribution_by_author_date{$contributor}};
my $last_contrib_date = $contribution_dates[$#contribution_dates];
my $first_contrib_date = $contribution_dates[0];
print DEST_FILE
"$indent - \@b ", int(10000.0 * $c_ratio) / 100, "% ($last_contribution_date), ", int(10000.0 * ($contribution_by_author_date{$contributor}{$last_contrib_date} / $contribution_by_date{$last_contrib_date})) / 100, "% ($last_contrib_date), ", int(10000.0 * ($contribution_by_author_date{$contributor}{$first_contrib_date} / $contribution_by_date{$first_contrib_date})) / 100, "% ($first_contrib_date)\n";
print DEST_FILE
"$indent - ", (scalar @classes_sorted), " class(es): ", join(", ", @ok_classes), "...\n" if @classes_sorted;
print DEST_FILE
"$indent - ", (scalar @files_sorted), " file(s): ", join(", ", @ok_files), "...\n" if @files_sorted;
}
print DEST_FILE "\n*/\n\n";
# -------------------------------------------------------------------------
# Create history files
print "Creating history files in ", $args{"history_dir"}, "\n";
mkpath($args{"history_dir"});
foreach my $contributor (@contributors_sorted) {
my $history_name = $args{"history_dir"} . "/$contributor.dat";
sysopen(HISTORY_FILE,
$history_name,
O_WRONLY|O_TRUNC|O_CREAT|$open_file_as_text)
or croak "$PROGNAME: unable to open history file $history_name\n";
foreach my $date
(sort {$a cmp $b} keys %{$contribution_by_author_date{$contributor}}) {
print HISTORY_FILE $date . ' ' . ($contribution_by_author_date{$contributor}{$date} / $contribution_by_date{$date}) * 100.0 . "\n";
}
close(HISTORY_FILE);
}
# -------------------------------------------------------------------------
# Create gnuplot file
print "Creating gnuplot command file\n", $args{"gnuplot_file"}, "\n";
my ($year, $month, $mday) = split('-', $last_contribution_date);
my $last_contribution_sec = timelocal(0, 0, 0, $mday, $month -1 , $year-1900);
my $history_dir_abs = abs_path($args{"history_dir"});
my @plots;
foreach my $contributor (@contributors_sorted) {
last if scalar @plots >= $args{"history_max_nb"};
my $history_filename = $history_dir_abs . "/$contributor.dat";
push @plots, "\"$history_filename\" using 1:2 title \"" . (exists $authors{$contributor}{'name'} ? $authors{$contributor}{'name'} : $contributor) . "\"" if -e $history_filename;
}
my $plot = 'plot ' . join(', ', @plots);
sysopen(GNUPLOT_FILE,
$args{"gnuplot_file"},
O_WRONLY|O_TRUNC|O_CREAT|$open_file_as_text)
or croak "$PROGNAME: unable to open gnuplot command file " . $args{"gnuplot_file"} . "\n";
print GNUPLOT_FILE <<EOT;
set grid
set xdata time
set timefmt "%Y/%m/%d"
set format x "%Y/%m"
set xlabel "Time (Year/Month)"
set ylabel "Contribution (%)"
set key bottom outside below Left title 'Legend:' nobox
set mytics 5
set size 1.2,1.4
set terminal png small
set timestamp top
EOT
my $date_from;
foreach my $history (@{$args{"history_img"}}) {
my ($delta, $style, $filename) = split(/\|/, $history);
if ($delta eq '') {
print GNUPLOT_FILE "# Whole period\n\n";
$date_from = $first_contribution_date;
print GNUPLOT_FILE "set title \"Contributions over time (whole period: $date_from to $last_contribution_date)\"\n\n";
} else {
print GNUPLOT_FILE "# Back $delta days\n\n";
($mday, $month, $year) =
(localtime($last_contribution_sec - $delta * 24 * 60 * 60))[3, 4, 5];
$date_from = sprintf("%04d/%02d/%02d", $year+1900, $month + 1, $mday);
print GNUPLOT_FILE "set title \"Contributions over time ($delta days ago: $date_from to $last_contribution_date)\"\n\n";
}
print GNUPLOT_FILE "set xrange [\"$date_from\":]\n\n";
print GNUPLOT_FILE "set output \"" .
abs_path(dirname($filename)) . '/' . basename($filename) . "\"\n\n";
print GNUPLOT_FILE "set data style $style\n\n";
print GNUPLOT_FILE "$plot\n\n";
if ($plot ne 'replot') {
$plot = 'replot';
}
}
close(GNUPLOT_FILE);
# -------------------------------------------------------------------------
print "Finished in ", time() - $start_time, " s.\n";