Subversion Repositories DevTools

Rev

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

Rev Author Line No. Line
1293 dpurdie 1
#!/usr/bin/perl -w
2
 
3
###############################################################################
4
# Codestriker: Copyright (c) 2001, 2002 David Sitsky.  All rights reserved.
5
# sits@users.sourceforge.net
6
#
7
# This program is free software; you can redistribute it and modify it under
8
# the terms of the GPL.
9
 
10
# This script used to be known as checksetup.pl, but was renamed to install.pl
11
# as it was confusing to new users installing the product the first time.
12
#
13
# This script is similar to bugzilla's checksetup.pl.  It can be run whenever
14
# you like, but ideally should be done after every upgrade.  Currently the
15
# module does the following:
16
#
17
# - check for the required perl modules
18
# - creates the database "codestriker" if the database does not exist
19
# - creates the tables inside the database if they don't exist
20
# - authomatically changes the table definitions of older codestriker
21
#   installations, and does data migration automatically.
22
 
23
use strict;
24
use Config;
25
use lib '../lib';
26
 
27
require 5.8.0;
28
 
29
# Now load up the required modules.  Do this is a lazy fashion so that Perl
30
# doesn't try to grab this during compile time, otherwise nasty-looking
31
# error messages will appear to the user.
32
eval("use Cwd");
33
eval("use CPAN");
34
eval("use File::Path");
35
eval("use Codestriker");
36
eval("use Codestriker::DB::Database");
37
eval("use Codestriker::DB::Column");
38
eval("use Codestriker::DB::Table");
39
eval("use Codestriker::DB::Index");
40
eval("use Codestriker::Action::SubmitComment");
41
eval("use Codestriker::Repository::RepositoryFactory");
42
eval("use Codestriker::FileParser::Parser");
43
eval("use Codestriker::FileParser::UnknownFormat");
44
eval("use Codestriker::Model::File");
45
 
46
# Set this variables, to avoid compilation warnings below.
47
$Codestriker::COMMENT_SUBMITTED = 0;
48
@Codestriker::valid_repositories = ();
49
 
50
# Initialise Codestriker, load up the configuration file.
51
Codestriker->initialise(cwd() . '/..');
52
 
53
# Make sure the $db configuration variable has been set, and if not
54
# complain and exit.
55
if (! defined $Codestriker::db) {
56
    print STDERR
57
	"The database configuration variable \$db has not been set.\n";
58
    print STDERR
59
	"Please edit the codestriker.conf file and run this command again.\n";
60
    exit -1;
61
}
62
 
63
# Indicate which modules are required for codestriker (this code is
64
# completely stolen more-or-less verbatim from Bugzilla)
65
my $modules = [ 
66
    { 
67
        name => 'LWP::UserAgent', 
68
        version => '0' 
69
    }, 
70
    { 
71
        name => 'CGI', 
72
        version => '2.56' 
73
    }, 
74
    { 
75
        name => 'Net::SMTP', 
76
        version => '0' 
77
    }, 
78
    {
79
	name => 'MIME::QuotedPrint',
80
	version => '2.14'
81
    },
82
    { 
83
        name => 'DBI', 
84
        version => '1.13' 
85
    }, 
86
    { 
87
        name => 'Template', 
88
        version => '2.07' 
89
    },
90
    { 
91
        name => 'HTML::Entities', 
92
        version => '0' 
93
    },
94
    { 
95
        name => 'File::Temp', 
96
        version => '0' 
97
    },
98
    { 
99
        name => 'XML::RSS', 
100
        version => '1.05',
101
        optional => 1
102
    },
103
    { 
104
        name => 'Encode::Byte', 
105
        version => '0',
106
        optional => 0
107
    },
108
    { 
109
        name => 'Encode::Unicode', 
110
        version => '0',
111
        optional => 0
112
    },
113
    { 
114
        name => 'Authen::SASL', 
115
        version => '0',
116
        optional => 0
117
    }
118
];
119
 
120
# Retrieve the database module dependencies.  Put this in an eval block to
121
# handle the case where the user hasn't installed the DBI module yet,
122
# which prevents the following code from running.
123
my $database = undef;
124
eval {
125
    $database = Codestriker::DB::Database->get_database();
126
    push @{$modules}, $database->get_module_dependencies();
127
};
128
 
129
# Check for various character encoding modules that are required.
130
if (defined $Codestriker::topic_text_encoding) {
131
    if ($Codestriker::topic_text_encoding =~ /euc\-cn|gb2312|hz|gbk/) {
132
	push @{$modules}, { name => 'Encode::CN', version => '0' };
133
    }
134
    if ($Codestriker::topic_text_encoding =~ /jp|jis/) {
135
	push @{$modules}, { name => 'Encode::JP', version => '0' };
136
    }
137
    if ($Codestriker::topic_text_encoding =~ /kr|johab/) {
138
	push @{$modules}, { name => 'Encode::KR', version => '0' };
139
    }
140
    if ($Codestriker::topic_text_encoding =~ /big5/) {
141
	push @{$modules}, { name => 'Encode::TW', version => '0' };
142
    }
143
}
144
 
145
# Check if the ClearCase::CtCmd module is required by checking if a
146
# ClearCaseDynamic repository is defined.
147
if (grep(/^clearcase:dyn/, @Codestriker::valid_repositories)) {
148
    push @{$modules}, { name => 'ClearCase::CtCmd', version => '0' };
149
}
150
 
151
my %missing_optional = ();
152
my %missing = ();
153
foreach my $module (@{$modules}) {
154
 
155
    my $optional = exists($module->{optional}) && $module->{optional};
156
 
157
    unless (have_vers($module->{name}, $module->{version},$optional)) { 
158
        if ( $optional == 0) {
159
        $missing{$module->{name}} = $module->{version};
160
    }
161
        else {
162
            $missing_optional{$module->{name}} = $module->{version};
163
        }
164
    }
165
}
166
 
