/[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

Diff of /trunk/svn2cvs.pl

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

revision 3 by dpavlin, Tue Mar 9 21:45:32 2004 UTC revision 21 by dpavlin, Fri Jul 1 19:07:10 2005 UTC
# Line 8  Line 8 
8  # http://raw.no/personal/blog  # http://raw.no/personal/blog
9  #  #
10  # 2004-03-09 Dobrica Pavlinusic <dpavlin@rot13.org>  # 2004-03-09 Dobrica Pavlinusic <dpavlin@rot13.org>
11    #
12    # documentation is after __END__
13    
14  use strict;  use strict;
15  use File::Temp qw/ tempdir /;  use File::Temp qw/ tempdir /;
16    use File::Path;
17  use Data::Dumper;  use Data::Dumper;
18  use XML::Simple;  use XML::Simple;
19    
20  # get current user home directory  if (@ARGV < 2) {
21  my $HOME = $ENV{'HOME'} || die "can't get home directory!";          print "usage: $0 SVN_URL CVSROOT CVSREPOSITORY\n";
22            exit 1;
23    }
24    
25    my ($SVNROOT,$CVSROOT, $CVSREP) = @ARGV;
26    
27  # cvsroot directory  if ($SVNROOT !~ m,^[\w+]+:///*\w+,) {
28  my $CVSROOT="$HOME/x/cvsroot";          print "ERROR: invalid svn root $SVNROOT\n";
29  # name of cvs repository to commit to          exit 1;
30  my $CVSREP="svn2cvs";  }
31    
32  # svnroot directory  # Ensure File::Temp::END can clean up:
33  my $SVNROOT="file://$HOME/private/svn/svn2cvs";  $SIG{__DIE__} = sub { chdir("/tmp"); die @_ };
 # name of respository  
 my $SVNREP="trunk";  
   
 # webpac example  
 #$CVSROOT="$HOME/x/cvsroot";  
 #$CVSREP="webpac";  
 #$SVNROOT="file://$HOME/private/svn/webpac/";  
 #$SVNREP="trunk";  
34    
35  my $TMPDIR=tempdir( "/tmp/checkoutXXXXX", CLEANUP => 1 );  my $TMPDIR=tempdir( "/tmp/checkoutXXXXX", CLEANUP => 1 );
36    
# Line 40  chdir($TMPDIR) || die "can't cd to $TMPD Line 39  chdir($TMPDIR) || die "can't cd to $TMPD
39  # cvs command with root  # cvs command with root
40  my $cvs="cvs -d $CVSROOT";  my $cvs="cvs -d $CVSROOT";
41    
   
42  #  #
43  # sub to do logging and system calls  # sub to do logging and system calls
44  #  #
45  sub log_system($$) {  sub log_system($$) {
46          my ($cmd,$errmsg) = @_;          my ($cmd,$errmsg) = @_;
47          print STDERR "## $cmd\n";          print STDERR "## $cmd\n";
48          system $cmd || die "$errmsg: $!";          system($cmd) == 0 || die "$errmsg: $!";
49  }  }
50    
51  #  #
# Line 59  sub commit_svnrev { Line 57  sub commit_svnrev {
57    
58          die "commit_svnrev needs revision" if (! defined($rev));          die "commit_svnrev needs revision" if (! defined($rev));
59    
60          open(SVNREV,"> $CVSREP/.svnrev") || die "can't open $TMPDIR/$CVSREP/.svnrev: $!";          open(SVNREV,"> .svnrev") || die "can't open $TMPDIR/$CVSREP/.svnrev: $!";
61          print SVNREV $rev;          print SVNREV $rev;
62          close(SVNREV);          close(SVNREV);
63    
64          my $path=".svnrev";          my $path=".svnrev";
65    
66          if ($add_new) {          if ($add_new) {
67                  system "$cvs add $CVSREP/$path" || die "cvs add of $path failed: $!";                  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          }          }
   
         my $msg="subversion revision $rev commited to CVS";  
         print "$msg\n";  
         system "$cvs commit -m \"$msg\" $CVSREP/$path" || die "cvs commit of $path failed: $!";  
