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

sunnavy at bestpractical.com sunnavy at bestpractical.com
Thu Jun 11 23:57:39 EDT 2009


The branch, master has been updated
       via  1c4c703922acdff74cd0f8352b790576311bbcc3 (commit)
       via  4ea89072bfd9576201e13e5aef40c054209e328e (commit)
       via  ddfac2dee4a0152e8d7d61d33fbbf12528ad1ab6 (commit)
      from  0a38d002f7e4b21951e9f1495cae3f9cd6f44533 (commit)

Summary of changes:
 lib/App/SD/Replica/github.pm                       |   29 ++-
 lib/App/SD/Replica/github/PullEncoder.pm           |  255 ++++++++++++++++++++
 .../SD/Replica/{gcode => github}/PushEncoder.pm    |   81 ++-----
 lib/App/SD/Util.pm                                 |   21 ++-
 4 files changed, 316 insertions(+), 70 deletions(-)
 create mode 100644 lib/App/SD/Replica/github/PullEncoder.pm
 copy lib/App/SD/Replica/{gcode => github}/PushEncoder.pm (60%)

- Log -----------------------------------------------------------------
commit ddfac2dee4a0152e8d7d61d33fbbf12528ad1ab6
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Jun 12 11:55:03 2009 +0800

    make string_to_datetime know the format like 2009/06/12 11:53:20 +0800

diff --git a/lib/App/SD/Util.pm b/lib/App/SD/Util.pm
index 5b91200..91cd81f 100644
--- a/lib/App/SD/Util.pm
+++ b/lib/App/SD/Util.pm
@@ -14,7 +14,26 @@ sub string_to_datetime {
                                 second => $sec,
                                 time_zone => 'GMT');
         return $dt;