167
# vers_cmp is adapted from Sort::Versions 1.3 1996/07/11 13:37:00 kjahds,
168
# which is not included with Perl by default, hence the need to copy it here.
169
# Seems silly to require it when this is the only place we need it...
170
sub vers_cmp {
171
  if (@_ < 2) { die "not enough parameters for vers_cmp" }
172
  if (@_ > 2) { die "too many parameters for vers_cmp" }
173
  my ($a, $b) = @_;
174
  my (@A) = ($a =~ /(\.|\d+|[^\.\d]+)/g);
175
  my (@B) = ($b =~ /(\.|\d+|[^\.\d]+)/g);
176
  my ($A,$B);
177
  while (@A and @B) {
178
    $A = shift @A;
179
    $B = shift @B;
180
    if ($A eq "." and $B eq ".") {
181
      next;
182
    } elsif ( $A eq "." ) {
183
      return -1;
184
    } elsif ( $B eq "." ) {
185
      return 1;
186
    } elsif ($A =~ /^\d+$/ and $B =~ /^\d+$/) {
187
      return $A <=> $B if $A <=> $B;
188
    } else {
189
      $A = uc $A;
190
      $B = uc $B;
191
      return $A cmp $B if $A cmp $B;
192
    }
193
  }
194
  @A <=> @B;
195
}
196
 
197
# This was originally clipped from the libnet Makefile.PL, adapted here to
198
# use the above vers_cmp routine for accurate version checking.
199
sub have_vers {
200
  my ($pkg, $wanted, $optional) = @_;
201
  my ($msg, $vnum, $vstr);
202
  no strict 'refs';
203
  printf("Checking for %15s %-9s ", $pkg, !$wanted?'(any)':"(v$wanted)");
204
 
205
  eval { my $p; ($p = $pkg . ".pm") =~ s!::!/!g; require $p; };
206
 
207
  $vnum = ${"${pkg}::VERSION"} || ${"${pkg}::Version"} || 0;
208
  $vnum = -1 if $@;
209
 
210
  if ($vnum eq "-1") { # string compare just in case it's non-numeric
211
    if ( $optional ) {
212
        $vstr = "ok: not found, optional";
213
    } 
214
    else {
215
        $vstr = "    not found";
216
    }
217
 
218
  }
219
  elsif (vers_cmp($vnum,"0") > -1) {
220
    $vstr = "found v$vnum";
221
  }
222
  else {
223
    $vstr = "found unknown version";
224
  }
225
 
226
  my $vok = (vers_cmp($vnum,$wanted) > -1);
227
  print ((($vok) ? "ok: " : " "), "$vstr\n");
228
  return $vok;
229
}
230
 
231
# Determine if this process is running under Windows, as the installation
232
# process is slightly different.
233
my $osname = $Config{'osname'};
234
my $windows = (defined $osname && $osname eq "MSWin32") ? 1 : 0;
235
 
236
# Output any modules which may be missing.
237
if (%missing) {
238
 
239
    # First, output the generic "missing module" message.
240
    print "\n\n";
241
    print "Codestriker requires some Perl modules which are either missing\n" .
242
	  "from your system, or the version on your system is too old.\n";
243
 
244
    if ($windows) {
245
	foreach my $module (keys %missing) {
246
	    print " Missing \"$module\"\n";
247
	    if ($missing{$module} > 0) {
248
		print "   Minimum version required: $missing{$module}\n";
249
	    }
250
	}
251
 
252
	print <<EOF;
253
 
254
These can be installed by doing the following in PPM:
255
 
256
C:\> ppm
257
 
258
C:\> ppm
259
PPM> rep add theory http://theoryx5.uwinnipeg.ca/cgi-bin/ppmserver?urn:/PPMServer58
260
PPM> install (package-name)
261
 
262
*NOTE* The Template package name may not be "Template" but "Template-Toolkit"
263
when entering the commands above.
264
 
265
Go to http://theoryx5.uwinnipeg.ca/ppms if you have any installation problems.
266
Other Win32 ppm repositories are listed there.
267
 
268
The ActiveState default repository in PPM has almost all of the packages
269
required.
270
EOF
271
    }
272
    else {
273
	print "They can be installed by running (as root) the following:\n";
274
	foreach my $module (keys %missing) {
275
	    print "   perl -MCPAN -e 'install \"$module\"'\n";
276
	    if ($missing{$module} > 0) {
277
		print "   Minimum version required: $missing{$module}\n";
278
	    }
279
	}
280
	print "\n";
281
	print "Modules can also be downloaded from http://www.cpan.org.\n\n";
282
    }
283
 
284
    if ($windows) {
285
	# Need to find out how to do automatic installs with PPM.
286
	exit -1;
287
    }
288
 
289
    # Check we are running as root so the Perl modules can be properly
290
    # installed.
291
    print "\n";
292
 
293
    if ($< != 0) {
294
        print "Execute this script as root so I can install these modules ";
295
        print "automatically.\n\n";
296
        exit -1;
297
    }
298
 
299
    print "Shall I try to download and install these modules for you? (y/n): ";
300
    flush STDOUT;
301
 
302
    my $answer = <STDIN>;
303
    chop $answer;
304
    if ($answer =~ /^y/i) {
305
	# Try to install the modules using CPAN.
306
	foreach my $module (keys %missing) {
307
	    my $obj = CPAN::Shell->expand('Module', $module);
308
 
309
	    if (! $obj->install) {
310
		print STDERR "\n\nFailed to install module: $module.\n";
311
		print STDERR "Try to install this module manually, " .
312
		    "and run this script again.\n\n";
313
		exit(1);
314
	    }
315
	}
316
 
317
    } else {
318
	# User decided to bail out.
319
	exit -1;
320
    }
321
}
322
 
323
 
324
# Obtain a database connection.
325
my $dbh = $database->get_connection();
326
 