73  }  }
74    
75    # current revision in CVS
76    my $rev;
77    
78  # ok, now do the checkout  # ok, now do the checkout
79    eval {
80            log_system("$cvs -q checkout $CVSREP", "cvs checkout failed");
81    };
82    
83  log_system("$cvs -q checkout $CVSREP","cvs checkout failed");  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    
 my $rev;  
123    
124  # check if svnrev exists          # check if svnrev exists
125  if (! -e "$CVSREP/.svnrev") {          if (! -e ".svnrev") {
126          print <<_USAGE_;                  print <<_USAGE_;
127    
128  Your CVS repository doesn't have .svnrev file!  Your CVS repository doesn't have .svnrev file!
129    
130  This file is used to keep CVS repository and SubVersion in sync, so  This file is used to keep CVS repository and Subversion in sync, so
131  that only newer changes will be commited.  that only newer changes will be commited.
132    
133  It's quote possible that this is first svn2cvs run for this repository.  It's quote possible that this is first svn2cvs run for this repository.
# Line 96  corresponds to current version of CVS re Line 136  corresponds to current version of CVS re
136  been checkouted.  been checkouted.
137    
138  If you migrated your cvs repository to svn using cvs2svn, this will be  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  last Subversion revision. If this is initial run of conversion of
140  SubVersion repository to CVS, correct revision is 0.  Subversion repository to CVS, correct revision is 0.
141    
142  _USAGE_  _USAGE_
143    
144          print "svn revision corresponding to CVS [abort]: ";                  print "svn revision corresponding to CVS [abort]: ";
145          my $in = <STDIN>;                  my $in = <STDIN>;
146          chomp($in);                  chomp($in);
147          if ($in !~ /^\d+$/) {                  if ($in !~ /^\d+$/) {
148                  print "Aborting: revision not a number\n";                          print "Aborting: revision not a number\n";
149                  exit 1;                          exit 1;
150                    } else {
151                            $rev = $in;
152                            commit_svnrev($rev,1);  # create new
153                    }
154          } else {          } else {
155                  $rev = $in;                  open(SVNREV,".svnrev") || die "can't open $TMPDIR/$CVSREP/.svnrev: $!";
156                  commit_svnrev($rev,1);  # create new                  $rev = <SVNREV>;
157                    chomp($rev);
158                    close(SVNREV);
159          }          }
 } else {  
         open(SVNREV,"$CVSREP/.svnrev") || die "can't open $TMPDIR/$CVSREP/.svnrev: $!";  
         my $rev = <SVNREV>;  
         chomp($rev);  
         close(SVNREV);  
 }  
160    
161  print "Starting after revision $rev\n";          print "Starting after revision $rev\n";
162  $rev++;          $rev++;
163    }
164    
165    
166  #  #
# Line 129  $rev++; Line 170  $rev++;
170  # case much about accuracy and completnes of logs there, this might  # case much about accuracy and completnes of logs there, this might
171  # be good. YMMV  # be good. YMMV
172  #  #
173  open(LOG, "svn log -r $rev:HEAD -v --xml $SVNROOT/$SVNREP |") || die "svn log for repository $SVNROOT/$SVNREP failed: $!";  open(LOG, "svn log -r $rev:HEAD -v --xml $SVNROOT |") || die "svn log for repository $SVNROOT failed: $!";
174  my $log;  my $log;
175  while(<LOG>) {  while(<LOG>) {
176          $log .= $_;          $log .= $_;
177  }  }
178  close(LOG);  close(LOG);
179    
 my $xml = XMLin($log, ForceArray => [ 'logentry', 'path' ]);  
180    
181    my $xml;
182    eval {
183            $xml = XMLin($log, ForceArray => [ 'logentry', 'path' ]);
184    };
185    
 =begin log_example  
