File Coverage

blib/lib/Net/Gnats/PR.pm
Criterion Covered Total %
statement 204 244 83.6
branch 76 92 82.6
condition 16 19 84.2
subroutine 29 34 85.2
pod 16 18 88.8
total 341 407 83.7


line stmt bran cond sub pod time code
1             package Net::Gnats::PR;
2 40     40   1267 use 5.010_000;
  40         143  
3 40     40   223 use utf8;
  40         73  
  40         295  
4 40     40   924 use strictures;
  40         139  
  40         223  
5              
6             BEGIN {
7 40     40   9370 $Net::Gnats::PR::VERSION = '0.22';
8             }
9 40     40   206 use vars qw($VERSION);
  40         80  
  40         1530  
10              
11 40     40   203 use Carp;
  40         69  
  40         2647  
12 40     40   43135 use MIME::Base64;
  40         36771  
  40         2850  
13 40     40   1011 use Net::Gnats::Constants qw(FROM_FIELD REPLYTO_FIELD TO_FIELD CC_FIELD SUBJECT_FIELD SENDPR_VER_FIELD NOTIFY_FIELD);
  40         81  
  40         3354  
14              
15 40     40   776 use Net::Gnats::FieldInstance;
  40         80  
  40         1019  
16 40     40   21427 use Net::Gnats::Attachment;
  40         93  
  40         170145  
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 54 my ( $class, %options ) = @_;
53 19         45 my $self = bless {}, $class;
54 19         118 $self->{number} = undef;
55 19         47 $self->{fieldlist} = [];
56 19 50       92 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 528 my $self = shift;
96 7         29 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 12 my ( $self, $field ) = @_;
114 2         11 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 5 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 335 my ($self, $field) = @_;
153 247         691 $self->{fields}->{$field->name} = $field;
154             # manage the list of fields in order only if it's not a header field.
155 247 100       693 unless (_is_header_field($field->name)) {
156 142         150 push @{ $self->{fieldlist} }, $field->name;
  142         479  
157             }
158 247         527 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 469 my ($self, $fieldname) = @_;
169 306 100       1148 return $self->{fields}->{$fieldname} if defined $self->{fields}->{$fieldname};
170 118         426 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 14 my ($self, $name, $value, $reason_value) = @_;
210 5 50       16 return 0 if not defined $self->get_field($name);
211 5 50       17 return 0 if not $self->setField($name, $value, $reason_value);
212              
213 5         14 my $f = $self->get_field($name);
214              
215 5 100       16 if ($f->schema->requires_change_reason) {
216 3         16 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 113 my ($self, $name, $value, $reason_value) = @_;
241 52 100       111 return 0 if not defined $self->get_field($name);
242 23         58 my $f = $self->get_field($name);
243              
244 23 100       78 if ($f->schema->requires_change_reason) {
245 6 100       20 return 0 if (not defined $reason_value);
246 5         18 my $cr_instance =
247             Net::Gnats::FieldInstance->new(
248             schema => $f->schema->change_reason_field($name));
249 5         17 $cr_instance->value($reason_value);
250 5         14 $self->add_field($cr_instance);
251             }
252              
253 22         70 $f->value($value);
254 22         82 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 10 my ($self, $force) = @_;
282              
283 3 100 100     8 return $self if defined $self->get_field('Number') and not defined $force;
284              
285 2         8 my $command = Net::Gnats
286             ->current_session
287             ->issue(Net::Gnats::Command->subm(pr => $self));
288 2 50       8 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         4 my $number = @{ $command->response->as_list }[1];
  2         6  
292 2 100       6 if ( $self->get_field('Number') ) {
293 1         3 $self->get_field('Number')->value($number);
294             }
295             else {
296 1         5 my $field_schema = Net::Gnats->current_session->schema->field('Number');
297 1         5 $self->add_field(Net::Gnats::FieldInstance->new( name => 'Number',
298             value => $number,
299             schema => $field_schema ));
300             }
301 2         19 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 774 my ( $line, $known ) = @_;
368 210         313 my $result = [];
369              
370 210 100       399 if (_is_header_line($line)) {
371 96         431 my @found = $line =~ /^([\w\-\{\}]+):\s*(.*)$/;
372 96         353 return \@found;
373             }
374              
375 114         447 my @found = $line =~ /^>([\w\-\{\d\}]+):\s*(.*)$/;
376              
377 114 100       281 if ( not defined $found[0] ) {
378 32         40 @{ $result }[1] = $line;
  32         73  
379 32         92 return $result;
380             }
381              
382 82         160 my $schemaname = _schema_fieldname($found[0]);
383              
384 82         111 my $schema_found = grep { $_ eq $schemaname } @{ $known };
  1858         2967  
  82         154  
385              
386 82 100       220 if ( $schema_found == 0 ) {
387 1         2 @{ $result }[1] = $line;
  1         3  
388 1         7 return $result;
389             }
390              
391 81         120 @{ $result }[0] = $found[0];
  81         195  
392 81         191 $found[1] =~ s/\s+$//;
393 81         106 @{ $result }[1] = $found[1];
  81         161  
394 81         365 return $result;
395             }
396              
397             sub _schema_fieldname {
398 159     159   212 my ( $fieldname ) = @_;
399 159         194 my $schemaname = $fieldname;
400 159         242 $schemaname =~ s/{\d+}$//;
401 159         393 return $schemaname;
402             }
403              
404             sub _schema_fieldinstance {
405 77     77   115 my ( $self, $fieldname ) = @_;
406 77         240 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   394 my ( $self, $line ) = @_;
414 220 50       432 if ( not defined $line ) { return; }
  0         0  
415              
416 220         759 $line =~ s/\r|\n//gsm;
417             # $line =~ s/^[.][.]/./gsm;
418 220         436 return $line;
419             }
420              
421             sub _is_header_line {
422 377     377   495 my ( $line ) = @_;
423 377 100       410 return 1 if $line =~ /^${\(FROM_FIELD)}:/;
  377         1477  
424 346 100       487 return 1 if $line =~ /^${\(REPLYTO_FIELD)}:/;
  346         1246  
425 315 100       441 return 1 if $line =~ /^${\(TO_FIELD)}:/;
  315         1112  
426 284 100       428 return 1 if $line =~ /^${\(CC_FIELD)}:/;
  284         1041  
427 253 100       342 return 1 if $line =~ /^${\(SUBJECT_FIELD)}:/;
  253         943  
428 222 100       298 return 1 if $line =~ /^${\(SENDPR_VER_FIELD)}:/;
  222         912  
429 191 50       266 return 1 if $line =~ /^${\(NOTIFY_FIELD)}:/;
  191         647  
430 191         677 return 0;
431             }
432              
433             sub _is_header_field {
434 247     247   321 my ( $name ) = @_;
435 247 100       546 return 1 if $name eq FROM_FIELD;
436 232 100       449 return 1 if $name eq REPLYTO_FIELD;
437 217 100       452 return 1 if $name eq TO_FIELD;
438 202 100       437 return 1 if $name eq CC_FIELD;
439 187 100       396 return 1 if $name eq SUBJECT_FIELD;
440 172 100       395 return 1 if $name eq SENDPR_VER_FIELD;
441 157 100       319 return 1 if $name eq NOTIFY_FIELD;
442 142         337 return 0;
443             }
444              
445             sub _is_first_line {
446 167     167   247 my ( $line ) = @_;
447 167 100       194 return 1 if $line =~ /^${\(FROM_FIELD)}:/;
  167         741  
448 152         598 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 2 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         8 my @lines = split /\n/, $data;
465 1         4 return Net::Gnats::PR->deserialize(data => \@lines,
466             schema => Net::Gnats->current_session->schema);
467             }
468              
469             sub deserialize {
470 15     15 1 75 my ($self, %options) = @_;
471              
472 15         35 my $data = $options{data};
473 15         27 my $schema = $options{schema};
474              
475 15         52 my $pr = Net::Gnats::PR->new();
476 15         29 my $field;
477              
478 15         24 foreach my $line (@{$options{data}}) {
  15         42  
479 220         498 $line = $self->_clean($line);
480 220 100 100     959 next if $line eq '' or $line eq '.';
481              
482 196         228 my ( $name, $content ) = @{ parse_line( $line, $schema->fields ) };
  196         532  
483 196 50 66     1165 next if not defined $name and $content eq '';
484              
485 196 100 100     687 if ( defined $name and _is_first_line( $name . ':') ) {
486             # put last PR in array, start new PR
487             }
488              
489 196 100 100     719 if ( defined $name and _is_header_line( $name . ':' ) ) {
490 90         298 $pr->add_field(
491             Net::Gnats::FieldInstance->new( name => $name,
492             value => $content,
493             schema => $schema->field($name)));
494 90         206 next;
495             }
496              
497             # known header field found, save.
498 106 100       219 if ( defined $name ) {
499 77         191 $field = $self->_schema_fieldinstance($name);
500 77         243 $field->value($content);
501 77         158 $pr->add_field($field);
502             }
503             # known header field not found, append to last.
504             else {
505 29         86 $field->value( $field->value . "\n" . $content );
506 29         74 $pr->setField($field);
507             }
508             }
509              
510              
511 15 50       50 $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       37 if (not defined $pr->get_field('X-GNATS-Notify')) {
516 15         62 $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       44 if (not $pr->get_field($UNFORMATTED_FIELD)) {
522 11         38 $pr->add_field(Net::Gnats::FieldInstance->new( name => $UNFORMATTED_FIELD,
523             value => '' ));
524             }
525              
526 15         97 my @attachments = split /$attachment_delimiter/,
527             $pr->get_field($UNFORMATTED_FIELD)->value;
528              
529 15 100       120 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         5 foreach my $attachment (@attachments) {
535             # encoded PR always has a space in front of it
536 3         4 push @{$pr->{attachments}},
  3         17  
537             Net::Gnats::Attachment->new( payload => $attachment );
538             }
539              
540 2         8 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     42 my $purpose ||= 'gnatsd';
556 7   50     20 $user ||= 'bugs';
557 7         9 my ( $tmp, $text );
558 7         13 my $debug = 0;
559              
560             # First create or reconstruct the Unformatted field containing the
561             # attachments, if any.
562 7         9 my %fields = %{$pr->{fields}};
  7         75  
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         29 $text .= FROM_FIELD . ': ' . $user . "\n";
582 7         16 $text .= REPLYTO_FIELD . ': ' . $user . "\n";
583 7         14 $text .= TO_FIELD . ': bugs' . "\n";
584 7         11 $text .= CC_FIELD . ': ' . "\n";
585 7         20 $text .= SUBJECT_FIELD . ': ' . $pr->get_field($SYNOPSIS_FIELD)->value . "\n";
586 7         28 $text .= SENDPR_VER_FIELD . ': Net::Gnats ' . $Net::Gnats::VERSION . "\n";
587 7         1021 $text .= "\n";
588              
589 7         11 foreach my $fn (@{ $pr->{fieldlist} } ) {
  7         24  
590 68         141 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       166 if ($fn eq 'Unformatted') {
605 7         16 next;
606             }
607             # $fields{$_} ||= ''; # Default to empty
608 61 100       174 if ( $field->schema->type eq 'MultiText' ) {
609 14         36 $tmp = $field->value;
610 14         23 $tmp =~ s/\r//;
611 14         19 $tmp =~ s/^[.]/../gm;
612 14         21 chomp($tmp);
613 14         37 $text .= sprintf(">%s:\n%s\n", $field->name, $tmp);
614             }
615             else {
616             # Format string derived from gnats/pr.c.
617 47         133 $text .= sprintf("%-16s %s\n", '>' . $field->name . ':', $field->value);
618             }
619              
620 61 50       202 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         21 $text =~ s/\r//;
629 7         52 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__