File Coverage

blib/lib/Net/Gnats/PR.pm
Criterion Covered Total %
statement 205 245 83.6
branch 76 92 82.6
condition 16 19 84.2
subroutine 29 34 85.2
pod 16 18 88.8
total 342 408 83.8


line stmt bran cond sub pod time code
1             package Net::Gnats::PR;
2 40     40   997 use 5.010_000;
  40         153  
  40         1652  
3 40     40   205 use utf8;
  40         63  
  40         374  
4 40     40   904 use strictures;
  40         64  
  40         198  
5              
6             BEGIN {
7 40     40   3603 $Net::Gnats::PR::VERSION = '0.21';
8             }
9 40     40   220 use vars qw($VERSION);
  40         73  
  40         1774  
10              
11 40     40   211 use Carp;
  40         65  
  40         2818  
12 40     40   36773 use MIME::Base64;
  40         33336  
  40         2871  
13 40     40   636 use Net::Gnats::Constants qw(FROM_FIELD REPLYTO_FIELD TO_FIELD CC_FIELD SUBJECT_FIELD SENDPR_VER_FIELD NOTIFY_FIELD);
  40         63  
  40         4363  
14              
15 40     40   568 use Net::Gnats::FieldInstance;
  40         69  
  40         878  
16 40     40   17045 use Net::Gnats::Attachment;
  40         85  
  40         156776  
