Subversion Repositories DevTools

Rev

Rev 1300 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
1293 dpurdie 1
###############################################################################
2
# Codestriker: Copyright (c) 2001, 2002 David Sitsky.  All rights reserved.
3
# sits@users.sourceforge.net
4
#
5
# This program is free software; you can redistribute it and modify it under
6
# the terms of the GPL.
7
 
8
# Main package which contains a reference to all configuration variables.
9
 
10
package Codestriker;
11
 
12
use strict;
13
use Encode;
14
use Config;
15
 
16
use Time::Local;
17
use IPC::Open3;
18
use File::Temp qw/ tempdir /;
19
use File::Path;
20
use Fatal qw / open close waitpid /;
21
 
22
# Export codestriker.conf configuration variables.
23
use vars qw ( $mailhost $mailuser $mailpasswd $use_compression
24
	      $gzip $cvs $svn $ssh $p4 $vss $bugtracker
25
	      @valid_repositories $default_topic_create_mode $default_tabwidth
26
	      $file_reviewer $db $dbuser $dbpasswd $codestriker_css
27
	      $NORMAL_MODE $COLOURED_MODE $COLOURED_MONO_MODE @topic_states
28
	      $bug_db $bug_db_host $bug_db_name $bug_db_password $bug_db_user
29
	      $lxr_map $email_send_options $default_topic_br_mode
30
	      $allow_delete $allow_searchlist $default_file_to_view
31
              $allow_projects $antispam_email $VERSION $title $BASEDIR
32
	      $metric_config $tmpdir @metric_schema $comment_state_metrics
33
	      $project_states $rss_enabled
34
	      $repository_name_map $repository_url_map
35
	      @valid_repository_names $topic_text_encoding
36
          @default_topic_states @default_states_indexes
37
	      );
38
 
39
# Version of Codestriker.
40
$Codestriker::VERSION = "1.9.4";
41
 
42
# Default title to display on each Codestriker screen.
43
$Codestriker::title = "Codestriker $Codestriker::VERSION";
44
 
45
# The maximum size of a diff file to accept.  At the moment, this is 20Mb.
46
$Codestriker::DIFF_SIZE_LIMIT = 20000 * 1024;
47
 
48
# Indicate what base directory Codestriker is running in.  This may be set
49
# in cgi-bin/codestriker.pl, depending on the environment the script is
50
# running in.  By default, assume the script is running in the cgi-bin
51
# directory (this is not the case for Apache2 + mod_perl).
52
$Codestriker::BASEDIR = "..";
53
 
54
# Error codes.
55
$Codestriker::OK = 1;
56
$Codestriker::STALE_VERSION = 2;
57
$Codestriker::INVALID_TOPIC = 3;
58
$Codestriker::INVALID_PROJECT = 4;
59
$Codestriker::DUPLICATE_PROJECT_NAME = 5;
60
$Codestriker::UNSUPPORTED_OPERATION = 6;
61
$Codestriker::DIFF_TOO_BIG = 7;
62
$Codestriker::LISTENER_ABORT = 8;
63
 
64
# Revision number constants used in the filetable with special meanings.
65
$Codestriker::ADDED_REVISION = "1.0";
66
$Codestriker::REMOVED_REVISION = "0.0";
67
$Codestriker::PATCH_REVISION = "0.1";
68
 
69
# Participant type constants.
70
$Codestriker::PARTICIPANT_REVIEWER = 0;
71
$Codestriker::PARTICIPANT_CC = 1;
72
 
73
# Default email context to use.
74
$Codestriker::EMAIL_CONTEXT = 8;
75
 
76
# Valid comment states, the only one that is special is the submitted state.
77
$Codestriker::COMMENT_SUBMITTED = 0;
78
 
79
# Day strings
80
@Codestriker::days = ("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday",
81
		      "Friday", "Saturday");
82
 
83
# Month strings
84
@Codestriker::months = ("January", "February", "March", "April", "May", "June",
85
			"July", "August", "September", "October", "November",
86
			"December");
