Switch to a BSD 2 clause license
[git2svn.git] / git2svn
blob3a5677d717e7ceb3d3a07d59a0fda9aba93a4973
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 $gitdump = ".data/git.dump-${shortbranch}";
216 my $svndump = ".data/svn.dump-${shortbranch}";
217 my $log = ".data/log-${shortbranch}";
219 my $gittree = $ARGV[0];
220 $svntree = $ARGV[1];
222 parse_git_tree($gittree, $branch, $shortbranch);
224 my $cwd = `pwd`;
225 chomp($cwd);
226 parse_svn_tree("file://" . $cwd ."/". $svntree);
228 system(">$log");
230 print STDERR "git fast-export $branch ($fexport)\n" if ($verbose);
232 system("(cd $gittree && git fast-export $fexport) > $gitdump 2>$log") == 0 or
233 die "git fast-export: $!";
235 open IN, "$gitdump" or
236 die "failed to open $gitdump";
238 open OUT, ">$svndump" or
239 die "failed to open $svndump";
241 print STDERR "creating svn dump from revision $revision...\n" if ($verbose);
243 print OUT "SVN-fs-dump-format-version: 3\n";
245 my $next = next_line();
246 COMMAND: while (!eof(IN)) {
247 if ($next eq "") {
248 $next = next_line($IN);
249 next COMMAND;
250 } elsif ($next =~ /^commit (.*)/) {
252 my %commit;
254 $next = next_line($IN);
255 if ($next =~ m/mark +(.*)/) {
256 $commit{Mark} = $1;
257 $next = next_line($IN);
259 if ($next =~ m/author +(.*)/) {
260 $commit{Author} = $1;
261 $next = next_line($IN);
263 unless ($next =~ m/committer +(.+) +<([^>]+)> +(\d+) +[+-](\d+)$/) {
264 die "missing comitter: $_";
267 $commit{CommitterName} = $1;
268 $commit{CommitterEmail} = $2;
269 $commit{CommitterWhen} = $3;
270 $commit{CommitterTZ} = $4;
272 $next = next_line($IN);
273 my $log = read_data($IN, $next);
275 $next = next_line($IN);
276 if ($next =~ m/from (.*)/) {
277 $commit{From} = $1;
278 $next = next_line($IN);
280 if ($next =~ m/merge (.*)/) {
281 $commit{Merge} = $1;
282 $next = next_line($IN);
285 my $date =
286 strftime("%Y-%m-%dT%H:%M:%S.000000Z",
287 gmtime($commit{CommitterWhen}));
289 my $author = "(no author)";
290 if ($commit{CommitterEmail} =~ m/([^@]+)/) {
291 $author = $1;
293 $author = "git2svn-dump" if ($author eq "(no author)");
295 my $props = "";
296 $props .= prop("svn:author", $author);
297 $props .= prop("svn:log", $log);
298 $props .= prop("svn:date", $date);
299 $props .= "PROPS-END";
301 # push out svn info
303 printf OUT "Revision-number: $revision\n"; $revision++;
304 printf OUT "Prop-content-length: ". length($props) . "\n";
305 printf OUT "Content-length: " . length($props) . "\n";
306 printf OUT "\n";
307 print OUT "$props\n";
309 while (1) {
310 if ($next =~ m/M (\d+) (\S+) (.*)$/) {
311 my ($mode, $dataref, $path) = (oct $1, $2, $3);
312 my $content;
313 if ($dataref eq "inline") {
314 $next = next_line($IN);
315 $content = read_data($IN, $next);
316 } else {
317 die "Revision missing content ref $dataref"
318 unless(defined $blob{$dataref});
320 $content = $blob{$dataref};
321 # here we really want to delete $blob{$dataref},
322 # but it might be referenced in the future. To
323 # avoid keepig everything in memory for larger
324 # repositories this must be written out to disk
325 # and removed when done.
328 $path = "$basedir/$path";
329 checkdirs($path);
331 my $action = "add";
333 if ($paths{$path}) {
334 die "file was a dir" if ($paths{$path} != 2);
335 $action = "change";
336 } else {
337 $paths{$path} = 2;
341 my $type = $mode & 0777000;
342 my $kind = "";
343 $kind = "file" if ($type == 0100000);
344 $kind = "symlink" if ($type == 0120000);
345 die "$type unknown" if ($kind eq "");
347 $props = "";
348 $props .= prop("svn:executable", "on") if ($mode & 0111);
349 $props .= prop("svn:special", "*") if ($kind eq "symlink");
350 $props .= auto_props($path);
351 $props .= "PROPS-END" if ($props ne "");
353 $content = "link $content" if ($kind eq "symlink");
355 my $plen = length($props);
356 my $clen = length($content);
358 printf OUT "Node-path: $path\n";
359 printf OUT "Node-kind: file\n";
360 printf OUT "Node-action: $action\n";
361 printf OUT "Text-content-length: $clen\n";
362 printf OUT "Content-length: " . ($clen + $plen) . "\n";
363 printf OUT "Prop-content-length: $plen\n" if ($plen);
364 printf OUT "\n";
366 print OUT "$props\n" if ($plen);
368 print OUT $content;
369 printf OUT "\n";
370 } elsif ($next =~ m/D (.*)/) {
371 my $path = $basedir . "/". $1;
373 if ($paths{$path}) {
374 delete $paths{$path};
376 printf OUT "Node-path: $path\n";
377 printf OUT "Node-action: delete\n";
378 printf OUT "\n";
379 } elsif ($verbose) {
380 print STDERR "deleting non existing object: $path\n";
383 } elsif ($next =~ m/^C (.*)/) {
384 die "file copy ?";
385 } elsif ($next =~ m/^R (.*)/) {
386 die "file rename ?";
387 } elsif ($next =~ m/^filedeleteall$/) {
388 die "file delete all ?";
389 } else {
390 next COMMAND;
392 $next = next_line($IN);
395 } elsif ($next =~ /^tag .*/) {
396 } elsif ($next =~ /^reset .*/) {
397 } elsif ($next =~ /^blob/) {
398 my $mark = undef;
399 $next = next_line($IN);
400 if ($next =~ m/mark (.*)/) {
401 $mark = $1;
402 $next = next_line($IN);
404 my $data = read_data($IN, $next);
405 $blob{$mark} = $data if (defined $mark);
406 } elsif ($next =~ /^checkpoint .*/) {
407 } elsif ($next =~ /^progress (.*)/) {
408 print STDERR "progress: $1\n" if ($verbose);
409 } else {
410 die "unknown command $next";
412 $next = next_line($IN);
415 close IN;
416 close OUT;
418 print STDERR "...dumped to revision $revision\n" if ($verbose);
419 print STDERR "(re-)setting sync-tag to new master\n" if ($verbose);
421 unless ($no_load) {
422 system("cd $gittree && ".
423 "git tag -m \"sync $(date)\" -a -f ${syncname} ${masterrev}");
426 print STDERR "loading dump into svn\n" if ($verbose);
428 unless ($no_load) {
429 system("svnadmin load $svntree < $svndump >>$log 2>&1") == 0 or
430 die "svnadmin load";
433 unlink $svndump, $gitdump, $log unless ($keeplogs);
435 exit 0;
438 __END__
440 =head1 NAME
442 B<git2svn> - converts a git branch to a svn ditto
444 =head1 SYNOPSIS
446 B<git2svn> [options] git-repro svn-repro
448 =head1 OPTIONS
450 =over 8
452 =item B<--git-branch>
454 The git branch to export. The default is branch is master.
456 =item B<--svn-prefix>
458 The svn prefix where the branch is import. The default is trunk to
459 match the default GIT branch (master).
461 =item B<--no-load>
463 Don't load the svn repository or update the syncpoint tagname.
465 =item B<--keep-logs>
467 Don't delete the logs in $CWD/.data on success.
469 =item B<--verbose>
471 More verbose output, can be give more then once to increase the verbosity.
473 =item B<--help>
475 Print a brief help message and exits.
477 =back
479 =head1 DESCRIPTION
481 B<git2svn> will convert a git branch to a svn ditto, it also
482 support incremantal updates.
484 B<git2svn> takes a git fast-export dump and converts it into a
485 svn dump that is feed into svnadmin load.
487 B<git2svn> assumes its the only process that writes into the svn
488 repository. This is because of the race between getting the to svn
489 Revsion number from the svn, creating the dump with correct Revsions,
490 and do the svnadmin load.
492 B<git2svn> also support incremental updates from a git branch to
493 a svn reprositry. Its does this by setting a git tag
494 (git2svn-syncpoint-<branchname>) where the last update was pulled
495 from.
497 B<git2svn> was created as a hack over a weekend to support a
498 smoother migration away from svn and allow users access to tools to
499 browse and search code (fisheye) and use anonymouns svn servers.
501 =head1 EXAMPLES
503 B<git2svn> ~/src/heimdal svn-repro
505 B<git2svn> --git-branch heimdal-1-0-branch \
506 --svn-prefix branches/heimdal-1-0-branch \
507 ~/src/heimdal svn-repro
509 =head1 DOWNLOAD
511 B<git2svn> is avaible from repo.or.cz
513 git clone git://repo.or.cz/git2svn.git
515 =head1 AUTHORS
517 Love Hörnquist Åstrand <lha@kth.se>
519 =head1 BUGS
521 Send bug reports to lha@kth.se
523 =cut