/[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 22 - (show annotations)
Tue Jul 19 16:10:29 2005 UTC (18 years, 9 months ago) by dpavlin
File MIME type: text/plain
File size: 11474 byte(s)
rewritten import to empty repository, better import of deep directory
structures, initial support for recovery from partial commit
added automatic cvs commit of webpage

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 } else {
257 print "NOTICE: can't deduce svn dir from $SVNROOT - skipping\n";
258 next;
259 }
260 } until ($tmppath =~ m/^$SVNREP/);
261
262 print "NOTICE: using $SVNREP as directory for svn\n";
263
264 printf($fmt, $e->{'revision'}, $e->{'author'}, $e->{'date'}, $e->{'msg'});
265 my @commit;
266
267 foreach my $p (@{$e->{'paths'}->{'path'}}) {
268 my ($action,$path) = ($p->{'action'},$p->{'content'});
269
270 print "svn2cvs: $action $path\n";
271
272 # prepare path and message
273 my $file = $path;
274 $path =~ s#^\Q$SVNREP\E/*## || die "BUG: can't strip SVNREP '$SVNREP' from path";
275
276 if (! $path) {
277 print "NOTICE: skipped this operation. Probably trunk creation\n";
278 next;
279 }
280
281 my $msg = $e->{'msg'};
282 $msg =~ s/'/'\\''/g; # quote "
283
284 if ($action =~ /M/) {
285 print "svn2cvs: modify $path -- nop\n";
286 } elsif ($action =~ /A/) {
287 if (-d $path) {
288 add_dir($path, $msg);
289 } elsif ($path =~ m,^(.+)/[^/]+$, && ! -e "$1/CVS/Root") {
290 my $dir = $1;
291 in_entries($dir) || add_dir($dir, $msg);
292 in_entries($path) || log_system("$cvs add $path", "cvs add of $path failed");
293 } else {
294 in_entries($path) || log_system("$cvs add $path", "cvs add of $path failed");
295 }
296 } elsif ($action =~ /D/) {
297 if (-e $path) {
298 unlink $path || die "can't delete $path: $!";
299 log_system("$cvs delete $path", "cvs delete of $path failed");
300 } else {
301 print "WARNING: $path is not present, skipping...\n";
302 undef $path;
303 }
304 } else {
305 print "WARNING: action $action not implemented on $path. Bug or missing feature of $0\n";
306 }
307
308 # save commits for later
309 push @commit, $path if ($path);
310
311 }
312
313 my $msg = $e->{'msg'};
314 $msg =~ s/'/'\\''/g; # quote "
315
316 # now commit changes
317 log_system("$cvs commit -m '$msg' ".join(" ",@commit), "cvs commit of ".join(",",@commit)." failed");
318
319 commit_svnrev($rev);
320 }
321
322 # cd out of $CVSREP before File::Temp::END is called
323 chdir("/tmp") || die "can't cd to /tmp: $!";
324
325 __END__
326
327 =pod
328
329 =head1 NAME
330
331 svn2cvs - save subversion commits to (read-only) cvs repository
332
333 =head1 SYNOPSIS
334
335 ./svn2cvs.pl SVN_URL CVSROOT CVSREPOSITORY
336
337 Usage example (used to self-host this script):
338
339 ./svn2cvs.pl file:///home/dpavlin/private/svn/svn2cvs/trunk/ \
340 :pserver:dpavlin@cvs.tigris.org:/cvs svn2cvs/src
341
342 =head1 DESCRIPTION
343
344 This script will allows you to commit changes made to Subversion repository to
345 (read-only) CVS repository manually or from Subversion's C<post-commit> hook.
346
347 It's using F<.svnrev> file (which will be created on first run) in
348 B<CVSROOT/CVSREPOSITORY> to store last Subversion revision which was
349 committed into CVS.
350
351 One run will do following things:
352
353 =over 4
354
355 =item *
356 checkout B<CVSREPOSITORY> from B<CVSROOT> to temporary directory
357
358 =item *
359 check if F<.svnrev> file exists and create it if it doesn't
360
361 =item *
362 loop through all revisions from current in B<CVSROOT/CVSREPOSITORY> (using
363 F<.svnrev>) up to B<HEAD> (current one)
364
365 =over 5
366
367 =item *
368 checkout next Subversion revision from B<SVN_URL> over CVS checkout
369 temporary directory
370
371 =item *
372 make modification (add and/or delete) done in that revision
373
374 =item *
375 commit modification (added, deleted or modified files/dirs) while
376 preserving original message from CVS
377
378 =item *
379 update F<.svnrev> to match current revision
380
381 =back
382
383 =item *
384 cleanup temporary directory
385
386 =back
387
388 If checkout fails for some reason (e.g. flaky ssh connection), you will
389 still have valid CVS repository, so all you have to do is run B<svn2cvs.pl>
390 again.
391
392 =head1 WARNINGS
393
394 "Cheap" copy operations in Subversion are not at all cheap in CVS. They will
395 create multiple copies of files in CVS repository!
396
397 This script assume that you want to sync your C<trunk> (or any other
398 directory for that matter) directory with CVS, not root of your subversion.
399 This might be considered bug, but since common practise is to have
400 directories C<trunk> and C<branches> in svn and source code in them, it's
401 not serious limitation.
402
403 =head1 RELATED PROJECTS
404
405 B<Subversion> L<http://subversion.tigris.org/> version control system that is a
406 compelling replacement for CVS in the open source community.
407
408 B<cvs2svn> L<http://cvs2svn.tigris.org/> converts a CVS repository to a
409 Subversion repository. It is designed for one-time conversions, not for
410 repeated synchronizations between CVS and Subversion.
411
412 =head1 CHANGES
413
414 Versions of this utility are actually Subversion repository revisions,
415 so they might not be in sequence.
416
417 =over 3
418
419 =item r10
420
421 First release available to public
422
423 =item r15
424
425 Addition of comprehensive documentation, fixes for quoting in commit
426 messages, and support for skipping changes which are not under current
427 Subversion checkout root (e.g. branches).
428
429 =item r18
430
431 Support for importing your svn into empty CVS repository (it will first
432 create module and than dump all revisions).
433 Group commit operations to save round-trips to CVS server.
434 Documentation improvements and other small fixes.
435
436 =item r20
437
438 Fixed path deduction (overlap between Subversion reporistory and CVS checkout).
439
440 =item r21
441
442 Use C<update -d> instead of checkout after import.
443 Added fixes by Paul Egan <paulegan@mail.com> for XMLin and fixing working
444 directory.
445
446 =item r22
447
448 Rewritten import from revision 0 to empty repository, better importing
449 of deep directory structures, initial support for recovery from partial
450 commit.
451
452 =back
453
454 =head1 AUTHOR
455
456 Dobrica Pavlinusic <dpavlin@rot13.org>
457
458 L<https://www.rot13.org/~dpavlin/>
459
460 =head1 LICENSE
461
462 This product is licensed under GNU Public License (GPL) v2 or later.
463
464 =cut
465

Properties

Name Value
svn:executable

  ViewVC Help
Powered by ViewVC 1.1.26