/[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 20 by dpavlin, Mon Apr 18 16:43:08 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 Data::Dumper;  use Data::Dumper;
17  use XML::Simple;  use XML::Simple;
18    
19  # get current user home directory  if (@ARGV < 2) {
20  my $HOME = $ENV{'HOME'} || die "can't get home directory!";          print "usage: $0 SVN_URL CVSROOT CVSREPOSITORY\n";
21            exit 1;
22    }
23    
24    my ($SVNROOT,$CVSROOT, $CVSREP) = @ARGV;
25    
26  # cvsroot directory  if ($SVNROOT !~ m,^[\w+]+:///*\w+,) {
27  my $CVSROOT="$HOME/x/cvsroot";          print "ERROR: invalid svn root $SVNROOT\n";
28  # name of cvs repository to commit to          exit 1;
29  my $CVSREP="svn2cvs";  }
   
 # svnroot directory  
 my $SVNROOT="file://$HOME/private/svn/svn2cvs";  
 # name of respository  
 my $SVNREP="trunk";  
   
 # webpac example  
 #$CVSROOT="$HOME/x/cvsroot";  
 #$CVSREP="webpac";  
 #$SVNROOT="file://$HOME/private/svn/webpac/";  
 #$SVNREP="trunk";  
30    
31  my $TMPDIR=tempdir( "/tmp/checkoutXXXXX", CLEANUP => 1 );  my $TMPDIR=tempdir( "/tmp/checkoutXXXXX", CLEANUP => 1 );
32    
# Line 40  chdir($TMPDIR) || die "can't cd to $TMPD Line 35  chdir($TMPDIR) || die "can't cd to $TMPD
35  # cvs command with root  # cvs command with root
36  my $cvs="cvs -d $CVSROOT";  my $cvs="cvs -d $CVSROOT";
37    
   
38  #  #
39  # sub to do logging and system calls  # sub to do logging and system calls
40  #  #
41  sub log_system($$) {  sub log_system($$) {
42          my ($cmd,$errmsg) = @_;          my ($cmd,$errmsg) = @_;
43          print STDERR "## $cmd\n";          print STDERR "## $cmd\n";
44          system $cmd || die "$errmsg: $!";          system($cmd) == 0 || die "$errmsg: $!";
45  }  }
46    
47  #  #
# Line 59  sub commit_svnrev { Line 53  sub commit_svnrev {
53    
54          die "commit_svnrev needs revision" if (! defined($rev));          die "commit_svnrev needs revision" if (! defined($rev));
55    
56          open(SVNREV,"> $CVSREP/.svnrev") || die "can't open $TMPDIR/$CVSREP/.svnrev: $!";          open(SVNREV,"> .svnrev") || die "can't open $TMPDIR/$CVSREP/.svnrev: $!";
57          print SVNREV $rev;          print SVNREV $rev;
58          close(SVNREV);          close(SVNREV);
59    
60          my $path=".svnrev";          my $path=".svnrev";
61    
62          if ($add_new) {          if ($add_new) {
63                  system "$cvs add $CVSREP/$path" || die "cvs add of $path failed: $!";                  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          }          }
   
         my $msg="subversion revision $rev commited to CVS";  
         print "$msg\n";  
         system "$cvs commit -m \"$msg\" $CVSREP/$path" || die "cvs commit of $path failed: $!";  
69  }  }
70    
71    # current revision in CVS
72    my $rev;
73    
74  # ok, now do the checkout  # ok, now do the checkout
75    eval {
76            log_system("$cvs -q checkout $CVSREP", "cvs checkout failed");
77    };
78    
79  log_system("$cvs -q checkout $CVSREP","cvs checkout failed");  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    
 my $rev;  
119    
120  # check if svnrev exists          # check if svnrev exists
121  if (! -e "$CVSREP/.svnrev") {          if (! -e ".svnrev") {
122          print <<_USAGE_;                  print <<_USAGE_;
123    
124  Your CVS repository doesn't have .svnrev file!  Your CVS repository doesn't have .svnrev file!
125    
126  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
127  that only newer changes will be commited.  that only newer changes will be commited.
128    
129  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 132  corresponds to current version of CVS re
132  been checkouted.  been checkouted.
133    
134  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
135  last SubVersion revision. If this is initial run of conversion of  last Subversion revision. If this is initial run of conversion of
136  SubVersion repository to CVS, correct revision is 0.  Subversion repository to CVS, correct revision is 0.
137    
138  _USAGE_  _USAGE_
139    
140          print "svn revision corresponding to CVS [abort]: ";                  print "svn revision corresponding to CVS [abort]: ";
141          my $in = <STDIN>;                  my $in = <STDIN>;
142          chomp($in);                  chomp($in);
143          if ($in !~ /^\d+$/) {                  if ($in !~ /^\d+$/) {
144                  print "Aborting: revision not a number\n";                          print "Aborting: revision not a number\n";
145                  exit 1;                          exit 1;
146                    } else {
147                            $rev = $in;
148                            commit_svnrev($rev,1);  # create new
149                    }
150          } else {          } else {
151                  $rev = $in;                  open(SVNREV,".svnrev") || die "can't open $TMPDIR/$CVSREP/.svnrev: $!";
152                  commit_svnrev($rev,1);  # create new                  $rev = <SVNREV>;
153                    chomp($rev);
154                    close(SVNREV);
155          }          }
 } else {  
         open(SVNREV,"$CVSREP/.svnrev") || die "can't open $TMPDIR/$CVSREP/.svnrev: $!";  
         my $rev = <SVNREV>;  
         chomp($rev);  
         close(SVNREV);  
 }  