87
 
88
# Short day strings
89
@Codestriker::short_days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
90
 
91
# Short month strings
92
@Codestriker::short_months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun",
93
			      "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
94
 
95
$metric_config = "";
96
 
97
# name => The short name of the metric. This name will be used in the
98
# SQL table, in the data download, in the input tables, and perhaps in
99
# the .conf file.
100
#
101
# description => The long description of the item. Displayed as online help (?)
102
#
103
# enabled => If 1, the metrics are enabled by default in "basic"
104
# configs. Otherwise the $metric_config option on the .conf will
105
# override this.
106
#
107
# scope => This will be "topic", "reviewer", "author".  A "topic"
108
# metric that has a 1 to 1 relationship with the topic itself.  If it
109
# is not a topic metric, it is a kind of user metric. User metrics have
110
# a 1-1 relationship with each user in the topic. If the type is
111
# reviewer, it is only needed by a user that is a reviewer (but not
112
# author), of the topic. If the type is author, it is only needed by
113
# the author of the metric, and if it is participants, it is needed by
114
# all users regardless of the role.
115
#
116
# filter => The type of data being stored. "hours" or "count". Data
117
# will not be stored to the database if it does not pass the format
118
# expected for the filter type.
119
 
120
my @metrics_schema = 
121
( 
122
  # planning time
123
  {
124
  name=>"entry time",
125
  description=>"Work hours spent by the inspection leader to check that entry conditions are met, and to work towards meeting them.",
126
  enabled=>0,
127
  scope=>"author",
128
  filter=>"hours"
129
  },
130
  {
131
  name=>"kickoff time",
132
  description=>"Total work hours used per individual for the kickoff meeting and for planning of the kickoff meeting.",
133
  scope=>"participant",
134
  enabled=>0,
135
  filter=>"hours"
136
  },
137
  {
138
  name=>"planning time",
139
  description=>"Total work hours used to create the inspection master plan.",
140
  scope=>"participant",
141
  enabled=>1,
142
  filter=>"hours"
143
  },
144
 
145
  # checking time
146
  {
147
  name=>"preparation time",
148
  description=>"The total time in hours spent prepare and review the topic.",
149
  scope=>"participant",
150
  enabled=>1, 
151
  filter=>"hours"
152
  },
153
  {
154
  name=>"lines studied",
155
  description=>"The number of lines which have been closly scrutinized at or near optimum checking rate.",
156
  scope=>"participant",
157
  enabled=>0,
158
  filter=>"count"
159
  },
160
  {
161
  name=>"lines scanned",
162
  description=>"The number of lines which have been looked at higher then the optimum checking rate.",
163
  scope=>"participant",
164
  enabled=>0,
165
  filter=>"count"
166
  },
167
  {
168
  name=>"studied time",
169
  description=>"The time in hours spent closely scrutinized at or near optimum checking rate.",
170
  scope=>"participant",
171
  enabled=>0,
172
  filter=>"hours"
173
  },
174
  {
175
  name=>"scanned time",
176
  description=>"The time in hours spent looking at the topic at higher then the optimum checking rate.",
177
  scope=>"participant",
178
  enabled=>0,
179
  filter=>"hours"
180
  },
181
 
182
  # logging meeting time.
183
  {
184
  name=>"meeting duration",
185
  description=>"The total time in hours of the review meeting.",
186
  scope=>"topic",
187
  enabled=>1, 
188
  filter=>"hours"
189
  },
190
  {
191
  name=>"logging meeting logging duration",
192
  description=>"The wall clock time spent reporting issues and searching for new issues.",
193
  scope=>"topic",
194
  enabled=>0,
195
  filter=>"hours"
196
  },
197
  {
198
  name=>"logging meeting discussion duration",
199
  description=>"The wall clock time spent not reporting issues and searching for new issues.",
200
  scope=>"topic",
201
  enabled=>0,
202
  filter=>"hours"
203
  },
204
  {
205
  name=>"logging meeting logging time",
206
  description=>"The total time spent reporting issues and searching for new issues.",
207
  scope=>"participant",
208
  enabled=>0,
209
  filter=>"hours"
210
  },
211
  {
212
  name=>"logging meeting discussion time",
213
  description=>"The total time spent not reporting issues and searching for new issues.",
214
  scope=>"participant",
215
  enabled=>0,
216
  filter=>"hours"
217
  },
218
  ,
219
  {
220
  name=>"logging meeting new issues logged",
221
  description=>"The total number of issues that were not noted before the meeting and found during the meeting.",
222
  scope=>"topic",
223
  enabled=>0,
224
  filter=>"count"
225
  },
226
 
227
  # editing
228
 
229
  {
230
  name=>"rework time",
231
  description=>"The total time in hours spent rework all items.",
232
  scope=>"author",
233
  enabled=>1,
234
  filter=>"hours"
235
  },
236
 
237
  {
238
  name=>"follow up time",
239
  description=>"The total time in hours spent by the leader to check exit criteria and do exit activities.",
240
  scope=>"reviewer",
241
  enabled=>1,
242
  filter=>"hours"
243
  },
244
 
245
  {
246
  name=>"exit time",
247
  description=>"The total time spent by the leader to check exit criteria and do exit activities.",
248
  scope=>"author",
249
  enabled=>0,
250
  filter=>"hours"
251
  },
252
 
253
  {
254
  name=>"correct fix rate",
255
  description=>"The percentage of edit corrections attempts with correct fix a defect and not introduce new defects.",
256
  scope=>"author",
257
  enabled=>0,
258
  filter=>"percent"
259
  },
260
 
261
);
262
 