186    
187  ------------------------------------------------------------------------  #=begin log_example
188  r256 | dpavlin | 2004-03-09 13:18:17 +0100 (Tue, 09 Mar 2004) | 2 lines  #
189    #------------------------------------------------------------------------
190  ported r254 from hidra branch  #r256 | dpavlin | 2004-03-09 13:18:17 +0100 (Tue, 09 Mar 2004) | 2 lines
191    #
192  =cut  #ported r254 from hidra branch
193    #
194    #=cut
195    
196  my $fmt = "\n" . "-" x 79 . "\nr%5s| %8s | %s\n\n%s\n";  my $fmt = "\n" . "-" x 79 . "\nr%5s| %8s | %s\n\n%s\n";
197    
198  if (! $xml->{'logentry'}) {  if (! $xml->{'logentry'}) {
199          print "no newer log entries in SubVersion repostory. CVS is current\n";          print "no newer log entries in Subversion repostory. CVS is current\n";
200          exit 0;          exit 0;
201  }  }
202    
203  print Dumper($xml);  # 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'}}) {  foreach my $e (@{$xml->{'logentry'}}) {
225          die "BUG: revision from .svnrev ($rev) greater than from subversion (".$e->{'revision'}.")" if ($rev > $e->{'revision'});          die "BUG: revision from .svnrev ($rev) greater than from subversion (".$e->{'revision'}.")" if ($rev > $e->{'revision'});
226          $rev = $e->{'revision'};          $rev = $e->{'revision'};
227          log_system("svn export --force -q -r $rev $SVNROOT/$SVNREP $CVSREP", "svn export of revision $rev failed");          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'});          printf($fmt, $e->{'revision'}, $e->{'author'}, $e->{'date'}, $e->{'msg'});
245            my @commit;
246    
247          foreach my $p (@{$e->{'paths'}->{'path'}}) {          foreach my $p (@{$e->{'paths'}->{'path'}}) {
248                  my ($action,$path) = ($p->{'action'},$p->{'content'});                  my ($action,$path) = ($p->{'action'},$p->{'content'});
249    
# Line 170  foreach my $e (@{$xml->{'logentry'}}) { Line 251  foreach my $e (@{$xml->{'logentry'}}) {
251    
252                  # prepare path and message                  # prepare path and message
253                  my $file = $path;                  my $file = $path;
254                  $path =~ s,^/$SVNREP/*,, || die "BUG: can't strip SVNREP from path";                  $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'};                  my $msg = $e->{'msg'};
262                  $msg =~ s/"/\\"/g;      # quote "                  $msg =~ s/'/'\\''/g;    # quote "
263    
264                  if ($action =~ /M/) {                  if ($action =~ /M/) {
265                          print "svn2cvs: modify $path -- nop\n";                          print "svn2cvs: modify $path -- nop\n";
266                  } elsif ($action =~ /A/) {                  } elsif ($action =~ /A/) {
267                          log_system("$cvs add -m \"$msg\" $CVSREP/$path", "cvs add of $path failed");                          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/) {                  } elsif ($action =~ /D/) {
286                          log_system("$cvs delete -m \"$msg\" $CVSREP/$path", "cvs delete of $path failed");                          unlink $path || die "can't delete $path: $!";
287                            log_system("$cvs delete $path", "cvs delete of $path failed");
288                  } else {                  } else {
289                          print "WARNING: action $action not implemented on $path. Bug or missing feature of $0\n";                          print "WARNING: action $action not implemented on $path. Bug or missing feature of $0\n";
290                  }                  }
291    
292                  # now commit changes                  # save commits for later
293                  log_system("$cvs commit -m \"$msg\" $CVSREP/$path", "cvs commit of $path failed");                  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);          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__  __END__
310    
311  svn export --force "$SVNROOT/$SVNREP" "$CVSREP"  =pod
312    
313  cd dotfiles  =head1 NAME
314    
315  for file in $(find -type f -not -path \*CVS\*); do  svn2cvs - save subversion commits to (read-only) cvs repository
316          FILE=$(basename $file)  
317          DIR=$(dirname $file)  =head1 SYNOPSIS
318          if ! grep -q "^/$FILE/" $DIR/CVS/Entries ; then  
319                  cvs add $file    ./svn2cvs.pl SVN_URL CVSROOT CVSREPOSITORY
320          fi  
321  done  Usage example (used to self-host this script):
322    
323  #cvs commit -m "Automatic commit from SVN"    ./svn2cvs.pl file:///home/dpavlin/private/svn/svn2cvs/trunk/ \
324                   :pserver:dpavlin@cvs.tigris.org:/cvs svn2cvs/src
325  #rm -rf $TMPDIR  
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    
 echo "cvs left in $TMPDIR"  

Legend:
Removed from v.3  
changed lines
  Added in v.21

  ViewVC Help
Powered by ViewVC 1.1.26