#!/usr/local/bin/perl #------------------------------------------------------- # Build a changelog file for a CVS project # Serveral output format are available #------------------------------------------------------- # $Revision: 1.79 $ - $Author: eldy $ - $Date: 2006/11/17 01:12:42 $ use strict; no strict "refs"; use Time::Local; use CGI qw/:standard/; #use diagnostics; #------------------------------------------------------- # Defines #------------------------------------------------------- my $REVISION='$Revision: 1.79 $'; $REVISION =~ /\s(.*)\s/; $REVISION=$1; my $VERSION="2.3 (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 $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"); # 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.="?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; } ($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 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(" -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 $Module\n"; writeoutputfile < EOF writeoutputfile "\n"; writeoutputfile "\n"; } # 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;