156    
157  print "Starting after revision $rev\n";          print "Starting after revision $rev\n";
158  $rev++;          $rev++;
159    }
160    
161    
162  #  #
# Line 129  $rev++; Line 166  $rev++;
166  # case much about accuracy and completnes of logs there, this might  # case much about accuracy and completnes of logs there, this might
167  # be good. YMMV  # be good. YMMV
168  #  #
169  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: $!";
170  my $log;  my $log;
171  while(<LOG>) {  while(<LOG>) {
172          $log .= $_;          $log .= $_;
# Line 139  close(LOG); Line 176  close(LOG);
176  my $xml = XMLin($log, ForceArray => [ 'logentry', 'path' ]);  my $xml = XMLin($log, ForceArray => [ 'logentry', 'path' ]);
177    
178    
179  =begin log_example  #=begin log_example
180    #
181  ------------------------------------------------------------------------  #------------------------------------------------------------------------
182  r256 | dpavlin | 2004-03-09 13:18:17 +0100 (Tue, 09 Mar 2004) | 2 lines  #r256 | dpavlin | 2004-03-09 13:18:17 +0100 (Tue, 09 Mar 2004) | 2 lines
183    #
184  ported r254 from hidra branch  #ported r254 from hidra branch
185    #
186  =cut  #=cut
187    
188  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";
189    
190  if (! $xml->{'logentry'}) {  if (! $xml->{'logentry'}) {
191          print "no newer log entries in SubVersion repostory. CVS is current\n";          print "no newer log entries in Subversion repostory. CVS is current\n";
192          exit 0;          exit 0;
193  }  }
194    
195  print Dumper($xml);  # 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'}}) {  foreach my $e (@{$xml->{'logentry'}}) {
215          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'});
216          $rev = $e->{'revision'};          $rev = $e->{'revision'};
217          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");
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#(/[^/]+)/*$##) {
225                            $SVNREP = $1 . $SVNREP;
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'});          printf($fmt, $e->{'revision'}, $e->{'author'}, $e->{'date'}, $e->{'msg'});
235            my @commit;
236    
237          foreach my $p (@{$e->{'paths'}->{'path'}}) {          foreach my $p (@{$e->{'paths'}->{'path'}}) {
238                  my ($action,$path) = ($p->{'action'},$p->{'content'});                  my ($action,$path) = ($p->{'action'},$p->{'content'});
239    
# Line 170  foreach my $e (@{$xml->{'logentry'}}) { Line 241  foreach my $e (@{$xml->{'logentry'}}) {
241    
242                  # prepare path and message                  # prepare path and message
243                  my $file = $path;                  my $file = $path;
244                  $path =~ s,^/$SVNREP/*,, || die "BUG: can't strip SVNREP from path";                  $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'};                  my $msg = $e->{'msg'};
252                  $msg =~ s/"/\\"/g;      # quote "                  $msg =~ s/'/'\\''/g;    # quote "
253    
254                  if ($action =~ /M/) {                  if ($action =~ /M/) {
255                          print "svn2cvs: modify $path -- nop\n";                          print "svn2cvs: modify $path -- nop\n";
256                  } elsif ($action =~ /A/) {                  } elsif ($action =~ /A/) {
257                          log_system("$cvs add -m \"$msg\" $CVSREP/$path", "cvs add of $path failed");                          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/) {                  } elsif ($action =~ /D/) {
276                          log_system("$cvs delete -m \"$msg\" $CVSREP/$path", "cvs delete of $path failed");                          unlink $path || die "can't delete $path: $!";
277                            log_system("$cvs delete $path", "cvs delete of $path failed");
278                  } else {                  } else {
279                          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";
280                  }                  }
281    
282                  # now commit changes                  # save commits for later
283                  log_system("$cvs commit -m \"$msg\" $CVSREP/$path", "cvs commit of $path failed");                  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);          commit_svnrev($rev);
294  }  }
295    
296  __END__  __END__
297    
298  svn export --force "$SVNROOT/$SVNREP" "$CVSREP"  =pod
299    
300  cd dotfiles  =head1 NAME
301    
302  for file in $(find -type f -not -path \*CVS\*); do  svn2cvs - save subversion commits to (read-only) cvs repository
303          FILE=$(basename $file)  
304          DIR=$(dirname $file)  =head1 SYNOPSIS
305          if ! grep -q "^/$FILE/" $DIR/CVS/Entries ; then  
306                  cvs add $file    ./svn2cvs.pl SVN_URL CVSROOT CVSREPOSITORY
307          fi  
308  done  Usage example (used to self-host this script):
309    
310  #cvs commit -m "Automatic commit from SVN"    ./svn2cvs.pl file:///home/dpavlin/private/svn/svn2cvs/trunk/ \
311                   :pserver:dpavlin@cvs.tigris.org:/cvs svn2cvs/src
312  #rm -rf $TMPDIR  
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    =item r20
408    
409    Fixed path deduction (overlap between Subversion reporistory and CVS checkout).
410    
411    =back
412    
413    =head1 AUTHOR
414    
415    Dobrica Pavlinusic <dpavlin@rot13.org>
416    
417    L<https://www.rot13.org/~dpavlin/>
418    
419    =head1 LICENSE
420    
421    This product is licensed under GNU Public License (GPL) v2 or later.
422    
423    =cut
424    
 echo "cvs left in $TMPDIR"  

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

  ViewVC Help
Powered by ViewVC 1.1.26