/[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 21 - (hide annotations)
Fri Jul 1 19:07:10 2005 UTC (18 years, 10 months ago) by dpavlin
File MIME type: text/plain
File size: 11582 byte(s)
Use C<update -d> instead of checkout after import, added fixes by Paul Egan
<paulegan@mail.com> for XMLin and fixing working directory.

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

Properties

Name Value
svn:executable

  ViewVC Help
Powered by ViewVC 1.1.26