From 03669d59ee98d609a039b7bace7bcf2a6e09cb13 Mon Sep 17 00:00:00 2001 From: Stefan Metzmacher Date: Thu, 20 Dec 2007 19:15:53 +0100 Subject: [PATCH] add code based on SVN and SVK perl bindings to get the full diffs out of subversion even for deleted subdirs metze --- SVN2GitEditor.pm | 316 ++++++++++++++++++++++++++++++++++++++++++++++ SVN2GitPatch.pm | 200 +++++++++++++++++++++++++++++ sync-v4-0-test.pl | 69 ++++++++++ 3 files changed, 585 insertions(+) create mode 100644 SVN2GitEditor.pm create mode 100644 SVN2GitPatch.pm create mode 100755 sync-v4-0-test.pl diff --git a/SVN2GitEditor.pm b/SVN2GitEditor.pm new file mode 100644 index 0000000..c3416c4 --- /dev/null +++ b/SVN2GitEditor.pm @@ -0,0 +1,316 @@ + +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 Eclkao@clkao.orgE + +=head1 COPYRIGHT + +Copyright 2003-2005 by Chia-liang Kao Eclkao@clkao.orgE. + +This program is free software; you can redistribute it and/or modify it +under the same terms as Perl itself. + +See L + +=cut + +1; diff --git a/SVN2GitPatch.pm b/SVN2GitPatch.pm new file mode 100644 index 0000000..d3f668a --- /dev/null +++ b/SVN2GitPatch.pm @@ -0,0 +1,200 @@ +#!/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; diff --git a/sync-v4-0-test.pl b/sync-v4-0-test.pl new file mode 100755 index 0000000..8aa68de --- /dev/null +++ b/sync-v4-0-test.pl @@ -0,0 +1,69 @@ +#!/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); +} -- 2.34.1