3 # sorry to embed HTML, I'm too lazy to keep two versions of this!
4 our $USAGE = <<'EOUSAGE';
5 vss2svn.pl, Copyright (C) 2004 by Toby Johnson.
7 This program is free software; you can redistribute it and/or
8 modify it under the terms of the GNU General Public License
9 as published by the Free Software Foundation; either version 2
10 of the License, or (at your option) any later version.
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
16 http://www.gnu.org/copyleft/gpl.html
18 <b>usage: vss2svn.pl [options] --vssproject $/vss/project --svnrepo http://svn/repository/url</b>
20 <b>--vssproject</b> : full path to VSS project you want to migrate
21 <b>--svnrepo</b> : URL to target Subversion repository
24 <b>--comment=...</b> : optional text to add to end of every migrated comment
25 <b>--setdates</b> : Sets the "svn:date" property off all commits to reflect the
26 original VSS commit date, so that the original commit dates
27 (and not today's date) show up in your new SVN logs. This is
28 not the default, since setting svn:date could lead to
29 problems if not done correctly. Using this also requires the
30 "pre-revprop-change" Hook Script to be set; see
31 <a href="http://svnbook.red-bean.com/svnbook/ch05s02.html#svn-ch-5-sect-2.1">http://svnbook.red-bean.com/svnbook/ch05s02.html#svn-ch-5-sect-2.1</a>
32 <b>--login=user:pwd</b> : Set VSS username and password, separated by a colon.
33 <b>WARNING --</b> if the username/password combo you provide is
34 incorrect, this program will hang as SS prompts you for
35 a username! Even after I set the <b>ss.exe -I-</b> option, which
36 MICROS~1 claims means "to ensure that VSS never asks for user
38 <b>--noprompt</b> : Don't prompt user to create usernames after the first stage
39 of the migration (see last paragraph below)
41 The URL you provide for "svnrepo" will become the base URL for all migrated
42 files, so for the usage example above, <b>$/vss/project/foo.c</b> would become
43 <b>http://svn/repository/url/foo.c</b>. Plan your migration accordingly so that you
44 end up with the structure that you want. The URL also cannot contain any
45 existing files; but as long as the "parent" of the URL is a Subversion
46 repository, any non-existent directories in the URL will be created.
48 The <b>$SSDIR</b> environment variable must be set to the directory where your
49 system srcsafe.ini file is located; see the VSS online help for more info.
50 The "svn" and "ss" command-line executables must also be in your PATH.
52 This script is released into the public domain. In case you're wondering
53 about why the Vss2Svn packages have unused methods, it's because they came
54 from in-house modules which had more functionality than just this conversion.
56 I recommend converting only a small branch at first to see how things go.
57 This process takes a very long time for large databases. I have made liberal
59 Partway through the migration, you will be presented with a list of all
60 usernames which performed any checkin operations in the given VSS project.
61 If you want these user names to be preserved, you must add this list
62 (including a user "vss_migration" for creating directories and such) to your
63 Apache AuthUserFile with *blank passwords*. Apache must also *require* that
64 usernames be passed, otherwise SVN will use anonymous access and you lose
65 the usernames. So you need an "AuthType Basic" line or the like, as well as
66 an AuthUserFile. See <a href="http://svnbook.red-bean.com/svnbook/ch06s04.html#svn-ch-6-sect-4.3">http://svnbook.red-bean.com/svnbook/ch06s04.html#svn-ch-6-sect-4.3</a>
80 our(%gCfg, $VSS, $SVN, $TREE, %USERS,);
82 # http://www.perl.com/tchrist/defop/defconfaq.html#What_is_the_proposed_operat
85 &Vss2Svn
::Subversion
::Initialize
;
86 &Vss2Svn
::VSS
::Initialize
;
88 sub PrintMsg
; # defined later
90 warn "\n\n**** BUILDING INITIAL STRUCTURES; PLEASE WAIT... ****\n\n";
95 # redirect STDERR to logfile
96 open THE_REAL_STDERR
, ">&STDERR";
97 $gCfg{logfile
} = "$gCfg{workbase}/logfile.txt";
98 open STDERR
, ">$gCfg{logfile}"
99 or die "Couldn't open logfile $gCfg{workbase}/logfile.txt";
101 # the svn client program outputs to STDOUT; redirect to STDERR instead
102 open STDOUT
, ">&STDERR";
104 select THE_REAL_STDERR
;
108 # since we redirected STDERR, make sure user sees die() messages!
109 $SIG{__DIE__
} = \
&MyDie
;
113 &GiveUserMessage
unless $gCfg{noprompt
};
121 PrintMsg
"\n\n**** VSS MIGRATION COMPLETED SUCCESSFULLY!! ****\n";
124 open STDERR
, ">&THE_REAL_STDERR"; # yes, we're about to exit, but leaving
125 # STDERR dangling always makes me nervous!
127 close THE_REAL_STDERR
;
131 ###############################################################################
133 ###############################################################################
135 $TREE = $VSS->project_tree($gCfg{vssproject
},1,1)
136 or die "Couldn't create project tree for $gCfg{vssproject}";
139 ###############################################################################
141 ###############################################################################
143 chdir "$gCfg{importdir}" or die;
145 PrintMsg
"\n\n**** BUILDING VSS HISTORY ****\n\n";
147 &WalkTreeBranch
($TREE, $gCfg{vssproject
});
150 ###############################################################################
152 ###############################################################################
154 my($branch, $project) = @_;
155 PrintMsg
"ENTERING PROJECT $project...\n";
157 my($key, $val, $newproj);
160 foreach $key (sort keys %$branch) {
161 $val = $branch->{$key};
163 if (ref($val) eq 'HASH') {
164 # subproject; create a new branch of the tree
166 push @branches, {branch
=> $val, project
=> "$key"};
168 } elsif (!ref $val) {
169 # a scalar, i.e. regular file
171 &AddFileHistory
($project, $key);
176 foreach my $subbranch (@branches) {
177 mkdir $subbranch->{project
};
178 chdir $subbranch->{project
} or die;
180 ($newproj = "$project/$subbranch->{project}") =~ s
://:/:;
182 &WalkTreeBranch
($subbranch->{branch
}, $newproj);
188 ###############################################################################
190 ###############################################################################
192 my($project, $file) = @_;
194 # build the revision history for this file
196 (my $filepath = "$project/$file") =~ s
://:/:;
197 my $filehist = $VSS->file_history("$filepath");
198 die if !defined $filehist;
200 PrintMsg
" $filepath\n";
202 foreach my $rev (@
$filehist) {
203 $gCfg{globalCount
}++;
205 $rev->{user
} = lc( $rev->{user
} ); # normalize usernames to lowercase
206 $rev->{comment
} .= "\n\n$gCfg{comment}" if defined $gCfg{comment
};
208 $rev->{date
} =~ s/-//g;
209 $rev->{time} =~ s/://;
211 &InsertDatabaseRevision
($filepath, $rev);
213 $USERS{ $rev->{user
} } = 1;
218 ###############################################################################
219 # InsertDatabaseRevision
220 ###############################################################################
221 sub InsertDatabaseRevision
{
222 my($filepath, $rev) = @_;
224 my %data = %$rev; # don't pollute $rev
226 #quote the text fields
227 map { $data{$_} = $gCfg{dbh
}->quote( $rev->{$_} ) }
228 qw(date time user comment);
230 $filepath = $gCfg{dbh
}->quote($filepath);
256 $gCfg{dbh
}->do($cmd) or die;
258 } #End InsertDatabaseRevision
260 ###############################################################################
262 ###############################################################################
263 sub GiveUserMessage
{
264 print THE_REAL_STDERR
<<"EOTXT";
267 Following is a list of all VSS users who have made updates at any time in the
268 specified project. In order to preserve the user history during migration to
269 Subversion, these users must exist in the Subversion authentication file.
271 Usually, this is done with an Apache "Basic" HTTP authorization file, where
272 each username is followed by a colon and the hashed password for that user.
273 A blank password is permissible. Copy and paste the following lines into this
274 authorization file in order to allow this user history to be migrated.
278 print THE_REAL_STDERR
join("\n", map {"$_:"} sort keys %USERS),
279 "\n\nPRESS ENTER TO CONTINUE (or enter [q] to quit and start over)...";
284 print THE_REAL_STDERR
"\n\nQuitting...\n";
289 ###############################################################################
291 ###############################################################################
292 sub SetupSvnProject
{
293 PrintMsg
"\n\n**** SETTING UP SUBVERSION DIRECTORIES ****\n\n";
295 chdir $gCfg{importdir
} or die;
296 $SVN->do('import', '.', '--message "Initial Import"', 0) or die;
298 chdir $gCfg{workdir
} or die;
299 $SVN->do('checkout', '', '"."') or die;
302 ###############################################################################
304 ###############################################################################
305 sub ImportSvnHistory
{
306 # we will walk the history table in date/time order, GETting from VSS
307 # as we go. VSS doesn't allow atomic multi-item commits, so we'll detect
308 # these assuming if the user and comment are the same from one item to the
309 # next, they were part of the "same" action.
311 my($row, $upd, $commitinfo);
313 my %prev = (user
=> '', comment
=> '', grain
=> 0);
314 my %all = (); # hash of all files ever added
315 my %thistime = (); # hash of files added on this commit
318 my $grain = 0.000001;
320 PrintMsg
"\n\n**** MIGRATING VSS HISTORY TO SUBVERSION ****\n\n";
322 # date, time, and file fields are formatted to enable sorting numerically
323 my $cmd = "SELECT * FROM history ORDER BY date, time, file";
324 my $sth = $gCfg{dbh
}->prepare($cmd) or die;
325 $sth->execute or die;
328 while ($row = $sth->fetchrow_hashref) {
329 $row->{date
} =~ s/(....)(..)(..)/$1-$2-$3/;
330 $row->{time} =~ s/(..)(..)/$1:$2/;
332 if (!exists $row->{comment
} || !defined $row->{comment
});
334 PrintMsg
" ($gCfg{commitNumber})File $row->{file}, "
335 . "$row->{date} $row->{time}...\n";
337 if (defined $prev{date
} &&
338 ($row->{date
} eq $prev{date
}) &&
339 ($row->{user
} eq $prev{user
}) &&
340 ($row->{comment
} eq $prev{comment
}) &&
341 (!defined $thistime{ $row->{file
} })) {
343 # user and comment are same; this will be multi-item commit
346 } elsif ($multiple) {
347 # we're in a multi-item commit but user or comment changed;
348 # commit previous action
350 &CommitSvn
(1, $prev{comment
}, $commitinfo);
352 &SetSvnDates
(\
%prev) if $gCfg{setdates
};
355 } elsif (defined $commitinfo) {
356 # we're not in a multi-item commit and user or comment
357 # changed; commit the single previous file
360 &CommitSvn
(0, $prev{comment
}, $commitinfo);
362 &SetSvnDates
(\
%prev) if $gCfg{setdates
};
366 if (defined $prev{date
} && ($row->{date
} ne $prev{date
})) {
369 if (defined $commitinfo) {
370 # done with this date, so commit what we have so far
371 &CommitSvn
($multiple, $prev{comment
}, $commitinfo);
374 &SetSvnDates
(\
%prev) if $gCfg{setdates
};
382 $upd = $all{ $row->{file
} }++;
383 $commitinfo = &GetVssRevision
($row, $upd, \
%thistime,);
385 %prev = (%$row, (grain
=> $grain));
390 if (defined $commitinfo) {
391 &CommitSvn
($multiple, $prev{comment
}, $commitinfo);
393 &SetSvnDates
(\
%prev) if $gCfg{setdates
};
401 ###############################################################################
403 ###############################################################################
405 my($row, $upd, $thisRef) = @_;
406 # Gets a version of a file from VSS and adds it to SVN
407 # $row is the row hash ref from the history SQLite table
408 # $upd is true if this is an update rather than add
410 my $vsspath = $row->{file
};
412 $row->{file
} =~ m/^(.*\/)(.*)/ or die;
413 my($path, $file) = ($1, $2);
415 $path =~ s/$gCfg{vssprojmatch}// or die;
416 $path =~ s/\/$//; # remove trailing slash
418 (my $dospath = "$gCfg{workdir}/$path") =~ s
/\
//\\/g
; # use backslashes
419 $dospath =~ s/\\$//; # remove trailing backslash if $path was empty
420 $dospath =~ s/\\\\/\\/g; # replace double backslashes with single
422 my $cmd = "GET -GTM -W -GL\"$dospath\" -V$row->{version} \"$vsspath\"";
423 $VSS->ss($cmd) or die;
425 chdir $dospath or die;
428 $SVN->svn("add \"$file\"") or die;
433 user
=> $row->{user
},
434 dospath
=> $dospath,};
436 $thisRef->{ $row->{file
} } = 1;
441 ###############################################################################
443 ###############################################################################
445 my($multiple, $comment, $commitinfo) = @_;
447 open COMMENTFILE
, ">$gCfg{tmpfiledir}/comment.txt" or die;
448 print COMMENTFILE
$comment;
451 $multiple?
&CommitMultipleItems
($commitinfo)
452 : &CommitSingleItem
($commitinfo);
454 $gCfg{commitNumber
}++;
458 ###############################################################################
460 ###############################################################################
461 sub CommitSingleItem
{
462 my($commitinfo) = @_;
464 warn "SINGLE COMMIT\n";
465 chdir $commitinfo->{dospath
} or die;
466 $SVN->{user
} = $commitinfo->{user
};
467 $SVN->svn("commit --file \"$gCfg{tmpfiledir}/comment.txt\" "
468 . "--non-recursive $commitinfo->{file}") or die;
471 ###############################################################################
472 # CommitMultipleItems
473 ###############################################################################
474 sub CommitMultipleItems
{
475 my($commitinfo) = @_;
477 warn "MULTIPLE COMMIT\n";
478 chdir $gCfg{workdir
} or die;
479 $SVN->{user
} = $commitinfo->{user
};
480 $SVN->svn("commit --file \"$gCfg{tmpfiledir}/comment.txt\" \".\"") or die;
483 ###############################################################################
485 ###############################################################################
489 my $grain = sprintf '%0.6f', $info->{grain
};
490 my $svn_date = "$info->{date}T$info->{time}:${grain}Z";
492 my $cmd = "propset --revprop -rHEAD svn:date $svn_date $gCfg{svnrepo}";
493 $SVN->svn($cmd) or die;
497 ###############################################################################
499 ###############################################################################
500 sub RecursiveDelete
{
504 opendir(DIR
, $parent);
505 @dirs = readdir(DIR
);
508 foreach $dir (@dirs) {
509 if ($dir ne '.' && $dir ne '..') {
510 &RecursiveDelete
("$parent/$dir");
523 ###############################################################################
525 ###############################################################################
527 # print to logfile (redirected STDERR) and screen (STDOUT)
529 print THE_REAL_STDERR
@_;
532 ###############################################################################
534 ###############################################################################
536 # any die() is trapped by $SIG{__DIE__} to ensure user sees fatal errors
538 print THE_REAL_STDERR
"\n", @_;
540 (my $logfile = $gCfg{logfile
}) =~ s
:/:\\:g
;
541 print THE_REAL_STDERR
<<"EOERR";
543 A fatal error has occured. See $logfile for more information.
548 ###############################################################################
550 ###############################################################################
552 GetOptions
(\
%gCfg,'vssproject=s','svnrepo=s','comment=s','login=s',
553 'setdates','noprompt','help');
555 &GiveHelp
(undef, 1) if defined $gCfg{help
};
557 defined $gCfg{vssproject
} or GiveHelp
("must specify --vssproject\n");
558 defined $gCfg{svnrepo
} or GiveHelp
("must specify --svnrepo\n");
559 defined $ENV{SSDIR
} or GiveHelp
("\$SSDIR not defined");
561 $gCfg{vssproject
} =~ s
:/$:: unless $gCfg{vssproject} eq '$/';
562 $gCfg{vssprojmatch} = quotemeta( $gCfg{vssproject} );
564 $VSS = Vss2Svn::VSS->new($ENV{SSDIR}, $gCfg{vssproject});
565 $VSS->{interactive} = 'Y
';
568 if (defined $gCfg{login}) {
569 @{$VSS}{'user
', 'passwd
'} = split ':', $gCfg{login};
572 $SVN = Vss2Svn::Subversion->new( $gCfg{svnrepo} );
573 $SVN->{interactive} = 0;
574 $SVN->{user} = 'vss_migration
';
575 $SVN->{passwd} = ''; # all passwords are blank
578 %USERS = ( vss_migration => 1, );
580 $gCfg{globalCount} = 1;
581 $gCfg{commitNumber} = 1;
583 $gCfg{workbase} = cwd() . "/_vss2svn";
584 &RecursiveDelete( $gCfg{workbase} );
585 mkdir $gCfg{workbase} or die "Couldn't create
$gCfg{workbase
}";
587 $gCfg{workdir} = "$gCfg{workbase
}/work
";
588 mkdir $gCfg{workdir} or die "Couldn
't create $gCfg{workdir}";
590 $gCfg{importdir} = "$gCfg{workbase}/import";
591 mkdir $gCfg{importdir} or die "Couldn't create
$gCfg{importdir
}";
593 $gCfg{tmpfiledir} = "$gCfg{workbase
}/tmpfile
";
594 mkdir $gCfg{tmpfiledir} or die "Couldn
't create $gCfg{tmpfiledir}";
596 $gCfg{dbdir} = "$gCfg{workbase}/db";
597 mkdir $gCfg{dbdir} or die "Couldn't create
$gCfg{dbdir
}";
599 $VSS->{use_tempfiles} = "$gCfg{tmpfiledir
}";
602 ###############################################################################
604 ###############################################################################
606 $gCfg{dbh} = DBI->connect("dbi
:SQLite
(RaiseError
=>1,AutoCommit
=>0)"
607 . ":dbname
=$gCfg{dbdir
}/vss2svn
.db
","","");
613 date char(8) NOT NULL,
614 time char(5) NOT NULL,
615 file varchar(1024) NOT NULL,
616 version long NOT NULL,
617 user varchar(256) NOT NULL,
618 comment blob NOT NULL,
619 global_count long NOT NULL
623 $gCfg{dbh
}->do($cmd) or die;
624 } #End CreateDatabase
626 ###############################################################################
628 ###############################################################################
631 $gCfg{dbh
}->disconnect;
634 ###############################################################################
636 ###############################################################################
638 my($msg, $full) = @_;
639 $msg .= "\n" if defined $msg;
643 usage: vss2svn.pl [options] --vssproject \$/vss/project --svnrepo http://svn/repository/url
645 --vssproject : full path to VSS project you want to migrate
646 --svnrepo : URL to target Subversion repository
647 --help : see full help info
649 USE --help TO VIEW ALL OPTIONAL PARAMETERS
652 exit(0) unless $full;
654 # de-html; kinda kludgy but gets the job done
655 $USAGE =~ s
:</?
(a
( href
=".*?")?
|b
)>::g
;
665 &$code && return $_ for @_;
672 ###############################################################################
674 ###############################################################################
687 our $VERSION = '1.00';
689 ###############################################################################
691 ###############################################################################
693 my($self, $user, $passwd) = @_;
695 $self->{user
} = $user;
698 no warnings
'uninitialized'; # we want to undef passwd if none passed
699 $self->{passwd
} = $passwd unless $passwd eq '';
709 ###############################################################################
710 # package Vss2Svn::Subversion #
711 ###############################################################################
713 package Vss2Svn
::Subversion
;
723 &$code && return $_ for @_;
735 our(%gInteractiveCmds);
737 ###############################################################################
739 ###############################################################################
741 my($class, $svnrep, $project) = @_;
743 if (!defined $svnrep) {
744 croak
"Must specify Subversion repository URL";
747 $project = '' if ! defined $project;
751 repository
=> $svnrep,
763 implicit_projects
=> undef,
770 # test to ensure 'svn' command is available
771 $self->svn("help", -2) or
772 croak
"Could not run Subversion 'svn' command: "
773 . "ensure it is in your PATH";
775 $self->set_project($project);
781 ###############################################################################
783 ###############################################################################
785 my($self, $project) = @_;
788 $self->{project
} = $project;
792 ###############################################################################
794 ###############################################################################
796 my($self, $cmd, $file, $args, $silent) = @_;
798 # basically a wrapper for "svn" to set current project and repository
800 my $url = "$self->{repository}/";
801 $url .= $self->{project
}
802 if defined $self->{project
} && $self->{project
} ne '';
804 $url .= $file if defined $file;
805 $args = '' unless defined $args;
807 return $self->svn("$cmd $url $args", $silent);
810 ###############################################################################
812 ###############################################################################
814 my($self, $cmd, $silent) = @_;
815 # "raw" svn client access.
818 # 0: print everything
819 # 1: print program output only
820 # 2: print err msgs only
822 # -n: use 'n' only if 'silent' attribute not set
824 if (defined($silent) && $silent < 0) {
825 $silent = first
{defined} $self->{silent
}, $silent;
827 $silent = first
{defined} $silent, $self->{silent
}, 0;
830 $silent = abs($silent);
832 $cmd =~ s/^\s*(svn)?\s*//; #take off "svn" if present; we'll add back later
839 if (defined $gInteractiveCmds{$1} && !$self->{interactive
}) {
840 $cmd = "$cmd --non-interactive";
842 if (defined $self->{user
} && $cmd !~ /--username/) {
843 if (defined $self->{passwd
} && $cmd !~ /--password/) {
844 $disp_cmd = "$cmd --username \"$self->{user}\" --password *****";
845 $cmd = "$cmd --username \"$self->{user}\" "
846 . "--password \"$self->{passwd}\"";
848 $disp_cmd = $cmd = "$cmd --username \"$self->{user}\"";
858 warn "DEBUG: $disp_cmd\n\n" if $self->{_debug
};
860 open CMDOUT
, '-|', "$cmd 2>&1";
867 $output =~ s/\s+$// if defined $output;
870 if ($self->{paginate
}) {
873 foreach my $line (split "\n", $output) {
876 unless ($linecount++ % $self->{paginate
}) {
877 print "Hit ENTER to continue...\r";
895 if (!$success && ($silent == 0 || $silent == 2)) {
897 carp
"\nERROR in Vss2Svn::Subversion-\>ss\n"
898 . "Command was: $disp_cmd\n "
899 . "(Error $ev) $output\n ";
904 $self->{svn_output
} = $output;
909 ###############################################################################
911 ###############################################################################
914 # commands which allow --non-interactive
915 %gInteractiveCmds = ( map {$_,1 }
916 qw(blame cat checkout co commit ci copy cp delete del
917 remove rm diff di export import list ls log merge
918 mkdir move rename rn propdel pdel pd propedit pedit pe
919 propget pget pg proplist plist pl propset pset ps
920 status stat st switch sw update up))
924 ###############################################################################
925 # package Vss2Svn::VSS #
926 ###############################################################################
928 package Vss2Svn
::VSS
;
944 our $VERSION = '1.05';
946 our(%gErrMatch, %gHistLineMatch, @gDevPatterns);
948 ###############################################################################
950 ###############################################################################
952 my($class, $db, $project) = @_;
955 croak
"Must specify VSS database path";
958 $db =~ s/[\/\\]?(srcsafe.ini)?$//i
;
960 if (defined $project && $project ne ''
961 && $project ne '$' && $project !~ /^\$\//) {
962 croak
"Project path must be absolute (begin with $/)";
965 $project = first
{defined} $project, '$/';
976 last_ss_output
=> undef,
980 implicit_projects
=> undef,
987 # test to ensure 'ss' command is available
988 $self->ss("WHOAMI", -2) or
989 croak
"Could not run VSS 'ss' command: ensure it is in your PATH";
991 $self->{_whoami
} = $self->{last_ss_output
};
992 $self->{_whoami
} =~ s/\s*$//;
993 $self->{_whoami
} =~ s/^.*\n//;
995 if ($self->{last_ss_output
} =~ /changing project/im ||
996 !$self->_check_ss_inifile) {
997 croak
"FATAL ERROR: You must not set the Force_Dir or Force_Prj VSS\n"
998 . "variables when running SourceSync. These variables can be\n"
999 . "cleared by unchecking the two \"Assume...\" boxes in SourceSafe\n"
1000 . "Explorer under Tools -> Options -> Command Line Options.\n ";
1003 if ($project eq '') {
1004 $self->ss('PROJECT', -2);
1006 $project = $self->{last_ss_output
};
1007 $project =~ s/^Current project is *//i;
1008 $project .= '/' unless $project =~ m
/\
/$/;
1010 $self->{project
} = $project;
1012 $self->set_project($project);
1019 ###############################################################################
1021 ###############################################################################
1022 sub _check_ss_inifile
{
1025 my $user = lc($self->{_whoami
});
1026 my $path = "$self->{database}/users/$user/ss.ini";
1028 open SSINI
, $path or croak
"Could not open user init file $path";
1042 } # End _check_ss_inifile
1044 ###############################################################################
1046 ###############################################################################
1048 my($self, $project) = @_;
1050 $project .= '/' unless $project =~ m
/\
/$/;
1052 $self->ss("CP \"$project\"", -2) or
1053 croak
"Could not set current project to $project:\n"
1054 . " $self->{last_ss_output}\n ";
1056 $self->{project
} = $project;
1060 ###############################################################################
1062 ###############################################################################
1064 my($self, $project, $recursive, $remove_dev) = @_;
1066 # returns a nested-hash "tree" of all subprojects and files below the given
1067 # project; the "leaves" of regular files are the value "1".
1069 $project = $self->full_path($project);
1070 $recursive = 1 unless defined $recursive;
1071 $remove_dev = 0 unless defined $remove_dev;
1073 if ($self->filetype($project) ) { # projects are type 0
1074 carp
"project_tree(): '$project' is not a valid project";
1078 my $cmd = "DIR \"$project\"";
1079 $cmd .= ($recursive)?
' -R' : ' -R-';
1081 $self->ss($cmd, -2) or return undef;
1083 # It would be nice if Microsoft made it easy for scripts to pick useful
1084 # information out of the project 'DIR' listings, but unfortunately that's
1085 # not the case. It appears that project listings always follow blank
1086 # lines, and begin with the full project path with a colon appended.
1087 # Within a listing, subprojects come first and begin with a dollar sign,
1088 # then files are listed alphabetically. If there are no items in a project,
1089 # it prints out a message saying so. And at the end of it all, you get
1090 # a statement like "7 item(s)".
1093 my $branch_ref = \
%tree;
1095 my $seen_blank_line = 0;
1096 my($current_project);
1097 my $match_project = quotemeta($project);
1100 foreach my $line (split "\n", $self->{last_ss_output
}) {
1104 if ($seen_blank_line) {
1105 carp
"project_tree(): an internal error has occured -- 1";
1109 $seen_blank_line = 1;
1113 $seen_blank_line = 0;
1115 if ($line =~ m/^\d+\s+item\(s\)$/i) {
1116 # this is a count of # of items found; ignore
1119 } elsif ($line =~ m/^No items found under/i) {
1123 } elsif ($line =~ m/^(\$\/.*):$/) {
1124 # this is the beginning of a project's listing
1125 $current_project = $1;
1126 # make current project relative to initial
1127 $current_project =~ s/^$match_project\/?//i
;
1128 $current_project =~ s/^\$\///; # take off initial $/ if still there
1130 $branch_ref = \
%tree;
1132 if ($current_project ne '') {
1133 # get a reference to the end branch of subprojects
1134 ($branch_ref) = reverse(map {$branch_ref = $branch_ref->{$_}}
1135 split('/', $current_project));
1138 if (!defined $branch_ref) {
1139 carp
"project_tree(): an internal error has occured -- 2";
1144 } elsif ($line =~ m/^\$(.*)/) {
1145 # this is a subproject; create empty hash if not already there
1146 if (!defined $current_project) {
1147 carp
"project_tree(): an internal error has occured -- 3";
1151 $branch_ref->{$1} = {} unless defined($branch_ref->{$1});
1153 # just a regular file
1154 if (!defined $current_project) {
1155 carp
"project_tree(): an internal error has occured -- 4";
1160 foreach my $pattern (@gDevPatterns) {
1161 next LINE
if $line =~ m/$pattern/;
1165 $branch_ref->{$line} = 1;
1172 } # End project_tree
1174 ###############################################################################
1176 ###############################################################################
1178 my($self, $file) = @_;
1179 # returns an array ref of hash refs from earliest to most recent;
1180 # each hash has the following items:
1181 # version: version (revision) number
1182 # user : name of user who committed change
1183 # date : date in YYYYMMDD format
1184 # time : time in HH:MM (24h) format
1185 # comment: checkin comment
1187 $file = $self->full_path($file);
1189 if ($self->filetype($file) < 1) { # regular files are type 1 or 2
1190 carp
"file_history(): '$file' is not a valid regular file";
1194 my $cmd = "HISTORY \"$file\"";
1197 $self->ss($cmd, -2) or return undef;
1201 my $last = 0; # what type was the last line read?
1202 # 0=start;1=version line;2=user/date/time;3="Checked In";
1205 my $last_version = -1;
1207 my$rev = {}; # hash of info for the lastent revision
1208 my($year,$month,$day,$hour,$min,$ampm,$comment,$version);
1211 foreach my $line (split "\n", $self->{last_ss_output
}) {
1212 if ($self->{_debug
}) {
1213 warn "\nDEBUG:($last)<$line>\n";
1217 if ($line =~ m/$gHistLineMatch{version}/) {
1219 if ($last_version == 0 ||
1220 (($last_version != -1) && ($1 != ($last_version - 1)))) {
1222 # each version should be one less than the last
1223 print "file_history(): internal consistency failure";
1228 $rev->{version
} = $1;
1235 if ($line =~ m/$gHistLineMatch{userdttm}/) {
1239 # TIMEFORMAT: modify the code below to pull dates and times out according to your locale
1240 ($rev->{user
}, $month, $day, $year, $hour, $min, $ampm)
1241 = ($1, $2, $3, $4, $5, $6, $7);
1243 $month = sprintf "%2.2i", $month;
1244 $day = sprintf "%2.2i", $day;
1245 $year = ($year > 79)?
"19$year" : "20$year";
1247 $hour += 12 if $ampm =~ /p/i;
1248 $hour = sprintf "%2.2i", $hour;
1250 $rev->{date
} = "$year-$month-$day";
1251 $rev->{time} = "$hour:$min";
1252 } elsif ($line =~ m/$gHistLineMatch{label}/) {
1253 # this is an inherited Label; ignore it
1256 # user, date, and time should always come after header line
1257 print "file_history(): internal consistency failure";
1265 if ($line =~ s/$gHistLineMatch{comment}//) {
1274 if ($line =~ m/$gHistLineMatch{version}/) {
1278 $comment =~ s/\s+$//;
1279 $comment =~ s/^\s+//;
1280 $rev->{comment
} = $comment;
1282 unshift @
$hist, $rev;
1285 $rev->{version
} = $version;
1287 $comment .= "\n$line";
1295 $comment =~ s/\n/ /g;
1296 $comment =~ s/\s+$//;
1297 $comment =~ s/^\s+//;
1298 $rev->{comment
} = $comment;
1300 # last line of history should always be part of a comment, but
1301 # sometimes VSS doesn't include the final comment line
1302 $rev->{comment
} = '(no comment)';
1305 unshift @
$hist, $rev;
1309 ###############################################################################
1311 ###############################################################################
1318 my($self, $file) = @_;
1319 return -1 unless defined $file;
1324 return 0 if $file eq '$/';
1325 return -1 if $file eq '$';
1327 # VSS has no decent way of determining whether an item is a project of
1328 # a file, so we do this in a somewhat roundabout way
1330 $file =~ s/[\/\\]$//;
1333 $bare =~ s/.*[\/\\]//;
1334 $bare = quotemeta($bare);
1336 $self->ss("PROPERTIES \"$file\" -R-", -3) or return -1;
1338 my $match_isproject = "^Project:.*$bare\\s*\$";
1339 my $match_notfound = "$bare\\s*is not an existing filename or project";
1341 if ($self->{last_ss_output
} =~ m/$match_isproject/mi) {
1343 } elsif ($self->{last_ss_output
} =~ m/$match_notfound/mi) {
1346 $self->ss("FILETYPE \"$file\"", -3) or return -1;
1348 if ($self->{last_ss_output
} =~ m/^$bare\s*Text/mi) {
1358 ###############################################################################
1360 ###############################################################################
1362 # returns the full VSS path to a given project file.
1364 my($self, $file) = @_;
1368 $file =~ s/\/$// unless $file eq '$/';
1370 return $file if $self->{implicit_projects};
1372 $file = "$self->{project}$file" unless $file =~ m/^\$/;
1373 $file =~ s/\/$// unless $file eq '$/'; # in case empty string was passed
1378 ###############################################################################
1380 ###############################################################################
1382 my($self, $cmd, $silent) = @_;
1384 # SS command-line tool access.
1387 # 0: print everything
1388 # 1: print program output only
1389 # 2: print err msgs only
1391 # -n: use 'n
' only if 'silent
' attribute not set
1393 if (defined($silent) && $silent < 0) {
1394 $silent = first {defined} $self->{silent}, $silent;
1396 $silent = first {defined} $silent, $self->{silent}, 0;
1399 $silent = abs($silent);
1404 (my $cmd_word = lc($cmd)) =~ s/^(ss(\.exe)?\s+)?(\S+).*/$3/i;
1406 $cmd = "ss $cmd" unless ($cmd =~ m/^ss(\.exe)?\s/i);
1408 if ($self->{interactive} =~ m/^y/i) {
1410 } elsif ($self->{interactive} =~ m/^n/i) {
1412 } elsif (!$self->{interactive}) {
1416 my $disp_cmd = $cmd;
1418 if (defined $self->{user} && $cmd !~ /\s-Y/i) {
1419 if (defined $self->{passwd}) {
1420 $disp_cmd = "$cmd -Y$self->{user},******";
1421 $cmd = "$cmd -Y$self->{user},$self->{passwd}";
1423 $disp_cmd = $cmd = "$cmd -Y$self->{user}";
1429 warn "DEBUG: $disp_cmd\n\n" if $self->{_debug};
1431 $ENV{SSDIR} = $self->{database};
1433 if ($self->{use_tempfiles} &&
1434 $cmd_word =~ /^(dir|filetype|history|properties)$/) {
1435 my $tmpfile = "$self->{use_tempfiles}/${cmd_word}_cmd.txt";
1437 $cmd = "$cmd \"-O\&$tmpfile\"";
1440 if (open SS_OUTPUT, "$tmpfile") {
1442 $output = scalar <SS_OUTPUT>;
1446 warn "Can't
open '$cmd_word' tempfile
$tmpfile";
1451 open SS_OUTPUT, '-|', "$cmd 2>&1";
1453 while (<SS_OUTPUT>) {
1458 $output =~ s/\s+$// if defined $output;
1462 if ($self->{paginate}) {
1465 foreach my $line (split "\n", $output) {
1468 unless ($linecount++ % $self->{paginate}) {
1469 print "Hit ENTER to
continue...\r";
1486 # SourceSafe returns 1 to indicate warnings, such as no results returned
1487 # from a 'DIR'. We don't want to consider these an error.
1488 my $success = !($ev > 1);
1491 # This is interesting. If a command only partially fails (such as GET-ing
1492 # multiple files), that's apparently considered a success. So we have to
1494 my $base_cmd = uc($cmd);
1495 $base_cmd =~ s/^(ss\s*)?(\w+).*/$2/i;
1499 if (defined($err_match = $gErrMatch{$base_cmd}) &&
1500 $output =~ m/$err_match/m) {
1506 if (!$success && ($silent == 0 || $silent == 2)) {
1508 carp "\nERROR
in Vss2Svn
::VSS
-\
>ss
\n"
1509 . "Command was
: $disp_cmd\n "
1510 . "(Error
$ev) $output\n ";
1515 $self->{last_ss_output} = $output;
1520 ###############################################################################
1522 ###############################################################################
1525 print @_ unless $self->{silent};
1528 ###############################################################################
1529 # _vm -- "verbose message
"
1530 ###############################################################################
1533 print @_ if $self->{verbose};
1536 ###############################################################################
1538 ###############################################################################
1540 # see ss method for explanation of this
1542 GET => 'is not an existing filename or project',
1543 CREATE => 'Cannot change project to',
1544 CP => 'Cannot change project to',
1547 # TIMEFORMAT: modify "userdttm
" below if necessary for your format.
1549 version => qr/^\*+\s*Version\s+(\d+)\s*\*+\s*$/,
1550 userdttm => qr/^User:\s+([\S]+)\s+Date:\s+(\d+)\/(\d+)
1551 \/(\d+)\s+Time:\s+(\d+):(\d+)([ap]*)\s*$/x,
1552 comment => qr/^Comment:\s*/,
1553 label => qr/^Label:/,
1556 # patterns to match development files that project_tree will ignore
1568 &$code && return $_ for @_;