File Coverage

blib/lib/App/Sqitch/ItemFormatter.pm
Criterion Covered Total %
statement 55 55 100.0
branch 8 8 100.0
condition n/a
subroutine 19 19 100.0
pod 1 1 100.0
total 83 83 100.0


line stmt bran cond sub pod time code
1              
2             use 5.010;
3 6     6   91168 use strict;
  6         31  
4 6     6   31 use warnings;
  6         16  
  6         107  
5 6     6   24 use utf8;
  6         11  
  6         139  
6 6     6   508 use Locale::TextDomain qw(App-Sqitch);
  6         22  
  6         27  
7 6     6   529 use App::Sqitch::X qw(hurl);
  6         16389  
  6         52  
8 6     6   26754 use List::Util qw(max);
  6         15  
  6         61  
9 6     6   1482 use Moo;
  6         12  
  6         336  
10 6     6   34 use Types::Standard qw(Str Int);
  6         12  
  6         65  
11 6     6   1957 use Type::Utils qw(enum class_type);
  6         12  
  6         52  
12 6     6   4485 use String::Formatter;
  6         4380  
  6         44  
13 6     6   5836 use namespace::autoclean;
  6         16202  
  6         28  
14 6     6   1579 use Try::Tiny;
  6         9932  
  6         56  
15 6     6   404 use Term::ANSIColor 2.02 qw(colorvalid);
  6         12  
  6         384  
16 6     6   1778 my $encolor = \&Term::ANSIColor::color;
  6         22169  
  6         2874  
17              
18             use constant CAN_OUTPUT_COLOR => $^O eq 'MSWin32'
19             ? try { require Win32::Console::ANSI }
20 1         278 : -t *STDOUT;
21 6 100   6   54  
  6         11  
  6         491  
