Subversion Repositories DevTools

Rev

Details | Last modification | View Log | RSS feed

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