17              
18             $| = 1;
19             require Exporter;
20             our @ISA = qw(Exporter);
21             our @EXPORT_OK = qw( serialize deserialize parse_line);
22              
23             # TODO: These came from gnatsweb.pl for the parsepr and unparsepr routines.
24             # should be done a better way?
25             my $UNFORMATTED_FIELD = 'Unformatted';
26             my $SYNOPSIS_FIELD = 'Synopsis';
27             my $ORIGINATOR_FIELD = 'Originator';
28             my $attachment_delimiter = "----gnatsweb-attachment----\n";
29             my $SENDINCLUDE = 1; # whether the send command should include the field
30             our $REVISION = '$Id: PR.pm,v 1.8 2014/08/16 23:40:56 thacker Exp $'; #'
31              
32             #******************************************************************************
33             # Sub: new
34             # Description: Constructor
35             # Args: hash (parameter list)
36             # Returns: self
37             #******************************************************************************
38              
39             =head1 CONSTRUCTOR
40              
41             =head2 new
42              
43             The new() constructor does not expect any options. However, this may
44             change in the future when PR initialization is moved from Net::Gnats
45             to this class.
46              
47             my $pr = Net::Gnats::PR->new
48              
49             =cut
50              
51             sub new {
52 19     19 1 55 my ( $class, %options ) = @_;
53 19         74 my $self = bless {}, $class;
54 19         96 $self->{number} = undef;
55 19         49 $self->{fieldlist} = [];
56 19 50       100 return $self if not %options;
57 0         0 return $self;
58             }
59              
60              
61             =head1 ACCESSORS
62              
63             =head2 asHash
64              
65             Returns the PR formatted as a hash. The returned hash contains field
66             names as keys, and the corresponding field values as hash values.
67              
68             CHANGE ALERT: This method now returns all FieldInstance objects.
69              
70             DEPRECATION NOTICE: This accessor will be removed in the near future.
71              
72             =cut
73              
74             sub asHash {
75 0     0 1 0 my ( $self ) = shift;
76 0 0       0 return %{$self->{fields}} if defined($self->{fields}); #XXX Deep copy?
  0         0  
77 0         0 return undef;
78             }
79              
80             =head2 asString
81              
82             Returns the PR object formatted as a Gnats recongizable string. The
83             result is suitable for submitting to Gnats.
84              
85             my $serialized_pr = $pr->asString;
86              
87             DEPRECATION NOTICE: This accessor will be removed in the near future.
88             Instead, use:
89              
90             my $serialized_pr = $pr->serialize;
91              
92             =cut
93              
94             sub asString {
95 7     7 1 445 my $self = shift;
96 7         37 return Net::Gnats::PR->serialize($self,
97             Net::Gnats->current_session->username);
98             }
99              
100             =head2 getField
101              
102             Returns the string value of a PR field.
103              
104             $pr->getField('field');
105              
106             DEPRECATION NOTICE: this will be deprecated in the near future. Use instead:
107              
108             $r->get_field('field')->value
109              
110             =cut
111              
112             sub getField {
113 2     2 1 7 my ( $self, $field ) = @_;
114 2         12 return $self->{fields}->{$field}->value;
115             }
116              
117             =head2 getKeys
118              
119             Returns the list of PR fields contained in the object.
120              
121             =cut
122              
123             sub getKeys {
124 0     0 1 0 return keys %{shift->{fields}};
  0         0  
125             }
126              
127             =head2 getNumber
128              
129             Returns the gnats PR number. In previous versions of gnatsperl the
130             Number field was explicitly known to Net::Gnats::PR. This method
131             remains for backwards compatibility.
132              
133             DEPRECATION NOTICE: this will be deprecated in the near future. Use
134             instead:
135              
136             $r->get_field('Number')->value
137              
138             =cut
139              
140             sub getNumber {
141 1     1 1 11 return shift->{fields}->{'Number'}->value;
142             }
143              
144             =head2 add_field
145              
146             Adds a field instance to the fieldlist of a Nets::Gnats::PR object for fields that are not header fields.
147              
148             =cut
149              
150              
151             sub add_field {
152 247     247 1 253 my ($self, $field) = @_;
153 247         548 $self->{fields}->{$field->name} = $field;
154             # manage the list of fields in order only if it's not a header field.
155 247 100       464 unless (_is_header_field($field->name)) {
156 142         126 push @{ $self->{fieldlist} }, $field->name;
  142         332  
157             }
158 247         384 return;
159             }
160              
161             =head2 get_field
162              
163             Returns a field instance of an Nets::Gnats::PR object if field instance is defined.
164              
165             =cut
166              
167             sub get_field {
168 306     306 1 314 my ($self, $fieldname) = @_;
169 306 100       863 return $self->{fields}->{$fieldname} if defined $self->{fields}->{$fieldname};
170 118         264 return undef;
171             }
172              
173             =head2 get_field_from
174              
175             Return an anonymous array of Nets::Gnats::PR object fields instances from a word part match.
176              
177             =cut
178              
179              
180             sub get_field_from {
181 0     0 1 0 my ( $self, $fieldname) = @_;
182 0         0 my $result = [];
183              
184 0         0 foreach my $field ( sort keys %{ $self->{fields} } ) {
  0         0  
185 0 0       0 push @$result, $field if $field =~ qr/^$fieldname/;
186             }
187              
188 0         0 return $result;
189             }
190              
191             =head2 replaceField
192              
193             Sets a new value for an existing field.
194              
195             If the field requires a Change Reason, and the field does not exist in
196             the PR, then the FieldInstance for the Change Reason is created.
197              
198             Returns 0 if the field does not exist.
199              
200             Returns 0 if the field requires a changeReason, but one was not provided.
201              
202             Returns 0 if the change did not occur successfully.
203              
204             Returns 1 if the field is set and flushed to Gnats.
205              
206             =cut
207              
208             sub replaceField {
209 5     5 1 15 my ($self, $name, $value, $reason_value) = @_;
210 5 50       13 return 0 if not defined $self->get_field($name);
211 5 50       20 return 0 if not $self->setField($name, $value, $reason_value);
212              
213 5         13 my $f = $self->get_field($name);
214              
215 5 100       14 if ($f->schema->requires_change_reason) {
216 3         14 return Net::Gnats
217             ->current_session
218             ->issue(Net::Gnats::Command->repl(
219             pr_number => $self->get_field('Number')->value,
220             field => $f,
221             field_change_reason => $self->get_field($name . '-Changed-Why')))
222             ->is_ok;
223             }
224 2         10 return Net::Gnats
225             ->current_session
226             ->issue(Net::Gnats::Command->repl(pr_number => $self->get_field('Number')->value,
227             field => $f))->is_ok;
228             }
229              
230              
231             =head2 setField
232              
233             Sets a Gnats field value. Expects two arguments: the field name
234             followed by the field value. If the field requires a change reason,
235             provide it as a third argument.
236              
237             =cut
238              
239             sub setField {
240 52     52 1 89 my ($self, $name, $value, $reason_value) = @_;
241 52 100       81 return 0 if not defined $self->get_field($name);
242 23         32 my $f = $self->get_field($name);
243              
244 23 100       61 if ($f->schema->requires_change_reason) {
245 6 100       22 return 0 if (not defined $reason_value);
246 5         17 my $cr_instance =
247             Net::Gnats::FieldInstance->new(
248             schema => $f->schema->change_reason_field($name));
249 5         21 $cr_instance->value($reason_value);
250 5         12 $self->add_field($cr_instance);
251             }
252              
253 22         51 $f->value($value);
254 22         74 return 1;
255             }
256              
257             =head2 submit
258              
259             Submit this PR to Gnats. It uses the currently active session to
260             perform the submit.
261              
262             $pr = $pr->submit;
263              
264             After a successful submit, the PR with the PR Number is returned.
265              
266             say 'My new number is: ' . $pr->get_field('Number')->value;
267              
268             By default, submit will not send a PR which already has a PR Number.
269             If your intent is to create a new PR based on this one, use the force
270             option (may change in the future). This is useful when a series of
271             similar PRs need to be submitted.
272              
273             $pr = $pr->submit(1);
274              
275             If the PR submission based on force was not successful, the PR will
276             return with the same PR Number.
277              
278             =cut
279              
280             sub submit {
281 3     3 1 12 my ($self, $force) = @_;
282              
283 3 100 100     9 return $self if defined $self->get_field('Number') and not defined $force;
284              
285 2         10 my $command = Net::Gnats
286             ->current_session
287             ->issue(Net::Gnats::Command->subm(pr => $self));
288 2 50       10 return $self if $command->is_ok == 0;
289              
290             # the number is in the second response item. This should probably be in SUBM.pm.
291 2         6 my $number = @{ $command->response->as_list }[1];
  2         6  
292 2 100       7 if ( $self->get_field('Number') ) {
293 1         4 $self->get_field('Number')->value($number);
294             }
295             else {
296 1         5 my $field_schema = Net::Gnats->current_session->schema->field('Number');
297 1         6 $self->add_field(Net::Gnats::FieldInstance->new( name => 'Number',
298             value => $number,
299             schema => $field_schema ));
300             }
301 2         31 return $self;
302             }
303              
304             =head2 split_csl
305              
306             Split comma-separated list.
307             Commas in quotes are not separators!
308              
309             =cut
310              
311             sub split_csl {
312 0     0 1 0 my ($list) = @_;
313              
314             # Substitute commas in quotes with \002.
315 0         0 while ($list =~ m~"([^"]*)"~g)
316             {
317 0         0 my $pos = pos($list);
318 0         0 my $str = $1;
319 0         0 $str =~ s~,~\002~g;
320 0         0 $list =~ s~"[^"]*"~"$str"~;
321 0         0 pos($list) = $pos;
322             }
323              
324 0         0 my @res;
325 0         0 foreach my $person (split(/\s*,\s*/, $list))
326             {
327 0         0 $person =~ s/\002/,/g;
328 0 0       0 push(@res, $person) if $person;
329             }
330 0         0 return @res;
331             }
332              
333             =head2 fix_email_addrs
334              
335             Trim email addresses as they appear in an email From or Reply-To
336             header into a comma separated list of just the addresses.
337              
338             Delete everything inside ()'s and outside <>'s, inclusive.
339              
340             =cut
341              
342             sub fix_email_addrs
343             {
344 0     0 1 0 my $addrs = shift;
345 0         0 my @addrs = split_csl ($addrs);
346 0         0 my @trimmed_addrs;
347             my $addr;
348 0         0 foreach $addr (@addrs)
349             {
350 0         0 $addr =~ s/\(.*\)//;
351 0         0 $addr =~ s/.*<(.*)>.*/$1/;
352 0         0 $addr =~ s/^\s+//;
353 0         0 $addr =~ s/\s+$//;
354 0         0 push(@trimmed_addrs, $addr);
355             }
356 0         0 $addrs = join(', ', @trimmed_addrs);
357 0         0 $addrs;
358             }
359              
360             =head2 parse_line
361              
362             Breaks down a Gnats query result.
363              
364             =cut
365              
366             sub parse_line {
367 210     210 1 1077 my ( $line, $known ) = @_;
368 210         261 my $result = [];
369              
370 210 100       299 if (_is_header_line($line)) {
371 96         665 my @found = $line =~ /^([\w\-\{\}]+):\s*(.*)$/;
372 96         287 return \@found;
373             }
374              
375 114         506 my @found = $line =~ /^>([\w\-\{\d\}]+):\s*(.*)$/;
376              
377 114 100       238 if ( not defined $found[0] ) {
378 32         25 @{ $result }[1] = $line;
  32         52  
379 32         60 return $result;
380             }
381              
382 82         167 my $schemaname = _schema_fieldname($found[0]);
383              
384 82         86 my $schema_found = grep { $_ eq $schemaname } @{ $known };
  1858         1966  
  82         147  
385              
386 82 100       159 if ( $schema_found == 0 ) {
387 1         3 @{ $result }[1] = $line;
  1         2  
388 1         5 return $result;
389             }
390              
391 81         93 @{ $result }[0] = $found[0];
  81         161  
392 81         178 $found[1] =~ s/\s+$//;
393 81         90 @{ $result }[1] = $found[1];
  81         115  
394 81         211 return $result;
395             }
396              
397             sub _schema_fieldname {
398 159     159   192 my ( $fieldname ) = @_;
399 159         208 my $schemaname = $fieldname;
400 159         216 $schemaname =~ s/{\d+}$//;
401 159         369 return $schemaname;
402             }
403              
404             sub _schema_fieldinstance {
405 77     77   95 my ( $self, $fieldname ) = @_;
406 77         239 return Net::Gnats->current_session
407             ->schema
408             ->field(_schema_fieldname($fieldname))
409             ->instance( for_name => $fieldname );
410             }
411              
412             sub _clean {
413 220     220   246 my ( $self, $line ) = @_;
414 220 50       354 if ( not defined $line ) { return; }
  0         0  
415              
416 220         870 $line =~ s/\r|\n//gsm;
417             # $line =~ s/^[.][.]/./gsm;
418 220         361 return $line;
419             }
420              
421             sub _is_header_line {
422 377     377   374 my ( $line ) = @_;
423 377 100       363 return 1 if $line =~ /^${\(FROM_FIELD)}:/;
  377         1493  
424 346 100       391 return 1 if $line =~ /^${\(REPLYTO_FIELD)}:/;
  346         1097  
425 315 100       325 return 1 if $line =~ /^${\(TO_FIELD)}:/;
  315         950  
426 284 100       305 return 1 if $line =~ /^${\(CC_FIELD)}:/;
  284         883  
427 253 100       299 return 1 if $line =~ /^${\(SUBJECT_FIELD)}:/;
  253         823  
428 222 100       227 return 1 if $line =~ /^${\(SENDPR_VER_FIELD)}:/;
  222         828  
429 191 50       206 return 1 if $line =~ /^${\(NOTIFY_FIELD)}:/;
  191         575  
430 191         563 return 0;
431             }
432              
433             sub _is_header_field {
434 247     247   247 my ( $name ) = @_;
435 247 100       451 return 1 if $name eq FROM_FIELD;
436 232 100       381 return 1 if $name eq REPLYTO_FIELD;
437 217 100       341 return 1 if $name eq TO_FIELD;
438 202 100       343 return 1 if $name eq CC_FIELD;
439 187 100       349 return 1 if $name eq SUBJECT_FIELD;
440 172 100       276 return 1 if $name eq SENDPR_VER_FIELD;
441 157 100       252 return 1 if $name eq NOTIFY_FIELD;
442 142         270 return 0;
443             }
444              
445             sub _is_first_line {
446 167     167   189 my ( $line ) = @_;
447 167 100       166 return 1 if $line =~ /^${\(FROM_FIELD)}:/;
  167         666  
448 152         850 return 0;
449             }
450              
451             =head2 deserialize
452              
453             Deserializes a PR from Gnats and returns a hydrated PR.
454              
455             my $pr = Net::Gnats::PR->deserialize(raw => $c->response->raw,
456             schema => $s->schema);
457              
458             =cut
459              
460             sub setFromString {
461 1     1 0 3 my ($self, $data) = @_;
462             # expects just a block of text, so we need to break it out
463 1         4 $data =~ s/\r//g;
464 1         13 my @lines = split /\n/, $data;
465 1         7 return Net::Gnats::PR->deserialize(data => \@lines,
466             schema => Net::Gnats->current_session->schema);
467             }
468              
469             sub deserialize {
470 15     15 1 80 my ($self, %options) = @_;
471              
472 15         39 my $data = $options{data};
473 15         27 my $schema = $options{schema};
474              
475 15         60 my $pr = Net::Gnats::PR->new();
476 15         22 my $field;
477              
478 15         29 foreach my $line (@{$options{data}}) {
  15         41  
479 220         596 $line = $self->_clean($line);
480 220 100 100     828 next if $line eq '' or $line eq '.';
481              
482 196         178 my ( $name, $content ) = @{ parse_line( $line, $schema->fields ) };
  196         485  
483 196 50 66     981 next if not defined $name and $content eq '';
484              
485 196 100 100     594 if ( defined $name and _is_first_line( $name . ':') ) {
486             # put last PR in array, start new PR
487             }
488              
489 196 100 100     564 if ( defined $name and _is_header_line( $name . ':' ) ) {
490 90         324 $pr->add_field(
491             Net::Gnats::FieldInstance->new( name => $name,
492             value => $content,
493             schema => $schema->field($name)));
494 90         174 next;
495             }
496              
497             # known header field found, save.
498 106 100       191 if ( defined $name ) {
499 77         198 $field = $self->_schema_fieldinstance($name);
500 77         209 $field->value($content);
501 77         144 $pr->add_field($field);
502             }
503             # known header field not found, append to last.
504             else {
505 29         49 $field->value( $field->value . "\n" . $content );
506 29         51 $pr->setField($field);
507             }
508             }
509              
510              
511 15 50       74 $pr->get_field('Reply-To')->value($pr->get_field('From'))
512             if not defined $pr->get_field('Reply-To')->value;
513              
514             # create X-GNATS-Notify if we did not receive it.
515 15 50       40 if (not defined $pr->get_field('X-GNATS-Notify')) {
516 15         51 $pr->add_field(Net::Gnats::FieldInstance->new( name => 'X-GNATS-Notify',
517             value => '' ));
518             }
519              
520             # Create an Unformatted field if it doesn't come in from Gnats.
521 15 100       58 if (not $pr->get_field($UNFORMATTED_FIELD)) {
522 11         40 $pr->add_field(Net::Gnats::FieldInstance->new( name => $UNFORMATTED_FIELD,
523             value => '' ));
524             }
525              
526 15         130 my @attachments = split /$attachment_delimiter/,
527             $pr->get_field($UNFORMATTED_FIELD)->value;
528              
529 15 100       133 return $pr if scalar ( @attachments ) == 0;
530              
531             # First element is any random text which precedes delimited attachments.
532 2         4 $pr->get_field($UNFORMATTED_FIELD)->value( shift @attachments );
533              
534 2         4 foreach my $attachment (@attachments) {
535             # encoded PR always has a space in front of it
536 3         3 push @{$pr->{attachments}},
  3         15  
537             Net::Gnats::Attachment->new( payload => $attachment );
538             }
539              
540 2         6 return $pr;
541             }
542              
543             # unparse -
544             # Turn PR fields hash into a multi-line string.
545             #
546             # The $purpose arg controls how things are done. The possible values
547             # are:
548             # 'gnatsd' - PR will be filed using gnatsd; proper '.' escaping done
549             # 'send' - PR will be field using gnatsd, and is an initial PR.
550             # 'test' - we're being called from the regression tests
551              
552             # What is the user from the session? Need to have user passed for originator.
553             sub serialize {
554 7     7 0 16 my ( $self, $pr, $user ) = @_;
555 7   50     44 my $purpose ||= 'gnatsd';
556 7   50     66 $user ||= 'bugs';
557 7         12 my ( $tmp, $text );
558 7         10 my $debug = 0;
559              
560             # First create or reconstruct the Unformatted field containing the
561             # attachments, if any.
562 7         7 my %fields = %{$pr->{fields}};
  7         77  
563              
564             #if (not defined $pr->get_field('Unformatted')) {
565             # $pr->add_field(Net::Gnats::FieldInstance->new( name => $UNFORMATTED_FIELD,
566             # value => '',
567             # schema => ));
568             #}
569              
570             # deal with attachment later
571             # my $array_ref = $fields{'attachments'};
572             # foreach my $hash_ref (@$array_ref) {
573             # my $attachment_data = $$hash_ref{'original_attachment'};
574             # # Deleted attachments leave empty hashes behind.
575             # next unless defined($attachment_data);
576             # $fields{$UNFORMATTED_FIELD} .= $attachment_delimiter . $attachment_data . "\n";
577             # }
578             # warn "unparsepr 2 =>$fields{$UNFORMATTED_FIELD}<=\n" if $debug;
579              
580             # Headers are necessary because Gnats expects it.
581 7         36 $text .= FROM_FIELD . ': ' . $user . "\n";
582 7         16 $text .= REPLYTO_FIELD . ': ' . $user . "\n";
583 7         16 $text .= TO_FIELD . ': bugs' . "\n";
584 7         11 $text .= CC_FIELD . ': ' . "\n";
585 7         21 $text .= SUBJECT_FIELD . ': ' . $pr->get_field($SYNOPSIS_FIELD)->value . "\n";
586 7         20 $text .= SENDPR_VER_FIELD . ': Net::Gnats ' . $Net::Gnats::VERSION . "\n";
587 7         12 $text .= "\n";
588              
589 7         11 foreach my $fn (@{ $pr->{fieldlist} } ) {
  7         21  
590 68         90 my $field = $pr->get_field($fn);
591             #next if /^.$/;
592             #next if (not defined($fields{$_})); # Don't send fields that aren't defined.
593             # Do include Unformatted field in 'send' operation, even though
594             # it's excluded. We need it to hold the file attachment.
595             # XXX ??? !!! FIXME
596              
597             # if(($purpose eq 'send')
598             # && (! ($self->{__gnatsObj}->getFieldTypeInfo ($_, 'flags') & $SENDINCLUDE))
599             # && ($_ ne $UNFORMATTED_FIELD))
600             # {
601             # next;
602             # }
603              
604 68 100       102 if ($fn eq 'Unformatted') {
605 7         18 next;
606             }
607             # $fields{$_} ||= ''; # Default to empty
608 61 100       113 if ( $field->schema->type eq 'MultiText' ) {
609 14         25 $tmp = $field->value;
610 14         16 $tmp =~ s/\r//;
611 14         12 $tmp =~ s/^[.]/../gm;
612 14         15 chomp($tmp);
613 14         24 $text .= sprintf(">%s:\n%s\n", $field->name, $tmp);
614             }
615             else {
616             # Format string derived from gnats/pr.c.
617 47         87 $text .= sprintf("%-16s %s\n", '>' . $field->name . ':', $field->value);
618             }
619              
620 61 50       125 if ($pr->get_field($field->name . '-Changed-Why')) {
621             # Lines which begin with a '.' need to be escaped by another '.'
622             # if we're feeding it to gnatsd.
623 0         0 $tmp = $pr->get_field($_ . '-Changed-Why')->value;
624 0         0 $tmp =~ s/^[.]/../gm;
625 0         0 $text .= sprintf(">%s-Changed-Why:\n%s\n", $field->name, $tmp);
626             }
627             }
628 7         20 $text =~ s/\r//;
629 7         50 return $text;
630             }
631              
632              
633             # preloaded methods go here.
634              
635             # Autoload methods go after =cut, and are processed by the autosplit program.
636              
637             1;
638             __END__