#!/usr/bin/env perl -w ############################################################################### # cvsplot: Copyright (c) 2001, 2002, 2003, 2004 David Sitsky. # All rights reserved. # # cvsplot is a perl script which is used to extract information from CVS and # plots the total number of lines and number of files in a selected file set # against time. # # File sets can be specified using regular expressions. # The start and end dates may also be specified. # # This program is free software; you can redistribute it and modify it under # the terms of the GPL. use Config; use Cwd; use Symbol; use IPC::Open3; # Whether debugging is enabled or not. $debug = 0; # Additional global arguments to use with CVS commands. $cvs_global_args = ""; # The start date in which to gather statistics. $start_date = ""; # The final date in which to gather statistics. $end_date = ""; # Indicate whether to count the number of lines _changed_, or the # the number of lines added. $count_lines_changed = 0; # The directory is which to gather the cvs statistics (where the cvs log # command is run from), or the directory of the CVS repository, if the # -rlog option is used. $cvsdir = ""; # The module to run cvs rlog over, if the -rlog option is specified. $rlog_module = ""; # The branch that we are collecting statistics from. By default, the main # branch is used. $branch_tag = ""; # Parallel arrays of file patterns which indicate whether it is an include # or exclude pattern, and what the regular expression is. @pattern_include = (); @pattern_regexp = (); # Where the number of lines statistics will be stored. $linedata = ""; # Where the number of files statistics will be stored. $filedata = ""; # Where the per-user line statistics will be stored. $userdata = ""; # A set of users whose data needs to be examined individually. %filterusers = (); # A map of a set of group to user memberships. %usergroups = (); # The name of the default user group (if any). $default_user_group = ""; # A hash (by date) of a hash (by filename) of lines added. %line_stats = (); # A hash (by date) of a hash (by filename) of the status. %state_stats = (); # A hash (by date) of a hash (by filename) of the revision. %revision_stats = (); # A hash (by date) of a hash (by filename) of the author. %author_stats = (); # A hash (by filename) of a hash (by version) of lines added. %file_version_delta = (); # A hash (by filename) of a hash (by version) of the file state. %file_version_state = (); # A hash (by filename) of a hash (by version) of the author. %file_version_author = (); # A hash (by filename) of the magic branch number. %file_branch_number = (); # A hash (by filename) of the number of branch revisions made. %file_number_branch_revisions = (); # A hash (by date) of the total number of lines. %total_lines = (); # A hash (by date) of the total number of files. %total_files = (); # A hash (by date) of a hash (by user) of their total number of lines. %total_user_lines = (); # Flag to indicate if gnuplot is to be used. $use_gnuplot = 0; # The location of the gnuplot binary. It is assumed to be in the # path, but may be over-ridden on the command-line with the # -gnuplot argument. For Window platforms, this is useful, since # the name of the binary is not "gnuplot", but often "wgnupl32" # or "pgnuplot". $gnuplot = "gnuplot"; # The gnuplot output filename to write the output of the line data. $gnuplot_linedata = ""; # The gnuplot output filename to write the output of the file data. $gnuplot_filedata = ""; # The gnuplot output filename to write the output of the user data. $gnuplot_userdata = ""; # The gnuplot "set term" expression to use when generating output. # By default, generate colour png files. $gnuplot_setterm = "png color"; # A general gnuplot command that can be executed to change some # aspect of the plotting command, such as the format of the x values. $gnuplot_command = ""; # Flag to indicate if lines are to be used in the final plot, rather # than discrete points. For large plots, this can sometimes improve # readability. $use_linestyle = 0; # Determine if this process is running under Windows. $osname = $Config{'osname'}; $windows = (defined $osname && $osname eq "MSWin32") ? 1 : 0; check_missing_modules(); process_command_line_arguments(); get_cvs_statistics(); analyse_statistics(); generate_data_files(); generate_plots(); ############################################################################### # Utility method for quoting an argument for a shell command. ShellQuote # is good for UNIX boxes, but doesn't work for DOS platforms as it uses # single quotes, while DOS needs double quotes. Its a shame shell_quote # isn't cross-platform. sub quote { my ($arg) = @_; if ($windows) { return "\"$arg\""; } else { String::ShellQuote::shell_quote($arg); } } ############################################################################### # Method for writing out help if modules are missing. sub check_missing_modules { my @missing = (); # Load the Date::Manip module. eval { require Date::Manip; }; if ($@) { push @missing, 'Date::Manip'; } # Load the String::ShellQuote module for UNIX platforms. eval { if (! $windows) { require String::ShellQuote; } }; if ($@) { push @missing, 'String::ShellQuote'; } # Check if there are any missing modules. return if $#missing == -1; # First, output the generic "missing module" message. print "\n"; print "Cvsplot requires some Perl modules which are missing " . "from your system.\n"; if ($windows) { print "These can be installed by issuing the following commands:\n\n"; foreach my $module (@missing) { $module =~ s/:://g; print 'C:\> ' . "ppm install $module\n"; } print "\n"; } else { print "They can be installed by running (as root) the following:\n"; foreach my $module (@missing) { print " perl -MCPAN -e 'install \"$module\"'\n"; } print "\n"; print "Modules can also be downloaded from http://www.cpan.org.\n\n"; } exit; } ############################################################################### # Check whether the supplied file is to be examined or not depending on what # the user set for the -include and -exclude options. Return true if the # file is to be included. If no -include or -exclude options have been # set by the user, return true by default. # sub include_file { my ($filename) = @_; # If there are no settings, include everything. if ($#pattern_regexp == -1) { return 1; } # Go through the pattern_regexp array, and see if there is any matches. for ($i = 0; $i <= $#pattern_regexp; $i++) { if ($filename =~ /$pattern_regexp[$i]/) { # Got a match, return whether or not the file should be included # or not. return $pattern_include[$i]; } } # No matches, don't include this file. return 0; } ############################################################################### # Using "cvs log" and a few other commands, gather all of the necessary # statistics. # sub get_cvs_statistics { if ($debug && defined $osname) { print "Platform is $osname\n"; } # Explicitly set the timezone for window platforms, so that DateManip # works. if ($windows) { $ENV{TZ} = "C"; } my $working_file = ""; my $relative_working_file = ""; my $working_cvsdir = ""; my $search_file = 0; # Change to the directory nominated by $cvsdir, and save the current # directory, only if we aren't using the -rlog option. if ($rlog_module eq "") { $saved_cwd = cwd(); chdir $cvsdir || die "Failed to change to directory \"$cvsdir\": $!"; } else { # Remove the accessor part, and just get the pathname. $cvsdir =~ /([^:]+)$/; $working_cvsdir = $1; print "Got working_cvsdir as $working_cvsdir\n" if $debug; # Since this is used in a regexp below, need to make sure DOS pathnames # are correctly matched against. $working_cvsdir =~ s/\\/\\\\/g; } # Flag to indicate what the state is when parsing the output from cvs log. # true indicates that the parser is waiting for the start of a cvs log # entry. $search_file = 1; # Build up the command string appropriately, depending on what options # have been set. my $command = ($rlog_module eq '') ? "cvs $cvs_global_args log" : sprintf("cvs $cvs_global_args -d %s rlog %s", quote($cvsdir), quote($rlog_module)); print "Executing \"$command\"\n" if $debug; open (CVSLOG, "$command |") || die "Couldn't execute \"$command\""; while () { if ($search_file == 1) { # Need to locate the name of the working file if (/^RCS file: (.*),v$/) { $working_file = $1; $working_file =~ s/Attic\///g; $relative_working_file = ""; # Check if this file is to be included or not. if (include_file($working_file)) { # Yep, search for more details on this file. $search_file = 0; if ($branch_tag eq "") { # Main branch to be investigated only. $file_branch_number{$working_file} = "1"; $file_number_branch_revisions{$working_file} = 0; } print "Including file \"$working_file\"\n" if $debug; } else { print "Excluding file \"$working_file\"\n" if $debug; } } } else { # Collective the relative part for those runs that don't use # -rlog. if (/^Working file: (.*)$/) { $relative_working_file = $1; } # Handle repositories working off an explicit numbering scheme, # such as 8.1. Only do this if the user hasn't specified an # explicit branch to gather statistics over. In most cases, # the result will still be 1, but this handles the stranger # repositories out there. elsif ($branch_tag eq "" && /^head: (\d+)\./) { $file_branch_number{$working_file} = $1; } # If we are collecting statistics on a branch, determine the magic # branch number for this file. elsif ( (! defined $file_branch_number{$working_file}) && (/^\s*${branch_tag}: ([\d\.]+)\.0\.(\d+)$/) ) { $file_branch_number{$working_file} = "${1}.${2}"; $file_number_branch_revisions{$working_file} = 0; if ($debug) { print "Got branch $file_branch_number{$working_file}"; print " for file \"$working_file\"\n"; } } elsif (/^keyword substitution: b$/) { # This is a binary file, ignore it. undef($file_branch_number{$working_file}); undef($file_number_branch_revisions{$working_file}); $search_file = 1; print "Excluding binary file \"$working_file\"\n" if $debug; } elsif (/^=============================================================================$/) { # End of the log entry for this file, start parsing for the # next file. $search_file = 1; next; } elsif (/^----------------------------$/) { # Matched the description separator. If a branch has been # specified, but this file doesn't exist on it, skip this file. if (($branch_tag ne "") && (! defined $file_branch_number{$working_file})) { if ($debug) { print "File \"$working_file\" not on branch\n"; } $search_file = 1; next; } # Read the revision line, and record the appropriate # information. $_ = ; if (/^revision ([\d\.]+)$/) { # Record the revision, and whether it is part of the tag # of interest. $revision = $1; if ($revision =~ /^$file_branch_number{$working_file}\.\d+$/) { $file_on_branch = 1; $file_number_branch_revisions{$working_file}++; } else { $file_on_branch = 0; } if ($debug) { print "Got branch number: $file_branch_number{$working_file} rev $revision on branch: $file_on_branch\n"; } } else { # Problem in parsing, skip it. print "Couldn't parse line: $_\n"; $search_file = 1; next; } $_ = ; # Read the "date" line. if (/^date: (\d\d\d\d\/\d\d\/\d\d \d\d:\d\d:\d\d); .* author: (.*); .* state: (.*);.*lines: \+(\d+) \-(\d+).*$/) { # Note for some CVS clients, state dead is presented in # this this way, as the following pattern. $date = $1; $author = $2; $users{$author} = 1; $state = $3; $lines_added = $4; $lines_removed = $5; $number_lines = $count_lines_changed ? $lines_added + $lines_removed : $lines_added - $lines_removed; $file_version_delta{$working_file}{$revision} = $number_lines; $file_version_state{$working_file}{$revision} = $state; $file_version_author{$working_file}{$revision} = $author; if ($file_on_branch) { # This revision lives on the branch of interest. $line_stats{$date}{$working_file} += $number_lines; $state_stats{$date}{$working_file} = $state; $revision_stats{$date}{$working_file} = $revision; $author_stats{$date}{$working_file} = $author; } } elsif (/^date: (\d\d\d\d\/\d\d\/\d\d \d\d:\d\d:\d\d); .* author: (.*); .* state: dead;.*$/) { # File has been removed. $date = $1; $author = $2; $users{$author} = 1; $file_version_delta{$working_file}{$revision} = 0; $file_version_state{$working_file}{$revision} = "dead"; $file_version_author{$working_file}{$revision} = $author; if ($file_on_branch) { $line_stats{$date}{$working_file} = 0; $state_stats{$date}{$working_file} = "dead"; $revision_stats{$date}{$working_file} = $revision; $author_stats{$date}{$working_file} = $author; } } elsif (/^date: (\d\d\d\d\/\d\d\/\d\d \d\d:\d\d:\d\d); .* author: (.*); .* state: ([^;]*);.*$/) { $date = $1; $author = $2; $users{$author} = 1; $state = $3; # Unfortunately, cvs log doesn't indicate the number of # lines an initial revision is created with, so find this # out using the following cvs command. Note the regexp # below has an optional drive delimeter to support DOS # installations. my $lccmd = ""; if ($rlog_module ne "") { print "Working cvsdir is: $working_cvsdir working file: $working_file\n" if $debug; # For DOS-based repositories, the filename may contain # a drive letter. Also need to be flexible with the # pathname separator. if (! ($working_file =~ /^([A-z]:)?${working_cvsdir}[\/\\](.*)$/)) { print STDERR "-cvsdir argument $working_cvsdir doesn't match "; print STDERR "repository filename prefix $working_file\n"; print STDERR "Please correct your -cvsdir argument and try again\n"; exit 1; } $lccmd = sprintf("cvs $cvs_global_args -d %s co -r %s -p %s", quote($cvsdir), quote($revision), quote($2)); } else { $lccmd = sprintf("cvs $cvs_global_args update -r %s -p %s", quote($revision), quote($relative_working_file)); } print "Executing $lccmd\n" if $debug; my $WTR = gensym(); my $RDR = gensym(); my $ERR = gensym(); my $pid = open3($WTR, $RDR, $ERR, $lccmd); for ($number_lines = 0; defined <$RDR>; $number_lines++) {} close ($RDR); my $error_string = ""; while (<$ERR>) { $error_string .= $_; } waitpid $pid, 0; if ($?) { print "CVS command failed: \"$lccmd\" status $?\n"; print "$error_string\n"; exit 1; } print "$working_file 1.1 = $number_lines lines\n" if $debug; $file_version_delta{$working_file}{$revision} = $number_lines; $file_version_state{$working_file}{$revision} = $state; $file_version_author{$working_file}{$revision} = $author; if ($file_on_branch) { $line_stats{$date}{$working_file} += $number_lines; $state_stats{$date}{$working_file} = "Exp"; $revision_stats{$date}{$working_file} = $revision; $author_stats{$date}{$working_file} = $author; } } else { print "Couldn't parse: $_"; } if ($debug) { print "File \"$working_file\" rev $revision "; print "delta $file_version_delta{$working_file}{$revision} "; print "state $file_version_state{$working_file}{$revision}\n"; print "author $file_version_author{$working_file}{$revision}\n"; } } } } close(CVSLOG); # Go back to the original directory if we aren't using the -rlog option. if ($rlog_module eq "") { chdir $saved_cwd; } } # Variable to store results when calling get_line_count. %memorise_line_count = (); ############################################################################### # Return the number of lines that constitute a particular revision of a file. # sub get_line_count { my ($filename, $revision) = @_; my $count = get_line_count_inner($filename, $revision); # Store this result for future intermediate calculations. $memorise_line_count{$filename}{$revision} = $count; if ($debug) { print "get_line_count($filename, $revision) = $count\n"; } return $count; } sub get_line_count_inner { my ($filename, $revision) = @_; my $count = 0; my $finished = 0; while (!$finished) { if (defined $memorise_line_count{$filename}{$revision}) { $count += $memorise_line_count{$filename}{$revision}; $finished = 1; } elsif (! defined($file_version_state{$filename}{$revision})) { # Case where we are looking for a revision that hasn't # been found in the output of the CVS log command. This is # usually because a developer decided to start the file # revision at something other than 1.1. $memorise_line_count{$filename}{$revision} = 0; $finished = 1; } elsif ($revision eq "1.1") { # Base case where the revision is 1.1 $memorise_line_count{$filename}{$revision} = $file_version_delta{$filename}{$revision}; $count += $memorise_line_count{$filename}{$revision}; $finished = 1; } elsif ($file_version_state{$filename}{$revision} eq "dead") { # Case where file has been removed. The file count is # effectively the previous version's count. $revision =~ /^([\d\.]+)\.(\d+)$/; $previous_subrevision = $2 - 1; $previous_revision = "${1}.${previous_subrevision}"; $revision = $previous_revision; } elsif ($revision =~ /^([\d\.]+)\.\d+\.1$/) { # Case where need to decend down branch point and find the # contributions made there. $branch_point_revision = $1; if (! defined($file_version_delta{$filename}{$revision})) { print "file_version_data not defined for $filename $revision\n"; } $count += $file_version_delta{$filename}{$revision}; $revision = $branch_point_revision; } elsif ($revision =~ /^([\d\.]+)\.(\d+)$/) { # Need to determine previous revision number + this revision's # contribution. $previous_subrevision = $2 - 1; $previous_revision = "${1}.${previous_subrevision}"; if (! defined($file_version_delta{$filename}{$revision})) { print "[2] file_version_data not defined for $filename $revision\n"; } $count += $file_version_delta{$filename}{$revision}; $revision = $previous_revision; } else { print "Unhandled case for file $filename revision $revision\n"; exit 0; } } return $count; } ############################################################################### # Sum up those entries with the same date, and add up the line count. # When a file has been removed, its contribution from the total file # count must be removed completely. # sub analyse_statistics { # Keep a record of what files are present when gathering statistics. my %files_present = (); # Keep a record of the current revision a file has when gathering # statistics. Their initial revisions will be the revision that # they branched from if we are doing statistics on a branch. my %file_revision = (); # Keep a record of the current state of a file when gathering # statistics. my %file_state = (); # Keep a record of per-user line counts; my %user_total = (); # A hash of filenames to a hash of usernames to linecount totals. This # is maintained as the statistics are processed, so that when files are # removed or re-added, all of the individual contributions are properly # maintained to the correct authors. my %file_author_linecount = (); if ($branch_tag ne "") { foreach $file ( keys %file_branch_number ) { $file_branch_number{$file} =~ /^([\d\.]+)\.\d+$/; my $base_revision = $1; $file_revision{$file} = $base_revision; if ($file_version_state{$file}{$base_revision} ne "dead") { $files_present{$file} = 1; } } } # Go through the records in order of earliest to latest. foreach $date ( sort keys %line_stats ) { # Investigate what CVS operations occured on this date. Record # individual author contributions. foreach $file ( keys %{ $line_stats{$date} } ) { # Update the current revision the file has. $file_revision{$file} = $revision_stats{$date}{$file}; $author = $author_stats{$date}{$file}; # Record if a file was removed or not. if ($state_stats{$date}{$file} eq "dead") { delete $files_present{$file}; # For each author contribution made to this file, deduct them # now. foreach my $innerauthor (keys %{ $file_author_linecount{$file} }) { my $amount = $file_author_linecount{$file}{$innerauthor}; $user_total{$innerauthor} -= $amount; $total_user_lines{$innerauthor}{$date} = $user_total{$innerauthor}; } } else { $files_present{$file} = 1; # If the file was removed in the previous commit, need to add # the entire line count broken amongst the appropriate authors, # otherwise just add the increment. my $state = $file_state{$file}; if (defined $state && $state eq "dead") { # For each author contribution made to this file, add them # to the appropriate authors now. foreach my $innerauthor (keys %{ $file_author_linecount{$file} }) { my $amount = $file_author_linecount{$file}{$innerauthor}; $user_total{$innerauthor} += $amount; $total_user_lines{$innerauthor}{$date} = $amount; } } # Now add in the increment done in this commit. $user_total{$author} += $line_stats{$date}{$file}; $file_author_linecount{$file}{$author} += $line_stats{$date}{$file}; $total_user_lines{$author}{$date} = $user_total{$author}; } # Record the current state of this file. $file_state{$file} = $state_stats{$date}{$file}; } # Calculate the total number of files present on this date. $total_files{$date} = scalar keys %files_present; # Count the total number of lines present for the current file set. my $total_line_count = 0; foreach $file ( keys %file_revision ) { if (defined $files_present{$file}) { $total_line_count += get_line_count($file, $file_revision{$file}); } } $total_lines{$date} = $total_line_count; } # Filter out those entries to only contain what the user specified # in the date interval. This could be done far more efficiently, but # for now... foreach $date ( sort keys %total_lines ) { $current_date = Date::Manip::ParseDate($date); if (($start_date ne "" && Date::Manip::Date_Cmp($current_date, $start_date) < 0) || ($end_date ne "" && Date::Manip::Date_Cmp($current_date, $end_date) > 0)) { # This date is before the start date specified by the user, or # this date is after the end date specified by the user. # Delete it. delete $total_lines{$date}; delete $total_files{$date}; # Also handle per-user date filtering. foreach $author ( keys %total_user_lines ) { delete $total_user_lines{$author}{$date}; } } } } ############################################################################### # Generate the data files into the specified locations. Only write out # those entries within the user's specified date interval. Note, this could # be handled far more efficiently, but for now... # sub generate_data_files { # Create a map from user names to ids, and likewise for groups, so that # each distinct user/group is mapped to an id, which is used later for # Gnuplot. my %user_map = (); my %group_map = (); my $index = 0; foreach my $user ( sort keys %total_user_lines ) { $user_map{$user} = $index++; # If groups are enabled, and the default group is specified, check if # this user needs to be added to it. if ($default_user_group ne '') { my $found = 0; foreach my $group ( sort keys %usergroups ) { if (exists $usergroups{$group}{$user}) { $found = 1; last; } } if ($found == 0) { # Add to default group. $usergroups{$default_user_group}{$user} = 1; } } } $index = 0; foreach my $group ( sort keys %usergroups ) { $group_map{$group} = $index++; } # Keep a running record of total linecounts for each group. my %total_group_lines = (); # Determine if authors are being filtered. my $filtering_users = scalar keys %filterusers != 0; my $filtering_groups = scalar keys %usergroups != 0; # Write out the data for each commit in date order. foreach $date ( sort keys %total_lines ) { print LINEDATA "$date $total_lines{$date}\n"; print FILEDATA "$date $total_files{$date}\n"; # Check if there is any author data to output. if ($userdata ne "") { foreach my $author ( sort keys %user_map ) { # Only output if it is an author of interest. next if ($filtering_users && $default_user_group eq '' && ! exists $filterusers{$author}); # Update author/group data if defined. my $data = $total_user_lines{$author}{$date}; if (defined $data) { if ($filtering_groups) { # Find out the group membership for this author. foreach my $group ( sort keys %usergroups ) { if (exists $usergroups{$group}{$author}) { # Author is a member of this group, update the # group stats and update the total count. $total_group_lines{$group}{$author} = $data; my $total = 0; foreach my $user (keys %{ $total_group_lines{$group} }) { $total += $total_group_lines{$group}{$user}; } print USERDATA "$date $group " . "$group_map{$group} " . "$total\n"; } } } else { # Output author information. print USERDATA "$date $author " . "$user_map{$author} $data\n"; } } } } } close LINEDATA; close FILEDATA; close USERDATA if $userdata ne ""; } ############################################################################### # Generate the gnuplot data files into the specified locations. # # useful reference : http://www.cs.uni.edu/Help/gnuplot/TOC.html # sub generate_plots { my $linestyle_command = ""; if ($use_linestyle) { $linestyle_command = "set data style lines"; } if ($use_gnuplot) { # Generate the gnuplot command scripts to build the necessary images. if ($gnuplot_linefiledata) { # Produce a combined plot if the user has specified to build one. $command = <= 0) { $found_include = 0; for ($i = 0; $i <= $#pattern_include; $i++) { if ($pattern_include[$i]) { $found_include = 1; last; } } if ($found_include == 0) { print "error: empty file set specified: "; print "missing -include option\n"; print " You probably want to add \"-include \'.*\'\""; print " to the end of your command.\n"; exit 1; } } # Check the mandatory arguments have been set. if ($cvsdir eq "" || $linedata eq "" || $filedata eq "") { print "error: Not all mandatory arguments specified.\n\n"; usage(); } # The line and file (or linefile) gnuplot options need to be set if a # gnuplot option is used. if ($use_gnuplot) { if ($gnuplot_linedata eq "" || $gnuplot_filedata eq "") { print "error: Both the -gnuplotlinedata and -gnuplotfiledata "; print "options must be specified if using gnuplot.\n\n"; usage(); } } # If -userlist has been specified, make sure -userdata has. if (scalar keys %filterusers != 0 && $userdata eq "") { print "error: -userdata needs to be specified if -userdata is.\n\n"; usage(); } # If -gnuplotuseruserdata has been specified, make sure -userdata has. if ($gnuplot_userdata ne "" && $userdata eq "") { print "error: -userdata needs to be specified if -gnuplotuserdata is.\n\n"; usage(); } # If both the start and end dates are specified, check that the start date # occurs before the end date. if ($start_date ne "" && $end_date ne "" && &Date_Cmp($start_date, $end_date) >= 0) { print "error: Start date specified must occur before the end date.\n\n"; usage(); } # If the -rlog option has been specified, need to make sure that the # CVS version install is >= 1.11.1, as it is not supported in earlier # versions. if ($rlog_module ne "") { my $WTR = gensym(); my $RDR = gensym(); my $ERR = gensym(); my $pid = open3($WTR, $RDR, $ERR, "cvs $cvs_global_args rlog"); my $deprecated_found = 0; while (<$ERR>) { $deprecated_found = 1 if (/deprecated/); } close $WTR; close $RDR; close $ERR; waitpid $pid, 0; if ($deprecated_found) { print "error: -rlog option requires CVS version >= 1.11.1\n\n"; exit 1; } } # Open the specified output files. open (LINEDATA, ">$linedata") || die "Failed to create file \"$linedata\": $!"; open (FILEDATA, ">$filedata") || die "Failed to create file \"$filedata\": $!"; if ($userdata ne "") { open (USERDATA, ">$userdata") || die "Failed to create file \"$userdata\": $!"; } } ############################################################################### # Print out a usage message. # sub usage { print "cvsplot version 1.7.4 - "; print "Copyright David Sitsky: sits\@users.sourceforge.net\n\n"; print "cvsplot collects statistics from CVS controlled files.\n\n"; print "usage:\n"; print "cvsplot.pl -cvsdir [-rlog ] -linedata -filedata \n"; print " [-userdata ] [-userlist [groupname=]]*\n"; print " [-defaultusergroup ]\n"; print " [-cvs-global-args ]\n"; print " [-countchangedlines]\n"; print " [-branch ] [-start ] [-end ]\n"; print " {-include | -exclude } [-linestyle]\n"; print " [-gnuplotlinedata \n"; print " -gnuplotfiledata \n"; print " [-gnuplot ]\n"; print " [-gnuplotlinefiledata ]\n"; print " [-gnuplotuserdata \n"; print " [-gnuplotsetterm ]\n"; print " [-gnuplotcommand ]]\n\n"; print "See http://cvsplot.sourceforge.net for updates.\n"; exit 1; }