Add parsing of options.
[git2svn.git] / git2svn
blob5ec3af8cba5534509114c3421102396e571051f9
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 =head 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 =cut