91daeeca69c6706dc56f2aa2831ca6a901a7c605
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-parse $name 2>/dev/null|" or
99 die "git rev-parse $name failed";
104 $id = "" if ($id eq $name);
110 my ($gittree, $branch, $shortbranch) = (shift, shift, shift);
112 $syncname = "git2svn-syncpoint-${shortbranch}";
113 print STDERR
"syncname tag: $syncname\n" if ($verbose);
115 $masterrev = find_branch_id
($gittree, $branch);
116 die "No head found for ${branch}" if ($masterrev eq "");
118 my $oldmasterrev = find_branch_id
($gittree, $syncname);
120 if ($oldmasterrev ne "") {
122 die "no $svntree, but incremental (have synctag) ?\n".
123 "(\"cd $gittree && git tag -d $syncname\" to remove)"
124 unless ( -d
$svntree);
126 if (${oldmasterrev
} eq $masterrev) {
127 print STDERR
"nothing to sync, $syncname matches $branch\n"
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 "keep-logs" => \
$keeplogs,
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 my $gitdump = ".data/git.dump-${shortbranch}";
192 my $svndump = ".data/svn.dump-${shortbranch}";
193 my $log = ".data/log-${shortbranch}";
195 my $gittree = $ARGV[0];
198 parse_git_tree
($gittree, $branch, $shortbranch);
202 parse_svn_tree
("file://" . $cwd ."/". $svntree);
206 print STDERR
"git fast-export $branch ($fexport)\n" if ($verbose);
208 system("(cd $gittree && git fast-export $fexport) > $gitdump 2>$log") == 0 or
209 die "git fast-export: $!";
211 open IN
, "$gitdump" or
212 die "failed to open $gitdump";
214 open OUT
, ">$svndump" or
215 die "failed to open $svndump";
217 print STDERR
"creating svn dump from revision $revision...\n" if ($verbose);
219 print OUT
"SVN-fs-dump-format-version: 3\n";
221 my $next = next_line
();
222 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 (.*)/) {
254 $next = next_line
($IN);
256 if ($next =~ m/merge (.*)/) {
258 $next = next_line
($IN);
262 strftime
("%Y-%m-%dT%H:%M:%S.000000Z",
263 gmtime($commit{CommitterWhen
}));
265 my $author = "(no author)";
266 if ($commit{CommitterEmail
} =~ m/([^@]+)/) {
269 $author = "git2svn-dump" if ($author eq "(no author)");
272 $props .= prop
("svn:author", $author);
273 $props .= prop
("svn:log", $log);
274 $props .= prop
("svn:date", $date);
275 $props .= "PROPS-END";
279 printf OUT
"Revision-number: $revision\n"; $revision++;
280 printf OUT
"Prop-content-length: ". length($props) . "\n";
281 printf OUT
"Content-length: " . length($props) . "\n";
283 print OUT
"$props\n";
286 if ($next =~ m/M (\d+) (\S+) (.*)$/) {
287 my ($mode, $dataref, $path) = (oct $1, $2, $3);
289 if ($dataref eq "inline") {
290 $next = next_line
($IN);
291 $content = read_data
($IN, $next);
293 die "Revision missing content ref $dataref"
294 unless(defined $blob{$dataref});
296 $content = $blob{$dataref};
297 # here we really want to delete $blob{$dataref},
298 # but it might be referenced in the future. To
299 # avoid keepig everything in memory for larger
300 # repositories this must be written out to disk
301 # and removed when done.
304 $path = "$basedir/$path";
310 die "file was a dir" if ($paths{$path} != 2);
317 my $type = $mode & 0777000;
319 $kind = "file" if ($type == 0100000);
320 $kind = "symlink" if ($type == 0120000);
321 die "$type unknown" if ($kind eq "");
324 $props .= prop
("svn:executable", "on") if ($mode & 0111);
325 $props .= prop
("svn:special", "*") if ($kind eq "symlink");
326 $props .= "PROPS-END" if ($props ne "");
328 $content = "link $content" if ($kind eq "symlink");
330 my $plen = length($props);
331 my $clen = length($content);
333 printf OUT
"Node-path: $path\n";
334 printf OUT
"Node-kind: file\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/) {
374 $next = next_line
($IN);
375 if ($next =~ m/mark (.*)/) {
377 $next = next_line
($IN);
379 my $data = read_data
($IN, $next);
380 $blob{$mark} = $data if (defined $mark);
381 } elsif ($next =~ /^checkpoint .*/) {
382 } elsif ($next =~ /^progress (.*)/) {
383 print STDERR
"progress: $1\n" if ($verbose);
385 die "unknown command $next";
387 $next = next_line
($IN);
393 print STDERR
"...dumped to revision $revision\n" if ($verbose);
394 print STDERR
"(re-)setting sync-tag to new master\n" if ($verbose);
397 system("cd $gittree && ".
398 "git tag -m \"sync $(date)\" -a -f ${syncname} ${masterrev}");
401 print STDERR
"loading dump into svn\n" if ($verbose);
404 system("svnadmin load $svntree < $svndump >>$log 2>&1") == 0 or
408 unlink $svndump, $gitdump, $log unless ($keeplogs);
417 B<git2svn> - converts a git branch to a svn ditto
421 B<git2svn> [options] git-repro svn-repro
427 =item B<--git-branch>
429 The git branch to export. The default is branch is master.
431 =item B<--svn-prefix>
433 The svn prefix where the branch is import. The default is trunk to
434 match the default GIT branch (master).
438 Don't load the svn repository or update the syncpoint tagname.
442 Don't delete the logs in $CWD/.data on success.
446 More verbose output, can be give more then once to increase the verbosity.
450 Print a brief help message and exits.
456 B<git2svn> will convert a git branch to a svn ditto, it also
457 support incremantal updates.
459 B<git2svn> takes a git fast-export dump and converts it into a
460 svn dump that is feed into svnadmin load.
462 B<git2svn> assumes its the only process that writes into the svn
463 repository. This is because of the race between getting the to svn
464 Revsion number from the svn, creating the dump with correct Revsions,
465 and do the svnadmin load.
467 B<git2svn> also support incremental updates from a git branch to
468 a svn reprositry. Its does this by setting a git tag
469 (git2svn-syncpoint-<branchname>) where the last update was pulled
472 B<git2svn> was created as a hack over a weekend to support a
473 smoother migration away from svn and allow users access to tools to
474 browse and search code (fisheye) and use anonymouns svn servers.
478 B<git2svn> ~/src/heimdal svn-repro
480 B<git2svn> --git-branch heimdal-1-0-branch \
481 --svn-prefix branches/heimdal-1-0-branch \
482 ~/src/heimdal svn-repro
486 B<git2svn> is avaible from repo.or.cz
488 git clone git://repo.or.cz/git2svn.git
492 Love Hörnquist Åstrand <lha@kth.se>
496 Send bug reports to lha@kth.se