263
# Return the schema for the codestriker metric support. It insures that the 
264
# settings in the conf file are applied to the schema.
265
sub get_metric_schema {
266
 
267
    # Make each of the metrics schema's are enabled according to the .conf file.
268
    foreach my $metric (@metrics_schema) {
269
	if ((! defined $metric_config) || $metric_config eq "" ||
270
	    $metric_config eq "none") {
271
	    $metric->{enabled} = 0;	
272
	}
273
	elsif ($metric_config eq "basic") {
274
	    # Leave the default enabled values.
275
	}
276
	elsif ($metric_config eq "all") {
277
	    $metric->{enabled} = 1;	
278
	}
279
	else {
280
	    # Make sure it matches the entire thing.
281
	    my $regex = "(^|,)$metric->{name}(,|\$)";
282
 
283
	    if ($metric_config =~ /$regex/) {
284
		$metric->{enabled} = 1;	
285
	    }
286
	    else {
287
		$metric->{enabled} = 0;
288
	    }
289
	}
290
 
291
	# This metric is not a "built it" metric. Meaning that it 
292
	# comes out of the db, rather than being generated on the fly
293
	# from other parts of the db (like the topic history).
294
	$metric->{builtin} = 0;
295
    }
296
 
297
    return @metrics_schema;
298
}
299
 
