File Coverage

blib/lib/App/Sqitch/ItemFormatter.pm
Criterion Covered Total %
statement 52 52 100.0
branch 8 8 100.0
condition n/a
subroutine 18 18 100.0
pod 1 1 100.0
total 79 79 100.0


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