[Bps-public-commit] SD branch, master, updated. e611e17b537573972897eebc90f6d491bd4cb06e

jesse jesse at bestpractical.com
Thu May 14 11:45:42 EDT 2009


The branch, master has been updated
       via  e611e17b537573972897eebc90f6d491bd4cb06e (commit)
       via  81fbc2faa95c996f7798c470fc920305f99fb13e (commit)
      from  78a782f7f925bfdd7da22d80fd69cf532709d0f4 (commit)

Summary of changes:
 lib/App/SD/ForeignReplica.pm                      |    2 +-
 lib/App/SD/Replica/gcode.pm                       |   86 +++++++++++++++++++++
 lib/App/SD/Replica/{trac => gcode}/PullEncoder.pm |   34 +++++----
 lib/App/SD/Replica/{trac => gcode}/PushEncoder.pm |    0 
 lib/App/SD/Replica/trac.pm                        |   65 +--------------
 5 files changed, 111 insertions(+), 76 deletions(-)
 create mode 100644 lib/App/SD/Replica/gcode.pm
 copy lib/App/SD/Replica/{trac => gcode}/PullEncoder.pm (92%)
 copy lib/App/SD/Replica/{trac => gcode}/PushEncoder.pm (100%)

- Log -----------------------------------------------------------------
commit 81fbc2faa95c996f7798c470fc920305f99fb13e
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Thu May 14 11:17:59 2009 -0400

    Finish refactoring the trac replica to use the newly factored infrastructure

