#!/usr/bin/perl
my
%opts
;
getopts(
'f:ho:st:vDHS:'
, \
%opts
);
if
(
$opts
{h}) {
pod2usage(
-verbose
=> 1);
}
if
(
$opts
{v}) {
print
"$0\n"
;
print
"CVS::Metrics Version $CVS::Metrics::VERSION\n"
;
exit
(0);
}
my
$cfg
=
'.cvs_metrics'
;
our
(
$title
,
$regex_tag
,
$flg_head
,
$flg_dead
,
$flg_css
,
$start_date
,
$regex_ignore_tag
);
if
( -r
$cfg
) {
print
"reading $cfg\n"
;
require
$cfg
;
}
my
$cvs_logfile
;
if
(
$opts
{f}) {
$cvs_logfile
=
$opts
{f};
}
else
{
my
$cvs
= FindCvs();
$cvs_logfile
=
$cvs
.
' log |'
;
}
if
(
$opts
{t}) {
$title
=
$opts
{t};
}
else
{
$title
=
'total'
unless
(
defined
$title
);
}
if
(
$opts
{H}) {
$flg_head
= 1;
}
if
(
$opts
{D}) {
$flg_dead
= 1;
}
if
(
$opts
{s}) {
$flg_css
= 1;
}
unless
(
defined
$regex_tag
) {
$regex_tag
=
'\d+'
;
}
if
(
$opts
{S}) {
$start_date
=
$opts
{S};
}
else
{
$start_date
=
'2006/01/01'
unless
(
defined
$start_date
);
}
my
$output
=
$opts
{o};
if
(
$output
and ! -d
$output
) {
mkpath(
$output
)
or
die
"can't create $output ($!)."
;
}
my
$cvs_log
= CVS::Metrics::CvsLog(
stream
=>
$cvs_logfile
,
);
if
(
$cvs_log
) {
my
@tags
;
my
$timed
=
$cvs_log
->getTimedTag(
$regex_ignore_tag
);
my
%matched
;
while
(
my
(
$tag
,
$date
) =
each
%{
$timed
}) {
print
"Tag: "
,
$tag
;
if
(
$tag
=~ /
$regex_tag
/) {
$matched
{
$date
.
$tag
} =
$tag
;
print
" ... matched"
;
}
print
"\n"
;
}
foreach
(
sort
keys
%matched
) {
push
@tags
,
$matched
{
$_
};
}
if
(
$flg_head
) {
push
@tags
,
'HEAD'
;
$cvs_log
->insertHead();
}
my
$top
= Top->new(
$cvs_log
, \
@tags
,
$title
,
$flg_dead
,
$flg_css
,
$start_date
,
$output
);
MainLoop();
}
sub
FindCvs {
my
$cvs
;
if
($^O eq
'MSWin32'
) {
eval
'use File::Which'
;
$cvs
= which(
'cvs'
);
unless
(
defined
$cvs
) {
eval
'use Win32::TieRegistry(Delimiter => "/")'
;
my
$cvs_setting
;
my
$hkey
=
'HKEY_CURRENT_USER/Software/WinCvs/wincvs/CVS settings'
;
eval
'$cvs_setting = $Registry->{$hkey}'
;
$cvs
=
$cvs_setting
->{
'/P_WhichCvs'
};
if
(
defined
$cvs
) {
$cvs
=~ s/[\000\001]//g;
$cvs
=~ s/wincvs\.exe\@$//;
if
( -e
"${cvs}CVSNT\\\\cvs.exe"
) {
$cvs
.=
"CVSNT\\\\cvs.exe"
;
}
else
{
$cvs
.=
'cvs.exe'
;
}
}
}
die
"cvs not found !\n"
unless
(
defined
$cvs
);
warn
"Using CVS : $cvs\n"
;
return
q{"}
.
$cvs
.
q{"}
;
}
else
{
return
'cvs'
;
}
}
sub
GenerateHTML {
my
(
$cvs_log
,
$tags
,
$title
,
$path
,
$tag_from
,
$tag_to
,
$flg_css
,
$start_date
,
$output
) =
@_
;
my
$html
=
q{
<?xml version='1.0' encoding='ISO-8859-1'?>
<head>
<meta http-equiv='Content-Type' content='text/html; charset=ISO-8859-1' />
<meta name='generator' content='<TMPL_VAR NAME=generator>' />
<meta name='date' content='<TMPL_VAR NAME=date>' />
<title>cvs_tklog <!-- TMPL_VAR NAME=title --></title>
<!-- TMPL_IF NAME=css -->
<link href='cvs_tklog.css' rel='stylesheet' type='text/css'/>
<!-- TMPL_ELSE -->
<style type='text/css'>
<!-- TMPL_VAR NAME=style -->
</style>
<!-- /TMPL_IF -->
</head>
<body>
<h1>Evolution Report</h1>
<h1><!-- TMPL_VAR NAME=title --></h1>
<hr />
<h2>Activity</h2>
<img src='<TMPL_VAR NAME=a_img>' />
<h2>Context</h2>
<table class='layout'>
<tr>
<td valign='top'><img src='<TMPL_VAR NAME=e_img>' /></td>
<td valign='top'>
<table border='1' cellpadding='5'>
<tr>
<th>Tag</th>
<th>Date</th>
</tr>
<!-- TMPL_LOOP NAME=timed_tag -->
<tr>
<td><!-- TMPL_VAR NAME=tag --></td>
<td><!-- TMPL_VAR NAME=timed --></td>
</tr>
<!-- /TMPL_LOOP -->
</table>
</td>
</tr>
</table>
<hr />
<h2>Evolution Report Summary</h2>
<table border='1' cellpadding='5'>
<tr>
<th width='40%'>Directories</th>
<th width='20%'>Added files</th>
<th width='20%'>Modified files</th>
<th width='20%'>Deleted files</th>
</tr>
<!-- TMPL_LOOP NAME=summary -->
<tr>
<td><a href='#<TMPL_VAR NAME=dir>'><!-- TMPL_VAR NAME=dir --></a></td>
<td><!-- TMPL_VAR NAME=added --></td>
<td><!-- TMPL_VAR NAME=modified --></td>
<td><!-- TMPL_VAR NAME=deleted --></td>
</tr>
<!-- /TMPL_LOOP -->
</table>
<hr />
<h2>Detailed Evolution Report</h2>
<table border='1' cellpadding='5'>
<tr>
<th width='20%'>Directories</th>
<th width='40%'>Messages</th>
<th width='30%'>File Descriptions</th>
<th width='10%'>Actions</th>
</tr>
<!-- TMPL_LOOP NAME=dirs --><tr>
<td valign='top' rowspan='<TMPL_VAR NAME=rowspan>'><a id='<TMPL_VAR NAME=dir>'
name='<TMPL_VAR NAME=dir>' /><!-- TMPL_VAR NAME=dir --></td>
<!-- TMPL_LOOP NAME=comments -->
<!-- TMPL_UNLESS NAME=__FIRST__ --><tr><!-- /TMPL_UNLESS -->
<td valign='top' rowspan='<TMPL_VAR NAME=rowspan>'><!-- TMPL_VAR NAME=comment --></td>
<!-- TMPL_LOOP NAME=files -->
<!-- TMPL_UNLESS NAME=__FIRST__ --><tr><!-- /TMPL_UNLESS -->
<td><span class='filename'><!-- TMPL_VAR NAME=filename --></span>
<span class='revision'><!-- TMPL_VAR NAME=revision --></span><br />
<!-- TMPL_LOOP NAME=tags -->
<span class='tag'><!-- TMPL_VAR NAME=tag --></span><br />
<!-- /TMPL_LOOP -->
<span class='author'><!-- TMPL_VAR NAME=author --></span>
<span class='date'><!-- TMPL_VAR NAME=date --></span></td>
<td><!-- TMPL_VAR NAME=action --></td>
</tr><!-- /TMPL_LOOP -->
<!-- /TMPL_LOOP -->
<!-- /TMPL_LOOP -->
</table>
<hr />
<cite>Generated by cvs_tklog (<!-- TMPL_VAR NAME=date -->)</cite>
</body>
</html>
}
;
my
$style
=
q{
body { background-color: #FFFFCC }
table { background-color:
th { background-color:
h1 { text-align: center }
h2 { color: red }
td a { font-weight: bold }
table.layout { background-color:
span.author { font-weight: bold }
span.filename { color: blue }
span.revision { font-weight: bold; color: blue }
span.tag { font-weight: bold }
span.date { }
span.deleted { font-weight: bold; color: red }
span.added { font-weight: bold; color: blue }
span.modified { font-weight: bold }
};
my
$template
= HTML::Template->new(
loop_context_vars
=> 1,
scalarref
=> \
$html
,
);
die
"can't create template ($!).\n"
unless
(
defined
$template
);
my
$now
=
localtime
();
my
$generator
=
'cvs_tklog '
.
$CVS::Metrics::VERSION
.
' (Perl '
. $] .
')'
;
my
$dir
=
$path
eq
'.'
?
'all'
:
$path
;
my
$title_full
=
"${title} ${dir} ${tag_from} to ${tag_to}"
;
my
$image
=
$cvs_log
->EnergyGD(
$tags
,
$path
,
$dir
, 600, 400,
$tag_from
,
$tag_to
);
my
$e_img
=
"e_${title_full}.png"
;
$e_img
=~ s/[ \/]/_/g;
if
(
defined
$image
) {
my
$filename
= (
defined
$output
) ?
$output
.
"/"
.
$e_img
:
$e_img
;
open
my
$OUT
,
'>'
,
$filename
or
die
"can't open $filename ($!).\n"
;
binmode
$OUT
,
':raw'
;
print
$OUT
$image
->png();
close
$OUT
;
}
my
$timed_tag
=
$cvs_log
->getTimedTag(
$regex_ignore_tag
);
my
@timed_tag
= ();
foreach
my
$tag
(@{
$tags
}) {
if
(
$tag
eq
'HEAD'
) {
push
@timed_tag
, {
tag
=>
$tag
,
timed
=>
'now'
,
};
}
else
{
push
@timed_tag
, {
tag
=>
$tag
,
timed
=>
substr
(
$timed_tag
->{
$tag
}, 0, 10),
};
}
}
my
$date_from
=
substr
$timed_tag
->{
$tag_from
}, 0, 10;
my
$date_to
=
substr
$timed_tag
->{
$tag_to
}, 0, 10;
$image
=
$cvs_log
->ActivityGD(
$path
,
$dir
,
$start_date
, 800, 225,
$date_from
,
$date_to
);
my
$a_img
=
"a_${title_full}.png"
;
$a_img
=~ s/[ \/]/_/g;
if
(
defined
$image
) {
my
$filename
= (
defined
$output
) ?
$output
.
'/'
.
$a_img
:
$a_img
;
open
my
$OUT
,
'>'
,
$filename
or
die
"can't open $filename ($!).\n"
;
binmode
$OUT
,
':raw'
;
print
$OUT
$image
->png();
close
$OUT
;
}
my
$dir_evol
=
$cvs_log
->getDirEvolution(
$path
,
$tag_from
,
$tag_to
,
$tags
);
my
@summary
= ();
foreach
my
$dirname
(
sort
keys
%{
$dir_evol
}) {
my
@val
= @{
$dir_evol
->{
$dirname
}};
next
unless
(
$val
[0] or
$val
[1] or
$val
[2]);
push
@summary
, {
dir
=>
$dirname
,
added
=>
$val
[0],
modified
=>
$val
[1],
deleted
=>
$val
[2],
};
}
my
$evol
=
$cvs_log
->getEvolution(
$path
,
$tag_from
,
$tag_to
,
$tags
);
my
@dirs
= ();
foreach
my
$dirname
(
sort
keys
%{
$evol
}) {
my
$dir
=
$evol
->{
$dirname
};
my
%date_sorted
;
next
unless
(
scalar
keys
%{
$dir
});
my
$i
= 0;
foreach
my
$message
(
keys
%{
$dir
}) {
my
$files
=
$dir
->{
$message
};
my
$file0
=
$files
->[0];
$date_sorted
{
$file0
->{date} .
"#$i"
} =
$message
;
$i
++;
}
my
$rowspan1
= 0;
my
@comments
= ();
foreach
(
sort
keys
%date_sorted
) {
my
$message
=
$date_sorted
{
$_
};
my
$files
=
$dir
->{
$message
};
my
$rowspan2
= 0;
my
@files
= ();
foreach
my
$file
(@{
$files
}) {
my
@tags
= ();
foreach
my
$tag
(
sort
@{
$file
->{tags}}) {
push
@tags
, {
tag
=>
$tag
,
};
}
my
$action
;
if
(
$file
->{state} eq
'dead'
) {
$action
=
"<span class='deleted'>DELETED</span>"
;
}
else
{
if
(
$file
->{revision} =~ /^1\.1(\.\d+\.1)?$/) {
$action
=
"<span class='added'>ADDED</span>"
;
}
else
{
$action
=
"<span class='modified'>MODIFIED</span>"
;
}
}
push
@files
, {
filename
=> basename(
$file
->{filename}),
revision
=>
$file
->{revision},
date
=>
$file
->{date},
author
=>
$file
->{author},
action
=>
$action
,
tags
=> \
@tags
,
};
$rowspan1
++;
$rowspan2
++;
}
$message
=~ s/&/
&
;/g;
$message
=~ s/</
<
;/g;
$message
=~ s/>/
>
;/g;
$message
=~ s/\n/<br \/>/g;
push
@comments
, {
rowspan
=>
$rowspan2
,
comment
=>
$message
,
files
=> \
@files
,
};
}
push
@dirs
, {
rowspan
=>
$rowspan1
,
dir
=>
$dirname
,
comments
=> \
@comments
,
}
}
$template
->param(
css
=>
$flg_css
,
style
=>
$style
,
generator
=>
$generator
,
date
=>
$now
,
title
=>
$title_full
,
e_img
=>
$e_img
,
a_img
=>
$a_img
,
timed_tag
=> \
@timed_tag
,
summary
=> \
@summary
,
dirs
=> \
@dirs
,
);
my
$basename
=
"${title_full}.html"
;
$basename
=~ s/[ \/]/_/g;
my
$filename
= (
defined
$output
) ?
$output
.
'/'
.
$basename
:
$basename
;
open
my
$OUT
,
'>'
,
$filename
or
die
"can't open $filename ($!)\n"
;
print
$OUT
$template
->output();
close
$OUT
;
if
(
$flg_css
) {
my
$stylesheet
=
'cvs_tklog.css'
;
$stylesheet
=
$output
.
'/'
.
$stylesheet
if
(
$output
);
unless
(-e
$stylesheet
) {
open
my
$OUT
,
'>'
,
$stylesheet
or
die
"can't open $stylesheet ($!)\n"
;
print
$OUT
$style
;
close
$OUT
;
}
}
return
$filename
;
}
sub
new {
my
$proto
=
shift
;
my
$class
=
ref
(
$proto
) ||
$proto
;
my
$self
= {};
bless
(
$self
,
$class
);
my
(
$cvs_log
,
$tags
,
$title
,
$flg_dead
,
$flg_css
,
$start_date
,
$output
) =
@_
;
$self
->{cvs_log} =
$cvs_log
;
$self
->{tags} =
$tags
;
$self
->{title} =
$title
;
$self
->{css} =
$flg_css
;
$self
->{start_date} =
$start_date
;
$self
->{output} =
$output
;
$self
->{mw} = MainWindow->new();
$self
->{mw}->title(
"CVS log - $title"
);
$self
->{mw}->minsize(800, 500);
$self
->{tree} =
$self
->{mw}->Scrolled(
'Tree'
,
-scrollbars
=>
'osw'
,
-command
=> [
sub
{
shift
->_upd_img(); },
$self
],
);
$self
->{tree}->
pack
(
-side
=>
'left'
,
-fill
=>
'both'
,
-expand
=> 1,
);
$self
->_init_tree(
$flg_dead
);
$self
->{fr} =
$self
->{mw}->Frame();
$self
->{fr}->
pack
(
-side
=>
'left'
,
-fill
=>
'y'
,
);
$self
->{plot} =
$self
->{cvs_log}->EnergyCv(
$self
->{tags},
'.'
,
$self
->{title}, 600, 400,
$self
->{fr});
$self
->{plot}->configure(
-bd
=> 2,
-relief
=>
'sunken'
,
);
$self
->{plot}->
pack
();
my
$fr2
=
$self
->{fr}->Frame();
$fr2
->
pack
(
-side
=>
'bottom'
,
);
if
(
scalar
(
$self
->{tags}) >= 2) {
my
$b_audit
=
$fr2
->Button(
-text
=>
'Audit :'
,
-padx
=> 10,
-command
=> [
sub
{
shift
->_audit(); },
$self
],
);
$b_audit
->
pack
(
-side
=>
'left'
,
);
$self
->{tag_from} = ${
$self
->{tags}}[-2];
$self
->{tag_to} = ${
$self
->{tags}}[-1];
my
@tags_from
= @{
$self
->{tags}};
pop
@tags_from
;
my
@tags_to
= @{
$self
->{tags}};
shift
@tags_to
;
my
$cb_from
=
$fr2
->Optionmenu(
-options
=> \
@tags_from
,
-variable
=> \
$self
->{tag_from},
);
my
$cb_to
=
$fr2
->Optionmenu(
-options
=> \
@tags_to
,
-variable
=> \
$self
->{tag_to},
);
my
$l_from
=
$fr2
->Label(
-text
=>
'from'
,
);
my
$l_to
=
$fr2
->Label(
-text
=>
'to'
,
);
$l_from
->
pack
(
-side
=>
'left'
,
);
$cb_from
->
pack
(
-side
=>
'left'
,
);
$l_to
->
pack
(
-side
=>
'left'
,
);
$cb_to
->
pack
(
-side
=>
'left'
,
);
}
$self
->{tree}->focus();
return
$self
;
}
sub
_init_tree {
my
$self
=
shift
;
my
(
$flg_dead
) =
@_
;
my
%dir
= (
'.'
=> 1
);
while
(
my
(
$filename
,
$file
) =
each
%{
$self
->{cvs_log}}) {
if
(
$flg_dead
) {
my
$head
=
$file
->{head};
my
$state
=
$file
->{description}->{
$head
}->{state};
next
if
(
$state
eq
'dead'
);
}
my
$path
= dirname(
$filename
);
$dir
{
$path
} = 1;
while
((
$path
= dirname(
$path
)) ne
'.'
) {
$dir
{
$path
} = 1;
}
}
my
$img
=
$self
->{mw}->Getimage(
'folder'
);
foreach
(
sort
keys
%dir
) {
my
$path
=
$_
;
unless
(
$_
eq
'.'
) {
$path
=~ s/\./_/g;
$path
=~ s/\//\./g;
$path
=
'.'
.
$path
;
}
$self
->{tree}->add(
$path
,
-text
=>
$_
,
-image
=>
$img
);
}
$self
->{tree}->autosetmode();
}
sub
_upd_img {
my
$self
=
shift
;
$self
->{plot}->destroy();
my
@sel
=
$self
->{tree}->selectionGet();
my
$path
=
$sel
[0] ||
'.'
;
my
$title
;
if
(
$path
eq
'.'
) {
$title
=
$self
->{title};
}
else
{
$path
=~ s/^\.//;
$path
=~ s/\./\//g;
$title
=
$path
;
}
print
$path
,
"\n"
;
$self
->{plot} =
$self
->{cvs_log}->EnergyCv(
$self
->{tags},
$path
,
$title
, 600, 400,
$self
->{fr});
$self
->{plot}->configure(
-bd
=> 2,
-relief
=>
'sunken'
,
);
$self
->{plot}->
pack
(
);
}
sub
_audit {
my
$self
=
shift
;
my
@tags
= @{
$self
->{tags}};
while
(
$self
->{tag_from} ne
$tags
[0]) {
shift
@tags
;
}
shift
@tags
;
my
$found
= 0;
foreach
(
@tags
) {
$found
= 1
if
(
$_
eq
$self
->{tag_to});
}
unless
(
$found
) {
$self
->{mw}->messageBox(
-message
=>
"$self->{tag_from} >= $self->{tag_to}"
,
-icon
=>
'error'
,
-type
=>
'OK'
,
);
return
;
}
my
@sel
=
$self
->{tree}->selectionGet();
my
$path
=
$sel
[0] ||
'.'
;
if
(
$path
ne
'.'
) {
$path
=~ s/^\.//;
$path
=~ s/\./\//g;
}
my
$html
= main::GenerateHTML(
$self
->{cvs_log},
$self
->{tags},
$self
->{title},
$path
,
$self
->{tag_from},
$self
->{tag_to},
$self
->{css},
$self
->{start_date},
$self
->{output});
print
"Starting browser...\n"
;
if
($^O eq
'MSWin32'
) {
system
"start $html"
;
}
else
{
system
"x-www-browser $html &"
;
}
}