gitloggl 0.1.0
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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +127 -0
- data/LICENSE.txt +20 -0
- data/README.md +11 -0
- data/Rakefile +6 -0
- data/bin/console +5 -0
- data/bin/gitloggl +11 -0
- data/bin/setup +8 -0
- data/exe/gitloggl +18 -0
- data/gitloggl.gemspec +46 -0
- data/lib/gitloggl/cli.rb +16 -0
- data/lib/gitloggl/command.rb +167 -0
- data/lib/gitloggl/commands/gitlab_cfg.rb +50 -0
- data/lib/gitloggl/commands/main.rb +20 -0
- data/lib/gitloggl/commands/sync.rb +133 -0
- data/lib/gitloggl/commands/toggl_cfg.rb +53 -0
- data/lib/gitloggl/connection.rb +39 -0
- data/lib/gitloggl/const.rb +8 -0
- data/lib/gitloggl/gitlab/cli.rb +86 -0
- data/lib/gitloggl/gitlab/issue.rb +25 -0
- data/lib/gitloggl/gitlab/stack/abstract.rb +50 -0
- data/lib/gitloggl/gitlab/stack/detect_project.rb +19 -0
- data/lib/gitloggl/gitlab/stack/filter.rb +15 -0
- data/lib/gitloggl/gitlab/stack/group_agg.rb +21 -0
- data/lib/gitloggl/gitlab/stack/load_notes.rb +31 -0
- data/lib/gitloggl/gitlab/stack/update_spent.rb +45 -0
- data/lib/gitloggl/templates/.gitkeep +1 -0
- data/lib/gitloggl/templates/toggle_conf/.gitkeep +1 -0
- data/lib/gitloggl/toggle/cli.rb +60 -0
- data/lib/gitloggl/version.rb +3 -0
- data/lib/gitloggl.rb +58 -0
- metadata +344 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitloggl
|
|
4
|
+
module Commands
|
|
5
|
+
class Sync < Gitloggl::Command
|
|
6
|
+
attr_accessor :date_from, :date_to
|
|
7
|
+
attr_accessor :toggl_bar, :gitlab_bar
|
|
8
|
+
|
|
9
|
+
def execute
|
|
10
|
+
self.date_from, self.date_to = select_dates
|
|
11
|
+
|
|
12
|
+
setup_toggl_hooks
|
|
13
|
+
setup_gitlab_hooks
|
|
14
|
+
|
|
15
|
+
toggl.run
|
|
16
|
+
|
|
17
|
+
print_tables
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def print_tables
|
|
23
|
+
if updated.any?
|
|
24
|
+
puts pastel.green('Updated tracks')
|
|
25
|
+
puts(updated_table)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if rejected.any?
|
|
29
|
+
puts pastel.yellow('Rejected tracks')
|
|
30
|
+
puts(rejected_table)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def setup_toggl_hooks
|
|
35
|
+
toggl.on_first_page do |response|
|
|
36
|
+
self.toggl_bar = bars.register("toggl [:bar] :percent", total: response.total_pages)
|
|
37
|
+
self.gitlab_bar = bars.register("gitlab [:bar] :percent", total: response.total_count)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
toggl.on_each_page do
|
|
41
|
+
toggl_bar.advance
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
toggl.on_each_page do |response|
|
|
45
|
+
issues = response.body.fetch('data').map do |row|
|
|
46
|
+
Gitlab::Issue.new(
|
|
47
|
+
spent_ms: Integer(row.fetch('dur')),
|
|
48
|
+
description: row.fetch('description')
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
gitlab.batch(issues)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def setup_gitlab_hooks
|
|
57
|
+
gitlab.on_completed do
|
|
58
|
+
gitlab_bar.advance
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
gitlab.on_skipped do
|
|
62
|
+
gitlab_bar.advance
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
gitlab.on_rejected do |issue, reason|
|
|
66
|
+
rejected.push([issue, reason])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
gitlab.on_updated do |issue, diff|
|
|
70
|
+
updated.push([issue, diff])
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def rejected
|
|
75
|
+
@rejected ||= []
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def updated
|
|
79
|
+
@updated ||= []
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def rejected_table
|
|
83
|
+
table = TTY::Table.new header: %w[Track Reason]
|
|
84
|
+
|
|
85
|
+
rejected.uniq { |(i)| i.description }.each do |(issue, reason)|
|
|
86
|
+
table << [issue.description, reason]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
table.render :unicode, padding: 1
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def updated_table
|
|
93
|
+
table = TTY::Table.new header: %w[Track Diff]
|
|
94
|
+
|
|
95
|
+
updated.each do |(issue, diff)|
|
|
96
|
+
table << [issue.description, diff]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
table.render :unicode, padding: 1
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def bars
|
|
103
|
+
@bars ||= TTY::ProgressBar::Multi.new("sync in progress #{date_from} - #{date_to} [:bar] :percent")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def toggl
|
|
107
|
+
@toggl ||= Toggle::Cli.new do |c|
|
|
108
|
+
c.verbose = verbose?
|
|
109
|
+
c.workspace_id = config.fetch(Const::TOGGL_WORKSPACE_ID)
|
|
110
|
+
c.token = config.fetch(Const::TOGGL_TOKEN)
|
|
111
|
+
c.date_from = date_from
|
|
112
|
+
c.date_to = date_to
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def gitlab
|
|
117
|
+
@gitlab ||= Gitlab::Cli.new do |c|
|
|
118
|
+
c.verbose = verbose?
|
|
119
|
+
c.url = config.fetch(Const::GITLAB_URL)
|
|
120
|
+
c.token = config.fetch(Const::GITLAB_TOKEN)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def select_dates
|
|
125
|
+
prompt.select('Select period for sync. Tracks will be added to gitlab if diff will be detected') do |menu|
|
|
126
|
+
menu.choice 'Past 3 weeks', [3.weeks.ago.to_date, Date.current]
|
|
127
|
+
menu.choice 'Past 2 weeks', [2.weeks.ago.to_date, Date.current]
|
|
128
|
+
menu.choice 'Past 7 days', [7.days.ago.to_date, Date.current]
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitloggl
|
|
4
|
+
module Commands
|
|
5
|
+
class TogglCfg < Gitloggl::Command
|
|
6
|
+
def execute(*)
|
|
7
|
+
render_config_config
|
|
8
|
+
|
|
9
|
+
prompt.select('') do |menu|
|
|
10
|
+
menu.enum ')'
|
|
11
|
+
menu_back(menu)
|
|
12
|
+
menu.choice 'Change', -> { change }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def change
|
|
19
|
+
workspace_id = ask_workspace_id
|
|
20
|
+
token = ask_token
|
|
21
|
+
|
|
22
|
+
return unless prompt.yes? <<~ECHO
|
|
23
|
+
Change settings? WorkspaceID: #{pastel.green(workspace_id)}, token: #{pastel.green(token)}
|
|
24
|
+
ECHO
|
|
25
|
+
|
|
26
|
+
config.set(Const::TOGGL_WORKSPACE_ID, value: workspace_id)
|
|
27
|
+
config.set(Const::TOGGL_TOKEN, value: token)
|
|
28
|
+
|
|
29
|
+
config.write(force: true)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def ask_workspace_id
|
|
33
|
+
prompt.ask('Enter workspaceID:') do |q|
|
|
34
|
+
q.required true
|
|
35
|
+
q.validate /\A.+\Z/
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def ask_token
|
|
40
|
+
prompt.ask('Enter token:') do |q|
|
|
41
|
+
q.required true
|
|
42
|
+
q.validate /\A.+\Z/
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def render_config_config
|
|
47
|
+
table = TTY::Table.new header: %w[WorkspaceID Token]
|
|
48
|
+
table << [config.fetch(Const::TOGGL_WORKSPACE_ID), config.fetch(Const::TOGGL_TOKEN)]
|
|
49
|
+
puts table.render :unicode, padding: 1
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Gitloggl
|
|
2
|
+
class Connection
|
|
3
|
+
USER_AGENT = 'Gitloggl bot'
|
|
4
|
+
|
|
5
|
+
attr_reader :url, :headers, :verbose
|
|
6
|
+
|
|
7
|
+
delegate :get, :post, :in_parallel, to: :transport
|
|
8
|
+
|
|
9
|
+
def initialize(url, headers: {}, verbose: false)
|
|
10
|
+
@url = url
|
|
11
|
+
@headers = headers
|
|
12
|
+
@verbose = verbose
|
|
13
|
+
|
|
14
|
+
yield(self) if block_given?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def transport
|
|
18
|
+
@transport ||= Faraday.new(url: url, headers: default_headers.merge(headers)) do |conn|
|
|
19
|
+
conn.use Faraday::Request::UrlEncoded
|
|
20
|
+
conn.use Faraday::Response::RaiseError
|
|
21
|
+
conn.response :json
|
|
22
|
+
conn.use Faraday::Response::Logger, self, bodies: true if verbose
|
|
23
|
+
conn.adapter :typhoeus
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def default_headers
|
|
28
|
+
@default_headers ||= {
|
|
29
|
+
'User-Agent' => USER_AGENT
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
%i[debug info warn error fatal].each do |name|
|
|
34
|
+
define_method name do |label, &block|
|
|
35
|
+
puts("[#{label}] #{block.call}")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module Gitloggl
|
|
2
|
+
module Gitlab
|
|
3
|
+
class Cli
|
|
4
|
+
include Hooks
|
|
5
|
+
include Hooks::InstanceHooks
|
|
6
|
+
|
|
7
|
+
define_hook :on_rejected, scope: ->(*) { nil }
|
|
8
|
+
define_hook :on_updated, scope: ->(*) { nil }
|
|
9
|
+
define_hook :on_completed, scope: ->(*) { nil }
|
|
10
|
+
define_hook :on_skipped, scope: ->(*) { nil }
|
|
11
|
+
|
|
12
|
+
attr_accessor :token, :url, :verbose
|
|
13
|
+
|
|
14
|
+
def initialize(params = {})
|
|
15
|
+
params.each { |k, v| public_send("#{k}=", v) }
|
|
16
|
+
yield(self) if block_given?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def batch(issues)
|
|
20
|
+
stack = Middleware::Builder.new do |b|
|
|
21
|
+
b.use Stack::Filter, where: ->(issue) do
|
|
22
|
+
next true if issue.path.present?
|
|
23
|
+
|
|
24
|
+
rejected(issue, %{cant recognize gitlab issue-path})
|
|
25
|
+
|
|
26
|
+
false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
b.use Stack::GroupAgg, callback: ->(issues) do
|
|
30
|
+
issues.each { skipped }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
b.use Stack::DetectProject
|
|
34
|
+
|
|
35
|
+
b.use Stack::Filter, where: -> (issue) do
|
|
36
|
+
next true if issue.path.project_id.present?
|
|
37
|
+
|
|
38
|
+
rejected(issue, %{cant recognize gitlab projectID for issue})
|
|
39
|
+
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
b.use Stack::LoadNotes
|
|
44
|
+
|
|
45
|
+
b.use Stack::UpdateSpent, callback: ->(issue, diff_sec) do
|
|
46
|
+
updated(issue, %{added +#{ChronicDuration.output(diff_sec, keep_zero: true)} })
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
env = OpenStruct.new(cli: self, issues: issues)
|
|
51
|
+
|
|
52
|
+
stack.call(env)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def rejected(issue, reason)
|
|
56
|
+
run_hook(:on_completed, issue)
|
|
57
|
+
run_hook(:on_rejected, issue, reason)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def updated(issue, diff)
|
|
61
|
+
run_hook(:on_completed, issue)
|
|
62
|
+
run_hook(:on_updated, issue, diff)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def skipped
|
|
66
|
+
run_hook(:on_skipped)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def connection
|
|
70
|
+
@connection ||= Connection.new(url, headers: { 'PRIVATE-TOKEN' => token }, verbose: verbose).tap do |c|
|
|
71
|
+
c.transport.basic_auth(token, 'api_token')
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [Array<JSON>]
|
|
76
|
+
def projects
|
|
77
|
+
@projects ||= connection.get("/api/v4/projects").body
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @return [Integer]
|
|
81
|
+
def current_user_id
|
|
82
|
+
@current_user_id ||= connection.get("/api/v4/user").body.fetch('id')
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Gitloggl
|
|
2
|
+
module Gitlab
|
|
3
|
+
class Issue < Struct.new(:description, :project_id, :spent_ms, keyword_init: true)
|
|
4
|
+
ISSUE_PATH_RE = /([[[:alnum:]]_-]*)#(\d+)/ # "poslogic-partner#471"
|
|
5
|
+
|
|
6
|
+
attr_accessor :notes
|
|
7
|
+
|
|
8
|
+
def spent_sec
|
|
9
|
+
spent_ms.fdiv(1000).round
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def path
|
|
13
|
+
return nil unless description =~ ISSUE_PATH_RE
|
|
14
|
+
return nil unless $1.present? && $2.present?
|
|
15
|
+
|
|
16
|
+
@path ||= OpenStruct.new(project_path: $1, issue_id: $2)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def +(issue)
|
|
20
|
+
self.spent_ms = spent_ms + issue.spent_ms
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitloggl
|
|
4
|
+
module Gitlab
|
|
5
|
+
module Stack
|
|
6
|
+
class Abstract
|
|
7
|
+
attr_reader :app, :env, :options
|
|
8
|
+
|
|
9
|
+
def self.opt(*names)
|
|
10
|
+
names.each do |name|
|
|
11
|
+
define_method "#{name}!" do
|
|
12
|
+
options.fetch(name)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(app, options = {})
|
|
18
|
+
@app = app
|
|
19
|
+
@options = options
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(env)
|
|
23
|
+
@env = env
|
|
24
|
+
|
|
25
|
+
before_call
|
|
26
|
+
|
|
27
|
+
around_call do
|
|
28
|
+
@app.call(env)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
after_call
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
protected
|
|
35
|
+
|
|
36
|
+
def before_call
|
|
37
|
+
#:stub:
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def around_call
|
|
41
|
+
yield
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def after_call
|
|
45
|
+
#:stub:
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitloggl
|
|
4
|
+
module Gitlab
|
|
5
|
+
module Stack
|
|
6
|
+
class DetectProject < Abstract
|
|
7
|
+
def before_call
|
|
8
|
+
env.issues.each do |issue|
|
|
9
|
+
env.cli.projects.find do |row|
|
|
10
|
+
next unless row.fetch('path_with_namespace') =~ %r[#{issue.path.project_path}]
|
|
11
|
+
|
|
12
|
+
issue.path.project_id = row.fetch('id')
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitloggl
|
|
4
|
+
module Gitlab
|
|
5
|
+
module Stack
|
|
6
|
+
class GroupAgg < Abstract
|
|
7
|
+
opt :callback
|
|
8
|
+
|
|
9
|
+
def before_call
|
|
10
|
+
env.issues = env.issues.group_by(&:path).each_with_object([]) do |(_, group), object|
|
|
11
|
+
object.push(group.inject { |a, b| a + b })
|
|
12
|
+
|
|
13
|
+
next unless group.many?
|
|
14
|
+
|
|
15
|
+
callback!.call(group[1..-1])
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitloggl
|
|
4
|
+
module Gitlab
|
|
5
|
+
module Stack
|
|
6
|
+
class LoadNotes < Abstract
|
|
7
|
+
def before_call
|
|
8
|
+
result = {}
|
|
9
|
+
|
|
10
|
+
env.cli.connection.in_parallel do
|
|
11
|
+
issue_paths.each do |path|
|
|
12
|
+
response = env.cli.connection.get("/api/v4/projects/#{path.project_id}/issues/#{path.issue_id}/notes")
|
|
13
|
+
result[path] = response
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
env.issues.each do |issue|
|
|
18
|
+
issue.notes = result[issue.path]&.body || []
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def issue_paths
|
|
26
|
+
env.issues.map(&:path).uniq.compact
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitloggl
|
|
4
|
+
module Gitlab
|
|
5
|
+
module Stack
|
|
6
|
+
class UpdateSpent < Abstract
|
|
7
|
+
GITLAB_SPENT_NOTE_RE = /added (.+) of time spent/
|
|
8
|
+
|
|
9
|
+
opt :callback
|
|
10
|
+
|
|
11
|
+
def before_call
|
|
12
|
+
queue = env.issues.each_with_object([]) do |issue, object|
|
|
13
|
+
diff_sec = issue.spent_sec - spent_sec(issue.notes)
|
|
14
|
+
diff_sec = diff_sec > 0 ? diff_sec : 0
|
|
15
|
+
|
|
16
|
+
callback!.call(issue, diff_sec)
|
|
17
|
+
|
|
18
|
+
next if diff_sec.zero?
|
|
19
|
+
|
|
20
|
+
object << [
|
|
21
|
+
"/api/v4/projects/#{issue.path.project_id}/issues/#{issue.path.issue_id}/add_spent_time",
|
|
22
|
+
duration: "#{diff_sec}sec"
|
|
23
|
+
]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
env.cli.connection.in_parallel do
|
|
27
|
+
queue.each { |argv| env.cli.connection.post(*argv) }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# @return [Integer]
|
|
34
|
+
def spent_sec(notes)
|
|
35
|
+
notes.inject(0) do |memo, row|
|
|
36
|
+
next memo unless row.fetch('author').fetch('id') == env.cli.current_user_id
|
|
37
|
+
next memo unless row.fetch('body') =~ GITLAB_SPENT_NOTE_RE
|
|
38
|
+
|
|
39
|
+
memo + ChronicDuration.parse($1).to_i
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module Gitloggl
|
|
2
|
+
module Toggle
|
|
3
|
+
class Cli
|
|
4
|
+
include Hooks
|
|
5
|
+
include Hooks::InstanceHooks
|
|
6
|
+
|
|
7
|
+
define_hook :on_first_page, scope: ->(*) { nil }
|
|
8
|
+
define_hook :on_each_page, scope: ->(*) { nil }
|
|
9
|
+
|
|
10
|
+
attr_accessor :token, :workspace_id, :date_from, :date_to, :verbose
|
|
11
|
+
|
|
12
|
+
def initialize(params = {})
|
|
13
|
+
params.each { |k, v| public_send("#{k}=", v) }
|
|
14
|
+
yield(self) if block_given?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run(page: 1)
|
|
18
|
+
body = request(page)
|
|
19
|
+
|
|
20
|
+
total_count = body.fetch('total_count')
|
|
21
|
+
per_page = body.fetch('per_page')
|
|
22
|
+
total_pages = total_count.fdiv(per_page).ceil
|
|
23
|
+
|
|
24
|
+
result = OpenStruct.new(
|
|
25
|
+
total_count: total_count,
|
|
26
|
+
per_page: per_page,
|
|
27
|
+
total_pages: total_pages,
|
|
28
|
+
page: page,
|
|
29
|
+
body: body
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
run_hook(:on_first_page, result) if page == 1
|
|
33
|
+
run_hook(:on_each_page, result)
|
|
34
|
+
|
|
35
|
+
run(page: page.next) if body['data'].length >= per_page
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# @return [JSON]
|
|
41
|
+
def request(page)
|
|
42
|
+
response = connection.get('/reports/api/v2/details', {
|
|
43
|
+
workspace_id: workspace_id,
|
|
44
|
+
since: date_from,
|
|
45
|
+
until: date_to,
|
|
46
|
+
user_agent: Connection::USER_AGENT,
|
|
47
|
+
page: page
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
response.body
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def connection
|
|
54
|
+
@connection ||= Connection.new('https://toggl.com/reports/api/v2/details', verbose: verbose).tap do |c|
|
|
55
|
+
c.transport.basic_auth(token, 'api_token')
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/gitloggl.rb
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require 'tty-table'
|
|
2
|
+
require 'tty-progressbar'
|
|
3
|
+
require 'ostruct'
|
|
4
|
+
require 'faraday'
|
|
5
|
+
require 'faraday_middleware'
|
|
6
|
+
require 'typhoeus'
|
|
7
|
+
require 'json'
|
|
8
|
+
require 'chronic_duration'
|
|
9
|
+
require 'middleware'
|
|
10
|
+
require 'hooks'
|
|
11
|
+
require 'active_support/core_ext/object/blank'
|
|
12
|
+
require 'active_support/core_ext/enumerable'
|
|
13
|
+
require 'active_support/dependencies/autoload'
|
|
14
|
+
require 'active_support/core_ext/module/delegation'
|
|
15
|
+
require 'active_support/duration'
|
|
16
|
+
require 'active_support/core_ext/date'
|
|
17
|
+
require 'active_support/core_ext/numeric/time'
|
|
18
|
+
require 'gitloggl/const'
|
|
19
|
+
require 'gitloggl/version'
|
|
20
|
+
require 'gitloggl/cli'
|
|
21
|
+
require 'gitloggl/command'
|
|
22
|
+
require 'gitloggl/connection'
|
|
23
|
+
require 'gitloggl/commands/main'
|
|
24
|
+
|
|
25
|
+
module Gitloggl
|
|
26
|
+
module Commands
|
|
27
|
+
extend ActiveSupport::Autoload
|
|
28
|
+
|
|
29
|
+
autoload :Main
|
|
30
|
+
autoload :TogglCfg
|
|
31
|
+
autoload :GitlabCfg
|
|
32
|
+
autoload :Sync
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module Toggle
|
|
36
|
+
extend ActiveSupport::Autoload
|
|
37
|
+
|
|
38
|
+
autoload :Cli
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
module Gitlab
|
|
42
|
+
extend ActiveSupport::Autoload
|
|
43
|
+
|
|
44
|
+
autoload :Issue
|
|
45
|
+
autoload :Cli
|
|
46
|
+
|
|
47
|
+
module Stack
|
|
48
|
+
extend ActiveSupport::Autoload
|
|
49
|
+
|
|
50
|
+
autoload :Abstract
|
|
51
|
+
autoload :Filter
|
|
52
|
+
autoload :GroupAgg
|
|
53
|
+
autoload :DetectProject
|
|
54
|
+
autoload :LoadNotes
|
|
55
|
+
autoload :UpdateSpent
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|