diff --git a/lib/App/SD/ForeignReplica.pm b/lib/App/SD/ForeignReplica.pm
index 47e7563..ef44086 100644
--- a/lib/App/SD/ForeignReplica.pm
+++ b/lib/App/SD/ForeignReplica.pm
@@ -68,7 +68,7 @@ sub record_pushed_transactions {
 
         # if the transaction id is older than the id of the last changeset
         # we got from the original source of this changeset, we're done
-        last if $txn->{id} <= $self->upstream_last_txn();
+        last if $txn->{id} <= $self->upstream_last_txn($args{changeset});
         
         # if the transaction from RT is more recent than the most recent
         # transaction we got from the original source of the changeset
diff --git a/lib/App/SD/Replica/trac.pm b/lib/App/SD/Replica/trac.pm
index aa31880..000012d 100644
--- a/lib/App/SD/Replica/trac.pm
+++ b/lib/App/SD/Replica/trac.pm
@@ -48,7 +48,6 @@ sub BUILD {
 }
 
 
-
 sub get_txn_list_by_date {
     my $self   = shift;
     my $ticket = shift;
@@ -56,70 +55,16 @@ sub get_txn_list_by_date {
     my $ticket_obj = Net::Trac::Ticket->new( connection => $self->trac);
     $ticket_obj->load($ticket);
         
-    my @txns   = map { { id => $_->id, creator => $_->author, created => $_->date->epoch } }
+    my @txns   = map { { id => $_->date->epoch, creator => $_->author, created => $_->date->epoch } }
         sort {$b->date <=> $a->date }  @{$ticket_obj->history->entries};
     return @txns;
 }
+        
 
-
-
-sub record_pushed_transactions {
+sub upstream_last_txn { 
     my $self = shift;
-    my %args = validate( @_,
-        { ticket => 1, changeset => { isa => 'Prophet::ChangeSet' }, start_time => 1} );
-
-
-    my $earliest_valid_txn_date;
-    
-    # walk through every transaction on the ticket, starting with the latest
-    my $ticket = Net::Trac::Ticket->new( connection => $self->trac);
-    $ticket->load($args{ticket});
-
-    for my $txn ( sort {$b->date <=> $a->date }  @{$ticket->history->entries}) {
-
-        warn "Recording that we pushed ".$ticket->id. " " .$txn->date;
-
-        my $oldest_changeset_for_ticket = $self->app_handle->handle->last_changeset_from_source( $args{changeset}->original_source_uuid);
-
-        # walk backwards through all transactions on the ticket we just updated
-        # Skip any transaction where the remote user isn't me, this might include any transaction
-        # RT created with a scrip on your behalf
-
-        next unless $txn->author eq $self->trac->user;
-        # XXX - are we always decoding txn author correctly?
-
-        # get the completion time _after_ we do our next round trip to rt to try to make sure
-        # a bit of lag doesn't skew us to the wrong side of a 1s boundary
-        my $txn_created_dt = $txn->date;
-        unless($txn_created_dt) {
-            die $args{ticket}. " - Couldn't parse '".$txn->created."' as a timestamp";
-        }
-        my $txn_created = $txn_created_dt->epoch;
-
-
-        # skip any transaction created more than 5 seconds before the push started.
-        if (!$earliest_valid_txn_date){
-            my $change_window =  time() - $args{start_time};
-            # I can't think of any reason that number shouldn't be 1, but clocks are fickle
-            $earliest_valid_txn_date = $txn_created - ($change_window + 5); 
-        }      
-
-        last if $txn_created < $earliest_valid_txn_date;
-
-        # if the transaction id is older than the id of the last changeset
-        # we got from the original source of this changeset, we're done
-        last if $txn_created <= $oldest_changeset_for_ticket;
-
-        # if the transaction from trac is more recent than the most recent
-        # transaction we got from the original source of the changeset
-        # then we should record that we sent that transaction upstream
-
-        $self->record_pushed_transaction(
-            transaction => $txn_created,
-            changeset   => $args{'changeset'},
-            record      => $args{'ticket'}
-        );
-    }
+    my $changeset = shift;
+    return $self->app_handle->handle->last_changeset_from_source( $changeset->original_source_uuid);
 }
 
 =head2 uuid

commit e611e17b537573972897eebc90f6d491bd4cb06e
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Thu May 14 11:45:19 2009 -0400

    First pass at stealing our trac implementation for google code

diff --git a/lib/App/SD/Replica/gcode.pm b/lib/App/SD/Replica/gcode.pm
new file mode 100644
index 0000000..eb1781e
--- /dev/null
+++ b/lib/App/SD/Replica/gcode.pm
@@ -0,0 +1,86 @@
+package App::SD::Replica::gcode;
+use Any::Moose;
+extends qw/App::SD::ForeignReplica/;
+
+use Params::Validate qw(:all);
+use Path::Class;
+use File::Temp 'tempdir';
+use Memoize;
+
+use constant scheme => 'gcode';
+use constant pull_encoder => 'App::SD::Replica::gcode::PullEncoder';
+use constant push_encoder => 'App::SD::Replica::gcode::PushEncoder';
+
+
+use Prophet::ChangeSet;
+
+has gcode => ( isa => 'Net::Google::Code', is => 'rw');
+has project => ( isa => 'Str', is => 'rw');
+
+sub remote_url { return "http://code.google.com/p/".shift->project}
+sub foreign_username { return shift->gcode->user(@_) }
+
+sub BUILD {
+    my $self = shift;
+
+    # Require rather than use to defer load
+    require Net::Google::Code;
+
+    my (  $project ) = $self->{url} =~ m/^gcode:(.*?)$/
+        or die "Can't parse Google::Code server spec. Expected gcode:k9mail";
+    $self->project($project);
+
+    #( $username, $password ) = $self->prompt_for_login( $uri, $username ) unless $password;
+    $self->gcode( Net::Google::Code->new( project => $self->project));
+    $self->gcode->load();
+}
+
+
+sub get_txn_list_by_date {
+    my $self   = shift;
+    my $ticket = shift;
+
+    my $ticket_obj = Net::Google::Code::Issue->new( project => $self->project);
+    $ticket_obj->load($ticket);
+        
+    my @txns   = map { { id => $_->date->epoch, creator => $_->author, created => $_->date->epoch } }
+        sort {$b->date <=> $a->date }  @{$ticket_obj->comments};
+    return @txns;
+}
+        
+
+sub upstream_last_txn { 
+    my $self = shift;
+    my $changeset = shift;
+    return $self->app_handle->handle->last_changeset_from_source( $changeset->original_source_uuid);
+}
+
+=head2 uuid
+
+Return the replica's UUID
+
+=cut
+
+sub uuid {
+    my $self = shift;
+    return $self->uuid_for_url( $self->remote_url);
+}
+
+sub remote_uri_path_for_comment {
+    my $self = shift;
+    my $id = shift;
+    return "/comment/".$id;
+}
+
+sub remote_uri_path_for_id {
+    my $self = shift;
+    my $id = shift;
+    return "/ticket/".$id;
+}
+
+
+
+__PACKAGE__->meta->make_immutable;
+no Any::Moose;
+
+1;
diff --git a/lib/App/SD/Replica/gcode/PullEncoder.pm b/lib/App/SD/Replica/gcode/PullEncoder.pm
new file mode 100644
index 0000000..a0e0069
--- /dev/null
+++ b/lib/App/SD/Replica/gcode/PullEncoder.pm
@@ -0,0 +1,397 @@
+package App::SD::Replica::gcode::PullEncoder;
+use Any::Moose;
+extends 'App::SD::ForeignReplica::PullEncoder';
+
+use Params::Validate qw(:all);
+use Memoize;
+use Time::Progress;
+use DateTime;
+
+has sync_source => (
+    isa => 'App::SD::Replica::gcode',
+    is  => 'rw',
+    weak_ref => 1
+);
+
+sub run {
+    ### XXX TODO - can we factor this out?
+    my $self = shift;
+    my %args = validate(
+        @_,
+        {   after    => 1,
+            callback => 1,
+        }
+    );
+    $self->sync_source->log('Finding matching tickets');
+    my @tickets = @{ $self->find_matching_tickets() };
+
+    if ( @tickets == 0 ) {
+        $self->sync_source->log("No tickets found.");
+        return;
+    }
+
+    my @changesets;
+    my $counter = 0;
+    $self->sync_source->log("Discovering ticket history");
+    my $progress = Time::Progress->new();
+    $progress->attr( max => $#tickets );
+    local $| = 1;
+
+    my $last_modified_date;
+
+    for my $ticket (@tickets) {
+        print $progress->report( "%30b %p Est: %E\r", $counter );
+        $self->sync_source->log(
+            "Fetching ticket @{[$ticket->id]} - " . ++$counter . " of " . scalar @tickets );
+
+        $last_modified_date = $ticket->last_modified
+            if ( !$last_modified_date || $ticket->last_modified > $last_modified_date );
+
+        my $ticket_data         = $self->_translate_final_ticket_state($ticket);
+        my $ticket_initial_data = {%$ticket_data};
+        my $txns                = $self->skip_previously_seen_transactions(
+            ticket       => $ticket,
+            transactions => $ticket->history->entries,
+            starting_transaction => $self->sync_source->app_handle->handle->last_changeset_from_source(
+ $self->sync_source->uuid_for_remote_id( $ticket->id )
+        )
+
+        );
+    
+        # Walk transactions newest to oldest.
+        for my $txn ( sort { $b->date <=> $a->date } @$txns ) {
+            $self->sync_source->log( $ticket->id . " - Transcoding transaction  @{[$txn->date]} " );
+
+            # the changesets are older than the ones that came before, so they goes first
+            unshift @changesets,
+                grep {defined} $self->transcode_one_txn( $txn, $ticket_initial_data, $ticket_data );
+        }
+
+    }
+
+    $args{callback}->($_) for @changesets;
+    $self->sync_source->record_upstream_last_modified_date($last_modified_date);
+}
+
+
+sub _translate_final_ticket_state {
+    my $self          = shift;
+    my $ticket_object = shift;
+    
+    my $content = $ticket_object->description;
+    my $ticket_data = {
+
+        $self->sync_source->uuid . '-id' => $ticket_object->id,
+
+        owner => ( $ticket_object->owner || undef ),
+        type => ($ticket_object->type || undef),
+        created     => ( $ticket_object->created->ymd . " " . $ticket_object->created->hms ),
+        reporter    => ( $ticket_object->reporter || undef ),
+        status      => $self->translate_status( $ticket_object->status ),
+        summary     => ( $ticket_object->summary || undef ),
+        description => ( $content||undef),
+        tags        => ( $ticket_object->keywords || undef ),
+        component   => ( $ticket_object->component || undef ),
+        milestone   => ( $ticket_object->milestone || undef ),
+        priority    => ( $ticket_object->priority || undef ),
+        severity    => ( $ticket_object->severity || undef ),
+        cc          => ( $ticket_object->cc || undef ),
+    };
+
+
+
+
+    # delete undefined and empty fields
+    delete $ticket_data->{$_}
+        for grep !defined $ticket_data->{$_} || $ticket_data->{$_} eq '', keys %$ticket_data;
+
+    return $ticket_data;
+}
+
+=head2 find_matching_tickets QUERY
+
+Returns a Google Code::TicketSearch collection for all tickets found matching your QUERY hash.
+
+=cut
+
+sub find_matching_tickets {
+    my $self  = shift;
+    my %query = (@_);
+   my $last_changeset_seen_dt =   $self->_only_pull_tickets_modified_after();
+    $self->sync_source->log("Searching for tickets");
+    require Net::Google::Code::Issue::Search;
+    my $search = Net::Google::Code::Issue::Search->new( project =>  $self->sync_source->project, limit => 9999 );
+    $search->search();
+    my @ids = @{$search->ids};
+    my @results;
+    foreach my $item (@results) {
+        my $t = $self->sync_source->ticket->load($item);
+        if (!$last_changeset_seen_dt || ($t->last_modified >= $last_changeset_seen_dt)) {
+            push @results, $t;
+        }
+    }
+    return \@results;
+}
+
+=head2 skip_previously_seen_transactions { ticket => $id, starting_transaction => $num, transactions => \@txns  }
+
+Returns a reference to an array of all transactions (as hashes) on ticket $id after transaction $num.
+
+=cut
+
+sub skip_previously_seen_transactions {
+    my $self = shift;
+    my %args = validate( @_, { ticket => 1, transactions => 1, starting_transaction => 0 } );
+    my @txns;
+
+    for my $txn ( sort @{ $args{transactions} } ) {
+        my $txn_date = $txn->date->epoch;
+
+        # Skip things we know we've already pulled
+        next if $txn_date < ( $args{'starting_transaction'} ||0 );
+        # Skip things we've pushed
+        next if ($self->sync_source->foreign_transaction_originated_locally($txn_date, $args{'ticket'}->id) );
+
+        # ok. it didn't originate locally. we might want to integrate it
+        push @txns, $txn;
+    }
+    $self->sync_source->log('Done looking at pulled txns');
+    return \@txns;
+}
+
+sub build_initial_ticket_state {
+    my $self          = shift;
+    my $final_state   = shift;
+    my $ticket_object = shift;
+
+    my %initial_state = %{$final_state};
+
+    for my $txn ( reverse @{ $ticket_object->history->entries } ) {
+        for my $pc ( values %{ $txn->prop_changes } ) {
+            unless ( $initial_state{ $pc->property } eq $pc->new_value ) {
+                warn "I was expecting "
+                    . $pc->property
+                    . " to be "
+                    . $pc->new_value
+                    . " but it was actually "
+                    . $initial_state{ $pc->property };
+            }
+            $initial_state{ $pc->property } = $pc->old_value;
+
+        }
+    }
+    return \%initial_state;
+}
+
+sub transcode_create_txn {
+    my $self        = shift;
+    my $txn         = shift;
+    my $create_data = shift;
+    my $final_data = shift;
+    my $ticket      = $txn->ticket;
+             # this sequence_no only works because gcode tickets only allow one update 
+             # per ticket per second.
+             # we decrement by 1 on the off chance that someone created and 
+             # updated the ticket in the first second
+    my $changeset = Prophet::ChangeSet->new(
+        {   original_source_uuid => $self->sync_source->uuid_for_remote_id( $ticket->id ),
+            original_sequence_no => ( $ticket->created->epoch-1),
+            creator => $self->resolve_user_id_to( email_address => $create_data->{reporter} ),
+            created => $ticket->created->ymd ." ".$ticket->created->hms
+        }
+    );
+
+    my $change = Prophet::Change->new(
+        {   record_type => 'ticket',
+            record_uuid => $self->sync_source->uuid_for_remote_id( $ticket->id ),
+            change_type => 'add_file'
+        }
+    );
+
+    for my $prop ( keys %$create_data ) {
+        next unless defined $create_data->{$prop};
+        next if $prop =~ /^(?:patch)$/;
+        $change->add_prop_change( name => $prop, old => '', new => $create_data->{$prop} );
+    }
+
+    $changeset->add_change( { change => $change } );
+    return $changeset;
+}
+
+            # we might get return:
+            # 0 changesets if it was a null txn
+            # 1 changeset if it was a normal txn
+            # 2 changesets if we needed to to some magic fixups.
+sub transcode_one_txn {
+    my ( $self, $txn, $ticket, $ticket_final ) = (@_);
+
+
+    if ($txn->is_create) {
+        return $self->transcode_create_txn($txn,$ticket,$ticket_final);
+    }
+
+    my $ticket_uuid = $self->sync_source->uuid_for_remote_id( $ticket->{ $self->sync_source->uuid . '-id' } );
+
+    my $changeset = Prophet::ChangeSet->new(
+        {   original_source_uuid => $ticket_uuid,
+            original_sequence_no => $txn->date->epoch,    # see comment on ticket
+                                                          # create changeset
+            creator => $self->resolve_user_id_to( email_address => $txn->author ),
+            created => $txn->date->ymd . " " . $txn->date->hms
+        }
+    );
+
+    my $change = Prophet::Change->new(
+        {   record_type => 'ticket',
+            record_uuid => $ticket_uuid,
+            change_type => 'update_file'
+        }
+    );
+
+#    warn "right here, we need to deal with changed data that gcode failed to record";
+
+    foreach my $prop_change ( values %{ $txn->prop_changes || {} } ) {
+        my $new      = $prop_change->new_value;
+        my $old      = $prop_change->old_value;
+        my $property = $prop_change->property;
+        next if $property =~ /^(?:patch)$/;
+
+        $old = undef if ( $old eq '' );
+        $new = undef if ( $new eq '' );
+
+        if (!exists $ticket_final->{$property}) {
+                $ticket_final->{$property} = $new;
+                $ticket->{$property} = $new;
+        }
+
+
+        # walk back $ticket's state
+        if (   ( !defined $new && !defined $ticket->{$property} )
+            || ( defined $new && defined $ticket->{$property} && $ticket->{$property} eq $new ) )
+        {
+            $ticket->{$property} = $old;
+        }
+
+        $change->add_prop_change( name => $property, old => $old, new => $new );
+
+    }
+
+    $changeset->add_change( { change => $change } ) if $change->has_prop_changes;
+
+    my $comment = Prophet::Change->new(
+        {   record_type => 'comment',
+            record_uuid => Data::UUID->new->create_str()
+            ,    # comments are never edited, we can have a random uuid
+            change_type => 'add_file'
+        }
+    );
+
+    if ( my $content = $txn->content ) {
+        if ( $content !~ /^\s*$/s ) {
+            $comment->add_prop_change( name => 'created', new  => $txn->date->ymd . ' ' . $txn->date->hms);
+            $comment->add_prop_change( name => 'creator', new  => $self->resolve_user_id_to( email_address => $txn->author ));
+            $comment->add_prop_change( name => 'content',      new => $content );
+            $comment->add_prop_change( name => 'content_type', new => 'text/html' );
+            $comment->add_prop_change( name => 'ticket', new  => $ticket_uuid);
+
+            $changeset->add_change( { change => $comment } );
+        }
+    }
+
+    return undef unless $changeset->has_changes;
+    return $changeset;
+}
+
+sub _recode_attachment_create {
+    my $self   = shift;
+    my %args   = validate( @_, { ticket => 1, txn => 1, changeset => 1, attachment => 1 } );
+    my $change = Prophet::Change->new(
+        {   record_type => 'attachment',
+            record_uuid => $self->sync_source->uuid_for_url(
+                $self->sync_source->remote_url . "/attachment/" . $args{'attachment'}->{'id'}
+            ),
+            change_type => 'add_file'
+        }
+    );
+    $change->add_prop_change(
+        name => 'content_type',
+        old  => undef,
+        new  => $args{'attachment'}->{'ContentType'}
+    );
+    $change->add_prop_change( name => 'created', old => undef, new => $args{'txn'}->{'Created'} );
+    $change->add_prop_change(
+        name => 'creator',
+        old  => undef,
+        new  => $self->resolve_user_id_to( email_address => $args{'attachment'}->{'Creator'} )
+    );
+    $change->add_prop_change(
+        name => 'content',
+        old  => undef,
+        new  => $args{'attachment'}->{'Content'}
+    );
+    $change->add_prop_change(
+        name => 'name',
+        old  => undef,
+        new  => $args{'attachment'}->{'Filename'}
+    );
+    $change->add_prop_change(
+        name => 'ticket',
+        old  => undef,
+        new  => $self->sync_source->uuid_for_remote_id(
+            $args{'ticket'}->{ $self->sync_source->uuid . '-id' }
+        )
+    );
+    $args{'changeset'}->add_change( { change => $change } );
+}
+
+sub translate_status {
+    my $self   = shift;
+    my $status = shift;
+
+    $status =~ s/^resolved$/closed/;
+    return $status;
+}
+
+my %PROP_MAP;
+sub translate_prop_names {
+    my $self      = shift;
+    my $changeset = shift;
+
+    for my $change ( $changeset->changes ) {
+        next unless $change->record_type eq 'ticket';
+
+        my @new_props;
+        for my $prop ( $change->prop_changes ) {
+            next if ( ( $PROP_MAP{ lc( $prop->name ) } || '' ) eq '_delete' );
+            $prop->name( $PROP_MAP{ lc( $prop->name ) } ) if $PROP_MAP{ lc( $prop->name ) };
+
+            # Normalize away undef -> "" and vice-versa
+            for (qw/new_value old_value/) {
+                $prop->$_("") if !defined( $prop->$_() );
+            }
+            next if ( $prop->old_value eq $prop->new_value );
+
+            if ( $prop->name =~ /^cf-(.*)$/ ) {
+                $prop->name( 'custom-' . $1 );
+            }
+
+            push @new_props, $prop;
+
+        }
+        $change->prop_changes( \@new_props );
+
+    }
+    return $changeset;
+}
+
+sub resolve_user_id_to {
+    my $self = shift;
+    my $to   = shift;
+    my $id   = shift;
+    return $id . '@gcode-instance.local';
+
+}
+
+__PACKAGE__->meta->make_immutable;
+no Any::Moose;
+1;
diff --git a/lib/App/SD/Replica/gcode/PushEncoder.pm b/lib/App/SD/Replica/gcode/PushEncoder.pm
new file mode 100644
index 0000000..7621715
--- /dev/null
+++ b/lib/App/SD/Replica/gcode/PushEncoder.pm
@@ -0,0 +1,167 @@
+package App::SD::Replica::trac::PushEncoder;
+use Any::Moose; 
+use Params::Validate;
+use Path::Class;
+use Time::HiRes qw/usleep/;
+
+has sync_source => 
+    ( isa => 'App::SD::Replica::trac',
+      is => 'rw');
+
+sub integrate_change {
+    my $self = shift;
+    my ( $change, $changeset ) = validate_pos(
+        @_,
+        { isa => 'Prophet::Change' },
+        { isa => 'Prophet::ChangeSet' }
+    );
+    my ($id, $record);
+
+    # if the original_sequence_no of this changeset is <= 
+    # the last changeset our sync source for the original_sequence_no, we can skip it.
+    # XXX TODO - this logic should be at the changeset level, not the cahnge level, as it applies to all
+    # changes in the changeset
+    return
+        if $self->sync_source->app_handle->handle->last_changeset_from_source(
+                $changeset->original_source_uuid
+        ) >= $changeset->original_sequence_no;
+
+    my $before_integration = time();
+
+    eval {
+        if (    $change->record_type eq 'ticket'
+            and $change->change_type eq 'add_file' ) {
+            $id = $self->integrate_ticket_create( $change, $changeset );
+            $self->sync_source->record_remote_id_for_pushed_record(
+                uuid      => $change->record_uuid,
+                remote_id => $id);
+        }
+        elsif ( $change->record_type eq 'attachment'
+            and $change->change_type eq 'add_file') {
+            $id = $self->integrate_attachment( $change, $changeset );
+        }
+        elsif ( $change->record_type eq 'comment'
+            and $change->change_type eq 'add_file' ) {
+            $id = $self->integrate_comment( $change, $changeset );
+        }
+        elsif ( $change->record_type eq 'ticket' ) {
+            $id = $self->integrate_ticket_update( $change, $changeset );
+        }
+        else {
+            $self->sync_source->log('I have no idea what I am doing for '.$change->record_uuid);
+            return undef;
+        }
+
+        $self->sync_source->record_pushed_transactions(
+            start_time => $before_integration,
+            ticket    => $id,
+            changeset => $changeset);
+    };
+
+    if (my $err = $@) {
+        $self->sync_source->log("Push error: ".$err);
+    }
+
+    usleep(1100); # trac only accepts one ticket update per second. Yes. 
+
+    return $id;
+}
+
+sub integrate_ticket_update {
+    my $self = shift;
+    my ( $change, $changeset ) = validate_pos(
+        @_,
+        { isa => 'Prophet::Change' },
+        { isa => 'Prophet::ChangeSet' }
+    );
+
+    # Figure out the remote site's ticket ID for this change's record
+    my $remote_ticket_id =
+      $self->sync_source->remote_id_for_uuid( $change->record_uuid );
+    my $ticket = Net::Trac::Ticket->new( connection => $self->sync_source->trac);
+    $ticket->load($remote_ticket_id);
+    $ticket->update( %{ $self->_recode_props_for_integrate($change) }, no_auto_status => 1);
+    return $remote_ticket_id;
+}
+
+sub integrate_ticket_create {
+    my $self = shift;
+    my ( $change, $changeset ) = validate_pos(
+        @_,
+        { isa => 'Prophet::Change' },
+        { isa => 'Prophet::ChangeSet' }
+    );
+
+    # Build up a ticket object out of all the record's attributes
+    my $ticket = Net::Trac::Ticket->new(
+       connection    => $self->sync_source->trac);
+    my $id = $ticket->create( %{ $self->_recode_props_for_integrate($change) });
+
+    return $id
+}
+
+sub integrate_comment {
+    my $self = shift;
+    my ($change, $changeset) = validate_pos( @_, { isa => 'Prophet::Change' }, {isa => 'Prophet::ChangeSet'} );
+
+    # Figure out the remote site's ticket ID for this change's record
+
+    my %props = map { $_->name => $_->new_value } $change->prop_changes;
+
+    my $ticket_id     = $self->sync_source->remote_id_for_uuid( $props{'ticket'} );
+    my $ticket = Net::Trac::Ticket->new( trac => $self->sync_source->trac, id => $ticket_id);
+
+    my %content = ( message => $props{'content'},   
+                );
+
+    if (  ($props{'type'} ||'') eq 'comment' ) {
+        $ticket->comment( %content);
+    } else {
+        $ticket->correspond(%content);
+    }
+    return $ticket_id;
+} 
+
+sub integrate_attachment {
+    my ($self, $change, $changeset ) = validate_pos( @_, { isa => 'App::SD::Replica::rt::PushEncoder'}, { isa => 'Prophet::Change' }, { isa => 'Prophet::ChangeSet' });
+
+
+    my %props = map { $_->name => $_->new_value } $change->prop_changes;
+    my $ticket_id = $self->sync_source->remote_id_for_uuid( $props{'ticket'});
+    my $ticket = Net::Trac::Ticket->new( trac => $self->sync_source->trac, id => $ticket_id );
+
+    my $tempdir = File::Temp::tempdir( CLEANUP => 1 );
+    my $file = file( $tempdir => ( $props{'name'} || 'unnamed' ) );
+    my $fh = $file->openw;
+    print $fh $props{content};
+    close $fh;
+    my %content = ( message     => '(See attachments)', attachments => ["$file"]);
+    $ticket->correspond(%content);
+    return $ticket_id;
+}
+
+sub _recode_props_for_integrate {
+    my $self = shift;
+    my ($change) = validate_pos( @_, { isa => 'Prophet::Change' } );
+
+    my %props = map { $_->name => $_->new_value } $change->prop_changes;
+    my %attr;
+
+    for my $key ( keys %props ) {
+        next unless ( $key =~ /^(summary|queue|status|owner|custom)/ );
+        if ( $key =~ /^custom-(.*)/ ) {
+            $attr{cf}->{$1} = $props{$key};
+        } else {
+            $attr{$key} = $props{$key};
+        }
+        if ( $key eq 'status' ) {
+            $attr{$key} =~ s/^closed$/resolved/;
+        }
+    }
+    return \%attr;
+}
+
+__PACKAGE__->meta->make_immutable;
+no Any::Moose;
+
+1;

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



More information about the Bps-public-commit mailing list