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: (\d+)/) {
93 my ($gittree, $name) = (shift, shift);
97 open FOO
, "cd $gittree ".
98 "&& git rev-list --max-count=1 $name 2>/dev/null|" or
99 die "git rev-list $name failed";
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 if (${oldmasterrev
} eq $masterrev) {
122 print STDERR
"nothing to sync, $syncname matches $branch\n"
127 die "no $svntree, but incremental (have synctag) ?\n".
128 "(\"cd $gittree && git tag -d $syncname\" to remove)"
129 unless ( -d
$svntree);
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";
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];
197 parse_git_tree
($gittree, $branch, $shortbranch);
201 parse_svn_tree
("file://" . $cwd ."/". $svntree);
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
)) {
224 $next = next_line
($IN);
226 } elsif ($next =~ /^commit (.*)/) {
230 $next = next_line
($IN);
231 if ($next =~ m/mark +(.*)/) {
233 $next = next_line
($IN);
235 if ($next =~ m/author +(.*)/) {
236 $commit{author
} = $1;
237 $next = next_line
($IN);
239 unless ($next =~ m/committer +(.+) +<([^>]+)> +(\d+) +[+-](\d+)$/) {
240 die "missing comitter: $_";
243 $commit{CommitterName
} = $1;
244 $commit{CommitterEmail
} = $2;
245 $commit{CommitterWhen
} = $3;
246 $commit{CommitterTZ
} = $4;
248 $next = next_line
($IN);
249 my $log = read_data
($IN, $next);
251 $next = next_line
($IN);
252 if ($next =~ m/from (.*)/) {
253 $next = next_line
($IN);
255 if ($next =~ m/merge (.*)/) {
256 $next = next_line
($IN);
260 strftime
("%Y-%m-%dT%H:%M:%S.000000Z",
261 gmtime($commit{CommitterWhen
}));
263 my $author = "(no author)";
264 if ($commit{CommitterEmail
} =~ m/([^@]+)/) {
267 $author = "git2svn-dump" if ($author eq "(no author)");
270 $props .= prop
("svn:author", $author);
271 $props .= prop
("svn:log", $log);
272 $props .= prop
("svn:date", $date);
273 $props .= "PROPS-END";
277 printf OUT
"Revision-number: $revision\n"; $revision++;
278 printf OUT
"Prop-content-length: ". length($props) . "\n";
279 printf OUT
"Content-length: " . length($props) . "\n";
281 print OUT
"$props\n";
284 if ($next =~ m/M (\d+) (\S+) (.*)$/) {
285 my ($mode, $dataref, $path) = (oct $1, $2, $3);
287 if ($dataref eq "inline") {
288 $next = next_line
($IN);
289 $content = read_data
($IN, $next);
291 die "Revision missing content ref $dataref"
292 unless(defined $blob{$dataref});
294 $content = $blob{$dataref};
295 # here we really want to delete $blob{$dataref},
296 # but it might be referenced in the future. To
297 # avoid keepig everything in memory for larger
298 # repositories this must be written out to disk
299 # and removed when done.
302 $path = "$basedir/$path";
308 die "file was a dir" if ($paths{$path} != 2);
315 my $type = $mode & 0777000;
317 $kind = "file" if ($type == 0100000);
318 $kind = "symlink" if ($type == 0120000);
319 die "$type unknown" if ($kind eq "");
322 $props .= prop
("svn:executable", "on") if ($mode & 0111);
323 $props .= prop
("svn:special", "*") if ($kind eq "symlink");
324 $props .= "PROPS-END" if ($props ne "");
326 $content = "link $content" if ($kind eq "symlink");
328 my $plen = length($props);
329 my $clen = length($content);
331 printf OUT
"Node-path: $path\n";
332 printf OUT
"Node-kind: file\n";
333 printf OUT
"Node-action: $action\n";
334 printf OUT
"Text-content-length: $clen\n";
335 printf OUT
"Content-length: " . ($clen + $plen) . "\n";
336 printf OUT
"Prop-content-length: $plen\n" if ($plen);
339 print OUT
"$props\n" if ($plen);
343 } elsif ($next =~ m/D (.*)/) {
344 my $path = $basedir . "/". $1;
347 delete $paths{$path};
349 printf OUT
"Node-path: $path\n";
350 printf OUT
"Node-action: delete\n";
353 print STDERR
"deleting non existing object: $path\n";
356 } elsif ($next =~ m/^C (.*)/) {
358 } elsif ($next =~ m/^R (.*)/) {
360 } elsif ($next =~ m/^filedeleteall$/) {
361 die "file delete all ?";
365 $next = next_line
($IN);
368 } elsif ($next =~ /^tag .*/) {
369 } elsif ($next =~ /^reset .*/) {
370 } elsif ($next =~ /^blob/) {
371 $next = next_line
($IN);
372 if ($next =~ m/mark (.*)/) {
374 $next = next_line
($IN);
376 my $data = read_data
($IN, $next);
377 $blob{$mark} = $data if (defined $mark);
378 } elsif ($next =~ /^checkpoint .*/) {
379 } elsif ($next =~ /^progress (.*)/) {
380 print STDERR
"progress: $1\n" if ($verbose);
382 die "unknown command $next";
384 $next = next_line
($IN);
390 print STDERR
"...dumped to revision $revision\n" if ($verbose);
391 print STDERR
"(re-)setting sync-tag to new master\n" if ($verbose);
394 system("cd $gittree && ".
395 "git tag -m \"sync $(date)\" -a -f ${syncname} ${masterrev}");
398 print STDERR
"loading dump into svn\n" if ($verbose);
401 system("svnadmin load $svntree < $svndump >>$log 2>&1") == 0 or
405 unlink $svndump, $gitdump, $log unless ($keeplogs);
414 B<git2svn> - converts a git branch to a svn ditto
418 B<git2svn> [options] git-repro svn-repro
424 =item B<--git-branch>
426 The git branch to export. The default is branch is master.
428 =item B<--svn-prefix>
430 The svn prefix where the branch is import. The default is trunk to
431 match the default GIT branch (master).
435 Don't load the svn repository or update the syncpoint tagname.
439 Don't delete the logs in $CWD/.data on success.
443 More verbose output, can be give more then once to increase the verbosity.
447 Print a brief help message and exits.
453 B<git2svn> will convert a git branch to a svn ditto, it also
454 support incremantal updates.
456 B<git2svn> takes a git fast-export dump and converts it into a
457 svn dump that is feed into svnadmin load.
459 B<git2svn> assumes its the only process that writes into the svn
460 repository. This is because of the race between getting the to svn
461 Revsion number from the svn, creating the dump with correct Revsions,
462 and do the svnadmin load.
464 B<git2svn> also support incremental updates from a git branch to
465 a svn reprositry. Its does this by setting a git tag
466 (git2svn-syncpoint-<branchname>) where the last update was pulled
469 B<git2svn> was created as a hack over a weekend to support a
470 smoother migration away from svn and allow users access to tools to
471 browse and search code (fisheye) and use anonymouns svn servers.
475 B<git2svn> ~/src/heimdal svn-repro
477 B<git2svn> --git-branch heimdal-1-0-branch \
478 --svn-prefix branches/heimdal-1-0-branch \
479 ~/src/heimdal svn-repro
483 B<git2svn> is avaible from repo.or.cz
485 git clone git://repo.or.cz/git2svn.git
489 Love Hörnquist Åstrand <lha@kth.se>
493 Send bug reports to lha@kth.se