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/>.
20 use POSIX
qw(strftime);
21 use Getopt
::Long qw
/:config gnu_getopt no_ignore_case auto_abbrev/;
27 my ($help, $verbose, $no_unlink, $no_load);
30 my $svntree = "repro";
31 my $basedir = "trunk";
35 my $branch = "master";
47 my ($IN, $next, $length, $data, $l) = (shift, shift);
48 unless($next =~ m/^data (\d+)/) { die "missing data: $next" ; }
51 $l = read(IN
, $data, $length);
52 unless ($l == $length) { die "failed to read data $l != $length"; }
53 $data = "" if ($length == 0);
59 my ($key, $value) = (shift, shift);
60 "K " . length($key) . "\n$key\nV " . length($value) . "\n$value\n";
66 my ($SVN, $type, $name);
68 open(SVN
, "svn ls -R $url|") or die "failed to open svn ls -R $url";
72 $name = "$basedir/$1";
75 $name = "$basedir/$_";
77 $paths{$name} = $type;
81 open(SVN
, "svn info $url|") or die "failed to open svn info $url";
84 if (/^Revision: (\d+)/) {
96 foreach my $m ("heads", "remotes") {
97 my $n = "${gittree}/.git/refs/${m}/${name}";
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);
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";
134 $fexport="${masterrev}";
136 system("svnadmin create ./$svntree") unless (-d
$svntree);
146 # pick first dir, create, take next dir, continue until we reached basename
147 while ($path =~ m@
^([^/]+)/(.*)$@
) {
148 my $first = $base . $1;
150 $base = $first . "/";
151 next if ($paths{$first});
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";
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}";
202 parse_git_tree
($gittree);
206 parse_svn_tree
("file://" . $cwd ."/". $svntree);
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
)) {
229 $next = next_line
($IN);
231 } elsif ($next =~ /^commit (.*)/) {
235 $next = next_line
($IN);
236 if ($next =~ m/mark +(.*)/) {
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);
265 strftime
("%Y-%m-%dT%H:%M:%S.000000Z",
266 gmtime($commit{CommitterWhen
}));
268 my $author = "(no author)";
269 if ($commit{CommitterEmail
} =~ m/([^@]+)/) {
272 $author = "git2svn-dump" if ($author eq "(no author)");
275 $props .= prop
("svn:author", $author);
276 $props .= prop
("svn:log", $log);
277 $props .= prop
("svn:date", $date);
278 $props .= "PROPS-END";
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";
286 print OUT
"$props\n";
289 if ($next =~ m/M (\d+) (\S+) (.*)$/) {
290 my ($mode, $dataref, $path) = (oct $1, $2, $3);
292 if ($dataref eq "inline") {
293 $next = next_line
($IN);
294 $content = read_data
($IN, $next);
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";
313 die "file was a dir" if ($paths{$path} != 2);
320 my $type = $mode & 0777000;
322 $kind = "file" if ($type == 0100000);
323 $kind = "symlink" if ($type == 0120000);
324 die "$type unknown" if ($kind eq "");
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);
344 print OUT
"$props\n" if ($plen);
348 } elsif ($next =~ m/D (.*)/) {
349 my $path = $basedir . "/". $1;
352 delete $paths{$path};
354 printf OUT
"Node-path: $path\n";
355 printf OUT
"Node-action: delete\n";
358 print STDERR
"deleting non existing object: $path\n";
361 } elsif ($next =~ m/^C (.*)/) {
363 } elsif ($next =~ m/^R (.*)/) {
365 } elsif ($next =~ m/^filedeleteall$/) {
366 die "file delete all ?";
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 (.*)/) {
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);
387 die "unknown command $next";
389 $next = next_line
($IN);
395 print STDERR
"(re-)setting sync-tag to new master\n" if ($verbose);
398 system("cd $gittree && ".
399 "git tag -m \"sync $(date)\" -a -f ${syncname} ${masterrev}");
402 print STDERR
"loading dump into svn\n" if ($verbose);
405 system("svnadmin load $svntree < $svndump >>$log 2>&1") == 0 or
409 unlink $svndump, $gitdump, $log unless ($no_unlink);
418 git2svn - converts a git branch to a svn ditto
422 git2svn [options] git-repro svn-repro
430 The git branch to export. The default is branch is master.
434 The svn prefix where the branch is import. The default is trunk to
435 match the default GIT branch (master).
439 More verbose output, can be give more then once to increase the verbosity.
443 Print a brief help message and exits.
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.
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
473 git2svn is avaible from repo.or.cz
475 git clone git://repo.or.cz/git2svn.git
479 Love Hörnquist Åstrand <lha@kth.se>
483 Send bug reports to lha@kth.se