327
# Convenience methods and variables for creating table objects.
328
my $TEXT = $Codestriker::DB::Column::TYPE->{TEXT};
329
my $VARCHAR = $Codestriker::DB::Column::TYPE->{VARCHAR};
330
my $INT32 = $Codestriker::DB::Column::TYPE->{INT32};
331
my $INT16 = $Codestriker::DB::Column::TYPE->{INT16};
332
my $DATETIME = $Codestriker::DB::Column::TYPE->{DATETIME};
333
my $FLOAT = $Codestriker::DB::Column::TYPE->{FLOAT};
334
sub col { return Codestriker::DB::Column->new(@_); }
335
sub dbindex { return Codestriker::DB::Index->new(@_); }
336
sub table { return Codestriker::DB::Table->new(@_); }
337
 
338
# The topic table.
339
my $topic_table =
340
  table(name => "topic",
341
	columns => [col(name=>"id", type=>$INT32, pk=>1),
342
		    col(name=>"author", type=>$VARCHAR, length=>200),
343
		    col(name=>"title", type=>$VARCHAR, length=>255),
344
		    col(name=>"description", type=>$TEXT),
345
		    col(name=>"document", type=>$TEXT),
346
		    col(name=>"state", type=>$INT16),
347
		    col(name=>"creation_ts", type=>$DATETIME),
348
		    col(name=>"modified_ts", type=>$DATETIME),
349
		    col(name=>"version", type=>$INT32),
350
		    col(name=>"start_tag", type=>$TEXT, mandatory=>0),
351
		    col(name=>"end_tag", type=>$TEXT, mandatory=>0),
352
		    col(name=>"module", type=>$TEXT, mandatory=>0),
353
		    col(name=>"repository", type=>$TEXT, mandatory=>0),
354
		    col(name=>"projectid", type=>$INT32)
355
		   ],
356
	indexes => [dbindex(name=>"author_idx", column_names=>["author"])]);
357
 
358
# The topichistory table.  Holds information relating to how a topic
359
# has changed over time.  Only changeable topic attributes are
360
# recorded in this table.
361
my $topichistory_table =
362
  table(name => "topichistory",
363
	columns => [col(name=>"topicid", type=>$INT32, pk=>1),
364
		    col(name=>"author", type=>$VARCHAR, length=>200),
365
		    col(name=>"title", type=>$VARCHAR, length=>255),
366
		    col(name=>"description", type=>$TEXT, length=>255),
367
		    col(name=>"state", type=>$INT16),
368
		    col(name=>"modified_ts", type=>$DATETIME),
369
		    col(name=>"version", type=>$INT32, pk=>1),
370
		    col(name=>"repository", type=>$TEXT, mandatory=>0),
371
		    col(name=>"projectid", type=>$INT32),
372
		    col(name=>"reviewers", type=>$TEXT),
373
		    col(name=>"cc", type=>$TEXT, mandatory=>0),
374
		    col(name=>"modified_by_user", type=>$VARCHAR, length=>200, mandatory=>0)
375
		   ],
376
	indexes => [dbindex(name=>"th_idx", column_names=>["topicid"])]);
377
 
378
# Holds information as to when a user viewed a topic.
379
my $topicviewhistory_table =
380
  table(name => "topicviewhistory",
381
	columns => [col(name=>"topicid", type=>$INT32),
382
		    col(name=>"email", type=>$VARCHAR, length=>200, mandatory=>0),
383
		    col(name=>"creation_ts", type=>$DATETIME)
384
		   ],
385
	indexes => [dbindex(name=>"tvh_idx", column_names=>["topicid"])]);
386
 
387
# Holds all of the metric data that is owned by a specific user on a specific 
388
# topic. One row per metric. Metric data that is left empty does not get a row.
389
my $topicusermetric_table =
390
  table(name => "topicusermetric",
391
	columns => [col(name=>"topicid", type=>$INT32, pk=>1),
392
		    col(name=>"email", type=>$VARCHAR, length=>200, pk=>1),
393
		    col(name=>"metric_name", type=>$VARCHAR, length=>80, pk=>1),
394
		    col(name=>"value", type=>$FLOAT)
395
		   ],
396
	indexes => [dbindex(name=>"tum_idx",
397
			    column_names=>["topicid", "email"])]);
398
 
399
# Holds all of the metric data that is owned by a specific topic. One row per 
400
# metric. Metric data that is empty does not get a row.
401
my $topicmetric_table =
402
  table(name => "topicmetric",
403
	columns => [col(name=>"topicid", type=>$INT32, pk=>1),
404
		    col(name=>"metric_name", type=>$VARCHAR, length=>80,pk=>1),
405
		    col(name=>"value", type=>$FLOAT)
406
		   ],
407
	indexes => [dbindex(name=>"tm_idx", column_names=>["topicid"])]);
408
 
409
# Holds record of which topics obsolete other topics, which is a many-to-many
410
# relationship.
411
my $topicobsolete_table =
412
  table(name => "topicobsolete",
413
	columns => [col(name=>"topicid", type=>$INT32, pk=>1),
414
		    col(name=>"obsoleted_by", type=>$INT32, pk=>1)
415
		   ],
416
	indexes => [dbindex(name=>"to1_idx", column_names=>["topicid"]),
417
		    dbindex(name=>"to2_idx", column_names=>["obsoleted_by"])]);
418
 
419
# Hold a specific datum of column data entered by a specific user for a
420
# specific line.
421
my $commentdata_table =
422
  table(name => "commentdata",
423
	columns => [col(name=>"commentstateid", type=>$INT32),
424
		    col(name=>"commentfield", type=>$TEXT),
425
		    col(name=>"author", type=>$VARCHAR, length=>200),
426
		    col(name=>"creation_ts", type=>$DATETIME)
427
		   ],
428
	indexes => [dbindex(name=>"comment_idx",
429
			    column_names=>["commentstateid"])]);
430
 
