Handle symlinks, they are magic in svn dump files (and undocumented).
[git2svn.git] / git2svn
bloba679b6d55a6f195d45efa3325f84ae86b75a493d
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 .= prop("svn:special", "*") if ($kind eq "symlink");
329 $props .= "PROPS-END" if ($props ne "");
331 $content = "link $content" if ($kind eq "symlink");
333 my $plen = length($props);
334 my $clen = length($content);
336 printf OUT "Node-path: $path\n";
337 printf OUT "Node-kind: file\n";
338 printf OUT "Node-action: $action\n";
339 printf OUT "Text-content-length: $clen\n";
340 printf OUT "Content-length: " . ($clen + $plen) . "\n";
341 printf OUT "Prop-content-length: $plen\n" if ($plen);
342 printf OUT "\n";
344 print OUT "$props\n" if ($plen);
346 print OUT $content;
347 printf OUT "\n";
348 } elsif ($next =~ m/D (.*)/) {
349 my $path = $basedir . "/". $1;
351 if ($paths{$path}) {
352 delete $paths{$path};
354 printf OUT "Node-path: $path\n";
355 printf OUT "Node-action: delete\n";
356 printf OUT "\n";
357 } elsif ($verbose) {
358 print STDERR "deleting non existing object: $path\n";
361 } elsif ($next =~ m/^C (.*)/) {
362 die "file copy ?";
363 } elsif ($next =~ m/^R (.*)/) {
364 die "file rename ?";
365 } elsif ($next =~ m/^filedeleteall$/) {
366 die "file delete all ?";
367 } else {
368 next COMMAND;
370 $next = next_line($IN);
373 } elsif ($next =~ /^tag .*/) {
374 } elsif ($next =~ /^reset .*/) {
375 } elsif ($next =~ /^blob/) {
376 $next = next_line($IN);
377 if ($next =~ m/mark (.*)/) {
378 $mark = $1;
379 $next = next_line($IN);
381 my $data = read_data($IN, $next);
382 $blob{$mark} = $data if (defined $mark);
383 } elsif ($next =~ /^checkpoint .*/) {
384 } elsif ($next =~ /^progress (.*)/) {
385 print STDERR "progress: $1\n" if ($verbose);
386 } else {
387 die "unknown command $next";
389 $next = next_line($IN);
392 close IN;
393 close OUT;
395 print STDERR "(re-)setting sync-tag to new master\n" if ($verbose);
397 unless ($no_load) {
398 system("cd $gittree && ".
399 "git tag -m \"sync $(date)\" -a -f ${syncname} ${masterrev}");
402 print STDERR "loading dump into svn\n" if ($verbose);
404 unless ($no_load) {
405 system("svnadmin load $svntree < $svndump >>$log 2>&1") == 0 or
406 die "svnadmin load";
409 unlink $svndump, $gitdump, $log unless ($no_unlink);
411 exit 0;
414 __END__
416 =head1 NAME
418 git2svn - converts a git branch to a svn ditto
420 =head1 SYNOPSIS
422 git2svn [options] git-repro svn-repro
424 =head1 OPTIONS
426 =over 8
428 =item B<-git-branch>
430 The git branch to export. The default is branch is master.
432 =item B<-svn-prefix>
434 The svn prefix where the branch is import. The default is trunk to
435 match the default GIT branch (master).
437 =item B<-verbose>
439 More verbose output, can be give more then once to increase the verbosity.
441 =item B<-help>
443 Print a brief help message and exits.
445 =back
447 =head1 DESCRIPTION
449 B<This program> will convert a git branch to a svn ditto, it also
450 support incremantal updates.
452 git2svn takes a git fast-export dump and converts it into a svn dump
453 that is feed into svnadmin load.
455 git2svn assumes its the only process that writes into the svn
456 repository. This is because of the race between getting the to svn
457 Revsion number from the svn, creating the dump with correct Revsions,
458 and do the svnadmin load.
460 git2svn also support incremental updates from a git branch to a svn
461 reprositry. Its does this by setting a git tag
462 (git2svn-syncpoint-<branchname>) where the last update was pulled from.
464 =head1 EXAMPLES
466 ./git2svn ~/src/heimdal svn-repro
467 ./git2svn --git-branch heimdal-1-0-branch \
468 --svn-prefix branches/heimdal-1-0-branch \
469 ~/src/heimdal svn-repro
471 =head1 DOWNLOAD
473 git2svn is avaible from repo.or.cz
475 git clone git://repo.or.cz/git2svn.git
477 =head1 AUTHORS
479 Love Hörnquist Åstrand <lha@kth.se>
481 =head1 BUGS
483 Send bug reports to lha@kth.se
485 =cut