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, $keeplogs, $no_load);
30 my $svntree = "repro";
31 my $basedir = "trunk";
35 my $branch = "master";
45 my ($IN, $next, $length, $data, $l) = (shift, shift);
46 unless($next =~ m/^data (\d+)/) { die "missing data: $next" ; }
49 $l = read(IN
, $data, $length);
50 unless ($l == $length) { die "failed to read data $l != $length"; }
51 $data = "" if ($length == 0);
57 my ($key, $value) = (shift, shift);
58 "K " . length($key) . "\n$key\nV " . length($value) . "\n$value\n";
64 my ($SVN, $type, $name);
66 open(SVN
, "svn ls -R $url|") or die "failed to open svn ls -R $url";
76 $paths{$name} = $type;
80 open(SVN
, "svn info $url|") or die "failed to open svn info $url";
83 if ($revision eq 1 && /^Revision: (\d+)/) {
92 my ($gittree, $name) = (shift, shift);
96 open FOO
, "cd $gittree ".
97 "&& git rev-parse $name 2>/dev/null|" or
98 die "git rev-parse $name failed";
103 $id = "" if ($id eq $name);
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"
131 $fexport = "$oldmasterrev..$masterrev";
133 $fexport="$masterrev";
135 system("svnadmin create ./$svntree") unless (-d
$svntree);
145 # pick first dir, create, take next dir, continue until we reached basename
146 while ($path =~ m@
^([^/]+)/(.*)$@
) {
147 my $first = $base . $1;
149 $base = $first . "/";
150 next if ($paths{$first});
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";
172 # This is to allow setting props for a path, XXX add configuration
173 # file/options for this.
179 ### given path, return prop("prop", "value")
187 $result = GetOptions
("git-branch=s" => \
$branch,
188 "svn-prefix=s" => \
$basedir,
189 "keep-logs" => \
$keeplogs,
190 "no-load" => \
$no_load,
191 "verbose+" => \
$verbose,
192 "help" => \
$help) or pod2usage
(2);
194 pod2usage
(0) if ($help);
196 die "to few arguments" if ($#ARGV < 1);
198 mkdir ".data" unless (-d
".data");
200 die "cant find branch name" unless ($branch =~ m@
/?([^/]+)$@
);
201 my $shortbranch = $1;
203 my $gitdump = ".data/git.dump-${shortbranch}";
204 my $svndump = ".data/svn.dump-${shortbranch}";
205 my $log = ".data/log-${shortbranch}";
207 my $gittree = $ARGV[0];
210 parse_git_tree
($gittree, $branch, $shortbranch);
214 parse_svn_tree
("file://" . $cwd ."/". $svntree);
218 print STDERR
"git fast-export $branch ($fexport)\n" if ($verbose);
220 system("(cd $gittree && git fast-export $fexport) > $gitdump 2>$log") == 0 or
221 die "git fast-export: $!";
223 open IN
, "$gitdump" or
224 die "failed to open $gitdump";
226 open OUT
, ">$svndump" or
227 die "failed to open $svndump";
229 print STDERR
"creating svn dump from revision $revision...\n" if ($verbose);
231 print OUT
"SVN-fs-dump-format-version: 3\n";
233 my $next = next_line
();
234 COMMAND
: while (!eof(IN
)) {
236 $next = next_line
($IN);
238 } elsif ($next =~ /^commit (.*)/) {
242 $next = next_line
($IN);
243 if ($next =~ m/mark +(.*)/) {
245 $next = next_line
($IN);
247 if ($next =~ m/author +(.*)/) {
248 $commit{Author
} = $1;
249 $next = next_line
($IN);
251 unless ($next =~ m/committer +(.+) +<([^>]+)> +(\d+) +[+-](\d+)$/) {
252 die "missing comitter: $_";
255 $commit{CommitterName
} = $1;
256 $commit{CommitterEmail
} = $2;
257 $commit{CommitterWhen
} = $3;
258 $commit{CommitterTZ
} = $4;
260 $next = next_line
($IN);
261 my $log = read_data
($IN, $next);
263 $next = next_line
($IN);
264 if ($next =~ m/from (.*)/) {
266 $next = next_line
($IN);
268 if ($next =~ m/merge (.*)/) {
270 $next = next_line
($IN);
274 strftime
("%Y-%m-%dT%H:%M:%S.000000Z",
275 gmtime($commit{CommitterWhen
}));
277 my $author = "(no author)";
278 if ($commit{CommitterEmail
} =~ m/([^@]+)/) {
281 $author = "git2svn-dump" if ($author eq "(no author)");
284 $props .= prop
("svn:author", $author);
285 $props .= prop
("svn:log", $log);
286 $props .= prop
("svn:date", $date);
287 $props .= "PROPS-END";
291 printf OUT
"Revision-number: $revision\n"; $revision++;
292 printf OUT
"Prop-content-length: ". length($props) . "\n";
293 printf OUT
"Content-length: " . length($props) . "\n";
295 print OUT
"$props\n";
298 if ($next =~ m/M (\d+) (\S+) (.*)$/) {
299 my ($mode, $dataref, $path) = (oct $1, $2, $3);
301 if ($dataref eq "inline") {
302 $next = next_line
($IN);
303 $content = read_data
($IN, $next);
305 die "Revision missing content ref $dataref"
306 unless(defined $blob{$dataref});
308 $content = $blob{$dataref};
309 # here we really want to delete $blob{$dataref},
310 # but it might be referenced in the future. To
311 # avoid keepig everything in memory for larger
312 # repositories this must be written out to disk
313 # and removed when done.
316 $path = "$basedir/$path";
322 die "file was a dir" if ($paths{$path} != 2);
329 my $type = $mode & 0777000;
331 $kind = "file" if ($type == 0100000);
332 $kind = "symlink" if ($type == 0120000);
333 die "$type unknown" if ($kind eq "");
336 $props .= prop
("svn:executable", "on") if ($mode & 0111);
337 $props .= prop
("svn:special", "*") if ($kind eq "symlink");
338 $props .= auto_props
($path);
339 $props .= "PROPS-END" if ($props ne "");
341 $content = "link $content" if ($kind eq "symlink");
343 my $plen = length($props);
344 my $clen = length($content);
346 printf OUT
"Node-path: $path\n";
347 printf OUT
"Node-kind: file\n";
348 printf OUT
"Node-action: $action\n";
349 printf OUT
"Text-content-length: $clen\n";
350 printf OUT
"Content-length: " . ($clen + $plen) . "\n";
351 printf OUT
"Prop-content-length: $plen\n" if ($plen);
354 print OUT
"$props\n" if ($plen);
358 } elsif ($next =~ m/D (.*)/) {
359 my $path = $basedir . "/". $1;
362 delete $paths{$path};
364 printf OUT
"Node-path: $path\n";
365 printf OUT
"Node-action: delete\n";
368 print STDERR
"deleting non existing object: $path\n";
371 } elsif ($next =~ m/^C (.*)/) {
373 } elsif ($next =~ m/^R (.*)/) {
375 } elsif ($next =~ m/^filedeleteall$/) {
376 die "file delete all ?";
380 $next = next_line
($IN);
383 } elsif ($next =~ /^tag .*/) {
384 } elsif ($next =~ /^reset .*/) {
385 } elsif ($next =~ /^blob/) {
387 $next = next_line
($IN);
388 if ($next =~ m/mark (.*)/) {
390 $next = next_line
($IN);
392 my $data = read_data
($IN, $next);
393 $blob{$mark} = $data if (defined $mark);
394 } elsif ($next =~ /^checkpoint .*/) {
395 } elsif ($next =~ /^progress (.*)/) {
396 print STDERR
"progress: $1\n" if ($verbose);
398 die "unknown command $next";
400 $next = next_line
($IN);
406 print STDERR
"...dumped to revision $revision\n" if ($verbose);
407 print STDERR
"(re-)setting sync-tag to new master\n" if ($verbose);
410 system("cd $gittree && ".
411 "git tag -m \"sync $(date)\" -a -f ${syncname} ${masterrev}");
414 print STDERR
"loading dump into svn\n" if ($verbose);
417 system("svnadmin load $svntree < $svndump >>$log 2>&1") == 0 or
421 unlink $svndump, $gitdump, $log unless ($keeplogs);
430 B<git2svn> - converts a git branch to a svn ditto
434 B<git2svn> [options] git-repro svn-repro
440 =item B<--git-branch>
442 The git branch to export. The default is branch is master.
444 =item B<--svn-prefix>
446 The svn prefix where the branch is import. The default is trunk to
447 match the default GIT branch (master).
451 Don't load the svn repository or update the syncpoint tagname.
455 Don't delete the logs in $CWD/.data on success.
459 More verbose output, can be give more then once to increase the verbosity.
463 Print a brief help message and exits.
469 B<git2svn> will convert a git branch to a svn ditto, it also
470 support incremantal updates.
472 B<git2svn> takes a git fast-export dump and converts it into a
473 svn dump that is feed into svnadmin load.
475 B<git2svn> assumes its the only process that writes into the svn
476 repository. This is because of the race between getting the to svn
477 Revsion number from the svn, creating the dump with correct Revsions,
478 and do the svnadmin load.
480 B<git2svn> also support incremental updates from a git branch to
481 a svn reprositry. Its does this by setting a git tag
482 (git2svn-syncpoint-<branchname>) where the last update was pulled
485 B<git2svn> was created as a hack over a weekend to support a
486 smoother migration away from svn and allow users access to tools to
487 browse and search code (fisheye) and use anonymouns svn servers.
491 B<git2svn> ~/src/heimdal svn-repro
493 B<git2svn> --git-branch heimdal-1-0-branch \
494 --svn-prefix branches/heimdal-1-0-branch \
495 ~/src/heimdal svn-repro
499 B<git2svn> is avaible from repo.or.cz
501 git clone git://repo.or.cz/git2svn.git
505 Love Hörnquist Åstrand <lha@kth.se>
509 Send bug reports to lha@kth.se