90009b60ce71e0d543080d4e40c092390b47eec8
[git2svn.git] / git2svn
blob90009b60ce71e0d543080d4e40c092390b47eec8
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, $ignore_author);
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 "ignore-author" => \$ignore_author,
204 "verbose+" => \$verbose,
205 "help" => \$help) or pod2usage(2);
207 pod2usage(0) if ($help);
209 die "to few arguments" if ($#ARGV < 1);
211 mkdir ".data" unless (-d ".data");
213 die "cant find branch name" unless ($branch =~ m@/?([^/]+)$@);
214 my $shortbranch = $1;
216 my $gittree = $ARGV[0];
217 $svntree = $ARGV[1];
219 # create an identifier by replacing path separators
220 # (i.e. "/", ":" and "\") with underscores
221 my $svntree_id = $svntree;
222 $svntree_id =~ s/[\/:\\]/_/g;
224 my $gitdump = ".data/git.dump-${svntree_id}-${shortbranch}";
225 my $svndump = ".data/svn.dump-${svntree_id}-${shortbranch}";
226 my $log = ".data/log-${svntree_id}-${shortbranch}";
228 parse_git_tree($gittree, $branch, $shortbranch);
230 my $cwd = `pwd`;
231 chomp($cwd);
232 parse_svn_tree("file://" . $cwd ."/". $svntree);
234 system(">$log");
236 print STDERR "git fast-export $branch ($fexport)\n" if ($verbose);
238 system("(cd $gittree && git fast-export $fexport) > $gitdump 2>$log") == 0 or
239 die "git fast-export: $!";
241 open IN, "$gitdump" or
242 die "failed to open $gitdump";
244 open OUT, ">$svndump" or
245 die "failed to open $svndump";
247 print STDERR "creating svn dump from revision $revision...\n" if ($verbose);
249 print OUT "SVN-fs-dump-format-version: 3\n";
251 my $next = next_line();
252 COMMAND: while (!eof(IN)) {
253 if ($next eq "") {
254 $next = next_line($IN);
255 next COMMAND;
256 } elsif ($next =~ /^commit (.*)/) {
258 my %commit;
260 $next = next_line($IN);
261 if ($next =~ m/mark +(.*)/) {
262 $commit{Mark} = $1;
263 $next = next_line($IN);
265 if ($next =~ m/author +(.+) +<([^>]+)> +(\d+) +[+-](\d+)$/) {
266 $commit{AuthorName} = $1;
267 $commit{AuthorEmail} = $2;
268 $commit{AuthorWhen} = $3;
269 $commit{AuthorTZ} = $4;
270 $next = next_line($IN);
272 unless ($next =~ m/committer +(.+) +<([^>]+)> +(\d+) +[+-](\d+)$/) {
273 die "missing committer: $_";
276 $commit{CommitterName} = $1;
277 $commit{CommitterEmail} = $2;
278 $commit{CommitterWhen} = $3;
279 $commit{CommitterTZ} = $4;
281 $next = next_line($IN);
282 my $log = read_data($IN, $next);
283 $log =~ s/\s+$//;
285 $next = next_line($IN);
286 if ($next =~ m/from (.*)/) {
287 $commit{From} = $1;
288 $next = next_line($IN);
290 if ($next =~ m/merge (.*)/) {
291 $commit{Merge} = $1;
292 $next = next_line($IN);
295 my $date =
296 strftime("%Y-%m-%dT%H:%M:%S.000000Z",
297 gmtime($commit{CommitterWhen}));
299 my $author = "git2svn-dump";
300 if ($commit{CommitterEmail} =~ m/([^@]+)/) {
301 $author = $1;
303 unless ($ignore_author) {
304 if ($commit{AuthorEmail} =~ m/([^@]+)/) {
305 $author = $1;
309 my $props = "";
310 $props .= prop("svn:author", $author);
311 $props .= prop("svn:log", $log);
312 $props .= prop("svn:date", $date);
313 $props .= "PROPS-END";
315 # push out svn info
317 printf OUT "Revision-number: $revision\n"; $revision++;
318 printf OUT "Prop-content-length: ". length($props) . "\n";
319 printf OUT "Content-length: " . length($props) . "\n";
320 printf OUT "\n";
321 print OUT "$props\n";
323 while (1) {
324 if ($next =~ m/M (\d+) (\S+) (.*)$/) {
325 my ($mode, $dataref, $path) = (oct $1, $2, $3);
326 my $content;
327 if ($dataref eq "inline") {
328 $next = next_line($IN);
329 $content = read_data($IN, $next);
330 } else {
331 if (defined $blob{$dataref}) {
332 $content = $blob{$dataref};
333 } else {
334 # Submodules cannot be converted
335 print STDERR "Ignored line, please check if this is a submodule: $next\n" if ($verbose);
336 $next = next_line($IN);
337 next;
339 # here we really want to delete $blob{$dataref},
340 # but it might be referenced in the future. To
341 # avoid keepig everything in memory for larger
342 # repositories this must be written out to disk
343 # and removed when done.
346 $path = "$basedir/$path";
347 checkdirs($path);
349 my $action = "add";
351 my $type = $mode & 0777000;
353 if ($paths{$path}) {
354 if (($paths{$path} != 2) && ($type != 0120000)) {
355 die "file was a dir";
356 } elsif (($paths{$path} != 2) && ($type == 0120000)) {
357 print STDERR "Dir is now a symlink, deleting: $path\n" if ($verbose);
358 foreach ( keys( %paths ) ) {
359 delete $paths{$_} if ( /^$path\// );
361 # This is now a file and not a path anymore
362 $paths{$path} = 2;
363 printf OUT "Node-path: $path\nNode-action: delete\n\n";
364 } else {
365 $action = "change";
367 } else {
368 $paths{$path} = 2;
372 my $kind = "";
373 $kind = "file" if ($type == 0100000);
374 $kind = "symlink" if ($type == 0120000);
375 die "$type unknown" if ($kind eq "");
377 $props = "";
378 $props .= prop("svn:executable", "on") if ($mode & 0111);
379 $props .= prop("svn:special", "*") if ($kind eq "symlink");
380 $props .= auto_props($path);
381 $props .= "PROPS-END\n" if ($props ne "");
383 $content = "link $content\n" if ($kind eq "symlink");
385 my $plen = length($props);
386 my $clen = length($content);
388 printf OUT "Node-path: $path\n";
389 printf OUT "Node-kind: file\n";
390 printf OUT "Node-action: $action\n";
391 printf OUT "Text-content-length: $clen\n";
392 printf OUT "Content-length: " . ($clen + $plen) . "\n";
393 printf OUT "Prop-content-length: $plen\n" if ($plen);
394 printf OUT "\n";
396 print OUT "$props" if ($plen);
398 print OUT $content;
399 printf OUT "\n";
400 } elsif ($next =~ m/D (.*)/) {
401 my $path = $basedir . "/". $1;
403 if ($paths{$path}) {
404 delete $paths{$path};
405 foreach ( keys( %paths ) ) {
406 delete $paths{$_} if ( /^$path\// );
409 printf OUT "Node-path: $path\n";
410 printf OUT "Node-action: delete\n";
411 printf OUT "\n";
412 } elsif ($verbose) {
413 print STDERR "deleting non existing object: $path\n";
416 } elsif ($next =~ m/^C (.*)/) {
417 die "file copy ?";
418 } elsif ($next =~ m/^R (.*)/) {
419 die "file rename ?";
420 } elsif ($next =~ m/^filedeleteall$/) {
421 die "file delete all ?";
422 } else {
423 next COMMAND;
425 $next = next_line($IN);
428 } elsif ($next =~ /^tag .*/) {
429 } elsif ($next =~ /^reset .*/) {
430 } elsif ($next =~ /^blob/) {
431 my $mark = undef;
432 $next = next_line($IN);
433 if ($next =~ m/mark (.*)/) {
434 $mark = $1;
435 $next = next_line($IN);
437 my $data = read_data($IN, $next);
438 $blob{$mark} = $data if (defined $mark);
439 } elsif ($next =~ /^checkpoint .*/) {
440 } elsif ($next =~ /^progress (.*)/) {
441 print STDERR "progress: $1\n" if ($verbose);
442 } else {
443 die "unknown command $next";
445 $next = next_line($IN);
448 close IN;
449 close OUT;
451 print STDERR "...dumped to revision $revision\n" if ($verbose);
452 print STDERR "(re-)setting sync-tag to new master\n" if ($verbose);
454 unless ($no_load) {
455 system("cd $gittree && ".
456 "git tag -m \"sync $(date)\" -a -f ${syncname} ${masterrev}");
459 print STDERR "loading dump into svn\n" if ($verbose);
461 unless ($no_load) {
462 system("svnadmin load $svntree < $svndump >>$log 2>&1") == 0 or
463 die "svnadmin load";
466 unlink $svndump, $gitdump, $log unless ($keeplogs);
468 exit 0;
471 __END__
473 =head1 NAME
475 B<git2svn> - converts a git branch to a svn ditto
477 =head1 SYNOPSIS
479 B<git2svn> [options] git-repro svn-repro
481 =head1 OPTIONS
483 =over 8
485 =item B<--git-branch>
487 The git branch to export. The default is branch is master.
489 =item B<--svn-prefix>
491 The svn prefix where the branch is import. The default is trunk to
492 match the default GIT branch (master).
494 =item B<--no-load>
496 Don't load the svn repository or update the syncpoint tagname.
498 =item B<--ignore-author>
500 Ignore "author" lines in the fast-import stream. Use "committer"
501 information instead.
503 =item B<--keep-logs>
505 Don't delete the logs in $CWD/.data on success.
507 =item B<--verbose>
509 More verbose output, can be give more then once to increase the verbosity.
511 =item B<--help>
513 Print a brief help message and exits.
515 =back
517 =head1 DESCRIPTION
519 B<git2svn> will convert a git branch to a svn ditto, it also
520 support incremantal updates.
522 B<git2svn> takes a git fast-export dump and converts it into a
523 svn dump that is feed into svnadmin load.
525 B<git2svn> assumes its the only process that writes into the svn
526 repository. This is because of the race between getting the to svn
527 Revsion number from the svn, creating the dump with correct Revsions,
528 and do the svnadmin load.
530 B<git2svn> also support incremental updates from a git branch to
531 a svn reprositry. Its does this by setting a git tag
532 (git2svn-syncpoint-<branchname>) where the last update was pulled
533 from.
535 B<git2svn> was created as a hack over a weekend to support a
536 smoother migration away from svn and allow users access to tools to
537 browse and search code (fisheye) and use anonymouns svn servers.
539 =head1 EXAMPLES
541 B<git2svn> ~/src/heimdal svn-repro
543 B<git2svn> --git-branch heimdal-1-0-branch \
544 --svn-prefix branches/heimdal-1-0-branch \
545 ~/src/heimdal svn-repro
547 =head1 DOWNLOAD
549 B<git2svn> is avaible from repo.or.cz
551 git clone git://repo.or.cz/git2svn.git
553 =head1 AUTHORS
555 Love Hörnquist Åstrand <lha@kth.se>
557 =head1 BUGS
559 Send bug reports to lha@kth.se
561 =cut