--- /dev/null
+
+package SVN2GitEditor;
+use strict;
+
+require SVN::Delta;
+our @ISA = qw(SVN::Delta::Editor);
+
+use SVK::I18N;
+use SVK::XD;
+use autouse 'SVK::Util' => qw( slurp_fh tmpfile mimetype_is_text catfile );
+
+sub set_target_revision {
+ my ($self, $revision) = @_;
+}
+
+sub open_root {
+ my ($self, $baserev) = @_;
+ return '';
+}
+
+sub add_file {
+ my ($self, $path, $pdir, $from_path, $from_rev, $pool) = @_;
+ $self->{info}{$path}{fpool} = $pool;
+ if (defined $from_path) {
+ $self->{info}{$path}{from_path} = $from_path;
+ $self->{info}{$path}{copied} = 1;
+ } else {
+ $self->{info}{$path}{added} = 1;
+ }
+ return $path;
+}
+
+sub open_file {
+ my ($self, $path, $pdir, $rev, $pool) = @_;
+ $self->{info}{$path}{from_path} = $path;
+ $self->{info}{$path}{fpool} = $pool;
+
+ return $path;
+}
+
+sub apply_textdelta {
+ my ($self, $path, $checksum, $pool) = @_;
+ return unless $path;
+ my $info = $self->{info}{$path};
+ $info->{base} = $self->{cb_basecontent}($info->{from_path}, $info->{fpool})
+ unless $info->{added};
+
+ unless ($self->{external}) {
+ my $newtype = $info->{prop} && $info->{prop}{'svn:mime-type'};
+ my $is_text = !$newtype || mimetype_is_text ($newtype);
+ if ($is_text && !$info->{added}) {
+ my $basetype = $self->{cb_baseprop}->($info->{from_path}, 'svn:mime-type', $pool);
+ $is_text = !$basetype || mimetype_is_text ($basetype);
+ }
+ unless ($is_text) {
+ confess("Cannot display: file marked as a binary type.\n");
+ }
+ }
+ my $new;
+ if ($self->{external}) {
+ my $tmp = tmpfile ('diff');
+ slurp_fh ($info->{base}, $tmp)
+ if $info->{base};
+ seek $tmp, 0, 0;
+ $info->{base} = $tmp;
+ $info->{new} = $new = tmpfile ('diff');
+ }
+ else {
+ $info->{new} = '';
+ open $new, '>', \$info->{new};
+ }
+
+ return [SVN::TxDelta::apply ($info->{base}, $new,
+ undef, undef, $pool)];
+}
+
+sub close_file {
+ my ($self, $path, $checksum, $pool) = @_;
+ return unless $path;
+ my $info = $self->{info}{$path};
+
+ if (exists $info->{new}) {
+ no warnings 'uninitialized';
+ my $rpath = $self->{report} ? catfile($self->{report}, $path) : $path;
+ my @label = map { $self->{$_} || $self->{"cb_$_"}->($path) } qw/llabel rlabel/;
+ my $showpath = ($self->{lpath} ne $self->{rpath});
+ my @showpath = map { $showpath ? $self->{$_} : undef } qw/lpath rpath/;
+ if ($self->{external}) {
+ # XXX: the 2nd file could be - and save some disk IO
+ my @content = map { ($info->{$_}->filename) } qw/base new/;
+ @content = reverse @content if $self->{reverse};
+ (system (split (/ /, $self->{external}),
+ '-L', _full_label ($rpath, $showpath[0], $label[0]),
+ $content[0],
+ '-L', _full_label ($rpath, $showpath[1], $label[1]),
+ $content[1]) >= 0) or die loc("Could not run %1: %2", $self->{external}, $?);
+ }
+ else {
+ $info->{base} = '';
+ $info->{base} = $self->{cb_basecontent}($info->{from_path}, $info->{fpool})
+ unless $info->{added};
+ my @content = ($info->{base}, \$self->{info}{$path}{new});
+ @content = reverse @content if $self->{reverse};
+ $self->output_diff ($rpath, @label, @showpath, @content);
+ }
+ }
+
+# $self->output_prop_diff ($path, $pool);
+ delete $self->{info}{$path};
+}
+
+sub oldfilemode
+{
+ my ($self, $path) = @_;
+ my $pool = $self->{info}{$path}{fpool};
+ my $name = "svn:executable";
+
+ return undef if $self->{info}{$path}{added};
+
+ my $prop = $self->{cb_baseprop}->($path, $name, $pool);
+
+ return "100644" unless defined($prop);
+
+ return "100755" if (length($prop) > 0);
+
+ return "100644";
+}
+
+sub newfilemode
+{
+ my ($self, $path, $oldmode) = @_;
+ my $name = "svn:executable";
+
+ return undef if $self->{info}{$path}{deleted};
+
+ my $prop = $self->{info}{$path}{prop}{$name};
+
+ my $changed = 1;
+ $changed = 0 unless defined($prop);
+ $changed = 1 if $self->{info}{$path}{added};
+
+ return $oldmode unless $changed;
+
+ return "100755" if (length($prop) > 0);
+
+ return "100644";
+}
+
+sub output_diff {
+ my ($self, $path, $llabel, $rlabel, $lpath, $rpath) = splice(@_, 0, 6);
+ my $fh = $self->_output_fh;
+
+ my $ofile = $self->{info}{$path}{added} ? "/dev/null": "a/$path";
+ my $nfile = $self->{info}{$path}{deleted} ? "/dev/null": "b/$path";
+
+ my $osha1 = $self->{info}{$path}{added} ? "0000000": "1234567";
+ my $nsha1 = $self->{info}{$path}{deleted} ? "0000000": "7654321";
+
+ my $omode = $self->oldfilemode($path);
+ my $nmode = $self->newfilemode($path, $omode);
+
+ my $name = "";
+ my $mode = "";
+
+ if (defined($omode) and defined($nmode)) {
+ if ($omode ne $nmode) {
+ $mode = "old mode $omode\nnew mode $nmode\n";
+ }
+ } elsif (defined($omode)) {
+ $mode = "deleted file mode $omode\n";
+ } elsif (defined($nmode)) {
+ $mode = "new file mode $nmode\n";
+ }
+
+ print $fh (
+ "diff --git $ofile $nfile\n",
+ $name,
+ $mode,
+ "index $osha1..$nsha1\n"
+ );
+
+ unshift @_, $self->_output_fh;
+ push @_, $ofile, $nfile;
+
+ goto &{$self->can('_output_diff_content')};
+}
+
+# _output_diff_content($fh, $ltext, $rtext, $llabel, $rlabel)
+sub _output_diff_content {
+ my ($fh, $ltext, $rtext, $llabel, $rlabel) = @_;
+
+ my ($lfh, $lfn) = tmpfile ('diff');
+ my ($rfh, $rfn) = tmpfile ('diff');
+
+ slurp_fh ($ltext => $lfh); close ($lfh);
+ slurp_fh ($rtext => $rfh); close ($rfh);
+
+ my $diff = SVN::Core::diff_file_diff( $lfn, $rfn );
+
+ SVN::Core::diff_file_output_unified(
+ $fh, $diff, $lfn, $rfn, $llabel, $rlabel
+ );
+
+ unlink ($lfn, $rfn);
+}
+
+sub output_prop_diff {
+ my ($self, $path, $pool) = @_;
+ if ($self->{info}{$path}{prop}) {
+ my $rpath = $self->{report} ? catfile($self->{report}, $path) : $path;
+ $self->_print("\n", loc("Property changes on: %1\n", $rpath), ('_' x 67), "\n");
+ for (sort keys %{$self->{info}{$path}{prop}}) {
+ $self->_print(loc("Name: %1\n", $_));
+ my $baseprop;
+ $baseprop = $self->{cb_baseprop}->($path, $_, $pool)
+ unless $self->{info}{$path}{added};
+ my @args =
+ map \$_,
+ map { (length || /\n$/) ? "$_\n" : $_ }
+ ($baseprop||''), ($self->{info}{$path}{prop}{$_}||'');
+ @args = reverse @args if $self->{reverse};
+
+ my $diff = '';
+ open my $fh, '>', \$diff;
+ _output_diff_content($fh, @args, '', '');
+ $diff =~ s/.*\n.*\n//;
+ $diff =~ s/^\@.*\n//mg;
+ $diff =~ s/^/ /mg;
+ $self->_print($diff);
+ }
+ $self->_print("\n");
+ }
+}
+
+sub add_directory {
+ my ($self, $path, $pdir, @arg) = @_;
+# $self->{info}{$path}{added} = 1;
+ return $path;
+}
+
+sub open_directory {
+ my ($self, $path, $pdir, $rev, @arg) = @_;
+ return $path;
+}
+
+sub close_directory {
+ my ($self, $path, $pool) = @_;
+# $self->output_prop_diff ($path, $pool);
+ delete $self->{info}{$path};
+}
+
+sub delete_entry {
+ my ($self, $path, $revision, $pdir, $pool) = @_;
+
+ my $fullpath = "$self->{branch}/$path";
+
+ if ($self->{oldroot}->is_file($fullpath)) {
+ $self->{info}{$path}{from_path} = $path;
+ $self->{info}{$path}{deleted} = 1;
+ $self->{info}{$path}{fpool} = $pool;
+ $self->{info}{$path}{new} = '';
+ $self->close_file($path, undef, $pool);
+ return;
+ }
+
+ my $entries = $self->{oldroot}->dir_entries($fullpath);
+ foreach my $c (keys %{$entries}) {
+ $self->delete_entry("$path/$c", $revision, $path, $pool);
+ }
+}
+
+sub change_file_prop {
+ my ($self, $path, $name, $value) = @_;
+ $self->{info}{$path}{prop}{$name} = $value;
+}
+
+sub change_dir_prop {
+ my ($self, $path, $name, $value) = @_;
+}
+
+sub close_edit {
+ my ($self, @arg) = @_;
+}
+
+sub _print {
+ my $self = shift;
+ $self->{output} or return print @_;
+ ${ $self->{output} } .= $_ for @_;
+}
+
+sub _output_fh {
+ my $self = shift;
+
+ no strict 'refs';
+ $self->{output} or return \*{select()};
+
+ open my $fh, '>>', $self->{output};
+ return $fh;
+}
+
+=head1 AUTHORS
+
+Chia-liang Kao E<lt>clkao@clkao.orgE<gt>
+
+=head1 COPYRIGHT
+
+Copyright 2003-2005 by Chia-liang Kao E<lt>clkao@clkao.orgE<gt>.
+
+This program is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself.
+
+See L<http://www.perl.com/perl/misc/Artistic.html>
+
+=cut
+
+1;
--- /dev/null
+#!/usr/bin/perl
+#
+
+use strict;
+
+use util;
+
+use Time::Local;
+use Time::gmtime;
+
+use SVN::Core;
+use SVN::Repos;
+use SVN::Fs;
+use SVN::Delta;
+use SVN::Ra;
+use SVK::Editor::Diff;
+use SVN2GitEditor;
+
+package SVN2GitPatch;
+
+sub confess($)
+{
+ # TODO use confess from 'use Carp'
+ my ($str) = @_;
+ die($str);
+}
+
+sub new($$;$) {
+ my ($this, $repo_path, $authors_file) = @_;
+
+ my $self = undef;
+
+ $self->{repos} = SVN::Repos::open($repo_path);
+ $self->{authors_file} = $authors_file;
+ $self->{authors} = undef;
+
+ if (defined($self->{authors_file})) {
+ my $f = util::FileLoad($self->{authors_file});
+ my @lines = split("\n", $f);
+
+ foreach my $l (@lines) {
+ if ($l =~ /^([\w\-]+) = (.*)$/) {
+ $self->{authors}->{$1} = $2;
+ next;
+ }
+
+ confess "line: $l: invalid";
+ }
+ }
+
+ bless $self;
+ return $self;
+}
+
+sub get_last_svn_rev($$)
+{
+ my ($self, $branch) = @_;
+
+ return $self->{repos}->fs->youngest_rev();
+}
+
+sub get_missing_svn_revs($$$)
+{
+ my ($self, $branch, $start_rev) = @_;
+ my $last_rev = $self->get_last_svn_rev($branch);
+ my @ret = ();
+
+ my $nroot = $self->{repos}->fs->revision_root($last_rev);
+ my $hist = $nroot->node_history($branch);
+
+ while ($hist = $hist->prev(0)) {
+ my $crev = $hist->location();
+ last unless defined($crev);
+ last unless $crev > 0;
+ last unless $crev > $start_rev;
+
+ unshift(@ret, $crev);
+ }
+
+ return @ret;
+}
+
+sub svn2git_author($$)
+{
+ my ($self, $in) = @_;
+
+ my $out = $self->{authors}->{$in};
+
+ confess "author: $in:not found" unless defined($out);
+
+ return $out;
+}
+
+sub svn2git_date($$)
+{
+ my ($self, $in) = @_;
+
+
+ my $tmp = $in;
+ my $out = undef;
+
+ if ($tmp =~ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\./) {
+ my $year = $1;
+ my $month = $2-1;
+ my $day = $3;
+ my $hour = $4;
+ my $min = $5;
+ my $sec = $6;
+
+ use Time::Local;
+ use Time::gmtime;
+ my $time = timegm($sec, $min, $hour, $day, $month, $year);
+ $out = gmctime($time);
+ }
+
+ confess "time: $in: invalid" unless defined($out);
+
+ return $out;
+}
+
+sub svn2git_log($$)
+{
+ my ($self, $in) = @_;
+
+ my $out = $in;
+
+ return $out;
+}
+
+sub get_git_patch($$$)
+{
+ my ($self, $branch, $rev) = @_;
+ my $orev = $rev-1;
+ my $nrev = $rev;
+ my $oroot = $self->{repos}->fs->revision_root($orev);
+ my $nroot = $self->{repos}->fs->revision_root($nrev);
+
+ my $p = undef;
+
+ $p->{svn_author} = $self->{repos}->fs->revision_prop($nrev, "svn:author");
+ $p->{svn_date} = $self->{repos}->fs->revision_prop($nrev, "svn:date");
+ $p->{svn_log} = $self->{repos}->fs->revision_prop($nrev, "svn:log");
+ $p->{svn_rev} = $rev;
+
+ $p->{git_diff} = "";
+ my $editor = SVN2GitEditor->new(
+ cb_basecontent => sub {
+ my ($path, $pool) = @_;
+ my $base = $oroot->file_contents("$branch/$path", $pool);
+ return $base;
+ },
+ cb_baseprop => sub {
+ my ($path, $pname, $pool) = @_;
+ my $prop = $oroot->node_prop("$branch/$path", $pname, $pool);
+ return $prop;
+ },
+ oldrepos => $self->{repos},
+ oldroot => $oroot,
+ oldrev => $orev,
+ newrev => $nrev,
+ branch => $branch,
+ llabel => "revision $orev",
+ rlabel => "revision $nrev",
+ external => undef,
+ output => \$p->{git_diff});
+
+ SVN::Repos::dir_delta($oroot,
+ $branch, '',
+ $nroot,
+ $branch,
+ $editor,
+ undef, 1, 1, 1, 1);
+
+ $p->{git_author} = $self->svn2git_author($p->{svn_author});
+ $p->{git_date} = $self->svn2git_date($p->{svn_date});
+ $p->{git_log} = $self->svn2git_log($p->{svn_log});
+
+ my @log = split("\n", $p->{git_log});
+ $p->{git_log_subject} = shift @log;
+ $p->{git_log_body} = join("\n", @log);
+
+ if ($p->{git_diff} eq "") {
+ $p->{git_patch} = undef;
+ return $p;
+ }
+
+ $p->{git_patch} = "";
+ $p->{git_patch} .= "From 123456789abcdef\n";
+ $p->{git_patch} .= "From: $p->{git_author}\n";
+ $p->{git_patch} .= "Date: $p->{git_date}\n";
+ $p->{git_patch} .= "Subject: [PATCH] r$p->{svn_rev}: $p->{git_log_subject}\n";
+ $p->{git_patch} .= "$p->{git_log_body}\n";
+ $p->{git_patch} .= "\n";
+ $p->{git_patch} .= $p->{git_diff};
+ $p->{git_patch} .= "\n---\nsvn-sync script\n\n";
+
+ return $p;
+}
+
+1;
--- /dev/null
+#!/usr/bin/perl
+#
+
+use strict;
+
+use SVN2GitPatch;
+
+my $svn_repo_path = "/tmp/svn/samba.repo";
+my $authors_file = "svn-authors";
+
+my $svn_branch = "branches/SAMBA_4_0";
+my $svn_start_rev = 25600;
+my $basepath = "/tmp/svn/v4-0-test";
+
+my $patch_path = "$basepath/patches";
+my $last_svn_rev_file = "$patch_path/latest.svnrev";
+my $git_repo_path = "$basepath/git";
+
+$ENV{LANG} = "en_US.UTF-8";
+
+my $r = SVN2GitPatch->new($svn_repo_path, $authors_file);
+
+sub get_last_svn_rev($;$)
+{
+ my ($file, $default_rev) = @_;
+
+ $default_rev = 25600 if defined($default_rev);
+
+ my $v = util::FileLoad($file);
+
+ $v = $default_rev if $v eq "";
+ $v *= 1;
+ $v = $default_rev if $v == 0;
+
+ print "get_last_svn_rev: $v\n";
+ return $v;
+}
+
+sub set_last_svn_rev($$)
+{
+ my ($file, $rev) = @_;
+ util::FileSave($file, $rev);
+ print "set_last_svn_rev: $rev\n";
+}
+
+my $last_rev = $r->get_last_svn_rev($svn_branch);
+my $start_rev = get_last_svn_rev($last_svn_rev_file, $svn_start_rev);
+my @revs = $r->get_missing_svn_revs($svn_branch, $start_rev);
+
+print "start: $start_rev last: $last_rev\n";
+
+foreach my $rev (@revs) {
+
+ print "Get patch for rev: $rev\n";
+ my $p = $r->get_git_patch($svn_branch, $rev);
+
+ next unless defined($p->{git_patch});
+
+ my $patch_path = "$patch_path/$rev.patch";
+ open(PATCH, ">$patch_path") or die ("failed to $patch_path for write");
+ print PATCH $p->{git_patch}."\n";
+ close PATCH;
+
+ my $applycmd = "cd $git_repo_path && git am --whitespace=nowarn --binary $patch_path";
+
+ my $apply = `$applycmd` or die "$applycmd: failed";
+
+ set_last_svn_rev($last_svn_rev_file, $rev);
+}