8ef55f16ca9368ab314980de46711b9e0238b1d0
3 # git2svn, converts a git branch to a svn ditto
5 # Copyright (c) 2008 Love Hörnquist Åstrand
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions
12 # 1. Redistributions of source code must retain the above copyright
13 # notice, this list of conditions and the following disclaimer.
15 # 2. Redistributions in binary form must reproduce the above copyright
16 # notice, this list of conditions and the following disclaimer in the
17 # documentation and/or other materials provided with the distribution.
19 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
20 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22 # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
23 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
25 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
28 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
32 use POSIX
qw(strftime);
33 use Getopt
::Long qw
/:config gnu_getopt no_ignore_case auto_abbrev/;
39 my ($help, $verbose, $keeplogs, $no_load, $ignore_author);
42 my $svntree = "repro";
43 my $basedir = "trunk";
47 my $branch = "master";
57 my ($IN, $next, $length, $data, $l) = (shift, shift);
58 unless($next =~ m/^data (\d+)/) { die "missing data: $next" ; }
61 $l = read(IN
, $data, $length);
62 unless ($l == $length) { die "failed to read data $l != $length"; }
63 $data = "" if ($length == 0);
69 my ($key, $value) = (shift, shift);
70 "K " . length($key) . "\n$key\nV " . length($value) . "\n$value\n";
76 my ($SVN, $type, $name);
78 open(SVN
, "svn ls -R $url|") or die "failed to open svn ls -R $url";
88 $paths{$name} = $type;
92 open(SVN
, "svn info $url|") or die "failed to open svn info $url";
95 if ($revision eq 1 && /^Revision: (\d+)/) {
104 my ($gittree, $name) = (shift, shift);
108 open FOO
, "cd $gittree ".
109 "&& git rev-parse $name 2>/dev/null|" or
110 die "git rev-parse $name failed";
115 $id = "" if ($id eq $name);
121 my ($gittree, $branch, $shortbranch) = (shift, shift, shift);
123 $syncname = "git2svn-syncpoint-${shortbranch}";
124 print STDERR
"syncname tag: $syncname\n" if ($verbose);
126 $masterrev = find_branch_id
($gittree, $branch);
127 die "No head found for ${branch}" if ($masterrev eq "");
129 my $oldmasterrev = find_branch_id
($gittree, $syncname);
131 if ($oldmasterrev ne "") {
133 die "no $svntree, but incremental (have synctag) ?\n".
134 "(\"cd $gittree && git tag -d $syncname\" to remove)"
135 unless ( -d
$svntree);
137 if (${oldmasterrev
} eq $masterrev) {
138 print STDERR
"nothing to sync, $syncname matches $branch\n"
143 $fexport = "$oldmasterrev..$masterrev";
145 $fexport="$masterrev";
147 system("svnadmin create ./$svntree") unless (-d
$svntree);
157 # pick first dir, create, take next dir, continue until we reached basename
158 while ($path =~ m@
^([^/]+)/(.*)$@
) {
159 my $first = $base . $1;
161 $base = $first . "/";
162 next if ($paths{$first});
166 printf OUT
"Node-path: $first\n";
167 printf OUT
"Node-kind: dir\n";
168 printf OUT
"Node-action: add\n";
169 printf OUT
"Prop-content-length: 0\n";
170 printf OUT
"Content-length: 0\n";
184 # This is to allow setting props for a path, XXX add configuration
185 # file/options for this.
191 ### given path, return prop("prop", "value")
199 $result = GetOptions
("git-branch=s" => \
$branch,
200 "svn-prefix=s" => \
$basedir,
201 "keep-logs" => \
$keeplogs,
202 "no-load" => \
$no_load,
203 "ignore-author" => \
$ignore_author,
204 "verbose+" => \
$verbose,
205 "help" => \
$help) or pod2usage
(2);
207 pod2usage
(0) if ($help);
209 die "to few arguments" if ($#ARGV < 1);
211 mkdir ".data" unless (-d
".data");
213 die "cant find branch name" unless ($branch =~ m@
/?([^/]+)$@
);
214 my $shortbranch = $1;
216 my $gittree = $ARGV[0];
219 # create an identifier by replacing path separators
220 # (i.e. "/", ":" and "\") with underscores
221 my $svntree_id = $svntree;
222 $svntree_id =~ s/[\/:\\]/_
/g
;
224 my $gitdump = ".data/git.dump-${svntree_id}-${shortbranch}";
225 my $svndump = ".data/svn.dump-${svntree_id}-${shortbranch}";
226 my $log = ".data/log-${svntree_id}-${shortbranch}";
228 parse_git_tree
($gittree, $branch, $shortbranch);
232 parse_svn_tree
("file://" . $cwd ."/". $svntree);
236 print STDERR
"git fast-export $branch ($fexport)\n" if ($verbose);
238 system("(cd $gittree && git fast-export $fexport) > $gitdump 2>$log") == 0 or
239 die "git fast-export: $!";
241 open IN
, "$gitdump" or
242 die "failed to open $gitdump";
244 open OUT
, ">$svndump" or
245 die "failed to open $svndump";
247 print STDERR
"creating svn dump from revision $revision...\n" if ($verbose);
249 print OUT
"SVN-fs-dump-format-version: 3\n";
251 my $next = next_line
();
252 COMMAND
: while (!eof(IN
)) {
254 $next = next_line
($IN);
256 } elsif ($next =~ /^commit (.*)/) {
260 $next = next_line
($IN);
261 if ($next =~ m/mark +(.*)/) {
263 $next = next_line
($IN);
265 if ($next =~ m/author +(.+) +<([^>]+)> +(\d+) +[+-](\d+)$/) {
266 $commit{AuthorName
} = $1;
267 $commit{AuthorEmail
} = $2;
268 $commit{AuthorWhen
} = $3;
269 $commit{AuthorTZ
} = $4;
270 $next = next_line
($IN);
272 unless ($next =~ m/committer +(.+) +<([^>]+)> +(\d+) +[+-](\d+)$/) {
273 die "missing committer: $_";
276 $commit{CommitterName
} = $1;
277 $commit{CommitterEmail
} = $2;
278 $commit{CommitterWhen
} = $3;
279 $commit{CommitterTZ
} = $4;
281 $next = next_line
($IN);
282 my $log = read_data
($IN, $next);
284 $next = next_line
($IN);
285 if ($next =~ m/from (.*)/) {
287 $next = next_line
($IN);
289 if ($next =~ m/merge (.*)/) {
291 $next = next_line
($IN);
295 strftime
("%Y-%m-%dT%H:%M:%S.000000Z",
296 gmtime($commit{CommitterWhen
}));
298 my $author = "git2svn-dump";
299 if ($commit{CommitterEmail
} =~ m/([^@]+)/) {
302 unless ($ignore_author) {
303 if ($commit{AuthorEmail
} =~ m/([^@]+)/) {
309 $props .= prop
("svn:author", $author);
310 $props .= prop
("svn:log", $log);
311 $props .= prop
("svn:date", $date);
312 $props .= "PROPS-END";
316 printf OUT
"Revision-number: $revision\n"; $revision++;
317 printf OUT
"Prop-content-length: ". length($props) . "\n";
318 printf OUT
"Content-length: " . length($props) . "\n";
320 print OUT
"$props\n";
323 if ($next =~ m/M (\d+) (\S+) (.*)$/) {
324 my ($mode, $dataref, $path) = (oct $1, $2, $3);
326 if ($dataref eq "inline") {
327 $next = next_line
($IN);
328 $content = read_data
($IN, $next);
330 if (defined $blob{$dataref}) {
331 $content = $blob{$dataref};
333 # Submodules cannot be converted
334 print STDERR
"Ignored line, please check if this is a submodule: $next\n" if ($verbose);
335 $next = next_line
($IN);
338 # here we really want to delete $blob{$dataref},
339 # but it might be referenced in the future. To
340 # avoid keepig everything in memory for larger
341 # repositories this must be written out to disk
342 # and removed when done.
345 $path = "$basedir/$path";
350 my $type = $mode & 0777000;
353 if (($paths{$path} != 2) && ($type != 0120000)) {
354 die "file was a dir";
355 } elsif (($paths{$path} != 2) && ($type == 0120000)) {
356 print STDERR
"Dir is now a symlink, deleting: $path\n" if ($verbose);
357 foreach ( keys( %paths ) ) {
358 delete $paths{$_} if ( /^$path\// );
360 # This is now a file and not a path anymore
362 printf OUT
"Node-path: $path\nNode-action: delete\n\n";
372 $kind = "file" if ($type == 0100000);
373 $kind = "symlink" if ($type == 0120000);
374 die "$type unknown" if ($kind eq "");
377 $props .= prop
("svn:executable", "on") if ($mode & 0111);
378 $props .= prop
("svn:special", "*") if ($kind eq "symlink");
379 $props .= auto_props
($path);
380 $props .= "PROPS-END\n" if ($props ne "");
382 $content = "link $content\n" if ($kind eq "symlink");
384 my $plen = length($props);
385 my $clen = length($content);
387 printf OUT
"Node-path: $path\n";
388 printf OUT
"Node-kind: file\n";
389 printf OUT
"Node-action: $action\n";
390 printf OUT
"Text-content-length: $clen\n";
391 printf OUT
"Content-length: " . ($clen + $plen) . "\n";
392 printf OUT
"Prop-content-length: $plen\n" if ($plen);
395 print OUT
"$props" if ($plen);
399 } elsif ($next =~ m/D (.*)/) {
400 my $path = $basedir . "/". $1;
403 delete $paths{$path};
404 foreach ( keys( %paths ) ) {
405 delete $paths{$_} if ( /^$path\// );
408 printf OUT
"Node-path: $path\n";
409 printf OUT
"Node-action: delete\n";
412 print STDERR
"deleting non existing object: $path\n";
415 } elsif ($next =~ m/^C (.*)/) {
417 } elsif ($next =~ m/^R (.*)/) {
419 } elsif ($next =~ m/^filedeleteall$/) {
420 die "file delete all ?";
424 $next = next_line
($IN);
427 } elsif ($next =~ /^tag .*/) {
428 } elsif ($next =~ /^reset .*/) {
429 } elsif ($next =~ /^blob/) {
431 $next = next_line
($IN);
432 if ($next =~ m/mark (.*)/) {
434 $next = next_line
($IN);
436 my $data = read_data
($IN, $next);
437 $blob{$mark} = $data if (defined $mark);
438 } elsif ($next =~ /^checkpoint .*/) {
439 } elsif ($next =~ /^progress (.*)/) {
440 print STDERR
"progress: $1\n" if ($verbose);
442 die "unknown command $next";
444 $next = next_line
($IN);
450 print STDERR
"...dumped to revision $revision\n" if ($verbose);
451 print STDERR
"(re-)setting sync-tag to new master\n" if ($verbose);
454 system("cd $gittree && ".
455 "git tag -m \"sync $(date)\" -a -f ${syncname} ${masterrev}");
458 print STDERR
"loading dump into svn\n" if ($verbose);
461 system("svnadmin load $svntree < $svndump >>$log 2>&1") == 0 or
465 unlink $svndump, $gitdump, $log unless ($keeplogs);
474 B<git2svn> - converts a git branch to a svn ditto
478 B<git2svn> [options] git-repro svn-repro
484 =item B<--git-branch>
486 The git branch to export. The default is branch is master.
488 =item B<--svn-prefix>
490 The svn prefix where the branch is import. The default is trunk to
491 match the default GIT branch (master).
495 Don't load the svn repository or update the syncpoint tagname.
497 =item B<--ignore-author>
499 Ignore "author" lines in the fast-import stream. Use "committer"
504 Don't delete the logs in $CWD/.data on success.
508 More verbose output, can be give more then once to increase the verbosity.
512 Print a brief help message and exits.
518 B<git2svn> will convert a git branch to a svn ditto, it also
519 support incremantal updates.
521 B<git2svn> takes a git fast-export dump and converts it into a
522 svn dump that is feed into svnadmin load.
524 B<git2svn> assumes its the only process that writes into the svn
525 repository. This is because of the race between getting the to svn
526 Revsion number from the svn, creating the dump with correct Revsions,
527 and do the svnadmin load.
529 B<git2svn> also support incremental updates from a git branch to
530 a svn reprositry. Its does this by setting a git tag
531 (git2svn-syncpoint-<branchname>) where the last update was pulled
534 B<git2svn> was created as a hack over a weekend to support a
535 smoother migration away from svn and allow users access to tools to
536 browse and search code (fisheye) and use anonymouns svn servers.
540 B<git2svn> ~/src/heimdal svn-repro
542 B<git2svn> --git-branch heimdal-1-0-branch \
543 --svn-prefix branches/heimdal-1-0-branch \
544 ~/src/heimdal svn-repro
548 B<git2svn> is avaible from repo.or.cz
550 git clone git://repo.or.cz/git2svn.git
554 Love Hörnquist Åstrand <lha@kth.se>
558 Send bug reports to lha@kth.se