/[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 21 - (show annotations)
Fri Jul 1 19:07:10 2005 UTC (18 years, 9 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 #!/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 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 system($cmd) == 0 || die "$errmsg: $!";
49 }
50
51 #
52 # sub to commit .svn rev file later
53 #
54 sub commit_svnrev {
55 my $rev = shift @_;
56 my $add_new = shift @_;
57
58 die "commit_svnrev needs revision" if (! defined($rev));
59
60 open(SVNREV,"> .svnrev") || die "can't open $TMPDIR/$CVSREP/.svnrev: $!";
61 print SVNREV $rev;
62 close(SVNREV);
63
64 my $path=".svnrev";
65
66 if ($add_new) {
67 system "$cvs add $path" || die "cvs add of $path failed: $!";
68 } else {
69 my $msg="subversion revision $rev commited to CVS";
70 print "$msg\n";
71 system "$cvs commit -m '$msg' $path" || die "cvs commit of $path failed: $!";
72 }
73 }
74
75 # current revision in CVS
76 my $rev;
77
78 # ok, now do the checkout
79 eval {
80 log_system("$cvs -q checkout $CVSREP", "cvs checkout failed");
81 };
82
83 if ($@) {
84 print <<_NEW_REP_;
85
86 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
89 Press enter to continue importing new CVS repository or CTRL+C to abort.
90
91 _NEW_REP_
92
93 print "start import of new module [yes]: ";
94 my $in = <STDIN>;
95 mkdir($CVSREP) || die "can't create $CVSREP: $!";
96
97 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 # and get new changes it
113 log_system("$cvs -q update -d $CVSREP", "cvs update -d failed");
114
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 Your CVS repository doesn't have .svnrev file!
129
130 This file is used to keep CVS repository and Subversion in sync, so
131 that only newer changes will be commited.
132
133 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
138 If you migrated your cvs repository to svn using cvs2svn, this will be
139 last Subversion revision. If this is initial run of conversion of
140 Subversion repository to CVS, correct revision is 0.
141
142 _USAGE_
143
144 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 } else {
155 open(SVNREV,".svnrev") || die "can't open $TMPDIR/$CVSREP/.svnrev: $!";
156 $rev = <SVNREV>;
157 chomp($rev);
158 close(SVNREV);
159 }
160
161 print "Starting after revision $rev\n";
162 $rev++;
163 }
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 open(LOG, "svn log -r $rev:HEAD -v --xml $SVNROOT |") || die "svn log for repository $SVNROOT failed: $!";
174 my $log;
175 while(<LOG>) {
176 $log .= $_;
177 }
178 close(LOG);
179
180
181 my $xml;
182 eval {
183 $xml = XMLin($log, ForceArray => [ 'logentry', 'path' ]);
184 };
185
186
187 #=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
196 my $fmt = "\n" . "-" x 79 . "\nr%5s| %8s | %s\n\n%s\n";
197
198 if (! $xml->{'logentry'}) {
199 print "no newer log entries in Subversion repostory. CVS is current\n";
200 exit 0;
201 }
202
203 # 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 if ($d !~ m,/$, && $d ne "") {
211 $d .= "/";
212 }
213 open(E, $d."CVS/Entries") || return 0;
214 while(<E>) {
215 return(1) if (m,^/$f/,);
216 }
217 close(E);
218 return 0;
219 }
220 }
221
222 chdir("$TMPDIR/$CVSREP") || die "can't cd to $TMPDIR/$CVSREP: $!";
223
224 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 log_system("svn export --force -q -r $rev $SVNROOT $TMPDIR/$CVSREP", "svn export of revision $rev failed");
228
229 # 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 if ($tmpsvn =~ s#(/[^/]+)/*$##) {
235 $SVNREP = $1 . $SVNREP;
236 } else {
237 print "NOTICE: can't deduce svn dir from $SVNROOT - skipping\n";
238 next;
239 }
240 } until ($tmppath =~ m/^$SVNREP/);
241
242 print "NOTICE: using $SVNREP as directory for svn\n";
243
244 printf($fmt, $e->{'revision'}, $e->{'author'}, $e->{'date'}, $e->{'msg'});
245 my @commit;
246
247 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 $path =~ s,^$SVNREP/*,, || die "BUG: can't strip SVNREP '$SVNREP' from path";
255
256 if (! $path) {
257 print "NOTICE: skipped this operation. Probably trunk creation\n";
258 next;
259 }
260
261 my $msg = $e->{'msg'};
262 $msg =~ s/'/'\\''/g; # quote "
263
264 if ($action =~ /M/) {
265 print "svn2cvs: modify $path -- nop\n";
266 } elsif ($action =~ /A/) {
267 if (-d $path) {
268 chdir($path) || die "can't cd into dir $path for import: $!";
269 log_system("$cvs import -d -m '$msg' $CVSREP/$path svn r$rev", "cvs import of $path failed");
270 chdir("$TMPDIR") || die "can't cd to $TMPDIR/$CVSREP: $!";
271 if (-d "$CVSREP/$path") {
272 rmtree "$CVSREP/$path" || die "can't remove $CVSREP/$path: $!";
273 } else {
274 unlink "$CVSREP/$path" || die "can't remove $CVSREP/$path: $!";
275 }
276 log_system("$cvs update -d $CVSREP", "cvs update -d of imported dir $path failed");
277 chdir("$TMPDIR/$CVSREP") || die "can't cd back to $TMPDIR/$CVSREP: $!";
278 } 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 } else {
283 in_entries($path) || log_system("$cvs add $path", "cvs add of $path failed");
284 }
285 } elsif ($action =~ /D/) {
286 unlink $path || die "can't delete $path: $!";
287 log_system("$cvs delete $path", "cvs delete of $path failed");
288 } else {
289 print "WARNING: action $action not implemented on $path. Bug or missing feature of $0\n";
290 }
291
292 # save commits for later
293 push @commit, $path;
294
295 }
296
297 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 commit_svnrev($rev);
304 }
305
306 # cd out of $CVSREP before File::Temp::END is called
307 chdir("/tmp") || die "can't cd to /tmp: $!";
308
309 __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 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
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 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 =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 =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 =item r18
414
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 =item r20
421
422 Fixed path deduction (overlap between Subversion reporistory and CVS checkout).
423
424 =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 =back
432
433 =head1 AUTHOR
434
435 Dobrica Pavlinusic <dpavlin@rot13.org>
436
437 L<https://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