91daeeca69c6706dc56f2aa2831ca6a901a7c605
[git2svn.git] / git2svn
blob91daeeca69c6706dc56f2aa2831ca6a901a7c605
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: (\d+)/) {
84 $revision = $1 + 1;
85 last;
88 close SVN;
91 sub find_branch_id
93 my ($gittree, $name) = (shift, shift);
95 my $GIT;
97 open FOO, "cd $gittree ".
98 "&& git rev-parse $name 2>/dev/null|" or
99 die "git rev-parse $name failed";
101 my $id = <FOO>;
102 close GIT;
103 chomp($id);
104 $id = "" if ($id eq $name);
105 return $id;
108 sub parse_git_tree
110 my ($gittree, $branch, $shortbranch) = (shift, shift, shift);
112 $syncname = "git2svn-syncpoint-${shortbranch}";
113 print STDERR "syncname tag: $syncname\n" if ($verbose);
115 $masterrev = find_branch_id($gittree, $branch);
116 die "No head found for ${branch}" if ($masterrev eq "");
118 my $oldmasterrev = find_branch_id($gittree, $syncname);
120 if ($oldmasterrev ne "") {
122 die "no $svntree, but incremental (have synctag) ?\n".
123 "(\"cd $gittree && git tag -d $syncname\" to remove)"
124 unless ( -d $svntree);
126 if (${oldmasterrev} eq $masterrev) {
127 print STDERR "nothing to sync, $syncname matches $branch\n"
128 if ($verbose);
129 exit 0;
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 "keep-logs" => \$keeplogs,
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 my $gitdump = ".data/git.dump-${shortbranch}";
192 my $svndump = ".data/svn.dump-${shortbranch}";
193 my $log = ".data/log-${shortbranch}";
195 my $gittree = $ARGV[0];
196 $svntree = $ARGV[1];
198 parse_git_tree($gittree, $branch, $shortbranch);
200 my $cwd = `pwd`;
201 chomp($cwd);
202 parse_svn_tree("file://" . $cwd ."/". $svntree);
204 system(">$log");
206 print STDERR "git fast-export $branch ($fexport)\n" if ($verbose);
208 system("(cd $gittree && git fast-export $fexport) > $gitdump 2>$log") == 0 or
209 die "git fast-export: $!";
211 open IN, "$gitdump" or
212 die "failed to open $gitdump";
214 open OUT, ">$svndump" or
215 die "failed to open $svndump";
217 print STDERR "creating svn dump from revision $revision...\n" if ($verbose);
219 print OUT "SVN-fs-dump-format-version: 3\n";
221 my $next = next_line();
222 COMMAND: while (!eof(IN)) {
223 if ($next eq "") {
224 $next = next_line($IN);
225 next COMMAND;
226 } elsif ($next =~ /^commit (.*)/) {
228 my %commit;
230 $next = next_line($IN);
231 if ($next =~ m/mark +(.*)/) {
232 $commit{Mark} = $1;
233 $next = next_line($IN);
235 if ($next =~ m/author +(.*)/) {
236 $commit{Author} = $1;
237 $next = next_line($IN);
239 unless ($next =~ m/committer +(.+) +<([^>]+)> +(\d+) +[+-](\d+)$/) {
240 die "missing comitter: $_";
243 $commit{CommitterName} = $1;
244 $commit{CommitterEmail} = $2;
245 $commit{CommitterWhen} = $3;
246 $commit{CommitterTZ} = $4;
248 $next = next_line($IN);
249 my $log = read_data($IN, $next);
251 $next = next_line($IN);
252 if ($next =~ m/from (.*)/) {
253 $commit{From} = $1;
254 $next = next_line($IN);
256 if ($next =~ m/merge (.*)/) {
257 $commit{Merge} = $1;
258 $next = next_line($IN);
261 my $date =
262 strftime("%Y-%m-%dT%H:%M:%S.000000Z",
263 gmtime($commit{CommitterWhen}));
265 my $author = "(no author)";
266 if ($commit{CommitterEmail} =~ m/([^@]+)/) {
267 $author = $1;
269 $author = "git2svn-dump" if ($author eq "(no author)");
271 my $props = "";
272 $props .= prop("svn:author", $author);
273 $props .= prop("svn:log", $log);
274 $props .= prop("svn:date", $date);
275 $props .= "PROPS-END";
277 # push out svn info
279 printf OUT "Revision-number: $revision\n"; $revision++;
280 printf OUT "Prop-content-length: ". length($props) . "\n";
281 printf OUT "Content-length: " . length($props) . "\n";
282 printf OUT "\n";
283 print OUT "$props\n";
285 while (1) {
286 if ($next =~ m/M (\d+) (\S+) (.*)$/) {
287 my ($mode, $dataref, $path) = (oct $1, $2, $3);
288 my $content;
289 if ($dataref eq "inline") {
290 $next = next_line($IN);
291 $content = read_data($IN, $next);
292 } else {
293 die "Revision missing content ref $dataref"
294 unless(defined $blob{$dataref});
296 $content = $blob{$dataref};
297 # here we really want to delete $blob{$dataref},
298 # but it might be referenced in the future. To
299 # avoid keepig everything in memory for larger
300 # repositories this must be written out to disk
301 # and removed when done.
304 $path = "$basedir/$path";
305 checkdirs($path);
307 my $action = "add";
309 if ($paths{$path}) {
310 die "file was a dir" if ($paths{$path} != 2);
311 $action = "change";
312 } else {
313 $paths{$path} = 2;
317 my $type = $mode & 0777000;
318 my $kind = "";
319 $kind = "file" if ($type == 0100000);
320 $kind = "symlink" if ($type == 0120000);
321 die "$type unknown" if ($kind eq "");
323 $props = "";
324 $props .= prop("svn:executable", "on") if ($mode & 0111);
325 $props .= prop("svn:special", "*") if ($kind eq "symlink");
326 $props .= "PROPS-END" if ($props ne "");
328 $content = "link $content" if ($kind eq "symlink");
330 my $plen = length($props);
331 my $clen = length($content);
333 printf OUT "Node-path: $path\n";
334 printf OUT "Node-kind: file\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 my $mark = undef;
374 $next = next_line($IN);
375 if ($next =~ m/mark (.*)/) {
376 $mark = $1;
377 $next = next_line($IN);
379 my $data = read_data($IN, $next);
380 $blob{$mark} = $data if (defined $mark);
381 } elsif ($next =~ /^checkpoint .*/) {
382 } elsif ($next =~ /^progress (.*)/) {
383 print STDERR "progress: $1\n" if ($verbose);
384 } else {
385 die "unknown command $next";
387 $next = next_line($IN);
390 close IN;
391 close OUT;
393 print STDERR "...dumped to revision $revision\n" if ($verbose);
394 print STDERR "(re-)setting sync-tag to new master\n" if ($verbose);
396 unless ($no_load) {
397 system("cd $gittree && ".
398 "git tag -m \"sync $(date)\" -a -f ${syncname} ${masterrev}");
401 print STDERR "loading dump into svn\n" if ($verbose);
403 unless ($no_load) {
404 system("svnadmin load $svntree < $svndump >>$log 2>&1") == 0 or
405 die "svnadmin load";
408 unlink $svndump, $gitdump, $log unless ($keeplogs);
410 exit 0;
413 __END__
415 =head1 NAME
417 B<git2svn> - converts a git branch to a svn ditto
419 =head1 SYNOPSIS
421 B<git2svn> [options] git-repro svn-repro
423 =head1 OPTIONS
425 =over 8
427 =item B<--git-branch>
429 The git branch to export. The default is branch is master.
431 =item B<--svn-prefix>
433 The svn prefix where the branch is import. The default is trunk to
434 match the default GIT branch (master).
436 =item B<--no-load>
438 Don't load the svn repository or update the syncpoint tagname.
440 =item B<--keep-logs>
442 Don't delete the logs in $CWD/.data on success.
444 =item B<--verbose>
446 More verbose output, can be give more then once to increase the verbosity.
448 =item B<--help>
450 Print a brief help message and exits.
452 =back
454 =head1 DESCRIPTION
456 B<git2svn> will convert a git branch to a svn ditto, it also
457 support incremantal updates.
459 B<git2svn> takes a git fast-export dump and converts it into a
460 svn dump that is feed into svnadmin load.
462 B<git2svn> assumes its the only process that writes into the svn
463 repository. This is because of the race between getting the to svn
464 Revsion number from the svn, creating the dump with correct Revsions,
465 and do the svnadmin load.
467 B<git2svn> also support incremental updates from a git branch to
468 a svn reprositry. Its does this by setting a git tag
469 (git2svn-syncpoint-<branchname>) where the last update was pulled
470 from.
472 B<git2svn> was created as a hack over a weekend to support a
473 smoother migration away from svn and allow users access to tools to
474 browse and search code (fisheye) and use anonymouns svn servers.
476 =head1 EXAMPLES
478 B<git2svn> ~/src/heimdal svn-repro
480 B<git2svn> --git-branch heimdal-1-0-branch \
481 --svn-prefix branches/heimdal-1-0-branch \
482 ~/src/heimdal svn-repro
484 =head1 DOWNLOAD
486 B<git2svn> is avaible from repo.or.cz
488 git clone git://repo.or.cz/git2svn.git
490 =head1 AUTHORS
492 Love Hörnquist Åstrand <lha@kth.se>
494 =head1 BUGS
496 Send bug reports to lha@kth.se
498 =cut