/[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 17 by dpavlin, Thu Mar 18 12:29:15 2004 UTC revision 47 by dpavlin, Fri Nov 2 12:11:31 2007 UTC
# Line 13  Line 13 
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  if (@ARGV < 2) {  # do we want to sync just part of repository?
21    my $partial_import = 1;
22    
23    # do we want to add svk-like prefix with original revision, author and date?
24    my $decorate_commit_message = 1;
25    
26    if ( @ARGV < 2 ) {
27          print "usage: $0 SVN_URL CVSROOT CVSREPOSITORY\n";          print "usage: $0 SVN_URL CVSROOT CVSREPOSITORY\n";
28          exit 1;          exit 1;
29  }  }
30    
31  my ($SVNROOT,$CVSROOT, $CVSREP) = @ARGV;  my ( $SVNROOT, $CVSROOT, $CVSREP ) = @ARGV;
32    
33  if ($SVNROOT !~ m,^[\w+]+:///*\w+,) {  if ( $SVNROOT !~ m,^[\w+]+:///*\w+, ) {
34          print "ERROR: invalid svn root $SVNROOT\n";          print "ERROR: invalid svn root $SVNROOT\n";
35          exit 1;          exit 1;
36  }  }
37    
38  my $TMPDIR=tempdir( "/tmp/checkoutXXXXX", CLEANUP => 1 );  # Ensure File::Temp::END can clean up:
39    $SIG{__DIE__} = sub { chdir("/tmp"); die @_ };
40    
41  chdir($TMPDIR) || die "can't cd to $TMPDIR: $!";  my $TMPDIR = tempdir( "/tmp/checkoutXXXXX", CLEANUP => 1 );
42    
43    sub cd_tmp {
44            chdir($TMPDIR) || die "can't cd to $TMPDIR: $!";
45    }
46    
47    sub cd_rep {
48            chdir("$TMPDIR/$CVSREP") || die "can't cd to $TMPDIR/$CVSREP: $!";
49    }
50    
51    print "## using TMPDIR $TMPDIR\n";
52    
53  # cvs command with root  # cvs command with root
54  my $cvs="cvs -d $CVSROOT";  my $cvs = "cvs -f -d $CVSROOT";
55    
56    # current revision in CVS
57    my $rev;
58    
59  #  #
60  # sub to do logging and system calls  # sub to do logging and system calls
61  #  #
62  sub log_system($$) {  sub log_system($$) {
63          my ($cmd,$errmsg) = @_;          my ( $cmd, $errmsg ) = @_;
64          print STDERR "## $cmd\n";          print STDERR "## $cmd\n";
65          system($cmd) == 0 || die "$errmsg: $!";          system($cmd) == 0 || die "$errmsg: $!";
66  }  }
# Line 48  sub log_system($$) { Line 69  sub log_system($$) {
69  # sub to commit .svn rev file later  # sub to commit .svn rev file later
70  #  #
71  sub commit_svnrev {  sub commit_svnrev {
72          my $rev = shift @_;          my $rev     = shift @_;
73          my $add_new = shift @_;          my $add_new = shift @_;
74    
75          die "commit_svnrev needs revision" if (! defined($rev));          die "commit_svnrev needs revision" if ( !defined($rev) );
76    
77          open(SVNREV,"> .svnrev") || die "can't open $TMPDIR/$CVSREP/.svnrev: $!";          open( SVNREV, "> .svnrev" )
78                    || die "can't open $TMPDIR/$CVSREP/.svnrev: $!";
79          print SVNREV $rev;          print SVNREV $rev;
80          close(SVNREV);          close(SVNREV);
81    
82          my $path=".svnrev";          my $path = ".svnrev";
83    
84          if ($add_new) {          if ($add_new) {
85                  system "$cvs add $path" || die "cvs add of $path failed: $!";                  system "$cvs add '$path'" || die "cvs add of $path failed: $!";
86          } else {          } else {
87                  my $msg="subversion revision $rev commited to CVS";                  my $msg = "subversion revision $rev commited to CVS";
88                  print "$msg\n";                  print "$msg\n";
89                  system "$cvs commit -m '$msg' $path" || die "cvs commit of $path failed: $!";                  system "$cvs commit -m '$msg' '$path'"
90                            || die "cvs commit of $path failed: $!";
91            }
92    }
93    
94    sub add_dir($$) {
95            my ( $path, $msg ) = @_;
96            print "# add_dir($path)\n";
97            die "add_dir($path) is not directory" unless ( -d $path );
98    
99            my $curr_dir;
100    
101            foreach my $d ( split( m#/#, $path ) ) {
102                    $curr_dir .= ( $curr_dir ? '/' : '' ) . $d;
103    
104                    next if in_entries($curr_dir);
105                    next if ( -e "$curr_dir/CVS" );
106    
107                    log_system( "touch '$curr_dir/.keepme'", "creation of .keepme file (to keep $curr_dir alive in CVS) failed" );
108                    log_system( "$cvs add '$curr_dir' '$curr_dir/.keepme'", "cvs add of $curr_dir failed" );
109          }          }
110  }  }
111    
112  # ok, now do the checkout  # ok, now do the checkout
113    eval {
114            cd_tmp;
115            log_system( "$cvs -q checkout $CVSREP", "cvs checkout failed" );
116    };
117    
118  log_system("$cvs -q checkout $CVSREP", "cvs checkout failed");  if ($@) {
119            print <<_NEW_REP_;
120    
121  chdir($CVSREP) || die "can't cd to $TMPDIR/$CVSREP: $!";  There is no CVS repository '$CVSREP' in your CVS. I will assume that
122    this is import of new module in your CVS and start from revision 0.
123    
124    Press enter to continue importing new CVS repository or CTRL+C to abort.
125    
126  my $rev;  _NEW_REP_
127    
128            print "start import of new module [yes]: ";
129            my $in = <STDIN>;
130            cd_tmp;
131            mkdir($CVSREP) || die "can't create $CVSREP: $!";
132            cd_rep;
133    
134            open( SVNREV, "> .svnrev" ) || die "can't open $CVSREP/.svnrev: $!";
135            print SVNREV "0";
136            close(SVNREV);
137    
138            $rev = 0;
139    
140            # create new module
141            cd_rep;
142            log_system( "$cvs import -d -m 'new CVS module' $CVSREP svn r$rev",
143                    "import of new repository" );
144            cd_tmp;
145            rmtree($CVSREP) || die "can't remove $CVSREP";
146            log_system( "$cvs -q checkout $CVSREP", "cvs checkout failed" );
147            cd_rep;
148    
149    } else {
150    
151            # import into existing module directory in CVS
152    
153            cd_rep;
154    
155  # check if svnrev exists          # check if svnrev exists
156  if (! -e ".svnrev") {          if ( !-e ".svnrev" ) {
157          print <<_USAGE_;                  print <<_USAGE_;
158    
159  Your CVS repository doesn't have .svnrev file!  Your CVS repository doesn't have .svnrev file!
160    
# Line 97  Subversion repository to CVS, correct re Line 172  Subversion repository to CVS, correct re
172    
173  _USAGE_  _USAGE_
174    
175          print "svn revision corresponding to CVS [abort]: ";                  print "svn revision corresponding to CVS [abort]: ";
176          my $in = <STDIN>;                  my $in = <STDIN>;
177          chomp($in);                  chomp($in);
178          if ($in !~ /^\d+$/) {                  if ( $in !~ /^\d+$/ ) {
179                  print "Aborting: revision not a number\n";                          print "Aborting: revision not a number\n";
180                  exit 1;                          exit 1;
181                    } else {
182                            $rev = $in;
183                            commit_svnrev( $rev, 1 );    # create new
184                    }
185          } else {          } else {
186                  $rev = $in;                  open( SVNREV, ".svnrev" )
187                  commit_svnrev($rev,1);  # create new                          || die "can't open $TMPDIR/$CVSREP/.svnrev: $!";
188                    $rev = <SVNREV>;
189                    chomp($rev);
190                    close(SVNREV);
191          }          }
 } else {  
         open(SVNREV,".svnrev") || die "can't open $TMPDIR/$CVSREP/.svnrev: $!";  
         $rev = <SVNREV>;  
         chomp($rev);  
         close(SVNREV);  
 }  
   
 print "Starting after revision $rev\n";  
 $rev++;  
192    
193            print "Starting after revision $rev\n";
194            $rev++;
195    }
196    
197  #  #
198  # FIXME!! HEAD should really be next verison and loop because this way we  # FIXME!! HEAD should really be next verison and loop because this way we
# Line 125  $rev++; Line 201  $rev++;
201  # case much about accuracy and completnes of logs there, this might  # case much about accuracy and completnes of logs there, this might
202  # be good. YMMV  # be good. YMMV
203  #  #
204  open(LOG, "svn log -r $rev:HEAD -v --xml $SVNROOT |") || die "svn log for repository $SVNROOT failed: $!";  open( LOG, "svn log -r $rev:HEAD -v --xml $SVNROOT |" )
205            || die "svn log for repository $SVNROOT failed: $!";
206  my $log;  my $log;
207  while(<LOG>) {  while (<LOG>) {
208          $log .= $_;          $log .= $_;
209  }  }
210  close(LOG);  close(LOG);
211    
212  my $xml = XMLin($log, ForceArray => [ 'logentry', 'path' ]);  my $xml;
213    eval { $xml = XMLin( $log, ForceArray => [ 'logentry', 'path' ] ); };
214    
215  #=begin log_example  #=begin log_example
216  #  #
# Line 146  my $xml = XMLin($log, ForceArray => [ 'l Line 223  my $xml = XMLin($log, ForceArray => [ 'l
223    
224  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";
225    
226  if (! $xml->{'logentry'}) {  if ( !$xml->{'logentry'} ) {
227          print "no newer log entries in Subversion repostory. CVS is current\n";          print "no newer log entries in Subversion repostory. CVS is current\n";
228          exit 0;          exit 0;
229  }  }
230    
231    # return all files in CVS/Entries
232    sub entries($) {
233            my $dir = shift;
234            die "entries expects directory argument!" unless -d $dir;
235            my @entries;
236            open( my $fh, "./$dir/CVS/Entries" ) || return 0;
237            while (<$fh>) {
238                    if ( m{^D/([^/]+)}, ) {
239                            my $sub_dir = $1;
240                            warn "#### entries recurse into: $dir/$sub_dir";
241                            push @entries, map {"$sub_dir/$_"} entries("$dir/$sub_dir");
242                            push @entries, $sub_dir;
243                    } elsif (m{^/([^/]+)/}) {
244                            push @entries, $1;
245                    } elsif ( !m{^D$} ) {
246                            die "can't decode entries line: $_";
247                    }
248            }
249            close($fh);
250            warn "#### entries($dir) => ", join( "|", @entries );
251            return @entries;
252    }
253    
254  # check if file exists in CVS/Entries  # check if file exists in CVS/Entries
255  sub in_entries($) {  sub in_entries($) {
256          my $path = shift;          my $path = shift;
257          if ($path !~ m,^(.*?/*)([^/]+)$,) {          if ( $path =~ m,^(.*?/*)([^/]+)$, ) {
258                  die "can't split '$path' to dir and file!";                  my ( $dir, $file ) = ( $1, $2 );
259          } else {                  if ( $dir !~ m,/$, && $dir ne "" ) {
260                  my ($d,$f) = ($1,$2);                          $dir .= "/";
                 if ($d !~ m,/$, && $d ne "") {  
                         $d .= "/";  
261                  }                  }
262                  open(E, $d."CVS/Entries") || die "can't open ${d}CVS/Entries: $!";  
263                  while(<E>) {                  open( my $fh, "./$dir/CVS/Entries" )
264                          return(1) if (m,^/$f/,);                          || return 0;    #die "no entries file: $dir/CVS/Entries";
265                    while (<$fh>) {
266                            return 1 if (m{^D?/$file/});
267                  }                  }
268                  close(E);                  close($fh);
269                  return 0;                  return 0;
270            } else {
271                    die "can't split '$path' to dir and file!";
272          }          }
273  }  }
274    
275  foreach my $e (@{$xml->{'logentry'}}) {  cd_tmp;
276          die "BUG: revision from .svnrev ($rev) greater than from subversion (".$e->{'revision'}.")" if ($rev > $e->{'revision'});  cd_rep;
277    
278    foreach my $e ( @{ $xml->{'logentry'} } ) {
279            die "BUG: revision from .svnrev ($rev) greater than from subversion ("
280                    . $e->{'revision'} . ")"
281                    if ( $rev > $e->{'revision'} );
282          $rev = $e->{'revision'};          $rev = $e->{'revision'};
283          log_system("svn export --force -q -r $rev $SVNROOT $TMPDIR/$CVSREP", "svn export of revision $rev failed");          log_system( "svn export --force -q -r $rev $SVNROOT $TMPDIR/$CVSREP",
284                    "svn export of revision $rev failed" );
285    
286          # deduce name of svn directory          # deduce name of svn directory
287          my $SVNREP = "";          my $SVNREP  = "";
288          my $tmpsvn = $SVNROOT || die "BUG: SVNROOT empty!";          my $tmpsvn  = $SVNROOT || die "BUG: SVNROOT empty!";
289          my $tmppath = $e->{'paths'}->{'path'}->[0]->{'content'} || die "BUG: tmppath empty!";          my $tmppath = $e->{'paths'}->{'path'}->[0]->{'content'}
290                    || die "BUG: tmppath empty!";
291          do {          do {
292                  if ($tmpsvn =~ s,(/\w+/*)$,,) {                  if ( $tmpsvn =~ s#(/[^/]+)/*$## ) {    # vim fix
293                          $SVNREP .= $1;                          $SVNREP = $1 . $SVNREP;
294                    } elsif ( $e->{'paths'}->{'path'}->[0]->{'copyfrom-path'} ) {
295                            print
296                                    "NOTICE: copyfrom outside synced repository ignored - skipping\n";
297                            next;
298                  } else {                  } else {
299                          print "NOTICE: can't deduce svn dir from $SVNROOT - skipping\n";                          print "NOTICE: can't deduce svn dir from $SVNROOT - skipping\n";
300                          next;                          next;
301                  }                  }
302          } until ($tmppath =~ m/^$SVNREP/);          } until ( $tmppath =~ m/^$SVNREP/ );
303    
304          print "NOTICE: using $SVNREP as directory for svn\n";          print "NOTICE: using $SVNREP as directory for svn\n";
305    
306          printf($fmt, $e->{'revision'}, $e->{'author'}, $e->{'date'}, $e->{'msg'});          printf( $fmt,
307          foreach my $p (@{$e->{'paths'}->{'path'}}) {                  $e->{'revision'}, $e->{'author'}, $e->{'date'}, $e->{'msg'} );
308                  my ($action,$path) = ($p->{'action'},$p->{'content'});          my @commit;
309    
310            my $msg = $e->{'msg'};
311            $msg =~ s/'/'\\''/g;    # quote "
312    
313            $msg = 'r' . $rev . ' ' . $e->{author} . ' | ' . $e->{date} . "\n" . $msg
314                    if $decorate_commit_message;
315    
316            sub cvs_commit {
317                    my $msg = shift || die "no msg?";
318                    if ( !@_ ) {
319                            warn "commit ignored, no files\n";
320                            return;
321                    }
322                    log_system(
323                            "$cvs commit -m '$msg' '" . join( "' '", @_ ) . "'",
324                            "cvs commit of " . join( ",",            @_ ) . " failed"
325                    );
326            }
327    
328            foreach my $p ( @{ $e->{'paths'}->{'path'} } ) {
329                    my ( $action, $path ) = ( $p->{'action'}, $p->{'content'} );
330    
331                    next if ( $path =~ m#/\.svnrev$# );
332    
333                  print "svn2cvs: $action $path\n";                  print "svn2cvs: $action $path\n";
334    
335                  # prepare path and message                  # prepare path and message
336                  my $file = $path;                  my $file = $path;
337                  $path =~ s,^$SVNREP/*,, || die "BUG: can't strip SVNREP from path";                  if ( $path !~ s#^\Q$SVNREP\E/*## ) {
338                            print
339                                    "NOTICE: skipping '$path' which isn't under repository root '$SVNREP'\n";
340                            die unless $partial_import;
341                            next;
342                    }
343    
344                  if (! $path) {                  if ( !$path ) {
345                          print "NOTICE: skipped this operation. Probably trunk creation\n";                          print "NOTICE: skipped this operation. Probably trunk creation\n";
346                          next;                          next;
347                  }                  }
348    
349                  my $msg = $e->{'msg'};                  my $msg = $e->{'msg'};
350                  $msg =~ s/'/'\\''/g;    # quote "                  $msg =~ s/'/'\\''/g;    # quote "
351    
352                    sub add_path {
353                            my $path = shift || die "no path?";
354    
355                  if ($action =~ /M/) {                          if ( -d $path ) {
356                          print "svn2cvs: modify $path -- nop\n";                                  add_dir( $path, $msg );
357                  } elsif ($action =~ /A/) {                          } elsif ( $path =~ m,^(.+)/[^/]+$, && !-e "$1/CVS/Root" ) {
                         if (-d $path) {  
                                 chdir($path) || die "can't cd into dir $path for import: $!";  
                                 log_system("$cvs import -d -m '$msg' $CVSREP/$path svn r$rev", "cvs import of $path failed");  
                                 chdir("$TMPDIR") || die "can't cd to $TMPDIR/$CVSREP: $!";  
                                 log_system("$cvs checkout $CVSREP/$path", "cvs checkout of imported dir $path failed");  
                                 chdir("$TMPDIR/$CVSREP") || die "can't cd back to $TMPDIR/$CVSREP: $!";  
                         } elsif ($path =~ m,^(.+)/[^/]+$, && ! -e "$1/CVS/Root") {  
358                                  my $dir = $1;                                  my $dir = $1;
359                                  in_entries($dir) || log_system("$cvs add $dir", "cvs add of dir $dir failed");                                  in_entries($dir) || add_dir( $dir, $msg );
360                                  in_entries($path) || log_system("$cvs add $path", "cvs add of $path failed");                                  in_entries($path) || log_system( "$cvs add '$path'",
361                                            "cvs add of $path failed" );
362                          } else {                          } else {
363                                  in_entries($path) || log_system("$cvs add $path", "cvs add of $path failed");                                  in_entries($path) || log_system( "$cvs add '$path'",
364                                            "cvs add of $path failed" );
365                            }
366                    }
367    
368                    if ( $action =~ /M/ ) {
369                            if ( in_entries($path) ) {
370                                    print "svn2cvs: modify $path -- nop\n";
371                            } else {
372                                    print "WARNING: modify $path which isn't in CVS, adding...\n";
373                                    add_path($path);
374                            }
375                    } elsif ( $action =~ /A/ ) {
376                            add_path($path);
377                    } elsif ( $action =~ /D/ ) {
378                            if ( -e $path ) {
379                                    if ( ! in_entries( $path ) ) {
380                                            print "WARNING: $path is not present in CVS, skipping...\n";
381                                            undef $path;
382                                    } elsif ( -d $path ) {
383                                            warn "#### remove directory: $path";
384                                            foreach my $f ( entries($path) ) {
385                                                    $f = "$path/$f";
386                                                    if ( -f $f ) {
387                                                            unlink($f) || die "can't delete file $f: $!";
388    
389                                                      #                                             } else {
390                                                      #                                                     rmtree($f) || die "can't delete dir $f: $!";
391                                                    }
392                                                    log_system( "$cvs delete '$f'",
393                                                            "cvs delete of file $f failed" );
394                                                    cvs_commit( $msg, $f );
395                                            }
396                                            log_system( "$cvs delete '$path'",
397                                                    "cvs delete of file $path failed" );
398                                            cvs_commit( $msg, $path );
399                                            log_system( "$cvs update -dP .",
400                                                    "cvs update -dP . failed" );
401                                            undef $path;
402                                    } else {
403                                            warn "#### remove file: $path";
404                                            unlink($path) || die "can't delete $path: $!";
405                                            log_system( "$cvs delete '$path'",
406                                                    "cvs delete of dir $path failed" );
407                                            cvs_commit( $msg, $path );
408                                            undef $path;
409                                    }
410                            } else {
411                                    print "WARNING: $path is not present, skipping...\n";
412                                    undef $path;
413                          }                          }
                 } elsif ($action =~ /D/) {  
                         unlink $path || die "can't delete $path: $!";  
                         log_system("$cvs delete $path", "cvs delete of $path failed");  
414                  } else {                  } else {
415                          print "WARNING: action $action not implemented on $path. Bug or missing feature of $0\n";                          print
416                                    "WARNING: action $action not implemented on $path. Bug or missing feature of $0\n";
417                  }                  }
418    
419                  # now commit changes                  # save commits for later
420                  log_system("$cvs commit -m '$msg' $path", "cvs commit of $path failed");                  push @commit, $path if ($path);
421    
422          }          }
423    
424            # now commit changes
425            cvs_commit( $msg, @commit );
426    
427          commit_svnrev($rev);          commit_svnrev($rev);
428  }  }
429    
430    # cd out of $CVSREP before File::Temp::END is called
431    chdir("/tmp") || die "can't cd to /tmp: $!";
432    
433  __END__  __END__
434    
435  =pod  =pod
# Line 258  Usage example (used to self-host this sc Line 449  Usage example (used to self-host this sc
449    
450  =head1 DESCRIPTION  =head1 DESCRIPTION
451    
452  This script will allow you to commit changes made to Subversion repository also to (read-only) CVS repository manually or from Subversion's C<post-commit> hook.  This script will allows you to commit changes made to Subversion repository to
453    (read-only) CVS repository manually or from Subversion's C<post-commit> hook.
454    
455  It's using F<.svnrev> file (which will be created on first run) in  It's using F<.svnrev> file (which will be created on first run) in
456  B<CVSROOT/CVSREPOSITORY> to store last Subversion revision which was  B<CVSROOT/CVSREPOSITORY> to store last Subversion revision which was
# Line 310  again. Line 502  again.
502  "Cheap" copy operations in Subversion are not at all cheap in CVS. They will  "Cheap" copy operations in Subversion are not at all cheap in CVS. They will
503  create multiple copies of files in CVS repository!  create multiple copies of files in CVS repository!
504    
505    This script assume that you want to sync your C<trunk> (or any other
506    directory for that matter) directory with CVS, not root of your subversion.
507    This might be considered bug, but since common practise is to have
508    directories C<trunk> and C<branches> in svn and source code in them, it's
509    not serious limitation.
510    
511  =head1 RELATED PROJECTS  =head1 RELATED PROJECTS
512    
513  B<Subversion> L<http://subversion.tigris.org/> version control system that is a  B<Subversion> L<http://subversion.tigris.org/> version control system that is a
# Line 336  Addition of comprehensive documentation, Line 534  Addition of comprehensive documentation,
534  messages, and support for skipping changes which are not under current  messages, and support for skipping changes which are not under current
535  Subversion checkout root (e.g. branches).  Subversion checkout root (e.g. branches).
536    
537    =item r18
538    
539    Support for importing your svn into empty CVS repository (it will first
540    create module and than dump all revisions).
541    Group commit operations to save round-trips to CVS server.
542    Documentation improvements and other small fixes.
543    
544    =item r20
545    
546    Fixed path deduction (overlap between Subversion reporistory and CVS checkout).
547    
548    =item r21
549    
550    Use C<update -d> instead of checkout after import.
551    Added fixes by Paul Egan <paulegan@mail.com> for XMLin and fixing working
552    directory.
553    
554    =item r22
555    
556    Rewritten import from revision 0 to empty repository, better importing
557    of deep directory structures, initial support for recovery from partial
558    commit.
559    
560  =back  =back
561    
562  =head1 AUTHOR  =head1 AUTHOR

Legend:
Removed from v.17  
changed lines
  Added in v.47

  ViewVC Help
Powered by ViewVC 1.1.26