431
# Contains the state of a bunch of comments on a specific line of code.
432
my $commentstate_table =
433
  table(name => "commentstate",
434
	columns => [col(name=>"id", type=>$INT32, autoincr=>1, pk=>1),
435
		    col(name=>"topicid", type=>$INT32),
436
		    col(name=>"fileline", type=>$INT32),
437
		    col(name=>"filenumber", type=>$INT32),
438
		    col(name=>"filenew", type=>$INT16),
439
		    col(name=>"state", type=>$INT16),  # Not used, old field.
440
		    col(name=>"version", type=>$INT32),
441
		    col(name=>"creation_ts", type=>$DATETIME),
442
		    col(name=>"modified_ts", type=>$DATETIME)
443
		   ],
444
	indexes => [dbindex(name=>"commentstate_topicid_idx",
445
			    column_names=>["topicid"])]);
446
 
447
# Contains the metrics associated with a commentstate record.  This is
448
# configurable over time, so basic string data is stored into here.
449
my $commentstatemetric_table =
450
  table(name => "commentstatemetric",
451
	columns => [col(name=>"id", type=>$INT32, pk=>1),
452
		    col(name=>"name", type=>$VARCHAR, length=>80, pk=>1),
453
		    col(name=>"value", type=>$VARCHAR, length=>80)
454
		    ],
455
	indexes => [dbindex(name=>"csm_id_idx", column_names=>["id"]),
456
		    dbindex(name=>"csm_name_idx", column_names=>["name"])]);
457
 
458
# Holds information relating to how a commentstate has changed over time.
459
# Only changeable commentstate attributes are recorded in this table.
460
my $commentstatehistory_table =
461
  table(name => "commentstatehistory",
462
	columns => [col(name=>"id", type=>$INT32, pk=>1),
463
                    col(name=>"state", type=>$INT16),  # Not used, old field.
464
		    col(name=>"metric_name", type=>$VARCHAR, length=>80),
465
		    col(name=>"metric_value", type=>$VARCHAR, length=>80),
466
		    col(name=>"version", type=>$INT32, pk=>1),
467
		    col(name=>"modified_ts", type=>$DATETIME),
468
		    col(name=>"modified_by_user", type=>$VARCHAR, length=>200)
469
		    ]);
470
 
471
# Indicate what participants there are in a topic.
472
my $participant_table =
473
  table(name => "participant",
474
	columns => [col(name=>"topicid", type=>$INT32, pk=>1),
475
		    col(name=>"email", type=>$VARCHAR, length=>200, pk=>1),
476
		    col(name=>"type", type=>$INT16, pk=>1),
477
		    col(name=>"state", type=>$INT16),
478
		    col(name=>"modified_ts", type=>$DATETIME),
479
		    col(name=>"version", type=>$INT32)
480
		   ],
481
	indexes => [dbindex(name=>"participant_tid_idx",
482
			    column_names=>["topicid"])]);
483
 
484
# Indicate how bug records are related to topics.
485
my $topicbug_table =
486
  table(name => "topicbug",
487
	columns => [col(name=>"topicid", type=>$INT32, pk=>1),
488
		    col(name=>"bugid", type=>$INT32, pk=>1)
489
		   ],
490
	indexes => [dbindex(name=>"topicbug_tid_idx",
491
			    column_names=>["topicid"])]);
492
 
493
# This table records which file fragments are associated with a topic.
494
my $topicfile_table =
495
  table(name => "topicfile",
496
	columns => [col(name=>"topicid", type=>$INT32, pk=>1),
497
		    col(name=>"sequence", type=>$INT16, pk=>1),
498
		    col(name=>"filename", type=>$VARCHAR, length=>255),
499
		    col(name=>"topicoffset", type=>$INT32),
500
		    col(name=>"revision", type=>$VARCHAR, length=>255),
501
		    col(name=>"binaryfile", type=>$INT16),
502
		    col(name=>"diff", type=>$TEXT, mandatory=>0)
503
		   ],
504
	indexes => [dbindex(name=>"topicfile_tid_idx",
505
			    column_names=>["topicid"])]);
506
 
507
# This table records a specific "delta", which is a chunk of a diff file.
508
my $delta_table =
509
  table(name => "delta",
510
	columns => [col(name=>"topicid", type=>$INT32, pk=>1),
511
		    col(name=>"file_sequence", type=>$INT16),
512
		    col(name=>"delta_sequence", type=>$INT16, pk=>1),
513
		    col(name=>"old_linenumber", type=>$INT32),
514
		    col(name=>"new_linenumber", type=>$INT32),
515
		    col(name=>"deltatext", type=>$TEXT),
516
		    col(name=>"description", type=>$TEXT, mandatory=>0),
517
		    col(name=>"repmatch", type=>$INT16)
518
		   ],
519
	indexes => [dbindex(name=>"delta_fid_idx",
520
			    column_names=>["topicid"])]);
521
 
522
# This table records all projects in the system.
523
my $project_table =
524
  table(name => "project",
525
	columns => [col(name=>"id", type=>$INT32, pk=>1, autoincr=>1),
526
		    col(name=>"name", type=>$VARCHAR, length=>255),
527
		    col(name=>"description", type=>$TEXT),
528
		    col(name=>"creation_ts", type=>$DATETIME),
529
		    col(name=>"modified_ts", type=>$DATETIME),
530
		    col(name=>"version", type=>$INT32),
531
		    col(name=>"state", type=>$INT16)
532
		   ],
533
	indexes => [dbindex(name=>"project_name_idx",
534
			    column_names=>["name"])]);
535
 
