18123a1fd87d15712c611a1f7dff2e25ff2bfe6e
[git2svn.git] / git2svn
blob18123a1fd87d15712c611a1f7dff2e25ff2bfe6e
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, $keeplogs, $no_load);
29 # svn
30 my $svntree = "repro";
31 my $basedir = "trunk";
32 my $revision = 1;
34 # git
35 my $branch = "master";
36 my $syncname;
37 my $masterrev;
38 my $fexport;
40 my %blob;
41 my %paths;
43 sub read_data
45 my ($IN, $next, $length, $data, $l) = (shift, shift);
46 unless($next =~ m/^data (\d+)/) { die "missing data: $next" ; }
47 $length = $1;
49 $l = read(IN, $data, $length);
50 unless ($l == $length) { die "failed to read data $l != $length"; }
51 $data = "" if ($length == 0);
52 return $data;
55 sub prop
57 my ($key, $value) = (shift, shift);
58 "K " . length($key) . "\n$key\nV " . length($value) . "\n$value\n";
61 sub parse_svn_tree
63 my $url = shift;
64 my ($SVN, $type, $name);
66 open(SVN, "svn ls -R $url|") or die "failed to open svn ls -R $url";
67 while (<SVN>) {
68 chomp;
69 if (m@/?(.*)/$@) {
70 $type = 1;
71 $name = "$1";
72 } else {
73 $type = 2;
74 $name = "$_";
76 $paths{$name} = $type;
78 close SVN;
80 open(SVN, "svn info $url|") or die "failed to open svn info $url";
81 while (<SVN>) {
82 chomp;
83 if ($revision eq 1 && /^Revision: (\d+)/) {
84 $revision = $1 + 1;
87 close SVN;
90 sub find_branch_id
92 my ($gittree, $name) = (shift, shift);
94 my $GIT;
96 open FOO, "cd $gittree ".
97 "&& git rev-parse $name 2>/dev/null|" or
98 die "git rev-parse $name failed";
100 my $id = <FOO>;
101 close GIT;
102 chomp($id);
103 $id = "" if ($id eq $name);
104 return $id;
107 sub parse_git_tree
109 my ($gittree, $branch, $shortbranch) = (shift, shift, shift);
111 $syncname = "git2svn-syncpoint-${shortbranch}";
112 print STDERR "syncname tag: $syncname\n" if ($verbose);
114 $masterrev = find_branch_id($gittree, $branch);
115 die "No head found for ${branch}" if ($masterrev eq "");
117 my $oldmasterrev = find_branch_id($gittree, $syncname);
119 if ($oldmasterrev ne "") {
121 die "no $svntree, but incremental (have synctag) ?\n".
122 "(\"cd $gittree && git tag -d $syncname\" to remove)"
123 unless ( -d $svntree);
125 if (${oldmasterrev} eq $masterrev) {
126 print STDERR "nothing to sync, $syncname matches $branch\n"
127 if ($verbose);
128 exit 0;
131 $fexport = "$oldmasterrev..$masterrev";
132 } else {
133 $fexport="$masterrev";
135 system("svnadmin create ./$svntree") unless (-d $svntree);
140 sub checkdirs
142 my $path = shift;
143 my $base = "";
145 # pick first dir, create, take next dir, continue until we reached basename
146 while ($path =~ m@^([^/]+)/(.*)$@) {
147 my $first = $base . $1;
148 $path = $2;
149 $base = $first . "/";
150 next if ($paths{$first});
152 $paths{$first} = 1;
154 printf OUT "Node-path: $first\n";
155 printf OUT "Node-kind: dir\n";
156 printf OUT "Node-action: add\n";
157 printf OUT "Prop-content-length: 0\n";
158 printf OUT "Content-length: 0\n";
159 printf OUT "\n";
163 sub next_line
165 my $IN = shift;
166 my $next = <IN>;
167 chomp $next;
168 return $next;
172 # This is to allow setting props for a path, XXX add configuration
173 # file/options for this.
176 sub auto_props
178 my $path = shift;
179 ### given path, return prop("prop", "value")
180 return "";
184 $|= 1;
186 my $result;
187 $result = GetOptions ("git-branch=s" => \$branch,
188 "svn-prefix=s" => \$basedir,
189 "keep-logs" => \$keeplogs,
190 "no-load" => \$no_load,
191 "verbose+" => \$verbose,
192 "help" => \$help) or pod2usage(2);
194 pod2usage(0) if ($help);
196 die "to few arguments" if ($#ARGV < 1);
198 mkdir ".data" unless (-d ".data");
200 die "cant find branch name" unless ($branch =~ m@/?([^/]+)$@);
201 my $shortbranch = $1;
203 my $gitdump = ".data/git.dump-${shortbranch}";
204 my $svndump = ".data/svn.dump-${shortbranch}";
205 my $log = ".data/log-${shortbranch}";
207 my $gittree = $ARGV[0];
208 $svntree = $ARGV[1];
210 parse_git_tree($gittree, $branch, $shortbranch);
212 my $cwd = `pwd`;
213 chomp($cwd);
214 parse_svn_tree("file://" . $cwd ."/". $svntree);
216 system(">$log");
218 print STDERR "git fast-export $branch ($fexport)\n" if ($verbose);
220 system("(cd $gittree && git fast-export $fexport) > $gitdump 2>$log") == 0 or
221 die "git fast-export: $!";
223 open IN, "$gitdump" or
224 die "failed to open $gitdump";
226 open OUT, ">$svndump" or
227 die "failed to open $svndump";
229 print STDERR "creating svn dump from revision $revision...\n" if ($verbose);
231 print OUT "SVN-fs-dump-format-version: 3\n";
233 my $next = next_line();
234 COMMAND: while (!eof(IN)) {
235 if ($next eq "") {
236 $next = next_line($IN);
237 next COMMAND;
238 } elsif ($next =~ /^commit (.*)/) {
240 my %commit;
242 $next = next_line($IN);
243 if ($next =~ m/mark +(.*)/) {
244 $commit{Mark} = $1;
245 $next = next_line($IN);
247 if ($next =~ m/author +(.*)/) {
248 $commit{Author} = $1;
249 $next = next_line($IN);
251 unless ($next =~ m/committer +(.+) +<([^>]+)> +(\d+) +[+-](\d+)$/) {
252 die "missing comitter: $_";
255 $commit{CommitterName} = $1;
256 $commit{CommitterEmail} = $2;
257 $commit{CommitterWhen} = $3;
258 $commit{CommitterTZ} = $4;
260 $next = next_line($IN);
261 my $log = read_data($IN, $next);
263 $next = next_line($IN);
264 if ($next =~ m/from (.*)/) {
265 $commit{From} = $1;
266 $next = next_line($IN);
268 if ($next =~ m/merge (.*)/) {
269 $commit{Merge} = $1;
270 $next = next_line($IN);
273 my $date =
274 strftime("%Y-%m-%dT%H:%M:%S.000000Z",
275 gmtime($commit{CommitterWhen}));
277 my $author = "(no author)";
278 if ($commit{CommitterEmail} =~ m/([^@]+)/) {
279 $author = $1;
281 $author = "git2svn-dump" if ($author eq "(no author)");
283 my $props = "";
284 $props .= prop("svn:author", $author);
285 $props .= prop("svn:log", $log);
286 $props .= prop("svn:date", $date);
287 $props .= "PROPS-END";
289 # push out svn info
291 printf OUT "Revision-number: $revision\n"; $revision++;
292 printf OUT "Prop-content-length: ". length($props) . "\n";
293 printf OUT "Content-length: " . length($props) . "\n";
294 printf OUT "\n";
295 print OUT "$props\n";
297 while (1) {
298 if ($next =~ m/M (\d+) (\S+) (.*)$/) {
299 my ($mode, $dataref, $path) = (oct $1, $2, $3);
300 my $content;
301 if ($dataref eq "inline") {
302 $next = next_line($IN);
303 $content = read_data($IN, $next);
304 } else {
305 die "Revision missing content ref $dataref"
306 unless(defined $blob{$dataref});
308 $content = $blob{$dataref};
309 # here we really want to delete $blob{$dataref},
310 # but it might be referenced in the future. To
311 # avoid keepig everything in memory for larger
312 # repositories this must be written out to disk
313 # and removed when done.
316 $path = "$basedir/$path";
317 checkdirs($path);
319 my $action = "add";
321 if ($paths{$path}) {
322 die "file was a dir" if ($paths{$path} != 2);
323 $action = "change";
324 } else {
325 $paths{$path} = 2;
329 my $type = $mode & 0777000;
330 my $kind = "";
331 $kind = "file" if ($type == 0100000);
332 $kind = "symlink" if ($type == 0120000);
333 die "$type unknown" if ($kind eq "");
335 $props = "";
336 $props .= prop("svn:executable", "on") if ($mode & 0111);
337 $props .= prop("svn:special", "*") if ($kind eq "symlink");
338 $props .= auto_props($path);
339 $props .= "PROPS-END" if ($props ne "");
341 $content = "link $content" if ($kind eq "symlink");
343 my $plen = length($props);
344 my $clen = length($content);
346 printf OUT "Node-path: $path\n";
347 printf OUT "Node-kind: file\n";
348 printf OUT "Node-action: $action\n";
349 printf OUT "Text-content-length: $clen\n";
350 printf OUT "Content-length: " . ($clen + $plen) . "\n";
351 printf OUT "Prop-content-length: $plen\n" if ($plen);
352 printf OUT "\n";
354 print OUT "$props\n" if ($plen);
356 print OUT $content;
357 printf OUT "\n";
358 } elsif ($next =~ m/D (.*)/) {
359 my $path = $basedir . "/". $1;
361 if ($paths{$path}) {
362 delete $paths{$path};
364 printf OUT "Node-path: $path\n";
365 printf OUT "Node-action: delete\n";
366 printf OUT "\n";
367 } elsif ($verbose) {
368 print STDERR "deleting non existing object: $path\n";
371 } elsif ($next =~ m/^C (.*)/) {
372 die "file copy ?";
373 } elsif ($next =~ m/^R (.*)/) {
374 die "file rename ?";
375 } elsif ($next =~ m/^filedeleteall$/) {
376 die "file delete all ?";
377 } else {
378 next COMMAND;
380 $next = next_line($IN);
383 } elsif ($next =~ /^tag .*/) {
384 } elsif ($next =~ /^reset .*/) {
385 } elsif ($next =~ /^blob/) {
386 my $mark = undef;
387 $next = next_line($IN);
388 if ($next =~ m/mark (.*)/) {
389 $mark = $1;
390 $next = next_line($IN);
392 my $data = read_data($IN, $next);
393 $blob{$mark} = $data if (defined $mark);
394 } elsif ($next =~ /^checkpoint .*/) {
395 } elsif ($next =~ /^progress (.*)/) {
396 print STDERR "progress: $1\n" if ($verbose);
397 } else {
398 die "unknown command $next";
400 $next = next_line($IN);
403 close IN;
404 close OUT;
406 print STDERR "...dumped to revision $revision\n" if ($verbose);
407 print STDERR "(re-)setting sync-tag to new master\n" if ($verbose);
409 unless ($no_load) {
410 system("cd $gittree && ".
411 "git tag -m \"sync $(date)\" -a -f ${syncname} ${masterrev}");
414 print STDERR "loading dump into svn\n" if ($verbose);
416 unless ($no_load) {
417 system("svnadmin load $svntree < $svndump >>$log 2>&1") == 0 or
418 die "svnadmin load";
421 unlink $svndump, $gitdump, $log unless ($keeplogs);
423 exit 0;
426 __END__
428 =head1 NAME
430 B<git2svn> - converts a git branch to a svn ditto
432 =head1 SYNOPSIS
434 B<git2svn> [options] git-repro svn-repro
436 =head1 OPTIONS
438 =over 8
440 =item B<--git-branch>
442 The git branch to export. The default is branch is master.
444 =item B<--svn-prefix>
446 The svn prefix where the branch is import. The default is trunk to
447 match the default GIT branch (master).
449 =item B<--no-load>
451 Don't load the svn repository or update the syncpoint tagname.
453 =item B<--keep-logs>
455 Don't delete the logs in $CWD/.data on success.
457 =item B<--verbose>
459 More verbose output, can be give more then once to increase the verbosity.
461 =item B<--help>
463 Print a brief help message and exits.
465 =back
467 =head1 DESCRIPTION
469 B<git2svn> will convert a git branch to a svn ditto, it also
470 support incremantal updates.
472 B<git2svn> takes a git fast-export dump and converts it into a
473 svn dump that is feed into svnadmin load.
475 B<git2svn> assumes its the only process that writes into the svn
476 repository. This is because of the race between getting the to svn
477 Revsion number from the svn, creating the dump with correct Revsions,
478 and do the svnadmin load.
480 B<git2svn> also support incremental updates from a git branch to
481 a svn reprositry. Its does this by setting a git tag
482 (git2svn-syncpoint-<branchname>) where the last update was pulled
483 from.
485 B<git2svn> was created as a hack over a weekend to support a
486 smoother migration away from svn and allow users access to tools to
487 browse and search code (fisheye) and use anonymouns svn servers.
489 =head1 EXAMPLES
491 B<git2svn> ~/src/heimdal svn-repro
493 B<git2svn> --git-branch heimdal-1-0-branch \
494 --svn-prefix branches/heimdal-1-0-branch \
495 ~/src/heimdal svn-repro
497 =head1 DOWNLOAD
499 B<git2svn> is avaible from repo.or.cz
501 git clone git://repo.or.cz/git2svn.git
503 =head1 AUTHORS
505 Love Hörnquist Åstrand <lha@kth.se>
507 =head1 BUGS
509 Send bug reports to lha@kth.se
511 =cut