herodot 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.gitignore +0 -1
- data/.rubocop.yml +4 -0
- data/.travis.yml +19 -3
- data/Gemfile.lock +66 -0
- data/README.md +3 -0
- data/bin/ci +17 -0
- data/herodot.gemspec +8 -6
- data/lib/herodot/commands.rb +59 -57
- data/lib/herodot/configuration.rb +36 -33
- data/lib/herodot/output.rb +49 -43
- data/lib/herodot/parser.rb +25 -23
- data/lib/herodot/project_link.rb +40 -38
- data/lib/herodot/version.rb +1 -1
- data/lib/herodot/worklog.rb +70 -67
- data/lib/herodot.rb +70 -68
- metadata +11 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 67bf83f2d851943bf0d40e5e292e09a522bceb8d38f73bc331db415694de94b1
|
4
|
+
data.tar.gz: f81ee485d789a866badc55c385a77daffeba0217fcc03fa0a5656c09c5c2827e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9412778f3a13fc66db849c373e11504dcc7033ba0cdd3728b7a52ab587183773563f59cd7d8fdb1b048a88d5f32d3c88d1964c213e8912f49bbbd1fb822c1bc1
|
7
|
+
data.tar.gz: 67baebca6ca6db82a8a1deae38a69f9321088e8ca9ff565af4dd0cd08b143c8389970abe9faa518af52b17fddc19da55814b1ef6006ca8d8d8d2b75bdc933af6
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
@@ -1,6 +1,22 @@
|
|
1
1
|
sudo: false
|
2
2
|
language: ruby
|
3
|
+
|
3
4
|
rvm:
|
4
|
-
- 2.
|
5
|
-
- 2.3.
|
6
|
-
|
5
|
+
- 2.1.0
|
6
|
+
- 2.3.5
|
7
|
+
- 2.4.2
|
8
|
+
|
9
|
+
before_install:
|
10
|
+
- gem install bundler -v 1.14.6
|
11
|
+
|
12
|
+
install:
|
13
|
+
- gem update --system
|
14
|
+
- bundle install --jobs=3 --retry=3
|
15
|
+
|
16
|
+
cache:
|
17
|
+
bundler: true
|
18
|
+
directories:
|
19
|
+
- vendor/bundle
|
20
|
+
|
21
|
+
script:
|
22
|
+
- bin/ci
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
herodot (0.2.1)
|
5
|
+
chronic
|
6
|
+
commander
|
7
|
+
rainbow
|
8
|
+
terminal-table
|
9
|
+
|
10
|
+
GEM
|
11
|
+
remote: https://rubygems.org/
|
12
|
+
specs:
|
13
|
+
ast (2.4.0)
|
14
|
+
chronic (0.10.2)
|
15
|
+
commander (4.4.3)
|
16
|
+
highline (~> 1.7.2)
|
17
|
+
diff-lcs (1.3)
|
18
|
+
highline (1.7.8)
|
19
|
+
parallel (1.12.1)
|
20
|
+
parser (2.5.0.0)
|
21
|
+
ast (~> 2.4.0)
|
22
|
+
powerpack (0.1.1)
|
23
|
+
rainbow (3.0.0)
|
24
|
+
rake (10.5.0)
|
25
|
+
rspec (3.5.0)
|
26
|
+
rspec-core (~> 3.5.0)
|
27
|
+
rspec-expectations (~> 3.5.0)
|
28
|
+
rspec-mocks (~> 3.5.0)
|
29
|
+
rspec-core (3.5.4)
|
30
|
+
rspec-support (~> 3.5.0)
|
31
|
+
rspec-expectations (3.5.0)
|
32
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
33
|
+
rspec-support (~> 3.5.0)
|
34
|
+
rspec-mocks (3.5.0)
|
35
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
36
|
+
rspec-support (~> 3.5.0)
|
37
|
+
rspec-support (3.5.0)
|
38
|
+
rubocop (0.52.1)
|
39
|
+
parallel (~> 1.10)
|
40
|
+
parser (>= 2.4.0.2, < 3.0)
|
41
|
+
powerpack (~> 0.1)
|
42
|
+
rainbow (>= 2.2.2, < 4.0)
|
43
|
+
ruby-progressbar (~> 1.7)
|
44
|
+
unicode-display_width (~> 1.0, >= 1.0.1)
|
45
|
+
rubocop-bitcrowd (1.2.0)
|
46
|
+
rubocop-rspec (1.22.2)
|
47
|
+
rubocop (>= 0.52.1)
|
48
|
+
ruby-progressbar (1.9.0)
|
49
|
+
terminal-table (1.8.0)
|
50
|
+
unicode-display_width (~> 1.1, >= 1.1.1)
|
51
|
+
unicode-display_width (1.3.0)
|
52
|
+
|
53
|
+
PLATFORMS
|
54
|
+
ruby
|
55
|
+
|
56
|
+
DEPENDENCIES
|
57
|
+
bundler (~> 1.14)
|
58
|
+
herodot!
|
59
|
+
rake (~> 10.0)
|
60
|
+
rspec (~> 3.0)
|
61
|
+
rubocop
|
62
|
+
rubocop-bitcrowd
|
63
|
+
rubocop-rspec
|
64
|
+
|
65
|
+
BUNDLED WITH
|
66
|
+
1.16.1
|
data/README.md
CHANGED
@@ -12,6 +12,8 @@ Install with:
|
|
12
12
|
|
13
13
|
$ gem install herodot
|
14
14
|
|
15
|
+
Make sure you have installed at least ruby 2.1 or any newer ruby version.
|
16
|
+
|
15
17
|
## Usage
|
16
18
|
|
17
19
|
Track a git repository:
|
@@ -50,6 +52,7 @@ Show Help:
|
|
50
52
|
$ herodot help show
|
51
53
|
|
52
54
|
## Linking to issue trackers
|
55
|
+
|
53
56
|
If you use https://github.com/bitcrowd/tickety-tick or otherwise have branch names, that contain
|
54
57
|
the issue number, you can link a tracked herodot repository with your issue tracker, so it
|
55
58
|
will print urls of issues it recognizes under the branch name.
|
data/bin/ci
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'English'
|
4
|
+
|
5
|
+
module TravisRunner
|
6
|
+
def self.execute(title, command)
|
7
|
+
puts "== Running #{title} =="
|
8
|
+
system command
|
9
|
+
yield if block_given?
|
10
|
+
raise "#{title} failed" unless $CHILD_STATUS.success?
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
system 'mkdir -p tmp'
|
15
|
+
|
16
|
+
TravisRunner.execute 'Rubocop', 'bundle exec rubocop'
|
17
|
+
TravisRunner.execute 'Rspec', 'bundle exec rspec --exclude-pattern "spec/features/**/*"'
|
data/herodot.gemspec
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
-
|
1
|
+
|
2
2
|
lib = File.expand_path('../lib', __FILE__)
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
4
|
require 'herodot/version'
|
5
5
|
|
6
|
+
# rubocop:disable Metrics/BlockLength
|
6
7
|
Gem::Specification.new do |spec|
|
7
8
|
spec.name = 'herodot'
|
8
9
|
spec.version = Herodot::VERSION
|
@@ -19,17 +20,18 @@ Gem::Specification.new do |spec|
|
|
19
20
|
f.match(%r{^(test|spec|features)/})
|
20
21
|
end
|
21
22
|
spec.bindir = 'exe'
|
22
|
-
spec.executables = %w
|
23
|
-
spec.require_paths = %w
|
23
|
+
spec.executables = %w[herodot]
|
24
|
+
spec.require_paths = %w[lib]
|
24
25
|
|
25
26
|
spec.add_development_dependency 'bundler', '~> 1.14'
|
26
27
|
spec.add_development_dependency 'rake', '~> 10.0'
|
27
28
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
28
29
|
spec.add_development_dependency 'rubocop'
|
29
|
-
spec.add_development_dependency 'rubocop-rspec'
|
30
30
|
spec.add_development_dependency 'rubocop-bitcrowd'
|
31
|
-
spec.
|
32
|
-
spec.add_dependency 'terminal-table'
|
31
|
+
spec.add_development_dependency 'rubocop-rspec'
|
33
32
|
spec.add_dependency 'chronic'
|
34
33
|
spec.add_dependency 'commander'
|
34
|
+
spec.add_dependency 'rainbow'
|
35
|
+
spec.add_dependency 'terminal-table'
|
35
36
|
end
|
37
|
+
# rubocop:enable Metrics/BlockLength
|
data/lib/herodot/commands.rb
CHANGED
@@ -2,74 +2,76 @@ require 'date'
|
|
2
2
|
require 'chronic'
|
3
3
|
require 'fileutils'
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
module Herodot
|
6
|
+
class Commands
|
7
|
+
SCRIPT = "#!/bin/sh\nherodot track $(pwd)".freeze
|
7
8
|
|
8
|
-
|
9
|
+
DEFAULT_RANGE = 'this week'.freeze
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
11
|
+
def self.show(args, config, opts = {})
|
12
|
+
subject = args.empty? ? DEFAULT_RANGE : args.join(' ')
|
13
|
+
range = Chronic.parse(subject, guess: false, context: :past)
|
14
|
+
abort "Date not parsable: #{args.join(' ')}" unless range
|
15
|
+
worklog = Parser.parse(range, config)
|
16
|
+
decorated_worklog = ProjectLink.new(worklog)
|
17
|
+
output = Output.print(decorated_worklog.totals, opts)
|
18
|
+
puts output
|
19
|
+
end
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
21
|
+
def self.init(path, config)
|
22
|
+
path = '.' if path.nil?
|
23
|
+
puts "Start tracking of `#{File.expand_path(path)}` into `#{config.worklog_file}`."
|
24
|
+
hooks = "#{path}/.git/hooks"
|
25
|
+
abort('Path is not a git repository.') unless File.exist?(hooks)
|
26
|
+
%w[post-checkout post-commit].each do |name|
|
27
|
+
File.open("#{hooks}/#{name}", 'w') { |file| file.write(SCRIPT) }
|
28
|
+
File.chmod(0o755, "#{hooks}/#{name}")
|
29
|
+
FileUtils.touch(config.worklog_file)
|
30
|
+
end
|
29
31
|
end
|
30
|
-
end
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
33
|
+
def self.track(path, config)
|
34
|
+
puts 'Logging into worklog'
|
35
|
+
File.open(config.worklog_file, 'a') do |worklog|
|
36
|
+
datestr = Time.now.strftime('%a %b %e %H:%M:%S %z %Y')
|
37
|
+
branch = `(cd #{path} && git rev-parse --abbrev-ref HEAD)`.strip
|
38
|
+
line = [datestr, path, branch].join(';')
|
39
|
+
worklog.puts(line)
|
40
|
+
end
|
39
41
|
end
|
40
|
-
end
|
41
42
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
43
|
+
def self.link(path)
|
44
|
+
path = '.' if path.nil?
|
45
|
+
choose do |menu|
|
46
|
+
menu.prompt = 'What tracker do you want to link to?'
|
47
|
+
menu.choice(:jira) { link_jira(path) }
|
48
|
+
menu.choice(:github) { link_github(path) }
|
49
|
+
menu.choice(:gitlab) { link_gitlab(path) }
|
50
|
+
menu.choices(:other) { link_other(path) }
|
51
|
+
menu.default = :other
|
52
|
+
end
|
51
53
|
end
|
52
|
-
end
|
53
54
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
55
|
+
def self.link_jira(path)
|
56
|
+
prefix = ask('Jira URL prefix (something for https://something.atlassian.net)?')
|
57
|
+
pattern = ask('Ticket prefix (ABCD for tickets like ABCD-123)')
|
58
|
+
ProjectLink.link(path, "http://#{prefix}.atlassian.net/browse/", "#{pattern}-\\d+")
|
59
|
+
end
|
59
60
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
61
|
+
def self.link_github(path)
|
62
|
+
handle = ask('Github handle (something/something for https://github.com/something/something)?')
|
63
|
+
ProjectLink.link(path, "https://github.com/#{handle}/issues/", '\\d+')
|
64
|
+
end
|
64
65
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
66
|
+
def self.link_gitlab(path)
|
67
|
+
handle = ask('GitLab handle (something/something for https://gitlab.com/something/something)?')
|
68
|
+
ProjectLink.link(path, "https://gitlab.com/#{handle}/issues/", '\\d+')
|
69
|
+
end
|
69
70
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
71
|
+
def self.link_other(path)
|
72
|
+
url = ask('URL to issue tracker:')
|
73
|
+
pattern = ask('Ticket regex pattern (ruby):')
|
74
|
+
ProjectLink.link(path, url, pattern)
|
75
|
+
end
|
74
76
|
end
|
75
77
|
end
|
@@ -1,44 +1,47 @@
|
|
1
1
|
require 'yaml'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
'
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
3
|
+
module Herodot
|
4
|
+
class Configuration
|
5
|
+
CONFIG_FILE = File.expand_path('~/.herodot.yml').freeze
|
6
|
+
DEFAULT_CONFIGURATION = {
|
7
|
+
'projects_directory' => '~',
|
8
|
+
'work_times' => {
|
9
|
+
'work_start' => '9:30',
|
10
|
+
'lunch_break_start' => '13:00',
|
11
|
+
'lunch_break_end' => '13:30',
|
12
|
+
'work_end' => '18:00'
|
13
|
+
}
|
14
|
+
}.freeze
|
14
15
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
16
|
+
def initialize(worklog_file = '~/.worklog')
|
17
|
+
@worklog_file = worklog_file
|
18
|
+
if File.exist?(CONFIG_FILE)
|
19
|
+
@config = load_configuration
|
20
|
+
else
|
21
|
+
@config = DEFAULT_CONFIGURATION
|
22
|
+
save_configuration
|
23
|
+
end
|
22
24
|
end
|
23
|
-
end
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
|
26
|
+
def worklog_file
|
27
|
+
File.expand_path(@worklog_file)
|
28
|
+
end
|
28
29
|
|
29
|
-
|
30
|
-
|
31
|
-
|
30
|
+
def projects_directory
|
31
|
+
File.expand_path(@config['projects_directory'] || DEFAULT_CONFIGURATION['projects_directory'])
|
32
|
+
end
|
32
33
|
|
33
|
-
|
34
|
-
|
35
|
-
|
34
|
+
def work_times
|
35
|
+
(@config['work_times'] || DEFAULT_CONFIGURATION['work_times'])
|
36
|
+
.map { |k, v| [k.to_sym, v.split(':').map(&:to_i)] }
|
37
|
+
end
|
36
38
|
|
37
|
-
|
38
|
-
|
39
|
-
|
39
|
+
def save_configuration
|
40
|
+
File.open(CONFIG_FILE, 'w') { |f| YAML.dump(@config, f) }
|
41
|
+
end
|
40
42
|
|
41
|
-
|
42
|
-
|
43
|
+
def load_configuration
|
44
|
+
File.open(CONFIG_FILE) { |f| YAML.load(f) }
|
45
|
+
end
|
43
46
|
end
|
44
47
|
end
|
data/lib/herodot/output.rb
CHANGED
@@ -1,58 +1,64 @@
|
|
1
1
|
require 'terminal-table'
|
2
2
|
require 'json'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
class << self
|
12
|
-
def format_time(time_is_seconds)
|
13
|
-
total_seconds = time_is_seconds.to_i
|
14
|
-
seconds = total_seconds % 60
|
15
|
-
minutes = (total_seconds / 60) % 60
|
16
|
-
hours = total_seconds / (60 * 60)
|
17
|
-
"#{hours}:#{minutes.to_s.rjust(2, '0')}:#{seconds.to_s.rjust(2, '0')}"
|
18
|
-
end
|
4
|
+
module Herodot
|
5
|
+
class Output
|
6
|
+
HEADERS = %w[Project Branch Time].freeze
|
7
|
+
EMPTY_WORKLOG_MESSAGE = Rainbow('Not enough entries in the worklog.').red +
|
8
|
+
' On a tracked repository `git checkout`'\
|
9
|
+
' and `git commit` will add entries.'.freeze
|
10
|
+
COLORS = %i[green yellow blue magenta cyan aqua silver aliceblue indianred].freeze
|
19
11
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
12
|
+
class << self
|
13
|
+
def format_time(time_is_seconds)
|
14
|
+
total_seconds = time_is_seconds.to_i
|
15
|
+
seconds = total_seconds % 60
|
16
|
+
minutes = (total_seconds / 60) % 60
|
17
|
+
hours = total_seconds / (60 * 60)
|
18
|
+
"#{hours}:#{minutes.to_s.rjust(2, '0')}:#{seconds.to_s.rjust(2, '0')}"
|
19
|
+
end
|
24
20
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
worklogs_totals_per_day.to_json
|
21
|
+
def print(worklogs_totals_per_day, opts)
|
22
|
+
return convert_format(worklogs_totals_per_day, opts.format) if opts.format
|
23
|
+
print_table(worklogs_totals_per_day)
|
29
24
|
end
|
30
|
-
end
|
31
25
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
table.add_separator
|
37
|
-
table << [date]
|
38
|
-
table.add_separator
|
39
|
-
print_day(times).each { |row| table << row }
|
40
|
-
table.add_separator
|
26
|
+
def convert_format(worklogs_totals_per_day, format)
|
27
|
+
case format
|
28
|
+
when 'json'
|
29
|
+
worklogs_totals_per_day.to_json
|
41
30
|
end
|
42
31
|
end
|
43
|
-
end
|
44
32
|
|
45
|
-
|
33
|
+
def print_table(worklogs_totals_per_day)
|
34
|
+
abort EMPTY_WORKLOG_MESSAGE if worklogs_totals_per_day.empty?
|
35
|
+
Terminal::Table.new(headings: HEADERS) do |table|
|
36
|
+
worklogs_totals_per_day.each do |date, times|
|
37
|
+
table.add_separator
|
38
|
+
table << [date]
|
39
|
+
table.add_separator
|
40
|
+
print_day(times).each { |row| table << row }
|
41
|
+
table.add_separator
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
46
45
|
|
47
|
-
|
48
|
-
|
49
|
-
|
46
|
+
private
|
47
|
+
|
48
|
+
def colorize(project)
|
49
|
+
Rainbow(project).color(COLORS[project.chars.map(&:ord).reduce(:+) % COLORS.size])
|
50
|
+
end
|
50
51
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
52
|
+
def times_by_project_and_branch(times)
|
53
|
+
times.sort_by { |log| [log[:project], log[:branch]] }
|
54
|
+
end
|
55
|
+
|
56
|
+
def print_day(times)
|
57
|
+
times_by_project_and_branch(times).flat_map do |log|
|
58
|
+
lines = [[colorize(log[:project]), log[:branch], format_time(log[:time])]]
|
59
|
+
lines << ['', Rainbow(log[:link]).color(80, 80, 80), ''] if log[:link]
|
60
|
+
lines
|
61
|
+
end
|
56
62
|
end
|
57
63
|
end
|
58
64
|
end
|
data/lib/herodot/parser.rb
CHANGED
@@ -1,31 +1,33 @@
|
|
1
1
|
require 'csv'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
3
|
+
module Herodot
|
4
|
+
class Parser
|
5
|
+
NO_SUCH_FILE = Rainbow('Worklog missing.').red +
|
6
|
+
' Use `herodot init` to start tracking a git repository'\
|
7
|
+
' or `herodot help` to open the man page.'.freeze
|
8
|
+
class << self
|
9
|
+
def parse(range, config)
|
10
|
+
worklog = Worklog.new(config)
|
11
|
+
from, to = from_to_from_range(range)
|
12
|
+
parse_into_worklog(worklog, config.worklog_file, from, to)
|
13
|
+
worklog
|
14
|
+
rescue Errno::ENOENT
|
15
|
+
abort NO_SUCH_FILE
|
16
|
+
end
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
def from_to_from_range(range)
|
19
|
+
return [range, Time.now] unless range.respond_to?(:begin) && range.respond_to?(:end)
|
20
|
+
[range.begin, range.end + 3600]
|
21
|
+
end
|
21
22
|
|
22
|
-
|
23
|
+
private
|
23
24
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
25
|
+
def parse_into_worklog(worklog, file, from, to)
|
26
|
+
CSV.foreach(file, col_sep: ';') do |row|
|
27
|
+
next if row[2] == 'HEAD'
|
28
|
+
time = Time.parse(row[0])
|
29
|
+
worklog.add_entry(time, row[1], row[2]) if time >= from && time <= to
|
30
|
+
end
|
29
31
|
end
|
30
32
|
end
|
31
33
|
end
|
data/lib/herodot/project_link.rb
CHANGED
@@ -1,52 +1,54 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module Herodot
|
2
|
+
class ProjectLink
|
3
|
+
PROJECT_CONFIG = '.herodot.yml'.freeze
|
3
4
|
|
4
|
-
|
5
|
-
|
6
|
-
|
5
|
+
def self.project_config_file(path)
|
6
|
+
File.join(File.expand_path(path), PROJECT_CONFIG)
|
7
|
+
end
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
def self.link(path, link, pattern)
|
10
|
+
puts "Write link into #{project_config_file(path)}"
|
11
|
+
File.open(project_config_file(path), 'w') do |f|
|
12
|
+
YAML.dump({ link: link, pattern: pattern }, f)
|
13
|
+
end
|
12
14
|
end
|
13
|
-
end
|
14
15
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
def initialize(worklog)
|
17
|
+
@worklog = worklog
|
18
|
+
@project_configurations = {}
|
19
|
+
end
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
|
21
|
+
def totals
|
22
|
+
@worklog.totals.map do |date, logs|
|
23
|
+
[date, decorated_logs(logs)]
|
24
|
+
end
|
23
25
|
end
|
24
|
-
end
|
25
26
|
|
26
|
-
|
27
|
+
private
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
def decorated_logs(logs)
|
30
|
+
logs.map do |log|
|
31
|
+
decorated_log(log)
|
32
|
+
end
|
31
33
|
end
|
32
|
-
end
|
33
34
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
35
|
+
def decorated_log(log)
|
36
|
+
link = issue_management_link(log)
|
37
|
+
return log if link.nil?
|
38
|
+
log.merge(link: link)
|
39
|
+
end
|
39
40
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
41
|
+
def issue_management_link(log)
|
42
|
+
config = @project_configurations.fetch(log[:path], load_project_configuration(log[:path]))
|
43
|
+
return nil unless config.fetch(:link, false)
|
44
|
+
ticket = log[:branch].scan(Regexp.new(config.fetch(:pattern, /$^/)))
|
45
|
+
[config[:link], ticket.first].join if ticket.any?
|
46
|
+
end
|
46
47
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
48
|
+
def load_project_configuration(path)
|
49
|
+
file = self.class.project_config_file(path)
|
50
|
+
return { link: false } unless File.exist?(file)
|
51
|
+
File.open(file) { |f| YAML.load(f) }
|
52
|
+
end
|
51
53
|
end
|
52
54
|
end
|
data/lib/herodot/version.rb
CHANGED
data/lib/herodot/worklog.rb
CHANGED
@@ -1,85 +1,88 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
module Herodot
|
2
|
+
class Worklog
|
3
|
+
attr_reader :branches
|
4
|
+
END_TRACK_EVENTS = %i[work_end lunch_break_start after_last_dates_end].freeze
|
5
|
+
START_TRACK_EVNETS = %i[work_start lunch_break_end before_first_dates_start].freeze
|
6
|
+
EVENTS = (END_TRACK_EVENTS + START_TRACK_EVNETS).freeze
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
def initialize(config)
|
9
|
+
@raw_logs = []
|
10
|
+
@branches = {}
|
11
|
+
@dates = []
|
12
|
+
@config = config
|
13
|
+
end
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
def add_entry(time, project_path, branch)
|
16
|
+
return if project_path.nil?
|
17
|
+
project = project_path.gsub(@config.projects_directory.to_s, '')
|
18
|
+
id = "#{project}:#{branch}"
|
19
|
+
@raw_logs << { time: time, id: id }
|
20
|
+
@branches[id] = { branch: branch, project: project, path: project_path }
|
21
|
+
end
|
20
22
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
23
|
+
def logs_with_events
|
24
|
+
filtered_logs = @raw_logs.chunk { |x| x[:id] }.map(&:last).map(&:first)
|
25
|
+
filtered_logs += work_time_events
|
26
|
+
filtered_logs << { time: Time.new(0), id: :before_first_dates_start }
|
27
|
+
filtered_logs << { time: Time.now, id: :after_last_dates_end }
|
28
|
+
filtered_logs.sort_by { |log| log[:time] }
|
29
|
+
end
|
28
30
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
31
|
+
def logs_with_times
|
32
|
+
current_id = nil
|
33
|
+
logs_with_events.each_cons(2).map do |log, following_log|
|
34
|
+
current_id = log[:id] unless EVENTS.include?(log[:id])
|
35
|
+
log.merge id: actual_id(current_id, log[:id]),
|
36
|
+
time: time_between(log, following_log),
|
37
|
+
date: log[:time].to_date
|
38
|
+
end
|
36
39
|
end
|
37
|
-
end
|
38
40
|
|
39
|
-
|
40
|
-
|
41
|
-
|
41
|
+
def logs_with_times_cleaned
|
42
|
+
logs_with_times.reject { |log| EVENTS.include?(log[:id]) }
|
43
|
+
end
|
42
44
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
45
|
+
def totals
|
46
|
+
grouped = logs_with_times_cleaned.group_by { |time| time[:date] }
|
47
|
+
dates.map do |date|
|
48
|
+
time_sums = grouped[date].each_with_object({}) do |time, sums|
|
49
|
+
id = time[:id]
|
50
|
+
sums[id] ||= { time: 0, **branch(id) }
|
51
|
+
sums[id][:time] += time[:time]
|
52
|
+
end
|
53
|
+
[date, time_sums.values]
|
50
54
|
end
|
51
|
-
[date, time_sums.values]
|
52
55
|
end
|
53
|
-
end
|
54
56
|
|
55
|
-
|
56
|
-
|
57
|
-
|
57
|
+
def branch(id)
|
58
|
+
@branches.fetch(id, {})
|
59
|
+
end
|
58
60
|
|
59
|
-
|
60
|
-
|
61
|
-
|
61
|
+
def dates
|
62
|
+
@raw_logs.map { |log| log[:time].to_date }.uniq.sort
|
63
|
+
end
|
62
64
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
65
|
+
def work_time_events
|
66
|
+
dates.flat_map do |date|
|
67
|
+
@config.work_times.map { |event, (hour, minute)|
|
68
|
+
time = Time.new(date.year, date.month, date.day, hour, minute)
|
69
|
+
next if time > Time.now
|
70
|
+
{ id: event, time: time }
|
71
|
+
}.compact
|
72
|
+
end
|
70
73
|
end
|
71
|
-
end
|
72
74
|
|
73
|
-
|
74
|
-
|
75
|
-
|
75
|
+
def same_date?(log_entry, other_log_entry)
|
76
|
+
log_entry[:time].to_date == other_log_entry[:time].to_date
|
77
|
+
end
|
76
78
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
79
|
+
def time_between(log_entry, following_entry)
|
80
|
+
return 0 unless same_date?(log_entry, following_entry)
|
81
|
+
following_entry[:time] - log_entry[:time]
|
82
|
+
end
|
81
83
|
|
82
|
-
|
83
|
-
|
84
|
+
def actual_id(current_id, id)
|
85
|
+
END_TRACK_EVENTS.include?(id) ? id : current_id || id
|
86
|
+
end
|
84
87
|
end
|
85
88
|
end
|
data/lib/herodot.rb
CHANGED
@@ -8,88 +8,90 @@ require_relative 'herodot/commands'
|
|
8
8
|
require_relative 'herodot/output'
|
9
9
|
require_relative 'herodot/project_link'
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
module Herodot
|
12
|
+
class Application
|
13
|
+
include Commander::Methods
|
14
|
+
USER_HOME = File.expand_path('~').to_s
|
14
15
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
def run
|
17
|
+
program :name, 'herodot'
|
18
|
+
program :version, VERSION
|
19
|
+
program :description, 'Tracks your work based on git branch checkouts'
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
21
|
+
config = Configuration.new
|
22
|
+
init_command(config)
|
23
|
+
track_command(config)
|
24
|
+
show_command(config)
|
25
|
+
link_command(config)
|
26
|
+
default_command :show
|
27
|
+
run!
|
28
|
+
end
|
28
29
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
30
|
+
INIT_DESCRIPTION = 'This command sets up post commit and post checkout hooks'\
|
31
|
+
', that will log the current branch into the worklog file.'.freeze
|
32
|
+
def init_command(config)
|
33
|
+
command :init do |c|
|
34
|
+
c.syntax = 'herodot init [<repository path>]'
|
35
|
+
c.summary = 'Start tracking a repository'
|
36
|
+
c.description = INIT_DESCRIPTION
|
37
|
+
c.example 'Start tracking current repository', 'herodot init'
|
38
|
+
c.action do |args, _|
|
39
|
+
Commands.init(args[0], config)
|
40
|
+
end
|
39
41
|
end
|
40
42
|
end
|
41
|
-
end
|
42
43
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
44
|
+
TRACK_DESCRIPTION = 'This command tracks the current branch/commit in a repo '\
|
45
|
+
'and is called from the git hooks installed via `herodot init`.'.freeze
|
46
|
+
def track_command(config)
|
47
|
+
command :track do |c|
|
48
|
+
c.syntax = 'herodot track <repository path>'
|
49
|
+
c.summary = 'Record git activity in a repository (used internally)'
|
50
|
+
c.description = TRACK_DESCRIPTION
|
51
|
+
c.example 'Record the latest branch name etc. to the worklog', 'herodot track .'
|
52
|
+
c.action do |args, _|
|
53
|
+
Commands.track(args[0], config)
|
54
|
+
end
|
53
55
|
end
|
54
56
|
end
|
55
|
-
end
|
56
57
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
58
|
+
SHOW_DESCRIPTION = 'This command parses the worklog file and returns the'\
|
59
|
+
'git branch based worklog according to the'\
|
60
|
+
'work times specified in the `~/.herodot.yml`.'.freeze
|
61
|
+
def show_command(config)
|
62
|
+
command :show do |c|
|
63
|
+
c.syntax = 'herodot show [<time range>]'
|
64
|
+
c.summary = 'Shows worklogs'
|
65
|
+
c.description = SHOW_DESCRIPTION
|
66
|
+
c.option '--format FORMAT', String, 'Uses specific output format (Supported: json)'
|
67
|
+
show_command_examples(c)
|
68
|
+
c.action do |args, options|
|
69
|
+
Commands.show(args, config, options)
|
70
|
+
end
|
69
71
|
end
|
70
72
|
end
|
71
|
-
end
|
72
73
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
74
|
+
LINK_DESCRIPTION = 'This command can link a repository to a project issue tracking tool.'\
|
75
|
+
' The commmands writes the settings in `project_path/.herodot.yml`.'.freeze
|
76
|
+
def link_command(_)
|
77
|
+
command :link do |c|
|
78
|
+
c.syntax = 'herodot link [<repository path>]'
|
79
|
+
c.summary = 'Link project with issue tracker'
|
80
|
+
c.description = SHOW_DESCRIPTION
|
81
|
+
c.example 'Link current repository', 'herodot link'
|
82
|
+
c.action do |args, _|
|
83
|
+
Commands.link(args[0])
|
84
|
+
end
|
83
85
|
end
|
84
86
|
end
|
85
|
-
end
|
86
87
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
88
|
+
def show_command_examples(c)
|
89
|
+
c.example 'Shows this weeks worklogs', 'herodot show'
|
90
|
+
c.example 'Shows last weeks worklogs', 'herodot show last week'
|
91
|
+
c.example 'Shows worklogs for last monday', 'herodot show monday'
|
92
|
+
c.example 'Shows worklogs for 12-12-2016', 'herodot show 12-12-2016'
|
93
|
+
c.example 'Shows last weeks worklogs as json', 'herodot show --format json last week'
|
94
|
+
c.example 'Shows last weeks worklogs as json (short)', 'herodot show -f json last week'
|
95
|
+
end
|
94
96
|
end
|
95
97
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: herodot
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- bitcrowd
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-02-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -67,7 +67,7 @@ dependencies:
|
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name: rubocop-
|
70
|
+
name: rubocop-bitcrowd
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - ">="
|
@@ -81,7 +81,7 @@ dependencies:
|
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name: rubocop-
|
84
|
+
name: rubocop-rspec
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
87
|
- - ">="
|
@@ -95,7 +95,7 @@ dependencies:
|
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
98
|
+
name: chronic
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - ">="
|
@@ -109,7 +109,7 @@ dependencies:
|
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
112
|
+
name: commander
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
114
114
|
requirements:
|
115
115
|
- - ">="
|
@@ -123,7 +123,7 @@ dependencies:
|
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0'
|
125
125
|
- !ruby/object:Gem::Dependency
|
126
|
-
name:
|
126
|
+
name: rainbow
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
128
128
|
requirements:
|
129
129
|
- - ">="
|
@@ -137,7 +137,7 @@ dependencies:
|
|
137
137
|
- !ruby/object:Gem::Version
|
138
138
|
version: '0'
|
139
139
|
- !ruby/object:Gem::Dependency
|
140
|
-
name:
|
140
|
+
name: terminal-table
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|
142
142
|
requirements:
|
143
143
|
- - ">="
|
@@ -165,9 +165,11 @@ files:
|
|
165
165
|
- ".rubocop.yml"
|
166
166
|
- ".travis.yml"
|
167
167
|
- Gemfile
|
168
|
+
- Gemfile.lock
|
168
169
|
- LICENSE.txt
|
169
170
|
- README.md
|
170
171
|
- Rakefile
|
172
|
+
- bin/ci
|
171
173
|
- bin/console
|
172
174
|
- bin/setup
|
173
175
|
- exe/herodot
|
@@ -200,7 +202,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
200
202
|
version: '0'
|
201
203
|
requirements: []
|
202
204
|
rubyforge_project:
|
203
|
-
rubygems_version: 2.5
|
205
|
+
rubygems_version: 2.7.5
|
204
206
|
signing_key:
|
205
207
|
specification_version: 4
|
206
208
|
summary: Track your work with your git activity.
|