536
# Add all of the Codestriker tables into an array.
537
my @tables = ();
538
push @tables, $topic_table;
539
push @tables, $topichistory_table;
540
push @tables, $topicviewhistory_table;
541
push @tables, $topicusermetric_table;
542
push @tables, $topicmetric_table;
543
push @tables, $topicobsolete_table;
544
push @tables, $commentdata_table;
545
push @tables, $commentstate_table;
546
push @tables, $commentstatemetric_table;
547
push @tables, $commentstatehistory_table;
548
push @tables, $participant_table;
549
push @tables, $topicbug_table;
550
push @tables, $topicfile_table;
551
push @tables, $delta_table;
552
push @tables, $project_table;
553
 
554
# Move a table into table_old, create the table with the new definitions,
555
# and create the indexes.
556
sub move_old_table ($$)
557
{
558
    my ($table, $pkey_column) = @_;
559
    my $tablename = $table->get_name();
560
 
561
    # Rename the table with this name to another name.
562
    $dbh->do("ALTER TABLE $tablename RENAME TO ${tablename}_old") ||
563
	die "Could not rename table $tablename: " . $dbh->errstr;
564
 
565
    # For PostgreSQL, need to drop and create the old primary key index
566
    # with a different name, otherwise the create table command below
567
    # will fail.
568
    if (defined $pkey_column && $Codestriker::db =~ /^DBI:pg/i) {
569
	$dbh->do("DROP INDEX ${tablename}_pkey") ||
570
	    die "Could not drop pkey index: " . $dbh->errstr;
571
	$dbh->do("CREATE UNIQUE INDEX ${tablename}_old_pkey ON " .
572
		 "${tablename}_old($pkey_column)") ||
573
		 die "Could not create pkey index for old table: " .
574
		 $dbh->errstr;
575
    }
576
 
577
    # Now create the table.
578
    $database->create_table($table);
579
}
580
 
581
# Create a new commentstate record with the specified data values.  Return
582
# the id of the commentstate record created.
583
sub create_commentstate ($$$$)
584
{
585
    my ($topicid, $line, $state, $version) = @_;
586
 
587
    print " Updating commentstate topicid $topicid offset $line\n";
588
 
589
    # Determine what filenumber, fileline and filenew the old "offset"
590
    # number refers to.  If it points to an actual diff/block, just
591
    # return 
592
    my ($filenumber, $filename, $fileline, $filenew, $accurate);
593
    my $rc = Codestriker::Action::SubmitComment->
594
	_get_file_linenumber($topicid, $line, \$filenumber, \$filename,
595
			     \$fileline, \$accurate, \$filenew);
596
    if ($rc == 0) {
597
	# Review is not a diff, just a single file.
598
	$filenumber = 1;
599
	$fileline = $line;
600
	$filenew = 1;
601
    } elsif ($filenumber == -1) {
602
	# Comment was made against a diff header.
603
	$filenumber = 1;
604
	$fileline = 1;
605
	$filenew = 1;
606
    }
607
 
608
    my $insert = $dbh->prepare_cached(
609
		"INSERT INTO commentstate (topicid, fileline, filenumber, " .
610
		"filenew, state, version, creation_ts, modified_ts) VALUES " .
611
	        "(?, ?, ?, ?, ?, ?, ?, ?)");
612
    my $timestamp = Codestriker->get_timestamp(time);
613
    $insert->execute($topicid, $fileline, $filenumber, $filenew,
614
		     $state, $version, $timestamp, $timestamp);
615
    print "Create commentstate\n";
616
 
617
    # Find out what the commentstateid is, and update the
618
    # topicoffset_map.
619
    my $check = $dbh->prepare_cached("SELECT id FROM commentstate " .
620
				     "WHERE topicid = ? AND " .
621
				     "fileline = ? AND " .
622
				     "filenumber = ? AND " .
623
				     "filenew = ?");
624
    $check->execute($topicid, $fileline, $filenumber, $filenew);
625
    my ($id) = $check->fetchrow_array();
626
    $check->finish();
627
 
628
    return $id;
629
}
630
 
631
# Migrate the "file" table to "topicfile", to avoid keyword issues with ODBC
632
# and Oracle.  Make sure the error values of the database connection are
633
# correctly set, to handle the most likely case where the "file" table doesn't
634
# even exist.  
635
$database->move_table("file", "topicfile");
636
 
637
# Migrate the "comment" table to "commentdata", to avoid keyword issues with
638
# ODBC and Oracle.  Make sure the error values of the database connection are
639
# correctly set, to handle the most likely case where the "file" table doesn't
640
# even exist.
641
$database->move_table("comment", "commentdata");
642
 
643
# Retrieve the tables which currently exist in the database, to determine
644
# which databases are missing.
645
my @existing_tables = $database->get_tables();
646
 
647
foreach my $table (@tables) {
648
    my $table_name = $table->get_name();
649
    next if grep /^${table_name}$/i, @existing_tables;
650
 
651
    print "Creating table " . $table->get_name() . "...\n";
652
    $database->create_table($table);
653
}
654
 
655
# Make sure the database is committed before proceeding.
656
$database->commit();
657
 
658
# Add new fields to the topic field when upgrading old databases.
659
$database->add_field('topic', 'repository', $TEXT);
660
$database->add_field('topic', 'projectid', $INT32);
661
$database->add_field('topic', 'start_tag', $TEXT);
662
$database->add_field('topic', 'end_tag', $TEXT);
663
$database->add_field('topic', 'module', $TEXT);
664
 
665
# Add the new metric fields to the commentstatehistory table.
666
$database->add_field('commentstatehistory', 'metric_name', $TEXT);
667
$database->add_field('commentstatehistory', 'metric_value', $TEXT);
668
 
669
# Add the new state field to the project table
670
$database->add_field('project', 'state', $INT16);
671
 
672
# If we are using MySQL, and we are upgrading from a version of the database
673
# which used "text" instead of "mediumtext" for certain fields, update the
674
# appropriate table columns.
675
if ($Codestriker::db =~ /^DBI:mysql/i) {
676
    # Check that document field in topic is up-to-date.
677
    my $ref = $database->get_field_def("topic", "document");
678
    my $text_type = $database->_map_type($TEXT);
679
    if ($$ref[1] ne $text_type) {
680
	print "Updating topic table for document field to be $text_type...\n";
681
	$dbh->do("ALTER TABLE topic CHANGE document document $text_type") ||
682
	    die "Could not alter topic table: " . $dbh->errstr;
683
    }
684
}
685
 
