File Coverage

blib/lib/App/bif.pm
Criterion Covered Total %
statement 245 466 52.5
branch 43 158 27.2
condition 34 106 32.0
subroutine 50 67 74.6
pod 29 37 78.3
total 401 834 48.0


line stmt bran cond sub pod time code
1             package App::bif;
2 46     46   1227832 use strict;
  46         182  
  46         1442  
3 46     46   250 use warnings;
  46         113  
  46         1704  
4 46     46   259 use feature 'state';
  46         1129  
  46         4722  
5 46     46   41782 use utf8; # for render_table
  46         510  
  46         270  
6 46     46   21226 use Bif::Mo;
  46         190  
  46         272  
7 46     46   262 use Carp ();
  46         83  
  46         1033  
8 46     46   33872 use Config::Tiny;
  46         49301  
  46         1541  
9 46     46   36017 use File::HomeDir;
  46         285316  
  46         3026  
10 46     46   2408 use Log::Any qw/$log/;
  46         344669  
  46         435  
11 46     46   498151 use Path::Tiny qw/path rootdir cwd/;
  46         22976  
  46         101754  
12              
13             our $VERSION = '0.1.5_7';
14             our $pager;
15              
16 463     463 0 10187 sub DBVERSION { 1 }
17 1     1 0 5 sub MSWin32 { $^O eq 'MSWin32' }
18              
19             has debug => ( is => 'rw', );
20              
21             has db => (
22             is => 'ro',
23             default => \&_build_db,
24             );
25              
26             has dbw => (
27             is => 'rw', # for bif-new-repo?
28             default => \&_build_dbw,
29             );
30              
31             has _colours => (
32             is => 'ro',
33             default => {},
34             );
35              
36             has config => (
37             is => 'ro',
38             default => {},
39             );
40              
41             has no_pager => (
42             is => 'rw',
43             default => sub {
44             my $self = shift;
45             return ( !-t STDOUT ) || $self->opts->{no_pager};
46             },
47             );
48              
49             has now => (
50             is => 'ro',
51             default => sub { time },
52             );
53              
54             has opts => (
55             is => 'ro',
56             required => 1,
57             );
58              
59             has repo => (
60             is => 'rw', # needed by init
61             default => \&_build_repo,
62             );
63              
64             has term_width => (
65             is => 'ro',
66             default => sub {
67             my $width;
68             if (MSWin32) {
69             require Term::Size::Win32;
70             $width = ( Term::Size::Win32::chars(*STDOUT) )[0] || 80;
71             }
72             else {
73             require Term::Size::Perl;
74             $width = ( Term::Size::Perl::chars(*STDOUT) )[0] || 80;
75             }
76             $log->debugf( 'bif: terminal width %d', $width );
77             return $width;
78             },
79             );
80              
81             has term_height => (
82             is => 'ro',
83             default => sub {
84             my $height;
85             if (MSWin32) {
86             require Term::Size::Win32;
87             $height = ( Term::Size::Win32::chars(*STDOUT) )[1] || 40;
88             }
89             else {
90             require Term::Size::Perl;
91             $height = ( Term::Size::Perl::chars(*STDOUT) )[1] || 40;
92             }
93             $log->debugf( 'bif: terminal height %d', $height );
94             return $height;
95             },
96             );
97              
98             has user_repo => (
99             is => 'ro',
100             default => \&_build_user_repo,
101             );
102              
103             has user_db => (
104             is => 'ro',
105             default => \&_build_user_db,
106             );
107              
108             has user_dbw => (
109             is => 'ro',
110             default => \&_build_user_dbw,
111             );
112              
113             has work_buffer => ( is => 'rw', );
114              
115             sub BUILD {
116 199     199 0 472 my $self = shift;
117 199         1294 my $opts = $self->opts;
118              
119             # For Term::ANSIColor
120 199   33     2360 $ENV{ANSI_COLORS_DISABLED} //= $opts->{no_color} || !-t STDOUT;
      66        
121              
122 43     43   247 binmode STDIN, ':encoding(utf8)';
  43         158  
  43         359  
  199         3626  
123 199         66807 binmode STDOUT, ':encoding(utf8)';
124              
125 199 100 33     6026 if ( $self->debug( $self->debug // $opts->{debug} ) ) {
126 1         7 require Log::Any::Adapter;
127 1 50       4 if ( exists $INC{'Test/More.pm'} ) {
128              
129             # Log::Any::Adapter::Diag can be found in t/lib/
130 1         10 Log::Any::Adapter->set('Diag');
131             }
132             else {
133 0         0 Log::Any::Adapter->set('+App::bif::LAA');
134             }
135 1         243 $self->start_pager();
136             }
137              
138 199         1477 $log->infof( 'bif: %s %s', ref $self, $opts );
139              
140 199         4969 return;
141             }
142              
143             sub _build_user_repo {
144 78     78   179 my $self = shift;
145 78         832 my $repo = path( File::HomeDir->my_home, '.bifu' )->absolute;
146              
147 78 100       12220 $self->err( 'UserRepoNotFound',
148             'user repository not found (try "bif init -u -i")' )
149             unless -d $repo;
150              
151 44         1112 $log->debug( 'bif: user_repo: ' . $repo );
152              
153 44         1172 my $file = $repo->child('config');
154 44 100       1451 return $repo unless $file->exists;
155              
156 39         1273 my $config = $self->config;
157 39   50     491 my $conf = Config::Tiny->read( $file, 'utf8' )
158             || return $self->err( 'ConfigNotFound',
159             $file . ' ' . Config::Tiny->errstr );
160              
161             # Merge in the repo config with the current context (user) config
162 39         12199 while ( my ( $k1, $v1 ) = each %$conf ) {
163 39 50       313 if ( ref $v1 eq 'HASH' ) {
164 39         294 while ( my ( $k2, $v2 ) = each %$v1 ) {
165 156 50       412 if ( $k1 eq '_' ) {
166 0         0 $config->{$k2} = $v2;
167             }
168             else {
169 156         847 $config->{$k1}->{$k2} = $v2;
170             }
171             }
172             }
173             else {
174 0         0 $config->{$k1} = $v1;
175             }
176             }
177              
178 39         643 return $repo;
179             }
180              
181             sub _build_repo {
182 34     34   84 my $self = shift;
183 34         413 $self->user_repo; # build user repo first
184              
185 4   66     137 my $repo = $self->find_repo('.bif')
186             || $self->err( 'RepoNotFound', 'directory not found: .bif' );
187              
188 3         14 $log->debug( 'bif: repo: ' . $repo );
189              
190 3         58 my $file = $repo->child('config');
191 3 50       84 return $repo unless $file->exists;
192              
193 0         0 $log->debug( 'bif: repo_conf: ' . $file );
194              
195             # Trigger user config
196 0         0 $self->user_repo;
197              
198 0         0 my $config = $self->config;
199 0   0     0 my $conf = Config::Tiny->read( $file, 'utf8' )
200             || return $self->err( 'ConfigNotFound',
201             $file . ' ' . Config::Tiny->errstr );
202              
203             # Merge in the repo config with the current context (user) config
204 0         0 while ( my ( $k1, $v1 ) = each %$conf ) {
205 0 0       0 if ( ref $v1 eq 'HASH' ) {
206 0         0 while ( my ( $k2, $v2 ) = each %$v1 ) {
207 0 0       0 if ( $k1 eq '_' ) {
208 0         0 $config->{$k2} = $v2;
209             }
210             else {
211 0         0 $config->{$k1}->{$k2} = $v2;
212             }
213             }
214             }
215             else {
216 0         0 $config->{$k1} = $v1;
217             }
218             }
219              
220 0         0 return $repo;
221             }
222              
223             sub dbfile {
224 215     215 0 394 my $self = shift;
225 215         338 my $version = shift;
226 215         1573 return sprintf( 'db-v%d.sqlite3', $version );
227             }
228              
229             sub _build_user_db {
230 1     1   2 my $self = shift;
231 1         4 my $file = $self->user_repo->child( $self->dbfile( $self->DBVERSION ) );
232              
233 0 0       0 return $self->err( 'DBNotFound', 'database not found: %s', $file )
234             unless -f $file;
235              
236 0         0 my $dsn = 'dbi:SQLite:dbname=' . $file;
237              
238 0         0 require Bif::DB;
239 0         0 my $db = Bif::DB->connect( $dsn, undef, undef, undef, $self->debug );
240              
241 0         0 $log->debug( 'bif: user_db: ' . $dsn );
242 0         0 $log->debug( 'bif: SQLite version: ' . $db->{sqlite_version} );
243              
244 0         0 return $db;
245             }
246              
247             sub _build_user_dbw {
248 1     1   2 my $self = shift;
249 1         4 my $file = $self->user_repo->child( $self->dbfile( $self->DBVERSION ) );
250              
251 0 0       0 return $self->err( 'DBNotFound', 'database not found: %s', $file )
252             unless -f $file;
253              
254 0         0 my $dsn = 'dbi:SQLite:dbname=' . $file;
255              
256 0         0 require Bif::DBW;
257 0         0 my $dbw = Bif::DBW->connect( $dsn, undef, undef, undef, $self->debug );
258              
259 0         0 $log->debug( 'bif: user_dbw: ' . $dsn );
260 0         0 $log->debug( 'bif: SQLite version: ' . $dbw->{sqlite_version} );
261              
262 0         0 return $dbw;
263             }
264              
265             sub _build_db {
266 16     16   36 my $self = shift;
267 16         162 my $file = $self->repo->child( $self->dbfile( $self->DBVERSION ) );
268              
269 0 0       0 return $self->err( 'DBNotFound', 'database not found: %s', $file )
270             unless -f $file;
271              
272 0         0 my $dsn = 'dbi:SQLite:dbname=' . $file;
273              
274 0         0 require Bif::DB;
275 0         0 my $db = Bif::DB->connect( $dsn, undef, undef, undef, $self->debug );
276              
277 0         0 $log->debug( 'bif: db: ' . $dsn );
278 0         0 $log->debug( 'bif: SQLite version: ' . $db->{sqlite_version} );
279              
280 0         0 return $db;
281             }
282              
283             sub _build_dbw {
284 144     144   342 my $self = shift;
285 144         599 my $file = $self->repo->child( $self->dbfile( $self->DBVERSION ) );
286              
287 131 50       4842 return $self->err( 'DBNotFound', 'database not found: %s', $file )
288             unless -f $file;
289              
290 131         3237 my $dsn = 'dbi:SQLite:dbname=' . $file;
291              
292 131         1399 require Bif::DBW;
293 131         734 my $dbw = Bif::DBW->connect( $dsn, undef, undef, undef, $self->debug );
294              
295 131         3963 $log->debug( 'bif: dbw: ' . $dsn );
296 131         4037 $log->debug( 'bif: SQLite version: ' . $dbw->{sqlite_version} );
297              
298 131         2860 return $dbw;
299             }
300              
301             ### class methods ###
302              
303             sub new_cmd {
304 0     0 1 0 my $self = shift;
305 0         0 my $class = shift;
306              
307 0 0       0 Carp::croak($@) unless eval "require $class;";
308              
309 0         0 return $class->new( %$self, @_ );
310             }
311              
312             sub dispatch {
313 158     158 1 433 my $self = shift;
314 158         340 my $class = shift;
315              
316 158 100       17051 Carp::croak($@) unless eval "require $class;";
317              
318 118         1567 return $class->new( %$self, @_ )->run;
319             }
320              
321             # Run user defined aliases
322             sub run {
323 0     0 1 0 my $self = shift;
324 0         0 my $opts = $self->opts;
325 0         0 my @cmd = @{ $opts->{alias} };
  0         0  
326 0         0 my $alias = shift @cmd;
327              
328 46     46   300 use File::HomeDir;
  46         109  
  46         2393  
329 46     46   250 use Path::Tiny;
  46         101  
  46         10855  
330              
331 0         0 my $repo = path( File::HomeDir->my_home, '.bifu' );
332 0 0       0 die usage(qq{unknown COMMAND or ALIAS "$alias"}) unless -d $repo;
333              
334             # Trigger user config
335 0         0 $self->user_repo;
336 0 0       0 my $str = $self->config->{'user.alias'}->{$alias}
337             or die usage(qq{unknown COMMAND or ALIAS "$alias"});
338              
339             # Make sure these options are correctly passed through (or not)
340 0         0 delete $opts->{alias};
341 0 0       0 $opts->{debug} = undef if exists $opts->{debug};
342 0 0       0 $opts->{no_pager} = undef if exists $opts->{no_pager};
343 0 0       0 $opts->{no_color} = undef if exists $opts->{no_color};
344 0 0       0 $opts->{user_repo} = undef if exists $opts->{user_repo};
345              
346 0         0 unshift( @cmd, split( ' ', $str ) );
347              
348 46     46   3251 use OptArgs qw/class_optargs/;
  46         91182  
  46         428  
349 0         0 my ( $class, $newopts ) = OptArgs::class_optargs( 'App::bif', $opts, @cmd );
350              
351 0         0 return $class->new(
352             opts => $newopts,
353             user_repo => $self->user_repo,
354             )->run;
355             }
356              
357             sub find_repo {
358 4     4 0 8 my $self = shift;
359 4         8 my $name = shift;
360              
361 4 100       16 return $self->user_repo if $self->opts->{user_repo};
362              
363 1         5 my $try = cwd;
364              
365 1         28 while (1) {
366 3 50       82 if ( -d ( my $repo = $try->child($name) ) ) {
    50          
367 0         0 return $repo;
368             }
369             elsif ( -f $repo ) { # inside a repo directory
370 0         0 return $try;
371             }
372 3 100       1527 last if $try->is_rootdir;
373 2         50 $try = $try->parent;
374             }
375              
376 1         111 return;
377             }
378              
379             sub colours {
380 82     82 1 203 my $self = shift;
381 82         41851 state $have_term_ansicolor = require Term::ANSIColor;
382              
383 82 100       346310 return map { '' } @_ if $self->opts->{no_color};
  16         37  
384              
385 74         704 my $ref = $self->_colours;
386 74   66     1043 map { $ref->{$_} //= Term::ANSIColor::color($_) } @_;
  149         2924  
387 74 50       1010 return map { $ref->{$_} } @_ if wantarray;
  149         489  
388 0         0 return $ref->{ $_[0] };
389             }
390              
391             sub header {
392 0     0 1 0 my $self = shift;
393 0         0 state $reset = $self->colours(qw/reset/);
394 0         0 state $dark = $self->colours(qw/dark/);
395              
396 0         0 my ( $key, $val, $val2 ) = @_;
397             return [
398 0 0       0 ( $key ? $key . ':' : '' ) . $reset,
    0          
399             $val . ( defined $val2 ? $dark . ' <' . $val2 . '>' : '' ) . $reset
400             ];
401             }
402              
403             sub s2hms {
404 0     0 1 0 my $self = shift;
405 0         0 my $s = shift;
406              
407 0         0 return sprintf(
408             '%+0.2d:%0.2d:%0.2d',
409             int( $s / 3600 ),
410             int( ( $s - 3600 * int( $s / 3600 ) ) / 60 ),
411             $s % 60
412             );
413             }
414              
415             sub s2hm {
416 0     0 1 0 my $self = shift;
417 0         0 my $s = shift;
418              
419 0         0 return sprintf( '%+0.2d:%0.2d',
420             int( $s / 3600 ),
421             int( $s - 3600 * int( $s / 3600 ) ) / 60 );
422             }
423              
424             sub datetime2s {
425 0     0 1 0 my $self = shift;
426 0         0 my $dt = shift;
427 0         0 my $new_dt = $dt;
428              
429 0 0       0 if ( $dt =~ m/^(\d?\d):(\d{2})$/ ) {
    0          
    0          
    0          
    0          
    0          
430 0         0 my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) =
431             localtime(time);
432              
433 0         0 $new_dt = sprintf(
434             '%0.4d-%0.2d-%0.2d %0.2d:%0.2d:%0.2d',
435             $year + 1900,
436             $mon + 1, $mday, $1, $2, 0
437             );
438             }
439             elsif ( $dt =~ m/^(\d{2}):(\d{2}):(\d{2})$/ ) {
440 0         0 my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) =
441             localtime(time);
442              
443 0         0 $new_dt = sprintf(
444             '%0.4d-%0.2d-%0.2d %0.2d:%0.2d:%0.2d',
445             $year + 1900,
446             $mon + 1, $mday, $1, $2, $3
447             );
448             }
449             elsif ( $dt =~ m/^yesterday (\d?\d):(\d{2})$/ ) {
450 0         0 my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) =
451             localtime( time - 24 * 60 * 60 );
452              
453 0         0 $new_dt = sprintf(
454             '%0.4d-%0.2d-%0.2d %0.2d:%0.2d:%0.2d',
455             $year + 1900,
456             $mon + 1, $mday, $1, $2, 0
457             );
458             }
459             elsif ( $dt =~ m/^yesterday (\d?\d):(\d{2}):(\d{2})$/ ) {
460 0         0 my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) =
461             localtime( time - 24 * 60 * 60 );
462              
463 0         0 $new_dt = sprintf(
464             '%0.4d-%0.2d-%0.2d %0.2d:%0.2d:%0.2d',
465             $year + 1900,
466             $mon + 1, $mday, $1, $2, $3
467             );
468             }
469             elsif ( $dt =~ m/^(\d{4})-(\d{2})-(\d{2})$/ ) {
470 0         0 $new_dt .= $dt . ' 00:00:00';
471             }
472             elsif ( $dt =~ m/^(\d{4})-(\d{2})-(\d{2}) (\d?\d):(\d{2})$/ ) {
473 0         0 $new_dt = $dt . ':00';
474             }
475              
476 0         0 $log->debugf( 'datetime2s "%s" -> "%s"', $dt, $new_dt );
477 0 0       0 if ( $new_dt =~ m/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/ ) {
478 0         0 require Time::Local;
479 0         0 return Time::Local::timelocal( $6, $5, $4, $3, $2 - 1, $1 - 1900 );
480             }
481              
482 0         0 return $self->err( 'InvalidDateTime', 'invalid date/time string: %s', $dt );
483             }
484              
485             sub ctime_ago {
486 0     0 0 0 my $self = shift;
487 0         0 my $row = shift;
488              
489 0         0 state $have_time_piece = require Time::Piece;
490 0         0 state $have_time_duration = require Time::Duration;
491              
492 46     46   97688 use locale;
  46         27205  
  46         280  
493              
494             return (
495             Time::Duration::ago( $row->{ctime_age}, 1 ),
496             Time::Piece->strptime( $row->{ctime} + $row->{ctimetz}, '%s' )
497             ->strftime('%a %Y-%m-%d %H:%M ') . $row->{ctimetzhm}
498 0         0 );
499             }
500              
501             sub mtime_ago {
502 0     0 0 0 my $self = shift;
503 0         0 my $row = shift;
504              
505 0         0 state $have_time_piece = require Time::Piece;
506 0         0 state $have_time_duration = require Time::Duration;
507              
508 46     46   6815 use locale;
  46         96  
  46         202  
509              
510             return (
511             Time::Duration::ago( $row->{mtime_age}, 1 ),
512             Time::Piece->strptime( $row->{mtime} + $row->{mtimetz}, '%s' )
513             ->strftime('%a %Y-%m-%d %H:%M ') . $row->{mtimetzhm}
514 0         0 );
515             }
516              
517             sub err {
518 39     39 1 3275 my $self = shift;
519 39 50       166 Carp::croak('err($type, $msg, [$arg])') unless @_ >= 2;
520 39         438 my $err = shift;
521 39         74 my $msg = shift;
522              
523 39 50       78 die $msg if eval { $msg->isa('Bif::Error') };
  39         525  
524 39         266 my ( $red, $reset ) = $self->colours(qw/red reset/);
525              
526 39         273 $msg = $red . 'error:' . $reset . ' ' . $msg . "\n";
527              
528 39         156 die Bif::Error->new( $self->opts, $err, $msg, @_ );
529             }
530              
531             sub ok {
532 90     90 1 1853 my $self = shift;
533 90 50       374 Carp::croak('ok($type, [$arg])') unless @_;
534 90         209 my $ok = shift;
535 90         474 return Bif::OK->new( $self->opts, $ok, @_ );
536             }
537              
538             sub start_pager {
539 1     1 1 2 my $self = shift;
540 1         3 my $lines = shift;
541              
542 1 50 33     13 return if $pager or $self->no_pager;
543              
544 0 0       0 if ($lines) {
545 0         0 my $term_height = $self->term_height;
546 0 0       0 if ( $lines <= $term_height ) {
547 0         0 $log->debug("bif: no start_pager ($lines <= $term_height)");
548 0         0 return;
549             }
550             }
551              
552 0         0 local $ENV{'LESS'} = '-FXeR';
553 0 0       0 local $ENV{'MORE'} = '-FXer' unless MSWin32;
554              
555 0         0 require App::bif::Pager;
556 0         0 $pager = App::bif::Pager->new;
557              
558 0         0 $log->debugf( 'bif: start_pager (fileno: %d)', fileno( $pager->fh ) );
559              
560 0         0 return $pager;
561             }
562              
563             sub end_pager {
564 0     0 1 0 my $self = shift;
565 0 0       0 return unless $pager;
566              
567 0         0 $log->debug('bif: end_pager');
568 0         0 $pager = undef;
569 0         0 return;
570             }
571              
572             sub user_id {
573 0     0 1 0 my $self = shift;
574 0         0 my $id = $self->db->xval(
575             select => 'bif.identity_id',
576             from => 'bifkv bif',
577             where => { 'bif.key' => 'self' },
578             );
579 0         0 return $id;
580             }
581              
582             sub uuid2id {
583 2     2 1 4 my $self = shift;
584 2   33     8 my $try = shift // Carp::confess 'uuid2id needs defined';
585 2         7 my $opts = $self->opts;
586 2 50       11 Carp::croak 'usage' if @_;
587              
588 2 50 33     24 return $try unless exists $opts->{uuid} && $opts->{uuid};
589 0         0 my @list = $self->db->uuid2id($try);
590              
591 0 0       0 return $self->err( 'UuidNotFound', "uuid not found: $try" )
592             unless @list;
593              
594             return $self->err( 'UuidAmbiguous',
595             "ambiguious uuid: $try\n "
596 0 0       0 . join( "\n ", map { "$_->[1] -> ID:$_->[0]" } @list ) )
  0         0  
597             if @list > 1;
598              
599 0         0 return $list[0]->[0];
600             }
601              
602             sub get_project {
603 0     0 1 0 my $self = shift;
604 0   0     0 my $path = shift // Carp::confess 'path must be defined';
605 0         0 my $db = $self->db;
606              
607 0         0 my @matches = $db->get_projects($path);
608              
609 0 0       0 return $self->err( 'ProjectNotFound', "project not found: $path" )
610             unless @matches;
611              
612 0 0       0 return $matches[0] if 1 == @matches;
613              
614             return $self->err( 'AmbiguousPath',
615             "ambiguous path \"$path\" matches the following:\n" . ' '
616 0         0 . join( "\n ", map { "$_->{path}" } @matches ) );
  0         0  
617             }
618              
619             sub get_hub {
620 0     0 1 0 my $self = shift;
621 0         0 my $name = shift; # || Carp::confess 'get_hub($name)';
622 0         0 my $hub = $self->db->get_hub($name);
623              
624 0 0       0 return $self->err( 'HubNotFound', "hub not found: $name" )
625             unless $hub;
626              
627 0         0 return $hub;
628             }
629              
630             sub render_table {
631 1     1 1 1 my $self = shift;
632 1         2 my $format = shift;
633 1         2 my $header = shift;
634 1         2 my $data = shift;
635 1   50     7 my $indent = shift || 0;
636              
637 1         3 my ( $white, $dark, $reset ) = $self->colours(qw/yellow dark reset/);
638 1         763 require Text::FormatTable;
639              
640 1         2845 my $table = Text::FormatTable->new($format);
641              
642 1 50       94 if ($header) {
643 0         0 $header->[0] = $white . $header->[0];
644 0         0 push( @$header, ( pop @$header ) . $reset );
645 0         0 $table->head(@$header);
646             }
647              
648 1         4 foreach my $row (@$data) {
649 13         263 $table->row(@$row);
650             }
651              
652 1         32 my $term_width = $self->term_width;
653 1 50       7 return $table->render($term_width) unless $indent;
654              
655 0         0 my $str = $table->render( $term_width - $indent );
656              
657 0         0 my $prefix = ' ' x $indent;
658 0         0 $str =~ s/^/$prefix/gm;
659 0         0 return $str;
660             }
661              
662             sub prompt_edit {
663 0     0 1 0 my $self = shift;
664 0         0 my %args = (
665             opts => {},
666             abort_on_empty => 1,
667             val => '',
668             @_,
669             );
670              
671 0   0     0 $args{txt} //= "\n";
672 0         0 $args{txt} .= "
673             # Please enter your message. Lines starting with '#'
674             # are ignored. Empty content aborts.
675             #
676             ";
677              
678 0         0 my $now = time;
679              
680 0         0 foreach my $key ( sort keys %{ $args{opts} } ) {
  0         0  
681 0 0       0 next if $key =~ m/^_/;
682 0 0       0 next unless defined $args{opts}->{$key};
683 0         0 $args{txt} .= "# $key: $args{opts}->{$key}\n";
684             }
685              
686 0         0 require IO::Prompt::Tiny;
687 0 0       0 if ( IO::Prompt::Tiny::_is_interactive() ) {
688 0         0 require App::bif::Editor;
689 0         0 $args{val} = App::bif::Editor->new( txt => $args{txt} )->result;
690             }
691              
692 0         0 $args{val} =~ s/^#.*//gm;
693 0         0 $args{val} =~ s/^\n+//s;
694 0         0 $args{val} =~ s/\n*$/\n/s;
695              
696 0 0       0 if ( $args{abort_on_empty} ) {
697             return $self->err( 'EmptyContent', 'aborting due to empty content.' )
698 0 0       0 if $args{val} =~ m/^[\s\n]*$/s;
699             }
700              
701 0         0 return $args{val};
702             }
703              
704             my $old = '';
705              
706             sub lprint {
707 0     0 1 0 my $self = shift;
708 0         0 my $msg = shift;
709              
710 0 0 0     0 if ( $pager or $self->opts->{debug} ) {
711 0         0 return print $msg . "\n";
712             }
713              
714 0         0 local $| = 1;
715              
716 0         0 my $chars = print ' ' x length($old), "\b" x length($old), $msg, "\r";
717 0 0       0 $old = $msg =~ m/\n/ ? '' : $msg;
718 0         0 return $chars;
719             }
720              
721             sub get_change {
722 0     0 1 0 my $self = shift;
723 0   0     0 my $token = shift // Carp::croak('get_change needs defined');
724 0         0 my $first_change_id = shift;
725              
726 0 0       0 return $self->err( 'InvalidChangeID',
727             "invalid change ID (must be cID): $token" )
728             unless $token =~ m/^c(\d+)$/;
729              
730 0         0 my $id = $1;
731 0         0 my $db = $self->db;
732              
733 0         0 my $data = $db->xhashref(
734             select => [ 'c.id AS id', 'c.uuid AS uuid', ],
735             from => 'changes c',
736             where => { 'c.id' => $id },
737             );
738              
739 0 0       0 return $self->err( 'ChangeNotFound', "change not found: $token" )
740             unless $data;
741              
742 0 0       0 if ($first_change_id) {
743 0         0 my $t = $db->xhashref(
744             select => 1,
745             from => 'changes_tree ct',
746             where => {
747             'ct.child' => $id,
748             'ct.parent' => $first_change_id,
749             },
750             );
751              
752 0 0       0 return $self->err( 'FirstChangeMismatch',
753             'first change id mismatch: c%d / c%d',
754             $first_change_id, $id )
755             unless $t;
756             }
757              
758 0         0 return $data;
759             }
760              
761             sub get_node {
762 2     2 1 5 my $self = shift;
763              
764 2   33     7 my $token = shift // Carp::confess('get_node needs defined');
765 2         158 my $kind = shift;
766 2         13 my $db = $self->db;
767              
768 0         0 state $have_qv = DBIx::ThinSQL->import(qw/ qv bv /);
769              
770 0 0       0 if ( $token =~ m/^\d+$/ ) {
771 0         0 my $data = $db->xhashref(
772             select => [
773             'n.id AS id',
774             'n.kind AS kind',
775             't.tkind AS tkind',
776             'n.uuid AS uuid',
777             'n.first_change_id AS first_change_id',
778             ],
779             from => 'nodes n',
780             left_join => 'topics t',
781             on => 't.id = n.id',
782             where => { 'n.id' => $token },
783             );
784              
785             return $self->err( 'WrongKind', 'node (%s) is not a %s: %d',
786             $data->{kind}, $kind, $token )
787 0 0 0     0 if $data && $kind && $kind ne $data->{kind};
      0        
788              
789 0 0       0 return $data if $data;
790             }
791              
792 0         0 my $pinfo = eval { $self->get_project($token) };
  0         0  
793 0 0 0     0 die $@ if ( $@ && $@->isa('Bif::Error::AmbiguousPath') );
794 0 0       0 return $pinfo if $pinfo;
795              
796 0   0     0 $kind ||= 'node';
797 0         0 return $self->err( 'TopicNotFound', "$kind not found: $token" );
798             }
799              
800             sub save_new_work {
801 41     41 1 131 my $self = shift;
802              
803 41         461 state $have_time_piece = require Time::Piece;
804 41         246 state $have_time_seconds = require Time::Seconds;
805 41         385 state $have_coalesce_qv = DBIx::ThinSQL->import(qw/ coalesce qv /);
806              
807             # Some error in Time::Piece exposed if gmtime is passed a
808             # Time::Seconds object so make it a plain scalar for now and open
809             # a bug at some point... in my spare time :-(
810              
811 41         2672 my $args = { offset => ${ Time::Piece->new->tzoffset }, @_ };
  41         487  
812              
813             # Prefer not to insert work entries that cross midnight so split
814             # the request up accordingly.
815 41         13197 my $start = Time::Piece->gmtime( $args->{start} + $args->{offset} );
816 41         1813 my $stop = Time::Piece->gmtime( $args->{stop} + $args->{offset} );
817              
818 41         1530 until ( $start->ymd eq $stop->ymd ) {
819 0         0 my $day_end =
820             Time::Piece->strptime( $start->ymd, '%Y-%m-%d' ) +
821             Time::Seconds->ONE_DAY - 1;
822              
823             $self->dbw->xdo(
824             insert_into => [
825             'func_new_work', qw/
826             change_id
827             node_id
828             offset
829             start
830             stop
831             comment
832             bill
833             /
834             ],
835             select => [
836             qv( $args->{change_id} ),
837             qv( $args->{node_id} ),
838             qv( $args->{offset} ),
839             qv( ( $start - $args->{offset} )->epoch ),
840             qv( ( $day_end - $args->{offset} )->epoch ),
841             qv( $args->{comment} ),
842             coalesce( qv( $args->{bill} ), 'n.bill' ),
843             ],
844             from => 'nodes n',
845             where => { 'n.id' => $args->{node_id} },
846 0         0 );
847              
848 0         0 $start = $day_end + 1;
849             }
850              
851             $self->dbw->xdo(
852             insert_into => [
853             'func_new_work', qw/
854             change_id
855             node_id
856             offset
857             start
858             stop
859             comment
860             bill
861             /
862             ],
863             select => [
864             qv( $args->{change_id} ),
865             qv( $args->{node_id} ),
866             qv( $args->{offset} ),
867             qv( ( $start - $args->{offset} )->epoch ),
868             qv( ( $stop - $args->{offset} )->epoch ),
869             qv( $args->{comment} ),
870             coalesce( qv( $args->{bill} ), 'n.bill' ),
871             ],
872             from => 'nodes n',
873             where => { 'n.id' => $args->{node_id} },
874 41         1687 );
875             }
876              
877             sub current_work {
878 43     43 1 98 my $self = shift;
879 43         209 return $self->dbw->xhashref(
880             select => [
881             'n.id AS node_id',
882             'COALESCE(t.tkind,n.kind) AS kind',
883             'n.path AS path',
884             'wb.start AS start',
885             time . ' AS stop',
886             'wb.offset AS offset',
887             'wb.bill AS bill',
888             'wb.comment AS comment',
889             'COALESCE(t.title,"") AS title',
890             ],
891             from => 'work_buffers wb',
892             inner_join => 'nodes n',
893             on => 'n.id = wb.node_id',
894             left_join => 'topics t',
895             on => 't.id = n.id',
896             );
897             }
898              
899             sub start_work {
900 0     0 1 0 my $self = shift;
901 0         0 my $args = {@_};
902 0         0 my $dbw = $self->dbw;
903              
904 0         0 state $have_coalesce_qv = DBIx::ThinSQL->import(qw/ coalesce qv /);
905              
906             return $self->dbw->xdo(
907             insert_into => [
908             'work_buffers', qw/
909             node_id
910             start
911             comment
912             bill
913             /
914             ],
915             select => [
916             qv( $args->{node_id} ),
917             qv( $args->{start} // time ),
918             qv( $args->{comment} ),
919             coalesce( qv( $args->{bill} ), 'n.bill' ),
920             ],
921             from => 'nodes n',
922             where => { 'n.id' => $args->{node_id} },
923 0   0     0 );
924             }
925              
926             sub stop_work {
927 0     0 1 0 my $self = shift;
928 0         0 my $args = {@_};
929 0         0 my $dbw = $self->dbw;
930 0   0     0 my $work = $self->current_work || return;
931              
932 0         0 $args->{change_id} = $self->new_change;
933              
934             $self->save_new_work(
935             change_id => $args->{change_id},
936             node_id => $work->{node_id},
937             start => $work->{start},
938             stop => $args->{stop} // time,
939             offset => $work->{offset},
940             comment => $args->{comment} // $work->{comment},
941             bill => $args->{bill} // $work->{bill},
942 0   0     0 );
      0        
      0        
943              
944             $self->end_change(
945             id => $args->{change_id},
946             action_format => "work $work->{kind} %s",
947             action_node_id_1 => $work->{node_id},
948 0         0 message => 'stuff', #$opts->{message},
949             );
950              
951 0         0 $dbw->xdo( delete_from => 'work_buffers', );
952             }
953              
954             sub pause_work {
955 42     42 1 113 my $self = shift;
956              
957 42 50       320 $self->work_buffer( $self->current_work ) || return;
958 0         0 print "Pausing work on ...\n";
959 0         0 $self->stop_work;
960             }
961              
962             sub resume_work {
963 41     41 1 112 my $self = shift;
964 41         114 my $args = {@_};
965 41   50     288 my $saved = $self->work_buffer || return;
966              
967 0         0 print "Resuming work on ...\n";
968             $self->start_work(
969             node_id => $saved->{node_id},
970             start => time,
971             comment => $saved->{comment},
972             bill => $saved->{bill},
973 0         0 );
974              
975 0         0 $self->work_buffer(undef);
976             }
977              
978             sub new_change {
979 42     42 1 1140 my $self = shift;
980 42         273 my %vals = @_;
981 42         220 my $dbw = $self->dbw;
982              
983 42   33     502 $vals{id} ||= $dbw->nextval('changes');
984 42         11395 my ( $author, $author_contact, $author_contact_method, $author_shortname )
985             = $dbw->xlist(
986             select => [qw/e.name ecm.mvalue ecm.method i.shortname/],
987             from => 'bifkv b',
988             inner_join => 'entities e',
989             on => 'e.id = b.identity_id',
990             inner_join => 'identities i',
991             on => 'i.id = b.identity_id',
992             inner_join => 'entity_contact_methods ecm',
993             on => 'ecm.id = e.default_contact_method_id',
994             where => { key => 'self' },
995             );
996 42   66     24439 $vals{author} //= $author;
997 42   66     367 $vals{author_contact} //= $author_contact;
998 42   66     190 $vals{author_contact_method} //= $author_contact_method;
999 42   66     178 $vals{author_shortname} //= $author_shortname;
1000              
1001             return $self->err( 'NoSelfIdentity',
1002             'no "self" identity defined at change begin' )
1003 42 100       188 unless $vals{author};
1004              
1005 41         230 $dbw->xdo(
1006             insert_into => 'func_begin_change',
1007             values => \%vals,
1008             );
1009              
1010 41         57889 return $vals{id};
1011             }
1012              
1013             sub check {
1014 41     41 1 115 my $self = shift;
1015 41         103 my $id = shift;
1016 41   33     234 my $db = shift || $self->db;
1017              
1018 41         31397 state $have_changeset = require Bif::DB::Plugin::ChangeUUIDv1;
1019 41         37712 state $have_yaml = require YAML::Tiny;
1020 41         238699 state $have_encode = require Encode;
1021 41         234 state $have_digest = require Digest::SHA;
1022              
1023 41   33     554 my $changeset =
1024             $db->uchangeset_v1($id) || Carp::croak "invalid change.id: $id";
1025              
1026 41         129 my $begin = shift @$changeset;
1027 41         108 my $end = pop @$changeset;
1028 41         139 my $uuid = delete $end->{uuid};
1029 41         186 my $short = substr( $uuid, 0, 8 );
1030              
1031 41         141 foreach my $x (@$changeset) {
1032             next
1033             unless ref $x eq 'HASH'
1034             && exists $x->{_delta}
1035 164 100 33     1629 && $x->{_delta} =~ m/^new_/;
      66        
1036              
1037 123         304 delete $x->{uuid};
1038             }
1039              
1040 41         275 my $yaml = YAML::Tiny::Dump( [ $begin, @$changeset, $end ] );
1041 41         76002 my $sha1 = Digest::SHA::sha1_hex( Encode::encode( 'UTF-8', $yaml ) );
1042              
1043 41         10265 my $action = $db->xval(
1044             select => ['c.action'],
1045             from => 'changes c',
1046             where => { 'c.id' => $id },
1047             );
1048              
1049 41         14782 my ( $red, $reset ) = $self->colours( 'red', 'reset' );
1050 41 50       933 return ( 1, "[change: $id <$short>] $action" ) if $uuid eq $sha1;
1051 0         0 return ( 0, "[change: $id <$short>] $action ${red}INVALID$reset" );
1052             }
1053              
1054             sub end_change {
1055 41     41 0 127 my $self = shift;
1056 41         209 my $dbw = $self->dbw;
1057 41         278 my %args = @_;
1058              
1059 41         495 my $iid = $dbw->xval(
1060             select => [ 'bif.identity_id', ],
1061             from => 'bifkv bif',
1062             where => { 'bif.key' => 'self' },
1063             );
1064              
1065 41 50       13740 return $self->err( 'NoSelfIdentity',
1066             'no "self" identity defined at change end' )
1067             unless $iid;
1068              
1069 41         1308 $dbw->xdo(
1070             insert_into => 'func_end_change',
1071             values => {
1072             identity_id => $iid,
1073             %args,
1074             },
1075             );
1076              
1077 41         39837 $dbw->xdo(
1078             insert_into => 'func_merge_changes',
1079             values => { merge => 1 },
1080             );
1081              
1082 41         309149 my $change_id = $dbw->xval( select => 'currval("changes")' );
1083 41         16246 my ( $ok, $str ) = $self->check( $change_id, $dbw );
1084 41 50       2737 return print $str. "\n" if $ok;
1085              
1086             # Show the diff to see what's wrong
1087 0         0 $self->new_cmd(
1088             'App::bif::show::change',
1089             db => $self->dbw, # to make current txn visible
1090             opts => {
1091             uid => 'c' . $change_id,
1092             diff => 1,
1093             }
1094             )->run;
1095              
1096 0         0 my $keep_invalid = $dbw->xval(
1097             select => 'bool_val',
1098             from => 'bifkv',
1099             where => { key => 'keep_invalid' },
1100             );
1101 0 0       0 return $self->err( 'InvalidUUID', $str ) unless $keep_invalid;
1102 0         0 return print $str;
1103             }
1104              
1105             sub DESTROY {
1106 199     199   28211 my $self = shift;
1107             Log::Any::Adapter->remove( $self->{_bif_log_any_adapter} )
1108 199 50       5785 if $self->{_bif_log_any_adapter};
1109             }
1110              
1111             package Bif::OK;
1112             use overload
1113 39     39   293 bool => sub { 1 },
1114 46         739 '""' => \&as_string,
1115 46     46   201351 fallback => 1;
  46         133  
1116              
1117             sub new {
1118 134     134   2899 my $proto = shift;
1119 134         256 my $opts = shift;
1120 134   33     598 $opts->{_bif_ok_type} = shift || Carp::confess('missing type');
1121 134   100     738 $opts->{_bif_ok_msg} = shift || '';
1122 134 100       436 $opts->{_bif_ok_msg} = sprintf( $opts->{_bif_ok_msg}, @_ ) if @_;
1123              
1124 134         440 my $class = $proto . '::' . $opts->{_bif_ok_type};
1125             {
1126 46     46   8268 no strict 'refs';
  46         118  
  46         8319  
  134         239  
1127 134         319 *{ $class . '::ISA' } = [$proto];
  134         2247  
1128             }
1129              
1130 134         3235 return bless {%$opts}, $class;
1131             }
1132              
1133             sub as_string {
1134 8     8   2560 my $self = shift;
1135             return $self->{_bif_ok_msg}
1136 8 100 66     79 if $self->{_bif_ok_msg} && !ref $self->{_bif_ok_msg};
1137 3         29 return ref $self;
1138             }
1139              
1140             package Bif::Error;
1141             use overload
1142 67     67   7486 bool => sub { 1 },
1143 46     46   301 fallback => 1;
  46         93  
  46         309  
1144              
1145             our @ISA = ('Bif::OK');
1146              
1147             1;
1148              
1149             __END__