cvschangelogbuilder-2.4/0000755000175300010010000000000011025725044014630 5ustar LaurentAucuncvschangelogbuilder-2.4/README.TXT0000444000175300010010000000446011025724322016166 0ustar LaurentAucun ------------- AWBot -------------- cvschangelogbuilder ---------------------------------- cvschangelogbuilder is an utility to generate a changelog for a project hosted on a CVS server. The main goal is to provide a better output that the 'cvs log' command and to permit to choose the sorting criteria. Another feature is that cvschangelogbuilder is able to show all the changes between two versions, even if versions are not in main branch. It also add information to know if file was added, changed or removed for delta output (only possible if inital and final version are in main branch). cvschangelogbuilder offer 4 kinds of output: listdeltaforrpm To build a changelog to include in a rpm .spec file. listdeltabydate To build a changelog by date (looks near 'cvs log'). listdeltabyfile To build a changelog by file. listdeltabylog To build a changelog by change comment. buildhtmlreport To build an html report of project CVS activity. License: GNU GPL (GNU General Public License. See COPYING.TXT file) Version : 2.4 Release date: June 2008 Platforms: All (Linux, NT, BSD, Solaris and other *NIX's, BeOS, OS/2...) Author: Laurent Destailleur cvschangelogbuilder official web site and latest version: http://cvschangelogb.sourceforge.net I - Files --------- The distribution of cvschangelogbuilder package includes the following files: README.TXT This file cvschangelogbuilder.pl The cvschangelogbuilder tool. docs/* The changelog and PAD files. II - ABOUT THE AUTHOR, LICENSE AND SUPPORT ------------------------------------------- Copyright (C) 2003-2008 - Laurent Destailleur - eldy@users.sourceforge.net This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details, in COPYING.TXT file. If you have not received a copy of this file along with this program, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. cvschangelogbuilder-2.4/cvschangelogbuilder.pl0000755000175300010010000027162611003344722021214 0ustar LaurentAucun#!/usr/bin/perl #------------------------------------------------------- # Build a changelog file for a CVS project # Serveral output format are available #------------------------------------------------------- # $Revision: 1.83 $ - $Author: eldy $ - $Date: 2008/04/22 11:19:46 $ use strict; no strict "refs"; use Time::Local; use CGI qw/:standard/; #use diagnostics; #------------------------------------------------------- # Defines #------------------------------------------------------- my $REVISION='$Revision: 1.83 $'; $REVISION =~ /\s(.*)\s/; $REVISION=$1; my $VERSION="2.4 (build $REVISION)"; # ---------- Init variables -------- use vars qw/ $TagStart $Branch $TagEnd $Since /; $TagStart=$Branch=$TagEnd=$Since=''; my $Debug=0; my $DIR; my $PROG; my $Extension; my $Help=''; my $AllowIndex=0; my $IncludeHeader=''; my $Module=''; my $CacheNameForModule=''; my $Output=''; # Default will be "listdeltabydate" my $OutputDir=''; my $CvsRoot=''; # Example ":ntserver:127.0.0.1:d:/temp/cvs" my $UseSsh=0; my $RLogFile; my $KeepRlogFile=0; my $RepositoryPath; my $filename=''; my $fullfilename=''; my %filesym=(); my $fileformat=''; my $filerevision=''; my $filedate=''; my $fileuser=''; my $filestate=''; my $filechange=''; my $filelineadd=0; my $filelinedel=0; my $filelinechange=0; my $filelog=''; my $oldfiledayuser=''; my $oldfilelog=''; my $EXTRACTFILENAME="^(?:RCS|Working) file: (.+)"; my $EXTRACTSYMBOLICNAMEAREA="symbolic names:"; my $EXTRACTSYMBOLICNAMEENTRY="^\\s(.+): ([\\d\\.]+)"; my $EXTRACTFILEVERSION="^revision (.+)"; my $EXTRACTFILEDATEUSERSTATE="date: (.+)\\sauthor: (.*)\\sstate: ([^\\s]+)(.*)"; my $CVSCLIENT="cvs -f"; my $COMP=""; # Do no use compression because it seems to return bugged rlog files for some servers/clients. my $ViewCvsUrl=""; my $ENABLEREQUESTFORADD=1; # Allow cvs request to get number of lines for added/removed files. my %IgnoreFileDir=(); my %OnlyFileDir=(); my %colorstate=('added'=>'#008822','changed'=>'#888888','removed'=>'#880000'); # ---------- Init Regex -------- use vars qw/ $reg1 /; $reg1=qr/x/i; # ---------- Init hash arrays -------- # For all my %maxincludedver=(); my %minexcludedver=(); my %tagsfulldate=(); my %tagsshortdate=(); my %tagstags=(); my %Cache=(); # For output by date my %DateUser=(); my %DateUserLog=(); my %DateUserLogFileRevState=(); my %DateUserLogFileRevLine=(); my %HourUser=(); # For output by file my %FilesLastVersion=(); my %FilesChangeDate=(); my %FilesChangeUser=(); my %FilesChangeState=(); my %FilesChangeLog=(); # For output by log my $LGMAXLOG=400; my %LogChange=(); my %LogChangeDate=(); my %LogChangeUser=(); my %LogChangeState=(); # For output by user my %UserChangeCommit=(); my %UserChangeLast=(); my %UserChangeLineAdd=(); my %UserChangeLineDel=(); my %UserChangeLineChange=(); # For html report output my $MAXLASTLOG=200; my $SORTBYREVISION=0; my $LOOSECOMMITS=0; my $NOSUMMARY=0; my $INCLUDEDIFF=0; my $NOLINESOFCODE=0; my $NODEVELOPERS=0; my $NODAYSOFWEEK=0; my $NOHOURS=0; my $NOTAGS=0; my $NOLASTLOGS=0; #------------------------------------------------------------------------------ # Functions #------------------------------------------------------------------------------ #------------------------------------------------------------------------------ # Function: mkdir_recursive # Return: None #------------------------------------------------------------------------------ sub mkdir_recursive() { my $mdir = shift; my $array = shift; if ($mdir && -d $mdir) { return 1; } if ($mdir =~ m/^(.+)[\/\\]+([^\/\\]*)$/) { my ($parent, $dir)=($1, $2); unless ($parent && -d $parent) { &mkdir_recursive($parent,$array); } if ($parent && $dir && -d $parent) { debug(" Create dir '$parent/$dir'",2); if (mkdir "$parent/$dir") { push @{$array}, "$parent/$dir"; #print STDERR "$parent/$dir\n"; return 1; } else { error("Cannot mkdir '$parent/$dir', $!\n"); return 0; } } else { return 0; } } else { debug(" Create dir '$mdir'",2); if (mkdir "$mdir") { push @{$array}, "$mdir"; #print STDERR "$parent/$dir\n"; return 1; } else { error("Cannot mkdir '$mdir', $!\n"); return 0; } } } #------------------------------------------------------------------------------ # Function: Write a warning message # Parameters: $message # Input: $HeaderHTTPSent $HeaderHTMLSent $WarningMessage %HTMLOutput # Output: None # Return: None #------------------------------------------------------------------------------ sub warning { print STDERR "Warning: $_[0]\n"; } #------------------------------------------------------- # Error #------------------------------------------------------- sub error { print STDERR "Error: $_[0]\n"; exit 1; } #------------------------------------------------------- # Debug #------------------------------------------------------- sub debug { my $level = $_[1] || 1; if ($Debug >= $level) { my $debugstring = $_[0]; if ($ENV{"GATEWAY_INTERFACE"}) { $debugstring =~ s/^ /   /; $debugstring .= "
"; } print STDERR "DEBUG $level - ".time." : $debugstring\n"; } 0; } #------------------------------------------------------- # Write #------------------------------------------------------- sub writeoutput { my $string=shift; my $screenonly=shift; print STDERR $string; 0; } sub writeoutputfile { my $string=shift; if ($OutputDir) { print FILE $string; } else { print $string; } 0; } #------------------------------------------------------- # LoadDataInMemory # Desc: Load data in data hashes (DataUser, ...) # Input global var: # $fullfilename - $filelog - $fileformat - $filename - $filerevision - $filedate - $fileuser # $filestate - $filelineadd - $filelinechange - $filelinedel - $filechange # Global var that can be modified: # $filestate ... #------------------------------------------------------- sub LoadDataInMemory { # Define filename my $newfilename=$fullfilename; # Define filelog $filelog =~ s/\n\s*\n/\n/g; # Remove blank lines $filelog =~ s/^\s*[\r\n]*//g; # Remove starting blank my $newfilelog=ucfirst("$filelog"); # If log start with "file .cvsignore was initially added on branch xxx" # It is an add in branch xxx if ($filelog =~ /file (.*) was initially added on branch (.*)/i) { if (! $Branch || (uc($Branch) != uc($2))) { debug(" Entry discarded because in branch $2",2); return; } } # DEFINE CHANGE STATUS (removed, changed or added) OF FILE my $newfilestate=''; if ($Output =~ /^listdelta/ || $Output =~ /^buildhtmlreport/) { if ($Branch) { # We work in a secondary BRANCH: Change status can't be defined if (!$filesym{$fullfilename}{$Branch}) { return; } # This entry is not in branch $newfilestate="unknown"; } else { # We work in main BRANCH if ($TagStart && $filesym{$fullfilename}{$TagStart}) { # File did exist at the beginning if ($TagEnd && ! $filesym{$fullfilename}{$TagEnd}) { # File was removed between TagStart and TagEnd $newfilestate="removed"; } else { if ($filestate !~ /dead/) { $newfilestate="changed"; } else { $newfilestate="removed"; } } } else { # File did not exist for required start if (! $TagEnd || $filesym{$fullfilename}{$TagEnd}) { # File was added after TagStart (and before TagEnd) # If file contains Attic, this means it was removed so, as it didn't exists in start tag version, # this means we can ignore this file if we need a delta. if ($filename =~ /[\\\/]Attic([\\\/][^\\\/]+)/ && $Output =~ /^listdelta/) { return; } if ($filestate !~ /dead/) { if ($filechange) { $newfilestate="changed"; # TODO Sometimes it should be "added" (if added after a remove). This will be corrected later. } else { # A file added after TagStart $newfilestate="added"; } } else { $newfilestate="removed"; } } else { $newfilestate="removed"; return; } } } } # We know state # If added or removed, value for lines added and deleted is not correct, so we download file to count them if ($Output =~ /^buildhtmlreport/ && ($newfilestate eq 'added' || $newfilestate eq 'removed') && $fileformat ne 'b' && $ENABLEREQUESTFORADD) { my $filerevisiontoscan=$filerevision; if ($newfilestate eq 'removed') { $filerevisiontoscan=DecreaseVersion($filerevisiontoscan); } my $nbline=0; my $relativefilename=ExcludeRepositoryFromPath("$fullfilename",0,1); my $relativefilenamekeepattic=ExcludeRepositoryFromPath("$fullfilename",1,1); my $relativefilenamenospace=$relativefilename; $relativefilenamenospace=~s/\s/%20/g; if (! defined($Cache{$relativefilenamenospace}{$filerevisiontoscan}) || $Cache{$relativefilenamenospace}{$filerevisiontoscan} =~ /^ERROR/) { # If number of lines for file not available in cache file, we download file #-------------------------------------------------------------------------- my $filenametoget=$relativefilenamekeepattic; # Create dir if not exists my @added_dir_to_remove=(); my @added_files_to_remove=(); debug(" Number of lines not available, need to get file '$filenametoget' $filerevisiontoscan\n",2); if ($filenametoget =~ /Attic\//) { my $dir=$filenametoget; #$dir =~ s/^[^\@]+\@//; $dir =~ s/[\/\\]*[^\/\\]+$//; if ($dir) { # Create dir to allow cvs update &mkdir_recursive("$dir/CVS",\@added_dir_to_remove); if (! -f "$dir/CVS/Entries") { debug(" Create file '$dir/CVS/Entries'",2); push @added_files_to_remove, "$dir/CVS/Entries"; open(ENTRIESFILE,">$dir/CVS/Entries"); close(ENTRIESFILE); } if (! -f "$dir/CVS/Repository") { debug(" Create file '$dir/CVS/Repository'",2); push @added_files_to_remove, "$dir/CVS/Repository"; my $relativepath=$relativefilename; $relativepath =~ s/[\\\/][^\/\\]+$//; open(REPOSITORY,">$dir/CVS/Repository"); print REPOSITORY "$Module/$relativepath"; close(REPOSITORY); } } $filenametoget =~ s/Attic\///; } # TODO update with both -p and -d does not work (don't know why). # Must change to first run update -d, then update -p -r xxx #my $command="$CVSCLIENT $COMP -d ".$ENV{"CVSROOT"}." update -d"; #my $command="$CVSCLIENT $COMP -d ".$ENV{"CVSROOT"}." update -p -r $filerevisiontoscan $filenametoget"; my $command="$CVSCLIENT $COMP -d \"".$ENV{"CVSROOT"}."\" update -p -d -r $filerevisiontoscan \"$filenametoget\""; debug(" Getting file '$relativefilename' revision '$filerevisiontoscan'\n",3); debug(" with command '$command'\n",3); my $errorstring=''; my $pid = open(PH, "$command 2>&1 |"); while () { #chomp $_; s/\r$//; #debug("$_"); if ($_ =~ /cvs \[update aborted\]: no repository/) { $errorstring=$_; $nbline=0; last; } if ($_ =~ /cvs \[update aborted\]:/) { $errorstring=$_; $nbline=0; last; } $nbline++; } # Show or exploit result if ($errorstring) { warning(" Failed to execute command: $command: $errorstring"); if ($Cache{$relativefilenamenospace}{$filerevisiontoscan} =~ /^ERROR(\d*)$/) { # If it was not in error before, we track the error in cache file #print CACHE "$relativefilenamenospace $filerevisiontoscan ERROR".(int($1)+1)." $command: $errorstring\n"; } else { # If it was not in error before, we track the error in cache file print CACHE "$relativefilenamenospace $filerevisiontoscan ERROR1 $command: $errorstring\n"; } } else { debug(" Nb of line : $nbline",2); $Cache{$relativefilenamenospace}{$filerevisiontoscan}=$nbline; # Save result in a cache for other run print CACHE "$relativefilenamenospace $filerevisiontoscan $nbline $fileformat\n"; } close(PH); # Remove downloaded files and dir foreach my $filetodelete (@added_files_to_remove) { debug(" Remove file '$filetodelete'",2); unlink $filetodelete; } foreach my $dirtodelete (reverse @added_dir_to_remove) { debug(" Remove dir '$dirtodelete'",2); rmdir $dirtodelete; } } else { $nbline=$Cache{$relativefilenamenospace}{$filerevisiontoscan}; } print STDERR "."; if ($newfilestate eq 'added') { $filechange="+$nbline -0"; $filelineadd=$nbline; } if ($newfilestate eq 'removed') { debug(" Nb of line : $nbline $relativefilename $filerevisiontoscan",2); $filechange="+0 -$nbline"; $filelinedel=$nbline; } } # Update last date of tags foreach my $tag (keys %{$filesym{$newfilename}}) { if ("$filesym{$newfilename}{$tag}" eq "$filerevision") { # Prendre comparaison texte pour avoir 1.1 != 1.10 if (! $tagsfulldate{$tag} || $filedate > $tagsfulldate{$tag}) { $tagsfulldate{$tag}=$filedate; $filedate =~ /^(\d\d\d\d\d\d\d\d)/; $tagsshortdate{$tag}="$1"; debug(" Update date of tag '$tag' with full date '$filedate' and short date '$1' (from $newfilename $filerevision)",5); } } } # All infos were found. We can process record debug(" >>>> File revision: $fileformat - $newfilename - $filerevision - $filedate - $fileuser - $filestate - $filelineadd - $filelinechange - $filelinedel - $filechange => $newfilestate",2); # For output by date if ($Output =~ /bydate/ || $Output =~ /forrpm/ || $Output =~ /buildhtmlreport/) { $filedate=~/(\d\d\d\d\d\d\d\d)\s(\d\d)/; my $fileday=$1; my $filehour=$2; $HourUser{"$filehour $fileuser"}++; $DateUser{"$fileday $fileuser"}++; $DateUserLog{"$fileday $fileuser"}{$newfilelog}++; $DateUserLogFileRevState{"$fileday $fileuser"}{$newfilelog}{"$newfilename $filerevision"}=$newfilestate; if ($newfilestate eq 'removed') { # Change a state of a revision from "changed" into "added" when previous revision was "removed" my $filerevisionnext=$filerevision; if ($filerevisionnext =~ /\.(\d+)$/) { my $newver=int($1)+1; $filerevisionnext =~ s/\.(\d+)$/\.$newver/; } if ($DateUserLogFileRevState{$oldfiledayuser}{$oldfilelog}{"$newfilename $filerevisionnext"} =~ /^changed$/) { debug(" Correct next version of $newfilename $filerevisionnext ($filerevisionnext should be 'added_forced' instead of 'changed')",3); $DateUserLogFileRevState{$oldfiledayuser}{$oldfilelog}{"$newfilename $filerevisionnext"}="added_forced"; } } # When a version file does not exists in end, all versions are at state 'removed'. # We must change this into "changed" for those whose next revision exists and is 'removed'. Only last one stay 'removed'. if ($newfilestate eq 'removed') { my $filerevisionnext=$filerevision; if ($filerevisionnext =~ /\.(\d+)$/) { my $newver=int($1)+1; $filerevisionnext =~ s/\.(\d+)$/\.$newver/; } if ($DateUserLogFileRevState{$oldfiledayuser}{$oldfilelog}{"$newfilename $filerevisionnext"} =~ /^(removed|changed_forced)$/) { debug(" Correct version of $newfilename $filerevision ($filerevision should be 'changed_forced' instead of 'removed')",3); $DateUserLogFileRevState{"$fileday $fileuser"}{$newfilelog}{"$newfilename $filerevision"}='changed_forced'; # with _forced to not be change again by previous test } } # Var used to retrieve easily the revision already read just before the one processed in this function $oldfiledayuser="$fileday $fileuser"; $oldfilelog="$newfilelog"; my $filechangebis=$filechange; $filechangebis=~s/\-/ \-/; if ($fileformat ne 'b') { $DateUserLogFileRevLine{"$fileday $fileuser"}{$newfilelog}{"$newfilename $filerevision"}=$filechangebis; } else { $DateUserLogFileRevLine{"$fileday $fileuser"}{$newfilelog}{"$newfilename $filerevision"}='binary'; } } # For output by file if ($Output =~ /byfile/ || $Output =~ /buildhtmlreport/) { if (! $FilesLastVersion{$newfilename}) { $FilesLastVersion{$newfilename}=$filerevision; } # Save 'last' file version $FilesChangeDate{$newfilename}{$filerevision}=$filedate; $FilesChangeUser{$newfilename}{$filerevision}=$fileuser; $FilesChangeState{$newfilename}{$filerevision}=$newfilestate; $FilesChangeLog{$newfilename}{$filerevision}=$newfilelog; } # For output by log if ($Output =~ /bylog/ || $Output =~ /buildhtmlreport/) { $LogChange{$newfilelog}=1; $LogChangeDate{$newfilelog}{"$newfilename $filerevision"}=$filedate; $LogChangeUser{$newfilelog}{"$newfilename $filerevision"}=$fileuser; $LogChangeState{$newfilelog}{"$newfilename $filerevision"}=$newfilestate; } if ($Output =~ /^buildhtmlreport/) { if (! $UserChangeLast{$fileuser} || int($UserChangeLast{$fileuser}) < int($filedate)) { $UserChangeLast{$fileuser}=$filedate; } $UserChangeCommit{$fileuser}{$fullfilename}++; if ($fileformat ne 'b') { $UserChangeLineAdd{$fileuser}+=$filelineadd; $UserChangeLineDel{$fileuser}+=$filelinedel; $UserChangeLineChange{$fileuser}+=$filelinechange; } } } #------------------------------------------------------------------------------ # Function: Formate a string to an html readable string # Parameters: stringtoencode # Input: None # Output: None # Return: encodedstring #------------------------------------------------------------------------------ sub HtmlEntities { my $stringtoclean=shift; $stringtoclean =~ s/>/>/g; # Replace html tags with save > $stringtoclean =~ s/'#008822','changed'=>'#888888','removed'=>'#880000'); return "$string"; } #------------------------------------------------------------------------------ # Function: Format a viewcvs file link #------------------------------------------------------------------------------ sub FormatCvsFileLink { my $url=shift; my $version=shift; if ($ViewCvsUrl) { my $string=''; $string="$ViewCvsUrl"; $string =~ s/__MODULE__/$Module/g; $url =~ s/$Module\@//g; $string.="$url"; $string.="?view=markup&rev=".$version; return "$url"; } else { return "$url"; } } #------------------------------------------------------------------------------ # Function: Format a viewcvs diff link # ViewCvsUrl = http://cvs.sourceforge.net/viewcvs.py/__MODULE__/ #------------------------------------------------------------------------------ sub FormatCvsDiffLink { my $url=shift; my $version=shift; if ($ViewCvsUrl) { my $string=''; my $label='diff'; $string="$ViewCvsUrl"; $string =~ s/__MODULE__/$Module/g; $url =~ s/$Module\@//g; $string.="$url"; if (CompareVersionBis($version,"1.1")>0) { my $versionprec=DecreaseVersion($version); $string.=".diff?r1=".$versionprec; $string.="&r2=".$version; } else { $string.="?rev=".$version; } return "$label"; } else { return "$url"; } } #------------------------------------------------------------------------------ # Function: Format a number # Input: number precision # Return: dd.d # String "Thu Mar 7 2002 xxx" if option is "rpm" # String "YYYY-MM-DD HH:MM" if option is "simple" #------------------------------------------------------------------------------ sub RoundNumber { my $number=shift; my $precision=shift; foreach (1..$precision) { $number*=10; } $number=int($number); foreach (1..$precision) { $number/=10; } return "$number"; } #------------------------------------------------------------------------------ # Function: Compare 2 CVS version number # Input: $a $b # Output: -1 if $a < $b, 1 if $a >= $b #------------------------------------------------------------------------------ sub CompareVersion { my $a=shift; my $b=shift; my $aint; my $adec; my $bint; my $bdec; if ($a =~ /^(\d+)\.(\d+)$/) { $aint=int($1); $adec=int($2); } else { $aint=int($a); $adec=0; } if ($b =~ /^(\d+)\.(\d+)$/) { $bint=int($1); $bdec=int($2); } else { $bint=int($b); $bdec=0; } if ($aint < $bint) { return -1; } if ($aint > $bint) { return 1; } if ($adec < $bdec) { return -1; } return 1; } #------------------------------------------------------------------------------ # Function: Compare 2 CVS version number # Input: $a $b # Output: -1 if $a < $b, 0 if $a = $b, 1 if $a > $b #------------------------------------------------------------------------------ sub CompareVersionBis { my $a=shift; my $b=shift; my $aint; my $adec; my $bint; my $bdec; if ($a =~ /^(\d+)\.(\d+)$/) { $aint=int($1); $adec=int($2); } else { $aint=int($a); $adec=0; } if ($b =~ /^(\d+)\.(\d+)$/) { $bint=int($1); $bdec=int($2); } else { $bint=int($b); $bdec=0; } if ($aint < $bint) { return -1; } if ($aint > $bint) { return 1; } if ($adec < $bdec) { return -1; } if ($adec > $bdec) { return 1; } return 0; } #------------------------------------------------------------------------------ # Function: Decrease a version number by one # Input: 1.159 # Output: 1.158 #------------------------------------------------------------------------------ sub DecreaseVersion { my $a=shift; if ($a =~ /^(\d+)\.(\d+)$/ && $2 > 0) { return "$1.".(int($2) - 1); } return $a; # can't automatically decrease } #------------------------------------------------------------------------------ # Function: Remove repository path from a full path # Input: string keepattic removemodule # Output: a string path #------------------------------------------------------------------------------ sub ExcludeRepositoryFromPath { my $file=shift; my $keepattic=shift; my $removemodule=shift; if (! $keepattic) { $file =~ s/[\\\/]Attic([\\\/][^\\\/]+)/$1/; } if ($removemodule) { $file =~ s/^$CacheNameForModule\@//; # Remove Module name } $file =~ s/^($CacheNameForModule\@)(\w:)?$RepositoryPath[\\\/]$Module[\\\/]/$1/; # Remove path repository $file =~ s/^(\w:)?$RepositoryPath[\\\/]$Module[\\\/]//; return $file; } #------------------------------------------------------------------------------ # Function: Return day of week of a day # Parameters: "$year$month$day" # Return: 1-7 (1 = monday, 7=sunday) #------------------------------------------------------------------------------ sub DayOfWeek { shift =~ /(\d\d\d\d)(\d\d)(\d\d)/; my ($day, $month, $year) = ($3, $2, $1); if ($Debug) { debug("DayOfWeek for $day $month $year",4); } if ($month < 3) { $month += 10; $year--; } else { $month -= 2; } my $cent = sprintf("%1i",($year/100)); my $y = ($year % 100); my $dw = (sprintf("%1i",(2.6*$month)-0.2) + $day + $y + sprintf("%1i",($y/4)) + sprintf("%1i",($cent/4)) - (2*$cent)) % 7; $dw += 7 if ($dw<0); if (! $dw) { $dw = 7; } # It's sunday if ($Debug) { debug(" is $dw",4); } return $dw; } #------------------------------------------------------- # MAIN #------------------------------------------------------- # GET PARAMETERS #--------------- my $QueryString=join(' ', @ARGV); if ($QueryString =~ /debug=(\d+)/i) { $Debug=$1; } if ($QueryString =~ /m(?:odule|)=([^\s]+)/i) { $Module=$1; } if ($QueryString =~ /output=([^\s]+)/i) { $Output=$1; } if ($QueryString =~ /branch=([^\s]+)/i) { $Branch=$1; } if ($QueryString =~ /since=([^\s]+)/i) { $Since=$1; } if ($QueryString =~ /tagstart=([^\s]+)/i) { $TagStart=$1; } if ($QueryString =~ /tagend=([^\s]+)/i) { $TagEnd=$1; } if ($QueryString =~ /-ssh/) { $UseSsh=1 } if ($QueryString =~ /rlogfile=([:\-\.\\\/\w~]+)/i) { $RLogFile=$1; } if ($QueryString =~ /keeprlogfile/i) { $KeepRlogFile=1; } if ($QueryString =~ /dir=([^\s]+)/i) { $OutputDir=$1; } if ($QueryString =~ /viewcvsurl=([^\s]+)/i) { $ViewCvsUrl=$1; } if ($QueryString =~ /-d=([^\s]+)/) { $CvsRoot=$1; } if ($QueryString =~ /-h/) { $Help=1; } if ($QueryString =~ /-ignore=([^\s]+)/i) { map { $IgnoreFileDir{quotemeta($_)}=1; } split(',',$1); } if ($QueryString =~ /-only=([^\s]+)/i) { $OnlyFileDir{$1}=1; } if ($QueryString =~ /-allowindex/i) { $AllowIndex=1; } if ($QueryString =~ /-includeheader=([^\s]+)/i) { $IncludeHeader=$1; } ($DIR=$0) =~ s/([^\/\\]+)$//; ($PROG=$1) =~ s/\.([^\.]*)$//; $Extension=$1; $DIR||='.'; $DIR =~ s/([^\/\\])[\\\/]+$/$1/; debug("Parameter Module : $Module"); debug("Parameter Output : $Output"); debug("Parameter OutputDir : $OutputDir"); debug("Parameter Branch : $Branch"); debug("Parameter ViewCvsUrl : $ViewCvsUrl"); debug("Parameter Since : $Since"); debug("Parameter Debug : $Debug"); debug("Parameter IgnoreFileDir: ".join(',',keys %IgnoreFileDir)); if ($ViewCvsUrl && $ViewCvsUrl !~ /\/$/) { $ViewCvsUrl.="/"; } # On determine chemin complet du repertoire racine et on en deduit les repertoires de travail my $REPRACINE; if (! $ENV{"SERVER_NAME"}) { $REPRACINE=($DIR?$DIR:".")."/.."; } else { $REPRACINE=$ENV{"DOCUMENT_ROOT"}; } # CHECK INPUT PARAMETERS VALIDITY #-------------------------------- if ($Help || ! $Output) { writeoutput("----- $PROG $VERSION (c) Laurent Destailleur -----\n"); writeoutput("$PROG generates advanced ChangeLog/Report files for CVS projects/modules.\n"); writeoutput("Note 1: Your cvs client (cvs or cvs.exe) must be in your PATH.\n"); writeoutput("Note 2: To use $PROG with a csv client through ssh, add option -ssh.\n"); writeoutput("\nUsage:\n"); writeoutput(" $PROG.$Extension -output=outputmode [-m=module -d=repository] [options]\n"); writeoutput("\n"); writeoutput("Where 'output' is:\n"); writeoutput(" listdeltabydate To get a changelog between 2 versions, sorted by date\n"); writeoutput(" listdeltabylog To get a changelog between 2 versions, sorted by log\n"); writeoutput(" listdeltabyfile To get a changelog between 2 versions, sorted by file\n"); writeoutput(" listdeltaforrpm To get a changelog between 2 versions for rpm spec files\n"); writeoutput(" buildhtmlreport To build an html report\n"); writeoutput("\n"); writeoutput(" Note that \"between 2 versions\" means (depends on tagstart/tagend options):\n"); writeoutput(" * from start to a tagged version (version changes included)\n"); writeoutput(" * from a tagged version (excluded) to another tagged version (included)\n"); writeoutput(" * or from a tagged version until now (version changes excluded)\n"); writeoutput("\n"); writeoutput(" You can also add extra parameters when output=buildhtmlreport by adding them\n"); writeoutput(" after a colon and separated by a comma, like this:\n"); writeoutput(" -output=buildhtmlreport:param1,param2\n"); writeoutput(" This is extra paremeters available for -output=buildhtmlreport mode:\n"); writeoutput(" nosummary To remove summary part\n"); writeoutput(" nolinesofcode To remove lines of code part\n"); writeoutput(" nodevelopers To remove developers part\n"); writeoutput(" nodaysofweek To remove days of week part\n"); writeoutput(" nohours To remove hours part\n"); writeoutput(" notags To remove tags part\n"); writeoutput(" nolastlogs To remove last logs part\n"); writeoutput(" nolimit To not limit last logs to last $MAXLASTLOG\n"); writeoutput(" sortbyrevision To sort last logs by revision\n"); writeoutput(" includediff To include diff inside report page (very slow)\n"); writeoutput(" loosecommits To separate commits for same log by spaces\n"); writeoutput("\n"); writeoutput("The 'module' and 'repository' are the CVS module name and the CVS repository.\n"); writeoutput(" If current directory is the root of a CVS project built from a cvs checkout,\n"); writeoutput(" cvschangelogbuilder will retreive module and repository value automatically.\n"); writeoutput(" If no local copy of repository are available or to force other value, use:\n"); writeoutput(" -m=module To force value of module name\n"); writeoutput(" -d=repository To force value of CVSROOT\n"); writeoutput("\n"); writeoutput("Options are:\n"); writeoutput(" -branch=branchname To work on another branch than default branch (!)\n"); writeoutput(" -tagstart=tagname To specify start tag version\n"); writeoutput(" -tagend=tagend To specify end tag version\n"); writeoutput("\n"); writeoutput(" !!! WARNING: If you use tagstart and/or tagend, check that tags are in SAME\n"); writeoutput(" BRANCH. Also, it must be the default branch, if not, you MUST use -branch to\n"); writeoutput(" give the name of the branch, otherwise you will get unpredicable result.\n"); writeoutput("\n"); writeoutput(" -ssh To run CVS through ssh (this set env var CVS_RSH=\"ssh\")\n"); writeoutput(" -rlogfile=rlogfile If an up-to-date log file already exists localy, you can\n"); writeoutput(" use this option to avoid log download, for a faster result.\n"); writeoutput(" -keeprlogfile Once process is finished, you can ask to not remove the\n"); writeoutput(" downloaded log file.\n"); writeoutput(" -dir=dirname Output is built in directory dirname.\n"); writeoutput(" -viewcvsurl=viewcvsurl File's revisions in reports built by buildhtmlreport\n"); writeoutput(" output are links to \"viewcvs\". String '__MODULE__'\n"); writeoutput(" will be replaced by name of CVS module.\n"); writeoutput(" -ignore=file/dir To exclude a file/dir off report.\n"); writeoutput(" -only=file/dir To have reports only on file/dir that match.\n"); writeoutput(" -includeheader=file To add content of a file after body tag.\n"); writeoutput(" -allowindex To allow meta tag index (noindex by default).\n"); writeoutput(" -debug=x To output on stderr some debug info with level x.\n"); writeoutput("\n"); writeoutput("Examples:\n"); writeoutput(" $PROG.$Extension -output=listdeltabyfile -module=myproject -tagstart=myproj_2_0 -d=john\@cvsserver:/cvsdir\n"); writeoutput(" $PROG.$Extension -output=listdeltabydate -module=mymodule -d=:ntserver:127.0.0.1:d:/mycvsdir\n"); writeoutput(" $PROG.$Extension -output=listdeltabylog -module=mymodule -d=:pserver:user\@127.0.0.1:/usr/local/cvsroot\n"); writeoutput(" $PROG.$Extension -output=buildhtmlreport -module=mymodule -d=:ext:user\@cvs.sourceforge.net:/cvsroot -viewcvsurl=\"http://savannah.nongnu.org/cgi-bin/viewcvs/project/__MODULE__\"\n"); writeoutput("\n"); exit 0; } if ($Output) { if ($Output =~ s/:(.*)//g) { # There is some parameters on output option my %param=(); foreach my $key (split(/,/,$1)) { $param{$key}=1; } if ($param{'nolimit'}) { $MAXLASTLOG=0; } if ($param{'sortbyrevision'}) { $SORTBYREVISION=1; } if ($param{'loosecommits'}) { $LOOSECOMMITS=1; } if ($param{'nosummary'}) { $NOSUMMARY=1; } if ($param{'includediff'}) { $INCLUDEDIFF=1; } if ($param{'nolinesofcode'}) { $NOLINESOFCODE=1; } if ($param{'nodevelopers'}) { $NODEVELOPERS=1; } if ($param{'nodaysofweek'}) { $NODAYSOFWEEK=1; } if ($param{'nohours'}) { $NOHOURS=1; } if ($param{'notags'}) { $NOTAGS=1; } if ($param{'nolastlogs'}) { $NOLASTLOGS=1; } } my %allowedvalueforoutput=( "listdeltabydate"=>1, "listdeltabylog"=>1, "listdeltabyfile"=>1, "listdeltaforrpm"=>1, "buildhtmlreport"=>1 ); if (! $allowedvalueforoutput{$Output}) { writeoutput("----- $PROG $VERSION (c) Laurent Destailleur -----\n"); writeoutput("Unknown value for output parameter.\n"); exit 1; } } # Get current time my $nowtime=time(); my ($nowsec,$nowmin,$nowhour,$nowday,$nowmonth,$nowyear) = localtime($nowtime); if ($nowyear < 100) { $nowyear+=2000; } else { $nowyear+=1900; } my $nowsmallyear=$nowyear;$nowsmallyear =~ s/^..//; if (++$nowmonth < 10) { $nowmonth = "0$nowmonth"; } if ($nowday < 10) { $nowday = "0$nowday"; } if ($nowhour < 10) { $nowhour = "0$nowhour"; } if ($nowmin < 10) { $nowmin = "0$nowmin"; } if ($nowsec < 10) { $nowsec = "0$nowsec"; } # Get tomorrow time (will be used to discard some record with corrupted date (future date)) my ($tomorrowsec,$tomorrowmin,$tomorrowhour,$tomorrowday,$tomorrowmonth,$tomorrowyear) = localtime($nowtime+86400); if ($tomorrowyear < 100) { $tomorrowyear+=2000; } else { $tomorrowyear+=1900; } my $tomorrowsmallyear=$tomorrowyear;$tomorrowsmallyear =~ s/^..//; if (++$tomorrowmonth < 10) { $tomorrowmonth = "0$tomorrowmonth"; } if ($tomorrowday < 10) { $tomorrowday = "0$tomorrowday"; } if ($tomorrowhour < 10) { $tomorrowhour = "0$tomorrowhour"; } if ($tomorrowmin < 10) { $tomorrowmin = "0$tomorrowmin"; } if ($tomorrowsec < 10) { $tomorrowsec = "0$tomorrowsec"; } my $timetomorrow=$tomorrowyear.$tomorrowmonth.$tomorrowday.$tomorrowhour.$tomorrowmin.$tomorrowsec; # -- Start for module # Check/Retrieve module name my $ModuleChoosed=$Module; if (! $Module || $Output =~ /^buildhtmlreport/) { $Module=''; if (-s "CVS/Repository") { open(REPOSITORY,") { chomp $_; s/\r$//; $Module=$_; last; } close(REPOSITORY); } } if ($Output =~ /^buildhtmlreport/ && ! $Module) { writeoutput("\n"); error("To generate the buildhtmlreport output, $PROG must be launched from the checkout root directory of project."); } if ($Output =~ /^buildhtmlreport/ && $ModuleChoosed && $Module && $Module ne $ModuleChoosed) { writeoutput("\n"); error("To generate the buildhtmlreport output, $PROG must be launched from the right checkout root directory.\n$PROG is launched from a checkout root directory of module '$Module' but you ask a report for module '$ModuleChoosed'."); } if (! $Module) { writeoutput("\n"); error("The module name was not provided and could not be detected.\nUse -m=cvsmodulename option to specifiy module name.\n\nExample: $PROG.$Extension -output=$Output -module=mymodule -d=:pserver:user\@127.0.0.1:/usr/local/cvsroot"); } writeoutput(ucfirst($PROG)." launched for module: $Module\n",1); # Define CacheNameForModule (use for cache file and fullfilename) $CacheNameForModule=$Module; $CacheNameForModule =~ s/[\/\\\s]/_/g; # In case $Module contains '/','\',' ' # Check/Retrieve CVSROOT environment variable (needed to get $RepositoryPath) my $CvsRootChoosed=$CvsRoot; if (! $CvsRoot || $Output =~ /^buildhtmlreport/) { $CvsRoot=''; # Try to get CvsRoot from CVS repository if (-s "CVS/Root") { open(REPOSITORY,") { chomp $_; s/\r$//; $CvsRoot=$_; last; } close(REPOSITORY); } } if ($Output =~ /^buildhtmlreport/ && ! $CvsRoot) { writeoutput("\n"); error("To generate the buildhtmlreport output, $PROG must be launched from a checkout root directory of project."); } if ($Output =~ /^buildhtmlreport/ && $CvsRootChoosed && $CvsRoot && $CvsRoot ne $CvsRootChoosed) { writeoutput("\n"); error("To generate the buildhtmlreport output, $PROG must be launched from the right checkout root directory.\n$PROG is launched from a checkout root directory of module '$Module' with cvsroot '$CvsRoot' but you ask a report ".($ModuleChoosed?"for module '$ModuleChoosed' ":"")."with a different cvsroot '$CvsRootChoosed'."); } if (! $CvsRoot) { # Try to set CvsRoot from CVSROOT environment variable if ($ENV{"CVSROOT"}) { $CvsRoot=$ENV{"CVSROOT"}; } } if (! $CvsRoot) { writeoutput("\n"); error("The repository value to use was not provided and could not be detected.\nUse -d=repository option to specifiy repository value.\n\nExample: $PROG.$Extension -output=$Output -module=mymodule -d=:pserver:user\@127.0.0.1:/usr/local/cvsroot"); } if ($OutputDir) { $OutputDir.="/"; } # Set use of ssh or not if ($UseSsh) { writeoutput("Set CVS_RSH=ssh\n",1); $ENV{'CVS_RSH'}='ssh'; } # SUMMARY OF PARAMETERS #------------------------------------------ $ENV{"CVSROOT"}=$CvsRoot; writeoutput(ucfirst($PROG)." launched for CVSRoot: $CvsRoot\n",1); $RepositoryPath=$CvsRoot; $RepositoryPath=~s/.*:([^:]+)/$1/; writeoutput(ucfirst($PROG)." launched for directory repository: $RepositoryPath\n",1); $RepositoryPath=quotemeta($RepositoryPath); writeoutput(ucfirst($PROG)." launched for Branch: ".($Branch?"$Branch":"HEAD")."\n",1); # LAUNCH CVS COMMAND RLOG TO WRITE RLOGFILE #------------------------------------------ if (! $RLogFile) { print STDERR "Need to download cvs log file, please wait...\n"; # Define temporary file my $TmpDir=""; $TmpDir||=$ENV{"TMP"}; $TmpDir||=$ENV{"TEMP"}; $TmpDir||='/tmp'; my $TmpFile="$TmpDir/$PROG.$CacheNameForModule.$$.tmp"; open(TEMPFILE,">$TmpFile") || error("Failed to open temp file '$TmpFile' for writing. Check directory and permissions."); my $command; if ($Branch) { $command="$CVSCLIENT $COMP -d \"" . $ENV{"CVSROOT"}."\" rlog -r${Branch} " . ($Since? " -d'" . $Since . "' " : "") . "\"$Module\""; } else { $command="$CVSCLIENT $COMP -d \"" . $ENV{"CVSROOT"}."\" rlog -b " . ($Since? " -d'" . $Since . "' " : "") . ($TagStart||$TagEnd?"-r${TagStart}::${TagEnd} ":"") . "\"$Module\""; } writeoutput("Downloading temporary cvs rlog file '$TmpFile'\n",1); writeoutput("with command '$command'\n",1); debug("CVSROOT value is '".$ENV{"CVSROOT"}."'"); my $result=`$command 2>&1`; print TEMPFILE "$result"; close TEMPFILE; if (! $result || $result !~ /cvs \w+: Logging/i) { # With log we get 'cvs server: Logging awstats' and with rlog we get 'cvs rlog: Logging awstats' error("Failure in cvs command: '$command'\n$result"); } $RLogFile=$TmpFile; } # LOAD CACHE OF NBOFLINES FOR EACH FILE/REVISION #---------------------------------------------------- my $cachefile="${OutputDir}${PROG}_${CacheNameForModule}.cache"; if ($Output =~ /^buildhtmlreport/) { # Try to read cache file # Cache file format are records: "file revision nboflines bin/ascii" debug(" Search for cache file '$cachefile' into current directory",1); if (-f $cachefile) { writeoutput("Load cache file '$cachefile' with number of lines for added files...\n",1); open(CACHE,"<$cachefile") || error("Failed to open cache file '$cachefile' for reading"); while () { chomp $_; s/\r$//; if (! $_) { next; } my ($file,$revision,$nbline,undef)=split(/\s+/,$_); debug(" Load cache entry for ($file,$revision)=$nbline",2); $Cache{$file}{$revision}=$nbline; # If duplicate records, the last one will be used } close CACHE; } else { print STDERR "No cache file can be found. This probably means you run $PROG for\n"; print STDERR "the first time. Building cache for the first update can take a very long\n"; print STDERR "time (between several seconds to hours depending on your CVS server response\n"; print STDERR "time), so please wait...\n"; } } # ANALYZE RLOGFILE AND UPDATE CACHE FILE #--------------------------------------- writeoutput("Analyzing rlog file '$RLogFile'\n",1); open(RLOGFILE,"<$RLogFile") || error("Can't open rlog file"); if ($Output =~ /^buildhtmlreport/) { # Open cache file to write new files entries open(CACHE,">>$cachefile") || error("Failed to open cache file '$cachefile' for writing"); } my $waitfor="filename"; while () { chomp $_; s/\r$//; my $line="$_"; debug("New read line: $line (waitfor=$waitfor)",3); if ($line =~ /^branches:/) { next; } if ($line =~ /^locks:/) { next; } if ($line =~ /^access list:/) { next; } # Check if there is a warning in rlog file #if ($line =~ /^cvs rlog: warning: no revision/) { print("$line\n"); next; } # End of revision if ($line =~ /--------/) { if ($waitfor eq "log" && $filename && $filename !~ /__discarded/) { # Load all data for this revision file in memory debug("Info are complete, we store them",2); LoadDataInMemory(); debug(" Revision info are stored.",2); } else { if ($filename =~ /__discarded/) { debug("File was discarded, we don't store info ($filename,$fullfilename,$fileformat,$filerevision,$filedate,$fileuser,$filestate,$filechange,$filelog,$filelineadd,$filelinedel,$filelinechange)",2); } else { debug("Info are not complete, we don't store them ($filename,$fullfilename,$fileformat,$filerevision,$filedate,$fileuser,$filestate,$filechange,$filelog,$filelineadd,$filelinedel,$filelinechange)",2); } } $waitfor="revision"; debug(" Now waiting for '$waitfor'.",2); $filerevision=''; $filedate=''; $fileuser=''; $filestate=''; $filechange=''; $filelog=''; $filelineadd=0; $filelinedel=0; $filelinechange=0; next; } # End of file if ($line =~ /========/) { if ($waitfor eq "log" && $filename && $filename !~ /__discarded/) { # Load all data for this revision file in memory debug("Info are complete, we store them",2); LoadDataInMemory(); debug(" Revision info are stored.",2); } else { if ($filename =~ /__discarded/) { debug("File was discarded, we don't store info ($filename,$fullfilename,$fileformat,$filerevision,$filedate,$fileuser,$filestate,$filechange,$filelog,$filelineadd,$filelinedel,$filelinechange)",2); } else { debug("Info are not complete, we don't store them ($filename,$fullfilename,$fileformat,$filerevision,$filedate,$fileuser,$filestate,$filechange,$filelog,$filelineadd,$filelinedel,$filelinechange)",2); } } $waitfor="filename"; debug(" Now waiting for '$waitfor'.",2); $filename=''; $fullfilename=''; $fileformat=''; $filerevision=''; $filedate=''; $fileuser=''; $filestate=''; $filechange=''; $filelog=''; $filelineadd=0; $filelinedel=0; $filelinechange=0; next; } #if ($line =~ /^cvs rlog: Logging (.*)/) { $Module=$1; } # Set module name from log file # New file found (even if not expected) if ($line =~ /$EXTRACTFILENAME/i) { $filename="$1"; $filename =~ s/,v$//; # Clean filename if ended with ",v" $fullfilename="$CacheNameForModule\@$filename"; my $truefilename=ExcludeRepositoryFromPath("$filename",0,1); # We found a new filename debug("Found a new file '$fullfilename', '$truefilename'",2); my $qualified=1; # Check if file qualified foreach my $key (keys %IgnoreFileDir) { debug("Check if file match IgnoreFileDir key '$key'",5); if ($truefilename =~ /$key/) { $qualified=-1; last; } } if (scalar keys %OnlyFileDir) { $qualified=-2; foreach my $key (keys %OnlyFileDir) { debug("Check if file match OnlyFileDir key '$key'",5); if ($truefilename =~ /$key/) { $qualified=1; last; } } } if ($qualified > 0) { debug("File is qualified to be included in report",2); $waitfor="symbolic_name"; $maxincludedver{"$fullfilename"}=0; $minexcludedver{"$fullfilename"}=0; } if ($qualified == -1) { debug("File discarded by ignore option",2); $filename='__discarded_by_ignore__'; } if ($qualified == -2) { debug("File discarded by only option",2); $filename='__discarded_by_only__'; } next; } # Wait for symbolic names area if ($waitfor eq "symbolic_name") { if ($line =~ /$EXTRACTSYMBOLICNAMEAREA/i) { # We found symbolic names area $waitfor="symbolic_name_entry"; debug("Found symbolic name area",2); } next; } # Wait for symbolic names entry if ($waitfor eq "symbolic_name_entry") { if ($line =~ /$EXTRACTSYMBOLICNAMEENTRY/i) { # We found symbolic name entry # We set symbolic name. Example: $filesym{$fullfilename}{MYAPPLI_1_0}=2.31 $filesym{$fullfilename}{$1}=$2; debug("Found symbolic name entry $1 is for version $filesym{$fullfilename}{$1}",2); if ($TagEnd && $TagEnd eq $1) { $maxincludedver{"$fullfilename"}=$2; debug(" Max included version for file '$fullfilename' set to $2",3); } if ($TagStart && $TagStart eq $1) { $minexcludedver{"$fullfilename"}=$2; debug(" Min excluded version for file '$fullfilename' set to $2",3); } } else { if ($line =~ /^keyword substitution: (\S+)/) { $fileformat=$1; } $waitfor="revision"; } next; } # Wait for a revision if ($waitfor eq "revision") { if ($line =~ /$EXTRACTFILEVERSION/i) { # We found a new revision number $filerevision=$1; $waitfor="dateuserstate"; debug("Found a new revision number: $filerevision. Now waiting for '$waitfor'.",2); } next; } # Wait for date and user of revision if ($waitfor eq "dateuserstate") { if ($line =~ /$EXTRACTFILEDATEUSERSTATE/i) { # We found date/user line # Date can be "2005/12/31 23:59:59", "2005-12-31 23:59:59" ... $filedate=$1; $fileuser=lc($2); $filestate=$3; $filechange=$4; $filedate =~ s/\///g; $filedate =~ s/-//g; $filelineadd=0; $filelinedel=0; $filelinechange=0; if ($filechange =~ s/.*([\+\-]\d+)\s+([\+\-]\d+).*/$1$2/g) { $filelineadd=int($1); $filelinedel=(-int($2)); if ($filelineadd>=$filelinedel) { $filelineadd-=$filelinedel; $filelinechange=$filelinedel; $filelinedel=0; } else { $filelinedel-=$filelineadd; $filelinechange=$filelineadd; $filelineadd=0; } } else { $filechange=""; # It's not a change but an add with cvsnt (+x -x are not reported with cvsnt) } $filedate =~ s/[\s;]+$//; $fileuser =~ s/[\s;]+$//; $filestate =~ s/[\s;]+$//; $filechange =~ s/\s+//g; $waitfor="log"; debug("Found a new date/user/state/nbadd/nbchange/nbdel $filedate $fileuser $filestate $filelineadd $filelinechange $filelinedel. Now waiting for '$waitfor'.",2); } next; } if ($waitfor eq "log") { # Line is log debug("Found a new line for log: $line",2); $filelog.="$line\n"; next; } } if ($Output =~ /^buildhtmlreport/) { close CACHE; } close RLOGFILE; # Build %tagsshortdate #--------------------- foreach my $tag (keys %tagsshortdate) { # $tagsshortdate{v1_0}=20041201 # $tagstags{20041201}{v1_0}=1 $tagstags{$tagsshortdate{$tag}}{$tag}=1; debug("Add entry in tagstags for key $tagsshortdate{$tag} with value $tag",2); } # BUILD OUTPUT #------------------------ my $OutputRootFile="${PROG}_".($Branch?"(${Branch})_${CacheNameForModule}":"${CacheNameForModule}"); # Start of true output if ($OutputDir) { open(FILE,">${OutputDir}${OutputRootFile}.html") || error("Error: Failed to open file '${OutputRootFile}.html' for output in directory '${OutputDir}'."); } writeoutput("\nBuild output for option '$Output'\n",1); # Build header my $headstring=''; my $rangestring=''; if ($Output !~ /buildhtmlreport$/) { $headstring.="\nChanges for '$Module'"; } else { $headstring.="\nCVS report for module '$Module'"; } if ($Branch) { $headstring.=" in branch $Branch"; $rangestring.="Branch $Branch"; } else { $rangestring.="Main Branch (HEAD)"; } if ($TagStart) { if ($TagEnd) { $headstring.=" beetween $TagStart (excluded) and $TagEnd (included)"; $rangestring.= " - Beetween $TagStart (excluded) and $TagEnd (included)"; } else { $headstring.=" since $TagStart (excluded)"; $rangestring.= " - Since $TagStart (excluded)"; } } elsif ($TagEnd) { $headstring.=" in $TagEnd"; $rangestring.= " in $TagEnd"; } $headstring.="\n built by $PROG $VERSION with option $Output."; if ($Output !~ /buildhtmlreport$/) { writeoutputfile "$headstring\n\n"; } else { writeoutputfile "\n\n"; writeoutputfile "\n"; writeoutputfile "\n"; writeoutputfile "\n"; writeoutputfile "\n"; writeoutputfile "CVS report for activity on module $Module\n"; writeoutputfile < EOF writeoutputfile "\n"; writeoutputfile "\n"; if ($IncludeHeader) { open(INCLUDE,"<$IncludeHeader") || die "Failed to open includeheader $IncludeHeader file"; my $line=''; while ($line=) { writeoutputfile $line; } close(INCLUDE); } } # For output by date if ($Output =~ /bydate$/ || $Output =~ /forrpm$/) { if (scalar keys %DateUser) { foreach my $dateuser (reverse sort keys %DateUser) { my $firstlineprinted=0; foreach my $logcomment (sort keys %{$DateUserLog{$dateuser}}) { foreach my $revision (sort keys %{$DateUserLogFileRevState{$dateuser}{$logcomment}}) { $revision=~/(.*)\s([\d\.]+)/; my ($file,$version)=($1,$2); if ($maxincludedver{"$file"} && (CompareVersionBis($2,$maxincludedver{"$file"}) > 0)) { debug("For file '$file' $2 > maxincludedversion= ".$maxincludedver{"$file"},3); next; } if ($minexcludedver{"$file"} && (CompareVersionBis($2,$minexcludedver{"$file"}) <= 0)) { debug("For file '$file' $2 <= minexcludedversion= ".$minexcludedver{"$file"},3); next; } if (! $firstlineprinted) { if ($Output =~ /forrpm$/) { writeoutputfile "* ".FormatDate($dateuser,'rpm')."\n"; } else { writeoutputfile FormatDate($dateuser)."\n"; } $firstlineprinted=1; } my $state=$DateUserLogFileRevState{$dateuser}{$logcomment}{$revision}; $state =~ s/_forced//; if ($Output !~ /forrpm$/) { writeoutputfile "\t* ".ExcludeRepositoryFromPath($file)." $version ($state):\n"; } } chomp $logcomment; $logcomment =~ s/\r$//; if ($firstlineprinted) { foreach my $logline (split(/\n/,$logcomment)) { if ($Output =~ /forrpm$/) { writeoutputfile "\t- $logline\n"; } else { writeoutputfile "\t\t$logline\n"; } } } } if ($firstlineprinted) { writeoutputfile "\n"; } } } else { writeoutputfile "No change detected.\n"; } } # For output by file if ($Output =~ /byfile$/) { if (scalar keys %FilesLastVersion) { foreach my $file (sort keys %FilesLastVersion) { my $firstlineprinted=0; my $val=''; foreach my $version (reverse sort { &CompareVersion($a,$b) } keys %{$FilesChangeDate{$file}}) { if ($maxincludedver{"$file"} && (CompareVersionBis($version,$maxincludedver{"$file"}) > 0)) { debug("For file '$file' $version > maxincludedversion= ".$maxincludedver{"$file"},3); next; } if ($minexcludedver{"$file"} && (CompareVersionBis($version,$minexcludedver{"$file"}) <= 0)) { debug("For file '$file' $version <= minexcludedversion= ".$minexcludedver{"$file"},3); next; } if (! $firstlineprinted) { writeoutputfile ExcludeRepositoryFromPath($file)."\n"; $firstlineprinted=1; } writeoutput sprintf ("\t* %-16s ",$version." (".$FilesChangeState{$file}{$version}.")"); writeoutputfile FormatDate($FilesChangeDate{$file}{$version})."\t$FilesChangeUser{$file}{$version}\n"; my $logcomment=$FilesChangeLog{$file}{$version}; chomp $logcomment; $logcomment =~ s/\r$//; if ($firstlineprinted) { foreach my $logline (split(/\n/,$logcomment)) { writeoutputfile "\t\t$logline\n"; } } } } } else { writeoutputfile "No change detected.\n"; } } # For output by log if ($Output =~ /bylog$/) { if (scalar keys %LogChange) { foreach my $logcomment (sort keys %LogChange) { my $firstlineprinted=0; my $newlogcomment=substr($logcomment,0,$LGMAXLOG); if (length($logcomment)>$LGMAXLOG) { $newlogcomment.="..."; } foreach my $revision (sort { &CompareVersion($a,$b) } keys %{$LogChangeDate{$logcomment}}) { $revision=~/^(.*)\s([\d\.]+)$/; my ($file,$version)=($1,$2); if ($maxincludedver{"$file"} && (CompareVersionBis($2,$maxincludedver{"$file"}) > 0)) { debug("For file '$file' $2 > maxincludedversion= ".$maxincludedver{"$file"},3); next; } if ($minexcludedver{"$file"} && (CompareVersionBis($2,$minexcludedver{"$file"}) <= 0)) { debug("For file '$file' $2 <= minexcludedversion= ".$minexcludedver{"$file"},3); next; } if (! $firstlineprinted) { writeoutputfile "$newlogcomment\n"; $firstlineprinted=1; } $file=ExcludeRepositoryFromPath($file); writeoutputfile "\t* ".FormatDate($LogChangeDate{$logcomment}{$revision})." $LogChangeUser{$logcomment}{$revision}\t $file $version ($LogChangeState{$logcomment}{$revision})\n"; } if ($firstlineprinted) { writeoutputfile "\n"; } } } else { writeoutputfile "No change detected.\n"; } } # Building html report #--------------------- if ($Output =~ /buildhtmlreport$/) { writeoutput("Generating HTML report...\n",1); my ($errorstringlines,$errorstringpie,$errorstringbars)=(); if (!eval ('require "GD/Graph/lines.pm";')) { $errorstringlines=($@?"Error: $@":"Error: Need Perl module GD::Graph::lines"); } if (!eval ('require "GD/Graph/pie.pm";')) { $errorstringpie=($@?"Error: $@":"Error: Need Perl module GD::Graph::pie"); } if (!eval ('require "GD/Graph/bars.pm";')) { $errorstringbars=($@?"Error: $@":"Error: Need Perl module GD::Graph::bars"); } my $color_user="#FFF0E0"; my $color_commit="#B0A0DD"; # my $color_commit="#9988EE"; my $color_commit2="#C0B0ED"; my $color_file="#AA88BB"; # my $color_file="#AA88BB"; my $color_lines="#E0D8F0"; my $color_lines2="#EFE2FF"; my $color_last="#A8C0A8"; # my $color_last="#88A495"; my $color_lightgrey="#F8F6F8"; my $color_grey="#CDCDCD"; # Made some calculation on commits by user my %nbcommit=(); my %nbfile=(); foreach my $user (sort keys %UserChangeCommit) { foreach my $file (keys %{$UserChangeCommit{$user}}) { $nbcommit{$user}+=$UserChangeCommit{$user}{$file}; $nbfile{$user}++; } } # Made some calculation on state my $TotalFile=0; my %TotalFile=(); my %TotalFileMonth=(); my %TotalFileDay=(); my $TotalCommit=0; my $TotalCommitMonth=0; my $TotalCommitDay=0; my %TotalUser=(); my %TotalUserMonth=(); my %TotalUserDay=(); my $TotalLine=0; my $LastCommitDate=0; my %TotalCommitByState=('added'=>0,'changed'=>0,'removed'=>0); my %TotalCommitMonthByState=('added'=>0,'changed'=>0,'removed'=>0); my %TotalCommitDayByState=('added'=>0,'changed'=>0,'removed'=>0); my %TotalLineByState=('added'=>0,'changed'=>0,'removed'=>0); my %TotalLineMonthByState=('added'=>0,'changed'=>0,'removed'=>0); my %TotalLineDayByState=('added'=>0,'changed'=>0,'removed'=>0); foreach my $dateuser (reverse sort keys %DateUser) { $dateuser=~/(\d\d\d\d)(\d\d)(\d\d)\s+(\S+)/; my ($year,$month,$day,$user)=($1,$2,$3,$4); if ($dateuser > $LastCommitDate) { $LastCommitDate="$year$month$day"; } foreach my $logcomment (sort keys %{$DateUserLog{$dateuser}}) { foreach my $filerevision (sort keys %{$DateUserLogFileRevState{$dateuser}{$logcomment}}) { my ($file,$revision)=split(/\s+/,$filerevision); my $state=$DateUserLogFileRevState{$dateuser}{$logcomment}{$filerevision}; $state =~ s/_forced//; $TotalCommitByState{$state}++; $TotalFile{$file}++; $TotalUser{$user}++; if ($year == $nowyear && $month == $nowmonth) { $TotalCommitMonthByState{$state}++; $TotalFileMonth{$file}++; $TotalUserMonth{$user}++; } if ($year == $nowyear && $month == $nowmonth && $day == $nowday) { $TotalCommitDayByState{$state}++; $TotalFileDay{$file}++; $TotalUserDay{$user}++; } } } } $TotalCommit=$TotalCommitByState{'added'}+$TotalCommitByState{'changed'}+$TotalCommitByState{'removed'}; $TotalCommitMonth=$TotalCommitMonthByState{'added'}+$TotalCommitMonthByState{'changed'}+$TotalCommitMonthByState{'removed'}; $TotalCommitDay=$TotalCommitDayByState{'added'}+$TotalCommitDayByState{'changed'}+$TotalCommitDayByState{'removed'}; $TotalFile=$TotalCommitByState{'added'}-$TotalCommitByState{'removed'}; my @absi=(); my @ordo=(); my %ordonbcommituser=(); my $cumul=0; my %cumulnbcommituser=(); # Made some calculation on commit by date, by user my %yearmonth=(); my %yearmonthusernbcommit=(); my $minyearmonth=''; my $maxyearmonth=''; foreach my $dateuser (sort keys %DateUser) { # By ascending date my ($date,$user)=split(/\s+/,$dateuser); if ($date =~ /^(\d\d\d\d)(\d\d)(\d\d)/) { my ($year,$month,$day)=($1,$2,$3); foreach my $logcomment (sort keys %{$DateUserLog{$dateuser}}) { foreach my $revision (sort keys %{$DateUserLogFileRevState{$dateuser}{$logcomment}}) { my ($add,$del)=split(/\s+/,$DateUserLogFileRevLine{$dateuser}{$logcomment}{$revision}); my $delta=int($add)+int($del); $yearmonthusernbcommit{$user}{"$year$month"}++; $yearmonth{"$year$month"}+=$delta; if ($delta >=0) { $TotalLineByState{'added'}+=$delta; $TotalLineByState{'changed'}+=(int($add)-$delta); } else { $TotalLineByState{'removed'}+=$delta; $TotalLineByState{'changed'}+=(-int($del)+$delta); } if ($year == $nowyear && $month == $nowmonth) { if ($delta >=0) { $TotalLineMonthByState{'added'}+=$delta; $TotalLineMonthByState{'changed'}+=(int($add)-$delta); } else { $TotalLineMonthByState{'removed'}+=$delta; $TotalLineMonthByState{'changed'}+=(-int($del)+$delta); } } if ($year == $nowyear && $month == $nowmonth && $day == $nowday) { if ($delta >=0) { $TotalLineDayByState{'added'}+=$delta; $TotalLineDayByState{'changed'}+=(int($add)-$delta); } else { $TotalLineDayByState{'removed'}+=$delta; $TotalLineDayByState{'changed'}+=(-int($del)+$delta); } } } } if ($TotalLineByState{'removed'}==0) { $TotalLineByState{'removed'}="-0"; } if ($TotalLineMonthByState{'removed'}==0) { $TotalLineMonthByState{'removed'}="-0"; } if ($TotalLineDayByState{'removed'}==0) { $TotalLineDayByState{'removed'}="-0"; } if (! $minyearmonth) { $minyearmonth="$year$month"; } $maxyearmonth="$year$month"; } } $TotalLine=$TotalLineByState{'added'}+$TotalLineByState{'removed'}; # Define absi and ordo and complete holes # We start with cursor = lower YYYYMM my $cursor=$minyearmonth; do { push @absi, substr($cursor,0,4)."-".substr($cursor,4,2); $cumul+=$yearmonth{$cursor}; push @ordo, ($cumul>=0?$cumul:0); # $cumul should not be negative but occurs sometimes when cvs errors foreach my $user (keys %yearmonthusernbcommit) { $cumulnbcommituser{$user}+=$yearmonthusernbcommit{$user}{$cursor}; if ($yearmonthusernbcommit{$user}{$cursor}) { push @{$ordonbcommituser{$user}}, $yearmonthusernbcommit{$user}{$cursor}; } else { push @{$ordonbcommituser{$user}}, 0; } } $cursor=~/(\d\d\d\d)(\d\d)/; # Increase cursor for next month $cursor=sprintf("%04d%02d",(int($1)+(int($2)>=12?1:0)),(int($2)>=12?1:(int($2)+1))); } until ($cursor > $maxyearmonth); writeoutputfile <  EOF # PARAMETERS #----------- writeoutputfile <  
CVS analysis' parameters 
EOF writeoutputfile "\n"; writeoutputfile "\n"; writeoutputfile "\n"; writeoutputfile "\n"; writeoutputfile "
Project module name$Module
CVS root used$CvsRoot
Range analysis$rangestring
Date analysis".FormatDate("$nowyear-$nowmonth-$nowday $nowhour:$nowmin").""; my $endtime=time(); writeoutputfile " (Built in ".($endtime-$nowtime)."s)
"; # LINKS #------ writeoutputfile ""; writeoutputfile "$headstring
"; writeoutputfile 'Summary   ' if (!$NOSUMMARY); writeoutputfile 'Lines of code   ' if (!$NOLINESOFCODE); writeoutputfile 'Developers activity   ' if (!$NODEVELOPERS); writeoutputfile 'Days of week   ' if (!$NODAYSOFWEEK); writeoutputfile 'Hours  ' if (!$NOHOURS); writeoutputfile 'Tags  ' if (!$NOTAGS); writeoutputfile 'Last commits  ' if (!$NOLASTLOGS); writeoutputfile ""; writeoutputfile "
"; # SUMMARY #-------- if (!$NOSUMMARY) { writeoutputfile < 
SummaryTop 
EOF writeoutputfile "\n"; writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; writeoutputfile "\n"; writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; writeoutputfile "\n"; writeoutputfile "\n"; writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; writeoutputfile "\n"; writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; writeoutputfile "\n"; writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; writeoutputfile "\n"; writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; writeoutputfile "\n"; writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; writeoutputfile "\n"; # Last commit my $pos=1; if ($LastCommitDate >= int("$nowyear${nowmonth}01")) { $pos=2; } if ($LastCommitDate >= int("$nowyear$nowmonth$nowday")) { $pos=3; } writeoutputfile "\n"; writeoutputfile <
Current status indicatorsValue  
Files currently in repository ".($TotalFile>0?"$TotalFile":"0")." 
Lines of code currently in repository (on non binary files only) ".($TotalLine>0?"$TotalLine":"0")." 
Activity indicatorsFrom startThis monthToday
Number of developers ".(scalar keys %TotalUser?"".(scalar keys %TotalUser)."":"0")."".(scalar keys %TotalUserMonth?"".(scalar keys %TotalUserMonth)."":"0")."".(scalar keys %TotalUserDay?"".(scalar keys %TotalUserDay)."":"0")."
Number of commits".($TotalCommit?"$TotalCommit":"0")."".($TotalCommitMonth?"$TotalCommitMonth":"0")."".($TotalCommitDay?"$TotalCommitDay":"0")."
Number of commits by status ".($TotalCommitByState{'added'}?"$TotalCommitByState{'added'} to add new file
":"").($TotalCommitByState{'changed'}?"$TotalCommitByState{'changed'} to change existing file
":"").($TotalCommitByState{'removed'}?"$TotalCommitByState{'removed'} to remove file":"")." 
".($TotalCommitMonthByState{'added'}?"$TotalCommitMonthByState{'added'} to add new file
":"").($TotalCommitMonthByState{'changed'}?"$TotalCommitMonthByState{'changed'} to change existing file
":"").($TotalCommitMonthByState{'removed'}?"$TotalCommitMonthByState{'removed'} to remove file":"")." 
".($TotalCommitDayByState{'added'}?"$TotalCommitDayByState{'added'} to add new file
":"").($TotalCommitDayByState{'changed'}?"$TotalCommitDayByState{'changed'} to change existing file
":"").($TotalCommitDayByState{'removed'}?"$TotalCommitDayByState{'removed'} to remove file":"")." 
Different files commited ".(scalar keys %TotalFile?"".(scalar keys %TotalFile)."":"0")."".(scalar keys %TotalFileMonth?"".(scalar keys %TotalFileMonth)."":"0")."".(scalar keys %TotalFileDay?"".(scalar keys %TotalFileDay)."":"0")."
Lines added / modified / removed (on non binary files only) ".(scalar keys %TotalUser?"":"")."+$TotalLineByState{'added'} / $TotalLineByState{'changed'} / $TotalLineByState{'removed'}".(scalar keys %TotalUser?"":"")."".(scalar keys %TotalUserMonth?"":"")."+$TotalLineMonthByState{'added'} / $TotalLineMonthByState{'changed'} / $TotalLineMonthByState{'removed'}".(scalar keys %TotalUserMonth?"":"")."".(scalar keys %TotalUserDay?"":"")."+$TotalLineDayByState{'added'} / $TotalLineDayByState{'changed'} / $TotalLineDayByState{'removed'}".(scalar keys %TotalUserDay?"":"")."
Last commit ".($pos>=1?FormatDate($LastCommitDate):" ")."".($pos>=2?FormatDate($LastCommitDate):" ")."".($pos>=3?FormatDate($LastCommitDate):" ")."

EOF } # LINES OF CODE #-------------- if (!$NOLINESOFCODE) { writeoutputfile < 
Lines of code*Top 
This chart represents the balance between number of lines added and removed in non binary files (source files).
EOF writeoutputfile ""; #writeoutputfile "\n"; writeoutputfile ""; # Build chart if ($errorstringlines) { writeoutputfile ""; } else { my $MAXABS=15; # TODO limit to 10 my $col="#706880"; $col=~s/#//; # Build graph my $pngfile="${OutputRootFile}_codelines.png"; my @data = ([@absi],[@ordo]); my $graph = GD::Graph::lines->new(640, 240); $graph->set( #title => 'Some simple graph', transparent => 1, x_label => 'Month', x_label_position =>0.5, x_label_skip =>6, x_all_ticks =>1, x_long_ticks =>0, x_ticks =>1, y_label => 'Code lines', y_min_value =>0, y_label_skip =>1, y_long_ticks =>1, y_tick_number =>10, #y_number_format => "%06d", boxclr => $color_lightgrey, fgclr => $color_grey, line_types => [1, 2, 3], dclrs => [ map{ sprintf("#%06x",(hex($col)+(hex("050503")*$_))) } (0..($MAXABS-1)) ] #borderclrs => [ qw(blue green pink blue) ], ) or die $graph->error; # # Define legend # $graph->set_legend(("All developers")); # $graph->set_legend_font(""); # $graph->set(legend_placement=>'Right'); my $gd = $graph->plot(\@data) or die $graph->error; open(IMG, ">${OutputDir}${pngfile}") or die "Error building ${OutputDir}${pngfile}: $!"; binmode IMG; print IMG $gd->png; close IMG; # End build graph writeoutputfile ""; } writeoutputfile "\n"; writeoutputfile "
This chart represents the balance between number of lines added and removed in non binary files (source files).
 Perl module GD::Graph::lines must be installed to get charts
 
\n"; writeoutputfile <

EOF } # BY DEVELOPERS #-------------- if (!$NODEVELOPERS) { my $MAXABS=5; writeoutputfile < 
Developers activity*Top 
EOF foreach my $developer (reverse sort { $nbcommit{$a} <=> $nbcommit{$b} } keys %nbcommit) { writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; } # Define another hash limited to $MAXABS my $i=0; my %newnbcommit=(); my $libother="Others (".((scalar keys %nbcommit) - $MAXABS).")"; foreach my $developer (reverse sort { $nbcommit{$a} <=> $nbcommit{$b} } keys %nbcommit) { $i++; if ($i <= $MAXABS) { $newnbcommit{$developer}=$nbcommit{$developer}; } else { $newnbcommit{$libother}+=$nbcommit{$developer}; } } $i=0; my %newnbfile=(); $libother="Others (".((scalar keys %nbfile) - $MAXABS).")"; foreach my $developer (reverse sort { $nbfile{$a} <=> $nbfile{$b} } keys %nbfile) { $i++; if ($i <= $MAXABS) { $newnbfile{$developer}=$nbfile{$developer}; } else { $newnbfile{$libother}+=$nbfile{$developer}; } } if (scalar keys %newnbcommit > 1) { if ($errorstringpie) { writeoutputfile ""; } else { # Build graph for developer commit ratio, hash used: newnbcommit{developer}=nb my $col=$color_commit; $col=~s/#//; my $pngfilenbcommit="${OutputRootFile}_developerscommit.png"; my @data = ([keys %newnbcommit],[values %newnbcommit]); my $graph = GD::Graph::pie->new(170, 138); $graph->set( title => "Number of commits", axislabelclr => qw(black), textclr => $color_commit, transparent => 1, accentclr => $color_grey, dclrs => [ map{ sprintf("#%06x",(hex($col)+(hex("050501")*$_))) } (0..((scalar keys %newnbcommit)-1)) ] ) or die $graph->error; my $gd = $graph->plot(\@data) or die $graph->error; open(IMG, ">${OutputDir}$pngfilenbcommit") or die $!; binmode IMG; print IMG $gd->png; close IMG; # End build graph # Build graph for developer file ratio, hash used: newnbfile{developer}=nb my $pngfilefile="${OutputRootFile}_developersfile.png"; @data = ([keys %newnbfile],[values %newnbfile]); $graph = GD::Graph::pie->new(170, 138); $col=$color_file; $col=~s/#//; $graph->set( title => 'Different files', axislabelclr => qw(black), textclr => $color_file, transparent => 1, accentclr => $color_grey, dclrs => [ map{ sprintf("#%06x",(hex($col)+(hex("050503")*$_))) } (0..((scalar keys %newnbfile)-1)) ] ) or die $graph->error; $gd = $graph->plot(\@data) or die $graph->error; open(IMG, ">${OutputDir}$pngfilefile") or die $!; binmode IMG; print IMG $gd->png; close IMG; # End build graph writeoutputfile "\n"; } } # Number of commits by developer in time $MAXABS=15; # TODO Mettre limit en utilisant newnbcommit au lieu de nbcommit mais necessite pour ca un newordonbcommituser if (scalar keys %nbcommit > 0) { if ($errorstringpie) { writeoutputfile ""; } else { my $TICKSNB=10; my $col=$color_commit; $col=~s/#//; # Build graph for activity by developer my $pngfile="${OutputRootFile}_commitshistorybyuser.png"; my $maxordo=0; my @data = (); my @legend = (); #my @absi = (); push @data, [@absi]; my $numdev=0; foreach my $developer (reverse sort { $nbcommit{$a} <=> $nbcommit{$b} } keys %nbcommit) { my @ordo=(); $numdev++; if ($numdev > $MAXABS) { last; } debug("Add array for developer=$developer",3); foreach my $val (@{$ordonbcommituser{$developer}}) { if ($val > $maxordo) { $maxordo=$val; } } push @data, [@{$ordonbcommituser{$developer}}]; push @legend, ucfirst($developer); } # We level value for maxordo; $maxordo=int($maxordo*1.05+1); my $graph = GD::Graph::lines->new(640+40, 240); $graph->set( #title => 'Some simple graph', transparent => 1, x_label => 'Month', x_label_position =>0.5, x_label_skip =>6, x_all_ticks =>1, x_long_ticks =>0, x_ticks =>1, y_label => 'Number of commits', y_min_value =>0, y_max_value =>$maxordo, y_label_skip =>1, y_long_ticks =>1, y_tick_number=>$TICKSNB, #y_number_format => "%06d", textclr => $color_commit, boxclr => $color_lightgrey, fgclr => $color_grey, # line_types => [1, 2, 3], # dclrs => [ map{ sprintf("#%06x",(hex($col)+(hex("050503")*$_))) } (0..($MAXABS-1)) ] #borderclrs => [ qw(blue green pink blue) ], ) or die $graph->error; # Define legend $graph->set_legend(@legend); $graph->set_legend_font(""); $graph->set(legend_placement=>'Right'); my $gd = $graph->plot(\@data) or die $graph->error; open(IMG, ">${OutputDir}$pngfile") or die $!; binmode IMG; print IMG $gd->png; close IMG; # End build graph writeoutputfile ""; } } writeoutputfile <
DeveloperNumber of commitsDifferent files commitedLines*
(added, modified, removed)
Lines by commit*
(added, modified, removed)
Last commit 
"; writeoutputfile $developer; writeoutputfile ""; writeoutputfile $nbcommit{$developer}; writeoutputfile ""; writeoutputfile $nbfile{$developer}; writeoutputfile ""; writeoutputfile $UserChangeLineAdd{$developer}." / ".$UserChangeLineChange{$developer}." / ".$UserChangeLineDel{$developer}; writeoutputfile ""; writeoutputfile RoundNumber($UserChangeLineAdd{$developer}/$nbcommit{$developer},1)." / ".RoundNumber($UserChangeLineChange{$developer}/$nbcommit{$developer},1)." / ".RoundNumber($UserChangeLineDel{$developer}/$nbcommit{$developer},1); writeoutputfile ""; writeoutputfile FormatDate($UserChangeLast{$developer},'simple'); writeoutputfile " 
Perl module GD::Graph::pie must be installed to get charts

                   
Perl module GD::Graph::pie must be installed to get charts


