1 ###############################################################################
2 # package Vss2Svn::VSS #
3 ###############################################################################
14 use Win32
::TieRegistry
(Delimiter
=> '/');
23 our $VERSION = '1.05';
25 our(%gCfg, %gErrMatch, %gHistLineMatch, @gDevPatterns);
27 ###############################################################################
29 ###############################################################################
31 my($class, $db, $project, $args) = @_;
34 croak
"Must specify VSS database path";
37 $db =~ s/[\/\\]?(srcsafe.ini)?$//i
;
39 if (defined $project && $project ne ''
40 && $project ne '$' && $project !~ /^\$\//) {
41 croak
"Project path must be absolute (begin with $/)";
44 $project = first
{defined} $project, '$/';
45 $args = first
{defined} $args, {};
61 implicit_projects
=> undef,
70 # test to ensure 'ss' command is available
71 $self->ss("WHOAMI", -2) or
72 croak
"Could not run VSS 'ss' command: ensure it is in your PATH";
74 $self->{_whoami
} = $self->{ss_output
};
75 $self->{_whoami
} =~ s/\s*$//;
76 $self->{_whoami
} =~ s/^.*\n//;
78 if ($self->{ss_output
} =~ /changing project/im ||
79 !$self->_check_ss_inifile) {
80 croak
"FATAL ERROR: You must not set the Force_Dir or Force_Prj VSS\n"
81 . "variables when running SourceSync. These variables can be\n"
82 . "cleared by unchecking the two \"Assume...\" boxes in SourceSafe\n"
83 . "Explorer under Tools -> Options -> Command Line Options.\n ";
87 $self->ss('PROJECT', -2);
89 $project = $self->{ss_output
};
90 $project =~ s/^Current project is *//i;
91 $project .= '/' unless $project =~ m
/\
/$/;
93 $self->{project
} = $project;
95 $self->set_project($project);
102 ###############################################################################
104 ###############################################################################
105 sub _check_ss_inifile
{
108 my $user = lc($self->{_whoami
});
109 my $path = "$self->{database}/users/$user/ss.ini";
111 open SSINI
, $path or croak
"Could not open user init file $path";
125 } # End _check_ss_inifile
127 ###############################################################################
129 ###############################################################################
131 my($self, $project) = @_;
133 $project .= '/' unless $project =~ m
/\
/$/;
135 $self->ss("CP \"$project\"", -2) or
136 croak
"Could not set current project to $project:\n"
137 . " $self->{ss_output}\n ";
139 $self->{project
} = $project;
143 ###############################################################################
145 ###############################################################################
147 my($self, $project, $recursive, $remove_dev) = @_;
149 # returns a nested-hash "tree" of all subprojects and files below the given
150 # project; the "leaves" of regular files are the value "1".
152 $project = $self->full_path($project);
153 $recursive = 1 unless defined $recursive;
154 $remove_dev = 0 unless defined $remove_dev;
156 if ($self->filetype($project) ) { # projects are type 0
157 carp
"project_tree(): '$project' is not a valid project";
161 my $cmd = "DIR \"$project\"";
162 $cmd .= ($recursive)?
' -R' : ' -R-';
164 $self->ss($cmd, -2) or return undef;
166 # It would be nice if Microsoft made it easy for scripts to pick useful
167 # information out of the project 'DIR' listings, but unfortunately that's
168 # not the case. It appears that project listings always follow blank
169 # lines, and begin with the full project path with a colon appended.
170 # Within a listing, subprojects come first and begin with a dollar sign,
171 # then files are listed alphabetically. If there are no items in a project,
172 # it prints out a message saying so. And at the end of it all, you get
173 # a statement like "7 item(s)".
176 my $branch_ref = \
%tree;
178 my $seen_blank_line = 0;
179 my($current_project);
180 my $match_project = quotemeta($project);
183 foreach my $line (split "\n", $self->{ss_output
}) {
187 if ($seen_blank_line) {
188 carp
"project_tree(): an internal error has occured -- 1";
192 $seen_blank_line = 1;
196 $seen_blank_line = 0;
198 if ($line =~ m/^\d+\s+item\(s\)$/i) {
199 # this is a count of # of items found; ignore
202 } elsif ($line =~ m/^No items found under/i) {
206 } elsif ($line =~ m/^(\$\/.*):$/) {
207 # this is the beginning of a project's listing
208 $current_project = $1;
209 # make current project relative to initial
210 $current_project =~ s/^$match_project\/?//i
;
211 $current_project =~ s/^\$\///; # take off initial $/ if still there
213 $branch_ref = \
%tree;
215 if ($current_project ne '') {
216 # get a reference to the end branch of subprojects
217 ($branch_ref) = reverse(map {$branch_ref = $branch_ref->{$_}}
218 split('/', $current_project));
221 if (!defined $branch_ref) {
222 carp
"project_tree(): an internal error has occured -- 2";
227 } elsif ($line =~ m/^\$(.*)/) {
228 # this is a subproject; create empty hash if not already there
229 if (!defined $current_project) {
230 carp
"project_tree(): an internal error has occured -- 3";
234 $branch_ref->{$1} = {} unless defined($branch_ref->{$1});
236 # just a regular file
237 if (!defined $current_project) {
238 carp
"project_tree(): an internal error has occured -- 4";
243 foreach my $pattern (@gDevPatterns) {
244 next LINE
if $line =~ m/$pattern/i;
248 $branch_ref->{$line} = 1;
257 ###############################################################################
259 ###############################################################################
261 my($self, $file) = @_;
262 # returns an array ref of hash refs from earliest to most recent;
263 # each hash has the following items:
264 # version: version (revision) number
265 # user : name of user who committed change
266 # date : date in YYYYMMDD format
267 # time : time in HH:MM (24h) format
268 # comment: checkin comment
270 $file = $self->full_path($file);
272 my $cmd = "HISTORY \"$file\"";
275 $self->ss($cmd, -2) or return undef;
279 my $last = 0; # what type was the last line read?
280 # 0=start;1=version line;2=user/date/time;3="Checked In";
283 my $last_version = -1;
285 my$rev = {}; # hash of info for the lastent revision
286 my($year,$month,$day,$hour,$min,$ampm,$comment,$version);
289 foreach my $line (split "\n", $self->{ss_output
}) {
290 if ($self->{_debug
}) {
291 warn "\nDEBUG:($last)<$line>\n";
295 if ($line =~ m/$gHistLineMatch{version}/) {
297 if ($last_version == 0 ||
298 (($last_version != -1) && ($1 != ($last_version - 1)))) {
300 # each version should be one less than the last
301 print "file_history(): internal consistency failure";
306 $rev->{version
} = $1;
313 if ($line =~ m/$gHistLineMatch{userdttm}/) {
317 if ($gCfg{dateFormat
} == 1) {
319 ($rev->{user
}, $day, $month, $year, $hour, $min, $ampm)
320 = ($1, $2, $3, $4, $5, $6, $7);
321 } elsif ($gCfg{dateFormat
} == 2) {
323 ($rev->{user
}, $year, $month, $day, $hour, $min, $ampm)
324 = ($1, $2, $3, $4, $5, $6, $7);
327 ($rev->{user
}, $month, $day, $year, $hour, $min, $ampm)
328 = ($1, $2, $3, $4, $5, $6, $7);
331 $year = ($year > 79)?
"19$year" : "20$year";
333 if ($ampm =~ /p/i && $hour < 12) {
335 } elsif ($ampm =~ /a/i && $hour == 12) {
339 if ($self->{timebias
} != 0) {
340 my $basis = parsedate
("$year/$month/$day $hour:$min");
341 (my $bias = $self->{timebias
}) =~ s/^(\d+)/+ $1/;
342 my $epoch_secs = parsedate
("$bias minutes",
345 (undef,$min,$hour,$day,$month,$year)
346 = localtime($epoch_secs);
349 $year += 1900; #no, not a Y2K bug; $year = 100 in 2000
352 $rev->{date
} = sprintf("%4.4i-%2.2i-%2.2i",
353 $year, $month, $day);
354 $rev->{time} = sprintf("%2.2i:%2.2i", $hour, $min);
355 } elsif ($line =~ m/$gHistLineMatch{label}/) {
356 # this is an inherited Label; ignore it
359 # user, date, and time should always come after header line
360 print "file_history(): internal consistency failure";
368 if ($line =~ s/$gHistLineMatch{comment}//) {
377 if ($line =~ m/$gHistLineMatch{version}/) {
381 $comment =~ s/\s+$//;
382 $comment =~ s/^\s+//;
383 $rev->{comment
} = $comment;
385 unshift @
$hist, $rev;
388 $rev->{version
} = $version;
390 $comment .= "\n$line";
398 $comment =~ s/\n/ /g;
399 $comment =~ s/\s+$//;
400 $comment =~ s/^\s+//;
401 $rev->{comment
} = $comment;
403 # last line of history should always be part of a comment, but
404 # sometimes VSS doesn't include the final comment line
405 $rev->{comment
} = '(no comment)';
408 unshift @
$hist, $rev;
412 ###############################################################################
414 ###############################################################################
421 my($self, $file) = @_;
422 return -1 unless defined $file;
427 return 0 if $file eq '$/';
428 return -1 if $file eq '$';
430 # VSS has no decent way of determining whether an item is a project or
431 # a file, so we do this in a somewhat roundabout way
433 $file =~ s/[\/\\]$//;
436 $bare =~ s/.*[\/\\]//;
437 $bare = quotemeta($bare);
439 $self->ss("PROPERTIES \"$file\" -R-", -3) or return -1;
441 my $match_isproject = "^Project:.*$bare\\s*\$";
442 my $match_notfound = "$bare\\s*is not an existing filename or project";
444 if ($self->{ss_output
} =~ m/$match_isproject/mi) {
446 } elsif ($self->{ss_output
} =~ m/$match_notfound/mi) {
449 $self->ss("FILETYPE \"$file\"", -3) or return -1;
451 if ($self->{ss_output
} =~ m/^$bare\s*Text/mi) {
461 ###############################################################################
463 ###############################################################################
465 # returns the full VSS path to a given project file.
467 my($self, $file) = @_;
471 $file =~ s/\/$// unless $file eq '$/';
473 return $file if $self->{implicit_projects};
475 $file = "$self->{project}$file" unless $file =~ m/^\$/;
476 $file =~ s/\/$// unless $file eq '$/'; # in case empty string was passed
481 ###############################################################################
483 ###############################################################################
485 my($self, $cmd, $silent) = @_;
487 # SS command-line tool access.
490 # 0: print everything
491 # 1: print program output only
492 # 2: print err msgs only
494 # -n: use 'n
' only if 'silent
' attribute not set
496 if (defined($silent) && $silent < 0) {
497 $silent = first {defined} $self->{silent}, $silent;
499 $silent = first {defined} $silent, $self->{silent}, 0;
502 $silent = abs($silent);
507 (my $cmd_word = lc($cmd)) =~ s/^(ss(\.exe)?\s+)?(\S+).*/$3/i;
509 $cmd = "ss $cmd" unless ($cmd =~ m/^ss(\.exe)?\s/i);
511 if ($self->{interactive} =~ m/^y/i) {
513 } elsif ($self->{interactive} =~ m/^n/i) {
515 } elsif (!$self->{interactive}) {
521 if (defined $self->{user} && $cmd !~ /\s-Y/i) {
522 if (defined $self->{passwd}) {
523 $disp_cmd = "$cmd -Y$self->{user},******";
524 $cmd = "$cmd -Y$self->{user},$self->{passwd}";
526 $disp_cmd = $cmd = "$cmd -Y$self->{user}";
532 warn "DEBUG: $disp_cmd\n\n" if $self->{_debug};
534 $ENV{SSDIR} = $self->{database};
536 if ($self->{use_tempfiles} &&
537 $cmd_word =~ /^(dir|filetype|history|properties)$/) {
538 my $tmpfile = "$self->{use_tempfiles}/${cmd_word}_cmd.txt";
540 $cmd = "$cmd \"-O\&$tmpfile\"";
543 if (open SS_OUTPUT, "$tmpfile") {
545 $output = scalar <SS_OUTPUT>;
549 warn "Can't
open '$cmd_word' tempfile
$tmpfile";
554 open SS_OUTPUT, '-|', "$cmd 2>&1";
556 while (<SS_OUTPUT>) {
561 $output =~ s/\s+$// if defined $output;
565 if ($self->{paginate}) {
568 foreach my $line (split "\n", $output) {
571 unless ($linecount++ % $self->{paginate}) {
572 print "Hit ENTER to
continue...\r";
589 # SourceSafe returns 1 to indicate warnings, such as no results returned
590 # from a 'DIR'. We don't want to consider these an error.
591 my $success = !($ev > 1);
594 # This is interesting. If a command only partially fails (such as GET-ing
595 # multiple files), that's apparently considered a success. So we have to
597 my $base_cmd = uc($cmd);
598 $base_cmd =~ s/^(ss\s*)?(\w+).*/$2/i;
602 if (defined($err_match = $gErrMatch{$base_cmd}) &&
603 $output =~ m/$err_match/m) {
610 $self->{ss_error} = undef;
612 $self->{ss_error} = "$disp_cmd\n$output";
615 if (!$success && ($silent == 0 || $silent == 2)) {
617 carp "\nERROR
in Vss2Svn
::VSS
-\
>ss
\n"
618 . "Command was
: $disp_cmd\n "
619 . "(Error
$ev) $output\n ";
624 $self->{ss_output} = $output;
629 ###############################################################################
631 ###############################################################################
634 print @_ unless $self->{silent};
637 ###############################################################################
638 # _vm -- "verbose message
"
639 ###############################################################################
642 print @_ if $self->{verbose};
645 ###############################################################################
647 ###############################################################################
649 my $dateFormat = $Registry->{'HKEY_CURRENT_USER/Control Panel/'
650 . 'International/iDate'} || 0;
651 my $dateSep = $Registry->{'HKEY_CURRENT_USER/Control Panel/'
652 . 'International/sDate'} || '/';
653 my $timeSep = $Registry->{'HKEY_CURRENT_USER/Control Panel/'
654 . 'International/sTime'} || ':';
655 $gCfg{dateFormat} = $dateFormat;
657 if ($dateFormat == 1) {
658 $gCfg{dateString} = "DD
${dateSep
}MM
${dateSep
}YY
";
659 } elsif ($dateFormat == 2) {
660 $gCfg{dateString} = "YY
${dateSep
}MM
${dateSep
}DD
";
662 $gCfg{dateString} = "MM
${dateSep
}DD
${dateSep
}YY
";
665 $gCfg{timeString} = "HH
${timeSep
}MM
";
667 # see ss method for explanation of this
669 GET => 'is not an existing filename or project',
670 CREATE => 'Cannot change project to',
671 CP => 'Cannot change project to',
675 version => qr/^\*+\s*Version\s+(\d+)\s*\*+\s*$/,
676 userdttm => qr/^User:\s+(.*?)\s+
677 Date:\s+(\d+)$dateSep(\d+)$dateSep(\d+)\s+
678 Time:\s+(\d+)$timeSep(\d+)([ap]*)\s*$/x,
679 comment => qr/^Comment:\s*/,
680 label => qr/^Label:/,
683 # patterns to match development files that project_tree will ignore
694 &$code && return $_ for @_;