/[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 42 - (hide annotations)
Sat Sep 22 13:57:28 2007 UTC (16 years, 6 months ago) by dpavlin
File MIME type: text/plain
File size: 14072 byte(s)
tidy more similar to my own style
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 41 log_system( "$cvs add '$curr_dir'", "cvs add of $curr_dir failed" );
108 dpavlin 22 }
109     }
110    
111 dpavlin 1 # ok, now do the checkout
112 dpavlin 18 eval {
113 dpavlin 22 cd_tmp;
114 dpavlin 41 log_system( "$cvs -q checkout $CVSREP", "cvs checkout failed" );
115 dpavlin 18 };
116 dpavlin 1
117 dpavlin 18 if ($@) {
118     print <<_NEW_REP_;
119 dpavlin 1
120 dpavlin 18 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 dpavlin 7
123 dpavlin 18 Press enter to continue importing new CVS repository or CTRL+C to abort.
124 dpavlin 7
125 dpavlin 18 _NEW_REP_
126 dpavlin 3
127 dpavlin 18 print "start import of new module [yes]: ";
128     my $in = <STDIN>;
129 dpavlin 22 cd_tmp;
130 dpavlin 18 mkdir($CVSREP) || die "can't create $CVSREP: $!";
131 dpavlin 22 cd_rep;
132 dpavlin 1
133 dpavlin 41 open( SVNREV, "> .svnrev" ) || die "can't open $CVSREP/.svnrev: $!";
134 dpavlin 18 print SVNREV "0";
135     close(SVNREV);
136    
137     $rev = 0;
138    
139     # create new module
140 dpavlin 22 cd_rep;
141 dpavlin 41 log_system( "$cvs import -d -m 'new CVS module' $CVSREP svn r$rev",
142     "import of new repository" );
143 dpavlin 22 cd_tmp;
144     rmtree($CVSREP) || die "can't remove $CVSREP";
145 dpavlin 41 log_system( "$cvs -q checkout $CVSREP", "cvs checkout failed" );
146 dpavlin 22 cd_rep;
147 dpavlin 18
148 dpavlin 42 } else {
149 dpavlin 41
150 dpavlin 18 # import into existing module directory in CVS
151    
152 dpavlin 22 cd_rep;
153 dpavlin 41
154 dpavlin 18 # check if svnrev exists
155 dpavlin 41 if ( !-e ".svnrev" ) {
156 dpavlin 18 print <<_USAGE_;
157    
158 dpavlin 3 Your CVS repository doesn't have .svnrev file!
159 dpavlin 1
160 dpavlin 12 This file is used to keep CVS repository and Subversion in sync, so
161 dpavlin 3 that only newer changes will be commited.
162 dpavlin 1
163 dpavlin 3 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 dpavlin 1
168 dpavlin 3 If you migrated your cvs repository to svn using cvs2svn, this will be
169 dpavlin 12 last Subversion revision. If this is initial run of conversion of
170     Subversion repository to CVS, correct revision is 0.
171 dpavlin 1
172     _USAGE_
173 dpavlin 3
174 dpavlin 18 print "svn revision corresponding to CVS [abort]: ";
175     my $in = <STDIN>;
176     chomp($in);
177 dpavlin 41 if ( $in !~ /^\d+$/ ) {
178 dpavlin 18 print "Aborting: revision not a number\n";
179     exit 1;
180 dpavlin 42 } else {
181 dpavlin 18 $rev = $in;
182 dpavlin 41 commit_svnrev( $rev, 1 ); # create new
183 dpavlin 18 }
184 dpavlin 42 } else {
185 dpavlin 41 open( SVNREV, ".svnrev" )
186     || die "can't open $TMPDIR/$CVSREP/.svnrev: $!";
187 dpavlin 18 $rev = <SVNREV>;
188     chomp($rev);
189     close(SVNREV);
190 dpavlin 3 }
191 dpavlin 18
192     print "Starting after revision $rev\n";
193     $rev++;
194 dpavlin 1 }
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 dpavlin 41 open( LOG, "svn log -r $rev:HEAD -v --xml $SVNROOT |" )
204     || die "svn log for repository $SVNROOT failed: $!";
205 dpavlin 1 my $log;
206 dpavlin 41 while (<LOG>) {
207 dpavlin 1 $log .= $_;
208     }
209     close(LOG);
210    
211 dpavlin 21 my $xml;
212 dpavlin 41 eval { $xml = XMLin( $log, ForceArray => [ 'logentry', 'path' ] ); };
213 dpavlin 1
214 dpavlin 12 #=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 dpavlin 1
223     my $fmt = "\n" . "-" x 79 . "\nr%5s| %8s | %s\n\n%s\n";
224    
225 dpavlin 41 if ( !$xml->{'logentry'} ) {
226 dpavlin 12 print "no newer log entries in Subversion repostory. CVS is current\n";
227 dpavlin 2 exit 0;
228     }
229    
230 dpavlin 35 # 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 dpavlin 41 open( my $fh, "./$dir/CVS/Entries" ) || return 0;
236     while (<$fh>) {
237 dpavlin 35 if ( m{^D/([^/]+)}, ) {
238     my $sub_dir = $1;
239     warn "#### entries recurse into: $dir/$sub_dir";
240 dpavlin 41 push @entries, map {"$sub_dir/$_"} entries("$dir/$sub_dir");
241 dpavlin 35 push @entries, $sub_dir;
242 dpavlin 42 } elsif (m{^/([^/]+)/}) {
243 dpavlin 35 push @entries, $1;
244 dpavlin 42 } elsif ( !m{^D$} ) {
245 dpavlin 35 die "can't decode entries line: $_";
246 dpavlin 14 }
247     }
248 dpavlin 35 close($fh);
249 dpavlin 41 warn "#### entries($dir) => ", join( "|", @entries );
250 dpavlin 35 return @entries;
251 dpavlin 14 }
252    
253 dpavlin 35 # check if file exists in CVS/Entries
254     sub in_entries($) {
255     my $path = shift;
256 dpavlin 41 if ( $path =~ m,^(.*?/*)([^/]+)$, ) {
257     my ( $dir, $file ) = ( $1, $2 );
258     if ( $dir !~ m,/$, && $dir ne "" ) {
259 dpavlin 37 $dir .= "/";
260     }
261    
262 dpavlin 41 open( my $fh, "./$dir/CVS/Entries" )
263     || return 0; #die "no entries file: $dir/CVS/Entries";
264     while (<$fh>) {
265 dpavlin 37 return 1 if (m{^/$file/});
266     }
267     close($fh);
268     return 0;
269 dpavlin 42 } else {
270 dpavlin 37 die "can't split '$path' to dir and file!";
271 dpavlin 35 }
272     }
273    
274 dpavlin 22 cd_tmp;
275     cd_rep;
276 dpavlin 21
277 dpavlin 41 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 dpavlin 1 $rev = $e->{'revision'};
282 dpavlin 41 log_system( "svn export --force -q -r $rev $SVNROOT $TMPDIR/$CVSREP",
283     "svn export of revision $rev failed" );
284 dpavlin 1
285 dpavlin 8 # deduce name of svn directory
286 dpavlin 41 my $SVNREP = "";
287     my $tmpsvn = $SVNROOT || die "BUG: SVNROOT empty!";
288     my $tmppath = $e->{'paths'}->{'path'}->[0]->{'content'}
289     || die "BUG: tmppath empty!";
290 dpavlin 8 do {
291 dpavlin 41 if ( $tmpsvn =~ s#(/[^/]+)/*$## ) { # vim fix
292 dpavlin 20 $SVNREP = $1 . $SVNREP;
293 dpavlin 42 } elsif ( $e->{'paths'}->{'path'}->[0]->{'copyfrom-path'} ) {
294 dpavlin 41 print
295     "NOTICE: copyfrom outside synced repository ignored - skipping\n";
296 dpavlin 26 next;
297 dpavlin 42 } else {
298 dpavlin 15 print "NOTICE: can't deduce svn dir from $SVNROOT - skipping\n";
299     next;
300 dpavlin 8 }
301 dpavlin 41 } until ( $tmppath =~ m/^$SVNREP/ );
302 dpavlin 8
303     print "NOTICE: using $SVNREP as directory for svn\n";
304    
305 dpavlin 41 printf( $fmt,
306     $e->{'revision'}, $e->{'author'}, $e->{'date'}, $e->{'msg'} );
307 dpavlin 18 my @commit;
308    
309 dpavlin 35 my $msg = $e->{'msg'};
310 dpavlin 41 $msg =~ s/'/'\\''/g; # quote "
311 dpavlin 35
312 dpavlin 41 $msg = 'r' . $rev . ' ' . $e->{author} . ' | ' . $e->{date} . "\n" . $msg
313     if $decorate_commit_message;
314 dpavlin 36
315 dpavlin 35 sub cvs_commit {
316     my $msg = shift || die "no msg?";
317 dpavlin 41 if ( !@_ ) {
318 dpavlin 35 warn "commit ignored, no files\n";
319     return;
320     }
321 dpavlin 41 log_system(
322     "$cvs commit -m '$msg' '" . join( "' '", @_ ) . "'",
323     "cvs commit of " . join( ",", @_ ) . " failed"
324     );
325 dpavlin 35 }
326    
327 dpavlin 41 foreach my $p ( @{ $e->{'paths'}->{'path'} } ) {
328     my ( $action, $path ) = ( $p->{'action'}, $p->{'content'} );
329 dpavlin 1
330 dpavlin 41 next if ( $path =~ m#/\.svnrev$# );
331 dpavlin 23
332 dpavlin 1 print "svn2cvs: $action $path\n";
333    
334     # prepare path and message
335     my $file = $path;
336 dpavlin 27 if ( $path !~ s#^\Q$SVNREP\E/*## ) {
337 dpavlin 41 print
338     "NOTICE: skipping '$path' which isn't under repository root '$SVNREP'\n";
339 dpavlin 27 die unless $partial_import;
340     next;
341     }
342 dpavlin 7
343 dpavlin 41 if ( !$path ) {
344 dpavlin 7 print "NOTICE: skipped this operation. Probably trunk creation\n";
345     next;
346     }
347    
348 dpavlin 1 my $msg = $e->{'msg'};
349 dpavlin 41 $msg =~ s/'/'\\''/g; # quote "
350 dpavlin 1
351 dpavlin 31 sub add_path {
352     my $path = shift || die "no path?";
353 dpavlin 41
354     if ( -d $path ) {
355     add_dir( $path, $msg );
356 dpavlin 42 } elsif ( $path =~ m,^(.+)/[^/]+$, && !-e "$1/CVS/Root" ) {
357 dpavlin 14 my $dir = $1;
358 dpavlin 41 in_entries($dir) || add_dir( $dir, $msg );
359     in_entries($path) || log_system( "$cvs add '$path'",
360     "cvs add of $path failed" );
361 dpavlin 42 } else {
362 dpavlin 41 in_entries($path) || log_system( "$cvs add '$path'",
363     "cvs add of $path failed" );
364     }
365 dpavlin 31 }
366    
367 dpavlin 41 if ( $action =~ /M/ ) {
368     if ( in_entries($path) ) {
369 dpavlin 31 print "svn2cvs: modify $path -- nop\n";
370 dpavlin 42 } else {
371 dpavlin 31 print "WARNING: modify $path which isn't in CVS, adding...\n";
372     add_path($path);
373     }
374 dpavlin 42 } elsif ( $action =~ /A/ ) {
375 dpavlin 31 add_path($path);
376 dpavlin 42 } elsif ( $action =~ /D/ ) {
377 dpavlin 41 if ( -e $path ) {
378 dpavlin 35 if ( -d $path ) {
379     warn "#### remove directory: $path";
380     my @sub_commit;
381     foreach my $f ( entries($path) ) {
382     $f = "$path/$f";
383 dpavlin 41 if ( -f $f ) {
384 dpavlin 35 unlink($f) || die "can't delete file $f: $!";
385 dpavlin 41
386     # } else {
387     # rmtree($f) || die "can't delete dir $f: $!";
388 dpavlin 35 }
389 dpavlin 41 log_system( "$cvs delete '$f'",
390     "cvs delete of file $f failed" );
391 dpavlin 35 push @sub_commit, $f;
392     }
393 dpavlin 41 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 dpavlin 40 undef $path;
401 dpavlin 42 } else {
402 dpavlin 35 warn "#### remove file: $path";
403     unlink($path) || die "can't delete $path: $!";
404 dpavlin 41 log_system( "$cvs delete '$path'",
405     "cvs delete of dir $path failed" );
406 dpavlin 35 }
407 dpavlin 42 } else {
408 dpavlin 22 print "WARNING: $path is not present, skipping...\n";
409     undef $path;
410     }
411 dpavlin 42 } else {
412 dpavlin 41 print
413     "WARNING: action $action not implemented on $path. Bug or missing feature of $0\n";
414     }
415 dpavlin 1
416 dpavlin 18 # save commits for later
417 dpavlin 22 push @commit, $path if ($path);
418 dpavlin 1
419     }
420    
421 dpavlin 18 # now commit changes
422 dpavlin 41 cvs_commit( $msg, @commit );
423 dpavlin 18
424 dpavlin 1 commit_svnrev($rev);
425     }
426 dpavlin 12
427 dpavlin 21 # cd out of $CVSREP before File::Temp::END is called
428     chdir("/tmp") || die "can't cd to /tmp: $!";
429    
430 dpavlin 12 __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 dpavlin 18 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 dpavlin 12
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 dpavlin 18 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 dpavlin 12 =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 dpavlin 16 =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 dpavlin 19 =item r18
535 dpavlin 18
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 dpavlin 20 =item r20
542 dpavlin 18
543 dpavlin 20 Fixed path deduction (overlap between Subversion reporistory and CVS checkout).
544    
545 dpavlin 21 =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 dpavlin 22 =item r22
552 dpavlin 21
553 dpavlin 22 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 dpavlin 16 =back
558    
559 dpavlin 12 =head1 AUTHOR
560    
561     Dobrica Pavlinusic <dpavlin@rot13.org>
562    
563     L<https://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