EOF } # BY DAYS OF WEEK #---------------- if (!$NODAYSOFWEEK) { writeoutputfile < 
Activity by days of weekTop 
EOF if ($errorstringbars) { writeoutputfile ""; } else { my @absi=('Mon','Tue','Wed','Thi','Fri','Sat','Sun'); my @ordo=(); my $cumul=0; # We need to build array values for chart foreach my $dateuser (sort keys %DateUser) { my ($date,$user)=split(/\s+/,$dateuser); my $dayofweek=&DayOfWeek($date); $ordo[$dayofweek-1]+=$DateUser{"$dateuser"}; } # Build graph my $pngfile="${OutputRootFile}_daysofweek.png"; my @data = ([@absi],[@ordo]); my $graph = GD::Graph::bars->new(260, 200); $graph->set( #title => 'Some simple graph', transparent => 1, x_label => 'Days of week', x_label_position =>0.5, x_all_ticks =>1, x_long_ticks =>0, x_ticks =>1, x_number_format => "%02d", y_label => 'Number of commits', y_min_value =>0, y_label_skip =>1, y_long_ticks =>1, y_tick_number =>10, #y_number_format => "%06d", textclr => $color_commit, boxclr => $color_lightgrey, fgclr => $color_grey, dclrs => [ $color_commit ], accentclr => "#444444", #borderclrs => [ qw(blue green pink blue) ], ) or die $graph->error; my $gd = $graph->plot(\@data) or die $graph->error; open(IMG, ">${OutputDir}$pngfile") or die $!; binmode IMG; print IMG $gd->png; close IMG; # End build graph writeoutputfile ""; } writeoutputfile <
Perl module GD::Graph::bars must be installed to get charts


EOF } # BY HOURS #--------- if (!$NOHOURS) { writeoutputfile < 
Activity by hoursTop 
EOF if ($errorstringbars) { writeoutputfile ""; } else { my @absi=(0..23); my @ordo=(); my $cumul=0; # We need to build array values for chart foreach my $houruser (sort keys %HourUser) { my ($hour,$user)=split(/\s+/,$houruser); $ordo[int($hour)]+=$HourUser{"$houruser"}; } # Build graph my $pngfile="${OutputRootFile}_hours.png"; my @data = ([@absi],[@ordo]); my $graph = GD::Graph::bars->new(640, 240); $graph->set( #title => 'Some simple graph', transparent => 1, x_label => 'Hours', x_label_position =>0.5, x_all_ticks =>1, x_long_ticks =>0, x_ticks =>1, x_number_format => "%02d", y_label => 'Number of commits', y_min_value =>0, y_label_skip =>1, y_long_ticks =>1, y_tick_number =>10, #y_number_format => "%06d", textclr => $color_commit, boxclr => $color_lightgrey, fgclr => $color_grey, dclrs => [ $color_commit ], accentclr => "#444444", #borderclrs => [ qw(blue green pink blue) ], ) or die $graph->error; my $gd = $graph->plot(\@data) or die $graph->error; open(IMG, ">${OutputDir}$pngfile") or die $!; binmode IMG; print IMG $gd->png; close IMG; # End build graph writeoutputfile ""; } writeoutputfile <
Perl module GD::Graph::bars must be installed to get charts


EOF } my $widthdate=90; my $widthfulldate=160; my $widthdev=90; my $widthtag=100; # TAGS #----- if (!$NOTAGS) { writeoutputfile < 
Last tags by dateTop 
EOF writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; writeoutputfile "\n"; foreach my $tag (reverse sort { "$tagsfulldate{$a}.$a" cmp "$tagsfulldate{$b}.$b" } keys %tagsfulldate) { writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; writeoutputfile "\n"; } writeoutputfile <
DateFull dateTags 
".FormatDate($tagsshortdate{$tag}).""; writeoutputfile FormatDate($tagsfulldate{$tag},'simple'); writeoutputfile ""; writeoutputfile "$tag"; writeoutputfile " 