300
# Initialise codestriker, by loading up the configuration file and exporting
301
# those values to the rest of the system.
302
sub initialise($$) {
303
    my ($type, $basedir) = @_;
304
 
305
    $BASEDIR = $basedir;
306
 
307
    # Load up the configuration file.
308
    my $config = "$BASEDIR/codestriker.conf";
309
    if (-f $config) {
310
	do $config;
311
    } else {
312
	die("Couldn't find configuration file: \"$config\".\n<BR>" .
313
	    "Please fix the \$config setting in codestriker.pl.");
314
    }
315
 
316
    # look for the extra file for the test scripts.
317
    if ( -f "$BASEDIR/codestriker_test.conf")
318
    {
319
	do "$BASEDIR/codestriker_test.conf";
320
    }
321
 
322
    # Fill in $repository_name_map for those repository entries which don't have
323
    # a mapping, with the same value as the repository value itself.
324
    foreach my $repository (@valid_repositories) {
325
	if (! exists $repository_name_map->{$repository}) {
326
	    $repository_name_map->{$repository} = $repository;
327
	}
328
    }
329
 
330
    # Define the equivalent list of valid repository names.
331
    @valid_repository_names = ();
332
    foreach my $repository (@valid_repositories) {
333
	push @valid_repository_names, $repository_name_map->{$repository};
334
    }
335
 
336
    # Define the reverse mapping now for convenience.
337
    foreach my $key (keys %${repository_name_map}) {
338
	$repository_url_map->{$repository_name_map->{$key}} = $key;
339
    }
340
 
341
    # Define the default lists of topic states
342
    # Which will appear on the default project page
343
    @default_states_indexes = ();
344
    if (defined @default_topic_states && defined @topic_states) {
345
        for (my $indx = 0; $indx < @default_topic_states; $indx++) {
346
            for (my $i = 0; $i < @topic_states; $i++) {
347
                if ($default_topic_states[$indx] eq $topic_states[$i]) {
348
                    push(@default_states_indexes, $i);
349
                    last;
350
                }
351
            }
352
        }
353
    } else {
354
        @default_states_indexes = (0);
355
    }
356
}
357
 
358
# Determine if we are running under Windows.
359
sub is_windows() {
360
    my $osname = $Config{'osname'};
361
    return defined $osname && $osname eq "MSWin32";
362
}
363
 
364
# Returns the current time in a format suitable for a DBI timestamp value.
365
sub get_timestamp($$) {
366
    my ($type, $time) = @_;
367
    my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
368
	localtime($time);
369
    $year += 1900;
370
 
371
    return sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year, $mon+1, $mday,
372
		   $hour, $min, $sec);
373
}
374
 
375
# Given a database formatted timestamp, output it in a human-readable form.
376
sub format_timestamp($$) {
377
    my ($type, $timestamp) = @_;
378
 
379
    if ($timestamp =~ /(\d\d\d\d)\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)/ ||
380
	$timestamp =~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/) {
381
	my $time_value = Time::Local::timelocal($6, $5, $4, $3, $2-1, $1);
382
	my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
383
	    localtime($time_value);
384
	$year += 1900;
385
	return sprintf("$Codestriker::days[$wday] " .
386
		       "$Codestriker::months[$mon] $mday, $year %02d:%02d:%02d ",
387
		       $hour, $min, $sec);
388
    } else {
389
	return $timestamp;
390
    }
391
}
392
 
393
# Given a database formatted timestamp, output it in a short,
394
# human-readable form.
395
sub format_short_timestamp($$) {
396
    my ($type, $timestamp) = @_;
397
 
398
    if ($timestamp =~ /(\d\d\d\d)\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)/ ||
399
	$timestamp =~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/) {
400
	my $time_value = Time::Local::timelocal($6, $5, $4, $3, $2-1, $1);
401
	my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
402
	    localtime($time_value);
403
	$year += 1900;
404
	return sprintf("%02d:%02d:%02d $Codestriker::short_days[$wday], " .
405
		       "$mday $Codestriker::short_months[$mon], $year",
406
		       $hour, $min, $sec);
407
    } else {
408
	return $timestamp;
409
    }
410
}
411
 
412
# Given a database formatted timestamp, output it in a short,
413
# human-readable date only form.
414
sub format_date_timestamp($$) {
415
    my ($type, $timestamp) = @_;
416
 
417
    if ($timestamp =~ /(\d\d\d\d)\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)/ ||
418
	$timestamp =~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/) {
419
	my $time_value = Time::Local::timelocal($6, $5, $4, $3, $2-1, $1);
420
	my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =
421
	    localtime($time_value);
422
	$year += 1900;
423
	return "$Codestriker::short_days[$wday] $Codestriker::short_months[$mon] $mday, $year";
424
 
425
    } else {
426
	return $timestamp;
427
    }
428
}
429
 
