[Rt-commit] rt branch, 4.4.1/serializer-importer-updates-4.4.1, created. rt-4.4.1-2-g1061f21

Jim Brandt jbrandt at bestpractical.com
Wed Dec 14 10:13:08 EST 2016


The branch, 4.4.1/serializer-importer-updates-4.4.1 has been created
        at  1061f217ffbdba540e3a0b8ad3d3456a72d59fe0 (commit)

- Log -----------------------------------------------------------------
commit 88c5d61ceb1ab03c69e43ed5c7e1374d90487295
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue Dec 13 11:50:21 2016 -0500

    Apply fixes and improvements from 4.2 work
    
    RT 4.2 had a series of updates applied via merge
    564b9de7. Pull them into 4.4.

diff --git a/lib/RT/Attribute.pm b/lib/RT/Attribute.pm
index 9943b57..eddd37e 100644
--- a/lib/RT/Attribute.pm
+++ b/lib/RT/Attribute.pm
@@ -632,6 +632,65 @@ sub FindDependencies {
 
     $self->SUPER::FindDependencies($walker, $deps);
     $deps->Add( out => $self->Object );
+
+    # dashboards in menu attribute has dependencies on each of its dashboards
+    if ($self->Name eq RT::User::_PrefName("DashboardsInMenu")) {
+        my $content = $self->Content;
+        for my $pane (values %{ $content || {} }) {
+            for my $dash_id (@$pane) {
+                my $attr = RT::Attribute->new($self->CurrentUser);
+                $attr->LoadById($dash_id);
+                $deps->Add( out => $attr );
+            }
+        }
+    }
+    # homepage settings attribute has dependencies on each of the searches in it
+    elsif ($self->Name eq RT::User::_PrefName("HomepageSettings")) {
+        my $content = $self->Content;
+        for my $pane (values %{ $content || {} }) {
+            for my $component (@$pane) {
+                # this hairy code mirrors what's in the saved search loader
+                # in /Elements/ShowSearch
+                if ($component->{type} eq 'saved') {
+                    if ($component->{name} =~ /^(.*?)-(\d+)-SavedSearch-(\d+)$/) {
+                        my $attr = RT::Attribute->new($self->CurrentUser);
+                        $attr->LoadById($3);
+                        $deps->Add( out => $attr );
+                    }
+                }
+                elsif ($component->{type} eq 'system') {
+                    my ($search) = RT::System->new($self->CurrentUser)->Attributes->Named( 'Search - ' . $component->{name} );
+                    unless ( $search && $search->Id ) {
+                        my (@custom_searches) = RT::System->new($self->CurrentUser)->Attributes->Named('SavedSearch');
+                        foreach my $custom (@custom_searches) {
+                            if ($custom->Description eq $component->{name}) { $search = $custom; last }
+                        }
+                    }
+                    $deps->Add( out => $search ) if $search;
+                }
+            }
+        }
+    }
+    # dashboards have dependencies on all the searches and dashboards they use
+    elsif ($self->Name eq 'Dashboard') {
+        my $content = $self->Content;
+        for my $pane (values %{ $content->{Panes} || {} }) {
+            for my $component (@$pane) {
+                if ($component->{portlet_type} eq 'search' || $component->{portlet_type} eq 'dashboard') {
+                    my $attr = RT::Attribute->new($self->CurrentUser);
+                    $attr->LoadById($component->{id});
+                    $deps->Add( out => $attr );
+                }
+            }
+        }
+    }
+    # each subscription depends on its dashboard
+    elsif ($self->Name eq 'Subscription') {
+        my $content = $self->Content;
+        my $attr = RT::Attribute->new($self->CurrentUser);
+        $attr->LoadById($content->{DashboardId});
+        $deps->Add( out => $attr );
+    }
 }
 
 sub PreInflate {
@@ -640,11 +699,220 @@ sub PreInflate {
 
     if ($data->{Object} and ref $data->{Object}) {
         my $on_uid = ${ $data->{Object} };
-        return if $importer->ShouldSkipTransaction($on_uid);
+
+        # skip attributes of objects we're not inflating
+        # exception: we don't inflate RT->System, but we want RT->System's searches
+        unless ($on_uid eq RT->System->UID && $data->{Name} =~ /Search/) {
+            return if $importer->ShouldSkipTransaction($on_uid);
+        }
     }
+
     return $class->SUPER::PreInflate( $importer, $uid, $data );
 }
 
+# this method will be called repeatedly to fix up this attribute's contents
+# (a list of searches, dashboards) during the import process, as the
+# ordinary dependency resolution system can't quite handle the subtlety
+# involved (e.g. a user simply declares out-dependencies on all of her
+# attributes, but those attributes (e.g. dashboards, saved searches,
+# dashboards in menu preferences) have dependencies amongst themselves).
+# if this attribute (e.g. a user's dashboard) fails to load an attribute
+# (e.g. a user's saved search) then it postpones and repeats the postinflate
+# process again when that user's saved search has been imported
+# this method updates Content each time through, each time getting closer and
+# closer to the fully inflated attribute
+sub PostInflateFixup {
+    my $self     = shift;
+    my $importer = shift;
+    my $spec     = shift;
+
+    # decode UIDs to be raw dashboard IDs
+    if ($self->Name eq RT::User::_PrefName("DashboardsInMenu")) {
+        my $content = $self->Content;
+
+        for my $pane (values %{ $content || {} }) {
+            for (@$pane) {
+                if (ref($_) eq 'SCALAR') {
+                    my $attr = $importer->LookupObj($$_);
+                    if ($attr) {
+                        $_ = $attr->Id;
+                    }
+                    else {
+                        $importer->Postpone(
+                            for    => $$_,
+                            uid    => $spec->{uid},
+                            method => 'PostInflateFixup',
+                        );
+                    }
+                }
+            }
+        }
+        $self->SetContent($content);
+    }
+    # decode UIDs to be saved searches
+    elsif ($self->Name eq RT::User::_PrefName("HomepageSettings")) {
+        my $content = $self->Content;
+
+        for my $pane (values %{ $content || {} }) {
+            for (@$pane) {
+                if (ref($_->{uid}) eq 'SCALAR') {
+                    my $uid = $_->{uid};
+                    my $attr = $importer->LookupObj($$uid);
+
+                    if ($attr) {
+                        if ($_->{type} eq 'saved') {
+                            $_->{name} = join '-', $attr->ObjectType, $attr->ObjectId, 'SavedSearch', $attr->id;
+                        }
+                        # if type is system, name doesn't need to change
+                        # if type is anything else, pass it through as is
+                        delete $_->{uid};
+                    }
+                    else {
+                        $importer->Postpone(
+                            for    => $$uid,
+                            uid    => $spec->{uid},
+                            method => 'PostInflateFixup',
+                        );
+                    }
+                }
+            }
+        }
+        $self->SetContent($content);
+    }
+    elsif ($self->Name eq 'Dashboard') {
+        my $content = $self->Content;
+
+        for my $pane (values %{ $content->{Panes} || {} }) {
+            for (@$pane) {
+                if (ref($_->{uid}) eq 'SCALAR') {
+                    my $uid = $_->{uid};
+                    my $attr = $importer->LookupObj($$uid);
+
+                    if ($attr) {
+                        # update with the new id numbers assigned to us
+                        $_->{id} = $attr->Id;
+                        $_->{privacy} = join '-', $attr->ObjectType, $attr->ObjectId;
+                        delete $_->{uid};
+                    }
+                    else {
+                        $importer->Postpone(
+                            for    => $$uid,
+                            uid    => $spec->{uid},
+                            method => 'PostInflateFixup',
+                        );
+                    }
+                }
+            }
+        }
+        $self->SetContent($content);
+    }
+    elsif ($self->Name eq 'Subscription') {
+        my $content = $self->Content;
+        if (ref($content->{DashboardId}) eq 'SCALAR') {
+            my $attr = $importer->LookupObj(${ $content->{DashboardId} });
+            if ($attr) {
+                $content->{DashboardId} = $attr->Id;
+            }
+            else {
+                $importer->Postpone(
+                    for    => ${ $content->{DashboardId} },
+                    uid    => $spec->{uid},
+                    method => 'PostInflateFixup',
+                );
+            }
+        }
+        $self->SetContent($content);
+    }
+}
+
+sub PostInflate {
+    my $self = shift;
+    my ($importer, $uid) = @_;
+
+    $self->SUPER::PostInflate( $importer, $uid );
+
+    # this method is separate because it needs to be callable multple times,
+    # and we can't guarantee that SUPER::PostInflate can deal with that
+    $self->PostInflateFixup($importer, { uid => $uid });
+}
+
+sub Serialize {
+    my $self = shift;
+    my %args = (@_);
+    my %store = $self->SUPER::Serialize(@_);
+
+    # encode raw dashboard IDs to be UIDs
+    if ($store{Name} eq RT::User::_PrefName("DashboardsInMenu")) {
+        my $content = $self->_DeserializeContent($store{Content});
+        for my $pane (values %{ $content || {} }) {
+            for (@$pane) {
+                my $attr = RT::Attribute->new($self->CurrentUser);
+                $attr->LoadById($_);
+                $_ = \($attr->UID);
+            }
+        }
+        $store{Content} = $self->_SerializeContent($content);
+    }
+    # encode saved searches to be UIDs
+    elsif ($store{Name} eq RT::User::_PrefName("HomepageSettings")) {
+        my $content = $self->_DeserializeContent($store{Content});
+        for my $pane (values %{ $content || {} }) {
+            for (@$pane) {
+                # this hairy code mirrors what's in the saved search loader
+                # in /Elements/ShowSearch
+                if ($_->{type} eq 'saved') {
+                    if ($_->{name} =~ /^(.*?)-(\d+)-SavedSearch-(\d+)$/) {
+                        my $attr = RT::Attribute->new($self->CurrentUser);
+                        $attr->LoadById($3);
+                        $_->{uid} = \($attr->UID);
+                    }
+                    # if we can't parse the name, just pass it through
+                }
+                elsif ($_->{type} eq 'system') {
+                    my ($search) = RT::System->new($self->CurrentUser)->Attributes->Named( 'Search - ' . $_->{name} );
+                    unless ( $search && $search->Id ) {
+                        my (@custom_searches) = RT::System->new($self->CurrentUser)->Attributes->Named('SavedSearch');
+                        foreach my $custom (@custom_searches) {
+                            if ($custom->Description eq $_->{name}) { $search = $custom; last }
+                        }
+                    }
+                    # if we can't load the search, just pass it through
+                    if ($search) {
+                        $_->{uid} = \($search->UID);
+                    }
+                }
+                # pass through everything else (e.g. component)
+            }
+        }
+        $store{Content} = $self->_SerializeContent($content);
+    }
+    # encode saved searches and dashboards to be UIDs
+    elsif ($store{Name} eq 'Dashboard') {
+        my $content = $self->_DeserializeContent($store{Content}) || {};
+        for my $pane (values %{ $content->{Panes} || {} }) {
+            for (@$pane) {
+                if ($_->{portlet_type} eq 'search' || $_->{portlet_type} eq 'dashboard') {
+                    my $attr = RT::Attribute->new($self->CurrentUser);
+                    $attr->LoadById($_->{id});
+                    $_->{uid} = \($attr->UID);
+                }
+                # pass through everything else (e.g. component)
+            }
+        }
+        $store{Content} = $self->_SerializeContent($content);
+    }
+    # encode subscriptions to have dashboard UID
+    elsif ($store{Name} eq 'Subscription') {
+        my $content = $self->_DeserializeContent($store{Content});
+        my $attr = RT::Attribute->new($self->CurrentUser);
+        $attr->LoadById($content->{DashboardId});
+        $content->{DashboardId} = \($attr->UID);
+        $store{Content} = $self->_SerializeContent($content);
+    }
+
+    return %store;
+}
+
 RT::Base->_ImportOverlays();
 
 1;
diff --git a/lib/RT/Group.pm b/lib/RT/Group.pm
index eb2c3a0..dddfa14 100644
--- a/lib/RT/Group.pm
+++ b/lib/RT/Group.pm
@@ -1636,13 +1636,12 @@ sub PreInflate {
         return;
     };
 
-    # Go looking for the pre-existing version of the it
+    # Go looking for the pre-existing version of it
     if ($data->{Domain} eq "ACLEquivalence") {
         $obj->LoadACLEquivalenceGroup( $data->{Instance} );
         return $duplicated->() if $obj->Id;
 
-        # Update the name and description for the new ID
-        $data->{Name} = 'User '. $data->{Instance};
+        # Update description for the new ID
         $data->{Description} = 'ACL equiv. for user '.$data->{Instance};
     } elsif ($data->{Domain} eq "UserDefined") {
         $data->{Name} = $importer->Qualify($data->{Name});
diff --git a/lib/RT/Migrate/Importer.pm b/lib/RT/Migrate/Importer.pm
index 7897434..5c25d9a 100644
--- a/lib/RT/Migrate/Importer.pm
+++ b/lib/RT/Migrate/Importer.pm
@@ -65,17 +65,20 @@ sub new {
 sub Init {
     my $self = shift;
     my %args = (
-        OriginalId  => undef,
-        Progress    => undef,
-        Statefile   => undef,
-        DumpObjects => undef,
-        HandleError => undef,
+        OriginalId          => undef,
+        Progress            => undef,
+        Statefile           => undef,
+        DumpObjects         => undef,
+        HandleError         => undef,
+        ExcludeOrganization => undef,
         @_,
     );
 
     # Should we attempt to preserve record IDs as they are created?
     $self->{OriginalId} = $args{OriginalId};
 
+    $self->{ExcludeOrganization} = $args{ExcludeOrganization};
+
     $self->{Progress} = $args{Progress};
 
     $self->{HandleError} = sub { 0 };
@@ -179,6 +182,9 @@ sub Resolve {
             Field => $ref->{uri},
             Value => $self->LookupObj($uid)->URI,
         ) if defined $ref->{uri};
+        if (my $method = $ref->{method}) {
+            $obj->$method($self, $ref, $class, $id);
+        }
     }
     delete $self->{Pending}{$uid};
 }
@@ -291,6 +297,7 @@ sub Qualify {
     my ($string) = @_;
     return $string if $self->{Clone};
     return $string if not defined $self->{Organization};
+    return $string if $self->{ExcludeOrganization};
     return $string if $self->{Organization} eq $RT::Organization;
     return $self->{Organization}.": $string";
 }
@@ -332,7 +339,7 @@ sub Create {
     # Load it back to get real values into the columns
     $obj = $class->new( RT->SystemUser );
     $obj->Load( $id );
-    $obj->PostInflate( $self );
+    $obj->PostInflate( $self, $uid );
 
     return $obj;
 }
@@ -399,9 +406,13 @@ sub ReadStream {
     # If it's a ticket, we might need to create a
     # TicketCustomField for the previous ID
     if ($class eq "RT::Ticket" and $self->{OriginalId}) {
+        my $value = $self->{ExcludeOrganization}
+                  ? $origid
+                  : $self->Organization . ":$origid";
+
         my ($id, $msg) = $obj->AddCustomFieldValue(
             Field             => $self->{OriginalId},
-            Value             => $self->Organization . ":$origid",
+            Value             => $value,
             RecordTransaction => 0,
         );
         warn "Failed to add custom field to $uid: $msg"
diff --git a/lib/RT/Migrate/Importer/File.pm b/lib/RT/Migrate/Importer/File.pm
index cfad9ae..3d36f3d 100644
--- a/lib/RT/Migrate/Importer/File.pm
+++ b/lib/RT/Migrate/Importer/File.pm
@@ -192,7 +192,7 @@ sub SaveState {
            NewQueues NewCFs
            SkipTransactions Pending Invalid
            UIDs
-           OriginalId Clone
+           OriginalId ExcludeOrganization Clone
           /;
     Storable::nstore(\%data, $self->{Statefile});
 
diff --git a/lib/RT/ObjectClass.pm b/lib/RT/ObjectClass.pm
index c51d9d4..f5a5da6 100644
--- a/lib/RT/ObjectClass.pm
+++ b/lib/RT/ObjectClass.pm
@@ -229,6 +229,18 @@ sub FindDependencies {
     $deps->Add( out => $obj );
 }
 
+sub Serialize {
+    my $self = shift;
+    my %args = (@_);
+    my %store = $self->SUPER::Serialize(@_);
+
+    if ($store{ObjectId}) {
+        my $obj = $self->ObjectType->new( RT->SystemUser );
+        $obj->Load( $store{ObjectId} );
+        $store{ObjectId} = \($obj->UID);
+    }
+    return %store;
+}
 
 RT::Base->_ImportOverlays();
 
diff --git a/lib/RT/ObjectScrip.pm b/lib/RT/ObjectScrip.pm
index 8399398..b270d25 100644
--- a/lib/RT/ObjectScrip.pm
+++ b/lib/RT/ObjectScrip.pm
@@ -272,6 +272,19 @@ sub FindDependencies {
     }
 }
 
+sub Serialize {
+    my $self = shift;
+    my %args = (@_);
+    my %store = $self->SUPER::Serialize(@_);
+
+    if ($store{ObjectId}) {
+        my $obj = RT::Queue->new( RT->SystemUser );
+        $obj->Load( $store{ObjectId} );
+        $store{ObjectId} = \($obj->UID);
+    }
+    return %store;
+}
+
 RT::Base->_ImportOverlays();
 
 1;
diff --git a/lib/RT/ObjectTopic.pm b/lib/RT/ObjectTopic.pm
index c9429fd..dfe2bfd 100644
--- a/lib/RT/ObjectTopic.pm
+++ b/lib/RT/ObjectTopic.pm
@@ -212,6 +212,19 @@ sub FindDependencies {
     $deps->Add( out => $obj );
 }
 
+sub Serialize {
+    my $self = shift;
+    my %args = (@_);
+    my %store = $self->SUPER::Serialize(@_);
+
+    if ($store{ObjectId}) {
+        my $obj = $self->ObjectType->new( RT->SystemUser );
+        $obj->Load( $store{ObjectId} );
+        $store{ObjectId} = \($obj->UID);
+    }
+    return %store;
+}
+
 RT::Base->_ImportOverlays();
 
 1;
diff --git a/lib/RT/Transaction.pm b/lib/RT/Transaction.pm
index 9afdedf..d84197f 100644
--- a/lib/RT/Transaction.pm
+++ b/lib/RT/Transaction.pm
@@ -2059,6 +2059,9 @@ sub Serialize {
         my $cf = RT::CustomField->new( RT->SystemUser );
         $cf->Load( $store{Field} );
         $store{Field} = \($cf->UID);
+
+        $store{OldReference} = \($self->OldReferenceObject->UID) if $self->OldReference;
+        $store{NewReference} = \($self->NewReferenceObject->UID) if $self->NewReference;
     } elsif ($type =~ /^(Take|Untake|Force|Steal|Give)$/) {
         for my $field (qw/OldValue NewValue/) {
             my $user = RT::User->new( RT->SystemUser );
diff --git a/sbin/rt-importer.in b/sbin/rt-importer.in
index 6d3cda5..387128e 100644
--- a/sbin/rt-importer.in
+++ b/sbin/rt-importer.in
@@ -95,6 +95,7 @@ GetOptions(
 
     "resume!",
     "originalid|i=s",
+    "exclude-organization",
 
     "ask",
     "ignore-errors",
@@ -142,11 +143,12 @@ elsif ($OPT{'ignore-errors'}) {
 }
 
 my $import = RT::Migrate::Importer::File->new(
-    Directory   => $dir,
-    OriginalId  => $OPT{originalid},
-    DumpObjects => $OPT{dump},
-    Resume      => $OPT{resume},
-    HandleError => $error_handler,
+    Directory           => $dir,
+    OriginalId          => $OPT{originalid},
+    ExcludeOrganization => $OPT{'exclude-organization'},
+    DumpObjects         => $OPT{dump},
+    Resume              => $OPT{resume},
+    HandleError         => $error_handler,
 );
 
 if ($import->Metadata and -t STDOUT and not $OPT{quiet}) {
@@ -222,6 +224,14 @@ current database; this may include users, queues, and tickets.
 It is possible to stop the import process with ^C; it can be later
 resumed by re-running the importer.
 
+Certain records (notably queues and groups) will have their original
+Organization name prepended to them on import. This is primarily to avoid
+duplicate names (for example importing a General queue into an RT that
+already has one would otherwise cause a name collision error). If you are
+confident you won't have any name collisions in queues or groups, you may
+suppress this behavior by passing the B<--exclude-organization> flag to
+C<rt-importer>.
+
 =head2 OPTIONS
 
 =over
@@ -236,6 +246,12 @@ Places the original ticket organization and ID into a global custom
 field with the given name.  If no global ticket custom field with that
 name is found in the current database, it will create one.
 
+=item B<--exclude-organization>
+
+Ordinarily certain records (groups, queues, the B<--originalid> custom field)
+include the organization name of the original RT instance. Use this option to
+suppress that behavior and use the original name directly.
+
 =item B<--ask>
 
 Prompt for action when an error occurs inserting a record into the

commit 1061f217ffbdba540e3a0b8ad3d3456a72d59fe0
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue Dec 13 14:26:16 2016 -0500

    New feature to serialize selected queues
    
    Changes pulled in from branch 4.2/migrator-additions.

diff --git a/lib/RT/Link.pm b/lib/RT/Link.pm
index 0dadc3b..afbad99 100644
--- a/lib/RT/Link.pm
+++ b/lib/RT/Link.pm
@@ -558,6 +558,27 @@ sub Serialize {
 
     delete $store{LocalBase}   if $store{Base};
     delete $store{LocalTarget} if $store{Target};
+
+    for my $dir (qw/Base Target/) {
+        my $uri = $self->${\($dir.'URI')};
+        my $object = $self->${\($dir.'Obj')};
+
+        if ($uri->IsLocal) {
+            if ($args{serializer}->Observe(object => $object)) {
+                # no action needed; the object is being migrated
+            }
+            elsif ($args{serializer}{HyperlinkUnmigrated}) {
+                # object is not being migrated; hyperlinkify
+                $store{$dir} = $uri->AsHREF;
+            }
+            else {
+                # object is not being migrated and hyperlinks not desired,
+                # so drop this RT::Link altogether
+                return;
+            }
+        }
+    }
+
     return %store;
 }
 
diff --git a/lib/RT/Migrate/Serializer.pm b/lib/RT/Migrate/Serializer.pm
index 381728e..a8f234d 100644
--- a/lib/RT/Migrate/Serializer.pm
+++ b/lib/RT/Migrate/Serializer.pm
@@ -58,6 +58,7 @@ sub cmp_version($$) { RT::Handle::cmp_version($_[0],$_[1]) };
 use RT::Migrate::Incremental;
 use RT::Migrate::Serializer::IncrementalRecord;
 use RT::Migrate::Serializer::IncrementalRecords;
+use List::MoreUtils 'none';
 
 sub Init {
     my $self = shift;
@@ -88,6 +89,9 @@ sub Init {
                   FollowScrips
                   FollowTickets
                   FollowACL
+                  Queues
+                  CustomFields
+                  HyperlinkUnmigrated
                   Clone
                   Incremental
               /;
@@ -251,6 +255,11 @@ sub PushBasics {
         OPERATOR => 'IN',
         VALUE => [ qw/RT::User RT::Group RT::Queue/ ],
     );
+
+    if ($self->{CustomFields}) {
+        $cfs->Limit(FIELD => 'id', OPERATOR => 'IN', VALUE => $self->{CustomFields});
+    }
+
     $self->PushObj( $cfs );
 
     # Global attributes
@@ -293,7 +302,14 @@ sub PushBasics {
         $self->PushCollections(qw(Topics Classes));
     }
 
-    $self->PushCollections(qw(Queues));
+    if ($self->{Queues}) {
+        my $queues = RT::Queues->new(RT->SystemUser);
+        $queues->Limit(FIELD => 'id', OPERATOR => 'IN', VALUE => $self->{Queues});
+        $self->PushObj($queues);
+    }
+    else {
+        $self->PushCollections(qw(Queues));
+    }
 }
 
 sub InitStream {
@@ -400,7 +416,25 @@ sub Observe {
     my $from = $args{from};
     if ($obj->isa("RT::Ticket")) {
         return 0 if $obj->Status eq "deleted" and not $self->{FollowDeleted};
+        my $queue = $obj->Queue;
+        return 0 if $self->{Queues} && none { $queue == $_ } @{ $self->{Queues} };
         return $self->{FollowTickets};
+    } elsif ($obj->isa("RT::Queue")) {
+        my $id = $obj->Id;
+        return 0 if $self->{Queues} && none { $id == $_ } @{ $self->{Queues} };
+        return 1;
+    } elsif ($obj->isa("RT::CustomField")) {
+        my $id = $obj->Id;
+        return 0 if $self->{CustomFields} && none { $id == $_ } @{ $self->{CustomFields} };
+        return 1;
+    } elsif ($obj->isa("RT::ObjectCustomFieldValue")) {
+        my $id = $obj->CustomField;
+        return 0 if $self->{CustomFields} && none { $id == $_ } @{ $self->{CustomFields} };
+        return 1;
+    } elsif ($obj->isa("RT::ObjectCustomField")) {
+        my $id = $obj->CustomField;
+        return 0 if $self->{CustomFields} && none { $id == $_ } @{ $self->{CustomFields} };
+        return 1;
     } elsif ($obj->isa("RT::ACE")) {
         return $self->{FollowACL};
     } elsif ($obj->isa("RT::Scrip") or $obj->isa("RT::Template") or $obj->isa("RT::ObjectScrip")) {
@@ -473,10 +507,13 @@ sub Visit {
             \%data,
         );
     } else {
+        my %serialized = $obj->Serialize(serializer => $self);
+        return unless %serialized;
+
         @store = (
             ref($obj),
             $obj->UID,
-            { $obj->Serialize },
+            \%serialized,
         );
     }
 
diff --git a/lib/RT/Transaction.pm b/lib/RT/Transaction.pm
index d84197f..3d7f12a 100644
--- a/lib/RT/Transaction.pm
+++ b/lib/RT/Transaction.pm
@@ -2080,19 +2080,45 @@ sub Serialize {
         if ($store{OldValue}) {
             my $base = RT::URI->new( $self->CurrentUser );
             $base->FromURI( $store{OldValue} );
-            $store{OldValue} = \($base->Object->UID) if $base->Resolver and $base->Object;
+            if ($base->Resolver && (my $object = $base->Object)) {
+                if ($args{serializer}->Observe(object => $object)) {
+                    $store{OldValue} = \($object->UID);
+                }
+                elsif ($args{serializer}{HyperlinkUnmigrated}) {
+                    $store{OldValue} = $base->AsHREF;
+                }
+                else {
+                    $store{OldValue} = "(not migrated)";
+                }
+            }
         }
     } elsif ($type eq "AddLink") {
         if ($store{NewValue}) {
             my $base = RT::URI->new( $self->CurrentUser );
             $base->FromURI( $store{NewValue} );
-            $store{NewValue} = \($base->Object->UID) if $base->Resolver and $base->Object;
+            if ($base->Resolver && (my $object = $base->Object)) {
+                if ($args{serializer}->Observe(object => $object)) {
+                    $store{NewValue} = \($object->UID);
+                }
+                elsif ($args{serializer}{HyperlinkUnmigrated}) {
+                    $store{NewValue} = $base->AsHREF;
+                }
+                else {
+                    $store{NewValue} = "(not migrated)";
+                }
+            }
         }
     } elsif ($type eq "Set" and $store{Field} eq "Queue") {
         for my $field (qw/OldValue NewValue/) {
             my $queue = RT::Queue->new( RT->SystemUser );
             $queue->Load( $store{$field} );
-            $store{$field} = \($queue->UID);
+            if ($args{serializer}->Observe(object => $queue)) {
+                $store{$field} = \($queue->UID);
+            }
+            else {
+                $store{$field} = "$RT::Organization: " . $queue->Name . " (not migrated)";
+
+            }
         }
     } elsif ($type =~ /^(Add|Open|Resolve)Reminder$/) {
         my $ticket = RT::Ticket->new( RT->SystemUser );
diff --git a/sbin/rt-serializer.in b/sbin/rt-serializer.in
index c008e78..2a9de13 100644
--- a/sbin/rt-serializer.in
+++ b/sbin/rt-serializer.in
@@ -104,6 +104,9 @@ GetOptions(
     "scrips!",
     "tickets!",
     "acls!",
+    "limit-queues=s@",
+    "limit-cfs=s@",
+    "hyperlink-unmigrated!",
 
     "clone",
     "incremental",
@@ -127,12 +130,53 @@ $args{FollowScrips}  = $OPT{scrips}   if defined $OPT{scrips};
 $args{FollowTickets} = $OPT{tickets}  if defined $OPT{tickets};
 $args{FollowACL}     = $OPT{acls}     if defined $OPT{acls};
 
+$args{HyperlinkUnmigrated} = $OPT{'hyperlink-unmigrated'} if defined $OPT{'hyperlink-unmigrated'};
+
 $args{Clone}         = $OPT{clone}       if $OPT{clone};
 $args{Incremental}   = $OPT{incremental} if $OPT{incremental};
 
 $args{GC}   = defined $OPT{gc}   ? $OPT{gc}   : 5000;
 $args{Page} = defined $OPT{page} ? $OPT{page} : 100;
 
+if ($OPT{'limit-queues'}) {
+    my @queue_ids;
+
+    for my $name (split ',', join ',', @{ $OPT{'limit-queues'} }) {
+        $name =~ s/^\s+//; $name =~ s/\s+$//;
+        my $queue = RT::Queue->new(RT->SystemUser);
+        $queue->Load($name);
+        if (!$queue->Id) {
+            die "Unable to load queue '$name'";
+        }
+        push @queue_ids, $queue->Id;
+    }
+
+    $args{Queues} = \@queue_ids;
+}
+
+if ($OPT{'limit-cfs'}) {
+    my @cf_ids;
+
+    for my $name (split ',', join ',', @{ $OPT{'limit-cfs'} }) {
+        $name =~ s/^\s+//; $name =~ s/\s+$//;
+
+        # numeric means id
+        if ($name =~ /^\d+$/) {
+            push @cf_ids, $name;
+        }
+        else {
+            my $cfs = RT::CustomFields->new(RT->SystemUser);
+            $cfs->Limit(FIELD => 'Name', VALUE => $name);
+            if (!$cfs->Count) {
+                die "Unable to load any custom field named '$name'";
+            }
+            push @cf_ids, map { $_->Id } @{ $cfs->ItemsArrayRef };
+        }
+    }
+
+    $args{CustomFields} = \@cf_ids;
+}
+
 if (($OPT{clone} or $OPT{incremental})
         and grep { /^(users|groups|deleted|scrips|tickets|acls)$/ } keys %OPT) {
     die "You cannot specify object types when cloning.\n\nPlease see $0 --help.\n";
@@ -321,6 +365,24 @@ serialized.
 
 Skip serialization of all ticket data.
 
+=item B<--limit-queues>
+
+Takes a list of queue IDs or names separated by commas. When provided, only
+that set of queues (and the tickets in them) will be serialized.
+
+=item B<--limit-cfs>
+
+Takes a list of custom field IDs or names separated by commas. When provided,
+only that set of custom fields will be serialized.
+
+=item B<--hyperlink-unmigrated>
+
+Replace links to local records which are not being migrated with hyperlinks.
+The hyperlinks will use the serializing RT's configured URL.
+
+Without this option, such links are instead dropped, and transactions which
+had updated such links will be replaced with an explanatory message.
+
 =item B<--clone>
 
 Serializes your entire database, creating a clone.  This option should

-----------------------------------------------------------------------


More information about the rt-commit mailing list