/[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

Annotation of /trunk/svn2cvs.pl

Parent Directory Parent Directory | Revision Log Revision Log


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

Properties

Name Value
svn:executable

  ViewVC Help
Powered by ViewVC 1.1.26