diff --git a/README.md b/README.md index 16e6a8200ac2a9b38bc460775ab07fbb12d61247..2771a657463bf51f31adc485eaf7980be9e37546 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,9 @@ hairy problem of migrating issues or even issue numbers and whatever embedded in Wiki text. Right now, synchronizing stuff that are related to users work. +Wiki migration seems to work now too. -The next step is to attack the Wiki. +The next step is are Wiki-Attachments. Why Perl? I'm not fluent enough in Ruby to even consider it as the tool of choice for this problem. The script directly talks with @@ -20,3 +21,8 @@ the MySQL databases of the Redmine instances, it basically ignores the API. +NOTES + +The project's entry in 'wikis' whould be added to syncs by hand +since Redmine creates the Wiki but the script currently doesn't +check for that, it only looks at the syncs table. diff --git a/lib/Redmine/DB/CTX.pm b/lib/Redmine/DB/CTX.pm index 7b67c10d05ffebab0ec73666919c6b890fafeba0..f364c6eddd19621d0c014ccec9e8c1711fceafe6 100644 --- a/lib/Redmine/DB/CTX.pm +++ b/lib/Redmine/DB/CTX.pm @@ -6,8 +6,10 @@ =head1 DESCRIPTION This implements what I call a "Redmine synchronisation context". -It has a synchronisation context id (sync_conext_id), a source (src) -and a destination (dst) which resemble database connections. +It has a synchronisation context id (sync_context_id), a source (src) +and a destination (dst) which resemble database connections. During +the synchronisation, this structure picks up a lot more of transient +information. =head1 SYNOPSIS @@ -20,6 +22,8 @@ package Redmine::DB::CTX; use strict; use parent 'Redmine::DB'; +use Data::Dumper; + =head2 $context->sync_project ($source_project_id, $destination_project_id) sync one project @@ -33,7 +37,8 @@ sub sync_project my $dp_id= shift; # $ctx->sync_project_members ($sp_id, $dp_id); - $ctx->sync_project_user_preferences ($sp_id, $dp_id); + # $ctx->sync_project_user_preferences ($sp_id, $dp_id); + $ctx->sync_wiki ($sp_id, $dp_id); } =head1 TRANSLATION @@ -92,7 +97,7 @@ sub init_translation print "NOTE: loading syncs\n"; $t= $ctx->{'tlt'}= {}; my $d= $ctx->{'dst'}->get_all_x ('syncs', [ 'sync_context_id=?', $ctx->{'ctx_id'} ] ); - # print "d: ", main::Dumper ($d); + # print "d: ", Dumper ($d); foreach my $id (keys %$d) { @@ -128,9 +133,11 @@ sub translate if (exists ($t->{$table_name}->{$src_id})) { my $x= $t->{$table_name}->{$src_id}; - # TODO: if verbosity ... print "TRANSLATE: table_name=[$table_name] src_id=[$src_id] tlt=[",join(',',@$x),"]\n"; + # TODO: if verbosity ... + print "TRANSLATE: table_name=[$table_name] src_id=[$src_id] tlt=[",join(',',@$x),"]\n"; return (wantarray) ? @$x : $x->[0]; } + print "TRANSLATE: table_name=[$table_name] src_id=[$src_id] tlt=undef\n"; return undef; } @@ -197,7 +204,7 @@ sub sync_project_members my $s_pcx= $src->pcx_members ($sp_id); my $d_pcx= $dst->pcx_members ($dp_id); # print "keys src: ", join (' ', keys %$src), "\n"; - # print "pcx: ", main::Dumper ($pcx); + # print "pcx: ", Dumper ($pcx); my ($s_members, $s_users)= map { $s_pcx->{$_} } qw(members users); my ($d_members, $d_users)= map { $d_pcx->{$_} } qw(members users); @@ -226,7 +233,7 @@ would not really be an issue. my $s_user= $s_users->{$s_user_id}; # next unless ($s_user->{'type'} eq 'Group'); - print "s_member: ", main::Dumper ($s_member); + print "s_member: ", Dumper ($s_member); my $d_user_id= $ctx->sync_user ($s_user_id, $s_user); my ($d_member_id, $d_status, $d_sync_date)= $ctx->translate ('members', $s_member_id); @@ -269,7 +276,7 @@ inherited_from points back to member_roles.id and that record might not be synce my $in= 'member_id IN ('. join(',', map { '?' } @s_member_ids) . ')'; my $s_mr_hash= $src->get_all_x ('member_roles', [ $in, @s_member_ids ]); - # print "s_mr_hash: ", main::Dumper ($s_mr_hash); + # print "s_mr_hash: ", Dumper ($s_mr_hash); my @s_mr_ids= sort { $a <=> $b } keys %$s_mr_hash; # maybe ordering helps print "s_mr_ids: [", join (',', @s_mr_ids), "]\n"; print "\n\n", '='x72, "MEMBER_ROLE processing\n", '-'x72, "\n"; @@ -277,7 +284,7 @@ inherited_from points back to member_roles.id and that record might not be synce { my $s_mr_id= shift @s_mr_ids; my $s_mr= $s_mr_hash->{$s_mr_id}; - print "member_role: ", main::Dumper ($s_mr); + print "member_role: ", Dumper ($s_mr); my $d_mr_id= $ctx->translate('member_roles', $s_mr_id); if (defined ($d_mr_id)) @@ -294,7 +301,7 @@ inherited_from points back to member_roles.id and that record might not be synce # users can inherit their roles from a group; # inherited_from is that group's id from the member_roles-table my $s_inh_from= $s_mr->{'inherited_from'}; - print "s_inh_from=[$s_inh_from] s_mr: ", main::Dumper ($s_mr); + print "s_inh_from=[$s_inh_from] s_mr: ", Dumper ($s_mr); my $d_inh_from; if (defined ($s_inh_from)) @@ -322,7 +329,7 @@ inherited_from points back to member_roles.id and that record might not be synce ); $d_mr{'inherited_from'}= $d_inh_from if (defined ($d_inh_from)); - print "new member_role record: ", main::Dumper (\%d_mr); + print "new member_role record: ", Dumper (\%d_mr); $d_mr_id= $ctx->{'dst'}->insert ('member_roles', \%d_mr); $ctx->store_translation('member_roles', $s_mr_id, $d_mr_id); @@ -337,7 +344,7 @@ sub sync_role my $res= $ctx->{'src'}->get_all_x ('roles', [ 'id=?', $s_role_id ]); return undef unless (defined ($res)); - print "sync_role: s_role_id=[$s_role_id] res: ", main::Dumper ($res); + print "sync_role: s_role_id=[$s_role_id] res: ", Dumper ($res); my $s_role= $res->{$s_role_id}; my %d_role= %$s_role; @@ -363,7 +370,7 @@ sub sync_user $s_user= $res->{$s_user_id}; } - print "s_user: ", main::Dumper ($s_user); + print "s_user: ", Dumper ($s_user); my ($d_user_id, $d_status, $d_sync_date)= $ctx->translate ('users', $s_user_id); print "s_user_id=[$s_user_id] d_user_id=[$d_user_id] d_status=[$d_status] d_sync_date=[$d_sync_date]\n"; @@ -371,7 +378,7 @@ sub sync_user unless (defined ($d_user_id)) { my $d_user= $ctx->clone_user ($s_user); - print "cloned_user: ", main::Dumper ($d_user); + print "cloned_user: ", Dumper ($d_user); $d_user_id= $ctx->{'dst'}->insert ('users', $d_user); $ctx->store_translation('users', $s_user_id, $d_user_id); @@ -435,11 +442,11 @@ sub sync_project_user_preferences } # second: see if users with the translated user_id are already on the destination and link their preferences into %d_uids - # print "user_id mapping: ", main::Dumper (\%s_uids); + # print "user_id mapping: ", Dumper (\%s_uids); my @tlt_uids= map { $s_uids{$_} } keys %s_uids; # verbose Redmine::DB::MySQL (1); my $d_pref= $dst->get_all_x ('user_preferences', [ 'user_id in ('.join(',',map { '?' } @tlt_uids).')', @tlt_uids ]); - # print "translated preferences: ", main::Dumper ($d_pref); + # print "translated preferences: ", Dumper ($d_pref); foreach my $d_id (keys %$d_pref) { my $x= $d_pref->{$d_id}; @@ -451,7 +458,7 @@ sub sync_project_user_preferences { my $x= $d_uids{$d_uid}; print '-'x72, "\n"; - print "d_uid=[$d_uid] ", main::Dumper ($x); + print "d_uid=[$d_uid] ", Dumper ($x); if (defined ($x->[1])) { @@ -461,12 +468,12 @@ sub sync_project_user_preferences { # no prefs record yet, copy it my %d_prefs= %{$s_up->{$x->[0]}}; - print "prefs on source: ", main::Dumper (\%d_prefs); + print "prefs on source: ", Dumper (\%d_prefs); my $s_prefs_id= delete ($d_prefs{'id'}); $d_prefs{'user_id'}= $d_uid; - print "save new prefs: ", main::Dumper (\%d_prefs); + print "save new prefs: ", Dumper (\%d_prefs); my $d_prefs_id= $dst->insert ('user_preferences', \%d_prefs); # NOTE: do we need the translation at all? possibly not, but what the heck @@ -481,18 +488,88 @@ sub sync_project_user_preferences sub sync_wiki { + my $ctx= shift; + my $sp_id= shift; + my $dp_id= shift; -=begin comment + my ($src, $dst)= map { $ctx->{$_} } qw(src dst); + # my $st= $ctx->stats('wiki'); each table has it's own counters + + my $s_pcx= $src->pcx_wiki($sp_id); + print "s_pcx: (", join (',', sort keys %$s_pcx), ")\n"; + # print Dumper ($s_pcx); exit; + + # NOTE: Let's assume that the destination does not receive pages from + # somewhere else (e.g. someone adding that by hand) + $ctx->sync_generic_table ($s_pcx, 'wikis', [ [ 'project_id' => 'projects' ] ]); + $ctx->sync_generic_table ($s_pcx, 'wiki_pages', [ [ 'wiki_id' => 'wikis' ], [ 'parent_id' => 'wiki_pages' ] ]); + $ctx->sync_generic_table ($s_pcx, 'wiki_redirects', [ [ 'wiki_id' => 'wikis' ] ]); + $ctx->sync_generic_table ($s_pcx, 'wiki_contents', [ [ 'page_id' => 'wiki_pages' ], ['author_id' => 'users' ] ]); + $ctx->sync_generic_table ($s_pcx, 'wiki_content_versions', [ [ 'wiki_content_id' => 'wiki_contents'], [ 'page_id' => 'wiki_pages' ], ['author_id' => 'users' ] ]); +} -sync wiki - # my $pcx= $src->pcx_wiki ($proj_id); +sub sync_generic_table +{ + my $ctx= shift; + my $s_pcx= shift; + my $table_name= shift; + my $tlt= shift; # list pairs - # print "pcx: ", main::Dumper ($pcx); - # print "src: ", main::Dumper ($src); + print '-'x72, "\n"; + print "sync_generic_table: table_name=[$table_name]\n"; + my $table= $s_pcx->{$table_name}; + # print "table [$table_name] ", Dumper ($table); exit; -=end comment -=cut + my $cnt= $ctx->stats($table_name); + my @s_ids= sort { $a <=> $b} keys %$table; # maybe sorting helps to bring order into an hierarchy + print "s_ids: ", join (',', @s_ids), "\n"; + ITEM: while (my $s_id= shift (@s_ids)) + { + my $d_id= $ctx->translate ($table_name, $s_id); + print "d_id=[$d_id]\n"; + $cnt->{'processed'}++; + + if (defined ($d_id)) + { + $cnt->{'unchanged'}++; + } + else + { + my %data= %{$table->{$s_id}}; + delete ($data{'id'}); + + # translate attributes (an) pointing to table (tn); $tlt is a list of pairs + TLT: foreach my $t (@$tlt) + { + my ($an, $tn)= @$t; + my $s_av= $data{$an}; + next TLT unless (defined ($s_av)); + my $d_av= $ctx->translate ($tn, $s_av); + + unless (defined ($d_av)) + { + if ($tn eq $table_name) + { # this is a self referential table, put the (yet unresolved) to the head of the queue + # TODO: this could lead to an endless loop! + unshift (@s_ids, $s_av); + push (@s_ids, $s_id); + next ITEM; + } + + print "ERROR: translation not known for an=[$an] s_av=[$s_av] in table=[$tn]\n"; + $cnt->{'av_tlt_missing'}++; + next TLT; + } + $data{$an}= $d_av; + } + + $d_id= $ctx->{'dst'}->insert ($table_name, \%data); + $ctx->store_translation($table_name, $s_id, $d_id); + $cnt->{'added'}++; + } + } + $cnt; } =head1 INTERNAL METHODS? @@ -506,7 +583,7 @@ sub stats my $t= $self->{'stats'}->{$what}; $t= $self->{'stats'}->{$what}= {} unless (defined ($t)); - # print "accessing stats=[$what]: ", main::Dumper($self); + # print "accessing stats=[$what]: ", Dumper($self); $t; } diff --git a/lib/Redmine/DB/MySQL.pm b/lib/Redmine/DB/MySQL.pm index d16e14658157efc213384c55a2c91665f67b1611..b5078549a26b769a2ddf801ea3f0d28b6c06088b 100644 --- a/lib/Redmine/DB/MySQL.pm +++ b/lib/Redmine/DB/MySQL.pm @@ -4,6 +4,8 @@ package Redmine::DB::MySQL; use strict; use parent 'Redmine::DB'; +use Data::Dumper; + # use Redmine::DB::Project; my $show_query= 0; @@ -34,7 +36,7 @@ sub table my $t= $self->{$table}; $t= $self->{$table}= {} unless (defined ($t)); - # print "accessing table=[$table]: ", main::Dumper($self); + # print "accessing table=[$table]: ", Dumper($self); $t; } @@ -48,14 +50,14 @@ sub get_all_x return undef unless (defined ($dbh)); # my $project= new Redmine::DB::Project (%par); - # print "project: ", main::Dumper ($project); + # print "project: ", Dumper ($project); my $ss= "SELECT * FROM $table"; my @v= (); if (defined ($where)) { - # print "where: ", main::Dumper ($where) if ($show_query); + # print "where: ", Dumper ($where) if ($show_query); $ss .= ' WHERE ' . shift (@$where); @v= @$where; } @@ -75,7 +77,7 @@ sub get_all_x while (defined (my $x= $sth->fetchrow_hashref())) { - print "x: ", main::Dumper ($x) if ($show_fetched); + print "x: ", Dumper ($x) if ($show_fetched); $t->{$x->{'id'}}= $x; } @@ -122,7 +124,7 @@ sub insert sub mysql { my $self= shift; - print "self: ", main::Dumper ($self); + print "self: ", Dumper ($self); my @cmd= ('mysql', '-h', $self->{'host'}, '-u', $self->{'username'}, $self->{'database'}, '--password='.$self->{'password'}); print ">> cmd=[", join (' ', @cmd), "]\n"; @@ -185,12 +187,12 @@ sub pcx_members $res->{'project'}= $proj; $res->{'members'}= $members; - # print "proj: ", main::Dumper($proj); + # print "proj: ", Dumper($proj); # -------------------------------------------------------------------- # check for members and users my $users= $self->table('users'); - # print "users: ", main::Dumper($users); + # print "users: ", Dumper($users); my @missing_users=(); foreach my $member_id (keys %$members) { @@ -207,6 +209,15 @@ sub pcx_members $res; } +=head2 $con->pcx_wiki ($project_id) + +retrieve data related to the Wiki + +Right now, we assume we can handle the amount of data returned, see +notes in the code. + +=cut + sub pcx_wiki { my $self= shift; @@ -226,14 +237,29 @@ sub pcx_wiki if (@wiki_ids > 1) { print "ATTN: too many(?) wikis for project=$proj_id "; - print main::Dumper ($wikis); + print Dumper ($wikis); } foreach my $wiki_id (@wiki_ids) { - my $wiki_pages= $self->get_all_x ('wiki_pages', [ 'wiki_id=?', $proj_id ]); - $res->{'wiki_pages'}->{$wiki_id}= $wiki_pages; - # print "wiki_id=[$wiki_id] wiki_pages: ", main::Dumper ($wiki_pages); + my $wiki_pages= $self->get_all_x ('wiki_pages', [ 'wiki_id=?', $wiki_id ]); + # $res->{'wiki_pages'}->{$wiki_id}= $wiki_pages; # one layer too many! + $res->{'wiki_pages'}= $wiki_pages; + # print "wiki_id=[$wiki_id] wiki_pages: ", Dumper ($wiki_pages); + + my $wiki_redirects= $self->get_all_x ('wiki_redirects', [ 'wiki_id=?', $wiki_id ]); + # $res->{'wiki_redirects'}->{$wiki_id}= $wiki_redirects; + $res->{'wiki_redirects'}= $wiki_redirects; + + # fetch the Wiki text + # TODO: for now, assume we can handle the amount of data returned; + # it might be necessary to introduce callbacks deal with the text + + my $sel= 'page_id IN (SELECT id FROM wiki_pages WHERE wiki_id=?)'; + my $wiki_contents= $self->get_all_x ('wiki_contents', [ $sel, $wiki_id ]); + my $wiki_content_versions= $self->get_all_x ('wiki_content_versions', [ $sel, $wiki_id ]); + $res->{'wiki_contents'}= $wiki_contents; + $res->{'wiki_content_versions'}= $wiki_content_versions; } }