EOF } # LASTLOGS #--------- if (!$NOLASTLOGS) { my $cursor=0; writeoutputfile < 
Last commit logsTop 
EOF writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; foreach my $dateuser (reverse sort keys %DateUser) { my ($date,$user)=split(/\s+/,$dateuser); writeoutputfile ""; my $shortdate=$date; writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; writeoutputfile ""; if ($MAXLASTLOG && $cursor >= $MAXLASTLOG) { my $rest="some"; # TODO put here value of not shown commits writeoutputfile ""; last; } } writeoutputfile <
TagsDateDeveloperLast ".($MAXLASTLOG?"$MAXLASTLOG ":"")."Commit Logs
"; if (keys %{$tagstags{$shortdate}}) { foreach my $tag (reverse sort keys %{$tagstags{$shortdate}}) { writeoutputfile "$tag
"; } } else { writeoutputfile " "; } writeoutputfile "
".FormatDate($date)."".$user.""; my (%date_user_commits, @normal_order); foreach my $logcomment (sort keys %{$DateUserLog{$dateuser}}) { my ($one_commit, $one_revision) = ("", ""); $cursor++; my $comment=$logcomment; chomp $comment; $comment =~ s/\r$//; $one_commit .= "

" if ($LOOSECOMMITS); foreach my $logline (split(/\n/,$comment)) { $one_commit .= "".HtmlEntities($logline)."
\n"; } foreach my $filerevision (reverse sort keys %{$DateUserLogFileRevState{$dateuser}{$logcomment}}) { $filerevision=~/(.*)\s([\d\.]+)/; my ($file,$version)=($1,$2); # if ($maxincludedver{"$file"} && (CompareVersionBis($2,$maxincludedver{"$file"}) > 0)) { debug("For file '$file' $2 > maxincludedversion= ".$maxincludedver{"$file"},3); next; } # if ($minexcludedver{"$file"} && (CompareVersionBis($2,$minexcludedver{"$file"}) <= 0)) { debug("For file '$file' $2 <= minexcludedversion= ".$minexcludedver{"$file"},3); next; } $one_revision = $file.$version; if ($maxincludedver{"$file"} && (CompareVersionBis($2,$maxincludedver{"$file"}) > 0)) { debug("For file '$file' $2 > maxincludedversion= ".$maxincludedver{"$file"},3); $date_user_commits{$one_revision} = $one_commit; next; } if ($minexcludedver{"$file"} && (CompareVersionBis($2,$minexcludedver{"$file"}) <= 0)) { debug("For file '$file' $2 <= minexcludedversion= ".$minexcludedver{"$file"},3); $date_user_commits{$one_revision} = $one_commit; next; } my $state=$DateUserLogFileRevState{$dateuser}{$logcomment}{$filerevision}; $state =~ s/_forced//; $one_commit .= "* ".FormatCvsFileLink(ExcludeRepositoryFromPath($file,0,1),$version)." $version (".FormatState($state); my $lines=$DateUserLogFileRevLine{$dateuser}{$logcomment}{$filerevision}; $one_commit .= ($state eq 'added' && $lines?" $lines":""); $one_commit .= ($state eq 'changed' && $lines?" $lines":""); $one_commit .= ($state eq 'removed' && $lines?" $lines":""); if ($state eq 'changed' && $DateUserLogFileRevLine{$dateuser}{$logcomment}{$filerevision} !~ /binary/) { if ($ViewCvsUrl) { $one_commit .= ", ".FormatCvsDiffLink(ExcludeRepositoryFromPath($file,0,1),$version); } if ($INCLUDEDIFF && CompareVersionBis($version,1.1) > 0) { my $relfile=ExcludeRepositoryFromPath($file,0,1); my $command="$CVSCLIENT $COMP -d ".$ENV{"CVSROOT"}. " diff -u -r ".DecreaseVersion($version)." -r $version $relfile"; my $result=`$command 2>&1`; $one_commit .= " Show Changes"; $one_commit .= "

Hide Changes
"; foreach my $line (split /\n/,$result) { if ($line =~ /^Index:/) { next; } if ($line =~ /^======/) { next; } if ($line =~ /^RCS file:/) { next; } if ($line =~ /^retrieving revision/) { next; } if ($line =~ /^diff/) { next; } if ($line =~ /^---/) { next; } if ($line =~ /^\+\+\+/) { next; } my $type = substr($line,0,1); if ($type eq '+') { $one_commit .= ""; } elsif ($type eq '-') { $one_commit .= ""; } $one_commit .= escapeHTML($line) . "\n"; if ($type eq '+' || $type eq '-') { $one_commit .= ""; } } $one_commit .= "
"; } } $one_commit .= ")
\n"; } $one_commit .= "

" if ($LOOSECOMMITS); $date_user_commits{$one_revision} = $one_commit; push @normal_order, $one_revision; if ($MAXLASTLOG && $cursor >= $MAXLASTLOG) { last; } } # sort by file-revision if ($SORTBYREVISION) { foreach my $commitline (reverse sort keys %date_user_commits) { writeoutputfile $date_user_commits{$commitline}; } } else { foreach my $commitline (@normal_order) { writeoutputfile $date_user_commits{$commitline}; } } writeoutputfile "
Other commits are hidden...

EOF } } # End buildhtmlreport # Footer if ($Output =~ /buildhtmlreport$/) { writeoutputfile ""; writeoutputfile "
* on non binary files only
Created by $PROG $VERSION
"; writeoutputfile "
\n"; writeoutputfile "\n\n"; } # Start of true output if ($OutputDir) { close FILE; } if (! $KeepRlogFile) { writeoutput("Remove temporary rlog file\n",1); unlink "$RLogFile"; } print STDERR ucfirst($PROG)." finished successfully.\n"; 0; cvschangelogbuilder-2.4/docs/0000755000175300010010000000000010366760140015563 5ustar LaurentAucuncvschangelogbuilder-2.4/docs/pad_cvschangelogbuilder.htm0000444000175300010010000001460211025724550023132 0ustar LaurentAucun<table border="0" bgcolor="#e0e0e0" > <tr> <td> <table border="0" bgcolor="#ffffe1" > <tr valign="top" > <td rowspan="2" valign="top" align="center" > <img src="http://cvschangelogb.sourceforge.net/docs/images/cvschangelogbuilder.gif"> </td> <td > <table border="0" width="100%" bgcolor="#FFFFF0"> <tr> <td colspan="2" valign="top"> <b><a href="http://cvschangelogb.sourceforge.net">CVSChangeLogBuilder</a> 2.4</b><br> </td> </tr> <tr> <td width="50%"> <font size="2"> Release Date: 06/17/2008<br> Release Status: <b>Minor Update</b> </font> </td> <td width="50%"> <font size="2"> Type: Freeware<br> Cost: $US </font> </td> </tr> </table> <p><b>Description:</b><br> <font size="2"> cvschangelogbuilder is an Perl utility to generate ChangeLogs for a project hosted on a CVS server. The main goal is to provide a better output that the 'cvs log' command, to permit to choose the sorting criteria, to add the status of the change (added,removed,modified) and to allow &quot;differential&quot; changelogs, between 2 versions, even when they are not in the main branch.<br> <a href="http://cvschangelogb.sourceforge.net/docs/images/screen_shot_1.jpg">Screen Shot</a> </font> <p><b>Keywords:</b><br> <font size="2"> cvschangelogbuilder </font> <hr width=100%> <table border="0" width=100%> <tr> <td colspan=2> <b>Contact Info:</b> </td> </tr> <tr valign="top"> <td> <font size="2"> <a href="http://www.destailleur.fr">Laurent Destailleur</a><br> 61 Boulevard Vauban<br><br> Montigny-le-Bretonneux, Yvelines 78180<br> FRANCE<br> </font> </td> <td> <font size="2"> <a href="http://www.asp-shareware.org">ASP Member</a>= </font> </td> </tr> <tr valign=top> <td> <font size="2"> General Phone: <br> General Email: <a href="mailto:eldy@users.sourceforge.net">eldy@users.sourceforge.net</a> </font> </td> <td> <font size="2"> Sales Phone: <br> Sales Email: <a href="mailto:eldy@users.sourceforge.net">eldy@users.sourceforge.net</a> </font> </tr> <tr valign=top> <td> <font size="2"> Support Phone: <br> Support Email: <a href="mailto:eldy@users.sourceforge.net">eldy@users.sourceforge.net</a> </font> </td> <td> <font size="2"> Order Online: <a href="http://cvschangelogb.sourceforge.net">http://cvschangelogb.sourceforge.net</a> </font> </tr> </table> <hr width=100%> <table border="0" > <tr> <td><b>Download URLs:</b></td> </tr> <tr> <td> <font size="2"> File Size: 125kB / 0.12MB<br> <a href="http://cvschangelogb.sourceforge.net/files/cvschangelogbuilder.tar.gz">http://cvschangelogb.sourceforge.net/files/cvschangelogbuilder.tar.gz</a><br> <a href="http://downloads.sourceforge.net/cvschangelogbuilder/cvschangelogbuilder-2.4.tar.gz">http://downloads.sourceforge.net/cvschangelogbuilder/cvschangelogbuilder-2.4.tar.gz</a><br> <a href=""></a><br> <a href=""></a><br> </font> </td> </tr> </table> </td> </tr> </table> <font size="2"> <b>Supported Operating Systems:</b> Win95,Win98,WinME,WinNT 4.x,Windows2000,WinXP,Unix,Linux,OS/2,OS/2 Warp,OS/2 Warp 4,MAC 68k,Mac PPC<br> <b>System Requirements:</b> CVS<br> <b>Install Support:</b> No Install Support </font> </td> </tr> </table> <hr> cvschangelogbuilder-2.4/docs/pad_cvschangelogbuilder.xml0000444000175300010010000001430111025724550023136 0ustar LaurentAucun 3.01 PADGen 3.0.1.37 http://www.padgen.org Portable Application Description, or PAD for short, is a data set that is used by shareware authors to disseminate information to anyone interested in their software products. To find out more go to http://www.asp-shareware.org/pad Laurent Destailleur 61 Boulevard Vauban Montigny-le-Bretonneux Yvelines 78180 FRANCE http://www.destailleur.fr Laurent Destailleur eldy@users.sourceforge.net Laurent Destailleur eldy@users.sourceforge.net eldy@users.sourceforge.net eldy@users.sourceforge.net eldy@users.sourceforge.net CVSChangeLogBuilder 2.4 06 17 2008 Freeware Minor Update No Install Support Win95,Win98,WinME,WinNT 4.x,Windows2000,WinXP,Unix,Linux,OS/2,OS/2 Warp,OS/2 Warp 4,MAC 68k,Mac PPC English Development Tools Development::Other CVS 128271 125 0.12 N Days cvschangelogbuilder cvschangelogbuilder - Build CVS ChangeLog cvschangelogbuilder - A friendly CVS ChangeLog builder (GPL) cvschangelogbuilder - A friendly CVS ChangeLog builder (GPL) cvschangelogbuilder is an Perl utility to generate ChangeLogs for a project hosted on a CVS server. The main goal is to provide a better output that the 'cvs log' command, to permit to choose the sorting criteria, to add the status of the change (added,removed,modified) and to allow "differential" changelogs, between 2 versions, even when they are not in the main branch. cvschangelogbuilder is an Perl utility to generate ChangeLogs for a project hosted on a CVS server. The main goal is to provide a better output that the 'cvs log' command, to permit to choose the sorting criteria, to add the status of the change (added,removed,modified) and to allow "differential" changelogs, between 2 versions, even when they are not in the main branch. The output are also friendly to read and records are grouped by the output criteria (by date, by file, by log). So you can, for example, know which files were modified between two old versions to prepare a light diff package for an old customer, you can see what was done on current development version, sorted by date, to know what happened during your 5 days holidays, you can build a changelog ready to be included in your rpm %changelog section with correct format, see how many commits were done on a particular file with no need scan the 'cvs rlog' output, etc... cvschangelogbuilder is also an easy to use command line interface (perl tool). cvschangelogbuilder offer 4 kinds of output: listdeltaforrpm To build a changelog to include in a rpm .spec file. listdeltabydate To build a changelog by date (looks near 'cvs log'). listdeltabyfile To build a changelog by file. listdeltabylog To build a changelog by change comment. http://cvschangelogb.sourceforge.net http://cvschangelogb.sourceforge.net http://cvschangelogb.sourceforge.net/docs/images/screen_shot_1.jpg http://cvschangelogb.sourceforge.net/docs/images/cvschangelogbuilder.gif http://cvschangelogb.sourceforge.net/docs/pad_cvschangelogbuilder.xml http://cvschangelogb.sourceforge.net/files/cvschangelogbuilder.tar.gz http://downloads.sourceforge.net/cvschangelogbuilder/cvschangelogbuilder-2.4.tar.gz GNU GPL GNU GPL cvschangelogbuilder-2.4/docs/styles.css0000644000175300010010000000233510240230226017607 0ustar LaurentAucunbody { background-color: #FFFFFF; font: 14px verdana,arial; font-family: sans-serif; margin-top: 4; margin-bottom: 4; margin-right: 4; margin-left: 4; } a:link { font: 14px verdana,arial; color: #2200C0; font-family: sans-serif; text-decoration: none; } a:visited { font: 14px verdana,arial; color: #2200C0; font-family: sans-serif; text-decoration: none; } a:active { font: 14px verdana,arial; color: #2200C0; font-family: sans-serif; text-decoration: none; } a:hover { font: 14px verdana,arial; color: #2200C0; font-family: sans-serif; text-decoration: underline; } .CHead { background-color: #FFE0B0; } .CTextAreaConf { font: 11px verdana,arial; color: #202020; font-family: sans-serif; text-decoration: none; } td.CFAQ { font: 14px verdana,arial; color: #000000; font-family: sans-serif; text-decoration: none; } .CProblem { font: 14px verdana,arial; color: #660000; font-family: sans-serif; text-decoration: none; } .CSolution { font: 14px verdana,arial; color: #448866; font-family: sans-serif; text-decoration: none; } input { font-family: arial,verdana,helvetica, sans-serif; font-weight: normal; border: 1px solid #DDC8C8; background-position : bottom; font: 12px verdana,arial; } cvschangelogbuilder-2.4/docs/images/0000755000175300010010000000000010366760142017032 5ustar LaurentAucuncvschangelogbuilder-2.4/docs/images/screen_shot_1.png0000644000175300010010000001257210234260104022266 0ustar LaurentAucunPNG  IHDRdK z.tEXtCreation Timeven. 29 avr. 2005 01:13:35 +0100CtIME pHYs  ~gAMA aIDATx\oGz"9<[V>K뵓u/ A/Xl<% O%@!lcm8k[-0E19z.r4f(r(i~$U_U}郞no~G[SɤUϤ bb޵ >eK]MRRql i+wY%G\6{ޚy\X(Tk5˲(Dad;dh6x"ht0d~I|B^iZ$g;͖^* l4 X-HgZ6E$mcJچ)ڶk mK4>b_ RAi+3. {AD(Lh3LpM eŰBWN@%Vh &t.O@ͦvoY-4WW*E`|B9fp;ON!%ᮝ"bif5]n .x^6)&>6?&(; s/[2߼T Tw;X|~a~fqJm݄b־Z@70ҋ?8z9*˪hs|gg0V嘐HP)4?ɠ!g 2;eҳ-tivۀvf#/D^$79$L5=@0 Am`'ȉaRMvJ*׉|͋ q~@ʸO@b&\aުZB Y>36Y-(Ȋ]qEoQF:ϫJ/q7LtU`pu*QwwMxA!hs\“J[*a~ga)RrkNWNϮjCI {PO`Ylwt[_}_6OR#W)RB'ʲ(&=cQ2lB aX:{m9Odf_weIUқ^DO'K$m $βحܭ]Ýl'qOLa&4mvz۟CpPGs1 Fw'; T?n}υ8fE eq=~@R= $ o.E3iNʙ""'-܉X,ܺ~^#᝭CQDcAH&&n?R9KWOnN~nQxl#M_W!: R 6|QA0M3!0ojj {w4$* c+DX,"dcBS/Ob>Gu8ܼyKVECYDV.)Yۚ(|[.H ַT!8_b}!P\/D.:B$|Ąl>2 i e<ͩ\~Χp.fx{6bA CMw\O4U. \Ko7-Q?@tnqNiz+͖ [Z+>7j4Ԯ㸍fkT<n8\2xX|2U_۪&)4RbZȜ!*ٽ|Nu:C@zk@L]+ze\"iEwf ""kHUoEta{ׄZi1?c[[PIP@2lYÃJ~ﻱ&I+1ak{!Hݣ@d $41ā8zuݭG9ODLpǽ3{^v~?awEs]p_6lu\+4-6땚 5(&$ xbPyng_7 ģ/(1 Ø-m=AL(ޡ4Ot UhoY*L3B{ )C afNUV:} @AV.+ L2$DŽPx"DK Y:2%".!bԅCnBYXX8YGY(xFb`}Xo瑾r Z!L9L`E= K5>~2|̘g@an$Yf[nm8===<5QO0 MfLv7i9N 筣=T$ݲ >CXP)WUBwBE AszayAp}𥥥K.zү׏0v[ے< 5%t& q044hRIƄŁXM%i޾}ja1MV֚>PM0̣+„,..RPS1j*Ht̉W)ԘիW~Θ]ߣ S1;;{n$CgamU ^GjaQ9+@PK] 5/rae)yPt{.؞rA+BQǡ&.9xԃ{$,L<{3ڥh( "%;-\-t GMuw:9#?"jlV<# YC>쥗^}]GCM?j ;9TF=%\سfDB9%aRsC6'^,Krjdy#x(d_{=Ga۵iqbviuk~zV I%`wmVѩ F&@6-9?LD!x32+.Lf-s3hͰgH_"4tv;$6r*&8Jp_jq &`p CKM5,|+Lw…8laS?ƈ/Ð-{#83=È4?txtt =mubC",ը3EB# Q C6Z !MOT#n} M ô (L[XSs<  *8r4~j_T8GMOWX:pdDO>ޜLFѭhpa%uas{â >GА;gN>cb̙SņϞޚF}_d ذŐ]F6#؎3LG2!GN3DCw c +l%GƐ侀Ğ \7ENpE}4BAmBgoK[kc07U;[n)Z$V}UOcRWW?UU;1J7p09dCg:o)FaѱI`|1ևݍDXrC`i|om썖[$Hl FV)Uo[0S_8{fؒKs/n_T糠1]=c-Lz|I?:["_8$zxၝ\1A$֦1h2[P/|FRmn2#×|l"n-+o'ٮø~63S$k+lh8[at&+/ۭݶq~f3|W$ h<~QdI ɉoUl5}t܄Vdqm }ITL=ZeGwmoHRDY-dKQh9fJKɐw"Wj{ZZ]}#ɉx/w:uI66ڏWoS߫EQxmkuqs\d7QS'{l~'gĥ/swYz2{L]aՕJ9&`LTql  }PB煲v}wTYT2^panlng_87ϷMC%^gOՖdEucyWο͏W5&#?MueVw}.2Sg_/f\|/ܗ~x~ϯmJPUlVۏG/#@H\ijݼٷ)/2?XH\|'[hdYݫ6ԙϬ 3㋟]fly!(z}Yg31&ӣ)L&0ob.\7)U}sq~i8$p:EJ&҆De"eWّ"V}l4:|*s MbZ‹/OLq(V.$d[[vN?~0{1;طo~7#[} LNb:_6~O? l¿< ӗ <""}/&m{y9v`pprqqE-%zIENDB`cvschangelogbuilder-2.4/docs/images/cvschangelogbuilder.gif0000644000175300010010000000216010671560276023537 0ustar LaurentAucunGIF87a JR{JRJZRR{RRRZ{RZRZRZRZRZRcZZsZZZcZcZcZcZcZcZkZkZkZkcZZccscc{ccccccckckckcskZZkckkkkkkkkksk{sss{s{{cR{kR{kc{s{{{{{{{{΄{ƄƄ֌{Όތޔ{{kΜΜޥkkέε{{εƵε޽{ֽֽ޽ƥƭƭs{΄ΌΔΔέέεεεք֌ֵֽ֥֔֜֜ޜޥޭޭ޵޵޽޽ƵƜΥ!, H*\ȰÇ#JHňgCM (SLjYHa=xهMF- ʩgMN%h(2ȕ8~:0ˀ/tG?ɉ&FwQC $]ęD IH"\ GRBf± (7&-biß pD8vׯ x$yrC A^G̖[(K:#+E4)R$IX B4ɨNJ [i)ĭT'IOGdž @4Z\=ٜ`>Ul޻RwfRJ>nL*y-S)e7GipҾE %-4Tl.uZclX]S*=B#\n)4V@Z:T:C!vs9RAʟ?BT~X)UsBIOikZfbK 2vyZWaͧ!T͆`[P'օ|5e0b}MWbl7W$bH_EQXaG ^]AiXArcZJ5$R2#|JfYC@tzn|{^i'[/&4% (aq&&L&ޙWݢ@jIj}9 h jWI:߆]%VUn 8p j'g՘ )MIX2ldIMUw8IfIUTuVʲz:V'{}r_W{%뼮ʯb @s(E^=Ә 1A#,qE<;`Ѐ8!V$"`1K!A#g2$4r/ bă@8b#Ltd(e-!EL MC#t@@sk 7v$*H R-2<.S`Ad\  0rP-@ tJ P25~D@ܮPPADA # B10P @C 0Aw{A]=#2!^П@H0>   0D~@Չ B m@@:7O g0J A` q䀁9,t@;X upY0:AsK'tA ^@0Pax020o H̀lM [ PMAH5LûXC D0pM YJLTB&b* l.w5䀂, x> I 1 ˇH ([AB A y&7@K`B0 BЁ@PB@"O @ ``$BE  en j3qs<J 5յR6@:)ėBl.U5 ,'>Ý`2q0fB @@ L2$P PG:8@0 w^׾k@;cvschangelogbuilder-2.4/docs/images/cvschangelogbuilder_logo1.png0000644000175300010010000000427211005127320024642 0ustar LaurentAucunPNG  IHDRnSIaºtIME:% pHYsxx˫gAMA aPLTE}گԬ׮﹔xܰƦĥɧåگ|u~󻎖⳹Ǧخȧѫ}Ϫݰr|뷡ݱ۰ť{̨oyΩ߱ҫ˨Ы̩鶵ޱᲵխ֭*=<3Ai%:REtRNS@f0IDATx헉[G_$\)ZVrKTATPڪJaݝLlص_?R@v5uu%2f/)KK/yp fDityRF2{O^J.Is&}x;Um֎B4qrw7[eaPsk`Zt&آ?mk0<=ZwǍh\spc MSŖah~}[`fP]xsƟ.gSd7ܼQ+Y5B!~ ^5WSSpJ*r;Auw~^k~I%ڭ /+aZ3Ob=+ywE_/~Z 6c^h'0?~>yw`Nlj&q%OX ݛ17Z`]#uQOQPC G:㋗gD12#_Axrr 7j&Yq@NT?zU\x%)#i-]q(>W JXWZOo[h?{ӷtXVv[DVfmt=ZC90r%þ~IKZoƼaG}+6`sb71. *Cnpg1>N`ݪ5Suk.jw(X9yji`eUI )ɢlIENDB`cvschangelogbuilder-2.4/docs/images/screen_shot_1.jpg0000644000175300010010000000502011005127320022247 0ustar LaurentAucunJFIFGGExifMM*C  !"$"$CKd"E !1A"aq#2Q34T$%BRSUCbcr+Qa!"1AR#2bq ?EY]TuW~>ȕf\Q1c}YENZ7;=VIѐIo2.83y: 2Ffˇ FhLG l'o/N(iޘHbyxbLYFf겸`E1o\$eK3=V]a)"qoΥ>rqITR()2SI5Oذ9*[1jj2FiD:6*/cW *e`/{k &YDfEˣ@߈6<`B>9?7m\5.o*2k~̃{APF.vu*}O*}O6rf _MBJ[װ<1l2Z@\Ɖn-u2:OS0r-|{4Rl5z0;lď1I=\U2)ج,H;1c݋)P)y($˞*Lo*ΫZ̃p-{r򾝕BYQ<ō3YB0fb,V7>f, 1~IkW}9U׼faՖV)$_p,FDžqcg*W}~! c*`vRJn[^l{ *7ç`e`-ߏbBqOsBYt/CҼ" pfi'ㅉ$7.7I'Ohpnn_Z'<1&旞WML#,m58:LwvpڤΤBV mgq*d5˺ xj|F+Jw^Uȫ~_i7ᷣo߉oZuopLދ=g{]ߑm9Ӆ|Ckv gOUoWh>ID:e| IZz¡cp~/B^BI?;1lX IKs͹|d})Ο+2ftqԬBjBζn=2 TvkjMM.RmlVRH 1 CVSChangelogBuiler logfile analyzer Documentation
See help by running cvschangelogbuilder.pl from command line.


