Make incremental support work again.
[git2svn.git] / git2svn
blob58d6c147993909d4951632b6bdc2f948d8b3dcc4
1 #!/usr/bin/perl
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/>.
19 use strict;
20 use POSIX qw(strftime);
21 use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
22 use Pod::Usage;
24 my $IN;
25 my $OUT;
27 my ($help, $verbose, $no_unlink, $no_load);
29 # svn
30 my $svntree = "repro";
31 my $basedir = "trunk";
32 my $revision = 1;
34 # git
35 my $branch = "master";
36 my $gittree;
37 my $syncname;
38 my $masterrev;
39 my $fexport;
42 my %blob;
43 my %paths;
45 sub read_data
47 my ($IN, $next, $length, $data, $l) = (shift, shift);
48 unless($next =~ m/^data (\d+)/) { die "missing data: $next" ; }
49 $length = $1;
51 $l = read(IN, $data, $length);
52 unless ($l == $length) { die "failed to read data $l != $length"; }
53 $data = "" if ($length == 0);
54 return $data;
57 sub prop
59 my ($key, $value) = (shift, shift);
60 "K " . length($key) . "\n$key\nV " . length($value) . "\n$value\n";
63 sub parse_svn_tree
65 my $url = shift;
66 my ($SVN, $type, $name);
68 open(SVN, "svn ls -R $url|") or die "failed to open svn ls -R $url";
69 while (<SVN>) {
70 chomp;
71 if (m@/?(.*)/$@) {
72 $type = 1;
73 $name = "$1";
74 } else {
75 $type = 2;
76 $name = "$_";
78 $paths{$name} = $type;
80 close SVN;
82 open(SVN, "svn info $url|") or die "failed to open svn info $url";
83 while (<SVN>) {
84 chomp;
85 if (/^Revision: (\d+)/) {
86 $revision = $1 + 1;
87 last;
90 close SVN;
93 sub find_branch_id
95 my $name = shift;
97 foreach my $m ("heads", "remotes") {
98 my $n = "${gittree}/.git/refs/${m}/${name}";
99 my $ID;
100 next unless (-f $n);
101 open FOO, "$n";
102 my $id = <FOO>;
103 chomp($id);
104 close FOO;
105 return $id;
107 return undef;
110 sub parse_git_tree
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);
126 exit 0;
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";
134 } else {
135 $fexport="${masterrev}";
137 system("svnadmin create ./$svntree") unless (-d $svntree);
142 sub checkdirs
144 my $path = shift;
145 my $base = "";
147 # pick first dir, create, take next dir, continue until we reached basename
148 while ($path =~ m@^([^/]+)/(.*)$@) {
149 my $first = $base . $1;
150 $path = $2;
151 $base = $first . "/";
152 next if ($paths{$first});
154 $paths{$first} = 1;
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";
161 printf OUT "\n";
165 sub next_line
167 my $IN = shift;
168 my $next = <IN>;
169 chomp $next;
170 return $next;
173 $|= 1;
175 my $result;
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}";
200 $gittree = $ARGV[0];
201 $svntree = $ARGV[1];
203 parse_git_tree($gittree);
205 my $cwd = `pwd`;
206 chomp($cwd);
207 parse_svn_tree("file://" . $cwd ."/". $svntree);
209 system(">$log");
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)) {
228 my $mark = undef;
229 if ($next eq "") {
230 $next = next_line($IN);
231 next COMMAND;
232 } elsif ($next =~ /^commit (.*)/) {
234 my %commit;
236 $next = next_line($IN);
237 if ($next =~ m/mark +(.*)/) {
238 $mark = $1;
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);
265 my $date =
266 strftime("%Y-%m-%dT%H:%M:%S.000000Z",
267 gmtime($commit{CommitterWhen}));
269 my $author = "(no author)";
270 if ($commit{CommitterEmail} =~ m/([^@]+)/) {
271 $author = $1;
273 $author = "git2svn-dump" if ($author eq "(no author)");
275 my $props = "";
276 $props .= prop("svn:author", $author);
277 $props .= prop("svn:log", $log);
278 $props .= prop("svn:date", $date);
279 $props .= "PROPS-END";
281 # push out svn info
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";
286 printf OUT "\n";
287 print OUT "$props\n";
289 while (1) {
290 if ($next =~ m/M (\d+) (\S+) (.*)$/) {
291 my ($mode, $dataref, $path) = (oct $1, $2, $3);
292 my $content;
293 if ($dataref eq "inline") {
294 $next = next_line($IN);
295 $content = read_data($IN, $next);
296 } else {
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";
309 checkdirs($path);
311 my $action = "add";
313 if ($paths{$path}) {
314 die "file was a dir" if ($paths{$path} != 2);
315 $action = "change";
316 } else {
317 $paths{$path} = 2;
321 my $type = $mode & 0777000;
322 my $kind = "";
323 $kind = "file" if ($type == 0100000);
324 $kind = "symlink" if ($type == 0120000);
325 die "$type unknown" if ($kind eq "");
327 $props = "";
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);
343 printf OUT "\n";
345 print OUT "$props\n" if ($plen);
347 print OUT $content;
348 printf OUT "\n";
349 } elsif ($next =~ m/D (.*)/) {
350 my $path = $basedir . "/". $1;
352 if ($paths{$path}) {
353 delete $paths{$path};
355 printf OUT "Node-path: $path\n";
356 printf OUT "Node-action: delete\n";
357 printf OUT "\n";
358 } elsif ($verbose) {
359 print STDERR "deleting non existing object: $path\n";
362 } elsif ($next =~ m/^C (.*)/) {
363 die "file copy ?";
364 } elsif ($next =~ m/^R (.*)/) {
365 die "file rename ?";
366 } elsif ($next =~ m/^filedeleteall$/) {
367 die "file delete all ?";
368 } else {
369 next COMMAND;
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 (.*)/) {
379 $mark = $1;
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);
387 } else {
388 die "unknown command $next";
390 $next = next_line($IN);
393 close IN;
394 close OUT;
396 print STDERR "(re-)setting sync-tag to new master\n" if ($verbose);
398 unless ($no_load) {
399 system("cd $gittree && ".
400 "git tag -m \"sync $(date)\" -a -f ${syncname} ${masterrev}");
403 print STDERR "loading dump into svn\n" if ($verbose);
405 unless ($no_load) {
406 system("svnadmin load $svntree < $svndump >>$log 2>&1") == 0 or
407 die "svnadmin load";
410 unlink $svndump, $gitdump, $log unless ($no_unlink);
412 exit 0;
415 __END__
417 =head1 NAME
419 git2svn - converts a git branch to a svn ditto
421 =head1 SYNOPSIS
423 git2svn [options] git-repro svn-repro
425 =head1 OPTIONS
427 =over 8
429 =item B<-git-branch>
431 The git branch to export. The default is branch is master.
433 =item B<-svn-prefix>
435 The svn prefix where the branch is import. The default is trunk to
436 match the default GIT branch (master).
438 =item B<-verbose>
440 More verbose output, can be give more then once to increase the verbosity.
442 =item B<-help>
444 Print a brief help message and exits.
446 =back
448 =head1 DESCRIPTION
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.
465 =head1 EXAMPLES
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
472 =head1 DOWNLOAD
474 git2svn is avaible from repo.or.cz
476 git clone git://repo.or.cz/git2svn.git
478 =head1 AUTHORS
480 Love Hörnquist Åstrand <lha@kth.se>
482 =head1 BUGS
484 Send bug reports to lha@kth.se
486 =cut