430
# Given a database formatted timestamp, return the time as a time_t. The
431
# number of seconds since the baseline time of the system.
432
sub convert_date_timestamp_time($$) {
433
    my ($type, $timestamp) = @_;
434
 
435
    if ($timestamp =~ /(\d\d\d\d)\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)/ ||
436
	$timestamp =~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/) {
437
	return Time::Local::timelocal($6, $5, $4, $3, $2-1, $1);
438
 
439
    } else {
440
	print STDERR "Unable to convert timestamp \"$timestamp\" to time_t.\n";
441
	return 0;
442
    }
443
}
444
 
445
 
446
# Given an email string, replace it in a non-SPAM friendly form.
447
# sits@users.sf.net -> sits@us...
448
sub make_antispam_email($$) {
449
    my ($type, $email) = @_;
450
 
451
    $email =~ s/([0-9A-Za-z\._]+@[0-9A-Za-z\._]{3})[0-9A-Za-z\._]+/$1\.\.\./g;
452
    return "$email";
453
}
454
 
455
sub filter_email {
456
    my ($type, $email) = @_;
457
 
458
    if ($Codestriker::antispam_email) {
459
	$email = $type->make_antispam_email($email);
460
    }
461
 
462
    return $email;
463
}
464
 
465
# Pass in two collections of string, it will return the elements in
466
# the string that were added and where removed. All 4 params are
467
# references to lists. Mainly used to compare lists of reviewers and
468
# cc.
469
sub set_differences($$$$)
470
{
471
    my ($list1_r, $list2_r, $added, $removed) = @_;
472
 
473
    my @list1 = sort @$list1_r;
474
    my @list2 = sort @$list2_r;
475
 
476
    my $new_index = 0;
477
    my $old_index = 0;
478
    while ($new_index < @list1 || $old_index < @list2) {
479
        my $r = 0;
480
 
481
        if ($new_index < @list1 && $old_index < @list2) {
482
	    $r = $list1[$new_index] cmp $list2[$old_index];
483
        }
484
        elsif ($new_index < @list1) {
485
	    $r = -1;
486
        }
487
        else {
488
	    $r = 1;
489
        }
490
 
491
        if ($r == 0) {
492
	    ++$new_index;
493
	    ++$old_index;
494
 
495
        }
496
        elsif ($r < 0) {
497
	    push(@$added, $list1[$new_index]);
498
	    ++$new_index;
499
        }
500
        else {
501
	    push(@$removed, $list2[$old_index]);
502
	    ++$old_index;
503
        }
504
    }
505
}
506
 
507
# Return true if project support has been enabled.
508
sub projects_disabled {
509
    if (defined @Codestriker::project_states) {
510
	return $#Codestriker::project_states == -1;
511
    } elsif (defined $Codestriker::allow_projects) {
512
	# Support for older codestriker.conf files.
513
	if ($Codestriker::allow_projects) {
514
	    $Codestriker::project_states = ('Open');
515
	}
516
	return $Codestriker::allow_projects == 0;
517
    } else {
518
	# Don't support projects if none of the above are defined.
519
	return 1;
520
    }
521
}
522
 
523
# Return true if there is more than one state associated with a project.
524
sub project_state_change_enabled {
525
    return $#Codestriker::project_states > 0;
526
}
527
 
528
# Returns true if the given topic is 'readonly', i.e. if the given topic
529
# status is in the list of readonly_states in codestriker.conf.
530
sub topic_readonly {
531
    my ($topic_state) = @_;
532
    if (defined @Codestriker::readonly_states) {
533
	return (grep /^$topic_state$/, @Codestriker::readonly_states);
534
    } else {
535
	# Backwards compatibility for older configs.
536
        return $topic_state eq "Open" ? 0 : 1;
537
    }
538
}
539
 
