############################################################################### # Codestriker: Copyright (c) 2001, 2002 David Sitsky. All rights reserved. # sits@users.sourceforge.net # # This program is free software; you can redistribute it and modify it under # the terms of the GPL. # Main package which contains a reference to all configuration variables. package Codestriker; use strict; use Encode; use Config; use Time::Local; use IPC::Open3; use File::Temp qw/ tempdir /; use File::Path; use Fatal qw / open close waitpid /; # Export codestriker.conf configuration variables. use vars qw ( $mailhost $mailuser $mailpasswd $use_compression $gzip $cvs $svn $ssh $p4 $vss $bugtracker @valid_repositories $default_topic_create_mode $default_tabwidth $file_reviewer $db $dbuser $dbpasswd $codestriker_css $NORMAL_MODE $COLOURED_MODE $COLOURED_MONO_MODE @topic_states $bug_db $bug_db_host $bug_db_name $bug_db_password $bug_db_user $lxr_map $email_send_options $default_topic_br_mode $allow_delete $allow_searchlist $default_file_to_view $allow_projects $antispam_email $VERSION $title $BASEDIR $metric_config $tmpdir @metric_schema $comment_state_metrics $project_states $rss_enabled $repository_name_map $repository_url_map @valid_repository_names $topic_text_encoding @default_topic_states @default_states_indexes ); # Version of Codestriker. $Codestriker::VERSION = "1.9.4"; # Default title to display on each Codestriker screen. $Codestriker::title = "Codestriker $Codestriker::VERSION"; # The maximum size of a diff file to accept. At the moment, this is 20Mb. $Codestriker::DIFF_SIZE_LIMIT = 20000 * 1024; # Indicate what base directory Codestriker is running in. This may be set # in cgi-bin/codestriker.pl, depending on the environment the script is # running in. By default, assume the script is running in the cgi-bin # directory (this is not the case for Apache2 + mod_perl). $Codestriker::BASEDIR = ".."; # Error codes. $Codestriker::OK = 1; $Codestriker::STALE_VERSION = 2; $Codestriker::INVALID_TOPIC = 3; $Codestriker::INVALID_PROJECT = 4; $Codestriker::DUPLICATE_PROJECT_NAME = 5; $Codestriker::UNSUPPORTED_OPERATION = 6; $Codestriker::DIFF_TOO_BIG = 7; $Codestriker::LISTENER_ABORT = 8; # Revision number constants used in the filetable with special meanings. $Codestriker::ADDED_REVISION = "1.0"; $Codestriker::REMOVED_REVISION = "0.0"; $Codestriker::PATCH_REVISION = "0.1"; # Participant type constants. $Codestriker::PARTICIPANT_REVIEWER = 0; $Codestriker::PARTICIPANT_CC = 1; # Default email context to use. $Codestriker::EMAIL_CONTEXT = 8; # Valid comment states, the only one that is special is the submitted state. $Codestriker::COMMENT_SUBMITTED = 0; # Day strings @Codestriker::days = ("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"); # Month strings @Codestriker::months = ("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"); # Short day strings @Codestriker::short_days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"); # Short month strings @Codestriker::short_months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"); $metric_config = ""; # name => The short name of the metric. This name will be used in the # SQL table, in the data download, in the input tables, and perhaps in # the .conf file. # # description => The long description of the item. Displayed as online help (?) # # enabled => If 1, the metrics are enabled by default in "basic" # configs. Otherwise the $metric_config option on the .conf will # override this. # # scope => This will be "topic", "reviewer", "author". A "topic" # metric that has a 1 to 1 relationship with the topic itself. If it # is not a topic metric, it is a kind of user metric. User metrics have # a 1-1 relationship with each user in the topic. If the type is # reviewer, it is only needed by a user that is a reviewer (but not # author), of the topic. If the type is author, it is only needed by # the author of the metric, and if it is participants, it is needed by # all users regardless of the role. # # filter => The type of data being stored. "hours" or "count". Data # will not be stored to the database if it does not pass the format # expected for the filter type. my @metrics_schema = ( # planning time { name=>"entry time", description=>"Work hours spent by the inspection leader to check that entry conditions are met, and to work towards meeting them.", enabled=>0, scope=>"author", filter=>"hours" }, { name=>"kickoff time", description=>"Total work hours used per individual for the kickoff meeting and for planning of the kickoff meeting.", scope=>"participant", enabled=>0, filter=>"hours" }, { name=>"planning time", description=>"Total work hours used to create the inspection master plan.", scope=>"participant", enabled=>1, filter=>"hours" }, # checking time { name=>"preparation time", description=>"The total time in hours spent prepare and review the topic.", scope=>"participant", enabled=>1, filter=>"hours" }, { name=>"lines studied", description=>"The number of lines which have been closly scrutinized at or near optimum checking rate.", scope=>"participant", enabled=>0, filter=>"count" }, { name=>"lines scanned", description=>"The number of lines which have been looked at higher then the optimum checking rate.", scope=>"participant", enabled=>0, filter=>"count" }, { name=>"studied time", description=>"The time in hours spent closely scrutinized at or near optimum checking rate.", scope=>"participant", enabled=>0, filter=>"hours" }, { name=>"scanned time", description=>"The time in hours spent looking at the topic at higher then the optimum checking rate.", scope=>"participant", enabled=>0, filter=>"hours" }, # logging meeting time. { name=>"meeting duration", description=>"The total time in hours of the review meeting.", scope=>"topic", enabled=>1, filter=>"hours" }, { name=>"logging meeting logging duration", description=>"The wall clock time spent reporting issues and searching for new issues.", scope=>"topic", enabled=>0, filter=>"hours" }, { name=>"logging meeting discussion duration", description=>"The wall clock time spent not reporting issues and searching for new issues.", scope=>"topic", enabled=>0, filter=>"hours" }, { name=>"logging meeting logging time", description=>"The total time spent reporting issues and searching for new issues.", scope=>"participant", enabled=>0, filter=>"hours" }, { name=>"logging meeting discussion time", description=>"The total time spent not reporting issues and searching for new issues.", scope=>"participant", enabled=>0, filter=>"hours" }, , { name=>"logging meeting new issues logged", description=>"The total number of issues that were not noted before the meeting and found during the meeting.", scope=>"topic", enabled=>0, filter=>"count" }, # editing { name=>"rework time", description=>"The total time in hours spent rework all items.", scope=>"author", enabled=>1, filter=>"hours" }, { name=>"follow up time", description=>"The total time in hours spent by the leader to check exit criteria and do exit activities.", scope=>"reviewer", enabled=>1, filter=>"hours" }, { name=>"exit time", description=>"The total time spent by the leader to check exit criteria and do exit activities.", scope=>"author", enabled=>0, filter=>"hours" }, { name=>"correct fix rate", description=>"The percentage of edit corrections attempts with correct fix a defect and not introduce new defects.", scope=>"author", enabled=>0, filter=>"percent" }, ); # Return the schema for the codestriker metric support. It insures that the # settings in the conf file are applied to the schema. sub get_metric_schema { # Make each of the metrics schema's are enabled according to the .conf file. foreach my $metric (@metrics_schema) { if ((! defined $metric_config) || $metric_config eq "" || $metric_config eq "none") { $metric->{enabled} = 0; } elsif ($metric_config eq "basic") { # Leave the default enabled values. } elsif ($metric_config eq "all") { $metric->{enabled} = 1; } else { # Make sure it matches the entire thing. my $regex = "(^|,)$metric->{name}(,|\$)"; if ($metric_config =~ /$regex/) { $metric->{enabled} = 1; } else { $metric->{enabled} = 0; } } # This metric is not a "built it" metric. Meaning that it # comes out of the db, rather than being generated on the fly # from other parts of the db (like the topic history). $metric->{builtin} = 0; } return @metrics_schema; } # Initialise codestriker, by loading up the configuration file and exporting # those values to the rest of the system. sub initialise($$) { my ($type, $basedir) = @_; $BASEDIR = $basedir; # Load up the configuration file. my $config = "$BASEDIR/codestriker.conf"; if (-f $config) { do $config; } else { die("Couldn't find configuration file: \"$config\".\n
" . "Please fix the \$config setting in codestriker.pl."); } # look for the extra file for the test scripts. if ( -f "$BASEDIR/codestriker_test.conf") { do "$BASEDIR/codestriker_test.conf"; } # Fill in $repository_name_map for those repository entries which don't have # a mapping, with the same value as the repository value itself. foreach my $repository (@valid_repositories) { if (! exists $repository_name_map->{$repository}) { $repository_name_map->{$repository} = $repository; } } # Define the equivalent list of valid repository names. @valid_repository_names = (); foreach my $repository (@valid_repositories) { push @valid_repository_names, $repository_name_map->{$repository}; } # Define the reverse mapping now for convenience. foreach my $key (keys %${repository_name_map}) { $repository_url_map->{$repository_name_map->{$key}} = $key; } # Define the default lists of topic states # Which will appear on the default project page @default_states_indexes = (); if (defined @default_topic_states && defined @topic_states) { for (my $indx = 0; $indx < @default_topic_states; $indx++) { for (my $i = 0; $i < @topic_states; $i++) { if ($default_topic_states[$indx] eq $topic_states[$i]) { push(@default_states_indexes, $i); last; } } } } else { @default_states_indexes = (0); } } # Determine if we are running under Windows. sub is_windows() { my $osname = $Config{'osname'}; return defined $osname && $osname eq "MSWin32"; } # Returns the current time in a format suitable for a DBI timestamp value. sub get_timestamp($$) { my ($type, $time) = @_; my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($time); $year += 1900; return sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year, $mon+1, $mday, $hour, $min, $sec); } # Given a database formatted timestamp, output it in a human-readable form. sub format_timestamp($$) { my ($type, $timestamp) = @_; if ($timestamp =~ /(\d\d\d\d)\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)/ || $timestamp =~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/) { my $time_value = Time::Local::timelocal($6, $5, $4, $3, $2-1, $1); my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($time_value); $year += 1900; return sprintf("$Codestriker::days[$wday] " . "$Codestriker::months[$mon] $mday, $year %02d:%02d:%02d ", $hour, $min, $sec); } else { return $timestamp; } } # Given a database formatted timestamp, output it in a short, # human-readable form. sub format_short_timestamp($$) { my ($type, $timestamp) = @_; if ($timestamp =~ /(\d\d\d\d)\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)/ || $timestamp =~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/) { my $time_value = Time::Local::timelocal($6, $5, $4, $3, $2-1, $1); my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($time_value); $year += 1900; return sprintf("%02d:%02d:%02d $Codestriker::short_days[$wday], " . "$mday $Codestriker::short_months[$mon], $year", $hour, $min, $sec); } else { return $timestamp; } } # Given a database formatted timestamp, output it in a short, # human-readable date only form. sub format_date_timestamp($$) { my ($type, $timestamp) = @_; if ($timestamp =~ /(\d\d\d\d)\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)/ || $timestamp =~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/) { my $time_value = Time::Local::timelocal($6, $5, $4, $3, $2-1, $1); my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($time_value); $year += 1900; return "$Codestriker::short_days[$wday] $Codestriker::short_months[$mon] $mday, $year"; } else { return $timestamp; } } # Given a database formatted timestamp, return the time as a time_t. The # number of seconds since the baseline time of the system. sub convert_date_timestamp_time($$) { my ($type, $timestamp) = @_; if ($timestamp =~ /(\d\d\d\d)\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)/ || $timestamp =~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/) { return Time::Local::timelocal($6, $5, $4, $3, $2-1, $1); } else { print STDERR "Unable to convert timestamp \"$timestamp\" to time_t.\n"; return 0; } } # Given an email string, replace it in a non-SPAM friendly form. # sits@users.sf.net -> sits@us... sub make_antispam_email($$) { my ($type, $email) = @_; $email =~ s/([0-9A-Za-z\._]+@[0-9A-Za-z\._]{3})[0-9A-Za-z\._]+/$1\.\.\./g; return "$email"; } sub filter_email { my ($type, $email) = @_; if ($Codestriker::antispam_email) { $email = $type->make_antispam_email($email); } return $email; } # Pass in two collections of string, it will return the elements in # the string that were added and where removed. All 4 params are # references to lists. Mainly used to compare lists of reviewers and # cc. sub set_differences($$$$) { my ($list1_r, $list2_r, $added, $removed) = @_; my @list1 = sort @$list1_r; my @list2 = sort @$list2_r; my $new_index = 0; my $old_index = 0; while ($new_index < @list1 || $old_index < @list2) { my $r = 0; if ($new_index < @list1 && $old_index < @list2) { $r = $list1[$new_index] cmp $list2[$old_index]; } elsif ($new_index < @list1) { $r = -1; } else { $r = 1; } if ($r == 0) { ++$new_index; ++$old_index; } elsif ($r < 0) { push(@$added, $list1[$new_index]); ++$new_index; } else { push(@$removed, $list2[$old_index]); ++$old_index; } } } # Return true if project support has been enabled. sub projects_disabled { if (defined @Codestriker::project_states) { return $#Codestriker::project_states == -1; } elsif (defined $Codestriker::allow_projects) { # Support for older codestriker.conf files. if ($Codestriker::allow_projects) { $Codestriker::project_states = ('Open'); } return $Codestriker::allow_projects == 0; } else { # Don't support projects if none of the above are defined. return 1; } } # Return true if there is more than one state associated with a project. sub project_state_change_enabled { return $#Codestriker::project_states > 0; } # Returns true if the given topic is 'readonly', i.e. if the given topic # status is in the list of readonly_states in codestriker.conf. sub topic_readonly { my ($topic_state) = @_; if (defined @Codestriker::readonly_states) { return (grep /^$topic_state$/, @Codestriker::readonly_states); } else { # Backwards compatibility for older configs. return $topic_state eq "Open" ? 0 : 1; } } # Decode the passed in string into UTF8. sub decode_topic_text { my ($string) = @_; # Assume input text is set to UTF8 by default, unless # it has been explicitly over-ridden in the codestriker.conf file. if ((! defined $Codestriker::topic_text_encoding) || $Codestriker::topic_text_encoding eq '') { $Codestriker::topic_text_encoding = 'utf8'; } return decode($Codestriker::topic_text_encoding, $string); } # Function for running an external command, and handles the subtle # issues for different web servers environments. sub execute_command { my $stdout_fh = shift; my $stderr_fh = shift; my $command = shift; my @args = @_; # Write error messages to STDERR if the file handle for error messages # is not defined. $stderr_fh = \*STDERR unless defined $stderr_fh; my $command_tmpdir; eval { if (exists $ENV{'MOD_PERL'}) { # The open3() call doesn't work under mod_perl/apache2, # so create a command which stores the stdout and stderr # into temporary files. if (defined $Codestriker::tmpdir && $Codestriker::tmpdir ne "") { $command_tmpdir = tempdir(DIR => $Codestriker::tmpdir); } else { $command_tmpdir = tempdir(); } # Build up the command string with naive quoting. my $command_line = "\"$command\""; foreach my $arg (@args) { $command_line .= " \"$arg\""; } my $stdout_filename = "$command_tmpdir/stdout.txt"; my $stderr_filename = "$command_tmpdir/stderr.txt"; # Thankfully this works under Windows. my $system_line = "$command_line > \"$stdout_filename\" 2> \"$stderr_filename\""; system($system_line) == 0 || croak "Failed to execute $system_line: $!\n"; open(TMP_STDOUT, $stdout_filename); binmode TMP_STDOUT; while () { print $stdout_fh $_; } binmode TMP_STDERR; open(TMP_STDERR, $stderr_filename); while () { print $stderr_fh $_; } close TMP_STDOUT; close TMP_STDERR; } else { my $write_stdin_fh = new FileHandle; my $read_stdout_fh = new FileHandle; my $read_stderr_fh = new FileHandle; my $pid = open3($write_stdin_fh, $read_stdout_fh, $read_stderr_fh, $command, @args); # Ideally, we should use IO::Select, but that is broken on Win32. # This is not ideal, but read first from stdout. If that is empty, # then an error has occurred, and that can be read from stderr. my $buf = ""; while (read($read_stdout_fh, $buf, 16384)) { print $stdout_fh $buf; } while (read($read_stderr_fh, $buf, 16384)) { print $stderr_fh $buf; } # Wait for the process to terminate. waitpid($pid, 0); } }; my $exception = $@; # Make sure the temporary directory is removed if it was created. if (defined $command_tmpdir) { rmtree($command_tmpdir); } if ($exception) { croak("Command failed: $@\n", "$command " . join(' ', @args) . "\n", "Check your webserver error log for more information.\n"); } # Flush the output file handles. $stdout_fh->flush; $stderr_fh->flush; } 1;