Authors and bugs reports
[git2svn.git] / git2svn
blob8cd448a27da027e424b39eb9b6405ff5f27bac3b
1 #!/usr/bin/perl
3 # git2svn, converts a git branch to a svn ditto
4 # Copyright (C) 2008 Love Hörnquist Åstrand
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 use strict;
20 use POSIX qw(strftime);
21 use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
22 use Pod::Usage;
24 my $IN;
25 my $OUT;
27 my ($help, $verbose, $no_unlink, $no_load);
29 # svn
30 my $svntree = "repro";
31 my $basedir = "trunk";
32 my $revision = 1;
34 # git
35 my $branch = "master";
36 my $gittree;
37 my $syncname;
38 my $masterrev;
39 my $fexport;
42 my %blob;
43 my %paths;
45 sub read_data
47 my ($IN, $next, $length, $data, $l) = (shift, shift);
48 unless($next =~ m/^data (\d+)/) { die "missing data: $next" ; }
49 $length = $1;
51 $l = read(IN, $data, $length);
52 unless ($l == $length) { die "failed to read data $l != $length"; }
53 $data = "" if ($length == 0);
54 return $data;
57 sub prop
59 my ($key, $value) = (shift, shift);
60 "K " . length($key) . "\n$key\nV " . length($value) . "\n$value\n";
63 sub parse_svn_tree
65 my $url = shift;
66 my ($SVN, $type, $name);
68 open(SVN, "svn ls -R $url|") or die "failed to open svn ls -R $url";
69 while (<SVN>) {
70 if (m@/(.*)/$@) {
71 $type = 1;
72 $name = "$basedir/$1";
73 } else {
74 $type = 2;
75 $name = "$basedir/$_";
77 $paths{$name} = $type;
79 close SVN;
81 open(SVN, "svn info $url|") or die "failed to open svn info $url";
82 while (<SVN>) {
83 chomp;
84 if (/^Revision: (\d+)/) {
85 $revision = $1 + 1;
86 last;
89 close SVN;
92 sub find_branch_id
94 my $name = shift;
96 foreach my $m ("heads", "remotes") {
97 my $n = "${gittree}/.git/refs/${m}/${name}";
98 my $ID;
99 next unless (-f $n);
100 open FOO, "$n";
101 my $id = <FOO>;
102 chomp($id);
103 close FOO;
104 return $id;
106 return undef;
109 sub parse_git_tree
111 $masterrev = find_branch_id($branch);
113 die "No head found for ${branch}" if ($masterrev eq "");
115 my $syncpoint="${gittree}/.git/refs/tags/${syncname}";
117 if (-f ${syncpoint}) {
118 my $oldmasterrev=`cat ${syncpoint}`;
119 chomp($oldmasterrev);
121 die "failed to get old parse name" if ($oldmasterrev eq "");
123 if (${oldmasterrev} eq ${masterrev}) {
124 print STDERR "nothing to sync\n" if ($verbose);
125 exit 0;
128 die "no $svntree, but incremental (have synctag) ?\n".
129 "(\"cd $gittree && git tag -d $syncname\" to remove)"
130 unless ( -d $svntree);
132 $fexport = "$oldmasterrev..$masterrev";
133 } else {
134 $fexport="${masterrev}";
136 system("svnadmin create ./$svntree") unless (-d $svntree);
141 sub checkdirs
143 my $path = shift;
144 my $base = "";
146 # pick first dir, create, take next dir, continue until we reached basename
147 while ($path =~ m@^([^/]+)/(.*)$@) {
148 my $first = $base . $1;
149 $path = $2;
150 $base = $first . "/";
151 next if ($paths{$first});
153 $paths{$first} = 1;
155 printf OUT "Node-path: $first\n";
156 printf OUT "Node-kind: dir\n";
157 printf OUT "Node-action: add\n";
158 printf OUT "Prop-content-length: 0\n";
159 printf OUT "Content-length: 0\n";
160 printf OUT "\n";
164 sub next_line
166 my $IN = shift;
167 my $next = <IN>;
168 chomp $next;
169 return $next;
172 $|= 1;
174 my $result;
175 $result = GetOptions ("git-branch=s" => \$branch,
176 "svn-prefix=s" => \$basedir,
177 "no-unlink" => \$no_unlink,
178 "no-load" => \$no_load,
179 "verbose+" => \$verbose,
180 "help" => \$help) or pod2usage(2);
182 pod2usage(0) if ($help);
184 die "to few arguments" if ($#ARGV < 1);
186 mkdir ".data" unless (-d ".data");
188 die "cant find branch name" unless ($branch =~ m@/?([^/]+)$@);
189 my $shortbranch = $1;
191 $syncname = "git2svn-syncpoint-${shortbranch}";
193 print STDERR "syncname tag: $syncname\n" if ($verbose);
195 my $gitdump = ".data/git.dump-${shortbranch}";
196 my $svndump = ".data/svn.dump-${shortbranch}";
197 my $log = ".data/log-${shortbranch}";
199 $gittree = $ARGV[0];
200 $svntree = $ARGV[1];
202 parse_git_tree($gittree);
204 my $cwd = `pwd`;
205 chomp($cwd);
206 parse_svn_tree("file://" . $cwd ."/". $svntree);
208 system(">$log");
210 print STDERR "git fast-export $branch ($fexport)\n" if ($verbose);
212 system("(cd $gittree && git fast-export $fexport) > $gitdump 2>$log") == 0 or
213 die "git fast-export: $!";
215 open IN, "$gitdump" or
216 die "failed to open $gitdump";
218 open OUT, ">$svndump" or
219 die "failed to open $svndump";
221 print STDERR "creating svn dump from revision $revision...\n" if ($verbose);
223 print OUT "SVN-fs-dump-format-version: 3\n";
225 my $next = next_line();
226 COMMAND: while (!eof(IN)) {
227 my $mark = undef;
228 if ($next eq "") {
229 $next = next_line($IN);
230 next COMMAND;
231 } elsif ($next =~ /^commit (.*)/) {
233 my %commit;
235 $next = next_line($IN);
236 if ($next =~ m/mark +(.*)/) {
237 $mark = $1;
238 $next = next_line($IN);
240 if ($next =~ m/author +(.*)/) {
241 $commit{author} = $1;
242 $next = next_line($IN);
244 unless ($next =~ m/committer +(.+) +<([^>]+)> +(\d+) +[+-](\d+)$/) {
245 die "missing comitter: $_";
248 $commit{CommitterName} = $1;
249 $commit{CommitterEmail} = $2;
250 $commit{CommitterWhen} = $3;
251 $commit{CommitterTZ} = $4;
253 $next = next_line($IN);
254 my $log = read_data($IN, $next);
256 $next = next_line($IN);
257 if ($next =~ m/from (.*)/) {
258 $next = next_line($IN);
260 if ($next =~ m/merge (.*)/) {
261 $next = next_line($IN);
264 my $date =
265 strftime("%Y-%m-%dT%H:%M:%S.000000Z",
266 gmtime($commit{CommitterWhen}));
268 my $author = "(no author)";
269 if ($commit{CommitterEmail} =~ m/([^@]+)/) {
270 $author = $1;
272 $author = "git2svn-dump" if ($author eq "(no author)");
274 my $props = "";
275 $props .= prop("svn:author", $author);
276 $props .= prop("svn:log", $log);
277 $props .= prop("svn:date", $date);
278 $props .= "PROPS-END";
280 # push out svn info
282 printf OUT "Revision-number: $revision\n"; $revision++;
283 printf OUT "Prop-content-length: ". length($props) . "\n";
284 printf OUT "Content-length: " . length($props) . "\n";
285 printf OUT "\n";
286 print OUT "$props\n";
288 while (1) {
289 if ($next =~ m/M (\d+) (\S+) (.*)$/) {
290 my ($mode, $dataref, $path) = (oct $1, $2, $3);
291 my $content;
292 if ($dataref eq "inline") {
293 $next = next_line($IN);
294 $content = read_data($IN, $next);
295 } else {
296 die "Revision missing content ref $dataref"
297 unless(defined $blob{$dataref});
299 $content = $blob{$dataref};
300 # here we really want to delete $blob{$dataref},
301 # but it might be referenced in the future. To
302 # avoid keepig everything in memory for larger
303 # repositories this must be written out to disk
304 # and removed when done.
307 $path = "$basedir/$path";
308 checkdirs($path);
310 my $action = "add";
312 if ($paths{$path}) {
313 die "file was a dir" if ($paths{$path} != 2);
314 $action = "change";
315 } else {
316 $paths{$path} = 2;
320 my $type = $mode & 0777000;
321 my $kind = "";
322 $kind = "file" if ($type == 0100000);
323 $kind = "symlink" if ($type == 0120000);
324 die "$type unknown" if ($kind eq "");
326 $props = "";
327 $props .= prop("svn:executable", "on") if ($mode & 0111);
328 $props .= "PROPS-END" if ($props ne "");
330 my $plen = length($props);
331 my $clen = length($content);
333 printf OUT "Node-path: $path\n";
334 printf OUT "Node-kind: $kind\n";
335 printf OUT "Node-action: $action\n";
336 printf OUT "Text-content-length: $clen\n";
337 printf OUT "Content-length: " . ($clen + $plen) . "\n";
338 printf OUT "Prop-content-length: $plen\n" if ($plen);
339 printf OUT "\n";
341 print OUT "$props\n" if ($plen);
343 print OUT $content;
344 printf OUT "\n";
345 } elsif ($next =~ m/D (.*)/) {
346 my $path = $basedir . "/". $1;
348 if ($paths{$path}) {
349 delete $paths{$path};
351 printf OUT "Node-path: $path\n";
352 printf OUT "Node-action: delete\n";
353 printf OUT "\n";
354 } elsif ($verbose) {
355 print STDERR "deleting non existing object: $path\n";
358 } elsif ($next =~ m/^C (.*)/) {
359 die "file copy ?";
360 } elsif ($next =~ m/^R (.*)/) {
361 die "file rename ?";
362 } elsif ($next =~ m/^filedeleteall$/) {
363 die "file delete all ?";
364 } else {
365 next COMMAND;
367 $next = next_line($IN);
370 } elsif ($next =~ /^tag .*/) {
371 } elsif ($next =~ /^reset .*/) {
372 } elsif ($next =~ /^blob/) {
373 $next = next_line($IN);
374 if ($next =~ m/mark (.*)/) {
375 $mark = $1;
376 $next = next_line($IN);
378 my $data = read_data($IN, $next);
379 $blob{$mark} = $data if (defined $mark);
380 } elsif ($next =~ /^checkpoint .*/) {
381 } elsif ($next =~ /^progress (.*)/) {
382 print STDERR "progress: $1\n" if ($verbose);
383 } else {
384 die "unknown command $next";
386 $next = next_line($IN);
389 close IN;
390 close OUT;
392 print STDERR "(re-)setting sync-tag to new master\n" if ($verbose);
394 unless ($no_load) {
395 system("cd $gittree && ".
396 "git tag -m \"sync $(date)\" -a -f ${syncname} ${masterrev}");
399 print STDERR "loading dump into svn\n" if ($verbose);
401 unless ($no_load) {
402 system("svnadmin load $svntree < $svndump >>$log 2>&1") == 0 or
403 die "svnadmin load";
406 unlink $svndump, $gitdump, $log unless ($no_unlink);
408 exit 0;
411 __END__
413 =head1 NAME
415 git2svn - converts a git branch to a svn ditto
417 =head1 SYNOPSIS
419 git2svn [options] git-repro svn-repro
421 =head1 OPTIONS
423 =over 8
425 =item B<-git-branch>
427 The git branch to export. The default is branch is master.
429 =item B<-svn-prefix>
431 The svn prefix where the branch is import. The default is trunk to
432 match the default GIT branch (master).
434 =item B<-verbose>
436 More verbose output, can be give more then once to increase the verbosity.
438 =item B<-help>
440 Print a brief help message and exits.
442 =back
444 =head1 DESCRIPTION
446 B<This program> will convert a git branch to a svn ditto, it also
447 support incremantal updates.
449 git2svn takes a git fast-export dump and converts it into a svn dump
450 that is feed into svnadmin load.
452 git2svn assumes its the only process that writes into the svn
453 repository. This is because of the race between getting the to svn
454 Revsion number from the svn, creating the dump with correct Revsions,
455 and do the svnadmin load.
457 git2svn also support incremental updates from a git branch to a svn
458 reprositry. Its does this by setting a git tag
459 (git2svn-syncpoint-<branchname>) where the last update was pulled from.
461 =head1 EXAMPLES
463 ./git2svn ~/src/heimdal svn-repro
464 ./git2svn --git-branch heimdal-1-0-branch \
465 --svn-prefix branches/heimdal-1-0-branch \
466 ~/src/heimdal svn-repro
468 =head1 DOWNLOAD
470 git2svn is avaible from repo.or.cz
472 git clone git://repo.or.cz/git2svn.git
474 =head1 AUTHORS
476 Love Hörnquist Åstrand <lha@kth.se>
478 =head1 BUGS
480 Send bug reports to lha@kth.se
482 =cut