Fixed the new deletion method. Deleting /a/path will delete /a/path/* also and not...
[git2svn.git] / git2svn
blobf6e17fe76b7f2dcc4c2cd52571c8ee94a6a8f631
1 #!/usr/bin/perl
3 # git2svn, converts a git branch to a svn ditto
5 # Copyright (c) 2008 Love Hörnquist Åstrand
6 # All rights reserved.
7 #
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions
10 # are met:
12 # 1. Redistributions of source code must retain the above copyright
13 # notice, this list of conditions and the following disclaimer.
15 # 2. Redistributions in binary form must reproduce the above copyright
16 # notice, this list of conditions and the following disclaimer in the
17 # documentation and/or other materials provided with the distribution.
19 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
20 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22 # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
23 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
25 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
28 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
29 # SUCH DAMAGE.
31 use strict;
32 use POSIX qw(strftime);
33 use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
34 use Pod::Usage;
36 my $IN;
37 my $OUT;
39 my ($help, $verbose, $keeplogs, $no_load);
41 # svn
42 my $svntree = "repro";
43 my $basedir = "trunk";
44 my $revision = 1;
46 # git
47 my $branch = "master";
48 my $syncname;
49 my $masterrev;
50 my $fexport;
52 my %blob;
53 my %paths;
55 sub read_data
57 my ($IN, $next, $length, $data, $l) = (shift, shift);
58 unless($next =~ m/^data (\d+)/) { die "missing data: $next" ; }
59 $length = $1;
61 $l = read(IN, $data, $length);
62 unless ($l == $length) { die "failed to read data $l != $length"; }
63 $data = "" if ($length == 0);
64 return $data;
67 sub prop
69 my ($key, $value) = (shift, shift);
70 "K " . length($key) . "\n$key\nV " . length($value) . "\n$value\n";
73 sub parse_svn_tree
75 my $url = shift;
76 my ($SVN, $type, $name);
78 open(SVN, "svn ls -R $url|") or die "failed to open svn ls -R $url";
79 while (<SVN>) {
80 chomp;
81 if (m@/?(.*)/$@) {
82 $type = 1;
83 $name = "$1";
84 } else {
85 $type = 2;
86 $name = "$_";
88 $paths{$name} = $type;
90 close SVN;
92 open(SVN, "svn info $url|") or die "failed to open svn info $url";
93 while (<SVN>) {
94 chomp;
95 if ($revision eq 1 && /^Revision: (\d+)/) {
96 $revision = $1 + 1;
99 close SVN;
102 sub find_branch_id
104 my ($gittree, $name) = (shift, shift);
106 my $GIT;
108 open FOO, "cd $gittree ".
109 "&& git rev-parse $name 2>/dev/null|" or
110 die "git rev-parse $name failed";
112 my $id = <FOO>;
113 close GIT;
114 chomp($id);
115 $id = "" if ($id eq $name);
116 return $id;
119 sub parse_git_tree
121 my ($gittree, $branch, $shortbranch) = (shift, shift, shift);
123 $syncname = "git2svn-syncpoint-${shortbranch}";
124 print STDERR "syncname tag: $syncname\n" if ($verbose);
126 $masterrev = find_branch_id($gittree, $branch);
127 die "No head found for ${branch}" if ($masterrev eq "");
129 my $oldmasterrev = find_branch_id($gittree, $syncname);
131 if ($oldmasterrev ne "") {
133 die "no $svntree, but incremental (have synctag) ?\n".
134 "(\"cd $gittree && git tag -d $syncname\" to remove)"
135 unless ( -d $svntree);
137 if (${oldmasterrev} eq $masterrev) {
138 print STDERR "nothing to sync, $syncname matches $branch\n"
139 if ($verbose);
140 exit 0;
143 $fexport = "$oldmasterrev..$masterrev";
144 } else {
145 $fexport="$masterrev";
147 system("svnadmin create ./$svntree") unless (-d $svntree);
152 sub checkdirs
154 my $path = shift;
155 my $base = "";
157 # pick first dir, create, take next dir, continue until we reached basename
158 while ($path =~ m@^([^/]+)/(.*)$@) {
159 my $first = $base . $1;
160 $path = $2;
161 $base = $first . "/";
162 next if ($paths{$first});
164 $paths{$first} = 1;
166 printf OUT "Node-path: $first\n";
167 printf OUT "Node-kind: dir\n";
168 printf OUT "Node-action: add\n";
169 printf OUT "Prop-content-length: 0\n";
170 printf OUT "Content-length: 0\n";
171 printf OUT "\n";
175 sub next_line
177 my $IN = shift;
178 my $next = <IN>;
179 chomp $next;
180 return $next;
184 # This is to allow setting props for a path, XXX add configuration
185 # file/options for this.
188 sub auto_props
190 my $path = shift;
191 ### given path, return prop("prop", "value")
192 return "";
196 $|= 1;
198 my $result;
199 $result = GetOptions ("git-branch=s" => \$branch,
200 "svn-prefix=s" => \$basedir,
201 "keep-logs" => \$keeplogs,
202 "no-load" => \$no_load,
203 "verbose+" => \$verbose,
204 "help" => \$help) or pod2usage(2);
206 pod2usage(0) if ($help);
208 die "to few arguments" if ($#ARGV < 1);
210 mkdir ".data" unless (-d ".data");
212 die "cant find branch name" unless ($branch =~ m@/?([^/]+)$@);
213 my $shortbranch = $1;
215 my $gittree = $ARGV[0];
216 $svntree = $ARGV[1];
218 # create an identifier by replacing path separators
219 # (i.e. "/", ":" and "\") with underscores
220 my $svntree_id = $svntree;
221 $svntree_id =~ s/[\/:\\]/_/g;
223 my $gitdump = ".data/git.dump-${svntree_id}-${shortbranch}";
224 my $svndump = ".data/svn.dump-${svntree_id}-${shortbranch}";
225 my $log = ".data/log-${svntree_id}-${shortbranch}";
227 parse_git_tree($gittree, $branch, $shortbranch);
229 my $cwd = `pwd`;
230 chomp($cwd);
231 parse_svn_tree("file://" . $cwd ."/". $svntree);
233 system(">$log");
235 print STDERR "git fast-export $branch ($fexport)\n" if ($verbose);
237 system("(cd $gittree && git fast-export $fexport) > $gitdump 2>$log") == 0 or
238 die "git fast-export: $!";
240 open IN, "$gitdump" or
241 die "failed to open $gitdump";
243 open OUT, ">$svndump" or
244 die "failed to open $svndump";
246 print STDERR "creating svn dump from revision $revision...\n" if ($verbose);
248 print OUT "SVN-fs-dump-format-version: 3\n";
250 my $next = next_line();
251 COMMAND: while (!eof(IN)) {
252 if ($next eq "") {
253 $next = next_line($IN);
254 next COMMAND;
255 } elsif ($next =~ /^commit (.*)/) {
257 my %commit;
259 $next = next_line($IN);
260 if ($next =~ m/mark +(.*)/) {
261 $commit{Mark} = $1;
262 $next = next_line($IN);
264 if ($next =~ m/author +(.*)/) {
265 $commit{Author} = $1;
266 $next = next_line($IN);
268 unless ($next =~ m/committer +(.+) +<([^>]+)> +(\d+) +[+-](\d+)$/) {
269 die "missing comitter: $_";
272 $commit{CommitterName} = $1;
273 $commit{CommitterEmail} = $2;
274 $commit{CommitterWhen} = $3;
275 $commit{CommitterTZ} = $4;
277 $next = next_line($IN);
278 my $log = read_data($IN, $next);
280 $next = next_line($IN);
281 if ($next =~ m/from (.*)/) {
282 $commit{From} = $1;
283 $next = next_line($IN);
285 if ($next =~ m/merge (.*)/) {
286 $commit{Merge} = $1;
287 $next = next_line($IN);
290 my $date =
291 strftime("%Y-%m-%dT%H:%M:%S.000000Z",
292 gmtime($commit{CommitterWhen}));
294 my $author = "(no author)";
295 if ($commit{CommitterEmail} =~ m/([^@]+)/) {
296 $author = $1;
298 $author = "git2svn-dump" if ($author eq "(no author)");
300 my $props = "";
301 $props .= prop("svn:author", $author);
302 $props .= prop("svn:log", $log);
303 $props .= prop("svn:date", $date);
304 $props .= "PROPS-END";
306 # push out svn info
308 printf OUT "Revision-number: $revision\n"; $revision++;
309 printf OUT "Prop-content-length: ". length($props) . "\n";
310 printf OUT "Content-length: " . length($props) . "\n";
311 printf OUT "\n";
312 print OUT "$props\n";
314 while (1) {
315 if ($next =~ m/M (\d+) (\S+) (.*)$/) {
316 my ($mode, $dataref, $path) = (oct $1, $2, $3);
317 my $content;
318 if ($dataref eq "inline") {
319 $next = next_line($IN);
320 $content = read_data($IN, $next);
321 } else {
322 if (defined $blob{$dataref}) {
323 $content = $blob{$dataref};
324 } else {
325 # Submodules cannot be converted
326 print STDERR "Ignored line, please check if this is a submodule: $next\n" if ($verbose);
327 $next = next_line($IN);
328 next;
330 # here we really want to delete $blob{$dataref},
331 # but it might be referenced in the future. To
332 # avoid keepig everything in memory for larger
333 # repositories this must be written out to disk
334 # and removed when done.
337 $path = "$basedir/$path";
338 checkdirs($path);
340 my $action = "add";
342 my $type = $mode & 0777000;
344 if ($paths{$path}) {
345 if (($paths{$path} != 2) && ($type != 0120000)) {
346 die "file was a dir";
347 } elsif (($paths{$path} != 2) && ($type == 0120000)) {
348 print STDERR "Dir is now a symlink, deleting: $path\n" if ($verbose);
349 delete $paths{$path};
350 foreach ( keys( %paths ) ) {
351 delete $paths{$_} if ( /^$path\// );
353 # This is now a file and not a path anymore
354 $paths{$path} = 2;
355 printf OUT "Node-path: $path\nNode-action: delete\n\n";
356 } else {
357 $action = "change";
359 } else {
360 $paths{$path} = 2;
364 my $kind = "";
365 $kind = "file" if ($type == 0100000);
366 $kind = "symlink" if ($type == 0120000);
367 die "$type unknown" if ($kind eq "");
369 $props = "";
370 $props .= prop("svn:executable", "on") if ($mode & 0111);
371 $props .= prop("svn:special", "*") if ($kind eq "symlink");
372 $props .= auto_props($path);
373 $props .= "PROPS-END\n" if ($props ne "");
375 $content = "link $content\n" if ($kind eq "symlink");
377 my $plen = length($props);
378 my $clen = length($content);
380 printf OUT "Node-path: $path\n";
381 printf OUT "Node-kind: file\n";
382 printf OUT "Node-action: $action\n";
383 printf OUT "Text-content-length: $clen\n";
384 printf OUT "Content-length: " . ($clen + $plen) . "\n";
385 printf OUT "Prop-content-length: $plen\n" if ($plen);
386 printf OUT "\n";
388 print OUT "$props" if ($plen);
390 print OUT $content;
391 printf OUT "\n";
392 } elsif ($next =~ m/D (.*)/) {
393 my $path = $basedir . "/". $1;
395 if ($paths{$path}) {
396 delete $paths{$path};
397 foreach ( keys( %paths ) ) {
398 delete $paths{$_} if ( /^$path\// );
401 printf OUT "Node-path: $path\n";
402 printf OUT "Node-action: delete\n";
403 printf OUT "\n";
404 } elsif ($verbose) {
405 print STDERR "deleting non existing object: $path\n";
408 } elsif ($next =~ m/^C (.*)/) {
409 die "file copy ?";
410 } elsif ($next =~ m/^R (.*)/) {
411 die "file rename ?";
412 } elsif ($next =~ m/^filedeleteall$/) {
413 die "file delete all ?";
414 } else {
415 next COMMAND;
417 $next = next_line($IN);
420 } elsif ($next =~ /^tag .*/) {
421 } elsif ($next =~ /^reset .*/) {
422 } elsif ($next =~ /^blob/) {
423 my $mark = undef;
424 $next = next_line($IN);
425 if ($next =~ m/mark (.*)/) {
426 $mark = $1;
427 $next = next_line($IN);
429 my $data = read_data($IN, $next);
430 $blob{$mark} = $data if (defined $mark);
431 } elsif ($next =~ /^checkpoint .*/) {
432 } elsif ($next =~ /^progress (.*)/) {
433 print STDERR "progress: $1\n" if ($verbose);
434 } else {
435 die "unknown command $next";
437 $next = next_line($IN);
440 close IN;
441 close OUT;
443 print STDERR "...dumped to revision $revision\n" if ($verbose);
444 print STDERR "(re-)setting sync-tag to new master\n" if ($verbose);
446 unless ($no_load) {
447 system("cd $gittree && ".
448 "git tag -m \"sync $(date)\" -a -f ${syncname} ${masterrev}");
451 print STDERR "loading dump into svn\n" if ($verbose);
453 unless ($no_load) {
454 system("svnadmin load $svntree < $svndump >>$log 2>&1") == 0 or
455 die "svnadmin load";
458 unlink $svndump, $gitdump, $log unless ($keeplogs);
460 exit 0;
463 __END__
465 =head1 NAME
467 B<git2svn> - converts a git branch to a svn ditto
469 =head1 SYNOPSIS
471 B<git2svn> [options] git-repro svn-repro
473 =head1 OPTIONS
475 =over 8
477 =item B<--git-branch>
479 The git branch to export. The default is branch is master.
481 =item B<--svn-prefix>
483 The svn prefix where the branch is import. The default is trunk to
484 match the default GIT branch (master).
486 =item B<--no-load>
488 Don't load the svn repository or update the syncpoint tagname.
490 =item B<--keep-logs>
492 Don't delete the logs in $CWD/.data on success.
494 =item B<--verbose>
496 More verbose output, can be give more then once to increase the verbosity.
498 =item B<--help>
500 Print a brief help message and exits.
502 =back
504 =head1 DESCRIPTION
506 B<git2svn> will convert a git branch to a svn ditto, it also
507 support incremantal updates.
509 B<git2svn> takes a git fast-export dump and converts it into a
510 svn dump that is feed into svnadmin load.
512 B<git2svn> assumes its the only process that writes into the svn
513 repository. This is because of the race between getting the to svn
514 Revsion number from the svn, creating the dump with correct Revsions,
515 and do the svnadmin load.
517 B<git2svn> also support incremental updates from a git branch to
518 a svn reprositry. Its does this by setting a git tag
519 (git2svn-syncpoint-<branchname>) where the last update was pulled
520 from.
522 B<git2svn> was created as a hack over a weekend to support a
523 smoother migration away from svn and allow users access to tools to
524 browse and search code (fisheye) and use anonymouns svn servers.
526 =head1 EXAMPLES
528 B<git2svn> ~/src/heimdal svn-repro
530 B<git2svn> --git-branch heimdal-1-0-branch \
531 --svn-prefix branches/heimdal-1-0-branch \
532 ~/src/heimdal svn-repro
534 =head1 DOWNLOAD
536 B<git2svn> is avaible from repo.or.cz
538 git clone git://repo.or.cz/git2svn.git
540 =head1 AUTHORS
542 Love Hörnquist Åstrand <lha@kth.se>
544 =head1 BUGS
546 Send bug reports to lha@kth.se
548 =cut