/[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 19 - (show annotations)
Sun Mar 6 12:16:58 2005 UTC (19 years, 1 month ago) by dpavlin
File MIME type: text/plain
File size: 11065 byte(s)
fixed revision

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

Properties

Name Value
svn:executable

  ViewVC Help
Powered by ViewVC 1.1.26