686
# If we are using MySQL, and we are upgrading from a version of the database
687
# which used TIMESTAMP instead of DATETIME for certain fields, update the
688
# appropriate table columns.
689
if ($Codestriker::db =~ /^DBI:mysql/i) {
690
 
691
    my @old_time_fields = ( 
692
        [ 'topic','creation_ts'],
693
        [ 'topic','modified_ts'],
694
        [ 'topichistory','modified_ts'],
695
        [ 'topicviewhistory','creation_ts'],
696
        [ 'commentdata','creation_ts'],
697
        [ 'commentstate','creation_ts' ],
698
        [ 'commentstate','modified_ts'],
699
        [ 'commentstatehistory','modified_ts'],
700
        [ 'participant','modified_ts'],
701
        [ 'project','creation_ts'],
702
        [ 'project','modified_ts']
703
        );
704
 
705
    foreach my $fields (@old_time_fields)
706
    {
707
        my $table = $fields->[0];
708
        my $field = $fields->[1];
709
 
710
        my $ref = $database->get_field_def($table, $field);
711
        my $text_type = $database->_map_type($DATETIME);
712
        if ($$ref[1] ne $text_type) {
713
	    print "Updating $table table for $field field to be $text_type...\n";
714
	    $dbh->do("ALTER TABLE $table CHANGE $field $field $text_type") ||
715
	        die "Could not alter " . $table . " table: " . $dbh->errstr;
716
        }
717
    }
718
 
719
}
720
 
721
 
722
# Determine if the commentdata and/or commentstate tables are old.
723
my $old_comment_table = $database->column_exists("commentdata", "line");
724
my $old_commentstate_table = $database->column_exists("commentstate", "line");
725
 
726
if ($old_comment_table) {
727
    my %topicoffset_map;
728
    print "Detected old version of commentdata table, migrating...\n";
729
 
730
    # Need to migrate the data to the new style of the table data.
731
 
732
    my $stmt;
733
    if ($old_commentstate_table) {
734
	print "Detected old version of commentstate table, migrating...\n";
735
	# Update the commentstate table.
736
	move_old_table($commentstate_table, "topicid, line");
737
	move_old_table($commentdata_table, undef);
738
	$stmt =
739
	    $dbh->prepare_cached("SELECT topicid, state, line, version " .
740
				 "FROM commentstate_old");
741
	$stmt->execute();
742
	while (my ($topicid, $state, $line, $version) =
743
	       $stmt->fetchrow_array()) {
744
	    $topicoffset_map{"$topicid|$line"} =
745
		create_commentstate($topicid, $line, $state, $version);
746
	}
747
	$stmt->finish();
748
	$dbh->do('DROP TABLE commentstate_old');
749
    } else { 
750
	# Version of codestriker which didn't have a commentstate table.
751
	# Need to create new commentstate rows for each distinct comment
752
	# first, then update each individual comment row appropriately.
753
	move_old_table($commentdata_table, undef);
754
 
755
	$stmt = $dbh->prepare_cached('SELECT DISTINCT topicid, line ' .
756
				     'FROM commentdata_old');
757
	$stmt->execute();
758
	while (my ($topicid, $line) = $stmt->fetchrow_array()) {
759
	    print " Migrating comment for topic $topicid offset $line...\n";
760
 
761
	    # Create a commentstate row for this comment.
762
	    my $id = create_commentstate($topicid, $line,
763
					 $Codestriker::COMMENT_SUBMITTED,
764
					 0);
765
	    $topicoffset_map{"$topicid|$line"} = $id;
766
	}
767
	$stmt->finish();
768
    }
769
 
770
    # Now update each comment row to refer to the appropriate commentstate
771
    # row.
772
    $stmt = $dbh->prepare_cached('SELECT topicid, commentfield, author, ' .
773
				 'line, creation_ts FROM commentdata_old');
774
    $stmt->execute();
775
    while (my ($topicid, $commentfield, $author, $line, $creation_ts) =
776
	   $stmt->fetchrow_array()) {
777
 
778
	# Update the associated row in the new comment table.
779
	my $insert = $dbh->prepare_cached('INSERT INTO commentdata ' .
780
					  '(commentstateid, commentfield, ' .
781
					  'author, creation_ts) VALUES ' .
782
					  '(?, ?, ?, ?)');
783
	print " Updating comment topicid $topicid offset $line...\n";
784
	$insert->execute($topicoffset_map{"$topicid|$line"},
785
			 $commentfield, $author, $creation_ts);
786
    }
787
    $stmt->finish();
788
 
789
    # Drop the old comment table.
790
    $dbh->do('DROP TABLE commentdata_old');
791
 
792
    # Commit these changes.
793
    $database->commit();
794
    print "Done\n";
795
}
796
 
797
# Create the appropriate file and delta rows for each topic, if they don't
798
# already exist.  Apparently SQL Server doesn't allow multiple statements
799
# to be active at any given time (gack!) so store the topic list into an
800
# array first.  The things we do... what a bloody pain and potential
801
# memory hog.
802
my $stmt = $dbh->prepare_cached('SELECT id FROM topic');
803
$stmt->execute();
804
my @topic_list = ();
805
while (my ($topicid) = $stmt->fetchrow_array()) {
806
    push @topic_list, $topicid;
807
}
808
$stmt->finish();
809
 
