line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package App::TimeTracker; |
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
# ABSTRACT: time tracking for impatient and lazy command line lovers |
4
|
|
|
|
|
|
|
our $VERSION = '3.008'; # VERSION |
5
|
|
|
|
|
|
|
|
6
|
13
|
|
|
13
|
|
1639652
|
use strict; |
|
13
|
|
|
|
|
53
|
|
|
13
|
|
|
|
|
470
|
|
7
|
13
|
|
|
13
|
|
82
|
use warnings; |
|
13
|
|
|
|
|
24
|
|
|
13
|
|
|
|
|
345
|
|
8
|
13
|
|
|
13
|
|
261
|
use 5.010; |
|
13
|
|
|
|
|
45
|
|
9
|
|
|
|
|
|
|
|
10
|
13
|
|
|
13
|
|
1619
|
use App::TimeTracker::Data::Task; |
|
13
|
|
|
|
|
69
|
|
|
13
|
|
|
|
|
510
|
|
11
|
13
|
|
|
13
|
|
7369
|
use DateTime; |
|
13
|
|
|
|
|
3742603
|
|
|
13
|
|
|
|
|
566
|
|
12
|
13
|
|
|
13
|
|
125
|
use Moose; |
|
13
|
|
|
|
|
26
|
|
|
13
|
|
|
|
|
199
|
|
13
|
13
|
|
|
13
|
|
96259
|
use Moose::Util::TypeConstraints; |
|
13
|
|
|
|
|
35
|
|
|
13
|
|
|
|
|
149
|
|
14
|
13
|
|
|
13
|
|
34106
|
use Path::Class qw(); |
|
13
|
|
|
|
|
105472
|
|
|
13
|
|
|
|
|
395
|
|
15
|
13
|
|
|
13
|
|
8711
|
use Path::Class::Iterator; |
|
13
|
|
|
|
|
123909
|
|
|
13
|
|
|
|
|
168
|
|
16
|
13
|
|
|
13
|
|
6304
|
use MooseX::Storage::Format::JSONpm; |
|
13
|
|
|
|
|
871841
|
|
|
13
|
|
|
|
|
595
|
|
17
|
13
|
|
|
13
|
|
110
|
use JSON::XS; |
|
13
|
|
|
|
|
28
|
|
|
13
|
|
|
|
|
5637
|
|
18
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
our $HOUR_RE = qr/(?<hour>[012]?\d)/; |
20
|
|
|
|
|
|
|
our $MINUTE_RE = qr/(?<minute>[0-5]?\d)/; |
21
|
|
|
|
|
|
|
our $DAY_RE = qr/(?<day>[0123]?\d)/; |
22
|
|
|
|
|
|
|
our $MONTH_RE = qr/(?<month>[01]?\d)/; |
23
|
|
|
|
|
|
|
our $YEAR_RE = qr/(?<year>2\d{3})/; |
24
|
|
|
|
|
|
|
|
25
|
|
|
|
|
|
|
with qw( |
26
|
|
|
|
|
|
|
MooseX::Getopt |
27
|
|
|
|
|
|
|
); |
28
|
|
|
|
|
|
|
|
29
|
|
|
|
|
|
|
subtype 'TT::DateTime' => as class_type('DateTime'); |
30
|
|
|
|
|
|
|
subtype 'TT::RT' => as 'Int'; |
31
|
|
|
|
|
|
|
subtype 'TT::Duration' => as enum( [qw(day week month year)] ); |
32
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
coerce 'TT::RT' => from 'Str' => via { |
34
|
|
|
|
|
|
|
my $raw = $_; |
35
|
|
|
|
|
|
|
$raw =~ s/\D//g; |
36
|
|
|
|
|
|
|
return $raw; |
37
|
|
|
|
|
|
|
}; |
38
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
coerce 'TT::DateTime' => from 'Str' => via { |
40
|
|
|
|
|
|
|
my $raw = $_; |
41
|
|
|
|
|
|
|
my $dt = DateTime->now; |
42
|
|
|
|
|
|
|
$dt->set_time_zone('local'); |
43
|
|
|
|
|
|
|
$dt->set( second => 0 ); |
44
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
if ($raw) { |
46
|
|
|
|
|
|
|
if ( $raw =~ /^ $HOUR_RE : $MINUTE_RE $/x ) { # "13:42" |
47
|
12
|
|
|
12
|
|
6218
|
$dt->set( hour => $+{hour}, minute => $+{minute} ); |
|
12
|
|
|
|
|
5410
|
|
|
12
|
|
|
|
|
3726
|
|
48
|
|
|
|
|
|
|
} |
49
|
|
|
|
|
|
|
elsif ( $raw =~ /^ $YEAR_RE [-.]? $MONTH_RE [-.]? $DAY_RE $/x ) |
50
|
|
|
|
|
|
|
{ # "2010-02-26" |
51
|
|
|
|
|
|
|
$dt->set( year => $+{year}, month => $+{month}, day => $+{day} ); |
52
|
|
|
|
|
|
|
$dt->truncate( to => 'day' ); |
53
|
|
|
|
|
|
|
} |
54
|
|
|
|
|
|
|
elsif ( $raw |
55
|
|
|
|
|
|
|
=~ /^ $YEAR_RE [-.]? $MONTH_RE [-.]? $DAY_RE \s+ $HOUR_RE : $MINUTE_RE $/x |
56
|
|
|
|
|
|
|
) |
57
|
|
|
|
|
|
|
{ # "2010-02-26 12:34" |
58
|
|
|
|
|
|
|
$dt->set( |
59
|
|
|
|
|
|
|
year => $+{year}, |
60
|
|
|
|
|
|
|
month => $+{month}, |
61
|
|
|
|
|
|
|
day => $+{day}, |
62
|
|
|
|
|
|
|
hour => $+{hour}, |
63
|
|
|
|
|
|
|
minute => $+{minute} ); |
64
|
|
|
|
|
|
|
} |
65
|
|
|
|
|
|
|
elsif ( $raw =~ /^ $DAY_RE [-.]? $MONTH_RE [-.]? $YEAR_RE $/x ) |
66
|
|
|
|
|
|
|
{ # "26-02-2010" |
67
|
|
|
|
|
|
|
$dt->set( year => $+{year}, month => $+{month}, day => $+{day} ); |
68
|
|
|
|
|
|
|
$dt->truncate( to => 'day' ); |
69
|
|
|
|
|
|
|
} |
70
|
|
|
|
|
|
|
elsif ( $raw |
71
|
|
|
|
|
|
|
=~ /^ $DAY_RE [-.]? $MONTH_RE [-.]? $YEAR_RE \s $HOUR_RE : $MINUTE_RE $/x |
72
|
|
|
|
|
|
|
) |
73
|
|
|
|
|
|
|
{ # "26-02-2010 12:34" |
74
|
|
|
|
|
|
|
$dt->set( |
75
|
|
|
|
|
|
|
year => $+{year}, |
76
|
|
|
|
|
|
|
month => $+{month}, |
77
|
|
|
|
|
|
|
day => $+{day}, |
78
|
|
|
|
|
|
|
hour => $+{hour}, |
79
|
|
|
|
|
|
|
minute => $+{minute} ); |
80
|
|
|
|
|
|
|
} |
81
|
|
|
|
|
|
|
else { |
82
|
|
|
|
|
|
|
confess "Invalid date format '$raw'"; |
83
|
|
|
|
|
|
|
} |
84
|
|
|
|
|
|
|
} |
85
|
|
|
|
|
|
|
return $dt; |
86
|
|
|
|
|
|
|
}; |
87
|
|
|
|
|
|
|
|
88
|
|
|
|
|
|
|
MooseX::Getopt::OptionTypeMap->add_option_type_to_map( 'TT::DateTime' => '=s', |
89
|
|
|
|
|
|
|
); |
90
|
|
|
|
|
|
|
MooseX::Getopt::OptionTypeMap->add_option_type_to_map( 'TT::RT' => '=i', ); |
91
|
|
|
|
|
|
|
|
92
|
13
|
|
|
13
|
|
115
|
no Moose::Util::TypeConstraints; |
|
13
|
|
|
|
|
47
|
|
|
13
|
|
|
|
|
162
|
|
93
|
|
|
|
|
|
|
|
94
|
|
|
|
|
|
|
has 'home' => ( |
95
|
|
|
|
|
|
|
is => 'ro', |
96
|
|
|
|
|
|
|
isa => 'Path::Class::Dir', |
97
|
|
|
|
|
|
|
traits => ['NoGetopt'], |
98
|
|
|
|
|
|
|
required => 1, |
99
|
|
|
|
|
|
|
); |
100
|
|
|
|
|
|
|
has 'config' => ( |
101
|
|
|
|
|
|
|
is => 'ro', |
102
|
|
|
|
|
|
|
isa => 'HashRef', |
103
|
|
|
|
|
|
|
required => 1, |
104
|
|
|
|
|
|
|
traits => ['NoGetopt'], |
105
|
|
|
|
|
|
|
); |
106
|
|
|
|
|
|
|
has '_current_project' => ( |
107
|
|
|
|
|
|
|
is => 'ro', |
108
|
|
|
|
|
|
|
isa => 'Str', |
109
|
|
|
|
|
|
|
predicate => 'has_current_project', |
110
|
|
|
|
|
|
|
traits => ['NoGetopt'], |
111
|
|
|
|
|
|
|
); |
112
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
has 'tags' => ( |
114
|
|
|
|
|
|
|
isa => 'ArrayRef', |
115
|
|
|
|
|
|
|
is => 'ro', |
116
|
|
|
|
|
|
|
traits => ['Array'], |
117
|
|
|
|
|
|
|
default => sub { [] }, |
118
|
|
|
|
|
|
|
handles => { |
119
|
|
|
|
|
|
|
insert_tag => 'unshift', |
120
|
|
|
|
|
|
|
add_tag => 'push', |
121
|
|
|
|
|
|
|
}, |
122
|
|
|
|
|
|
|
documentation => 'Tags [Multiple]', |
123
|
|
|
|
|
|
|
); |
124
|
|
|
|
|
|
|
|
125
|
|
|
|
|
|
|
has '_current_command' => ( |
126
|
|
|
|
|
|
|
isa => 'Str', |
127
|
|
|
|
|
|
|
is => 'rw', |
128
|
|
|
|
|
|
|
traits => ['NoGetopt'], |
129
|
|
|
|
|
|
|
); |
130
|
|
|
|
|
|
|
|
131
|
|
|
|
|
|
|
has '_current_task' => ( |
132
|
|
|
|
|
|
|
isa => 'App::TimeTracker::Data::Task', |
133
|
|
|
|
|
|
|
is => 'rw', |
134
|
|
|
|
|
|
|
traits => ['NoGetopt'], |
135
|
|
|
|
|
|
|
); |
136
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
has '_previous_task' => ( |
138
|
|
|
|
|
|
|
isa => 'App::TimeTracker::Data::Task', |
139
|
|
|
|
|
|
|
is => 'rw', |
140
|
|
|
|
|
|
|
traits => ['NoGetopt'], |
141
|
|
|
|
|
|
|
); |
142
|
|
|
|
|
|
|
|
143
|
|
|
|
|
|
|
sub run { |
144
|
1
|
|
|
1
|
0
|
20
|
my $self = shift; |
145
|
1
|
|
50
|
|
|
31
|
my $command = 'cmd_' . ( $self->extra_argv->[0] || 'missing' ); |
146
|
|
|
|
|
|
|
|
147
|
1
|
50
|
|
|
|
27
|
$self->cmd_commands() |
148
|
|
|
|
|
|
|
unless $self->can($command); |
149
|
0
|
|
|
|
|
0
|
$self->_current_command($command); |
150
|
0
|
|
|
|
|
0
|
$self->$command; |
151
|
|
|
|
|
|
|
} |
152
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
sub now { |
154
|
2
|
|
|
2
|
0
|
6285
|
my $dt = DateTime->now(); |
155
|
2
|
|
|
|
|
737
|
$dt->set_time_zone('local'); |
156
|
2
|
|
|
|
|
5754
|
return $dt; |
157
|
|
|
|
|
|
|
} |
158
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
sub beautify_seconds { |
160
|
15
|
|
|
15
|
0
|
8236
|
my ( $self, $s ) = @_; |
161
|
15
|
100
|
|
|
|
47
|
return '0' unless $s; |
162
|
13
|
|
|
|
|
31
|
my ( $m, $h ) = ( 0, 0 ); |
163
|
|
|
|
|
|
|
|
164
|
13
|
100
|
|
|
|
39
|
if ( $s >= 60 ) { |
165
|
12
|
|
|
|
|
30
|
$m = int( $s / 60 ); |
166
|
12
|
|
|
|
|
30
|
$s = $s - ( $m * 60 ); |
167
|
|
|
|
|
|
|
} |
168
|
13
|
100
|
100
|
|
|
62
|
if ( $m && $m >= 60 ) { |
169
|
4
|
|
|
|
|
8
|
$h = int( $m / 60 ); |
170
|
4
|
|
|
|
|
8
|
$m = $m - ( $h * 60 ); |
171
|
|
|
|
|
|
|
} |
172
|
13
|
|
|
|
|
205
|
return sprintf( "%02d:%02d:%02d", $h, $m, $s ); |
173
|
|
|
|
|
|
|
} |
174
|
|
|
|
|
|
|
|
175
|
|
|
|
|
|
|
sub find_task_files { |
176
|
5
|
|
|
5
|
0
|
12505
|
my ( $self, $args ) = @_; |
177
|
|
|
|
|
|
|
|
178
|
5
|
|
|
|
|
235
|
my $root = $self->home; |
179
|
5
|
|
|
|
|
14
|
my ( $cmp_from, $cmp_to ); |
180
|
|
|
|
|
|
|
|
181
|
5
|
100
|
|
|
|
27
|
if ( my $from = $args->{from} ) { |
182
|
4
|
|
66
|
|
|
41
|
my $to = $args->{to} || $self->now; |
183
|
4
|
100
|
|
|
|
36
|
$to->set( hour => 23, minute => 59, second => 59 ) unless $to->hour; |
184
|
4
|
|
|
|
|
826
|
$cmp_from = $from->strftime("%Y%m%d%H%M%S"); |
185
|
4
|
|
|
|
|
465
|
$cmp_to = $to->strftime("%Y%m%d%H%M%S"); |
186
|
|
|
|
|
|
|
|
187
|
4
|
100
|
|
|
|
268
|
if ( $from->year == $to->year ) { |
188
|
3
|
|
|
|
|
35
|
$root = $root->subdir( $from->year ); |
189
|
3
|
50
|
|
|
|
241
|
if ( $from->month == $to->month ) { |
190
|
3
|
|
|
|
|
36
|
$root = $root->subdir( $from->strftime("%m") ); |
191
|
|
|
|
|
|
|
} |
192
|
|
|
|
|
|
|
} |
193
|
|
|
|
|
|
|
} |
194
|
|
|
|
|
|
|
|
195
|
5
|
|
|
|
|
234
|
my $projects; |
196
|
5
|
100
|
|
|
|
22
|
if ( $args->{projects} ) { |
197
|
3
|
|
|
|
|
9
|
$projects = join( '|', map { s/-/./g; $_ } @{ $args->{projects} } ); |
|
3
|
|
|
|
|
9
|
|
|
3
|
|
|
|
|
12
|
|
|
3
|
|
|
|
|
12
|
|
198
|
|
|
|
|
|
|
} |
199
|
|
|
|
|
|
|
|
200
|
5
|
|
|
|
|
12
|
my $children; |
201
|
5
|
50
|
|
|
|
16
|
if ($args->{parent}) { |
202
|
0
|
|
|
|
|
0
|
my @kids = $args->{parent}; |
203
|
0
|
|
|
|
|
0
|
$self->all_childs_of($args->{parent},\@kids); |
204
|
0
|
|
|
|
|
0
|
$children = join( '|', map { s/-/./g; $_ } @kids ); |
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
205
|
|
|
|
|
|
|
} |
206
|
|
|
|
|
|
|
|
207
|
5
|
|
|
|
|
12
|
my $tags; |
208
|
5
|
50
|
|
|
|
14
|
if ( $args->{tags} ) { |
209
|
0
|
|
|
|
|
0
|
$tags = join( '|', @{ $args->{tags} } ); |
|
0
|
|
|
|
|
0
|
|
210
|
|
|
|
|
|
|
} |
211
|
|
|
|
|
|
|
|
212
|
5
|
|
|
|
|
10
|
my @found; |
213
|
5
|
|
|
|
|
43
|
my $iterator = Path::Class::Iterator->new( root => $root, ); |
214
|
5
|
|
66
|
|
|
22288
|
until ( !$iterator || $iterator->done ) { |
215
|
79
|
|
|
|
|
3011
|
my $file = $iterator->next; |
216
|
|
|
|
|
|
|
|
217
|
79
|
100
|
|
|
|
50646
|
next unless -f $file; |
218
|
75
|
|
|
|
|
3819
|
my $name = $file->basename; |
219
|
75
|
50
|
|
|
|
579
|
next unless $name =~ /\.trc$/; |
220
|
|
|
|
|
|
|
|
221
|
75
|
100
|
|
|
|
175
|
if ($cmp_from) { |
222
|
52
|
|
|
|
|
148
|
$file =~ /(\d{8})-(\d{6})/; |
223
|
52
|
|
|
|
|
1842
|
my $time = $1 . $2; |
224
|
52
|
100
|
|
|
|
242
|
next if $time < $cmp_from; |
225
|
41
|
100
|
|
|
|
129
|
next if $time > $cmp_to; |
226
|
|
|
|
|
|
|
} |
227
|
|
|
|
|
|
|
|
228
|
58
|
100
|
|
|
|
120
|
if ($projects) { |
229
|
49
|
100
|
|
|
|
364
|
next unless ( $name =~ m/$projects/i ); |
230
|
|
|
|
|
|
|
} |
231
|
|
|
|
|
|
|
|
232
|
34
|
50
|
|
|
|
70
|
if ($children) { |
233
|
0
|
0
|
|
|
|
0
|
next unless ( $name =~ m/$children/i ); |
234
|
|
|
|
|
|
|
} |
235
|
|
|
|
|
|
|
|
236
|
34
|
50
|
|
|
|
88
|
if ($tags) { |
237
|
0
|
|
|
|
|
0
|
my $raw_content = $file->slurp; |
238
|
0
|
0
|
|
|
|
0
|
next unless $raw_content =~ /$tags/i; |
239
|
|
|
|
|
|
|
} |
240
|
|
|
|
|
|
|
|
241
|
34
|
|
|
|
|
241
|
push( @found, $file ); |
242
|
|
|
|
|
|
|
} |
243
|
5
|
|
|
|
|
219
|
return sort @found; |
244
|
|
|
|
|
|
|
} |
245
|
|
|
|
|
|
|
|
246
|
|
|
|
|
|
|
sub project_tree { |
247
|
2
|
|
|
2
|
0
|
6
|
my $self = shift; |
248
|
2
|
|
|
|
|
63
|
my $file = $self->home->file('projects.json'); |
249
|
2
|
50
|
33
|
|
|
169
|
return unless -e $file && -s $file; |
250
|
2
|
|
|
|
|
234
|
my $decoder = JSON::XS->new->utf8->pretty->relaxed; |
251
|
2
|
|
|
|
|
13
|
my $projects = $decoder->decode( scalar $file->slurp ); |
252
|
|
|
|
|
|
|
|
253
|
2
|
|
|
|
|
583
|
my %tree; |
254
|
|
|
|
|
|
|
my $depth; |
255
|
2
|
|
|
|
|
15
|
while ( my ( $project, $location ) = each %$projects ) { |
256
|
6
|
|
50
|
|
|
48
|
$tree{$project} //= { parent => undef, children => {} }; |
257
|
|
|
|
|
|
|
# check config file for parent |
258
|
6
|
100
|
|
|
|
102
|
if ( -e $location ) { |
259
|
4
|
|
|
|
|
26
|
my $this_config = $decoder->decode( |
260
|
|
|
|
|
|
|
scalar Path::Class::file($location)->slurp ); |
261
|
4
|
50
|
|
|
|
1214
|
if ( my $parent = $this_config->{parent} ) { |
262
|
0
|
|
|
|
|
0
|
$tree{$project}->{parent} = $parent; |
263
|
0
|
|
|
|
|
0
|
$tree{$parent}->{children}{$project} = 1; |
264
|
0
|
|
|
|
|
0
|
next; |
265
|
|
|
|
|
|
|
} |
266
|
|
|
|
|
|
|
} |
267
|
|
|
|
|
|
|
# check path for parent |
268
|
6
|
|
|
|
|
25
|
my @parts = Path::Class::file($location)->parent->parent->dir_list; |
269
|
6
|
|
|
|
|
1040
|
foreach my $dir (@parts) { |
270
|
14
|
50
|
33
|
|
|
81
|
if ( $project ne $dir and my $parent = $projects->{$dir} ) { |
271
|
0
|
|
|
|
|
0
|
$tree{$project}->{parent} = $dir; |
272
|
0
|
|
|
|
|
0
|
$tree{$dir}->{children}{$project} = 1; |
273
|
|
|
|
|
|
|
} |
274
|
|
|
|
|
|
|
} |
275
|
|
|
|
|
|
|
} |
276
|
|
|
|
|
|
|
|
277
|
2
|
|
|
|
|
20
|
return \%tree; |
278
|
|
|
|
|
|
|
} |
279
|
|
|
|
|
|
|
|
280
|
|
|
|
|
|
|
sub all_childs_of { |
281
|
0
|
|
|
0
|
0
|
0
|
my ($self, $parent, $collector) = @_; |
282
|
|
|
|
|
|
|
|
283
|
0
|
|
|
|
|
0
|
my $tree = $self->project_tree; |
284
|
0
|
|
|
|
|
0
|
my $this = $tree->{$parent}; |
285
|
|
|
|
|
|
|
|
286
|
0
|
|
|
|
|
0
|
my @kids = keys %{$this->{children}}; |
|
0
|
|
|
|
|
0
|
|
287
|
|
|
|
|
|
|
|
288
|
0
|
0
|
|
|
|
0
|
if (@kids) { |
289
|
0
|
|
|
|
|
0
|
push(@$collector, @kids); |
290
|
0
|
|
|
|
|
0
|
foreach my $kid (@kids) { |
291
|
0
|
|
|
|
|
0
|
$self->all_childs_of($kid, $collector); |
292
|
|
|
|
|
|
|
} |
293
|
|
|
|
|
|
|
} |
294
|
|
|
|
|
|
|
} |
295
|
|
|
|
|
|
|
|
296
|
|
|
|
|
|
|
1; |
297
|
|
|
|
|
|
|
|
298
|
|
|
|
|
|
|
__END__ |
299
|
|
|
|
|
|
|
|
300
|
|
|
|
|
|
|
=pod |
301
|
|
|
|
|
|
|
|
302
|
|
|
|
|
|
|
=encoding UTF-8 |
303
|
|
|
|
|
|
|
|
304
|
|
|
|
|
|
|
=head1 NAME |
305
|
|
|
|
|
|
|
|
306
|
|
|
|
|
|
|
App::TimeTracker - time tracking for impatient and lazy command line lovers |
307
|
|
|
|
|
|
|
|
308
|
|
|
|
|
|
|
=head1 VERSION |
309
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
version 3.008 |
311
|
|
|
|
|
|
|
|
312
|
|
|
|
|
|
|
=head1 SYNOPSIS |
313
|
|
|
|
|
|
|
|
314
|
|
|
|
|
|
|
Backend for the C<tracker> command. See L<tracker> and/or C<perldoc tracker> for details. |
315
|
|
|
|
|
|
|
|
316
|
|
|
|
|
|
|
=head1 INSTALLATION |
317
|
|
|
|
|
|
|
|
318
|
|
|
|
|
|
|
L<App::TimeTracker> is a L<Perl|http://perl.org> application, and thus requires |
319
|
|
|
|
|
|
|
a recent Perl (>= 5.10). It also reuses a lot of code from |
320
|
|
|
|
|
|
|
L<CPAN|http://cpan.org>. |
321
|
|
|
|
|
|
|
|
322
|
|
|
|
|
|
|
=head2 From CPAN |
323
|
|
|
|
|
|
|
|
324
|
|
|
|
|
|
|
The easiest way to install the current stable version of L<App::TimeTracker> is |
325
|
|
|
|
|
|
|
via L<CPAN|http://cpan.org>. There are several different CPAN clients |
326
|
|
|
|
|
|
|
available: |
327
|
|
|
|
|
|
|
|
328
|
|
|
|
|
|
|
=head3 cpanminus |
329
|
|
|
|
|
|
|
|
330
|
|
|
|
|
|
|
~$ cpanm App::TimeTracker |
331
|
|
|
|
|
|
|
--> Working on App::TimeTracker |
332
|
|
|
|
|
|
|
Fetching http://search.cpan.org/CPAN/authors/id/D/DO/DOMM/App-TimeTracker-2.009.tar.gz ... OK |
333
|
|
|
|
|
|
|
Configuring App-TimeTracker-2.009 ... OK |
334
|
|
|
|
|
|
|
Building and testing App-TimeTracker-2.009 ... OK |
335
|
|
|
|
|
|
|
Successfully installed App-TimeTracker-2.009 |
336
|
|
|
|
|
|
|
1 distribution installed |
337
|
|
|
|
|
|
|
|
338
|
|
|
|
|
|
|
If you don't have C<cpanminus> installed yet, L<install it right |
339
|
|
|
|
|
|
|
now|http://search.cpan.org/dist/App-cpanminus/lib/App/cpanminus.pm#INSTALLATION>: |
340
|
|
|
|
|
|
|
|
341
|
|
|
|
|
|
|
~$ curl -L http://cpanmin.us | perl - --sudo App::cpanminus |
342
|
|
|
|
|
|
|
|
343
|
|
|
|
|
|
|
=head3 CPAN.pm |
344
|
|
|
|
|
|
|
|
345
|
|
|
|
|
|
|
CPAN.pm is available on ancient Perls, and feels a bit ancient, too. |
346
|
|
|
|
|
|
|
|
347
|
|
|
|
|
|
|
cpan App::TimeTracker |
348
|
|
|
|
|
|
|
|
349
|
|
|
|
|
|
|
=head2 From a tarball |
350
|
|
|
|
|
|
|
|
351
|
|
|
|
|
|
|
To install L<App::TimeTracker> from a tarball, do the usual CPAN module |
352
|
|
|
|
|
|
|
install dance: |
353
|
|
|
|
|
|
|
|
354
|
|
|
|
|
|
|
~/perl/App-TimeTracker$ perl Build.PL |
355
|
|
|
|
|
|
|
~/perl/App-TimeTracker$ ./Build |
356
|
|
|
|
|
|
|
~/perl/App-TimeTracker$ ./Build test |
357
|
|
|
|
|
|
|
~/perl/App-TimeTracker$ ./Build install # might require sudo |
358
|
|
|
|
|
|
|
|
359
|
|
|
|
|
|
|
=head2 From a git checkout |
360
|
|
|
|
|
|
|
|
361
|
|
|
|
|
|
|
Clone the repository if you have not already done so, and enter the |
362
|
|
|
|
|
|
|
C<App-TimeTracker> directory: |
363
|
|
|
|
|
|
|
|
364
|
|
|
|
|
|
|
~$ git clone git@github.com:domm/App-TimeTracker.git |
365
|
|
|
|
|
|
|
~$ cd App-TimeTracker |
366
|
|
|
|
|
|
|
|
367
|
|
|
|
|
|
|
C<App-TimeTracker> uses L<Dist::Zilla> to build, test and install the code, |
368
|
|
|
|
|
|
|
hence this must be installed first, e.g. with C<cpanm>: |
369
|
|
|
|
|
|
|
|
370
|
|
|
|
|
|
|
~/path/to/App-Tracker$ cpanm Dist::Zilla |
371
|
|
|
|
|
|
|
|
372
|
|
|
|
|
|
|
Now install the distribution's dependencies, test and install in the usual |
373
|
|
|
|
|
|
|
manner for C<Dist::Zilla> projects: |
374
|
|
|
|
|
|
|
|
375
|
|
|
|
|
|
|
~/path/to/App-Tracker$ dzil listdeps --missing | cpanm |
376
|
|
|
|
|
|
|
~/path/to/App-Tracker$ dzil test |
377
|
|
|
|
|
|
|
~/path/to/App-Tracker$ dzil install |
378
|
|
|
|
|
|
|
|
379
|
|
|
|
|
|
|
=head1 PLUGINS |
380
|
|
|
|
|
|
|
|
381
|
|
|
|
|
|
|
Custom commands or adaptations to your workflow can be implemented via |
382
|
|
|
|
|
|
|
an "interesting" set of L<Moose>-powered plugins. You can configure |
383
|
|
|
|
|
|
|
different sets of plugins for different jobs or projects. |
384
|
|
|
|
|
|
|
|
385
|
|
|
|
|
|
|
B<Tip:> Use C<tracker plugins> to list all installed plugins. Read more |
386
|
|
|
|
|
|
|
about each plugin in C<App::TimeTracker::Command::PLUGIN-NAME>. |
387
|
|
|
|
|
|
|
|
388
|
|
|
|
|
|
|
=head2 Note about (missing) Plugins |
389
|
|
|
|
|
|
|
|
390
|
|
|
|
|
|
|
Up to version 2.028 a lot of plugins where included in the main distribution |
391
|
|
|
|
|
|
|
C<App-TimeTracker>. To make installation easier and faster, all non-core |
392
|
|
|
|
|
|
|
command plugins have been moved into distinct, standalone distributions. |
393
|
|
|
|
|
|
|
|
394
|
|
|
|
|
|
|
The following plugins are affected: |
395
|
|
|
|
|
|
|
|
396
|
|
|
|
|
|
|
=over |
397
|
|
|
|
|
|
|
|
398
|
|
|
|
|
|
|
=item * App::TimeTracker::Git (which also includes SyncViaGit) |
399
|
|
|
|
|
|
|
|
400
|
|
|
|
|
|
|
=item * App::TimeTracker::RT |
401
|
|
|
|
|
|
|
|
402
|
|
|
|
|
|
|
=item * App::TimeTracker::TellRemote (which was called Post2IRC earlier) |
403
|
|
|
|
|
|
|
|
404
|
|
|
|
|
|
|
=item * App::TimeTracker::Overtime has been removed, while the idea is nice, the API and implementation are not good enough. |
405
|
|
|
|
|
|
|
|
406
|
|
|
|
|
|
|
=item * App::TimeTracker::TextNotify has been removed. |
407
|
|
|
|
|
|
|
|
408
|
|
|
|
|
|
|
=back |
409
|
|
|
|
|
|
|
|
410
|
|
|
|
|
|
|
=head1 SOURCE CODE |
411
|
|
|
|
|
|
|
|
412
|
|
|
|
|
|
|
=head2 git |
413
|
|
|
|
|
|
|
|
414
|
|
|
|
|
|
|
We use C<< git >> for version control and maintain a public repository on |
415
|
|
|
|
|
|
|
L<github|http://github.com>. |
416
|
|
|
|
|
|
|
|
417
|
|
|
|
|
|
|
You can find the latest version of L<App::TimeTracker> here: |
418
|
|
|
|
|
|
|
|
419
|
|
|
|
|
|
|
L<https://github.com/domm/App-TimeTracker> |
420
|
|
|
|
|
|
|
|
421
|
|
|
|
|
|
|
If you want to work on L<App::TimeTracker>, add a feature, add a plugin or fix |
422
|
|
|
|
|
|
|
a bug, please feel free to L<fork|http://help.github.com/fork-a-repo/> the |
423
|
|
|
|
|
|
|
repo and send us L<pull requests|http://help.github.com/send-pull-requests/> |
424
|
|
|
|
|
|
|
to merge your changes. |
425
|
|
|
|
|
|
|
|
426
|
|
|
|
|
|
|
To report a bug, please B<do not> use the C<< issues >> feature from github; |
427
|
|
|
|
|
|
|
use RT instead. |
428
|
|
|
|
|
|
|
|
429
|
|
|
|
|
|
|
=head2 CPAN |
430
|
|
|
|
|
|
|
|
431
|
|
|
|
|
|
|
L<App::TimeTracker> is distributed via L<CPAN|http://cpan.org/>, the |
432
|
|
|
|
|
|
|
Comprehensive Perl Archive Network. Here are a few different views of |
433
|
|
|
|
|
|
|
CPAN, offering slightly different features: |
434
|
|
|
|
|
|
|
|
435
|
|
|
|
|
|
|
=over |
436
|
|
|
|
|
|
|
|
437
|
|
|
|
|
|
|
=item * L<https://metacpan.org/release/App-TimeTracker/> |
438
|
|
|
|
|
|
|
|
439
|
|
|
|
|
|
|
=item * L<http://search.cpan.org/dist/App-TimeTracker/> |
440
|
|
|
|
|
|
|
|
441
|
|
|
|
|
|
|
=back |
442
|
|
|
|
|
|
|
|
443
|
|
|
|
|
|
|
=head1 Viewing and reporting Bugs |
444
|
|
|
|
|
|
|
|
445
|
|
|
|
|
|
|
We use L<rt.cpan.org|http://rt.cpan.org> (thank you |
446
|
|
|
|
|
|
|
L<BestPractical|http://rt.bestpractical.com>) for bug reporting. Please do |
447
|
|
|
|
|
|
|
not use the C<issues> feature of github! We pay no attention to those... |
448
|
|
|
|
|
|
|
|
449
|
|
|
|
|
|
|
Please use this URL to view and report bugs: |
450
|
|
|
|
|
|
|
|
451
|
|
|
|
|
|
|
L<https://rt.cpan.org/Public/Dist/Display.html?Name=App-TimeTracker> |
452
|
|
|
|
|
|
|
|
453
|
|
|
|
|
|
|
=head1 CONTRIBUTORS |
454
|
|
|
|
|
|
|
|
455
|
|
|
|
|
|
|
Maros Kollar, Klaus Ita, Yanick Champoux, Lukas Rampa, David Schmidt, Michael Kröll, Thomas Sibley, Nelo Onyiah, Jozef Kutej, Roland Lammel, Ruslan Zakirov, Kartik Thakore, Tokuhiro Matsuno, Paul Cochrane, David Provost, Mohammad S Anwar, Håkon Hægland |
456
|
|
|
|
|
|
|
|
457
|
|
|
|
|
|
|
=head1 AUTHOR |
458
|
|
|
|
|
|
|
|
459
|
|
|
|
|
|
|
Thomas Klausner <domm@plix.at> |
460
|
|
|
|
|
|
|
|
461
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
462
|
|
|
|
|
|
|
|
463
|
|
|
|
|
|
|
This software is copyright (c) 2011 - 2021 by Thomas Klausner. |
464
|
|
|
|
|
|
|
|
465
|
|
|
|
|
|
|
This is free software; you can redistribute it and/or modify it under |
466
|
|
|
|
|
|
|
the same terms as the Perl 5 programming language system itself. |
467
|
|
|
|
|
|
|
|
468
|
|
|
|
|
|
|
=cut |