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

Properties

Name Value
svn:executable

  ViewVC Help
Powered by ViewVC 1.1.26