810
foreach my $topicid (@topic_list) {
811
    # Check if there is an associated delta record, and if not create it.
812
    my $check = $dbh->prepare_cached('SELECT COUNT(*) FROM delta ' .
813
				     'WHERE topicid = ?');
814
    $check->execute($topicid);
815
    my ($count) = $check->fetchrow_array();
816
    $check->finish();
817
    next if ($count != 0);
818
 
819
    # Check if there is a file record for this topic.  If not, just create
820
    # a simple 1 file, 1 delta record, so that the old comment offsets are
821
    # preserved.
822
    $check = $dbh->prepare_cached('SELECT COUNT(*) FROM topicfile ' .
823
				  'WHERE topicid = ?');
824
    $check->execute($topicid);
825
    my ($filecount) = $check->fetchrow_array();
826
    $check->finish();
827
 
828
    # Determine what repository and document is associated with this topic.
829
    my $rep_stmt = $dbh->prepare_cached('SELECT repository, document '.
830
					'FROM topic WHERE id = ?');
831
    $rep_stmt->execute($topicid);
832
    my ($repository_url, $document) = $rep_stmt->fetchrow_array();
833
    $rep_stmt->finish();
834
 
835
    # Determine the appropriate repository object (if any) for this topic.
836
    my $repository = undef;
837
    if (defined $repository_url && $repository_url ne "") {
838
	$repository =
839
	    Codestriker::Repository::RepositoryFactory->get($repository);
840
    }
841
 
842
    # Load the default repository if nothing has been set.
843
    if (! defined($repository)) {
844
	$repository_url = $Codestriker::valid_repositories[0];
845
	$repository =
846
	    Codestriker::Repository::RepositoryFactory->get($repository_url);
847
    }
848
 
849
    # Create a temporary file containing the document, so that the
850
    # standard parsing routines can be used.
851
    my $tmpfile = "tmptopic.txt";
852
    open(TEMP_FILE, ">$tmpfile") ||
853
	die "Failed to create temporary topic file \"$tmpfile\": $!";
854
    print TEMP_FILE $document;
855
    close TEMP_FILE;
856
    open(TEMP_FILE, "$tmpfile") ||
857
	die "Failed to open temporary file \"$tmpfile\": $!";
858
 
859
    my @deltas = ();
860
    if ($filecount == 0) {
861
	# Parse the document as a single file, for backward compatibility,
862
	# so that the comment offsets are preserved.
863
	print "Creating 1 delta object for topic $topicid\n";
864
	@deltas =
865
	    Codestriker::FileParser::UnknownFormat->parse(\*TEMP_FILE);
866
 
867
    } else {
868
	# Parse the document, and extract the diffs out of it.
869
	@deltas =
870
	    Codestriker::FileParser::Parser->parse(\*TEMP_FILE, "text/plain",
871
						   $repository, $topicid);
872
	print "Creating $#deltas deltas for topic $topicid\n";
873
	my $deletefile_stmt =
874
	    $dbh->prepare_cached('DELETE FROM topicfile WHERE topicid = ?');
875
	$deletefile_stmt->execute($topicid);
876
    }
877
 
878
    print "Creating delta rows for topic $topicid\n";
879
    Codestriker::Model::File->create($dbh, $topicid, \@deltas,
880
				     $repository);
881
 
882
    # Delete the temporary file.
883
    close TEMP_FILE;
884
    unlink($tmpfile);
885
}
886
$database->commit();
887
 
888
# Check if the version to be upgraded has any project rows or not, and if
889
# not, link all topics to the default project.
890
$stmt = $dbh->prepare_cached('SELECT COUNT(*) FROM project');
891
$stmt->execute();
892
my ($project_count) = $stmt->fetchrow_array();
893
$stmt->finish();
894
if ($project_count == 0) {
895
    # Create a default project entry, which can then be modified by the user
896
    # later.
897
    print "Creating default project...\n";
898
    my $timestamp = Codestriker->get_timestamp(time);
899
    my $create = $dbh->prepare_cached('INSERT INTO project ' .
900
				      '(name, description, creation_ts, ' .
901
				      'modified_ts, version, state) ' .
902
				      'VALUES (?, ?, ?, ?, ?, ?) ');
903
    $create->execute('Default project', 'Default project description',
904
		     $timestamp, $timestamp, 0, 0);
905
 
906
    # Get the id of this project entry.
907
    my $select = $dbh->prepare_cached('SELECT MIN(id) FROM project');
908
    $select->execute();
909
    my ($projectid) = $select->fetchrow_array();
910
    $select->finish();
911
 
912
    # Now link all the topics in the system with this default project.
913
    print "Linking all topics to default project...\n";
914
    my $update = $dbh->prepare_cached('UPDATE topic SET projectid = ?');
915
    $update->execute($projectid);
916
}
917
$database->commit();
918
 
919
# Check if the version to be upgraded has any project rows and if
920
# so set the default state to open.
921
$stmt = $dbh->prepare_cached('UPDATE project SET state = 0 WHERE state IS NULL');
922
$stmt->execute();
923
$database->commit();
924
 
