#!/bin/sh # Start Tcl from a cluster bin directory \ exec /cluster/bin/tcl/`uname -m`/bin/tclsh8.4 $0 ${1+"$@"} ## # Program to generate reports from CVS log. ## package require Tclx package require Itcl ## # FIXME: # - indicate deleted files in index. # - be able to just specify the branch tag and not need branch point tag ## # # Class that contains the information about a file obtained from a cvs log # command. # ::itcl::class CVSFileInfo { # Serialization version number. private common SERIALIZE_VERSION 1 # Fields to serialize private common SERIALIZE_FIELDS { fRcsFile fWorkingFile fHead fBranch fKeywordSubst fBranchTable fTags fTagTable fRevisions } # Separator for file records private common FILE_REC_SEP "=============================================================================" # Separator for revision records private common REVISION_REC_SEP "----------------------------" # RCS file name (full repository path) private variable fRcsFile # Work file name (relative to current work directory) private variable fWorkingFile # Head and branch private variable fHead private variable fBranch private variable fBranchMagic {} # Keyword substitution private variable fKeywordSubst # Branch table, indexed by revision, with a list of branch revisions. # Doesn't contain unbranched revisions. private variable fBranchTable # Ordered list of tag names private variable fTags {} # Table of tags containing revision private variable fTagTable # Ordered list of revisions private variable fRevisions {} # Table of revisions information in the form: # {rev dateTime author state comment} # dateTime is not parsed private variable fRevisionTable # Unexpect EOF error private proc unexpectedEOFError {} { error "Unexpect EOF parsing CVS log" {} {CVS CVS_LOG_UNEXPECTED_EOF} } # Parse error private proc parseError {msg} { error "CVS log parse error: $msg" {} {CVS CVS_LOG_PARSE} } # Read a line from the CVS log that must not return EOF. private proc readCvsLog {chan} { if {[gets $chan line] < 0} { unexpectedEOFError } return $line } # Determine if line is one of the record end lines. private proc isRecordEnd {line} { return [expr {[cequal $line $FILE_REC_SEP] || [cequal $line $REVISION_REC_SEP]}] } # Parse the cvs log header. `RCS file:' has already been read. private method parseCvsLogHeader {chan} { set parsingTags 0 set inDescription 0 while {1} { set line [readCvsLog $chan] if {[isRecordEnd $line]} { break } if {[string match "\t*" $line]} { # Process symbolic names line. if {!$parsingTags} { parseError "unexpected symbolic names line: `$line'" } if {![regexp {^\t([^:]+): (.*)$} $line {} name value]} { parseError "can't parse symbolic names line: `$line'" } lappend fTags $name set fTagTable($name) $value } elseif {!$inDescription} { if {![regexp {^([^:]+): *(.*)$} $line {} name value]} { parseError "can't parse line: `$line'" } set parsingTags 0 switch -- $name { "RCS file" { parseError "`RCS file:' should have already been parsed" } "Working file" { set fWorkingFile $value } "head" { set fHead $value } "branch" { regsub -all {;} $value {} value set fBranch $value } "symbolic names" { set parsingTags 1 } "keyword substitution" { set fKeywordSubst $value } "description" { set inDescription 1 } } } } } # Parse a commit record. Return 1 if there are more to read, # zero if its the last. private method parseCvsLogCommitRec {chan} { set line [readCvsLog $chan] # Revision could have a `locked by:' (which is ignored) if {![regexp {^revision ([^;]+)} $line {} revision]} { parseError "invalid `revision' line, got `$line'" } set line [readCvsLog $chan] if {![regexp {^date: ([^;]+); author: ([^;]+); state: ([^;]+);} $line {} dateStr author state]} { parseError "invalid `date:' line, got `$line'" } # branches record is optional set line [readCvsLog $chan] if {[regexp {^branches: (.+;)} $line {} branchList]} { foreach branch $branchList { regsub -all {;| } $branch {} branch if {![lempty $branch]} { lappend fBranchTable($revision) $branch } } set line [readCvsLog $chan] } # Rest is comment set comment {} while {1} { if {[isRecordEnd $line]} { break } append comment $line \n set line [readCvsLog $chan] } if {[cequal $comment "*** empty log message ***\n"]} { set comment {} } # Got it all lappend fRevisions $revision set fRevisionTable($revision) [list $revision $dateStr $author $state $comment] return [cequal $line $REVISION_REC_SEP] } # Parse the output of cvs log. private method parseCvsLog {chan {modulePath {}}} { parseCvsLogHeader $chan while {[parseCvsLogCommitRec $chan]} { continue } if {![info exists fWorkingFile]} { if {[lempty $modulePath]} { error "didn't parse work file and don't have module path: $fRcsFile" } if {[string first $modulePath $fRcsFile] != 0} { error "rcs file \"$fRcsFile\" doesn't start with module path \"$modulePath\"" } set fWorkingFile [string range $fRcsFile [expr [string length $modulePath]+1] end] regsub {,v$} $fWorkingFile {} fWorkingFile } } # Parse a cvs log record and create an object for it. # Module path is needed to computer work directory if doing a rlog # Return {} on EOF. public proc readCvsLogRec {chan {modulePath {}}} { # Skip blank and `?" lines while {([gets $chan line] >= 0) \ && ([lempty $line] || [string match \\?* $line])} { continue } if {[eof $chan]} { return {} } if {![regexp {^RCS file: (.*)$} $line {} value]} { parseError "Expected `RCS file:' line, got `$line'" } set obj [CVSFileInfo "::#auto" $value] try_eval { $obj parseCvsLog $chan $modulePath } { $obj delete error $errorResult $errorInfo $errorCode } return $obj } # Constructor public constructor {rcsFileName} { set fRcsFile $rcsFileName } { } # Delete self public method delete {} { ::itcl::delete object $this } # Generate a command to recreate this object public method serialize {} { error "not implemented" set serializeVersion SERIALIZE_VERSION foreach var [concat serializeVersion $SERIALIZE_FIELDS] { Serialize::serializeVar } } # Load serialized object. Cmd is the result of serialize public proc deserialize {cmd} { error "not implemented" } # Compare revisions public method compareRevs {r1 r2} { set r1s [split $r1 .] set r2s [split $r2 .] set r1l [llength $r1s] set r2l [llength $r2s] set i 0 while {1} { if {($i >= $r1l) && ($i >= $r2l)} { return 0 } if {$i >= $r1l} { return -1 } if {$i >= $r2l} { return 1 } set n1 [lindex $r1s $i] set n2 [lindex $r2s $i] if {$n1 > $n2} { return 1 } if {$n1 < $n2} { return -1 } incr i 1 } } # Get the RCS file public method getRCSFile {} { return $fRcsFile } # Get the working file public method getWorkingFile {} { return $fWorkingFile } # Get the head public method getHead {} { return $fHead } # Get the branch, is in the form 1.37.20, not magic branch 1.37.0.20 public method getBranch {} { return $fBranch } # Get the branch in magic number format (1.37.0.20) public method getBranchMagic {} { if {$fBranchMagic == {}} { set p [split $fBranch .] set fBranchMagic [join [linsert $p end-1 0] .] } return $fBranchMagic } # Test if a revSpec is a number or a tag private method isRevNumber {revSpec} { return [ctype digit [cindex $revSpec 0]] } # Lookup a tag. If it doesn't exit, generate an error if check is 1 # of return {} if check is 0. The tag HEAD is supported. private method lookupTag {tag {check 1}} { if {$tag == "HEAD"} { return [getHead] } elseif {[info exists fTagTable($tag)]} { return $fTagTable($tag) } elseif {$check} { error "tag \"$tag\" does not exist" } else { return {} } } # Test if a tag or a revision is a magic branch public method isBranchMagic {revSpec} { if {![isRevNumber $revSpec]} { set revSpec [lookupTag $revSpec] } return [expr [lindex [split $revSpec .] end-1] == 0] } # Translate a magic branch number to an actual revision number # If not a magic branch, return unchanged. public method translateBranchMagic {rev} { if {[isBranchMagic $rev]} { set revs [split $rev .] return [join [concat [lrange $revs 0 end-2] [lindex $revs end]] .] } else { return $rev } } # Deterimine if a revision or tag is a branch (x.x.x) public method isBranch {revSpec} { if {![isRevNumber $revSpec]} { set revSpec [lookupTag $revSpec] } return [expr {[llength [split $revSpec .]] & 1}] } # Deterimine if a revision or tag is a branch (x.x.x) or branch magic (x.x.0.x) public method isBranchOrMagic {revSpec} { if {![isRevNumber $revSpec]} { set revSpec [lookupTag $revSpec] } set parts [split $revSpec .] return [expr {([llength $parts] & 1) || ([lindex $parts end-1] == 0)}] } # Get the list of tags public method getTags {} { return $fTags } # Determine if a tag exists. public method containsTag {tag} { return [info exists fTagTable($tag)] } # Look up a tags value by tag. If not found, return {} if # check is 0, or an error if check is 1. # This does not translation magic branch numbers. public method getTag {tag {check 0}} { return [lookupTag $tag $check] } # Look up a tags value by tag. If not found, return {} if # check is 0, or an error if check is 1. # Translates magic branch numbers. public method getTagRevision {tag {check 0}} { if {!$check && ($tag == "")} { return $tag } set rev [lookupTag $tag $check] if {$rev == {}} { return {} } else { return [translateBranchMagic $rev] } } # Given a revision spec (tag or revision), translate it to # a revision. Invalid tags generate an error if check is 1, # otherwise returns empty. Branch tags are translated from # magic number to revision. Branch numbers are passed through. # If not found, return {} if check is 0, or an error if check is 1. public method toRevision {revSpec {check 1}} { if {[isRevNumber $revSpec]} { return $revSpec } else { return [getTagRevision $revSpec $check] } } # does revSpec exists as a revison public method hasRevision {revSpec} { return [expr {![lempty [toRevision $revSpec 0]]}] } # Given a branch or revision number (or tag), return its parent. # A trunk revision returns {} public method getRevisionParent {revSpec} { # FIXME: doesn't handle magic branchs set revs [split [toRevision $revSpec] .] if {[llength $revs] & 1} { # Branch number return [join [lrange $revs 0 end-1] .] } else { # Revision number return [join [lrange $revs 0 end-2] .] } } # Given a revision number (or tag), return its branch number. # Branch numbers are returned unchanged. Head returns {}. # Magic branch numbers are convered to branch public method getRevisionBranch {revSpec} { set rev [toRevision $revSpec] set revs [split $rev .] if {[llength $revs] == 2} { return {} ;# Head } elseif {[llength $revs] & 1} { return $rev } elseif {[lindex $revs end-1] == 0} { return [translateBranchMagic $rev] } else { # Revision number return [join [lrange $revs 0 end-1] .] } } # Given a branch tag or magic branch revision, get the first and last # revision numbers for the branch. If no commits have been done on the # branch, return {}, otherwise return {firstRev lastRev} public method getBranchRevRange {branchSpec} { set branchRev [toRevision $branchSpec] if {[isBranchMagic $branchRev]} { set branchRev [translateBranchMagic $branchRev] } elseif {![isBranch $branchRev]} { error "Not a branch tag, branch revision or magic branch revision: $branchSpec" } # Search the revision list for the newest and oldest revisions # on the branch set newestRev {} set oldestRev {} foreach rev $fRevisions { if {[getRevisionBranch $rev] == $branchRev} { if {[lempty $newestRev]} { set newestRev $rev } set oldestRev $rev } } if {$newestRev == {}} { return {} } else { return [list $oldestRev $newestRev] } } # find the previous revision, or 1.0 if none public method getPrevRev {rev} { set parts [split $rev .] set lastIdx [expr [llength $parts]-1] set last [lvarpop parts $lastIdx] if {$last == 1} { set prevRev [getRevisionParent $rev] if {[lempty $prevRev]} { # will generate diff for original commit set prevRev 1.0 } } else { set prevRev [join [concat $parts [expr $last-1]] .] } return $prevRev } # Translate a revision specification: # - Tag names are translated to revisions (HEAD is supported) # - Revision numbers are returned as-is # - Branch revisions are returned as the head. public method translateRevision {revSpec} { return [translateBranchMagic [toRevision $revSpec]] } # Get the list of revision numbers on this file public method getRevisions {} { return $fRevisions } # Get a revision record. # Returns: {rev dateTime author state comment} public method getRevision {revSpec} { set rev [translateRevision $revSpec] if {![info exists fRevisionTable($rev)]} { error "invalid revision specification: \"$revSpec\"" } return $fRevisionTable($rev) } # Find most-recent dead for handling resurrections # or return "" if none. Scans backwards from to-tag rev passed in. public method mostRecentDead {rev} { while {$rev!="1.0"} { set frev [getRevision $rev] set state [lindex $frev 3] if {$state=="dead"} { return $rev } set rev [getPrevRev $rev] } return {} } # find max rev on the given branch private method getMaxRevOnBranch {branch minRev} { set maxRev $minRev foreach rev $fRevisions { if {([getRevisionBranch $rev] == $branch) && ([compareRevs $rev $maxRev] > 0)} { set maxRev $rev } } return $maxRev } # get list of revisions in the specified range (from < rev <= to) # handles toSpec being a branch magic tag. fromSpec can be empty # if toSpec is branch magic tag public method getRevsForRange {fromSpec toSpec} { # convert to range of revisions, without errors if uknown tag set fromRev [toRevision $fromSpec 0] set toRev [toRevision $toSpec 0] if {($fromRev == {}) && ($toRev == {})} { return {} ;# nothing in this range } if {$fromRev == {}} { # no from tag/revision, use branch if {[isBranchOrMagic $toRev]} { set fromRev [getRevisionParent $toRev] ;# get branch point } else { set fromRev 1.0 ;# just added } } if {$toRev == {}} { # deleted before second tag set toRev [getMaxRevOnBranch [getRevisionBranch $fromRev] $fromRev] } # convert to start and end revisions on the same branch if {[isBranchOrMagic $toRev]} { lassign [getBranchRevRange $toRev] startRev endRev if {$startRev == {}} { return {} ;# no commits in range } set toRev $endRev } # find revisions on the same branch as toRev and in range set toBranch [getRevisionBranch $toRev] set revs {} foreach rev $fRevisions { if {([getRevisionBranch $rev] == $toBranch) && ([compareRevs $fromRev $rev] < 0) && ([compareRevs $rev $toRev] <= 0)} { lappend revs $rev } } return $revs } # Convert to a printable string for debugging public method toString {{indent 0}} { set indentStr [replicate " " $indent] set indent2Str "$indentStr " append str $indentStr "fRcsFile=$fRcsFile\n" foreach var {fWorkingFile fHead fBranch fKeywordSubst} { append str $indent2Str "$var=[set $var]\n" } foreach br [lsort [array names fBranchTable]] { append str $indent2Str "branch=[list $br $fBranchTable($br)]\n" } foreach tag $fTags { append str $indent2Str "tag=[list $tag $fTagTable($tag)]\n" } foreach rev $fRevisions { set revInfo "$rev $fRevisionTable($rev)" regsub -all {\n} $revInfo {\n} revInfo append str $indent2Str "rev=$revInfo\n" } return $str } } package require Itcl # # Class to do invoke CVS # ::itcl::class CVSInvoke { private variable fRepository private variable fModule private variable fWorkDir private variable fDebug 0 # Create for accessing the specified repository. repository and module # maybe null depending on what commands are used. constructor {repository module workDir} { if {![lempty $repository]} { set fRepository $repository } if {![lempty $module]} { set fModule $module } set fWorkDir $workDir } # enable/disable debug tracing public method setDebug {val} { set fDebug $val } # evaluate a command in the working directory private method workDirEval {cmd} { set cwd [pwd] if {$fDebug} { puts stderr "cd $fWorkDir" } cd $fWorkDir if {[catch { set result [uplevel $cmd] } msg]} { cd $cwd error $msg $::errorInfo $::errorCode } cd $cwd return $result } # execute a cvs command private method cvsExec {argv} { if {$fDebug} { puts stderr [join $argv] } return [eval exec $argv] } # open a pipe from a command private method cvsOpen {argv} { if {$fDebug} { puts stderr [join [concat | $argv]] } return [open [concat | $argv 2>@stderr]] } # throw an error including a cvs commit private method throwError {cvsCmd} { error "$cvsCmd\n$::errorResult" $::errorInfo $::errorCode } # update the working directory public method update {} { set cmd [list cvs -Q update -kk -dP] try_eval { workDirEval { cvsExec $cmd } } { throwError $cmd } } # read file info objects from a cvs log stream private method readFileInfo {chan {modulePath {}}} { set infoObjs {} while {![lempty [set obj [CVSFileInfo::readCvsLogRec $chan $modulePath]]]} { lappend infoObjs $obj } return $infoObjs } # do an rlog, return a list of CVSFileInfo objects public method getRLogInfo {} { # construct actually part to module dir, this is needed because we are # doing an rlog and need to compute the work directory set cwd [pwd] cd $fRepository/$fModule set modulePath [pwd] cd $cwd set cmd [list cvs -q -d $fRepository rlog $fModule] try_eval { set chan [cvsOpen $cmd] set infoObjs [readFileInfo $chan $modulePath] close $chan } { throwError $cmd } { catch {close $chan} } return $infoObjs } # do an log, return a list of CVSFileInfo objects public method getLogInfo {{fileSubset {}}} { set cwd [pwd] set cmd [list cvs -q log] if {$fileSubset != {}} { set cmd [concat $cmd $fileSubset] } try_eval { workDirEval { set chan [cvsOpen $cmd] set infoObjs [readFileInfo $chan] close $chan } } { throwError $cmd } { catch {close $chan} } return $infoObjs } # return -r or -D for a revision private proc getRevFlag {rev} { if {[regexp {[-/]} $rev]} { return -D } else { return -r } } # do unified context diff on a file, requires a working directory # rev1 of 1.0 for a new file, rev maybe a file or date. rev2 maybe # empty. public method uniDiff {contextSize rev1 rev2 file} { # disabling for now ## -r 1.0 no longer accepted by new cvs diff ## maybe BASE work instead? (still needs -N) #if {$rev1 == 1.0} { # set rev1 "BASE" #} # -N required to make -r 1.0 work set cmd [list cvs -q diff -bB -U$contextSize -kk -N] lappend cmd [getRevFlag $rev1] $rev1 if {![lempty $rev2]} { lappend cmd [getRevFlag $rev2] $rev2 } lappend cmd $file try_eval { workDirEval { # only way to determine if an error occured vs diff exit 1 # is to check stderr set errTmp /var/tmp/err.[pid].tmp catch { lappend cmd 2> $errTmp cvsExec $cmd } diff if {![file exists $errTmp]} { error $diff } if {[file size $errTmp] != 0} { set errMsg [read_file $errTmp] if {[string first "obtained lock in" $errTmp] == -1} { progress "error: command=[$cmd]" file delete $errTmp error $errMsg } } file delete $errTmp } } { throwError $cmd } regsub {child process exited abnormally$} $diff {} diff return $diff } } # convert date/time string to sec proc cnvDateTime {dateStr} { # Tcl clock wants dates in form 2003-01-30 regsub -all / $dateStr - dateStr return [clock scan $dateStr] } # format a date proc fmtDate {date} { return [clock format $date -format %F] } # encode a string as HTML test proc htmlEncode {text} { regsub -all {&} $text {\&} text regsub -all {<} $text {\<} text regsub -all {>} $text {\>} text return $text } set gDiffStyle { } # HTML format a diff; desc should be HTML-ized. proc mkHtmlDiff {outHtml fileInfo title desc diff} { # include rev and user set fh [open $outHtml.tmp w] puts $fh "$title" puts $fh $::gDiffStyle puts $fh "" puts $fh "" puts $fh "

