| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package Net::Google::Code::Issue; | 
| 2 | 7 |  |  | 7 |  | 185036 | use Any::Moose; | 
|  | 7 |  |  |  |  | 160577 |  | 
|  | 7 |  |  |  |  | 55 |  | 
| 3 | 7 |  |  | 7 |  | 7420 | use Params::Validate qw(:all); | 
|  | 7 |  |  |  |  | 42634 |  | 
|  | 7 |  |  |  |  | 1833 |  | 
| 4 |  |  |  |  |  |  | with 'Net::Google::Code::TypicalRoles'; | 
| 5 | 7 |  |  | 7 |  | 4088 | use Net::Google::Code::DateTime; | 
|  | 7 |  |  |  |  | 24 |  | 
|  | 7 |  |  |  |  | 314 |  | 
| 6 | 7 |  |  | 7 |  | 5928 | use Net::Google::Code::Issue::Comment; | 
|  | 7 |  |  |  |  | 26 |  | 
|  | 7 |  |  |  |  | 348 |  | 
| 7 | 7 |  |  | 7 |  | 65 | use Net::Google::Code::Issue::Attachment; | 
|  | 7 |  |  |  |  | 14 |  | 
|  | 7 |  |  |  |  | 205 |  | 
| 8 | 7 |  |  | 7 |  | 39 | use Scalar::Util qw/blessed/; | 
|  | 7 |  |  |  |  | 15 |  | 
|  | 7 |  |  |  |  | 478 |  | 
| 9 |  |  |  |  |  |  |  | 
| 10 | 7 |  |  | 7 |  | 50 | use Net::Google::Code::Issue::Util; | 
|  | 7 |  |  |  |  | 17 |  | 
|  | 7 |  |  |  |  | 348 |  | 
| 11 |  |  |  |  |  |  | extends 'Net::Google::Code::Issue::Base'; | 
| 12 |  |  |  |  |  |  |  | 
| 13 |  |  |  |  |  |  | # set this to true to enable hybrid load, create and update | 
| 14 |  |  |  |  |  |  | our $USE_HYBRID; | 
| 15 |  |  |  |  |  |  |  | 
| 16 | 7 |  |  | 7 |  | 35 | use XML::FeedPP; | 
|  | 7 |  |  |  |  | 15 |  | 
|  | 7 |  |  |  |  | 40376 |  | 
| 17 |  |  |  |  |  |  |  | 
| 18 |  |  |  |  |  |  | has 'id' => ( | 
| 19 |  |  |  |  |  |  | isa      => 'Int', | 
| 20 |  |  |  |  |  |  | is       => 'rw', | 
| 21 |  |  |  |  |  |  | ); | 
| 22 |  |  |  |  |  |  |  | 
| 23 |  |  |  |  |  |  | has 'status' => ( | 
| 24 |  |  |  |  |  |  | isa => 'Str', | 
| 25 |  |  |  |  |  |  | is  => 'rw', | 
| 26 |  |  |  |  |  |  | ); | 
| 27 |  |  |  |  |  |  |  | 
| 28 |  |  |  |  |  |  | has 'owner' => ( | 
| 29 |  |  |  |  |  |  | isa => 'Str', | 
| 30 |  |  |  |  |  |  | is  => 'rw', | 
| 31 |  |  |  |  |  |  | ); | 
| 32 |  |  |  |  |  |  |  | 
| 33 |  |  |  |  |  |  | has 'cc' => ( | 
| 34 |  |  |  |  |  |  | isa => 'Str', | 
| 35 |  |  |  |  |  |  | is  => 'rw', | 
| 36 |  |  |  |  |  |  | ); | 
| 37 |  |  |  |  |  |  |  | 
| 38 |  |  |  |  |  |  | has 'summary' => ( | 
| 39 |  |  |  |  |  |  | isa => 'Str', | 
| 40 |  |  |  |  |  |  | is  => 'rw', | 
| 41 |  |  |  |  |  |  | ); | 
| 42 |  |  |  |  |  |  |  | 
| 43 |  |  |  |  |  |  | has 'reporter' => ( | 
| 44 |  |  |  |  |  |  | isa => 'Str', | 
| 45 |  |  |  |  |  |  | is  => 'rw', | 
| 46 |  |  |  |  |  |  | ); | 
| 47 |  |  |  |  |  |  |  | 
| 48 |  |  |  |  |  |  | has 'reported' => ( | 
| 49 |  |  |  |  |  |  | isa => 'DateTime', | 
| 50 |  |  |  |  |  |  | is  => 'rw', | 
| 51 |  |  |  |  |  |  | ); | 
| 52 |  |  |  |  |  |  |  | 
| 53 |  |  |  |  |  |  | has 'merged' => ( | 
| 54 |  |  |  |  |  |  | isa => 'Int', | 
| 55 |  |  |  |  |  |  | is  => 'rw', | 
| 56 |  |  |  |  |  |  | ); | 
| 57 |  |  |  |  |  |  |  | 
| 58 |  |  |  |  |  |  | has 'stars' => ( | 
| 59 |  |  |  |  |  |  | isa => 'Int', | 
| 60 |  |  |  |  |  |  | is  => 'rw', | 
| 61 |  |  |  |  |  |  | ); | 
| 62 |  |  |  |  |  |  |  | 
| 63 |  |  |  |  |  |  | has 'closed' => ( | 
| 64 |  |  |  |  |  |  | isa => 'Str', | 
| 65 |  |  |  |  |  |  | is  => 'rw', | 
| 66 |  |  |  |  |  |  | ); | 
| 67 |  |  |  |  |  |  |  | 
| 68 |  |  |  |  |  |  | has 'description' => ( | 
| 69 |  |  |  |  |  |  | isa => 'Str', | 
| 70 |  |  |  |  |  |  | is  => 'rw', | 
| 71 |  |  |  |  |  |  | ); | 
| 72 |  |  |  |  |  |  |  | 
| 73 |  |  |  |  |  |  | has 'labels' => ( | 
| 74 |  |  |  |  |  |  | isa     => 'ArrayRef', | 
| 75 |  |  |  |  |  |  | is      => 'rw', | 
| 76 |  |  |  |  |  |  | default => sub { [] }, | 
| 77 |  |  |  |  |  |  | ); | 
| 78 |  |  |  |  |  |  |  | 
| 79 |  |  |  |  |  |  | has 'comments' => ( | 
| 80 |  |  |  |  |  |  | isa     => 'ArrayRef[Net::Google::Code::Issue::Comment]', | 
| 81 |  |  |  |  |  |  | is      => 'rw', | 
| 82 |  |  |  |  |  |  | default => sub { [] }, | 
| 83 |  |  |  |  |  |  | ); | 
| 84 |  |  |  |  |  |  |  | 
| 85 |  |  |  |  |  |  | has 'attachments' => ( | 
| 86 |  |  |  |  |  |  | isa     => 'ArrayRef[Net::Google::Code::Issue::Attachment]', | 
| 87 |  |  |  |  |  |  | is      => 'rw', | 
| 88 |  |  |  |  |  |  | default => sub { [] }, | 
| 89 |  |  |  |  |  |  | ); | 
| 90 |  |  |  |  |  |  |  | 
| 91 |  |  |  |  |  |  | sub load { | 
| 92 | 2 |  |  | 2 | 1 | 1244 | my $self = shift; | 
| 93 | 2 |  | 33 |  |  | 12 | my $id = shift || $self->id; | 
| 94 | 2 | 50 |  |  |  | 9 | die "current object doesn't have id and load() is not passed an id either" | 
| 95 |  |  |  |  |  |  | unless $id; | 
| 96 |  |  |  |  |  |  |  | 
| 97 | 2 | 50 |  |  |  | 8 | if ($USE_HYBRID) { | 
| 98 | 0 | 0 | 0 |  |  | 0 | unless ( $self->{loaded_way} | 
|  |  |  | 0 |  |  |  |  | 
| 99 |  |  |  |  |  |  | && $self->{loaded_way} eq 'api' | 
| 100 |  |  |  |  |  |  | && $id == $self->id ) | 
| 101 |  |  |  |  |  |  | { | 
| 102 | 0 |  |  |  |  | 0 | my ($issue) = $self->list( id => $id ); | 
| 103 | 0 |  |  |  |  | 0 | %$self = %$issue; | 
| 104 |  |  |  |  |  |  | } | 
| 105 | 0 |  |  |  |  | 0 | $self->{loaded_way} = 'hybrid'; | 
| 106 |  |  |  |  |  |  |  | 
| 107 | 0 |  |  |  |  | 0 | $self->load_comments; | 
| 108 |  |  |  |  |  |  |  | 
| 109 |  |  |  |  |  |  | # here we do scraping to get stuff not can be seen from feeds | 
| 110 | 0 |  |  |  |  | 0 | my $content = | 
| 111 |  |  |  |  |  |  | $self->fetch( $self->base_url . "issues/detail?id=" . $id ); | 
| 112 | 0 |  |  |  |  | 0 | return $self->parse_hybrid($content); | 
| 113 |  |  |  |  |  |  | } | 
| 114 |  |  |  |  |  |  | else { | 
| 115 | 2 |  |  |  |  | 20 | my $content = | 
| 116 |  |  |  |  |  |  | $self->fetch( $self->base_url . "issues/detail?id=" . $id ); | 
| 117 | 2 |  |  |  |  | 41 | $self->id( $id ); | 
| 118 | 2 |  |  |  |  | 27 | $self->{loaded_way} = 'scraping'; | 
| 119 | 2 |  |  |  |  | 10 | return $self->parse($content); | 
| 120 |  |  |  |  |  |  | } | 
| 121 |  |  |  |  |  |  | } | 
| 122 |  |  |  |  |  |  |  | 
| 123 |  |  |  |  |  |  | sub parse { | 
| 124 | 2 |  |  | 2 | 1 | 4 | my $self    = shift; | 
| 125 | 2 |  |  |  |  | 5 | my $tree    = shift; | 
| 126 |  |  |  |  |  |  |  | 
| 127 | 2 |  |  |  |  | 8 | my $need_delete = not blessed $tree; | 
| 128 | 2 | 50 |  |  |  | 26 | $tree = $self->html_tree( html => $tree ) unless blessed $tree; | 
| 129 |  |  |  |  |  |  |  | 
| 130 |  |  |  |  |  |  | # extract summary | 
| 131 | 2 |  |  |  |  | 15 | my ($summary) = $tree->look_down( class => 'h3' ); | 
| 132 | 2 |  |  |  |  | 3092 | $self->summary( $summary->as_text ); | 
| 133 |  |  |  |  |  |  |  | 
| 134 |  |  |  |  |  |  | # extract reporter, reported and description | 
| 135 | 2 |  |  |  |  | 84 | my $description = $tree->look_down( class => 'vt issuedescription' ); | 
| 136 | 2 |  |  |  |  | 1910 | my $author_tag = $description->look_down( class => "author" ); | 
| 137 | 2 |  |  |  |  | 83 | $self->reporter( $author_tag->content_array_ref->[1]->as_text ); | 
| 138 | 2 |  |  |  |  | 63 | $self->reported( Net::Google::Code::DateTime->new_from_string($author_tag->look_down( class => 'date' )->attr('title') )); | 
| 139 |  |  |  |  |  |  |  | 
| 140 |  |  |  |  |  |  |  | 
| 141 | 2 |  |  |  |  | 22 | my $text = $description->find_by_tag_name('pre')->as_text; | 
| 142 | 2 |  |  |  |  | 298 | $text =~ s/^\s+//; | 
| 143 | 2 |  |  |  |  | 54 | $text =~ s/\s+$//; | 
| 144 | 2 |  |  |  |  | 26 | $text =~ s/\r\n/\n/g; | 
| 145 | 2 |  |  |  |  | 23 | $self->description( $text ); | 
| 146 |  |  |  |  |  |  |  | 
| 147 | 2 |  |  |  |  | 15 | my $att_tag = $description->look_down( class => 'attachments' ); | 
| 148 | 2 |  |  |  |  | 224 | my @attachments; | 
| 149 | 2 | 100 |  |  |  | 122 | @attachments = | 
| 150 |  |  |  |  |  |  | Net::Google::Code::Issue::Attachment->parse_attachments($att_tag) | 
| 151 |  |  |  |  |  |  | if $att_tag; | 
| 152 | 2 |  |  |  |  | 27 | $self->attachments( \@attachments ); | 
| 153 |  |  |  |  |  |  |  | 
| 154 | 2 |  |  |  |  | 11 | my ($meta) = $tree->look_down( id => 'issuemeta' ); | 
| 155 |  |  |  |  |  |  | { | 
| 156 |  |  |  |  |  |  |  | 
| 157 |  |  |  |  |  |  | # let's find stars | 
| 158 | 2 |  |  |  |  | 3144 | my ($header) = $tree->look_down( id => 'issueheader' ); | 
|  | 2 |  |  |  |  | 12 |  | 
| 159 | 2 | 50 | 33 |  |  | 2983 | if (   $header | 
| 160 |  |  |  |  |  |  | && $header->as_text =~ /(\d+) \w+ starred this issue/ ) | 
| 161 |  |  |  |  |  |  | { | 
| 162 |  |  |  |  |  |  | # the \w+ is person or people, I don't know if google will change that word | 
| 163 |  |  |  |  |  |  | # some time, so just use \w+ | 
| 164 | 2 |  |  |  |  | 284 | my $stars = $1; | 
| 165 | 2 |  |  |  |  | 32 | $self->stars($stars); | 
| 166 |  |  |  |  |  |  | } | 
| 167 |  |  |  |  |  |  | } | 
| 168 |  |  |  |  |  |  |  | 
| 169 | 2 |  |  |  |  | 12 | my @meta = $meta->find_by_tag_name('tr'); | 
| 170 | 2 |  |  |  |  | 336 | my @labels; | 
| 171 | 2 |  |  |  |  | 6 | for my $meta (@meta) { | 
| 172 | 9 |  |  |  |  | 12 | my ( $key, $value ); | 
| 173 | 9 | 100 |  |  |  | 23 | if ( my $k = $meta->find_by_tag_name('th') ) { | 
| 174 | 5 |  |  |  |  | 132 | my $v         = $meta->find_by_tag_name('td'); | 
| 175 | 5 |  |  |  |  | 156 | my $k_content = $k->content_array_ref->[0]; | 
| 176 | 5 |  |  |  |  | 52 | while ( ref $k_content ) { | 
| 177 | 0 |  |  |  |  | 0 | $k_content = $k_content->content_array_ref->[0]; | 
| 178 |  |  |  |  |  |  | } | 
| 179 | 5 |  |  |  |  | 8 | $key = $k_content;    # $key is like 'Status:#' | 
| 180 | 5 |  |  |  |  | 21 | $key =~ s/:.$//;      # s/:#$// doesn't work, no idea why | 
| 181 | 5 |  |  |  |  | 12 | $key = lc $key; | 
| 182 |  |  |  |  |  |  |  | 
| 183 | 5 | 50 |  |  |  | 11 | if ($v) { | 
| 184 | 5 |  |  |  |  | 18 | $value = $v->as_text; | 
| 185 | 5 |  |  |  |  | 115 | $value =~ s/^\s+//; | 
| 186 | 5 |  |  |  |  | 16 | $value =~ s/\s+$//; | 
| 187 |  |  |  |  |  |  | } | 
| 188 |  |  |  |  |  |  |  | 
| 189 | 5 | 50 |  |  |  | 59 | if ( $self->can( $key ) ) { | 
| 190 | 5 | 50 | 33 |  |  | 17 | if ( $key eq 'merged' && $value =~ /issue\s+(\d+)/ ) { | 
| 191 | 0 |  |  |  |  | 0 | $value = $1; | 
| 192 |  |  |  |  |  |  | } | 
| 193 | 5 |  |  |  |  | 39 | $self->$key( $value ); | 
| 194 |  |  |  |  |  |  | } | 
| 195 |  |  |  |  |  |  | else { | 
| 196 | 0 |  |  |  |  | 0 | warn "no idea where to keep $key"; | 
| 197 |  |  |  |  |  |  | } | 
| 198 |  |  |  |  |  |  | } | 
| 199 |  |  |  |  |  |  | else { | 
| 200 | 4 |  |  |  |  | 170 | my $href = $meta->look_down( class => 'label' )->attr('href'); | 
| 201 | 4 | 50 |  |  |  | 224 | if ( $href =~ /list\?q=label:(.+)/ ) { | 
| 202 | 4 |  |  |  |  | 14 | push @labels, $1; | 
| 203 |  |  |  |  |  |  | } | 
| 204 |  |  |  |  |  |  | } | 
| 205 |  |  |  |  |  |  | } | 
| 206 | 2 |  |  |  |  | 20 | $self->labels( \@labels ); | 
| 207 |  |  |  |  |  |  |  | 
| 208 |  |  |  |  |  |  | # extract comments | 
| 209 | 2 |  |  |  |  | 9 | my @comments_tag = $tree->look_down( class => 'vt issuecomment' ); | 
| 210 | 2 |  |  |  |  | 3237 | my @comments; | 
| 211 | 2 |  |  |  |  | 6 | for my $tag (@comments_tag) { | 
| 212 | 9 | 100 |  |  |  | 32 | next unless $tag->look_down( class => 'author' ); | 
| 213 | 7 |  |  |  |  | 405 | my $comment = | 
| 214 |  |  |  |  |  |  | Net::Google::Code::Issue::Comment->new( project => $self->project ); | 
| 215 | 7 |  |  |  |  | 78 | $comment->parse($tag); | 
| 216 | 7 |  |  |  |  | 15 | push @comments, $comment; | 
| 217 |  |  |  |  |  |  | } | 
| 218 |  |  |  |  |  |  |  | 
| 219 | 2 |  |  |  |  | 178 | my $initial_comment = Net::Google::Code::Issue::Comment->new( | 
| 220 |  |  |  |  |  |  | project     => $self->project, | 
| 221 |  |  |  |  |  |  | sequence    => 0, | 
| 222 |  |  |  |  |  |  | date        => $self->reported, | 
| 223 |  |  |  |  |  |  | author      => $self->reporter, | 
| 224 |  |  |  |  |  |  | content     => $self->description, | 
| 225 |  |  |  |  |  |  | attachments => $self->attachments, | 
| 226 |  |  |  |  |  |  | ); | 
| 227 |  |  |  |  |  |  |  | 
| 228 | 2 |  |  |  |  | 8 | my @initial_labels = @{$self->labels}; | 
|  | 2 |  |  |  |  | 15 |  | 
| 229 | 2 |  |  |  |  | 6 | my %meta = map { $_ => 1 } qw/summary status cc owner/; | 
|  | 8 |  |  |  |  | 26 |  | 
| 230 | 2 |  |  |  |  | 8 | for my $c ( reverse @comments ) { | 
| 231 | 7 |  |  |  |  | 22 | my $updates = $c->updates; | 
| 232 | 7 |  |  |  |  | 20 | for ( keys %meta ) { | 
| 233 |  |  |  |  |  |  | # once these changes, we can't know the inital value | 
| 234 | 23 | 100 |  |  |  | 64 | delete $meta{$_} if exists $updates->{$_}; | 
| 235 |  |  |  |  |  |  | } | 
| 236 | 7 | 50 |  |  |  | 27 | if ( $updates->{labels} ) { | 
| 237 | 0 |  |  |  |  | 0 | my @labels = @{$updates->{labels}}; | 
|  | 0 |  |  |  |  | 0 |  | 
| 238 | 0 |  |  |  |  | 0 | for my $label (@labels) { | 
| 239 | 0 | 0 |  |  |  | 0 | if ( $label =~ /^-(.*)$/ ) { | 
| 240 | 0 |  |  |  |  | 0 | unshift @initial_labels, $1; | 
| 241 |  |  |  |  |  |  | } | 
| 242 |  |  |  |  |  |  | else { | 
| 243 | 0 |  |  |  |  | 0 | @initial_labels = grep { $_ ne $label } @initial_labels; | 
|  | 0 |  |  |  |  | 0 |  | 
| 244 |  |  |  |  |  |  | } | 
| 245 |  |  |  |  |  |  | } | 
| 246 |  |  |  |  |  |  | } | 
| 247 |  |  |  |  |  |  | } | 
| 248 |  |  |  |  |  |  |  | 
| 249 | 2 |  |  |  |  | 31 | $initial_comment->updates->{labels} = \@initial_labels; | 
| 250 | 2 |  |  |  |  | 9 | for ( keys %meta ) { | 
| 251 | 6 |  |  |  |  | 38 | $initial_comment->updates->{$_} = $self->$_; | 
| 252 |  |  |  |  |  |  | } | 
| 253 |  |  |  |  |  |  |  | 
| 254 | 2 |  |  |  |  | 8 | unshift @comments, $initial_comment; | 
| 255 |  |  |  |  |  |  |  | 
| 256 | 2 |  |  |  |  | 39 | $self->comments( \@comments ); | 
| 257 | 2 | 50 |  |  |  | 20 | $tree->delete if $need_delete; | 
| 258 | 2 |  |  |  |  | 7907 | return 1; | 
| 259 |  |  |  |  |  |  | } | 
| 260 |  |  |  |  |  |  |  | 
| 261 |  |  |  |  |  |  | sub load_comments { | 
| 262 | 0 |  |  | 0 | 1 | 0 | my $self = shift; | 
| 263 | 0 |  |  |  |  | 0 | require Net::Google::Code::Issue::Comment; | 
| 264 | 0 |  |  |  |  | 0 | my $comment = Net::Google::Code::Issue::Comment->new( | 
| 265 |  |  |  |  |  |  | issue_id => $self->id, | 
| 266 | 0 |  |  |  |  | 0 | map { $_ => $self->$_ } | 
| 267 | 0 |  |  |  |  | 0 | grep { $self->$_ } qw/project email password token/ | 
| 268 |  |  |  |  |  |  | ); | 
| 269 |  |  |  |  |  |  |  | 
| 270 |  |  |  |  |  |  | # $comment is for initial comment we will work out | 
| 271 | 0 |  |  |  |  | 0 | $self->comments( [ $comment, $comment->list ] ); | 
| 272 |  |  |  |  |  |  | } | 
| 273 |  |  |  |  |  |  |  | 
| 274 |  |  |  |  |  |  | sub parse_hybrid { | 
| 275 | 0 |  |  | 0 | 1 | 0 | my $self    = shift; | 
| 276 | 0 |  |  |  |  | 0 | my $tree    = shift; | 
| 277 | 0 |  |  |  |  | 0 | my $need_delete = not blessed $tree; | 
| 278 | 0 | 0 |  |  |  | 0 | $tree = $self->html_tree( html => $tree ) unless blessed $tree; | 
| 279 |  |  |  |  |  |  |  | 
| 280 | 0 |  |  |  |  | 0 | my $description = $tree->look_down( class => 'vt issuedescription' ); | 
| 281 | 0 |  |  |  |  | 0 | my $att_tag = $description->look_down( class => 'attachments' ); | 
| 282 | 0 |  |  |  |  | 0 | my @attachments; | 
| 283 | 0 | 0 |  |  |  | 0 | @attachments = | 
| 284 |  |  |  |  |  |  | Net::Google::Code::Issue::Attachment->parse_attachments($att_tag) | 
| 285 |  |  |  |  |  |  | if $att_tag; | 
| 286 | 0 |  |  |  |  | 0 | $self->attachments( \@attachments ); | 
| 287 |  |  |  |  |  |  |  | 
| 288 | 0 |  |  |  |  | 0 | my ($meta) = $tree->look_down( id => 'issuemeta' ); | 
| 289 | 0 |  |  |  |  | 0 | my @meta = $meta->find_by_tag_name('tr'); | 
| 290 | 0 |  |  |  |  | 0 | my @labels; | 
| 291 | 0 |  |  |  |  | 0 | for my $meta (@meta) { | 
| 292 |  |  |  |  |  |  |  | 
| 293 | 0 |  |  |  |  | 0 | my ( $key, $value ); | 
| 294 | 0 | 0 |  |  |  | 0 | if ( my $k = $meta->find_by_tag_name('th') ) { | 
| 295 | 0 |  |  |  |  | 0 | my $v         = $meta->find_by_tag_name('td'); | 
| 296 | 0 |  |  |  |  | 0 | my $k_content = $k->content_array_ref->[0]; | 
| 297 | 0 |  |  |  |  | 0 | while ( ref $k_content ) { | 
| 298 | 0 |  |  |  |  | 0 | $k_content = $k_content->content_array_ref->[0]; | 
| 299 |  |  |  |  |  |  | } | 
| 300 | 0 |  |  |  |  | 0 | $key = $k_content;    # $key is like 'Status:#' | 
| 301 | 0 |  |  |  |  | 0 | $key =~ s/:.$//;      # s/:#$// doesn't work, no idea why | 
| 302 | 0 |  |  |  |  | 0 | $key = lc $key; | 
| 303 |  |  |  |  |  |  |  | 
| 304 | 0 | 0 |  |  |  | 0 | if ($v) { | 
| 305 | 0 |  |  |  |  | 0 | $value = $v->as_text; | 
| 306 | 0 |  |  |  |  | 0 | $value =~ s/^\s+//; | 
| 307 | 0 |  |  |  |  | 0 | $value =~ s/\s+$//; | 
| 308 |  |  |  |  |  |  | } | 
| 309 | 0 | 0 |  |  |  | 0 | if ( $self->can( $key ) ) { | 
| 310 | 0 | 0 | 0 |  |  | 0 | if ( $key eq 'merged' && $value =~ /issue\s+(\d+)/ ) { | 
| 311 | 0 |  |  |  |  | 0 | $value = $1; | 
| 312 |  |  |  |  |  |  | } | 
| 313 | 0 |  |  |  |  | 0 | $self->$key( $value ); | 
| 314 |  |  |  |  |  |  | } | 
| 315 |  |  |  |  |  |  | else { | 
| 316 | 0 |  |  |  |  | 0 | warn "no idea where to keep $key"; | 
| 317 |  |  |  |  |  |  | } | 
| 318 |  |  |  |  |  |  | } | 
| 319 |  |  |  |  |  |  | } | 
| 320 |  |  |  |  |  |  |  | 
| 321 |  |  |  |  |  |  | # extract comments | 
| 322 | 0 |  |  |  |  | 0 | my @comments_tag = $tree->look_down( class => 'vt issuecomment' ); | 
| 323 | 0 |  |  |  |  | 0 | ( undef, my @comments ) = @{$self->comments}; | 
|  | 0 |  |  |  |  | 0 |  | 
| 324 | 0 |  |  |  |  | 0 | my $number = 1; # 0 is for initial comment | 
| 325 | 0 |  |  |  |  | 0 | for my $tag (@comments_tag) { | 
| 326 | 0 | 0 |  |  |  | 0 | next unless $tag->look_down( class => 'author' ); | 
| 327 | 0 |  |  |  |  | 0 | my $comment = $self->comments->[$number++]; | 
| 328 | 0 |  |  |  |  | 0 | $comment->parse_hybrid($tag); | 
| 329 |  |  |  |  |  |  | } | 
| 330 |  |  |  |  |  |  |  | 
| 331 | 0 |  |  |  |  | 0 | my $initial_comment = Net::Google::Code::Issue::Comment->new( | 
| 332 |  |  |  |  |  |  | sequence    => 0, | 
| 333 |  |  |  |  |  |  | date        => $self->reported, | 
| 334 |  |  |  |  |  |  | author      => $self->reporter, | 
| 335 |  |  |  |  |  |  | content     => $self->description, | 
| 336 |  |  |  |  |  |  | attachments => $self->attachments, | 
| 337 |  |  |  |  |  |  | issue_id    => $self->id, | 
| 338 | 0 |  |  |  |  | 0 | map { $_ => $self->$_ } | 
| 339 | 0 |  |  |  |  | 0 | grep { $self->$_ } qw/project email password token/ | 
| 340 |  |  |  |  |  |  | ); | 
| 341 |  |  |  |  |  |  |  | 
| 342 | 0 |  |  |  |  | 0 | my @initial_labels = @{$self->labels}; | 
|  | 0 |  |  |  |  | 0 |  | 
| 343 | 0 |  |  |  |  | 0 | my %meta = map { $_ => 1 } qw/summary status cc owner/; | 
|  | 0 |  |  |  |  | 0 |  | 
| 344 | 0 |  |  |  |  | 0 | for my $c ( reverse @comments ) { | 
| 345 | 0 |  |  |  |  | 0 | my $updates = $c->updates; | 
| 346 | 0 |  |  |  |  | 0 | for ( keys %meta ) { | 
| 347 |  |  |  |  |  |  | # once these changes, we can't know the inital value | 
| 348 | 0 | 0 |  |  |  | 0 | delete $meta{$_} if exists $updates->{$_}; | 
| 349 |  |  |  |  |  |  | } | 
| 350 | 0 | 0 |  |  |  | 0 | if ( $updates->{labels} ) { | 
| 351 | 0 |  |  |  |  | 0 | my @labels = @{$updates->{labels}}; | 
|  | 0 |  |  |  |  | 0 |  | 
| 352 | 0 |  |  |  |  | 0 | for my $label (@labels) { | 
| 353 | 0 | 0 |  |  |  | 0 | if ( $label =~ /^-(.*)$/ ) { | 
| 354 | 0 |  |  |  |  | 0 | unshift @initial_labels, $1; | 
| 355 |  |  |  |  |  |  | } | 
| 356 |  |  |  |  |  |  | else { | 
| 357 | 0 |  |  |  |  | 0 | @initial_labels = grep { $_ ne $label } @initial_labels; | 
|  | 0 |  |  |  |  | 0 |  | 
| 358 |  |  |  |  |  |  | } | 
| 359 |  |  |  |  |  |  | } | 
| 360 |  |  |  |  |  |  | } | 
| 361 |  |  |  |  |  |  | } | 
| 362 |  |  |  |  |  |  |  | 
| 363 | 0 |  |  |  |  | 0 | $initial_comment->updates->{labels} = \@initial_labels; | 
| 364 | 0 |  |  |  |  | 0 | for ( keys %meta ) { | 
| 365 | 0 |  |  |  |  | 0 | $initial_comment->updates->{$_} = $self->$_; | 
| 366 |  |  |  |  |  |  | } | 
| 367 | 0 |  |  |  |  | 0 | $self->comments->[0] = $initial_comment; | 
| 368 | 0 | 0 |  |  |  | 0 | $tree->delete if $need_delete; | 
| 369 | 0 |  |  |  |  | 0 | return 1; | 
| 370 |  |  |  |  |  |  | } | 
| 371 |  |  |  |  |  |  |  | 
| 372 |  |  |  |  |  |  | sub _load_from_xml { | 
| 373 | 22 |  |  | 22 |  | 30 | my $self = shift; | 
| 374 | 22 |  |  |  |  | 105 | my $ref = | 
| 375 |  |  |  |  |  |  | Net::Google::Code::Issue::Util->translate_from_xml( shift, | 
| 376 |  |  |  |  |  |  | type => 'issue' ); | 
| 377 |  |  |  |  |  |  |  | 
| 378 | 22 |  |  |  |  | 100 | for my $k ( keys %$ref ) { | 
| 379 | 332 | 100 |  |  |  | 984 | if ( $self->can($k) ) { | 
| 380 | 222 |  |  |  |  | 495 | $self->{$k} = $ref->{$k}; | 
| 381 |  |  |  |  |  |  | } | 
| 382 |  |  |  |  |  |  | } | 
| 383 | 22 |  |  |  |  | 91 | return $self; | 
| 384 |  |  |  |  |  |  | } | 
| 385 |  |  |  |  |  |  |  | 
| 386 |  |  |  |  |  |  | sub create { | 
| 387 | 0 |  |  | 0 | 1 | 0 | my $self = shift; | 
| 388 | 0 |  |  |  |  | 0 | my %args = validate( | 
| 389 |  |  |  |  |  |  | @_, | 
| 390 |  |  |  |  |  |  | { | 
| 391 |  |  |  |  |  |  | labels => { type => ARRAYREF, optional => 1 }, | 
| 392 |  |  |  |  |  |  | files  => { type => ARRAYREF, optional => 1 }, | 
| 393 | 0 |  |  |  |  | 0 | map { $_ => { type => SCALAR, optional => 1 } } | 
| 394 |  |  |  |  |  |  | qw/comment summary status owner cc/, | 
| 395 |  |  |  |  |  |  | } | 
| 396 |  |  |  |  |  |  | ); | 
| 397 |  |  |  |  |  |  |  | 
| 398 | 0 | 0 | 0 |  |  | 0 | if ( $args{files} || !$USE_HYBRID) { | 
| 399 | 0 |  |  |  |  | 0 | $self->sign_in; | 
| 400 | 0 |  |  |  |  | 0 | $self->fetch( $self->base_url . 'issues/entry' ); | 
| 401 |  |  |  |  |  |  |  | 
| 402 | 0 | 0 |  |  |  | 0 | if ( $args{files} ) { | 
| 403 |  |  |  |  |  |  |  | 
| 404 |  |  |  |  |  |  | # hack hack hack | 
| 405 |  |  |  |  |  |  | # manually add file fields since we don't have them in page. | 
| 406 | 0 |  |  |  |  | 0 | my $html = $self->mech->content; | 
| 407 | 0 |  |  |  |  | 0 | for ( 1 .. @{ $args{files} } ) { | 
|  | 0 |  |  |  |  | 0 |  | 
| 408 | 0 |  |  |  |  | 0 | $html =~ | 
| 409 |  |  |  |  |  |  | s{(?<=id="attachmentareadeventry">)}{}; | 
| 410 |  |  |  |  |  |  | } | 
| 411 | 0 |  |  |  |  | 0 | $self->mech->update_html($html); | 
| 412 |  |  |  |  |  |  | } | 
| 413 |  |  |  |  |  |  |  | 
| 414 | 0 |  |  |  |  | 0 | $self->mech->form_with_fields( 'comment', 'summary' ); | 
| 415 |  |  |  |  |  |  |  | 
| 416 |  |  |  |  |  |  | # leave labels alone unless there're labels. | 
| 417 | 0 | 0 |  |  |  | 0 | $self->mech->field( 'label', $args{labels} ) if $args{labels}; | 
| 418 |  |  |  |  |  |  |  | 
| 419 | 0 | 0 |  |  |  | 0 | if ( $args{files} ) { | 
| 420 | 0 |  |  |  |  | 0 | for ( my $i = 0 ; $i < scalar @{ $args{files} } ; $i++ ) { | 
|  | 0 |  |  |  |  | 0 |  | 
| 421 | 0 |  |  |  |  | 0 | $self->mech->field( 'file' . ( $i + 1 ), $args{files}[$i] ); | 
| 422 |  |  |  |  |  |  | } | 
| 423 |  |  |  |  |  |  | } | 
| 424 |  |  |  |  |  |  |  | 
| 425 |  |  |  |  |  |  | $self->mech->submit_form( | 
| 426 | 0 |  |  |  |  | 0 | fields => { | 
| 427 | 0 |  |  |  |  | 0 | map { $_ => $args{$_} } | 
| 428 | 0 |  |  |  |  | 0 | grep { exists $args{$_} } qw/comment summary status owner cc/ | 
| 429 |  |  |  |  |  |  | } | 
| 430 |  |  |  |  |  |  | ); | 
| 431 |  |  |  |  |  |  |  | 
| 432 | 0 |  |  |  |  | 0 | my ( $contains, $id ) = $self->html_tree_contains( | 
| 433 |  |  |  |  |  |  | html      => $self->mech->content, | 
| 434 |  |  |  |  |  |  | look_down => [ class => 'notice' ], | 
| 435 |  |  |  |  |  |  | as_text   => qr/Issue\s+(\d+)/i, | 
| 436 |  |  |  |  |  |  | ); | 
| 437 |  |  |  |  |  |  |  | 
| 438 | 0 | 0 |  |  |  | 0 | if ($contains) { | 
| 439 | 0 |  |  |  |  | 0 | $self->load($id); | 
| 440 | 0 |  |  |  |  | 0 | return $id; | 
| 441 |  |  |  |  |  |  | } | 
| 442 |  |  |  |  |  |  | else { | 
| 443 | 0 |  |  |  |  | 0 | warn 'create issue failed'; | 
| 444 | 0 |  |  |  |  | 0 | return; | 
| 445 |  |  |  |  |  |  | } | 
| 446 |  |  |  |  |  |  | } | 
| 447 |  |  |  |  |  |  | else { | 
| 448 |  |  |  |  |  |  |  | 
| 449 |  |  |  |  |  |  | # we can use google's official api here | 
| 450 | 0 |  |  |  |  | 0 | my $author = $self->email; | 
| 451 | 0 |  |  |  |  | 0 | $author =~ s/@.*//; | 
| 452 | 0 |  |  |  |  | 0 | my %args = ( author => $author, @_ ); | 
| 453 |  |  |  |  |  |  |  | 
| 454 | 0 |  |  |  |  | 0 | my $xml = | 
| 455 |  |  |  |  |  |  | Net::Google::Code::Issue::Util->translate_to_xml( \%args, | 
| 456 |  |  |  |  |  |  | type => 'create' ); | 
| 457 | 0 |  |  |  |  | 0 | my $ua = $self->ua; | 
| 458 |  |  |  |  |  |  |  | 
| 459 | 0 |  |  |  |  | 0 | my $url     = $self->feeds_issues_url . '/full'; | 
| 460 | 0 |  |  |  |  | 0 | my $request = HTTP::Request->new( 'POST', $url, undef, $xml ); | 
| 461 | 0 |  |  |  |  | 0 | my $res     = $ua->request($request); | 
| 462 | 0 | 0 |  |  |  | 0 | if ( $res->is_success ) { | 
| 463 | 0 |  |  |  |  | 0 | my $content = $res->content; | 
| 464 |  |  |  |  |  |  |  | 
| 465 |  |  |  |  |  |  | # let's fake wrap the entry with | 
| 466 | 0 |  |  |  |  | 0 | $content =~ s! | 
| 467 | 0 |  |  |  |  | 0 | $content =~ s{$}{}; | 
| 468 | 0 |  |  |  |  | 0 | my $feed = XML::FeedPP->new($content); | 
| 469 | 0 |  |  |  |  | 0 | my ($item) = $feed->get_item; | 
| 470 | 0 |  |  |  |  | 0 | $self->_load_from_xml($item); | 
| 471 | 0 |  |  |  |  | 0 | $self->load( $self->id ); | 
| 472 | 0 |  |  |  |  | 0 | return 1; | 
| 473 |  |  |  |  |  |  | } | 
| 474 |  |  |  |  |  |  | else { | 
| 475 | 0 |  |  |  |  | 0 | die "try to POST $url failed: " | 
| 476 |  |  |  |  |  |  | . $res->status_line . "\n" | 
| 477 |  |  |  |  |  |  | . $res->content; | 
| 478 |  |  |  |  |  |  | } | 
| 479 |  |  |  |  |  |  |  | 
| 480 |  |  |  |  |  |  | } | 
| 481 |  |  |  |  |  |  | } | 
| 482 |  |  |  |  |  |  |  | 
| 483 |  |  |  |  |  |  | sub update { | 
| 484 | 0 |  |  | 0 | 1 | 0 | my $self = shift; | 
| 485 | 0 |  |  |  |  | 0 | my %args = validate( | 
| 486 |  |  |  |  |  |  | @_, | 
| 487 |  |  |  |  |  |  | { | 
| 488 |  |  |  |  |  |  | labels => { type => ARRAYREF, optional => 1 }, | 
| 489 |  |  |  |  |  |  | files  => { type => ARRAYREF, optional => 1 }, | 
| 490 | 0 |  |  |  |  | 0 | map { $_ => { type => SCALAR, optional => 1 } } | 
| 491 |  |  |  |  |  |  | qw/comment summary status owner merge_into cc blocked_on/, | 
| 492 |  |  |  |  |  |  | } | 
| 493 |  |  |  |  |  |  | ); | 
| 494 |  |  |  |  |  |  |  | 
| 495 | 0 | 0 | 0 |  |  | 0 | if (   $args{files} | 
|  |  |  | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
| 496 |  |  |  |  |  |  | || $args{merge_into} | 
| 497 |  |  |  |  |  |  | || $args{blocked_on} | 
| 498 |  |  |  |  |  |  | || !$USE_HYBRID ) | 
| 499 |  |  |  |  |  |  | { | 
| 500 |  |  |  |  |  |  |  | 
| 501 | 0 |  |  |  |  | 0 | $self->sign_in; | 
| 502 | 0 |  |  |  |  | 0 | $self->fetch( $self->base_url . 'issues/detail?id=' . $self->id ); | 
| 503 |  |  |  |  |  |  |  | 
| 504 | 0 | 0 |  |  |  | 0 | if ( $args{files} ) { | 
| 505 |  |  |  |  |  |  |  | 
| 506 |  |  |  |  |  |  | # hack hack hack | 
| 507 |  |  |  |  |  |  | # manually add file fields since we don't have them in page. | 
| 508 | 0 |  |  |  |  | 0 | my $html = $self->mech->content; | 
| 509 | 0 |  |  |  |  | 0 | for ( 1 .. @{ $args{files} } ) { | 
|  | 0 |  |  |  |  | 0 |  | 
| 510 | 0 |  |  |  |  | 0 | $html =~ | 
| 511 |  |  |  |  |  |  | s{(?<=id="attachmentarea">)}{}; | 
| 512 |  |  |  |  |  |  | } | 
| 513 | 0 |  |  |  |  | 0 | $self->mech->update_html($html); | 
| 514 |  |  |  |  |  |  | } | 
| 515 |  |  |  |  |  |  |  | 
| 516 | 0 |  |  |  |  | 0 | $self->mech->form_with_fields( 'comment', 'summary' ); | 
| 517 |  |  |  |  |  |  |  | 
| 518 |  |  |  |  |  |  | # leave labels alone unless there're labels. | 
| 519 | 0 | 0 |  |  |  | 0 | $self->mech->field( 'label', $args{labels} ) if $args{labels}; | 
| 520 | 0 | 0 |  |  |  | 0 | if ( $args{files} ) { | 
| 521 | 0 |  |  |  |  | 0 | for ( my $i = 0 ; $i < scalar @{ $args{files} } ; $i++ ) { | 
|  | 0 |  |  |  |  | 0 |  | 
| 522 | 0 |  |  |  |  | 0 | $self->mech->field( 'file' . ( $i + 1 ), $args{files}[$i] ); | 
| 523 |  |  |  |  |  |  | } | 
| 524 |  |  |  |  |  |  | } | 
| 525 |  |  |  |  |  |  |  | 
| 526 |  |  |  |  |  |  | $self->mech->submit_form( | 
| 527 | 0 |  |  |  |  | 0 | fields => { | 
| 528 | 0 |  |  |  |  | 0 | map { $_ => $args{$_} } | 
| 529 | 0 |  |  |  |  | 0 | grep { exists $args{$_} } | 
| 530 |  |  |  |  |  |  | qw/comment summary status owner merge_into cc blocked_on/ | 
| 531 |  |  |  |  |  |  | } | 
| 532 |  |  |  |  |  |  | ); | 
| 533 |  |  |  |  |  |  |  | 
| 534 | 0 | 0 |  |  |  | 0 | if ( | 
| 535 |  |  |  |  |  |  | $self->html_tree_contains( | 
| 536 |  |  |  |  |  |  | html      => $self->mech->content, | 
| 537 |  |  |  |  |  |  | look_down => [ class => 'notice' ], | 
| 538 |  |  |  |  |  |  | as_text   => qr/has been updated/, | 
| 539 |  |  |  |  |  |  | ) | 
| 540 |  |  |  |  |  |  | ) | 
| 541 |  |  |  |  |  |  | { | 
| 542 | 0 |  |  |  |  | 0 | $self->load( $self->id );    # maybe this is too much? | 
| 543 | 0 |  |  |  |  | 0 | return 1; | 
| 544 |  |  |  |  |  |  | } | 
| 545 |  |  |  |  |  |  | else { | 
| 546 | 0 |  |  |  |  | 0 | warn 'update failed'; | 
| 547 | 0 |  |  |  |  | 0 | return; | 
| 548 |  |  |  |  |  |  | } | 
| 549 |  |  |  |  |  |  | } | 
| 550 |  |  |  |  |  |  | else { | 
| 551 | 0 |  |  |  |  | 0 | my $author = $self->email; | 
| 552 | 0 |  |  |  |  | 0 | $author =~ s/@.*//; | 
| 553 | 0 |  |  |  |  | 0 | my %args = ( | 
| 554 |  |  |  |  |  |  | author => $author, | 
| 555 |  |  |  |  |  |  | ( | 
| 556 | 0 |  |  |  |  | 0 | map { $_ => $self->$_ } qw/title content status owner cc labels/ | 
| 557 |  |  |  |  |  |  | ), | 
| 558 |  |  |  |  |  |  | @_, | 
| 559 |  |  |  |  |  |  | ); | 
| 560 |  |  |  |  |  |  |  | 
| 561 | 0 |  |  |  |  | 0 | my $xml = | 
| 562 |  |  |  |  |  |  | Net::Google::Code::Issue::Util->translate_to_xml( \%args, | 
| 563 |  |  |  |  |  |  | type => 'update' ); | 
| 564 | 0 |  |  |  |  | 0 | my $ua  = $self->ua; | 
| 565 | 0 |  |  |  |  | 0 | my $url = $self->feeds_issues_url . '/' . $self->id . '/comments/full'; | 
| 566 |  |  |  |  |  |  |  | 
| 567 | 0 |  |  |  |  | 0 | my $request = HTTP::Request->new( 'POST', $url, undef, $xml ); | 
| 568 | 0 |  |  |  |  | 0 | my $res = $ua->request($request); | 
| 569 | 0 | 0 |  |  |  | 0 | if ( $res->is_success ) { | 
| 570 | 0 |  |  |  |  | 0 | $self->load( $self->id );    # let's reload | 
| 571 | 0 |  |  |  |  | 0 | return 1; | 
| 572 |  |  |  |  |  |  | } | 
| 573 |  |  |  |  |  |  | else { | 
| 574 | 0 |  |  |  |  | 0 | die "try to POST $url failed: " | 
| 575 |  |  |  |  |  |  | . $res->status_line . "\n" | 
| 576 |  |  |  |  |  |  | . $res->content; | 
| 577 |  |  |  |  |  |  | } | 
| 578 |  |  |  |  |  |  |  | 
| 579 |  |  |  |  |  |  | } | 
| 580 |  |  |  |  |  |  | } | 
| 581 |  |  |  |  |  |  |  | 
| 582 |  |  |  |  |  |  | sub updated { | 
| 583 | 1 |  |  | 1 | 1 | 20261 | my $self = shift; | 
| 584 | 1 |  |  |  |  | 6 | my $last_comment = $self->comments->[-1]; | 
| 585 | 1 | 50 |  |  |  | 28 | return $last_comment ? $last_comment->date : undef; | 
| 586 |  |  |  |  |  |  | } | 
| 587 |  |  |  |  |  |  |  | 
| 588 |  |  |  |  |  |  | sub list { | 
| 589 | 2 |  |  | 2 | 1 | 19 | my $self = shift; | 
| 590 | 2 |  |  |  |  | 120 | validate( | 
| 591 |  |  |  |  |  |  | @_, | 
| 592 |  |  |  |  |  |  | { | 
| 593 |  |  |  |  |  |  | q             => { optional => 1, type => SCALAR }, | 
| 594 |  |  |  |  |  |  | can           => { optional => 1, type => SCALAR }, | 
| 595 |  |  |  |  |  |  | author        => { optional => 1, type => SCALAR }, | 
| 596 |  |  |  |  |  |  | id            => { optional => 1, type => SCALAR }, | 
| 597 |  |  |  |  |  |  | label         => { optional => 1, type => SCALAR }, | 
| 598 |  |  |  |  |  |  | max_results   => { optional => 1, type => SCALAR }, | 
| 599 |  |  |  |  |  |  | owner         => { optional => 1, type => SCALAR }, | 
| 600 |  |  |  |  |  |  | published_min => { optional => 1, type => SCALAR }, | 
| 601 |  |  |  |  |  |  | published_max => { optional => 1, type => SCALAR }, | 
| 602 |  |  |  |  |  |  | updated_min   => { optional => 1, type => SCALAR }, | 
| 603 |  |  |  |  |  |  | updated_max   => { optional => 1, type => SCALAR }, | 
| 604 |  |  |  |  |  |  | start_index   => { optional => 1, type => SCALAR }, | 
| 605 |  |  |  |  |  |  | } | 
| 606 |  |  |  |  |  |  | ); | 
| 607 |  |  |  |  |  |  |  | 
| 608 | 2 |  |  |  |  | 22 | my %args = @_; | 
| 609 | 2 |  |  |  |  | 19 | my $url = $self->feeds_issues_url . '/full?'; | 
| 610 | 2 |  |  |  |  | 19 | require URI::Escape; | 
| 611 | 2 |  |  |  |  | 8 | for my $k ( keys %args ) { | 
| 612 | 1 | 50 |  |  |  | 6 | next unless $args{$k}; | 
| 613 | 1 |  |  |  |  | 2 | my $v = $args{$k}; | 
| 614 | 1 |  |  |  |  | 3 | $k =~ s/_/-/g; | 
| 615 | 1 |  |  |  |  | 7 | $url .= "$k=" . URI::Escape::uri_escape($v) . '&'; | 
| 616 |  |  |  |  |  |  | } | 
| 617 |  |  |  |  |  |  |  | 
| 618 | 2 |  |  |  |  | 58 | my $ua  = $self->ua; | 
| 619 | 2 |  |  |  |  | 11 | my $res = $ua->get($url); | 
| 620 | 2 | 50 |  |  |  | 157 | if ( $res->is_success ) { | 
| 621 | 2 |  |  |  |  | 113 | my $feed = XML::FeedPP->new($res->content); | 
| 622 | 2 |  |  |  |  | 58594 | my @items = $feed->get_item; | 
| 623 | 22 |  |  |  |  | 495 | my @list = map { | 
| 624 | 2 |  |  |  |  | 35 | my $t = Net::Google::Code::Issue->new( | 
| 625 |  |  |  |  |  |  | loaded_way => 'api', | 
| 626 | 88 |  |  |  |  | 284 | map { $_ => $self->$_ } | 
| 627 | 22 |  |  |  |  | 50 | grep { $self->$_ } qw/project email password token/ | 
| 628 |  |  |  |  |  |  | ); | 
| 629 | 22 |  |  |  |  | 88 | $t->_load_from_xml($_); | 
| 630 |  |  |  |  |  |  | } @items; | 
| 631 | 2 | 50 |  |  |  | 233 | return wantarray ? @list : \@list; | 
| 632 |  |  |  |  |  |  | } | 
| 633 |  |  |  |  |  |  | else { | 
| 634 | 0 |  |  |  |  |  | die "try to get $url failed: " | 
| 635 |  |  |  |  |  |  | . $res->status_line . "\n" | 
| 636 |  |  |  |  |  |  | . $res->content; | 
| 637 |  |  |  |  |  |  | } | 
| 638 |  |  |  |  |  |  | } | 
| 639 |  |  |  |  |  |  |  | 
| 640 | 7 |  |  | 7 |  | 77 | no Any::Moose; | 
|  | 7 |  |  |  |  | 14 |  | 
|  | 7 |  |  |  |  | 70 |  | 
| 641 |  |  |  |  |  |  | __PACKAGE__->meta->make_immutable; | 
| 642 |  |  |  |  |  |  |  | 
| 643 |  |  |  |  |  |  | 1; | 
| 644 |  |  |  |  |  |  |  | 
| 645 |  |  |  |  |  |  | __END__ |