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,
71 # test to ensure 'ss' command is available
72 $self->ss("WHOAMI", -2) or
73 croak
"Could not run VSS 'ss' command: ensure it is in your PATH";
75 $self->{_whoami
} = $self->{ss_output
};
76 $self->{_whoami
} =~ s/\s*$//;
77 $self->{_whoami
} =~ s/^.*\n//;
79 if ($self->{ss_output
} =~ /changing project/im ||
80 !$self->_check_ss_inifile) {
81 croak
"FATAL ERROR: You must not set the Force_Dir or Force_Prj VSS\n"
82 . "variables when running SourceSync. These variables can be\n"
83 . "cleared by unchecking the two \"Assume...\" boxes in SourceSafe\n"
84 . "Explorer under Tools -> Options -> Command Line Options.\n ";
88 $self->ss('PROJECT', -2);
90 $project = $self->{ss_output
};
91 $project =~ s/^Current project is *//i;
92 $project .= '/' unless $project =~ m
/\
/$/;
94 $self->{project
} = $project;
96 $self->set_project($project);
103 ###############################################################################
105 ###############################################################################
106 sub _check_ss_inifile
{
109 my $user = lc($self->{_whoami
});
110 my $path = "$self->{database}/users/$user/ss.ini";
112 open SSINI
, $path or croak
"Could not open user init file $path";
126 } # End _check_ss_inifile
128 ###############################################################################
130 ###############################################################################
132 my($self, $project) = @_;
134 $project .= '/' unless $project =~ m
/\
/$/;
136 $self->ss("CP \"$project\"", -2) or
137 croak
"Could not set current project to $project:\n"
138 . " $self->{ss_output}\n ";
140 $self->{project
} = $project;
144 ###############################################################################
146 ###############################################################################
148 my($self, $project, $recursive, $remove_dev) = @_;
150 # returns a nested-hash "tree" of all subprojects and files below the given
151 # project; the "leaves" of regular files are the value "1".
153 $project = $self->full_path($project);
154 $recursive = 1 unless defined $recursive;
155 $remove_dev = 0 unless defined $remove_dev;
157 if ($self->filetype($project) ) { # projects are type 0
158 carp
"project_tree(): '$project' is not a valid project";
162 my $cmd = "DIR \"$project\"";
163 $cmd .= ($recursive)?
' -R' : ' -R-';
165 $self->ss($cmd, -2) or return undef;
167 # It would be nice if Microsoft made it easy for scripts to pick useful
168 # information out of the project 'DIR' listings, but unfortunately that's
169 # not the case. It appears that project listings always follow blank
170 # lines, and begin with the full project path with a colon appended.
171 # Within a listing, subprojects come first and begin with a dollar sign,
172 # then files are listed alphabetically. If there are no items in a project,
173 # it prints out a message saying so. And at the end of it all, you get
174 # a statement like "7 item(s)".
177 my $branch_ref = \
%tree;
179 my $seen_blank_line = 0;
180 my($current_project);
181 my $match_project = quotemeta($project);
184 foreach my $line (split "\n", $self->{ss_output
}) {
188 if ($seen_blank_line) {
189 carp
"project_tree(): an internal error has occured -- 1";
193 $seen_blank_line = 1;
197 $seen_blank_line = 0;
199 if ($line =~ m/^\d+\s+item\(s\)$/i) {
200 # this is a count of # of items found; ignore
203 } elsif ($line =~ m/^No items found under/i) {
207 } elsif ($line =~ m/^(\$\/.*):$/) {
208 # this is the beginning of a project's listing
209 $current_project = $1;
210 # make current project relative to initial
211 $current_project =~ s/^$match_project\/?//i
;
212 $current_project =~ s/^\$\///; # take off initial $/ if still there
214 $branch_ref = \
%tree;
216 if ($current_project ne '') {
217 # get a reference to the end branch of subprojects
218 ($branch_ref) = reverse(map {$branch_ref = $branch_ref->{$_}}
219 split('/', $current_project));
222 if (!defined $branch_ref) {
223 carp
"project_tree(): an internal error has occured -- 2";
228 } elsif ($line =~ m/^\$(.*)/) {
229 # this is a subproject; create empty hash if not already there
230 if (!defined $current_project) {
231 carp
"project_tree(): an internal error has occured -- 3";
235 $branch_ref->{$1} = {} unless defined($branch_ref->{$1});
237 # just a regular file
238 if (!defined $current_project) {
239 carp
"project_tree(): an internal error has occured -- 4";
244 foreach my $pattern (@gDevPatterns) {
245 next LINE
if $line =~ m/$pattern/i;
249 $branch_ref->{$line} = 1;
258 ###############################################################################
259 # parse_date_time_user
260 ###############################################################################
261 sub parse_date_time_user
{
262 my($self, $line) = @_;
264 my ($user,$date,$time,$year,$month,$day,$hour,$min,$ampm);
266 $line =~ m/$gHistLineMatch{userdttm}/;
267 if ($gCfg{dateFormat
} == 1) {
269 ($user, $day, $month, $year, $hour, $min, $ampm)
270 = ($1, $2, $3, $4, $5, $6, $7);
271 } elsif ($gCfg{dateFormat
} == 2) {
273 ($user, $year, $month, $day, $hour, $min, $ampm)
274 = ($1, $2, $3, $4, $5, $6, $7);
277 ($user, $month, $day, $year, $hour, $min, $ampm)
278 = ($1, $2, $3, $4, $5, $6, $7);
281 $year = ($year > 79)?
"19$year" : "20$year";
283 if ($ampm =~ /p/i && $hour < 12) {
285 } elsif ($ampm =~ /a/i && $hour == 12) {
289 if ($self->{timebias
} != 0) {
290 my $basis = parsedate
("$year/$month/$day $hour:$min");
291 (my $bias = $self->{timebias
}) =~ s/^(\d+)/+ $1/;
292 my $epoch_secs = parsedate
("$bias minutes",
295 (undef,$min,$hour,$day,$month,$year)
296 = localtime($epoch_secs);
299 $year += 1900; #no, not a Y2K bug; $year = 100 in 2000
302 $date = sprintf("%4.4i-%2.2i-%2.2i",
303 $year, $month, $day);
304 $time = sprintf("%2.2i:%2.2i", $hour, $min);
314 ###############################################################################
316 ###############################################################################
318 my($self, $file, $versionNumber) = @_;
319 # returns an array ref of hash refs from earliest to most recent;
320 # each hash has the following items:
321 # version: version (revision) number
322 # user : name of user who committed change
323 # date : date in YYYYMMDD format
324 # time : time in HH:MM (24h) format
325 # comment: checkin comment
327 $file = $self->full_path($file);
329 my $cmd = "HISTORY -f- -l- ";
330 $cmd .= "-v$versionNumber~$versionNumber " if (defined $versionNumber && $versionNumber >= 0);
334 my $success = $self->ss($cmd, -2);
335 return undef if (!$success);
340 my $state = 0; # what type was the last line read?
341 # 0=start: look for a line with stars
348 # 8=additional comment lines
350 my $laststate = $state;
351 my $initstate = 1; # perform specific action when entering a state
353 my $last_version = -1;
355 my $rev = {}; # hash of info for the lastent revision
359 foreach my $line (split "\n", $self->{ss_output
}) {
361 my $line_processed = 0;
364 while (!$line_processed) {
365 if ($state == 0) { # search for the start of a record (a star line)
366 if ($line =~ m/$gHistLineMatch{version}/) {
368 } elsif ($line =~ m/$gHistLineMatch{stars22}/) {
371 # drop all other lines
374 } elsif ($state == 2) { # version record
376 if ($line =~ m/$gHistLineMatch{version}/) {
377 $rev->{version
} = $1;
380 $error = "wrong state condition ($state): version line expected\n";
382 } elsif ($line =~ m/$gHistLineMatch{label}/) {
384 } elsif ($line =~ m/$gHistLineMatch{userdttm}/) {
387 $error = "internal consistency failure, label or date/user line expected";
389 } elsif ($state == 3) { # project label
391 if ($line =~ m/$gHistLineMatch{stars22}/) {
394 $error = "wrong state condition ($state): star line expected\n";
396 } elsif ($line =~ m/$gHistLineMatch{label}/) {
399 $error = "internal consistency failure, label line expected";
401 } elsif ($state == 4) { # user and date line
403 if ($line =~ m/$gHistLineMatch{userdttm}/) {
404 %$rev = (%$rev, %{$self->parse_date_time_user ($line)});
407 $error = "wrong state condition ($state): date/user line expected\n";
409 } elsif ($line =~ m/$gHistLineMatch{action}/) {
411 } elsif ($line =~ m/$gHistLineMatch{action0}/) {
413 } elsif ($line =~ m/$gHistLineMatch{action1}/) {
415 } elsif ($line =~ m/$gHistLineMatch{action11}/) {
417 } elsif ($line =~ m/$gHistLineMatch{action2}/) {
419 } elsif ($line =~ m/$gHistLineMatch{action3}/) {
422 $error = "internal consistency failure, action line expected";
424 } elsif ($state == 5) { # label line
426 if ($line =~ m/$gHistLineMatch{label}/) {
430 $error = "wrong state condition ($state): label line expected\n";
432 } elsif ($line =~ m/$gHistLineMatch{userdttm}/) {
435 $error = "internal consistency failure, date/user line expected";
437 } elsif ($state == 6) { # action line
439 if ($line =~ m/$gHistLineMatch{action}/) {
440 $rev->{action
} = $line;
443 } elsif ($line =~ m/$gHistLineMatch{action0}/) {
444 $rev->{action
} = $line;
446 } elsif ($line =~ m/$gHistLineMatch{action1}/) {
448 $rev->{action
} = $2 . " " . $1; # translate to OLE string
450 } elsif ($line =~ m/$gHistLineMatch{action11}/) { # shared
451 # $1 is the path the share came from
452 # $2 is the name of the item
454 $rev->{action
} = "Shared \$/$1$2"; # translate to OLE string
456 } elsif ($line =~ m/$gHistLineMatch{action2}/) {
458 $rev->{action
} = "Renamed $1 to $2";
460 } elsif ($line =~ m/$gHistLineMatch{action3}/) {
462 $rev->{action
} = "Pinned $1 to Version $2";
465 $error = "wrong state condition ($state): action line expected\n";
467 } elsif ($line =~ m/$gHistLineMatch{comment}/) {
469 } elsif ($line eq '') {
472 $error = "internal consistency failure, comment or empty line expected\n";
474 } elsif ($state == 7) { # comment line (begin)
476 if ($line =~ m/$gHistLineMatch{comment}/) {
480 $error = "wrong state condition ($state): comment line expected\n";
482 } elsif ($line =~ m/$gHistLineMatch{stars}/) {
487 } elsif ($state == 8) { # additional comment lines
488 if ($line =~ m/$gHistLineMatch{stars}/) {
491 # accumulate all other comment lines
492 $comment .= "\n$line";
495 } elsif ($state == 9) { # create the record
496 # clean up comment text
497 $comment =~ s/\s+$//;
498 $comment =~ s/^\s+//;
499 if ($comment eq '') {
500 $comment = '(no comment)';
503 $rev->{comment
} = $comment;
505 if (exists $rev->{version
} && defined $rev->{version
} && $rev->{version
} ne '') {
506 warn "DEBUG: Version record: $rev->{version}, $rev->{user}, $rev->{date}, $rev->{time}: $rev->{comment}\n";
508 $rev->{labels
} = $labels;
511 unshift @
$hist, $rev;
512 } elsif ($rev->{label
} ne '') {
513 warn "DEBUG: Label record: $rev->{label}: $rev->{comment}\n";
514 unshift @
$labels, $rev;
520 # drop to state 0, start looking for the next record
524 if ($state ne $laststate) {
533 if ($error ne '') { # report an error an bail out
534 if ($self->{_debug
}) {
535 warn "DEBUG:($state)<$line>\n";
537 warn "DEBUG: file_history(): $error\n";
542 if ($self->{_debug
}) {
543 warn "DEBUG:($state)<$line>\n";
549 # the last record isn't finished if we are not in the initial state
551 # clean up comment text
552 $comment =~ s/\s+$//;
553 $comment =~ s/^\s+//;
554 if ($comment eq '') {
555 $comment = '(no comment)';
558 $rev->{comment
} = $comment;
560 if (exists $rev->{version
} && defined $rev->{version
} && $rev->{version
} ne '') {
561 warn "DEBUG: Version record: $rev->{version}, $rev->{user}, $rev->{date}, $rev->{time}: $rev->{comment}\n";
563 $rev->{labels
} = $labels;
566 unshift @
$hist, $rev;
567 } elsif ($rev->{label
} ne '') {
568 warn "DEBUG: Label record: $rev->{label}: $rev->{comment}\n";
569 unshift @
$labels, $rev;
577 ###############################################################################
579 ###############################################################################
586 my($self, $file) = @_;
587 return -1 unless defined $file;
592 return 0 if $file eq '$/';
593 return -1 if $file eq '$';
595 # special cases with version numbers ($;10 or $/;10)
596 if ($file =~ m/\$\;\d*/mi) {
599 elsif ($file =~ m/\$\/\
;\d
/mi
) {
603 # VSS has no decent way of determining whether an item is a project or
604 # a file, so we do this in a somewhat roundabout way
606 $file =~ s/[\/\\]$//;
609 $bare =~ s/.*[\/\\]//;
610 $bare = quotemeta($bare);
612 $self->ss("PROPERTIES \"$file\" -R-", -3) or return -1;
614 my $match_isproject = "^Project:.*$bare\\s*\$";
615 my $match_notfound = "$bare\\s*is not an existing filename or project";
617 if ($self->{ss_output
} =~ m/$match_isproject/mi) {
619 } elsif ($self->{ss_output
} =~ m/$match_notfound/mi) {
622 $self->ss("FILETYPE \"$file\"", -3) or return -1;
624 if ($self->{ss_output
} =~ m/^$bare\s*Text/mi) {
634 ###############################################################################
636 ###############################################################################
638 # returns the full VSS path to a given project file.
640 my($self, $file) = @_;
644 $file =~ s/\/$// unless $file eq '$/';
646 return $file if $self->{implicit_projects};
648 $file = "$self->{project}$file" unless $file =~ m/^\$/;
649 $file =~ s/\/$// unless $file eq '$/'; # in case empty string was passed
654 ###############################################################################
656 ###############################################################################
658 my($self, $cmd, $silent) = @_;
660 # SS command-line tool access.
663 # 0: print everything
664 # 1: print program output only
665 # 2: print err msgs only
667 # -n: use 'n
' only if 'silent
' attribute not set
669 if (defined($silent) && $silent < 0) {
670 $silent = first {defined} $self->{silent}, $silent;
672 $silent = first {defined} $silent, $self->{silent}, 0;
675 $silent = abs($silent);
680 (my $cmd_word = lc($cmd)) =~ s/^(ss(\.exe)?\s+)?(\S+).*/$3/i;
682 $cmd = $self->{executable} . " $cmd" unless ($cmd =~ m/^ss(\.exe)?\s/i);
684 if ($self->{interactive} =~ m/^y/i) {
686 } elsif ($self->{interactive} =~ m/^n/i) {
688 } elsif (!$self->{interactive}) {
694 if (defined $self->{user} && $cmd !~ /\s-Y/i) {
695 if (defined $self->{passwd}) {
696 $disp_cmd = "$cmd -Y$self->{user},******";
697 $cmd = "$cmd -Y$self->{user},$self->{passwd}";
699 $disp_cmd = $cmd = "$cmd -Y$self->{user}";
705 warn "DEBUG: $disp_cmd\n\n" if $self->{_debug};
707 $ENV{SSDIR} = $self->{database};
709 if ($self->{use_tempfiles} &&
710 $cmd_word =~ /^(dir|filetype|history|properties)$/) {
711 my $tmpfile = "$self->{use_tempfiles}/${cmd_word}_cmd.txt";
713 $cmd = "$cmd \"-O\&$tmpfile\"";
716 if (open SS_OUTPUT, "$tmpfile") {
718 $output = scalar <SS_OUTPUT>;
722 warn "Can't
open '$cmd_word' tempfile
$tmpfile";
727 open SS_OUTPUT, '-|', "$cmd 2>&1";
729 while (<SS_OUTPUT>) {
734 $output =~ s/\s+$// if defined $output;
738 if ($self->{paginate}) {
741 foreach my $line (split "\n", $output) {
744 unless ($linecount++ % $self->{paginate}) {
745 print "Hit ENTER to
continue...\r";
762 # SourceSafe returns 1 to indicate warnings, such as no results returned
763 # from a 'DIR'. We don't want to consider these an error.
764 my $success = !($ev > 1);
767 # This is interesting. If a command only partially fails (such as GET-ing
768 # multiple files), that's apparently considered a success. So we have to
770 my $base_cmd = uc($cmd);
771 $base_cmd =~ s/^(ss\s*)?(\w+).*/$2/i;
775 if (defined($err_match = $gErrMatch{$base_cmd}) &&
776 $output =~ m/$err_match/m) {
783 $self->{ss_error} = undef;
785 $self->{ss_error} = "$disp_cmd\n$output";
788 if (!$success && ($silent == 0 || $silent == 2)) {
790 carp "\nERROR
in Vss2Svn
::VSS
-\
>ss
\n"
791 . "Command was
: $disp_cmd\n "
792 . "(Error
$ev) $output\n ";
797 $self->{ss_output} = $output;
802 ###############################################################################
804 ###############################################################################
807 print @_ unless $self->{silent};
810 ###############################################################################
811 # _vm -- "verbose message
"
812 ###############################################################################
815 print @_ if $self->{verbose};
818 ###############################################################################
820 ###############################################################################
822 my $dateFormat = $Registry->{'HKEY_CURRENT_USER/Control Panel/'
823 . 'International/iDate'} || 0;
824 my $dateSep = $Registry->{'HKEY_CURRENT_USER/Control Panel/'
825 . 'International/sDate'} || '/';
826 my $timeSep = $Registry->{'HKEY_CURRENT_USER/Control Panel/'
827 . 'International/sTime'} || ':';
828 $gCfg{dateFormat} = $dateFormat;
830 if ($dateFormat == 1) {
831 $gCfg{dateString} = "DD
${dateSep
}MM
${dateSep
}YY
";
832 } elsif ($dateFormat == 2) {
833 $gCfg{dateString} = "YY
${dateSep
}MM
${dateSep
}DD
";
835 $gCfg{dateString} = "MM
${dateSep
}DD
${dateSep
}YY
";
838 $gCfg{timeString} = "HH
${timeSep
}MM
";
840 # see ss method for explanation of this
842 GET => 'is not an existing filename or project',
843 CREATE => 'Cannot change project to',
844 CP => 'Cannot change project to',
848 version => qr/^\*+\s*Version\s+(\d+)\s*\*+\s*$/,
849 userdttm => qr/^User:\s+(.*?)\s+
850 Date:\s+(\d+)$dateSep(\d+)$dateSep(\d+)\s+
851 Time:\s+(\d+)$timeSep(\d+)([ap]*)\s*$/x,
852 comment => qr/^(Comment|Label comment): (.*)/,
853 label => qr/^Label:\s+"(.*)"/,
854 stars => qr/^\*{5,22}/,
855 stars22 => qr/^\*{22}/,
856 action => qr/^(Checked in|Labeled|Created)\s*(.*)/,
857 action0 => qr/^Rolled back/,
858 action1 => qr/^(.*)\s+(added|deleted|destroyed|purged|recovered|unpinned)$/,
859 action11 => qr/^\$\/(.*\/)?(.*) shared$/,
860 action2 => qr/^(.*) renamed to (.*)$/,
861 action3 => qr/^(.*) pinned to version (.*)$/,
865 # patterns to match development files that project_tree will ignore
876 &$code && return $_ for @_;