925
# Check if the data needs to be upgraded to the new commentstate metric
926
# scheme from the old state_id scheme.  For now, assume the old state-ids
927
# are the default values present in Codestriker.conf.  If they were changed
928
# by the user, they could always modify the DB values appropriately.
929
eval {
930
    $dbh->{PrintError} = 0;
931
 
932
    # This array should reflect the value of @comment_states in your old
933
    # codestriker.conf file, and is used for data migration purposes.
934
    # This value represents the default value used in Codestriker.
935
    my @old_comment_states = ("Submitted", "Invalid", "Completed");
936
 
937
    $stmt = $dbh->prepare_cached('SELECT id, state, creation_ts, modified_ts '.
938
				 'FROM commentstate WHERE state >= 0');
939
    $stmt->execute();
940
 
941
    my $update = $dbh->prepare_cached('UPDATE commentstate ' .
942
				      'SET state = ?, creation_ts = ?, ' .
943
				      'modified_ts = ? ' .
944
				      'WHERE id = ?');
945
    my $insert = $dbh->prepare_cached('INSERT INTO commentstatemetric ' .
946
				      '(id, name, value) VALUES (?, ?, ?) ');
947
 
948
    my $count = 0;
949
    my @update_rows = ();
950
    while (my ($id, $state, $creation_ts, $modified_ts) =
951
	   $stmt->fetchrow_array()) {
952
	if ($count == 0) {
953
	    print "Migrating old commentstate records... \n";
954
	    print "Have you updated the \@old_comment_states variable on line 767? (y/n): ";
955
	    flush STDOUT;
956
	    my $answer = <STDIN>;
957
	    chop $answer;
958
	    if (! ($answer =~ /^y/i)) {
959
		print "Aborting script... update \@old_comment_states in this script and run again.\n";
960
		$stmt->finish();
961
		exit(1);
962
	    }
963
	}
964
 
965
	# We can't update this now due to ^%@$# SQL server, we do that below.
966
	my $value = $old_comment_states[$state];
967
	$value = "Unknown $state" unless defined $value;
968
	push @update_rows, { state => -$state - 1,
969
			     creation_ts => $creation_ts,
970
			     modified_ts => $modified_ts,
971
			     id => $id,
972
			     value => $value };
973
	$count++;
974
    }
975
    $stmt->finish();
976
 
977
    foreach my $row (@update_rows) {
978
	# Update the state to its negative value, so the information isn't
979
	# lost, but also to mark it as being migrated.
980
	$update->execute($row->{state}, $row->{creation_ts}, $row->{modified_ts}, $row->{id});
981
	$insert->execute($row->{id}, "Status", $row->{value});
982
    }
983
    print "Migrated $count records.\n" if $count > 0;
984
 
985
    # Now do the same for the commentstatehistory records.
986
    $stmt = $dbh->prepare_cached('SELECT id, state, version, modified_ts ' .
987
				 'FROM commentstatehistory ' .
988
				 'WHERE state >= 0');
989
    $stmt->execute();
990
 
991
    $update = $dbh->prepare_cached('UPDATE commentstatehistory ' .
992
				   'SET metric_name = ?, metric_value = ?, ' .
993
				   ' state = ?, modified_ts = ? ' .
994
				   'WHERE id = ? AND version = ?');
995
    $count = 0;
996
    @update_rows = ();
997
    while (my ($id, $state, $version, $modified_ts) =
998
	   $stmt->fetchrow_array()) {
999
	print "Migrating old commentstatehistory records...\n" if $count == 0;
1000
	my $value = $old_comment_states[$state];
1001
	$value = "Unknown $state" unless defined $value;
1002
 
1003
	push @update_rows, { value=>$value, state=>-$state -1,
1004
			     modified_ts=>$modified_ts, id=>$id,
1005
			     version=>$version };
1006
	$count++;
1007
    }
1008
    $stmt->finish();
1009
 
1010
    foreach my $row (@update_rows) {
1011
	$update->execute("Status", $row->{value}, $row->{state},
1012
			 $row->{modified_ts}, $row->{id},
1013
			 $row->{version});
1014
    }
1015
    print "Migrated $count records.\n" if $count > 0;
1016
    $database->commit();
1017
};
1018
if ($@) {
1019
    print "Failed because of $@\n";
1020
}
1021
 
1022
$dbh->{PrintError} = 1;
1023
 
1024
# Now generate the contents of the codestriker.pl file, with the appropriate
1025
# configuration details set (basically, the location of the lib dir).
1026
print "Generating cgi-bin/codestriker.pl file...\n";
1027
mkdir '../cgi-bin', 0755;
1028
 
1029
my $template = Template->new();
1030
 
1031
eval("use Template;");
1032
die "Unable to load Template module: $@\n" if $@;
1033
 
1034
my $template_vars = {};
1035
 
1036
# For Win32, don't enable tainting mode.  There are weird issues with
1037
# ActivePerl, and sometimes with IIS as well.  Make sure the Windows Perl
1038
# path is set correctly, as its location could be anywhere.
1039
my $perl = $^X;
1040
if ($windows) {
1041
    $perl =~ s/\\/\//g;
1042
    $template_vars->{hash_ex_line} = '#!' . $perl . ' -w';
1043
} else {
1044
    $template_vars->{hash_ex_line} = '#!' . $perl . ' -wT';
1045
}
1046
 
1047
$template_vars->{codestriker_lib} = 'use lib \'' . cwd() . '/../lib\';';
1048
 
1049
$template_vars->{codestriker_conf} = '\'' . cwd() . '/..\'';
1050
 
1051
$template_vars->{has_rss} = !exists($missing_optional{'XML::RSS'});
1052
 
1053
$template_vars->{scary_warning} = 
1054
    "# !!!! THIS FILE IS AUTO-GENERATED by bin/install.pl !!!\n".
1055
    "# !!!! DO NOT EDIT !!!\n".
1056
    "# The base source is bin/codestriker.pl.base.\n";
1057
 
1058
open(CODESTRIKER_PL, ">../cgi-bin/codestriker.pl")
1059
    || die "Unable to create ../cgi-bin/codestriker.pl file: $!";
1060
 
1061
$template->process("codestriker.pl.base", $template_vars,\*CODESTRIKER_PL);
1062
close CODESTRIKER_PL;
1063
 
1064
 
1065
# Make sure the generated file is executable.
1066
chmod 0755, '../cgi-bin/codestriker.pl';
1067
 
1068
# Clean out the contents of the data and template directory, but don't
1069
# remove them.
1070
print "Removing old generated templates...\n";
1071
chdir('../cgi-bin') ||
1072
    die "Couldn't change to cgi-dir directory: $!";
1073
if (-d 'template/en') {
1074
    print "Cleaning old template directory...\n";
1075
    rmtree(['template/en'], 0, 1);
1076
}
1077
 
1078
print "Done\n";
1079
 
1080
# Release the database connection.
1081
$database->release_connection();
1082
 
1083