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";
78 $paths{$name} = $type;
82 open(SVN
, "svn info $url|") or die "failed to open svn info $url";
85 if (/^Revision: (\d+)/) {
97 foreach my $m ("heads", "remotes") {
98 my $n = "${gittree}/.git/refs/${m}/${name}";
112 $masterrev = find_branch_id
($branch);
114 die "No head found for ${branch}" if ($masterrev eq "");
116 my $syncpoint="${gittree}/.git/refs/tags/${syncname}";
118 if (-f
${syncpoint
}) {
119 my $oldmasterrev=`cat ${syncpoint}`;
120 chomp($oldmasterrev);
122 die "failed to get old parse name" if ($oldmasterrev eq "");
124 if (${oldmasterrev
} eq ${masterrev
}) {
125 print STDERR
"nothing to sync\n" if ($verbose);
129 die "no $svntree, but incremental (have synctag) ?\n".
130 "(\"cd $gittree && git tag -d $syncname\" to remove)"
131 unless ( -d
$svntree);
133 $fexport = "$oldmasterrev..$masterrev";
135 $fexport="${masterrev}";
137 system("svnadmin create ./$svntree") unless (-d
$svntree);
147 # pick first dir, create, take next dir, continue until we reached basename
148 while ($path =~ m@
^([^/]+)/(.*)$@
) {
149 my $first = $base . $1;
151 $base = $first . "/";
152 next if ($paths{$first});
156 printf OUT
"Node-path: $first\n";
157 printf OUT
"Node-kind: dir\n";
158 printf OUT
"Node-action: add\n";
159 printf OUT
"Prop-content-length: 0\n";
160 printf OUT
"Content-length: 0\n";
176 $result = GetOptions
("git-branch=s" => \
$branch,
177 "svn-prefix=s" => \
$basedir,
178 "no-unlink" => \
$no_unlink,
179 "no-load" => \
$no_load,
180 "verbose+" => \
$verbose,
181 "help" => \
$help) or pod2usage
(2);
183 pod2usage
(0) if ($help);
185 die "to few arguments" if ($#ARGV < 1);
187 mkdir ".data" unless (-d
".data");
189 die "cant find branch name" unless ($branch =~ m@
/?([^/]+)$@
);
190 my $shortbranch = $1;
192 $syncname = "git2svn-syncpoint-${shortbranch}";
194 print STDERR
"syncname tag: $syncname\n" if ($verbose);
196 my $gitdump = ".data/git.dump-${shortbranch}";
197 my $svndump = ".data/svn.dump-${shortbranch}";
198 my $log = ".data/log-${shortbranch}";
203 parse_git_tree
($gittree);
207 parse_svn_tree
("file://" . $cwd ."/". $svntree);
211 print STDERR
"git fast-export $branch ($fexport)\n" if ($verbose);
213 system("(cd $gittree && git fast-export $fexport) > $gitdump 2>$log") == 0 or
214 die "git fast-export: $!";
216 open IN
, "$gitdump" or
217 die "failed to open $gitdump";
219 open OUT
, ">$svndump" or
220 die "failed to open $svndump";
222 print STDERR
"creating svn dump from revision $revision...\n" if ($verbose);
224 print OUT
"SVN-fs-dump-format-version: 3\n";
226 my $next = next_line
();
227 COMMAND
: while (!eof(IN
)) {
230 $next = next_line
($IN);
232 } elsif ($next =~ /^commit (.*)/) {
236 $next = next_line
($IN);
237 if ($next =~ m/mark +(.*)/) {
239 $next = next_line
($IN);
241 if ($next =~ m/author +(.*)/) {
242 $commit{author
} = $1;
243 $next = next_line
($IN);
245 unless ($next =~ m/committer +(.+) +<([^>]+)> +(\d+) +[+-](\d+)$/) {
246 die "missing comitter: $_";
249 $commit{CommitterName
} = $1;
250 $commit{CommitterEmail
} = $2;
251 $commit{CommitterWhen
} = $3;
252 $commit{CommitterTZ
} = $4;
254 $next = next_line
($IN);
255 my $log = read_data
($IN, $next);
257 $next = next_line
($IN);
258 if ($next =~ m/from (.*)/) {
259 $next = next_line
($IN);
261 if ($next =~ m/merge (.*)/) {
262 $next = next_line
($IN);
266 strftime
("%Y-%m-%dT%H:%M:%S.000000Z",
267 gmtime($commit{CommitterWhen
}));
269 my $author = "(no author)";
270 if ($commit{CommitterEmail
} =~ m/([^@]+)/) {
273 $author = "git2svn-dump" if ($author eq "(no author)");
276 $props .= prop
("svn:author", $author);
277 $props .= prop
("svn:log", $log);
278 $props .= prop
("svn:date", $date);
279 $props .= "PROPS-END";
283 printf OUT
"Revision-number: $revision\n"; $revision++;
284 printf OUT
"Prop-content-length: ". length($props) . "\n";
285 printf OUT
"Content-length: " . length($props) . "\n";
287 print OUT
"$props\n";
290 if ($next =~ m/M (\d+) (\S+) (.*)$/) {
291 my ($mode, $dataref, $path) = (oct $1, $2, $3);
293 if ($dataref eq "inline") {
294 $next = next_line
($IN);
295 $content = read_data
($IN, $next);
297 die "Revision missing content ref $dataref"
298 unless(defined $blob{$dataref});
300 $content = $blob{$dataref};
301 # here we really want to delete $blob{$dataref},
302 # but it might be referenced in the future. To
303 # avoid keepig everything in memory for larger
304 # repositories this must be written out to disk
305 # and removed when done.
308 $path = "$basedir/$path";
314 die "file was a dir" if ($paths{$path} != 2);
321 my $type = $mode & 0777000;
323 $kind = "file" if ($type == 0100000);
324 $kind = "symlink" if ($type == 0120000);
325 die "$type unknown" if ($kind eq "");
328 $props .= prop
("svn:executable", "on") if ($mode & 0111);
329 $props .= prop
("svn:special", "*") if ($kind eq "symlink");
330 $props .= "PROPS-END" if ($props ne "");
332 $content = "link $content" if ($kind eq "symlink");
334 my $plen = length($props);
335 my $clen = length($content);
337 printf OUT
"Node-path: $path\n";
338 printf OUT
"Node-kind: file\n";
339 printf OUT
"Node-action: $action\n";
340 printf OUT
"Text-content-length: $clen\n";
341 printf OUT
"Content-length: " . ($clen + $plen) . "\n";
342 printf OUT
"Prop-content-length: $plen\n" if ($plen);
345 print OUT
"$props\n" if ($plen);
349 } elsif ($next =~ m/D (.*)/) {
350 my $path = $basedir . "/". $1;
353 delete $paths{$path};
355 printf OUT
"Node-path: $path\n";
356 printf OUT
"Node-action: delete\n";
359 print STDERR
"deleting non existing object: $path\n";
362 } elsif ($next =~ m/^C (.*)/) {
364 } elsif ($next =~ m/^R (.*)/) {
366 } elsif ($next =~ m/^filedeleteall$/) {
367 die "file delete all ?";
371 $next = next_line
($IN);
374 } elsif ($next =~ /^tag .*/) {
375 } elsif ($next =~ /^reset .*/) {
376 } elsif ($next =~ /^blob/) {
377 $next = next_line
($IN);
378 if ($next =~ m/mark (.*)/) {
380 $next = next_line
($IN);
382 my $data = read_data
($IN, $next);
383 $blob{$mark} = $data if (defined $mark);
384 } elsif ($next =~ /^checkpoint .*/) {
385 } elsif ($next =~ /^progress (.*)/) {
386 print STDERR
"progress: $1\n" if ($verbose);
388 die "unknown command $next";
390 $next = next_line
($IN);
396 print STDERR
"(re-)setting sync-tag to new master\n" if ($verbose);
399 system("cd $gittree && ".
400 "git tag -m \"sync $(date)\" -a -f ${syncname} ${masterrev}");
403 print STDERR
"loading dump into svn\n" if ($verbose);
406 system("svnadmin load $svntree < $svndump >>$log 2>&1") == 0 or
410 unlink $svndump, $gitdump, $log unless ($no_unlink);
419 git2svn - converts a git branch to a svn ditto
423 git2svn [options] git-repro svn-repro
431 The git branch to export. The default is branch is master.
435 The svn prefix where the branch is import. The default is trunk to
436 match the default GIT branch (master).
440 More verbose output, can be give more then once to increase the verbosity.
444 Print a brief help message and exits.
450 B<This program> will convert a git branch to a svn ditto, it also
451 support incremantal updates.
453 git2svn takes a git fast-export dump and converts it into a svn dump
454 that is feed into svnadmin load.
456 git2svn assumes its the only process that writes into the svn
457 repository. This is because of the race between getting the to svn
458 Revsion number from the svn, creating the dump with correct Revsions,
459 and do the svnadmin load.
461 git2svn also support incremental updates from a git branch to a svn
462 reprositry. Its does this by setting a git tag
463 (git2svn-syncpoint-<branchname>) where the last update was pulled from.
467 ./git2svn ~/src/heimdal svn-repro
468 ./git2svn --git-branch heimdal-1-0-branch \
469 --svn-prefix branches/heimdal-1-0-branch \
470 ~/src/heimdal svn-repro
474 git2svn is avaible from repo.or.cz
476 git clone git://repo.or.cz/git2svn.git
480 Love Hörnquist Åstrand <lha@kth.se>
484 Send bug reports to lha@kth.se