/[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 42 - (show annotations)
Sat Sep 22 13:57:28 2007 UTC (16 years, 7 months ago) by dpavlin
File MIME type: text/plain
File size: 14072 byte(s)
tidy more similar to my own style
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 ( -d $path ) {
379 warn "#### remove directory: $path";
380 my @sub_commit;
381 foreach my $f ( entries($path) ) {
382 $f = "$path/$f";
383 if ( -f $f ) {
384 unlink($f) || die "can't delete file $f: $!";
385
386 # } else {
387 # rmtree($f) || die "can't delete dir $f: $!";
388 }
389 log_system( "$cvs delete '$f'",
390 "cvs delete of file $f failed" );
391 push @sub_commit, $f;
392 }
393 log_system( "$cvs delete '$path'",
394 "cvs delete of file $path failed" );
395 cvs_commit( $msg, @sub_commit, $path );
396 log_system(
397 "$cvs update -dP '$path'",
398 "cvs update -dP $path failed"
399 );
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 }
407 } else {
408 print "WARNING: $path is not present, skipping...\n";
409 undef $path;
410 }
411 } else {
412 print
413 "WARNING: action $action not implemented on $path. Bug or missing feature of $0\n";
414 }
415
416 # save commits for later
417 push @commit, $path if ($path);
418
419 }
420
421 # now commit changes
422 cvs_commit( $msg, @commit );
423
424 commit_svnrev($rev);
425 }
426
427 # cd out of $CVSREP before File::Temp::END is called
428 chdir("/tmp") || die "can't cd to /tmp: $!";
429
430 __END__
431
432 =pod
433
434 =head1 NAME
435
436 svn2cvs - save subversion commits to (read-only) cvs repository
437
438 =head1 SYNOPSIS
439
440 ./svn2cvs.pl SVN_URL CVSROOT CVSREPOSITORY
441
442 Usage example (used to self-host this script):
443
444 ./svn2cvs.pl file:///home/dpavlin/private/svn/svn2cvs/trunk/ \
445 :pserver:dpavlin@cvs.tigris.org:/cvs svn2cvs/src
446
447 =head1 DESCRIPTION
448
449 This script will allows you to commit changes made to Subversion repository to
450 (read-only) CVS repository manually or from Subversion's C<post-commit> hook.
451
452 It's using F<.svnrev> file (which will be created on first run) in
453 B<CVSROOT/CVSREPOSITORY> to store last Subversion revision which was
454 committed into CVS.
455
456 One run will do following things:
457
458 =over 4
459
460 =item *
461 checkout B<CVSREPOSITORY> from B<CVSROOT> to temporary directory
462
463 =item *
464 check if F<.svnrev> file exists and create it if it doesn't
465
466 =item *
467 loop through all revisions from current in B<CVSROOT/CVSREPOSITORY> (using
468 F<.svnrev>) up to B<HEAD> (current one)
469
470 =over 5
471
472 =item *
473 checkout next Subversion revision from B<SVN_URL> over CVS checkout
474 temporary directory
475
476 =item *
477 make modification (add and/or delete) done in that revision
478
479 =item *
480 commit modification (added, deleted or modified files/dirs) while
481 preserving original message from CVS
482
483 =item *
484 update F<.svnrev> to match current revision
485
486 =back
487
488 =item *
489 cleanup temporary directory
490
491 =back
492
493 If checkout fails for some reason (e.g. flaky ssh connection), you will
494 still have valid CVS repository, so all you have to do is run B<svn2cvs.pl>
495 again.
496
497 =head1 WARNINGS
498
499 "Cheap" copy operations in Subversion are not at all cheap in CVS. They will
500 create multiple copies of files in CVS repository!
501
502 This script assume that you want to sync your C<trunk> (or any other
503 directory for that matter) directory with CVS, not root of your subversion.
504 This might be considered bug, but since common practise is to have
505 directories C<trunk> and C<branches> in svn and source code in them, it's
506 not serious limitation.
507
508 =head1 RELATED PROJECTS
509
510 B<Subversion> L<http://subversion.tigris.org/> version control system that is a
511 compelling replacement for CVS in the open source community.
512
513 B<cvs2svn> L<http://cvs2svn.tigris.org/> converts a CVS repository to a
514 Subversion repository. It is designed for one-time conversions, not for
515 repeated synchronizations between CVS and Subversion.
516
517 =head1 CHANGES
518
519 Versions of this utility are actually Subversion repository revisions,
520 so they might not be in sequence.
521
522 =over 3
523
524 =item r10
525
526 First release available to public
527
528 =item r15
529
530 Addition of comprehensive documentation, fixes for quoting in commit
531 messages, and support for skipping changes which are not under current
532 Subversion checkout root (e.g. branches).
533
534 =item r18
535
536 Support for importing your svn into empty CVS repository (it will first
537 create module and than dump all revisions).
538 Group commit operations to save round-trips to CVS server.
539 Documentation improvements and other small fixes.
540
541 =item r20
542
543 Fixed path deduction (overlap between Subversion reporistory and CVS checkout).
544
545 =item r21
546
547 Use C<update -d> instead of checkout after import.
548 Added fixes by Paul Egan <paulegan@mail.com> for XMLin and fixing working
549 directory.
550
551 =item r22
552
553 Rewritten import from revision 0 to empty repository, better importing
554 of deep directory structures, initial support for recovery from partial
555 commit.
556
557 =back
558
559 =head1 AUTHOR
560
561 Dobrica Pavlinusic <dpavlin@rot13.org>
562
563 L<http://www.rot13.org/~dpavlin/>
564
565 =head1 LICENSE
566
567 This product is licensed under GNU Public License (GPL) v2 or later.
568
569 =cut
570

Properties

Name Value
svn:executable

  ViewVC Help
Powered by ViewVC 1.1.26