22             BEGIN {
23             $ENV{ANSI_COLORS_DISABLED} = 1 unless CAN_OUTPUT_COLOR;
24 6     6   943 }
25              
26             our $VERSION = 'v1.3.0'; # VERSION
27              
28             has abbrev => (
29             is => 'ro',
30             isa => Int,
31             default => 0,
32             );
33              
34             has date_format => (
35             is => 'ro',
36             isa => Str,
37             default => 'iso',
38             );
39              
40             has color => (
41             is => 'ro',
42             isa => enum([ qw(always never auto) ]),
43             default => 'auto',
44             );
45              
46             has formatter => (
47             is => 'ro',
48             lazy => 1,
49             isa => class_type('String::Formatter'),
50             default => sub {
51             my $self = shift;
52             no if $] >= 5.017011, warnings => 'experimental::smartmatch';
53 6     6   3681 String::Formatter->new({
  6         79  
  6         38  
54             input_processor => 'require_single_input',
55             string_replacer => 'method_replace',
56             codes => {
57             e => sub { $_[0]->{event} },
58             L => sub {
59             given ($_[0]->{event}) {
60             when ('deploy') { return __ 'Deploy' }
61             when ('revert') { return __ 'Revert' }
62             when ('fail') { return __ 'Fail' }
63             }
64             },
65             l => sub {
66             given ($_[0]->{event}) {
67             when ('deploy') { return __ 'deploy' }
68             when ('revert') { return __ 'revert' }
69             when ('fail') { return __ 'fail' }
70             }
71             },
72             _ => sub {
73             given ($_[1]) {
74             when ('event') { return __ 'Event: ' }
75             when ('change') { return __ 'Change: ' }
76             when ('committer') { return __ 'Committer:' }
77             when ('planner') { return __ 'Planner: ' }
78             when ('by') { return __ 'By: ' }
79             when ('date') { return __ 'Date: ' }
80             when ('committed') { return __ 'Committed:' }
81             when ('planned') { return __ 'Planned: ' }
82             when ('name') { return __ 'Name: ' }
83             when ('project') { return __ 'Project: ' }
84             when ('email') { return __ 'Email: ' }
85             when ('requires') { return __ 'Requires: ' }
86             when ('conflicts') { return __ 'Conflicts:' }
87             when (undef) {
88             hurl format => __ 'No label passed to the _ format';
89             }
90             default {
91             hurl format => __x(
92             'Unknown label "{label}" passed to the _ format',
93             label => $_[1],
94             );
95             }
96             };
97             },
98             H => sub { $_[0]->{change_id} },
99             h => sub {
100             if (my $abb = $_[1] || $self->abbrev) {
101             return substr $_[0]->{change_id}, 0, $abb;
102             }
103             return $_[0]->{change_id};
104             },
105             n => sub { $_[0]->{change} },
106             o => sub { $_[0]->{project} },
107              
108             c => sub {
109             return "$_[0]->{committer_name} <$_[0]->{committer_email}>"
110             unless defined $_[1];
111             return $_[0]->{committer_name} if $_[1] ~~ [qw(n name)];
112             return $_[0]->{committer_email} if $_[1] ~~ [qw(e email)];
113             return $_[0]->{committed_at}->as_string(
114             format => $_[1] || $self->date_format
115             ) if $_[1] =~ s/^d(?:ate)?(?::|$)//;
116             },
117              
118             p => sub {
119             return "$_[0]->{planner_name} <$_[0]->{planner_email}>"
120             unless defined $_[1];
121             return $_[0]->{planner_name} if $_[1] ~~ [qw(n name)];
122             return $_[0]->{planner_email} if $_[1] ~~ [qw(e email)];
123             return $_[0]->{planned_at}->as_string(
124             format => $_[1] || $self->date_format
125             ) if $_[1] =~ s/^d(?:ate)?(?::|$)//;
126             },
127              
128             t => sub {
129             @{ $_[0]->{tags} }
130             ? ' ' . join $_[1] || ', ' => @{ $_[0]->{tags} }
131             : '';
132             },
133             T => sub {
134             @{ $_[0]->{tags} }
135             ? ' (' . join($_[1] || ', ' => @{ $_[0]->{tags} }) . ')'
136             : '';
137             },
138             v => sub { "\n" },
139             C => sub {
140             if (($_[1] // '') eq ':event') {
141             # Select a color based on some attribute.
142             return $encolor->(
143             $_[0]->{event} eq 'deploy' ? 'green'
144             : $_[0]->{event} eq 'revert' ? 'blue'
145             : 'red'
146             );
147             }
148             hurl format => __x(
149             '{color} is not a valid ANSI color', color => $_[1]
150             ) unless $_[1] && colorvalid( $_[1] );
151             $encolor->( $_[1] );
152             },
153             s => sub {
154             ( my $s = $_[0]->{note} ) =~ s/\v.*//ms;
155             return ($_[1] // '') . $s;
156             },
157             b => sub {
158             return '' unless $_[0]->{note} =~ /\v/;
159             ( my $b = $_[0]->{note} ) =~ s/^.+\v+//;
160             $b =~ s/^/$_[1]/gms if defined $_[1] && length $b;
161             return $b;
162             },
163             B => sub {
164             return $_[0]->{note} unless defined $_[1];
165             ( my $note = $_[0]->{note} ) =~ s/^/$_[1]/gms;
166             return $note;
167             },
168             r => sub {
169             @{ $_[0]->{requires} }
170             ? ' ' . join $_[1] || ', ' => @{ $_[0]->{requires} }
171             : '';
172             },
173             R => sub {
174             return '' unless @{ $_[0]->{requires} };
175             return __ ('Requires: ') . ' ' . join(
176             $_[1] || ', ' => @{ $_[0]->{requires} }
177             ) . "\n";
178             },
179             x => sub {
180             @{ $_[0]->{conflicts} }
181             ? ' ' . join $_[1] || ', ' => @{ $_[0]->{conflicts} }
182             : '';
183             },
184             X => sub {
185             return '' unless @{ $_[0]->{conflicts} };
186             return __('Conflicts:') . ' ' . join(
187             $_[1] || ', ' => @{ $_[0]->{conflicts} }
188             ) . "\n";
189             },
190             a => sub {
191             hurl format => __x(
192             '{attr} is not a valid change attribute', attr => $_[1]
193             ) unless $_[1] && exists $_[0]->{ $_[1] };
194             my $val = $_[0]->{ $_[1] } // return '';
195              
196             if (ref $val eq 'ARRAY') {
197             return '' unless @{ $val };
198             $val = join ', ' => @{ $val };
199             } elsif (eval { $val->isa('App::Sqitch::DateTime') }) {
200             $val = $val->as_string( format => 'raw' );
201             }
202              
203             my $sp = ' ' x max(9 - length $_[1], 0);
204             return "$_[1]$sp $val\n";
205             }
206             },
207             });
208             }
209             );
210              
211             my $self = shift;
212             local $SIG{__DIE__} = sub {
213 462     462 1 282578 die @_ if $_[0] !~ /^Unknown conversion in stringf: (\S+)/;
214             hurl format => __x 'Unknown format code "{code}"', code => $1;
215 15 100   15   23481 };
216 3         13  
217 462         2570 # Older versions of TERM::ANSIColor check for definedness.
218             local $ENV{ANSI_COLORS_DISABLED} = $self->color eq 'always' ? undef
219             : $self->color eq 'never' ? 1
220             : $ENV{ANSI_COLORS_DISABLED};
221              
222 462 100       2945 return $self->formatter->format(@_);
    100          
223             }
224 462         8948  
225             1;
226              
227              
228             =head1 Name
229              
230             App::Sqitch::ItemFormatter - Format events and changes for command output
231              
232             =head1 Synopsis
233              
234             my $formatter = App::Sqitch::ItemFormatter->new(%params);
235             say $formatter->format($format, $item);
236              
237             =head1 Description
238              
239             This class is used by commands to format items for output. For example,
240             L<C<log>|sqitch-log> uses it to format the events it finds. It uses
241             L<String::Formatter> to do the actual formatting, but configures it for all
242             the various times of things typically displayed, such as change names, IDs,
243             event types, etc. This keeps things relatively simple, as all one needs to
244             pass to C<format()> is a format and then a hash reference of values to be used
245             in the format.
246              
247             =head1 Interface
248              
249             =head2 Constructor
250              
251             =head3 C<new>
252              
253             my $formatter = App::Sqitch::ItemFormatter->new(%params);
254              
255             Constructs and returns a formatter object. The supported parameters are:
256              
257             =over
258              
259             =item C<abbrev>
260              
261             Instead of showing the full 40-byte hexadecimal change ID, format as a partial
262             prefix the specified number of characters long.
263              
264             =item C<date_format>
265              
266             Format to use for timestamps. Defaults to C<iso>. Allowed values:
267              
268             =over
269              
270             =item C<iso>
271              
272             =item C<iso8601>
273              
274             ISO-8601 format.
275              
276             =item C<rfc>
277              
278             =item C<rfc2822>
279              
280             RFC-2822 format.
281              
282             =item C<full>
283              
284             =item C<long>
285              
286             =item C<medium>
287              
288             =item C<short>
289              
290             A format length to pass to the system locale's C<LC_TIME> category.
291              
292             =item C<raw>
293              
294             Raw format, which is strict ISO-8601 in the UTC time zone.
295              
296             =item C<strftime:$string>
297              
298             An arbitrary C<strftime> pattern. See L<DateTime/strftime Paterns> for
299             comprehensive documentation of supported patterns.
300              
301             =item C<cldr:$pattern>
302              
303             An arbitrary C<cldr> pattern. See L<DateTime/CLDR Paterns> for comprehensive
304             documentation of supported patterns.
305              
306             =back
307              
308             =item C<color>
309              
310             Controls the use of ANSI color formatting. The value may be one of:
311              
312             =over
313              
314             =item C<auto> (the default)
315              
316             =item C<always>
317              
318             =item C<never>
319              
320             =back
321              
322             =item C<formatter>
323              
324             A String::Formatter object. You probably don't want to pass one of these, as
325             the default one understands all the values to that Sqitch is likely to want to
326             format.
327              
328             =back
329              
330             =head2 Instance Methods
331              
332             =head3 C<format>
333              
334             $formatter->format( $format, $item );
335              
336             Formats an item as a string and returns it. The item will be formatted using
337             the first argument. See L</Formats> for the gory details.
338              
339             The second argument is a hash reference defining the item to be formatted.
340             These are simple key/value pairs, generally identifying attribute names and
341             values. The supported keys are:
342              
343             =over
344              
345             =item C<event>
346              
347             The type of event, which is one of:
348              
349             =over
350              
351             =item C<deploy>
352              
353             =item C<revert>
354              
355             =item C<fail>
356              
357             =back
358              
359             =item C<project>
360              
361             The name of the project with which the change is associated.
362              
363             =item C<change_id>
364              
365             The change ID.
366              
367             =item C<change>
368              
369             The name of the change.
370              
371             =item C<note>
372              
373             A brief description of the change.
374              
375             =item C<tags>
376              
377             An array reference of the names of associated tags.
378              
379             =item C<requires>
380              
381             An array reference of the names of any changes required by the change.
382              
383             =item C<conflicts>
384              
385             An array reference of the names of any changes that conflict with the change.
386              
387             =item C<committed_at>
388              
389             An L<App::Sqitch::DateTime> object representing the date and time at which the
390             event was logged.
391              
392             =item C<committer_name>
393              
394             Name of the user who deployed the change.
395              
396             =item C<committer_email>
397              
398             Email address of the user who deployed the change.
399              
400             =item C<planned_at>
401              
402             An L<App::Sqitch::DateTime> object representing the date and time at which the
403             change was added to the plan.
404              
405             =item C<planner_name>
406              
407             Name of the user who added the change to the plan.
408              
409             =item C<planner_email>
410              
411             Email address of the user who added the change to the plan.
412              
413             =back
414              
415             =head1 Formats
416              
417             The format argument to C<format()> specifies the item information to be
418             included in the resulting string. It works a little bit like C<printf> format
419             and a little like Git log format. For example, this format:
420              
421             format:The committer of %h was %{name}c%vThe title was >>%s<<%v
422              
423             Would show something like this:
424              
425             The committer of f26a3s was Tom Lane
426             The title was >>We really need to get this right.<<
427              
428             The placeholders are:
429              
430             =over
431              
432             =item * C<%H>: Event change ID
433              
434             =item * C<%h>: Event change ID (respects C<--abbrev>)
435              
436             =item * C<%n>: Event change name
437              
438             =item * C<%o>: Event change project name
439              
440             =item * C<%($len)h>: abbreviated change of length C<$len>
441              
442             =item * C<%e>: Event type (deploy, revert, fail)
443              
444             =item * C<%l>: Localized lowercase event type label
445              
446             =item * C<%L>: Localized title case event type label
447              
448             =item * C<%c>: Event committer name and email address
449              
450             =item * C<%{name}c>: Event committer name
451              
452             =item * C<%{email}c>: Event committer email address
453              
454             =item * C<%{date}c>: commit date (respects C<--date-format>)
455              
456             =item * C<%{date:rfc}c>: commit date, RFC2822 format
457              
458             =item * C<%{date:iso}c>: commit date, ISO-8601 format
459              
460             =item * C<%{date:full}c>: commit date, full format
461              
462             =item * C<%{date:long}c>: commit date, long format
463              
464             =item * C<%{date:medium}c>: commit date, medium format
465              
466             =item * C<%{date:short}c>: commit date, short format
467              
468             =item * C<%{date:cldr:$pattern}c>: commit date, formatted with custom L<CLDR pattern|DateTime/CLDR Patterns>
469              
470             =item * C<%{date:strftime:$pattern}c>: commit date, formatted with custom L<strftime pattern|DateTime/strftime Patterns>
471              
472             =item * C<%c>: Change planner name and email address
473              
474             =item * C<%{name}p>: Change planner name
475              
476             =item * C<%{email}p>: Change planner email address
477              
478             =item * C<%{date}p>: plan date (respects C<--date-format>)
479              
480             =item * C<%{date:rfc}p>: plan date, RFC2822 format
481              
482             =item * C<%{date:iso}p>: plan date, ISO-8601 format
483              
484             =item * C<%{date:full}p>: plan date, full format
485              
486             =item * C<%{date:long}p>: plan date, long format
487              
488             =item * C<%{date:medium}p>: plan date, medium format
489              
490             =item * C<%{date:short}p>: plan date, short format
491              
492             =item * C<%{date:cldr:$pattern}p>: plan date, formatted with custom L<CLDR pattern|DateTime/CLDR Patterns>
493              
494             =item * C<%{date:strftime:$pattern}p>: plan date, formatted with custom L<strftime pattern|DateTime/strftime Patterns>
495              
496             =item * C<%t>: Comma-delimited list of tags
497              
498             =item * C<%{$sep}t>: list of tags delimited by C<$sep>
499              
500             =item * C<%T>: Parenthesized list of comma-delimited tags
501              
502             =item * C<%{$sep}T>: Parenthesized list of tags delimited by C<$sep>
503              
504             =item * C<%s>: Subject (a.k.a. title line)
505              
506             =item * C<%r>: Comma-delimited list of required changes
507              
508             =item * C<%{$sep}r>: list of required changes delimited by C<$sep>
509              
510             =item * C<%R>: Localized label and list of comma-delimited required changes
511              
512             =item * C<%{$sep}R>: Localized label and list of required changes delimited by C<$sep>
513              
514             =item * C<%x>: Comma-delimited list of conflicting changes
515              
516             =item * C<%{$sep}x>: list of conflicting changes delimited by C<$sep>
517              
518             =item * C<%X>: Localized label and list of comma-delimited conflicting changes
519              
520             =item * C<%{$sep}X>: Localized label and list of conflicting changes delimited by C<$sep>
521              
522             =item * C<%b>: Body
523              
524             =item * C<%B>: Raw body (unwrapped subject and body)
525              
526             =item * C<%{$prefix}>B: Raw body with C<$prefix> prefixed to every line
527              
528             =item * C<%{event}_> Localized label for "event"
529              
530             =item * C<%{change}_> Localized label for "change"
531              
532             =item * C<%{committer}_> Localized label for "committer"
533              
534             =item * C<%{planner}_> Localized label for "planner"
535              
536             =item * C<%{by}_> Localized label for "by"
537              
538             =item * C<%{date}_> Localized label for "date"
539              
540             =item * C<%{committed}_> Localized label for "committed"
541              
542             =item * C<%{planned}_> Localized label for "planned"
543              
544             =item * C<%{name}_> Localized label for "name"
545              
546             =item * C<%{project}_> Localized label for "project"
547              
548             =item * C<%{email}_> Localized label for "email"
549              
550             =item * C<%{requires}_> Localized label for "requires"
551              
552             =item * C<%{conflicts}_> Localized label for "conflicts"
553              
554             =item * C<%v> vertical space (newline)
555              
556             =item * C<%{$color}C>: An ANSI color: black, red, green, yellow, reset, etc.
557              
558             =item * C<%{:event}C>: An ANSI color based on event type (green deploy, blue revert, red fail)
559              
560             =item * C<%{$attribute}a>: The raw attribute name and value, if it exists and has a value
561              
562             =back
563              
564             =head1 See Also
565              
566             =over
567              
568             =item L<sqitch-log>
569              
570             Documentation for the C<log> command to the Sqitch command-line client.
571              
572             =item L<sqitch>
573              
574             The Sqitch command-line client.
575              
576             =back
577              
578             =head1 Author
579              
580             David E. Wheeler <david@justatheory.com>
581              
582             =head1 License
583              
584             Copyright (c) 2012-2022 iovation Inc., David E. Wheeler
585              
586             Permission is hereby granted, free of charge, to any person obtaining a copy
587             of this software and associated documentation files (the "Software"), to deal
588             in the Software without restriction, including without limitation the rights
589             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
590             copies of the Software, and to permit persons to whom the Software is
591             furnished to do so, subject to the following conditions:
592              
593             The above copyright notice and this permission notice shall be included in all
594             copies or substantial portions of the Software.
595              
596             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
597             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
598             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
599             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
600             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
601             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
602             SOFTWARE.
603              
604             =cut