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 .= "PROPS-END" if ($props ne "");
330 my $plen = length($props);
331 my $clen = length($content);
333 printf OUT
"Node-path: $path\n";
334 printf OUT
"Node-kind: $kind\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);
341 print OUT
"$props\n" if ($plen);
345 } elsif ($next =~ m/D (.*)/) {
346 my $path = $basedir . "/". $1;
349 delete $paths{$path};
351 printf OUT
"Node-path: $path\n";
352 printf OUT
"Node-action: delete\n";
355 print STDERR
"deleting non existing object: $path\n";
358 } elsif ($next =~ m/^C (.*)/) {
360 } elsif ($next =~ m/^R (.*)/) {
362 } elsif ($next =~ m/^filedeleteall$/) {
363 die "file delete all ?";
367 $next = next_line
($IN);
370 } elsif ($next =~ /^tag .*/) {
371 } elsif ($next =~ /^reset .*/) {
372 } elsif ($next =~ /^blob/) {
373 $next = next_line
($IN);
374 if ($next =~ m/mark (.*)/) {
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);
384 die "unknown command $next";
386 $next = next_line
($IN);
392 print STDERR
"(re-)setting sync-tag to new master\n" if ($verbose);
395 system("cd $gittree && ".
396 "git tag -m \"sync $(date)\" -a -f ${syncname} ${masterrev}");
399 print STDERR
"loading dump into svn\n" if ($verbose);
402 system("svnadmin load $svntree < $svndump >>$log 2>&1") == 0 or
406 unlink $svndump, $gitdump, $log unless ($no_unlink);
415 git2svn - converts a git branch to a svn ditto
419 git2svn [options] git-repro svn-repro
427 The git branch to export. The default is branch is master.
431 The svn prefix where the branch is import. The default is trunk to
432 match the default GIT branch (master).
436 More verbose output, can be give more then once to increase the verbosity.
440 Print a brief help message and exits.
446 B<This program> will convert a git branch to a svn ditto, it also
447 support incremantal updates.
449 git2svn takes a git fast-export dump and converts it into a svn dump
450 that is feed into svnadmin load.
452 git2svn assumes its the only process that writes into the svn
453 repository. This is because of the race between getting the to svn
454 Revsion number from the svn, creating the dump with correct Revsions,
455 and do the svnadmin load.
457 git2svn also support incremental updates from a git branch to a svn
458 reprositry. Its does this by setting a git tag
459 (git2svn-syncpoint-<branchname>) where the last update was pulled from.
463 ./git2svn ~/src/heimdal svn-repro
464 ./git2svn --git-branch heimdal-1-0-branch \
465 --svn-prefix branches/heimdal-1-0-branch \
466 ~/src/heimdal svn-repro
470 git2svn is avaible from repo.or.cz
472 git clone git://repo.or.cz/git2svn.git
476 Love Hörnquist Åstrand <lha@kth.se>
480 Send bug reports to lha@kth.se