Rev 1300 | Blame | Compare with Previous | Last modification | View Log | RSS feed
################################################################################ 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<BR>" ."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 (<TMP_STDOUT>) {print $stdout_fh $_;}binmode TMP_STDERR;open(TMP_STDERR, $stderr_filename);while (<TMP_STDERR>) {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;