$title

" puts $fh $desc puts $fh "
"

    set diff [htmlEncode $diff]
    regsub -all -line {^!.*$} $diff {\0} diff
    regsub -all -line {^-.*$} $diff {\0} diff
    regsub -all -line {^\+.*$} $diff {\0} diff
    regsub -all -line {^@@.*$} $diff {\0} diff
    puts $fh $diff

    puts $fh "
" puts $fh "" puts $fh "" close $fh file rename -force $outHtml.tmp $outHtml } # Count number of changes lines proc cntDiffChgs {diff} { set changes 0 set adds 0 set replaces 0 set deletes 0 set inChgBlock 0 foreach line [split $diff \n] { set wasInChgBlock $inChgBlock switch -glob -- $line { +++* - ---* { set inChgBlock 0 } +* { incr adds set inChgBlock 1 } -* { incr deletes set inChgBlock 1 } !* { incr replaces set inChgBlock 1 } default { set inChgBlock 0 } } if {$wasInChgBlock && !$inChgBlock} { incr changes [expr max($adds,$deletes)+$replaces] set adds 0 set replaces 0 set deletes 0 } } if ($inChgBlock) { incr changes [expr max($adds,$deletes)+$replaces] } return $changes } # generate diffs for a revision as txt and html. If it already # exists, just return the files relative to outdir. Type is # context or full. Also returns the diff text. proc mkFileDiff {outDir fileInfo rev1 rev2 title desc type} { set srcFile [$fileInfo getWorkingFile] if {$rev1 == $rev2} { error "mkFileDiff $srcFile must have different recs, got $rev1 == $rev2" } if {$type == "full"} { set relBase full/$srcFile.$rev1-$rev2 set contentSize 1000000 } else { set relBase context/$srcFile.$rev1-$rev2 set contentSize 4 } set outTxtRel $relBase.diff set outTxt $outDir/$outTxtRel set outHtmlRel $relBase.html set outHtml $outDir/$outHtmlRel if {[file exists $outTxt] || [file exists $outHtml]} { # cached if {$type == "context"} { return [list $outTxtRel $outHtmlRel [read_file $outTxt]] } else { return [list $outTxtRel $outHtmlRel] } } set diff [$::gCvsInvoke uniDiff $contentSize $rev1 $rev2 $srcFile] file mkdir [file dirname $outHtml] write_file $outTxt.tmp $diff file rename -force $outTxt.tmp $outTxt mkHtmlDiff $outHtml $fileInfo $title $desc $diff return [list $outTxtRel $outHtmlRel $diff] } # Generate diffs and output the links. Returns {htmlLinks chgCount} proc mkDiffs {outDir fileInfo rev1 rev2 title desc} { lassign [mkFileDiff $outDir $fileInfo $rev1 $rev2 $title $desc context] \ contextTxtRel contextHtmlRel diffs lassign [mkFileDiff $outDir $fileInfo $rev1 $rev2 $title $desc full] \ fullTxtRel fullHtmlRel set chgCnt [cntDiffChgs $diffs] set links "lines changed: $chgCnt,\ context: html,\ text, \ full: html,\ text" return [list $links $chgCnt] } # returns {htmlLinks chgCnt} proc mkCommitDiffs {outDir fileInfo revInfo} { set title "[$fileInfo getWorkingFile] [lindex $revInfo 0]" set desc1 [htmlEncode [join [lrange $revInfo 0 2]]] set desc2 [htmlEncode [lindex $revInfo 4]] set desc "$desc1
$desc2" set rev2 [lindex $revInfo 0] set rev1 [$fileInfo getPrevRev $rev2] return [mkDiffs $outDir $fileInfo $rev1 $rev2 $title $desc] } # write index page of commits for a file proc fileReport {fh outDir fileInfo revisions} { set totalChgCnt 0 puts $fh "
  • [$fileInfo getWorkingFile]" puts $fh "" return $totalChgCnt } # returns {htmlLinks chgCnt} proc mkRangeDiffs {outDir fileInfo revInfo1 revInfo2} { set rev1 [lindex $revInfo1 0] set rev2 [lindex $revInfo2 0] set title "[$fileInfo getWorkingFile] $rev1 - $rev2" return [mkDiffs $outDir $fileInfo $rev1 $rev2 $title ""] } # write file info for change on a range of revisions proc fileRangeReport {fh outDir fileInfo revisions} { set rev1 [$fileInfo getPrevRev [lindex $revisions end]] set rev2 [lindex $revisions 0] set revInfo1 [$fileInfo getRevision $rev1] set revInfo2 [$fileInfo getRevision $rev2] lassign [mkRangeDiffs $outDir $fileInfo $revInfo1 $revInfo2] diffLinks chgCnt puts $fh "
  • [$fileInfo getWorkingFile] $rev1 - $rev2 $diffLinks" return $chgCnt } # write a report for a user in file and revision order proc userReportByFile {outDir title2 user userInfo rangeDiffs} { set userDir $outDir/$user file mkdir $userDir if {$rangeDiffs} { set switchAnchor "" set fh [open $userDir/index.html w] } else { set switchAnchor "switch to: grouped by commit view, user index
    " set fh [open $userDir/index-by-file.html w] } set title "$user: changes by file" puts $fh "$title" puts $fh "

    $title

    " puts $fh $switchAnchor puts $fh "

    $title2

    " puts $fh "" puts $fh $switchAnchor puts $fh "" close $fh return $totalChgCnt } # generate list of files by commit message # {commit {{fileInfo revision} ..} } proc sortByCommit {userInfo} { # build tmp table foreach fileRec $userInfo { lassign $fileRec fileInfo revisions foreach rev $revisions { set revInfo [$fileInfo getRevision $rev] set commit [string trim [lindex $revInfo 4]] # save file for sort lappend byCommit($commit) [list [$fileInfo getWorkingFile] $fileInfo $rev] # save lowest date/time for commit sort set time [lindex $revInfo 1] if {![info exists commitTimes($commit)] || ([string compare $time $commitTimes($commit)] > 0)} { set commitTimes($commit) $time } } } # Sort commits by time set commitTimeList {} foreach commit [array names commitTimes] { lappend commitTimeList [list $commitTimes($commit) $commit] } set commitTimeList [lsort -index 0 $commitTimeList] # now create commit list, sort each commit by file name set commits {} foreach commitTime $commitTimeList { set commit [lindex $commitTime 1] set commitInfo {} foreach fileRec [lsort -index 0 $byCommit($commit)] { lappend commitInfo [lrange $fileRec 1 2] } lappend commits [list $commit $commitInfo] } return $commits } # write commit info proc userCommitReport {fh outDir commit commitInfo} { puts $fh "
  • [htmlEncode $commit]" puts $fh "" } # write a report for a user grouped by commit proc userReportByCommit {outDir title2 user userCommits} { set userDir $outDir/$user file mkdir $userDir set switchAnchor "switch to grouped by file view, user index
    " set fh [open $userDir/index.html w] set title "$user: changes by commit" puts $fh "$title" puts $fh "

    $title

    " puts $fh $switchAnchor puts $fh "

    $title2

    " puts $fh "" puts $fh $switchAnchor puts $fh "" close $fh } proc genUserReports {outDir title2 userTableVar numChangedFiles rangeDiffs} { upvar $userTableVar userTable progress "sorting commit information" foreach u [array names userTable] { set userCommits($u) [sortByCommit $userTable($u)] } progress "generating user reports" exec rm -rf $outDir file mkdir $outDir set index $outDir/index.html set fh [open $index.tmp w] set title "CVS changes by user" puts $fh "$title" puts $fh "

    $title

    " puts $fh "

    $title2

    " puts $fh "" puts $fh "" puts $fh "" close $fh file rename -force $index.tmp $index return $totalChgCnt } proc genFileReports {outDir title2 fileTableVar lineChgCount rangeDiffs} { upvar $fileTableVar fileTable progress "generating file reports" exec rm -rf $outDir file mkdir $outDir set index $outDir/index.html set fh [open $index.tmp w] set title "CVS changes by file" puts $fh "$title" puts $fh "

    $title

    " puts $fh "

    $title2

    " puts $fh "" puts $fh "" puts $fh "" close $fh file rename -force $index.tmp $index } # check if fromTag is brach point of toTag proc isRevParent {fileInfo fromTag toTag} { return [expr {[$fileInfo getRevisionParent $toTag] == [lindex [$fileInfo getRevision $fromTag] 0]}] } proc isSameBranch {fileInfo fromTag toTag} { return [expr {[$fileInfo getRevisionBranch $fromTag] == [$fileInfo getRevisionBranch $toTag]}] } # Code will lose stuff if fromTag is not branch point of toTag or if # fromTag and toTag are on different branches. Check if tags are ok proc checkRevTags {fileInfo fromTag toTag} { # only check if file contans revision, and fromTag doesn't specify the base if {![lempty $fromTag] && [$fileInfo hasRevision $fromTag] && [$fileInfo hasRevision $toTag]} { if {!([isRevParent $fileInfo $fromTag $toTag] || [isSameBranch $fileInfo $fromTag $toTag])} { error "[$fileInfo getWorkingFile] fromTag $fromTag not on same branch as toTag $toTag or fromTag is not point branch point for two" } } } # select and sort by user add to use table # user table are entries of {{fileObj {rev ...} ...} # file table entries are {fileObj {rev ...}} proc selectFileRevs {fileInfo userTableVar fileTableVar fromTag toTag} { upvar $userTableVar userTable $fileTableVar fileTable checkRevTags $fileInfo $fromTag $toTag # sort revisions out by users set selectRevs [$fileInfo getRevsForRange $fromTag $toTag] # partition revisions my user foreach rev $selectRevs { lappend byUser([lindex [$fileInfo getRevision $rev] 2]) $rev } # add to tables foreach u [array names byUser] { lappend userTable($u) [list $fileInfo $byUser($u)] } if {![lempty $selectRevs]} { set fileTable([$fileInfo getWorkingFile]) [list $fileInfo $selectRevs] } } proc makeIndexPage {outDir title2 module} { file mkdir $outDir set index $outDir/index.html set fh [open $index.tmp w] set title "CVS changes: $module" puts $fh "$title" puts $fh "

    $title

    " puts $fh "

    $title2

    " puts $fh { } close $fh file rename -force $index.tmp $index } proc makeHelpPage {outDir} { file mkdir $outDir set help $outDir/help.html set fh [open $help.tmp w] puts $fh {CVS Reports

    CVS Reports

    Content diffs

    Two types of diffs are produced: The diffs are in two formats:

    The changed line counts are derived by examining the diffs. Blocks of deletes followed by inserts are considered replacements, so the change count is the larger of the delete count and add count. } close $fh file rename -force $help.tmp $help } proc getFileRevs {infoList userTableVar fileTableVar fromTag toTag} { upvar $userTableVar userTable $fileTableVar fileTable progress "sorting revision information" foreach f $infoList { selectFileRevs $f userTable fileTable $fromTag $toTag } } proc genReports {outDir title2 module infoList fromTag toTag rangeDiffs} { getFileRevs $infoList userTable fileTable $fromTag $toTag exec rm -rf $outDir/user.new set lineChgCount [genUserReports $outDir/user.new $title2 userTable [array size fileTable] $rangeDiffs] exec rm -rf $outDir/user file rename $outDir/user.new $outDir/user exec rm -rf $outDir/file.new genFileReports $outDir/file.new $title2 fileTable $lineChgCount $rangeDiffs exec rm -rf $outDir/file file rename $outDir/file.new $outDir/file makeIndexPage $outDir $title2 $module } # # Entry # set gUsage {cvs-reports [options] workdir outDir options: -start date - Starting date, in the form 2003/02/30 or 2003-02-30 -verbose - Generate progress messages -from fromTag -to toTag -fromDate date - only used in title -toDate date - only used in title -branchVersion version - only used in title -rangeDiffs - do just per-file diffs for target range, rather than per-commit -debug -no-update - don't do a cvs update on working directory -file path - only operate on path, relative to the working directory. Mainly for debugging, maybe repeated. fromTag can be a tag made at the branch point and toTag the branch tag } # # Usage error. # proc usage {{msg {}}} { if {![lempty $msg]} { puts stderr "Error: $msg" } puts stderr $::gUsage exit 1 } proc progress {msg} { if {$::gVerbose} { catch { puts "$msg" flush stdout } } } # these will be calculated from value(s) passed in set toTag {} set toTagDate {} set fromTag {} set fromTagDate {} set branchVersion {} set rangeDiffs 0 set fileSubset {} set gVerbose 0 set debug 0 set doUpdate 1 while {[string match -* [lindex $argv 0]]} { set opt [lvarpop argv] switch -- $opt { -from { if {[lempty $argv]} { usage "-from requires an argument" } set fromTag [lvarpop argv] } -to { if {[lempty $argv]} { usage "-to requires an argument" } set toTag [lvarpop argv] } -fromDate { if {[lempty $argv]} { usage "-fromDate requires an argument" } set fromTagDate [lvarpop argv] } -toDate { if {[lempty $argv]} { usage "-toDate requires an argument" } set toTagDate [lvarpop argv] } -whichReport { puts stderr "WARNING: -whichReport is no longer used" if {[lempty $argv]} { usage "-whichReport requires an argument" } lvarpop argv } -branchVersion { if {[lempty $argv]} { usage "-branchVersion requires an argument" } set branchVersion [lvarpop argv] } -verbose { set gVerbose 1 } -rangeDiffs { set rangeDiffs 1 } -debug { set debug 1 } -no-update { set doUpdate 0 } -file { if {[lempty $argv]} { usage "-file requires an argument" } lappend fileSubset [lvarpop argv] } default { usage "Invalid option \"$opt\"" } } } if {[llength $argv] != 2} { usage "wrong \# args" } lassign $argv workDir outDir if {![file isdir $workDir]} { usage "workdir does not exists: $workDir" } # make sure directory write perms are maintained umask 0002 # get module from workDir for use in messages set module [string trim [read_file $workDir/CVS/Repository]] set gCvsInvoke [CVSInvoke \#auto {} $module $workDir] $gCvsInvoke setDebug $debug progress "starting from-tag $fromTag" progress " ending to-tag $toTag" if {$fromTagDate != {}} { progress "starting from-tag-date $fromTagDate" } if {$toTagDate != {}} { progress " ending to-tag-date $toTagDate" } if {$branchVersion != {}} { progress " branch version: $branchVersion" } if {$doUpdate} { # update the working directory (if rdiff supported -U to specify context # size, we wouldn't need a working dir) progress "updating cvs working directory for $module" $gCvsInvoke update } progress "collecting cvs change logs for $module" set infoList [$gCvsInvoke getLogInfo $fileSubset] set title2 "$fromTag to $toTag" if {![lempty $fromTag]} { append title2 " ($fromTagDate to $toTagDate)" } append title2 " $branchVersion" genReports $outDir $title2 $module $infoList $fromTag $toTag $rangeDiffs progress "cvs report generation complete" # Local Variables: ** # mode: tcl ** # End: **