About my other Open-Source projects:
AWBot, an easy to use Perl tool to test a web site.
AWStats, an Advanced statistics log analyzer (web, ftp and mail).
CVSChangelogBuilder, this one.
Dolibarr, a powerfull ERC/CRM for small and medium companies or fundations.

About me and my other Web sites:
Laurent Destailleur (Eldy)
www.ChiensDeRace.com
www.ChatsDeRace.com
www.LesBonnesAnnonces.com
www.PourMaPlanete.com

cvschangelogbuilder-2.4/docs/cvschangelogbuilder_changelog.txt0000644000175300010010000000376611003345042024346 0ustar LaurentAucunCVSChangelogBuilder Changelog ----------------------------- $Revision: 1.12 $ - $Author: eldy $ - $Date: 2008/04/22 11:21:05 $ 2.4 - Fix: Link on php files does not works. - Fix: Exclude files if they are result of add in another branch. - New: Add option -allowindex to have meta robot index tag in HTML output. - New: Add option -includeheader to include content of a file after body tag in HTML output. 2.3 - Fix: The user is not case sensitive. - Fix: cvschangelogbuilder died for some CVS root in Windows. - Fix: Support module that contains spaces on it. - Fix: [ 1281761 ] diff link error. - New: Added -only option (for report on only files that match a regex). - More precise error message when writing png files. - Updated documentation. 2.2 - Fixed: Remove warning in diagnostic mode - Fixed: doc to use buildhtmlreport options - Fixed: [ 1004884 ] HTML tags is removed from HTML output - New: options: nosummary, nolinesofcode, nodevelopers, nodaysofweek, nohours, notags, nolastlogs, nolimit,sortbyrevision, loosecommits - New: cvs links open a new window. - Fixed: Support filanmes with spaces in it 2.1 - Fix: A better handling of corrupted rlog file. - Fix: Minor fixes. - Fix: Cache for files with space in name works correctly. - New: Added option -ignore and -only - New: Chart for commit and files by developpers are limited to 5 most active users and group others as "Others". - New: Added a date tag reports - Added tags informations in last commit list 2.0 - New: Added an option to build an HTML report of CVS activity: By authors, list commit logs, ... - New: Autodetect value for module name and repository if in a repository dir. - New: Added activity by days of week - New: Added activity by hours - New: Added pie chart for activity by developers. - Updated logo. 1.0 - First released version KNOWN BUGS - TODO Fix several reported "removed", keep only last entry as 'removed', others should be 'changed', like do the date report.cvschangelogbuilder-2.4/docs/COPYING.TXT0000644000175300010010000004365711005127320017277 0ustar LaurentAucun GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 19yy This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) 19yy name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. cvschangelogbuilder-2.4/docs/cvschangelogbuilder_faq.html0000644000175300010010000000677611005127320023316 0ustar LaurentAucun CvsChangeLogBuilder Official Web Site - Build friendly and accurate changelogs from CVS server.

CvsChangeLogBuilder FAQ
Build friendly, accurate, text or graphical changelogs for your CVS projects.



Frequently Asked Questions + Troubleshooting


ABOUT QUESTIONS:

COMMON SETUP/USAGE QUESTIONS:
Here, you can find the most common questions and answers about AWStats setup/usage process.

ERRORS/TROUBLESHOOTING QUESTIONS:
Here, you can find the most common questions and answers about errors or problems using AWStats.





FAQ-ABO300 : ABOUT CVSCHANGELOGBUILDERS HISTORY
CVSChangelogbuilder was designed because there was no exisisting and accurate tools to make reports of activity of a project managed by CVS.
I need such a tool to follow all works on my personal projects (www.chiensderace.com, www.chatsderace.com, www.lesbonnesannonces.com, www.pourmaplanete.com, www.dolibarr.org, and www.destailleur.fr)
Then in 2004, I decided to make a work to make my tool more easy to use and to distribute it as a GPL OpenSource software. There is not a lot of activity on this projet but the reason is simply because CVSchangelogBuilder is now mature and provide all informations a project leader need and can extract from a CVS server.
You can have a look at changelog file for full history of changes.