-    } elsif ($date) {
+    }
+    elsif (
+        $date =~ m!^(\d{4})/(\d{2})/(\d{2}) (\d{2}):(\d{2}):(\d{2}) ([-+]?\d{4})?! )
+    {
+        # e.g. 2009/03/21 10:03:05 -0700
+        my ( $year, $month, $day, $hour, $min, $sec, $tz ) =
+          ( $1, $2, $3, $4, $5, $6, $7 );
+        my $dt = DateTime->new(
+            year      => $year,
+            month     => $month,
+            day       => $day,
+            hour      => $hour,
+            minute    => $min,
+            second    => $sec,
+            time_zone => $tz || 'GMT'
+        );
+        $dt->set_time_zone( 'GMT' );
+        return $dt;
+    }
+    elsif ($date) {
         require DateTime::Format::Natural;
         # XXX DO we want floating or GMT?
         my $parser = DateTime::Format::Natural->new(time_zone => 'floating');

commit 4ea89072bfd9576201e13e5aef40c054209e328e
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Jun 12 11:56:05 2009 +0800

    update github.pm

diff --git a/lib/App/SD/Replica/github.pm b/lib/App/SD/Replica/github.pm
index f2e9a16..f567c34 100644
--- a/lib/App/SD/Replica/github.pm
+++ b/lib/App/SD/Replica/github.pm
@@ -16,16 +16,21 @@ use constant scheme => 'github';
 use constant pull_encoder => 'App::SD::Replica::github::PullEncoder';
 use constant push_encoder => 'App::SD::Replica::github::PushEncoder';
 
-has github     => ( isa => 'Net::Github', is => 'rw' );
-has remote_url => ( isa => 'Str',         is => 'rw' );
-has owner      => ( isa => 'Str',         is => 'rw' );
-has repo       => ( isa => 'Str',         is => 'rw' );
+has github     => ( isa => 'Net::GitHub::V2', is => 'rw' );
+has remote_url => ( isa => 'Str',             is => 'rw' );
+has owner      => ( isa => 'Str',             is => 'rw' );
+has repo       => ( isa => 'Str',             is => 'rw' );
+has query      => ( isa => 'Str',             is => 'rw' );
+
+our %PROP_MAP = ( state => 'status', );
 
 sub BUILD {
     my $self = shift;
 
-    my ( $server , $owner , $repo  ) = $self->{url} =~ m/^github:(.+?)\|(\w+)\|(\w+)\|$/
-        or die "Can't parse Github server spec. Expected github:http://user\@github.com|owner|repository|";
+    my ( $server, $owner, $repo ) =
+      $self->{url} =~ m/^github:(.*?)\|(.+)\|(.+)\|$/
+      or die
+"Can't parse Github server spec. Expected github:http://user\@github.com|owner|repository|";
 
 
     my $uri = URI->new($server);
@@ -58,6 +63,18 @@ sub uuid {
     return $self->uuid_for_url( join( '/', $self->remote_url, $self->owner , $self->repo ) );
 }
 
+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;

commit 1c4c703922acdff74cd0f8352b790576311bbcc3
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Jun 12 11:57:14 2009 +0800

    added PullEncoder and PushEncoder for github

diff --git a/lib/App/SD/Replica/github/PullEncoder.pm b/lib/App/SD/Replica/github/PullEncoder.pm
new file mode 100644
index 0000000..c7463c5
--- /dev/null
+++ b/lib/App/SD/Replica/github/PullEncoder.pm
@@ -0,0 +1,255 @@
+package App::SD::Replica::github::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::github',
+    is  => 'rw',
+);
+
+my %PROP_MAP = %App::SD::Replica::github::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 );
+    $last_changeset_seen_dt->set_time_zone( '-0700' );
+    my $dt = $last_changeset_seen_dt;
+    my $last_dt_str = sprintf(
+        "%4d/%02d/%02d %02d:%02d:%02d -0700",
+        $dt->year, $dt->month,  $dt->day,
+        $dt->hour, $dt->minute, $dt->second
+    );
+    my $issue = $self->sync_source->github->issue;
+    my @updated =
+      grep { $_->{updated_at} ge $last_dt_str }
+      ( @{ $issue->list('open') }, @{ $issue->list('closed') } );
+    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 =
+      @{ $self->sync_source->github->issue->comments( $args{ticket}->{number} ) };
+
+    for my $comment (@raw_txns) {
+        $comment->{date} =
+          App::SD::Util::string_to_datetime( $comment->{date} );
+    }
+
+    my @txns;
+    for my $txn ( sort { $a->{id} <=> $b->{id} } @raw_txns ) {
+        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'}->{number}
+            )
+          );
+
+        # ok. it didn't originate locally. we might want to integrate it
+        push @txns,
+          {
+            timestamp => $txn->{date},
+            serial    => $txn->{id},
+            object    => $txn,
+          };
+    }
+
+    my $ticket_created =
+      App::SD::Util::string_to_datetime( $args{ticket}->{created_at} );
+    if ( $ticket_created->epoch >= $args{'starting_transaction'} || 0 ) {
+        unshift @txns,
+          {
+            timestamp => $ticket_created,
+            serial    => 0,
+            object    => $args{ticket},
+          };
+    }
+
+    $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( email_address => $ticket->{user} );
+    my $created = $txn->{timestamp};
+    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/) {
+        $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->{serial} == 0 ) {
+        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->{id},
+            creator =>
+              $self->resolve_user_id_to( email_address => $txn->{author} ),
+            created => $txn->{date}->ymd . " " . $txn->{date}->hms
+        }
+    );
+
+    $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 = Prophet::Change->new(
+        {
+            record_type => 'comment',
+            record_uuid => $self->sync_source->app_handle->uuid_generator->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/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;
+    my $to   = shift;
+    my $id   = shift;
+    return $id . '@github';
+}
+
+__PACKAGE__->meta->make_immutable;
+no Any::Moose;
+1;
diff --git a/lib/App/SD/Replica/github/PushEncoder.pm b/lib/App/SD/Replica/github/PushEncoder.pm
new file mode 100644
index 0000000..11bca38
--- /dev/null
+++ b/lib/App/SD/Replica/github/PushEncoder.pm
@@ -0,0 +1,149 @@
+package App::SD::Replica::github::PushEncoder;
+use Any::Moose;
+use Params::Validate;
+use Path::Class;
+use Net::Google::Code::Issue;
+use Net::Google::Code;
+
+has sync_source => (
+    isa => 'App::SD::Replica::github',
+    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
+#    warn $self->sync_source->app_handle->handle->last_changeset_from_source(
+#        $changeset->original_source_uuid ), "\n";
+    return
+      if $self->sync_source->app_handle->handle->last_changeset_from_source(
+        $changeset->original_source_uuid ) >= $changeset->original_sequence_no;
+
+    my $before_integration = time();
+    my ( $email, $password );
+
+    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 '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;
+        }
+
+        $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->github->issue();
+    my $attr = $self->_recode_props_for_integrate($change);
+    $ticket->edit( $remote_ticket_id, $attr->{title}, $attr->{body} );
+    if ( $attr->{status} ) {
+        $ticket->reopen( $remote_ticket_id ) if $attr->{status} eq 'open';
+        $ticket->close( $remote_ticket_id ) if $attr->{status} eq 'closed';
+    }
+    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->github->issue;
+    my $attr = $self->_recode_props_for_integrate($change);
+    my $new =
+      $ticket->open( $attr->{title}, $attr->{body} );
+
+    return $new->{number};
+}
+
+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 = $self->sync_source->github->issue();
+    $ticket->comment($ticket_id, $props{'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 ) {
+        if ( $key =~ /^(title|body)/ ) {
+            $attr{$key} = $props{$key};
+        }
+        elsif ( $key eq 'status' ) {
+            $attr{state} = $props{$key} =~ /new|open/ ? 'open' : 'closed';
+        }
+    }
+    return \%attr;
+}
+
+__PACKAGE__->meta->make_immutable;
+no Any::Moose;
+
+1;

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



More information about the Bps-public-commit mailing list