/[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 23 - (show annotations)
Tue Jan 3 00:03:22 2006 UTC (18 years, 3 months ago) by dpavlin
File MIME type: text/plain
File size: 11511 byte(s)
skip .svnrev files when commiting back to CVS

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

Properties

Name Value
svn:executable

  ViewVC Help
Powered by ViewVC 1.1.26