Don't make $gittree a global.
[git2svn.git] / git2svn
bloba30e53d451796da4f9c5e598437548af7e52937b
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-list --max-count=1 $name 2>/dev/null|" or
99 die "git rev-list $name failed";
101 my $id = <FOO>;
102 close GIT;
103 chomp($id);
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 if (${oldmasterrev} eq $masterrev) {
122 print STDERR "nothing to sync, $syncname matches $branch\n"
123 if ($verbose);
124 exit 0;
127 die "no $svntree, but incremental (have synctag) ?\n".
128 "(\"cd $gittree && git tag -d $syncname\" to remove)"
129 unless ( -d $svntree);
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;
171 $|= 1;
173 my $result;
174 $result = GetOptions ("git-branch=s" => \$branch,
175 "svn-prefix=s" => \$basedir,
176 "keep-logs" => \$keeplogs,
177 "no-load" => \$no_load,
178 "verbose+" => \$verbose,
179 "help" => \$help) or pod2usage(2);
181 pod2usage(0) if ($help);
183 die "to few arguments" if ($#ARGV < 1);
185 mkdir ".data" unless (-d ".data");
187 die "cant find branch name" unless ($branch =~ m@/?([^/]+)$@);
188 my $shortbranch = $1;
190 my $gitdump = ".data/git.dump-${shortbranch}";
191 my $svndump = ".data/svn.dump-${shortbranch}";
192 my $log = ".data/log-${shortbranch}";
194 my $gittree = $ARGV[0];
195 $svntree = $ARGV[1];
197 parse_git_tree($gittree, $branch, $shortbranch);
199 my $cwd = `pwd`;
200 chomp($cwd);
201 parse_svn_tree("file://" . $cwd ."/". $svntree);
203 system(">$log");
205 print STDERR "git fast-export $branch ($fexport)\n" if ($verbose);
207 system("(cd $gittree && git fast-export $fexport) > $gitdump 2>$log") == 0 or
208 die "git fast-export: $!";
210 open IN, "$gitdump" or
211 die "failed to open $gitdump";
213 open OUT, ">$svndump" or
214 die "failed to open $svndump";
216 print STDERR "creating svn dump from revision $revision...\n" if ($verbose);
218 print OUT "SVN-fs-dump-format-version: 3\n";
220 my $next = next_line();
221 COMMAND: while (!eof(IN)) {
222 my $mark = undef;
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 $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 $next = next_line($IN);
255 if ($next =~ m/merge (.*)/) {
256 $next = next_line($IN);
259 my $date =
260 strftime("%Y-%m-%dT%H:%M:%S.000000Z",
261 gmtime($commit{CommitterWhen}));
263 my $author = "(no author)";
264 if ($commit{CommitterEmail} =~ m/([^@]+)/) {
265 $author = $1;
267 $author = "git2svn-dump" if ($author eq "(no author)");
269 my $props = "";
270 $props .= prop("svn:author", $author);
271 $props .= prop("svn:log", $log);
272 $props .= prop("svn:date", $date);
273 $props .= "PROPS-END";
275 # push out svn info
277 printf OUT "Revision-number: $revision\n"; $revision++;
278 printf OUT "Prop-content-length: ". length($props) . "\n";
279 printf OUT "Content-length: " . length($props) . "\n";
280 printf OUT "\n";
281 print OUT "$props\n";
283 while (1) {
284 if ($next =~ m/M (\d+) (\S+) (.*)$/) {
285 my ($mode, $dataref, $path) = (oct $1, $2, $3);
286 my $content;
287 if ($dataref eq "inline") {
288 $next = next_line($IN);
289 $content = read_data($IN, $next);
290 } else {
291 die "Revision missing content ref $dataref"
292 unless(defined $blob{$dataref});
294 $content = $blob{$dataref};
295 # here we really want to delete $blob{$dataref},
296 # but it might be referenced in the future. To
297 # avoid keepig everything in memory for larger
298 # repositories this must be written out to disk
299 # and removed when done.
302 $path = "$basedir/$path";
303 checkdirs($path);
305 my $action = "add";
307 if ($paths{$path}) {
308 die "file was a dir" if ($paths{$path} != 2);
309 $action = "change";
310 } else {
311 $paths{$path} = 2;
315 my $type = $mode & 0777000;
316 my $kind = "";
317 $kind = "file" if ($type == 0100000);
318 $kind = "symlink" if ($type == 0120000);
319 die "$type unknown" if ($kind eq "");
321 $props = "";
322 $props .= prop("svn:executable", "on") if ($mode & 0111);
323 $props .= prop("svn:special", "*") if ($kind eq "symlink");
324 $props .= "PROPS-END" if ($props ne "");
326 $content = "link $content" if ($kind eq "symlink");
328 my $plen = length($props);
329 my $clen = length($content);
331 printf OUT "Node-path: $path\n";
332 printf OUT "Node-kind: file\n";
333 printf OUT "Node-action: $action\n";
334 printf OUT "Text-content-length: $clen\n";
335 printf OUT "Content-length: " . ($clen + $plen) . "\n";
336 printf OUT "Prop-content-length: $plen\n" if ($plen);
337 printf OUT "\n";
339 print OUT "$props\n" if ($plen);
341 print OUT $content;
342 printf OUT "\n";
343 } elsif ($next =~ m/D (.*)/) {
344 my $path = $basedir . "/". $1;
346 if ($paths{$path}) {
347 delete $paths{$path};
349 printf OUT "Node-path: $path\n";
350 printf OUT "Node-action: delete\n";
351 printf OUT "\n";
352 } elsif ($verbose) {
353 print STDERR "deleting non existing object: $path\n";
356 } elsif ($next =~ m/^C (.*)/) {
357 die "file copy ?";
358 } elsif ($next =~ m/^R (.*)/) {
359 die "file rename ?";
360 } elsif ($next =~ m/^filedeleteall$/) {
361 die "file delete all ?";
362 } else {
363 next COMMAND;
365 $next = next_line($IN);
368 } elsif ($next =~ /^tag .*/) {
369 } elsif ($next =~ /^reset .*/) {
370 } elsif ($next =~ /^blob/) {
371 $next = next_line($IN);
372 if ($next =~ m/mark (.*)/) {
373 $mark = $1;
374 $next = next_line($IN);
376 my $data = read_data($IN, $next);
377 $blob{$mark} = $data if (defined $mark);
378 } elsif ($next =~ /^checkpoint .*/) {
379 } elsif ($next =~ /^progress (.*)/) {
380 print STDERR "progress: $1\n" if ($verbose);
381 } else {
382 die "unknown command $next";
384 $next = next_line($IN);
387 close IN;
388 close OUT;
390 print STDERR "...dumped to revision $revision\n" if ($verbose);
391 print STDERR "(re-)setting sync-tag to new master\n" if ($verbose);
393 unless ($no_load) {
394 system("cd $gittree && ".
395 "git tag -m \"sync $(date)\" -a -f ${syncname} ${masterrev}");
398 print STDERR "loading dump into svn\n" if ($verbose);
400 unless ($no_load) {
401 system("svnadmin load $svntree < $svndump >>$log 2>&1") == 0 or
402 die "svnadmin load";
405 unlink $svndump, $gitdump, $log unless ($keeplogs);
407 exit 0;
410 __END__
412 =head1 NAME
414 B<git2svn> - converts a git branch to a svn ditto
416 =head1 SYNOPSIS
418 B<git2svn> [options] git-repro svn-repro
420 =head1 OPTIONS
422 =over 8
424 =item B<--git-branch>
426 The git branch to export. The default is branch is master.
428 =item B<--svn-prefix>
430 The svn prefix where the branch is import. The default is trunk to
431 match the default GIT branch (master).
433 =item B<--no-load>
435 Don't load the svn repository or update the syncpoint tagname.
437 =item B<--keep-logs>
439 Don't delete the logs in $CWD/.data on success.
441 =item B<--verbose>
443 More verbose output, can be give more then once to increase the verbosity.
445 =item B<--help>
447 Print a brief help message and exits.
449 =back
451 =head1 DESCRIPTION
453 B<git2svn> will convert a git branch to a svn ditto, it also
454 support incremantal updates.
456 B<git2svn> takes a git fast-export dump and converts it into a
457 svn dump that is feed into svnadmin load.
459 B<git2svn> assumes its the only process that writes into the svn
460 repository. This is because of the race between getting the to svn
461 Revsion number from the svn, creating the dump with correct Revsions,
462 and do the svnadmin load.
464 B<git2svn> also support incremental updates from a git branch to
465 a svn reprositry. Its does this by setting a git tag
466 (git2svn-syncpoint-<branchname>) where the last update was pulled
467 from.
469 B<git2svn> was created as a hack over a weekend to support a
470 smoother migration away from svn and allow users access to tools to
471 browse and search code (fisheye) and use anonymouns svn servers.
473 =head1 EXAMPLES
475 B<git2svn> ~/src/heimdal svn-repro
477 B<git2svn> --git-branch heimdal-1-0-branch \
478 --svn-prefix branches/heimdal-1-0-branch \
479 ~/src/heimdal svn-repro
481 =head1 DOWNLOAD
483 B<git2svn> is avaible from repo.or.cz
485 git clone git://repo.or.cz/git2svn.git
487 =head1 AUTHORS
489 Love Hörnquist Åstrand <lha@kth.se>
491 =head1 BUGS
493 Send bug reports to lha@kth.se
495 =cut