Don't use a global $mark.
[git2svn.git] / git2svn
blobab46497a3c8bbcd5b774d1a71bbfe0921f2807a6
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 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;
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 if ($next eq "") {
223 $next = next_line($IN);
224 next COMMAND;
225 } elsif ($next =~ /^commit (.*)/) {
227 my %commit;
229 $next = next_line($IN);
230 if ($next =~ m/mark +(.*)/) {
231 $commit{Mark} = $1;
232 $next = next_line($IN);
234 if ($next =~ m/author +(.*)/) {
235 $commit{Author} = $1;
236 $next = next_line($IN);
238 unless ($next =~ m/committer +(.+) +<([^>]+)> +(\d+) +[+-](\d+)$/) {
239 die "missing comitter: $_";
242 $commit{CommitterName} = $1;
243 $commit{CommitterEmail} = $2;
244 $commit{CommitterWhen} = $3;
245 $commit{CommitterTZ} = $4;
247 $next = next_line($IN);
248 my $log = read_data($IN, $next);
250 $next = next_line($IN);
251 if ($next =~ m/from (.*)/) {
252 $commit{From} = $1;
253 $next = next_line($IN);
255 if ($next =~ m/merge (.*)/) {
256 $commit{Merge} = $1;
257 $next = next_line($IN);
260 my $date =
261 strftime("%Y-%m-%dT%H:%M:%S.000000Z",
262 gmtime($commit{CommitterWhen}));
264 my $author = "(no author)";
265 if ($commit{CommitterEmail} =~ m/([^@]+)/) {
266 $author = $1;
268 $author = "git2svn-dump" if ($author eq "(no author)");
270 my $props = "";
271 $props .= prop("svn:author", $author);
272 $props .= prop("svn:log", $log);
273 $props .= prop("svn:date", $date);
274 $props .= "PROPS-END";
276 # push out svn info
278 printf OUT "Revision-number: $revision\n"; $revision++;
279 printf OUT "Prop-content-length: ". length($props) . "\n";
280 printf OUT "Content-length: " . length($props) . "\n";
281 printf OUT "\n";
282 print OUT "$props\n";
284 while (1) {
285 if ($next =~ m/M (\d+) (\S+) (.*)$/) {
286 my ($mode, $dataref, $path) = (oct $1, $2, $3);
287 my $content;
288 if ($dataref eq "inline") {
289 $next = next_line($IN);
290 $content = read_data($IN, $next);
291 } else {
292 die "Revision missing content ref $dataref"
293 unless(defined $blob{$dataref});
295 $content = $blob{$dataref};
296 # here we really want to delete $blob{$dataref},
297 # but it might be referenced in the future. To
298 # avoid keepig everything in memory for larger
299 # repositories this must be written out to disk
300 # and removed when done.
303 $path = "$basedir/$path";
304 checkdirs($path);
306 my $action = "add";
308 if ($paths{$path}) {
309 die "file was a dir" if ($paths{$path} != 2);
310 $action = "change";
311 } else {
312 $paths{$path} = 2;
316 my $type = $mode & 0777000;
317 my $kind = "";
318 $kind = "file" if ($type == 0100000);
319 $kind = "symlink" if ($type == 0120000);
320 die "$type unknown" if ($kind eq "");
322 $props = "";
323 $props .= prop("svn:executable", "on") if ($mode & 0111);
324 $props .= prop("svn:special", "*") if ($kind eq "symlink");
325 $props .= "PROPS-END" if ($props ne "");
327 $content = "link $content" if ($kind eq "symlink");
329 my $plen = length($props);
330 my $clen = length($content);
332 printf OUT "Node-path: $path\n";
333 printf OUT "Node-kind: file\n";
334 printf OUT "Node-action: $action\n";
335 printf OUT "Text-content-length: $clen\n";
336 printf OUT "Content-length: " . ($clen + $plen) . "\n";
337 printf OUT "Prop-content-length: $plen\n" if ($plen);
338 printf OUT "\n";
340 print OUT "$props\n" if ($plen);
342 print OUT $content;
343 printf OUT "\n";
344 } elsif ($next =~ m/D (.*)/) {
345 my $path = $basedir . "/". $1;
347 if ($paths{$path}) {
348 delete $paths{$path};
350 printf OUT "Node-path: $path\n";
351 printf OUT "Node-action: delete\n";
352 printf OUT "\n";
353 } elsif ($verbose) {
354 print STDERR "deleting non existing object: $path\n";
357 } elsif ($next =~ m/^C (.*)/) {
358 die "file copy ?";
359 } elsif ($next =~ m/^R (.*)/) {
360 die "file rename ?";
361 } elsif ($next =~ m/^filedeleteall$/) {
362 die "file delete all ?";
363 } else {
364 next COMMAND;
366 $next = next_line($IN);
369 } elsif ($next =~ /^tag .*/) {
370 } elsif ($next =~ /^reset .*/) {
371 } elsif ($next =~ /^blob/) {
372 my $mark = undef;
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 "...dumped to revision $revision\n" if ($verbose);
393 print STDERR "(re-)setting sync-tag to new master\n" if ($verbose);
395 unless ($no_load) {
396 system("cd $gittree && ".
397 "git tag -m \"sync $(date)\" -a -f ${syncname} ${masterrev}");
400 print STDERR "loading dump into svn\n" if ($verbose);
402 unless ($no_load) {
403 system("svnadmin load $svntree < $svndump >>$log 2>&1") == 0 or
404 die "svnadmin load";
407 unlink $svndump, $gitdump, $log unless ($keeplogs);
409 exit 0;
412 __END__
414 =head1 NAME
416 B<git2svn> - converts a git branch to a svn ditto
418 =head1 SYNOPSIS
420 B<git2svn> [options] git-repro svn-repro
422 =head1 OPTIONS
424 =over 8
426 =item B<--git-branch>
428 The git branch to export. The default is branch is master.
430 =item B<--svn-prefix>
432 The svn prefix where the branch is import. The default is trunk to
433 match the default GIT branch (master).
435 =item B<--no-load>
437 Don't load the svn repository or update the syncpoint tagname.
439 =item B<--keep-logs>
441 Don't delete the logs in $CWD/.data on success.
443 =item B<--verbose>
445 More verbose output, can be give more then once to increase the verbosity.
447 =item B<--help>
449 Print a brief help message and exits.
451 =back
453 =head1 DESCRIPTION
455 B<git2svn> will convert a git branch to a svn ditto, it also
456 support incremantal updates.
458 B<git2svn> takes a git fast-export dump and converts it into a
459 svn dump that is feed into svnadmin load.
461 B<git2svn> assumes its the only process that writes into the svn
462 repository. This is because of the race between getting the to svn
463 Revsion number from the svn, creating the dump with correct Revsions,
464 and do the svnadmin load.
466 B<git2svn> also support incremental updates from a git branch to
467 a svn reprositry. Its does this by setting a git tag
468 (git2svn-syncpoint-<branchname>) where the last update was pulled
469 from.
471 B<git2svn> was created as a hack over a weekend to support a
472 smoother migration away from svn and allow users access to tools to
473 browse and search code (fisheye) and use anonymouns svn servers.
475 =head1 EXAMPLES
477 B<git2svn> ~/src/heimdal svn-repro
479 B<git2svn> --git-branch heimdal-1-0-branch \
480 --svn-prefix branches/heimdal-1-0-branch \
481 ~/src/heimdal svn-repro
483 =head1 DOWNLOAD
485 B<git2svn> is avaible from repo.or.cz
487 git clone git://repo.or.cz/git2svn.git
489 =head1 AUTHORS
491 Love Hörnquist Åstrand <lha@kth.se>
493 =head1 BUGS
495 Send bug reports to lha@kth.se
497 =cut