540
# Decode the passed in string into UTF8.
541
sub decode_topic_text {
542
    my ($string) = @_;
543
 
544
    # Assume input text is set to UTF8 by default, unless
545
    # it has been explicitly over-ridden in the codestriker.conf file.
546
    if ((! defined $Codestriker::topic_text_encoding) ||
547
	$Codestriker::topic_text_encoding eq '') {
548
	$Codestriker::topic_text_encoding = 'utf8';
549
    }
550
 
551
    return decode($Codestriker::topic_text_encoding, $string);
552
}
553
 
554
# Function for running an external command, and handles the subtle
555
# issues for different web servers environments.
556
sub execute_command {
557
    my $stdout_fh = shift;
558
    my $stderr_fh = shift;
559
    my $command = shift;
560
    my @args = @_;
561
 
562
    # Write error messages to STDERR if the file handle for error messages
563
    # is not defined.
564
    $stderr_fh = \*STDERR unless defined $stderr_fh;
565
 
566
    my $command_tmpdir;
567
    eval {
568
	if (exists $ENV{'MOD_PERL'}) {
569
	    # The open3() call doesn't work under mod_perl/apache2,
570
	    # so create a command which stores the stdout and stderr
571
	    # into temporary files.
572
	    if (defined $Codestriker::tmpdir && $Codestriker::tmpdir ne "") {
573
		$command_tmpdir = tempdir(DIR => $Codestriker::tmpdir);
574
	    } else {
575
		$command_tmpdir = tempdir();
576
	    }
577
	    # Build up the command string with naive quoting.
578
	    my $command_line = "\"$command\"";
579
	    foreach my $arg (@args) {
580
		$command_line .= " \"$arg\"";
581
	    }
582
 
583
	    my $stdout_filename = "$command_tmpdir/stdout.txt";
584
	    my $stderr_filename = "$command_tmpdir/stderr.txt";
585
 
586
	    # Thankfully this works under Windows.
587
	    my $system_line =
588
		"$command_line > \"$stdout_filename\" 2> \"$stderr_filename\"";
589
	    system($system_line) == 0 ||
590
		croak "Failed to execute $system_line: $!\n";
591
 
592
	    open(TMP_STDOUT, $stdout_filename);
593
	    binmode TMP_STDOUT;
594
	    while (<TMP_STDOUT>) {
595
		print $stdout_fh $_;
596
	    }
597
	    binmode TMP_STDERR;
598
 
599
	    open(TMP_STDERR, $stderr_filename);
600
	    while (<TMP_STDERR>) {
601
		print $stderr_fh $_;
602
	    }
603
 
604
	    close TMP_STDOUT;
605
	    close TMP_STDERR;
606
	} else {
607
	    my $write_stdin_fh = new FileHandle;
608
	    my $read_stdout_fh = new FileHandle;
609
	    my $read_stderr_fh = new FileHandle;
610
 
611
	    my $pid = open3($write_stdin_fh, $read_stdout_fh, $read_stderr_fh,
612
			    $command, @args);
613
 
614
	    # Ideally, we should use IO::Select, but that is broken on Win32.
615
	    # This is not ideal, but read first from stdout.  If that is empty,
616
	    # then an error has occurred, and that can be read from stderr.
617
	    my $buf = "";
618
	    while (read($read_stdout_fh, $buf, 16384)) {
619
		print $stdout_fh $buf;
620
	    }
621
	    while (read($read_stderr_fh, $buf, 16384)) {
622
		print $stderr_fh $buf;
623
	    }
624
 
625
	    # Wait for the process to terminate.
626
	    waitpid($pid, 0);
627
	}
628
    };
629
    my $exception = $@;
630
 
631
    # Make sure the temporary directory is removed if it was created.
632
    if (defined $command_tmpdir) {
633
	rmtree($command_tmpdir);
634
    }
635
 
636
    if ($exception) {
637
	croak("Command failed: $@\n",
638
	      "$command " . join(' ', @args) . "\n",
639
	      "Check your webserver error log for more information.\n");
640
    }
641
 
642
    # Flush the output file handles.
643
    $stdout_fh->flush;
644
    $stderr_fh->flush;
645
}
646
 
647
1;
648