/[svn2cvs]/trunk/svn2cvs.pl
This is repository of my old source code which isn't updated any more. Go to git.rot13.org for current projects!
ViewVC logotype

Contents of /trunk/svn2cvs.pl

Parent Directory Parent Directory | Revision Log Revision Log


Revision 45 - (show annotations)
Sun Sep 23 22:46:53 2007 UTC (16 years, 7 months ago) by dpavlin
File MIME type: text/plain
File size: 14193 byte(s)
better check if files are in CVS before delete
1 #!/usr/bin/perl -w
2
3 # This script will transfer changes from Subversion repository
4 # to CVS repository (e.g. SourceForge) while preserving commit
5 # logs.
6 #
7 # Based on original shell version by Tollef Fog Heen available at
8 # http://raw.no/personal/blog
9 #
10 # 2004-03-09 Dobrica Pavlinusic <dpavlin@rot13.org>
11 #
12 # documentation is after __END__
13
14 use strict;
15 use File::Temp qw/ tempdir /;
16 use File::Path;
17 use Data::Dumper;
18 use XML::Simple;
19
20 # do we want to sync just part of repository?
21 my $partial_import = 1;
22
23 # do we want to add svk-like prefix with original revision, author and date?
24 my $decorate_commit_message = 1;
25
26 if ( @ARGV < 2 ) {
27 print "usage: $0 SVN_URL CVSROOT CVSREPOSITORY\n";
28 exit 1;
29 }
30
31 my ( $SVNROOT, $CVSROOT, $CVSREP ) = @ARGV;
32
33 if ( $SVNROOT !~ m,^[\w+]+:///*\w+, ) {
34 print "ERROR: invalid svn root $SVNROOT\n";
35 exit 1;
36 }
37
38 # Ensure File::Temp::END can clean up:
39 $SIG{__DIE__} = sub { chdir("/tmp"); die @_ };
40
41 my $TMPDIR = tempdir( "/tmp/checkoutXXXXX", CLEANUP => 1 );
42
43 sub cd_tmp {
44 chdir($TMPDIR) || die "can't cd to $TMPDIR: $!";
45 }
46
47 sub cd_rep {
48 chdir("$TMPDIR/$CVSREP") || die "can't cd to $TMPDIR/$CVSREP: $!";
49 }
50
51 print "## using TMPDIR $TMPDIR\n";
52
53 # cvs command with root
54 my $cvs = "cvs -f -d $CVSROOT";
55
56 # current revision in CVS
57 my $rev;
58
59 #
60 # sub to do logging and system calls
61 #
62 sub log_system($$) {
63 my ( $cmd, $errmsg ) = @_;
64 print STDERR "## $cmd\n";
65 system($cmd) == 0 || die "$errmsg: $!";
66 }
67
68 #
69 # sub to commit .svn rev file later
70 #
71 sub commit_svnrev {
72 my $rev = shift @_;
73 my $add_new = shift @_;
74
75 die "commit_svnrev needs revision" if ( !defined($rev) );
76
77 open( SVNREV, "> .svnrev" )
78 || die "can't open $TMPDIR/$CVSREP/.svnrev: $!";
79 print SVNREV $rev;
80 close(SVNREV);
81
82 my $path = ".svnrev";
83
84 if ($add_new) {
85 system "$cvs add '$path'" || die "cvs add of $path failed: $!";
86 } else {
87 my $msg = "subversion revision $rev commited to CVS";
88 print "$msg\n";
89 system "$cvs commit -m '$msg' '$path'"
90 || die "cvs commit of $path failed: $!";
91 }
92 }
93
94 sub add_dir($$) {
95 my ( $path, $msg ) = @_;
96 print "# add_dir($path)\n";
97 die "add_dir($path) is not directory" unless ( -d $path );
98
99 my $curr_dir;
100
101 foreach my $d ( split( m#/#, $path ) ) {
102 $curr_dir .= ( $curr_dir ? '/' : '' ) . $d;
103
104 next if in_entries($curr_dir);
105 next if ( -e "$curr_dir/CVS" );
106
107 log_system( "$cvs add '$curr_dir'", "cvs add of $curr_dir failed" );
108 }
109 }
110
111 # ok, now do the checkout
112 eval {
113 cd_tmp;
114 log_system( "$cvs -q checkout $CVSREP", "cvs checkout failed" );
115 };
116
117 if ($@) {
118 print <<_NEW_REP_;
119
120 There is no CVS repository '$CVSREP' in your CVS. I will assume that
121 this is import of new module in your CVS and start from revision 0.
122
123 Press enter to continue importing new CVS repository or CTRL+C to abort.
124
125 _NEW_REP_
126
127 print "start import of new module [yes]: ";
128 my $in = <STDIN>;
129 cd_tmp;
130 mkdir($CVSREP) || die "can't create $CVSREP: $!";
131 cd_rep;
132
133 open( SVNREV, "> .svnrev" ) || die "can't open $CVSREP/.svnrev: $!";
134 print SVNREV "0";
135 close(SVNREV);
136
137 $rev = 0;
138
139 # create new module
140 cd_rep;
141 log_system( "$cvs import -d -m 'new CVS module' $CVSREP svn r$rev",
142 "import of new repository" );
143 cd_tmp;
144 rmtree($CVSREP) || die "can't remove $CVSREP";
145 log_system( "$cvs -q checkout $CVSREP", "cvs checkout failed" );
146 cd_rep;
147
148 } else {
149
150 # import into existing module directory in CVS
151
152 cd_rep;
153
154 # check if svnrev exists
155 if ( !-e ".svnrev" ) {
156 print <<_USAGE_;
157
158 Your CVS repository doesn't have .svnrev file!
159
160 This file is used to keep CVS repository and Subversion in sync, so
161 that only newer changes will be commited.
162
163 It's quote possible that this is first svn2cvs run for this repository.
164 If so, you will have to identify correct svn revision which
165 corresponds to current version of CVS repository that has just
166 been checkouted.
167
168 If you migrated your cvs repository to svn using cvs2svn, this will be
169 last Subversion revision. If this is initial run of conversion of
170 Subversion repository to CVS, correct revision is 0.
171
172 _USAGE_
173
174 print "svn revision corresponding to CVS [abort]: ";
175 my $in = <STDIN>;
176 chomp($in);
177 if ( $in !~ /^\d+$/ ) {
178 print "Aborting: revision not a number\n";
179 exit 1;
180 } else {
181 $rev = $in;
182 commit_svnrev( $rev, 1 ); # create new
183 }
184 } else {
185 open( SVNREV, ".svnrev" )
186 || die "can't open $TMPDIR/$CVSREP/.svnrev: $!";
187 $rev = <SVNREV>;
188 chomp($rev);
189 close(SVNREV);
190 }
191
192 print "Starting after revision $rev\n";
193 $rev++;
194 }
195
196 #
197 # FIXME!! HEAD should really be next verison and loop because this way we
198 # loose multiple edits of same file and corresponding messages. On the
199 # other hand, if you want to compress your traffic to CVS server and don't
200 # case much about accuracy and completnes of logs there, this might
201 # be good. YMMV
202 #
203 open( LOG, "svn log -r $rev:HEAD -v --xml $SVNROOT |" )
204 || die "svn log for repository $SVNROOT failed: $!";
205 my $log;
206 while (<LOG>) {
207 $log .= $_;
208 }
209 close(LOG);
210
211 my $xml;
212 eval { $xml = XMLin( $log, ForceArray => [ 'logentry', 'path' ] ); };
213
214 #=begin log_example
215 #
216 #------------------------------------------------------------------------
217 #r256 | dpavlin | 2004-03-09 13:18:17 +0100 (Tue, 09 Mar 2004) | 2 lines
218 #
219 #ported r254 from hidra branch
220 #
221 #=cut
222
223 my $fmt = "\n" . "-" x 79 . "\nr%5s| %8s | %s\n\n%s\n";
224
225 if ( !$xml->{'logentry'} ) {
226 print "no newer log entries in Subversion repostory. CVS is current\n";
227 exit 0;
228 }
229
230 # return all files in CVS/Entries
231 sub entries($) {
232 my $dir = shift;
233 die "entries expects directory argument!" unless -d $dir;
234 my @entries;
235 open( my $fh, "./$dir/CVS/Entries" ) || return 0;
236 while (<$fh>) {
237 if ( m{^D/([^/]+)}, ) {
238 my $sub_dir = $1;
239 warn "#### entries recurse into: $dir/$sub_dir";
240 push @entries, map {"$sub_dir/$_"} entries("$dir/$sub_dir");
241 push @entries, $sub_dir;
242 } elsif (m{^/([^/]+)/}) {
243 push @entries, $1;
244 } elsif ( !m{^D$} ) {
245 die "can't decode entries line: $_";
246 }
247 }
248 close($fh);
249 warn "#### entries($dir) => ", join( "|", @entries );
250 return @entries;
251 }
252
253 # check if file exists in CVS/Entries
254 sub in_entries($) {
255 my $path = shift;
256 if ( $path =~ m,^(.*?/*)([^/]+)$, ) {
257 my ( $dir, $file ) = ( $1, $2 );
258 if ( $dir !~ m,/$, && $dir ne "" ) {
259 $dir .= "/";
260 }
261
262 open( my $fh, "./$dir/CVS/Entries" )
263 || return 0; #die "no entries file: $dir/CVS/Entries";
264 while (<$fh>) {
265 return 1 if (m{^/$file/});
266 }
267 close($fh);
268 return 0;
269 } else {
270 die "can't split '$path' to dir and file!";
271 }
272 }
273
274 cd_tmp;
275 cd_rep;
276
277 foreach my $e ( @{ $xml->{'logentry'} } ) {
278 die "BUG: revision from .svnrev ($rev) greater than from subversion ("
279 . $e->{'revision'} . ")"
280 if ( $rev > $e->{'revision'} );
281 $rev = $e->{'revision'};
282 log_system( "svn export --force -q -r $rev $SVNROOT $TMPDIR/$CVSREP",
283 "svn export of revision $rev failed" );
284
285 # deduce name of svn directory
286 my $SVNREP = "";
287 my $tmpsvn = $SVNROOT || die "BUG: SVNROOT empty!";
288 my $tmppath = $e->{'paths'}->{'path'}->[0]->{'content'}
289 || die "BUG: tmppath empty!";
290 do {
291 if ( $tmpsvn =~ s#(/[^/]+)/*$## ) { # vim fix
292 $SVNREP = $1 . $SVNREP;
293 } elsif ( $e->{'paths'}->{'path'}->[0]->{'copyfrom-path'} ) {
294 print
295 "NOTICE: copyfrom outside synced repository ignored - skipping\n";
296 next;
297 } else {
298 print "NOTICE: can't deduce svn dir from $SVNROOT - skipping\n";
299 next;
300 }
301 } until ( $tmppath =~ m/^$SVNREP/ );
302
303 print "NOTICE: using $SVNREP as directory for svn\n";
304
305 printf( $fmt,
306 $e->{'revision'}, $e->{'author'}, $e->{'date'}, $e->{'msg'} );
307 my @commit;
308
309 my $msg = $e->{'msg'};
310 $msg =~ s/'/'\\''/g; # quote "
311
312 $msg = 'r' . $rev . ' ' . $e->{author} . ' | ' . $e->{date} . "\n" . $msg
313 if $decorate_commit_message;
314
315 sub cvs_commit {
316 my $msg = shift || die "no msg?";
317 if ( !@_ ) {
318 warn "commit ignored, no files\n";
319 return;
320 }
321 log_system(
322 "$cvs commit -m '$msg' '" . join( "' '", @_ ) . "'",
323 "cvs commit of " . join( ",", @_ ) . " failed"
324 );
325 }
326
327 foreach my $p ( @{ $e->{'paths'}->{'path'} } ) {
328 my ( $action, $path ) = ( $p->{'action'}, $p->{'content'} );
329
330 next if ( $path =~ m#/\.svnrev$# );
331
332 print "svn2cvs: $action $path\n";
333
334 # prepare path and message
335 my $file = $path;
336 if ( $path !~ s#^\Q$SVNREP\E/*## ) {
337 print
338 "NOTICE: skipping '$path' which isn't under repository root '$SVNREP'\n";
339 die unless $partial_import;
340 next;
341 }
342
343 if ( !$path ) {
344 print "NOTICE: skipped this operation. Probably trunk creation\n";
345 next;
346 }
347
348 my $msg = $e->{'msg'};
349 $msg =~ s/'/'\\''/g; # quote "
350
351 sub add_path {
352 my $path = shift || die "no path?";
353
354 if ( -d $path ) {
355 add_dir( $path, $msg );
356 } elsif ( $path =~ m,^(.+)/[^/]+$, && !-e "$1/CVS/Root" ) {
357 my $dir = $1;
358 in_entries($dir) || add_dir( $dir, $msg );
359 in_entries($path) || log_system( "$cvs add '$path'",
360 "cvs add of $path failed" );
361 } else {
362 in_entries($path) || log_system( "$cvs add '$path'",
363 "cvs add of $path failed" );
364 }
365 }
366
367 if ( $action =~ /M/ ) {
368 if ( in_entries($path) ) {
369 print "svn2cvs: modify $path -- nop\n";
370 } else {
371 print "WARNING: modify $path which isn't in CVS, adding...\n";
372 add_path($path);
373 }
374 } elsif ( $action =~ /A/ ) {
375 add_path($path);
376 } elsif ( $action =~ /D/ ) {
377 if ( -e $path ) {
378 if ( ! in_entries( $path ) ) {
379 print "WARNING: $path is not present in CVS, skipping...\n";
380 undef $path;
381 } elsif ( -d $path ) {
382 warn "#### remove directory: $path";
383 foreach my $f ( entries($path) ) {
384 $f = "$path/$f";
385 if ( -f $f ) {
386 unlink($f) || die "can't delete file $f: $!";
387
388 # } else {
389 # rmtree($f) || die "can't delete dir $f: $!";
390 }
391 log_system( "$cvs delete '$f'",
392 "cvs delete of file $f failed" );
393 cvs_commit( $msg, $f );
394 }
395 log_system( "$cvs delete '$path'",
396 "cvs delete of file $path failed" );
397 cvs_commit( $msg, $path );
398 log_system( "$cvs update -dP .",
399 "cvs update -dP . failed" );
400 undef $path;
401 } else {
402 warn "#### remove file: $path";
403 unlink($path) || die "can't delete $path: $!";
404 log_system( "$cvs delete '$path'",
405 "cvs delete of dir $path failed" );
406 cvs_commit( $msg, $path );
407 undef $path;
408 }
409 } else {
410 print "WARNING: $path is not present, skipping...\n";
411 undef $path;
412 }
413 } else {
414 print
415 "WARNING: action $action not implemented on $path. Bug or missing feature of $0\n";
416 }
417
418 # save commits for later
419 push @commit, $path if ($path);
420
421 }
422
423 # now commit changes
424 cvs_commit( $msg, @commit );
425
426 commit_svnrev($rev);
427 }
428
429 # cd out of $CVSREP before File::Temp::END is called
430 chdir("/tmp") || die "can't cd to /tmp: $!";
431
432 __END__
433
434 =pod
435
436 =head1 NAME
437
438 svn2cvs - save subversion commits to (read-only) cvs repository
439
440 =head1 SYNOPSIS
441
442 ./svn2cvs.pl SVN_URL CVSROOT CVSREPOSITORY
443
444 Usage example (used to self-host this script):
445
446 ./svn2cvs.pl file:///home/dpavlin/private/svn/svn2cvs/trunk/ \
447 :pserver:dpavlin@cvs.tigris.org:/cvs svn2cvs/src
448
449 =head1 DESCRIPTION
450
451 This script will allows you to commit changes made to Subversion repository to
452 (read-only) CVS repository manually or from Subversion's C<post-commit> hook.
453
454 It's using F<.svnrev> file (which will be created on first run) in
455 B<CVSROOT/CVSREPOSITORY> to store last Subversion revision which was
456 committed into CVS.
457
458 One run will do following things:
459
460 =over 4
461
462 =item *
463 checkout B<CVSREPOSITORY> from B<CVSROOT> to temporary directory
464
465 =item *
466 check if F<.svnrev> file exists and create it if it doesn't
467
468 =item *
469 loop through all revisions from current in B<CVSROOT/CVSREPOSITORY> (using
470 F<.svnrev>) up to B<HEAD> (current one)
471
472 =over 5
473
474 =item *
475 checkout next Subversion revision from B<SVN_URL> over CVS checkout
476 temporary directory
477
478 =item *
479 make modification (add and/or delete) done in that revision
480
481 =item *
482 commit modification (added, deleted or modified files/dirs) while
483 preserving original message from CVS
484
485 =item *
486 update F<.svnrev> to match current revision
487
488 =back
489
490 =item *
491 cleanup temporary directory
492
493 =back
494
495 If checkout fails for some reason (e.g. flaky ssh connection), you will
496 still have valid CVS repository, so all you have to do is run B<svn2cvs.pl>
497 again.
498
499 =head1 WARNINGS
500
501 "Cheap" copy operations in Subversion are not at all cheap in CVS. They will
502 create multiple copies of files in CVS repository!
503
504 This script assume that you want to sync your C<trunk> (or any other
505 directory for that matter) directory with CVS, not root of your subversion.
506 This might be considered bug, but since common practise is to have
507 directories C<trunk> and C<branches> in svn and source code in them, it's
508 not serious limitation.
509
510 =head1 RELATED PROJECTS
511
512 B<Subversion> L<http://subversion.tigris.org/> version control system that is a
513 compelling replacement for CVS in the open source community.
514
515 B<cvs2svn> L<http://cvs2svn.tigris.org/> converts a CVS repository to a
516 Subversion repository. It is designed for one-time conversions, not for
517 repeated synchronizations between CVS and Subversion.
518
519 =head1 CHANGES
520
521 Versions of this utility are actually Subversion repository revisions,
522 so they might not be in sequence.
523
524 =over 3
525
526 =item r10
527
528 First release available to public
529
530 =item r15
531
532 Addition of comprehensive documentation, fixes for quoting in commit
533 messages, and support for skipping changes which are not under current
534 Subversion checkout root (e.g. branches).
535
536 =item r18
537
538 Support for importing your svn into empty CVS repository (it will first
539 create module and than dump all revisions).
540 Group commit operations to save round-trips to CVS server.
541 Documentation improvements and other small fixes.
542
543 =item r20
544
545 Fixed path deduction (overlap between Subversion reporistory and CVS checkout).
546
547 =item r21
548
549 Use C<update -d> instead of checkout after import.
550 Added fixes by Paul Egan <paulegan@mail.com> for XMLin and fixing working
551 directory.
552
553 =item r22
554
555 Rewritten import from revision 0 to empty repository, better importing
556 of deep directory structures, initial support for recovery from partial
557 commit.
558
559 =back
560
561 =head1 AUTHOR
562
563 Dobrica Pavlinusic <dpavlin@rot13.org>
564
565 L<http://www.rot13.org/~dpavlin/>
566
567 =head1 LICENSE
568
569 This product is licensed under GNU Public License (GPL) v2 or later.
570
571 =cut
572

Properties

Name Value
svn:executable

  ViewVC Help
Powered by ViewVC 1.1.26