add list of authors
[git2svn.git] / git2svn
blob9ac6d3875e2542379b63e4f40387ec97170eee71
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 "";
195 sub isPrefix
197 my ($prefix, $string) = (shift, shift);
198 return $prefix eq substr($string, 0, length($prefix));
201 $|= 1;
203 my $result;
204 $result = GetOptions ("git-branch=s" => \$branch,
205 "svn-prefix=s" => \$basedir,
206 "keep-logs" => \$keeplogs,
207 "no-load" => \$no_load,
208 "ignore-author" => \$ignore_author,
209 "verbose+" => \$verbose,
210 "help" => \$help) or pod2usage(2);
212 pod2usage(0) if ($help);
214 die "to few arguments" if ($#ARGV < 1);
216 mkdir ".data" unless (-d ".data");
218 die "cant find branch name" unless ($branch =~ m@/?([^/]+)$@);
219 my $shortbranch = $1;
221 my $gittree = $ARGV[0];
222 $svntree = $ARGV[1];
224 # create an identifier by replacing path separators
225 # (i.e. "/", ":" and "\") with underscores
226 my $svntree_id = $svntree;
227 $svntree_id =~ s/[\/:\\]/_/g;
229 my $gitdump = ".data/git.dump-${svntree_id}-${shortbranch}";
230 my $svndump = ".data/svn.dump-${svntree_id}-${shortbranch}";
231 my $log = ".data/log-${svntree_id}-${shortbranch}";
233 parse_git_tree($gittree, $branch, $shortbranch);
235 my $cwd = `pwd`;
236 chomp($cwd);
237 parse_svn_tree("file://" . $cwd ."/". $svntree);
239 system(">$log");
241 print STDERR "git fast-export $branch ($fexport)\n" if ($verbose);
243 system("(cd $gittree && git fast-export $fexport) > $gitdump 2>$log") == 0 or
244 die "git fast-export: $!";
246 open IN, "$gitdump" or
247 die "failed to open $gitdump";
249 open OUT, ">$svndump" or
250 die "failed to open $svndump";
252 print STDERR "creating svn dump from revision $revision...\n" if ($verbose);
254 print OUT "SVN-fs-dump-format-version: 3\n";
256 my $next = next_line();
257 COMMAND: while (!eof(IN)) {
258 if ($next eq "") {
259 $next = next_line($IN);
260 next COMMAND;
261 } elsif ($next =~ /^commit (.*)/) {
263 my %commit;
265 $next = next_line($IN);
266 if ($next =~ m/mark +(.*)/) {
267 $commit{Mark} = $1;
268 $next = next_line($IN);
270 if ($next =~ m/author +(.+) +<([^>]+)> +(\d+) +[+-](\d+)$/) {
271 $commit{AuthorName} = $1;
272 $commit{AuthorEmail} = $2;
273 $commit{AuthorWhen} = $3;
274 $commit{AuthorTZ} = $4;
275 $next = next_line($IN);
277 unless ($next =~ m/committer +(.+) +<([^>]+)> +(\d+) +[+-](\d+)$/) {
278 die "missing committer: $_";
281 $commit{CommitterName} = $1;
282 $commit{CommitterEmail} = $2;
283 $commit{CommitterWhen} = $3;
284 $commit{CommitterTZ} = $4;
286 $next = next_line($IN);
287 my $log = read_data($IN, $next);
288 $log =~ s/\s+$//;
290 $next = next_line($IN);
291 if ($next =~ m/from (.*)/) {
292 $commit{From} = $1;
293 $next = next_line($IN);
295 if ($next =~ m/merge (.*)/) {
296 $commit{Merge} = $1;
297 $next = next_line($IN);
300 my $date =
301 strftime("%Y-%m-%dT%H:%M:%S.000000Z",
302 gmtime($commit{CommitterWhen}));
304 my $author = "git2svn-dump";
305 if ($commit{CommitterEmail} =~ m/([^@]+)/) {
306 $author = $1;
308 unless ($ignore_author) {
309 if ($commit{AuthorEmail} =~ m/([^@]+)/) {
310 $author = $1;
314 my $props = "";
315 $props .= prop("svn:author", $author);
316 $props .= prop("svn:log", $log);
317 $props .= prop("svn:date", $date);
318 $props .= "PROPS-END";
320 # push out svn info
322 printf OUT "Revision-number: $revision\n"; $revision++;
323 printf OUT "Prop-content-length: ". length($props) . "\n";
324 printf OUT "Content-length: " . length($props) . "\n";
325 printf OUT "\n";
326 print OUT "$props\n";
328 while (1) {
329 if ($next =~ m/M (\d+) (\S+) (.*)$/) {
330 my ($mode, $dataref, $path) = (oct $1, $2, $3);
331 my $content;
332 if ($dataref eq "inline") {
333 $next = next_line($IN);
334 $content = read_data($IN, $next);
335 } else {
336 if (defined $blob{$dataref}) {
337 $content = $blob{$dataref};
338 } else {
339 # Submodules cannot be converted
340 print STDERR "Ignored line, please check if this is a submodule: $next\n" if ($verbose);
341 $next = next_line($IN);
342 next;
344 # here we really want to delete $blob{$dataref},
345 # but it might be referenced in the future. To
346 # avoid keepig everything in memory for larger
347 # repositories this must be written out to disk
348 # and removed when done.
351 $path = "$basedir/$path";
352 checkdirs($path);
354 my $action = "add";
356 my $type = $mode & 0777000;
358 if ($paths{$path}) {
359 if (($paths{$path} != 2) && ($type != 0120000)) {
360 die "file was a dir";
361 } elsif (($paths{$path} != 2) && ($type == 0120000)) {
362 print STDERR "Dir is now a symlink, deleting: $path\n" if ($verbose);
363 foreach ( keys( %paths ) ) {
364 delete $paths{$_} if ( /^$path\// );
366 # This is now a file and not a path anymore
367 $paths{$path} = 2;
368 printf OUT "Node-path: $path\nNode-action: delete\n\n";
369 } else {
370 $action = "change";
372 } else {
373 $paths{$path} = 2;
377 my $kind = "";
378 $kind = "file" if ($type == 0100000);
379 $kind = "symlink" if ($type == 0120000);
380 die "$type unknown" if ($kind eq "");
382 $props = "";
383 $props .= prop("svn:executable", "on") if ($mode & 0111);
384 $props .= prop("svn:special", "*") if ($kind eq "symlink");
385 $props .= auto_props($path);
386 $props .= "PROPS-END\n" if ($props ne "");
388 $content = "link $content\n" if ($kind eq "symlink");
390 my $plen = length($props);
391 my $clen = length($content);
393 printf OUT "Node-path: $path\n";
394 printf OUT "Node-kind: file\n";
395 printf OUT "Node-action: $action\n";
396 printf OUT "Text-content-length: $clen\n";
397 printf OUT "Content-length: " . ($clen + $plen) . "\n";
398 printf OUT "Prop-content-length: $plen\n" if ($plen);
399 printf OUT "\n";
401 print OUT "$props" if ($plen);
403 print OUT $content;
404 printf OUT "\n";
405 } elsif ($next =~ m/D (.*)/) {
406 my $path = $basedir . "/". $1;
408 if ($paths{$path}) {
409 delete $paths{$path};
410 $path .= "/";
411 foreach ( keys( %paths ) ) {
412 delete $paths{$_} if ( isPrefix($path, $_) );
415 printf OUT "Node-path: $path\n";
416 printf OUT "Node-action: delete\n";
417 printf OUT "\n";
418 } elsif ($verbose) {
419 print STDERR "deleting non existing object: $path\n";
422 } elsif ($next =~ m/^C (.*)/) {
423 die "file copy ?";
424 } elsif ($next =~ m/^R (.*)/) {
425 die "file rename ?";
426 } elsif ($next =~ m/^filedeleteall$/) {
427 die "file delete all ?";
428 } else {
429 next COMMAND;
431 $next = next_line($IN);
434 } elsif ($next =~ /^tag .*/) {
435 } elsif ($next =~ /^reset .*/) {
436 } elsif ($next =~ /^blob/) {
437 my $mark = undef;
438 $next = next_line($IN);
439 if ($next =~ m/mark (.*)/) {
440 $mark = $1;
441 $next = next_line($IN);
443 my $data = read_data($IN, $next);
444 $blob{$mark} = $data if (defined $mark);
445 } elsif ($next =~ /^checkpoint .*/) {
446 } elsif ($next =~ /^progress (.*)/) {
447 print STDERR "progress: $1\n" if ($verbose);
448 } else {
449 die "unknown command $next";
451 $next = next_line($IN);
454 close IN;
455 close OUT;
457 print STDERR "...dumped to revision $revision\n" if ($verbose);
458 print STDERR "(re-)setting sync-tag to new master\n" if ($verbose);
460 unless ($no_load) {
461 system("cd $gittree && ".
462 "git tag -m \"sync $(date)\" -a -f ${syncname} ${masterrev}");
465 print STDERR "loading dump into svn\n" if ($verbose);
467 unless ($no_load) {
468 system("svnadmin load $svntree < $svndump >>$log 2>&1") == 0 or
469 die "svnadmin load";
472 unlink $svndump, $gitdump, $log unless ($keeplogs);
474 exit 0;
477 __END__
479 =head1 NAME
481 B<git2svn> - converts a git branch to a svn ditto
483 =head1 SYNOPSIS
485 B<git2svn> [options] git-repro svn-repro
487 =head1 OPTIONS
489 =over 8
491 =item B<--git-branch>
493 The git branch to export. The default is branch is master.
495 =item B<--svn-prefix>
497 The svn prefix where the branch is import. The default is trunk to
498 match the default GIT branch (master).
500 =item B<--no-load>
502 Don't load the svn repository or update the syncpoint tagname.
504 =item B<--ignore-author>
506 Ignore "author" lines in the fast-import stream. Use "committer"
507 information instead.
509 =item B<--keep-logs>
511 Don't delete the logs in $CWD/.data on success.
513 =item B<--verbose>
515 More verbose output, can be give more then once to increase the verbosity.
517 =item B<--help>
519 Print a brief help message and exits.
521 =back
523 =head1 DESCRIPTION
525 B<git2svn> will convert a git branch to a svn ditto, it also
526 support incremantal updates.
528 B<git2svn> takes a git fast-export dump and converts it into a
529 svn dump that is feed into svnadmin load.
531 B<git2svn> assumes its the only process that writes into the svn
532 repository. This is because of the race between getting the to svn
533 Revsion number from the svn, creating the dump with correct Revsions,
534 and do the svnadmin load.
536 B<git2svn> also support incremental updates from a git branch to
537 a svn reprositry. Its does this by setting a git tag
538 (git2svn-syncpoint-<branchname>) where the last update was pulled
539 from.
541 B<git2svn> was created as a hack over a weekend to support a
542 smoother migration away from svn and allow users access to tools to
543 browse and search code (fisheye) and use anonymouns svn servers.
545 =head1 EXAMPLES
547 B<git2svn> ~/src/heimdal svn-repro
549 B<git2svn> --git-branch heimdal-1-0-branch \
550 --svn-prefix branches/heimdal-1-0-branch \
551 ~/src/heimdal svn-repro
553 =head1 DOWNLOAD
555 B<git2svn> is avaible from repo.or.cz
557 git clone git://repo.or.cz/git2svn.git
559 =head1 AUTHORS
561 Love Hörnquist Åstrand <lha@kth.se>
563 =head1 BUGS
565 Send bug reports to lha@kth.se
567 =cut