[Bps-public-commit] SD branch, master, updated. 0.70-18-g67a7332

sunnavy at bestpractical.com sunnavy at bestpractical.com
Tue Sep 1 06:59:06 EDT 2009


The branch, master has been updated
       via  67a73323e34a9015ead1445fe09b5cbe28203978 (commit)
      from  08e358b1250973ac61baf99e3fef44a1f976f1e6 (commit)

Summary of changes:
 lib/App/SD/Replica/lighthouse.pm                   |  121 +++++++++++++++++++
 .../Replica/{github => lighthouse}/PullEncoder.pm  |  125 ++++++++++----------
 .../Replica/{github => lighthouse}/PushEncoder.pm  |   74 ++++--------
 3 files changed, 211 insertions(+), 109 deletions(-)
 create mode 100644 lib/App/SD/Replica/lighthouse.pm
 copy lib/App/SD/Replica/{github => lighthouse}/PullEncoder.pm (64%)
 copy lib/App/SD/Replica/{github => lighthouse}/PushEncoder.pm (59%)

- Log -----------------------------------------------------------------
commit 67a73323e34a9015ead1445fe09b5cbe28203978
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue Sep 1 18:58:47 2009 +0800

    add lighthouse lighthouse replica

diff --git a/lib/App/SD/Replica/lighthouse.pm b/lib/App/SD/Replica/lighthouse.pm
new file mode 100644
index 0000000..56bf6d5
--- /dev/null
+++ b/lib/App/SD/Replica/lighthouse.pm
@@ -0,0 +1,121 @@
+package App::SD::Replica::lighthouse;
+use Any::Moose;
+extends qw/App::SD::ForeignReplica/;
+
+use Params::Validate qw(:all);
+use Memoize;
+
+use URI;
+use Memoize;
+use Net::Lighthouse::Project;
+
+use Prophet::ChangeSet;
+
+use constant scheme => 'lighthouse';
+use constant pull_encoder => 'App::SD::Replica::lighthouse::PullEncoder';
+use constant push_encoder => 'App::SD::Replica::lighthouse::PushEncoder';
+
+has lighthouse => ( isa => 'Net::Lighthouse::Project', is => 'rw' );
+has remote_url => ( isa => 'Str',                      is => 'rw' );
+has account    => ( isa => 'Str',                      is => 'rw' );
+has project    => ( isa => 'Str',                      is => 'rw' );
+has query      => ( isa => 'Str',                      is => 'rw' );
+
+our %PROP_MAP = ( state => 'status', title => 'summary' );
+
+sub BUILD {
+    my $self = shift;
+
+    my ( $auth, $account, $project ) =
+      $self->{url} =~ m{^lighthouse:(?:(.*)@)?(.*?)/(.*)}
+      or die
+        "Can't parse Github server spec. Expected
+        lighthouse:user:password\@account/project or\n"
+        ."lighthouse:token\@account/project.";
+    my $server = "http://$account.lighthouseapp.com";
+
+    my ( $email, $password, $token );
+    if ($auth) {
+        if ( $auth =~ /@/ ) {
+            ( $email, $password ) = split /:/, $auth;
+        }
+        else {
+            $token = $auth;
+        }
+    }
+
+    unless ( $token || $password ) {
+        if ($email) {
+            ( undef, $password ) =
+              $self->prompt_for_login( $server, $email );
+        }
+        else {
+            ($token) = $self->prompt_for_login($server);
+        }
+    }
+
+    $self->remote_url($server);
+    $self->account( $account );
+    $self->project( $project );
+
+    my $lighthouse = Net::Lighthouse::Project->new(
+        $email ? ( email => $email, password => $password ) : (),
+        $token ? ( token => $token ) : (),
+        account => $account,
+    );
+    $lighthouse->load( $project );
+    $self->lighthouse( $lighthouse );
+}
+
+
+sub get_txn_list_by_date {
+    my $self   = shift;
+    my $ticket = shift;
+    my $ticket_obj = $self->lighthouse->ticket;
+    $ticket_obj->load($ticket);
+        
+    my @txns = map {
+        {
+            id      => $_->number,
+            creator => $_->creator_name,
+            created => $_->created_at->epoch,
+        }
+      }
+      sort { $b->created_at <=> $a->created_at } @{ $ticket_obj->versions };
+    return @txns;
+}
+
+sub foreign_username {
+    my $self = shift;
+    return $self->lighthouse->email;
+}
+
+sub uuid {
+    my $self = shift;
+    Carp::cluck "- can't make a uuid for this" unless ($self->remote_url && $self->account && $self->project );
+    return $self->uuid_for_url( join( '/', $self->remote_url, $self->project ) );
+}
+
+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;
+}
+
+sub database_settings {
+    my $self = shift;
+    return {
+        project_name => $self->account . '/' . $self->project,
+    };
+
+}
+
+__PACKAGE__->meta->make_immutable;
+no Any::Moose;
+1;
diff --git a/lib/App/SD/Replica/lighthouse/PullEncoder.pm b/lib/App/SD/Replica/lighthouse/PullEncoder.pm
new file mode 100644
index 0000000..2f4555b
--- /dev/null
+++ b/lib/App/SD/Replica/lighthouse/PullEncoder.pm
@@ -0,0 +1,249 @@
+package App::SD::Replica::lighthouse::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::lighthouse',
+    is  => 'rw',
+);
+
+my %PROP_MAP = %App::SD::Replica::lighthouse::PROP_MAP;
+
+sub ticket_id {
+    my $self   = shift;
+    return shift->number;
+}
+
+=head2 translate_ticket_state
+
+=cut
+
+sub translate_ticket_state {
+    my $self   = shift;
+    my $ticket = shift;
+
+    return $ticket;
+}
+
+=head2 find_matching_tickets QUERY
+
+Returns a array of 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()
+      || DateTime->from_epoch( epoch => 0 );
+    my @tickets = $self->sync_source->lighthouse->tickets;
+    my @updated = map { $_->load( $_->number ); $_ }
+      grep { $_->{updated_at} ge $last_changeset_seen_dt } @tickets;
+    return \@updated;
+}
+
+sub _only_pull_tickets_modified_after {
+    my $self = shift;
+
+    my $last_pull = $self->sync_source->upstream_last_modified_date();
+    return unless $last_pull;
+    my $before = App::SD::Util::string_to_datetime($last_pull);
+    $self->log_debug( "Failed to parse '" . $self->sync_source->upstream_last_modified_date() . "' as a timestamp. That means we have to sync ALL history") unless ($before);
+    return $before;
+}
+
+=head2 find_matching_transactions { ticket => $id, starting_transaction => $num  }
+
+Returns a reference to an array of all transactions (as hashes) on ticket $id after transaction $num.
+
+=cut
+
+sub find_matching_transactions {
+    my $self     = shift;
+    my %args     = validate( @_, { ticket => 1, starting_transaction => 1 } );
+    my @raw_txns = @{$args{ticket}->versions};
+
+    my @txns;
+    for my $txn ( @raw_txns ) {
+        my $txn_date = $txn->created_at->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'}->number
+            )
+          );
+
+        push @txns,
+          {
+            timestamp => $txn->created_at,
+            object    => $txn,
+            serial    => $txn->created_at->epoch,
+            $txn->created_at == $args{ticket}->created_at
+            ? ( is_create => 1 )
+            : (),
+          };
+    }
+
+    $self->sync_source->log_debug('Done looking at pulled txns');
+    return \@txns;
+}
+
+sub transcode_create_txn {
+    my $self        = shift;
+    my $txn         = shift;
+    my $ticket      = $txn->{object};
+    my $ticket_uuid = 
+          $self->sync_source->uuid_for_remote_id($ticket->number);
+    my $creator = $self->resolve_user_id_to( undef, $ticket->creator_name );
+    my $created = $ticket->created_at;
+    my $changeset = Prophet::ChangeSet->new(
+        {
+            original_source_uuid => $ticket_uuid,
+            original_sequence_no => 0,
+            creator              => $creator,
+            created              => $created->ymd . " " . $created->hms
+        }
+    );
+
+    my $change = Prophet::Change->new(
+        {
+            record_type => 'ticket',
+            record_uuid => $ticket_uuid,
+            change_type => 'add_file',
+        }
+    );
+
+    for my $prop (qw/title body state assigned_user_id milestone_id/) {
+        $change->add_prop_change(
+            name => $PROP_MAP{$prop} || $prop,
+            new => $ticket->$prop,
+        );
+    }
+
+    $change->add_prop_change(
+        name => $self->sync_source->uuid . '-id',
+        new => $ticket->number,
+    );
+
+    $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               = shift;
+    my $txn_wrapper        = shift;
+    my $ticket = shift;
+
+    my $txn = $txn_wrapper->{object};
+    if ( $txn_wrapper->{is_create} ) {
+        return $self->transcode_create_txn($txn_wrapper);
+    }
+
+    my $ticket_uuid =
+      $self->sync_source->uuid_for_remote_id( $ticket->number );
+
+    my $changeset = Prophet::ChangeSet->new(
+        {
+            original_source_uuid => $ticket_uuid,
+            original_sequence_no => $txn->created_at->epoch,
+            creator => $self->resolve_user_id_to( undef, $txn->creator_name ),
+            created => $txn->created_at->ymd . " " . $txn->created_at->hms
+        }
+    );
+
+    my $change = Prophet::Change->new(
+        {
+            record_type => 'ticket',
+            record_uuid => $ticket_uuid,
+            change_type => 'update_file'
+        }
+    );
+    my $diffable_attrs = $txn->diffable_attributes;
+    if (keys %$diffable_attrs) {
+        my %hash  = (
+            ':tag'           => 'tag',
+            ':milestone'     => 'milestone_id',
+            ':assigned_user' => 'assigned_user_id',
+            ':state'         => 'state',
+        );
+        for my $attr (keys %$diffable_attrs) {
+            next unless $hash{$attr};
+            my $method = $hash{$attr};
+            $change->add_prop_change(
+                name => $PROP_MAP{ $hash{$attr} } || $hash{$attr},
+                new  => $txn->$method,
+                old => $diffable_attrs->{$attr},
+            );
+        }
+    }
+
+    $changeset->add_change( { change => $change } )
+      if $change->has_prop_changes;
+
+
+    $self->_include_change_comment( $changeset, $ticket_uuid, $txn );
+
+    return unless $changeset->has_changes;
+    return $changeset;
+}
+
+sub _include_change_comment {
+    my $self        = shift;
+    my $changeset   = shift;
+    my $ticket_uuid = shift;
+    my $txn         = shift;
+
+    my $comment = $self->new_comment_creation_change();
+
+    if ( my $content = $txn->body ) {
+        if ( $content !~ /^\s*$/s ) {
+            $comment->add_prop_change(
+                name => 'created',
+                new  => $txn->created_at->ymd . ' ' . $txn->created_at->hms,
+            );
+            $comment->add_prop_change(
+                name => 'creator',
+                new  => $self->resolve_user_id_to( undef => $txn->creator_name ),
+            );
+            $comment->add_prop_change( name => 'content', new => $content );
+            $comment->add_prop_change(
+                name => 'content_type',
+                new  => 'text/plain',
+            );
+            $comment->add_prop_change( name => 'ticket', new => $ticket_uuid, );
+            $changeset->add_change( { change => $comment } );
+        }
+    }
+}
+
+sub translate_prop_status {
+    my $self   = shift;
+    my $status = shift;
+    return lc($status);
+}
+
+sub resolve_user_id_to {
+    my $self = shift;
+    shift;
+    my $id   = shift;
+    return $id;
+#    return $id . '@lighthouse';
+}
+
+__PACKAGE__->meta->make_immutable;
+no Any::Moose;
+1;
diff --git a/lib/App/SD/Replica/lighthouse/PushEncoder.pm b/lib/App/SD/Replica/lighthouse/PushEncoder.pm
new file mode 100644
index 0000000..b2ad521
--- /dev/null
+++ b/lib/App/SD/Replica/lighthouse/PushEncoder.pm
@@ -0,0 +1,132 @@
+package App::SD::Replica::lighthouse::PushEncoder;
+use Any::Moose;
+use Params::Validate;
+use Path::Class;
+
+has sync_source => (
+    isa => 'App::SD::Replica::lighthouse',
+    is  => 'rw',
+);
+
+sub integrate_change {
+    my $self = shift;
+    my ( $change, $changeset ) = validate_pos(
+        @_,
+        { isa => 'Prophet::Change' },
+        { isa => 'Prophet::ChangeSet' }
+    );
+    my ( $id, $record );
+
+
+    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 'ticket' )
+            || (    $change->record_type eq 'comment'
+                and $change->change_type eq 'add_file' )
+          )
+        {
+            $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;
+        }
+
+        $self->sync_source->record_pushed_transactions(
+            start_time => $before_integration,
+            ticket     => $id,
+            changeset  => $changeset,
+        );
+    };
+
+    if ( my $err = $@ ) {
+        $self->sync_source->log( "Push error: " . $err );
+    }
+
+    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 = $self->sync_source->lighthouse->ticket;
+    $ticket->load( $remote_ticket_id );
+    my $attr = $self->_recode_props_for_integrate($change);
+    $ticket->update(
+        map { $_ => $attr->{$_} }
+          grep { exists $attr->{$_} }
+          qw/title body state assigned_user_id milestone_id/
+    );
+    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 = $self->sync_source->lighthouse->ticket;
+    my $attr = $self->_recode_props_for_integrate($change);
+    $ticket->create(
+        map { $_ => $attr->{$_} }
+          grep { exists $attr->{$_} }
+          qw/title body state assigned_user_id milestone_id/
+    );
+    return $ticket->number;
+}
+
+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 ) {
+        if ( $key eq 'summary' ) {
+            $attr{title} = $props{$key};
+        }
+        elsif ( $key eq 'status' ) {
+            $attr{state} = $props{$key};
+        }
+        elsif ( $key eq 'body' ) {
+            $attr{$key} = $props{$key} || '[no body]';
+        }
+        else {
+            $attr{$key} = $props{$key};
+        }
+    }
+    return \%attr;
+}
+
+__PACKAGE__->meta->make_immutable;
+no Any::Moose;
+
+1;

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



More information about the Bps-public-commit mailing list