generate
[git2svn.git] / git2svn
blobd509900772c83960b495037ea3e11670c4466a15
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);
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 if (m@/(.*)/$@) {
71 $type = 1;
72 $name = "$basedir/$1";
73 } else {
74 $type = 2;
75 $name = "$basedir/$_";
77 $paths{$name} = $type;
79 close SVN;
81 open(SVN, "svn info $url|") or die "failed to open svn info $url";
82 while (<SVN>) {
83 if (/^Revision: (\d+)/) {
84 $revision = $1 + 1;
85 last;
88 close SVN;
91 sub parse_git_tree
93 $masterrev= `cat ${gittree}/.git/refs/heads/${branch}`;
94 chomp($masterrev);
96 die "No head found for ${branch}" if ($masterrev eq "");
98 my $syncpoint="${gittree}/.git/refs/tags/${syncname}";
100 if (-f ${syncpoint}) {
101 my $oldmasterrev=`cat ${syncpoint}`;
102 chomp($oldmasterrev);
104 if (${oldmasterrev} eq ${masterrev}) {
105 print STDERR "nothing to sync\n" if ($verbose);
106 exit 0;
109 die "no $svntree, but incremental (have synctag) ?\n".
110 "(\"cd $gittree && git tag -d $syncname\" to remove)"
111 unless ( -d $svntree);
113 $fexport = "$oldmasterrev..$masterrev";
114 } else {
115 $fexport="${masterrev}";
117 system("svnadmin create ./$svntree") unless (-d $svntree);
122 sub checkdirs
124 my $path = shift;
125 my $base = "";
127 # pick first dir, create, take next dir, continue until we reached basename
128 while ($path =~ m@^([^/]+)/(.*)$@) {
129 my $first = $base . $1;
130 $path = $2;
131 $base = $first . "/";
132 next if ($paths{$first});
134 $paths{$first} = 1;
136 printf OUT "Node-path: $first\n";
137 printf OUT "Node-kind: dir\n";
138 printf OUT "Node-action: add\n";
139 printf OUT "Prop-content-length: 0\n";
140 printf OUT "Content-length: 0\n";
141 printf OUT "\n";
145 sub next_line
147 my $IN = shift;
148 my $next = <IN>;
149 chomp $next;
150 return $next;
153 $|= 1;
155 my $result;
156 $result = GetOptions ("git-branch=s" => \$branch,
157 "svn-prefix=s" => \$basedir,
158 "verbose+" => \$verbose,
159 "help" => \$help) or pod2usage(2);
161 pod2usage(0) if ($help);
163 die "to few arguments" if ($#ARGV < 1);
165 mkdir ".data" unless (-d ".data");
167 $syncname = "git2svn-syncpoint-${branch}";
169 my $gitdump = ".data/git.dump-${branch}";
170 my $svndump = ".data/svn.dump-${branch}";
171 my $log = ".data/log-${branch}";
173 $gittree = $ARGV[0];
174 $svntree = $ARGV[1];
176 parse_git_tree($gittree);
178 my $cwd = `pwd`;
179 chomp($cwd);
180 parse_svn_tree("file://" . $cwd ."/". $svntree);
182 system(">$log");
184 print STDERR "git fast-export $branch ($fexport)\n" if ($verbose);
186 system("(cd $gittree && git fast-export $fexport) > $gitdump 2>$log") == 0 or
187 die "git fast-export: $!";
189 open IN, "$gitdump" or
190 die "failed to open $gitdump";
192 open OUT, ">$svndump" or
193 die "failed to open $svndump";
195 print STDERR "creating svn dump from revision $revision...\n" if ($verbose);
197 print OUT "SVN-fs-dump-format-version: 3\n";
199 my $next = next_line();
200 COMMAND: while (!eof(IN)) {
201 my $mark = undef;
202 if ($next eq "") {
203 $next = next_line($IN);
204 next COMMAND;
205 } elsif ($next =~ /^commit (.*)/) {
207 my %commit;
209 $next = next_line($IN);
210 if ($next =~ m/mark (.*)/) {
211 $mark = $1;
212 $next = next_line($IN);
214 if ($next =~ m/author (.*)/) {
215 $commit{author} = $1;
216 $next = next_line($IN);
218 unless ($next =~ m/committer (.+) <([^>]+)> (\d+) \+(\d+)$/) {
219 die "missing comitter"
222 $commit{CommitterName} = $1;
223 $commit{CommitterEmail} = $2;
224 $commit{CommitterWhen} = $3;
225 $commit{CommitterTZ} = $4;
227 $next = next_line($IN);
228 my $log = read_data($IN, $next);
230 $next = next_line($IN);
231 if ($next =~ m/from (.*)/) {
232 $next = next_line($IN);
234 if ($next =~ m/merge (.*)/) {
235 $next = next_line($IN);
238 my $date =
239 strftime("%Y-%m-%dT%H:%M:%S.000000Z",
240 gmtime($commit{CommitterWhen}));
242 my $author = "(no author)";
243 if ($commit{CommitterEmail} =~ m/([^@]+)/) {
244 $author = $1;
246 $author = "git2svn-dump" if ($author eq "(no author)");
248 my $props = "";
249 $props .= prop("svn:author", $author);
250 $props .= prop("svn:log", $log);
251 $props .= prop("svn:date", $date);
252 $props .= "PROPS-END";
254 # push out svn info
256 printf OUT "Revision-number: $revision\n"; $revision++;
257 printf OUT "Prop-content-length: ". length($props) . "\n";
258 printf OUT "Content-length: " . length($props) . "\n";
259 printf OUT "\n";
260 print OUT "$props\n";
262 while (1) {
263 if ($next =~ m/M (\d+) (\S+) (.*)$/) {
264 my ($mode, $dataref, $path) = (oct $1, $2, $3);
265 my $content;
266 if ($dataref eq "inline") {
267 $next = next_line($IN);
268 $content = read_data($IN, $next);
269 } else {
270 die "Revision missing content ref $dataref"
271 unless(defined $blob{$dataref});
273 $content = $blob{$dataref};
274 # here we really want to delete $blob{$dataref},
275 # but it might be referenced in the future. To
276 # avoid keepig everything in memory for larger
277 # repositories this must be written out to disk
278 # and removed when done.
281 checkdirs("$basedir/$path");
283 my $action = "add";
285 if ($paths{$path}) {
286 die "file was a dir" if ($paths{$path} != 2);
287 $action = "change";
288 } else {
289 $paths{$path} = 2;
293 my $type = $mode & 0777000;
294 my $kind = "";
295 $kind = "file" if ($type == 0100000);
296 $kind = "symlink" if ($type == 0120000);
297 die "$type unknown" if ($kind eq "");
299 $props = "";
300 $props .= prop("svn:executable", "on") if ($mode & 0111);
301 $props .= "PROPS-END" if ($props ne "");
303 my $plen = length($props);
304 my $clen = length($content);
306 printf OUT "Node-path: $basedir/$path\n";
307 printf OUT "Node-kind: $kind\n";
308 printf OUT "Node-action: $action\n";
309 printf OUT "Text-content-length: $clen\n";
310 printf OUT "Content-length: " . ($clen + $plen) . "\n";
311 printf OUT "Prop-content-length: $plen\n" if ($plen);
312 printf OUT "\n";
314 print OUT "$props\n" if ($plen);
316 print OUT $content;
317 printf OUT "\n";
318 } elsif ($next =~ m/D (.*)/) {
319 my $path = $1;
321 die "deleting non existing object" unless ($paths{$path});
323 delete $paths{$path};
325 printf OUT "Node-path: $basedir/$path\n";
326 printf OUT "Node-action: delete\n";
327 printf OUT "\n";
329 } elsif ($next =~ m/^C (.*)/) {
330 die "file copy ?";
331 } elsif ($next =~ m/^R (.*)/) {
332 die "file rename ?";
333 } elsif ($next =~ m/^filedeleteall$/) {
334 die "file delete all ?";
335 } else {
336 next COMMAND;
338 $next = next_line($IN);
341 } elsif ($next =~ /^tag .*/) {
342 } elsif ($next =~ /^reset .*/) {
343 } elsif ($next =~ /^blob/) {
344 $next = next_line($IN);
345 if ($next =~ m/mark (.*)/) {
346 $mark = $1;
347 $next = next_line($IN);
349 my $data = read_data($IN, $next);
350 $blob{$mark} = $data if (defined $mark);
351 } elsif ($next =~ /^checkpoint .*/) {
352 } elsif ($next =~ /^progress (.*)/) {
353 print STDERR "progress: $1\n" if ($verbose);
354 } else {
355 die "unknown command $next";
357 $next = next_line($IN);
360 close IN;
361 close OUT;
363 print STDERR "(re-)setting sync-tag to new master\n" if ($verbose);
365 system("cd $gittree && ".
366 "git tag -m \"sync $(date)\" -a -f ${syncname} ${masterrev}");
368 print STDERR "loading dump into svn\n" if ($verbose);
370 system("svnadmin load $svntree < $svndump >>$log 2>&1") == 0 or
371 die "svnadmin load";
373 unlink $svndump, $gitdump, $log;
375 exit 0;
378 __END__
380 =head1 NAME
382 git2svn - converts a git branch to a svn ditto
384 =head1 SYNOPSIS
386 git2svn [options] git-repro svn-repro
388 =head1 OPTIONS
390 =over 8
392 =item B<-git-branch>
394 The git branch to export. The default is branch is master.
396 =item B<-svn-prefix>
398 The svn prefix where the branch is import. The default is trunk to
399 match the default GIT branch (master).
401 =item B<-verbose>
403 More verbose output, can be give more then once to increase the verbosity.
405 =item B<-help>
407 Print a brief help message and exits.
409 =back
411 =head1 DESCRIPTION
413 B<This program> will convert a git branch to a svn ditto, it also
414 support incremantal updates.
416 git2svn takes a git fast-export dump and converts it into a svn dump
417 that is feed into svnadmin load.
419 git2svn assumes its the only process that writes into the svn
420 repository. This is because of the race between getting the to svn
421 Revsion number from the svn, creating the dump with correct Revsions,
422 and do the svnadmin load.
424 git2svn also support incremental updates from a git branch to a svn
425 reprositry. Its does this by setting a git tag
426 (git2svn-syncpoint-<branchname>) where the last update was pulled from.
428 =head1 EXAMPLES
430 ./git2svn ~/src/heimdal svn-repro
431 ./git2svn --git-branch heimdal-1-0-branch \
432 --svn-prefix branches/heimdal-1-0-branch \
433 ~/src/heimdal svn-repro
435 =head1 DOWNLOAD
437 git2svn is avaible from repo.or.cz
439 git clone git://repo.or.cz/git2svn.git
441 =cut