abt-cli 0.0.26 → 0.0.31
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/abt.rb +0 -4
- data/lib/abt/cli.rb +5 -1
- data/lib/abt/directory_config.rb +28 -10
- data/lib/abt/docs.rb +10 -6
- data/lib/abt/providers/asana.rb +1 -0
- data/lib/abt/providers/asana/base_command.rb +33 -3
- data/lib/abt/providers/asana/commands/add.rb +0 -4
- data/lib/abt/providers/asana/commands/branch_name.rb +0 -13
- data/lib/abt/providers/asana/commands/clear.rb +1 -1
- data/lib/abt/providers/asana/commands/current.rb +0 -18
- data/lib/abt/providers/asana/commands/finalize.rb +2 -4
- data/lib/abt/providers/asana/commands/pick.rb +11 -41
- data/lib/abt/providers/asana/commands/tasks.rb +2 -7
- data/lib/abt/providers/asana/commands/write_config.rb +73 -0
- data/lib/abt/providers/asana/configuration.rb +1 -1
- data/lib/abt/providers/asana/path.rb +2 -2
- data/lib/abt/providers/asana/services/project_picker.rb +54 -0
- data/lib/abt/providers/asana/services/task_picker.rb +83 -0
- data/lib/abt/providers/devops.rb +1 -0
- data/lib/abt/providers/devops/api.rb +27 -20
- data/lib/abt/providers/devops/base_command.rb +42 -25
- data/lib/abt/providers/devops/commands/branch_name.rb +8 -16
- data/lib/abt/providers/devops/commands/clear.rb +1 -1
- data/lib/abt/providers/devops/commands/current.rb +2 -21
- data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +8 -16
- data/lib/abt/providers/devops/commands/pick.rb +11 -60
- data/lib/abt/providers/devops/commands/work_items.rb +3 -7
- data/lib/abt/providers/devops/commands/write_config.rb +47 -0
- data/lib/abt/providers/devops/configuration.rb +1 -1
- data/lib/abt/providers/devops/path.rb +24 -8
- data/lib/abt/providers/devops/services/board_picker.rb +69 -0
- data/lib/abt/providers/devops/services/project_picker.rb +73 -0
- data/lib/abt/providers/devops/services/work_item_picker.rb +99 -0
- data/lib/abt/providers/harvest.rb +1 -0
- data/lib/abt/providers/harvest/base_command.rb +45 -3
- data/lib/abt/providers/harvest/commands/clear.rb +1 -1
- data/lib/abt/providers/harvest/commands/current.rb +0 -28
- data/lib/abt/providers/harvest/commands/pick.rb +12 -27
- data/lib/abt/providers/harvest/commands/projects.rb +2 -9
- data/lib/abt/providers/harvest/commands/tasks.rb +2 -19
- data/lib/abt/providers/harvest/commands/track.rb +72 -39
- data/lib/abt/providers/harvest/commands/write_config.rb +41 -0
- data/lib/abt/providers/harvest/configuration.rb +1 -1
- data/lib/abt/providers/harvest/harvest_helpers.rb +25 -0
- data/lib/abt/providers/harvest/path.rb +1 -1
- data/lib/abt/providers/harvest/services/project_picker.rb +53 -0
- data/lib/abt/providers/harvest/services/task_picker.rb +50 -0
- data/lib/abt/version.rb +1 -1
- metadata +13 -6
- data/lib/abt/providers/asana/commands/init.rb +0 -42
- data/lib/abt/providers/devops/commands/boards.rb +0 -34
- data/lib/abt/providers/devops/commands/init.rb +0 -79
- data/lib/abt/providers/harvest/commands/init.rb +0 -53
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
Dir.glob("#{File.expand_path(__dir__)}/harvest/*.rb").sort.each { |file| require file }
|
4
4
|
Dir.glob("#{File.expand_path(__dir__)}/harvest/commands/*.rb").sort.each { |file| require file }
|
5
|
+
Dir.glob("#{File.expand_path(__dir__)}/harvest/services/*.rb").sort.each { |file| require file }
|
5
6
|
|
6
7
|
module Abt
|
7
8
|
module Providers
|
@@ -26,13 +26,55 @@ module Abt
|
|
26
26
|
def require_project!
|
27
27
|
return if project_id
|
28
28
|
|
29
|
-
abort("No current/specified project. Did you
|
29
|
+
abort("No current/specified project. Did you forget to run `pick`?")
|
30
30
|
end
|
31
31
|
|
32
32
|
def require_task!
|
33
|
-
|
33
|
+
require_project!
|
34
|
+
return if task_id
|
34
35
|
|
35
|
-
abort("No current/specified task. Did you
|
36
|
+
abort("No current/specified task. Did you forget to run `pick`?")
|
37
|
+
end
|
38
|
+
|
39
|
+
def prompt_project!
|
40
|
+
result = Services::ProjectPicker.call(cli: cli, project_assignments: project_assignments)
|
41
|
+
@path = result.path
|
42
|
+
@project = result.project
|
43
|
+
end
|
44
|
+
|
45
|
+
def prompt_task!
|
46
|
+
result = Services::TaskPicker.call(cli: cli, path: path, project_assignment: project_assignment)
|
47
|
+
@path = result.path
|
48
|
+
@task = result.task
|
49
|
+
end
|
50
|
+
|
51
|
+
def task
|
52
|
+
return @task if instance_variable_defined?(:@task)
|
53
|
+
|
54
|
+
@task = if project_assignment
|
55
|
+
project_assignment["task_assignments"].map { |ta| ta["task"] }.find do |task|
|
56
|
+
task["id"].to_s == task_id
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def project
|
62
|
+
return @project if instance_variable_defined?(:@project)
|
63
|
+
|
64
|
+
@project = if project_assignment
|
65
|
+
project_assignment["project"].merge("client" => project_assignment["client"])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def project_assignment
|
70
|
+
@project_assignment ||= project_assignments.find { |pa| pa["project"]["id"].to_s == path.project_id }
|
71
|
+
end
|
72
|
+
|
73
|
+
def project_assignments
|
74
|
+
@project_assignments ||= begin
|
75
|
+
warn("Fetching Harvest data...")
|
76
|
+
api.get_paged("users/me/project_assignments")
|
77
|
+
end
|
36
78
|
end
|
37
79
|
|
38
80
|
def print_project(project)
|
@@ -22,7 +22,7 @@ module Abt
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def perform
|
25
|
-
abort("Flags --global and --all cannot be used
|
25
|
+
abort("Flags --global and --all cannot be used together") if flags[:global] && flags[:all]
|
26
26
|
|
27
27
|
config.clear_local unless flags[:global]
|
28
28
|
config.clear_global if flags[:global] || flags[:all]
|
@@ -40,34 +40,6 @@ module Abt
|
|
40
40
|
abort("Invalid project: #{project_id}") if project.nil?
|
41
41
|
abort("Invalid task: #{task_id}") if task_id && task.nil?
|
42
42
|
end
|
43
|
-
|
44
|
-
def project
|
45
|
-
return @project if instance_variable_defined?(:@project)
|
46
|
-
|
47
|
-
@project = if project_assignment
|
48
|
-
project_assignment["project"].merge("client" => project_assignment["client"])
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def task
|
53
|
-
return @task if instance_variable_defined?(:@task)
|
54
|
-
|
55
|
-
@task = if project_assignment
|
56
|
-
project_assignment["task_assignments"].map { |ta| ta["task"] }.find do |task|
|
57
|
-
task["id"].to_s == task_id
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def project_assignment
|
63
|
-
@project_assignment ||= begin
|
64
|
-
project_assignments.find { |pa| pa["project"]["id"].to_s == project_id }
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
def project_assignments
|
69
|
-
@project_assignments ||= api.get_paged("users/me/project_assignments")
|
70
|
-
end
|
71
43
|
end
|
72
44
|
end
|
73
45
|
end
|
@@ -15,46 +15,31 @@ module Abt
|
|
15
15
|
|
16
16
|
def self.flags
|
17
17
|
[
|
18
|
-
["-d", "--dry-run", "Keep existing configuration"]
|
18
|
+
["-d", "--dry-run", "Keep existing configuration"],
|
19
|
+
["-c", "--clean", "Don't reuse project configuration"]
|
19
20
|
]
|
20
21
|
end
|
21
22
|
|
22
23
|
def perform
|
23
|
-
|
24
|
-
require_project!
|
25
|
-
|
26
|
-
warn(project["name"])
|
27
|
-
task = pick_task
|
24
|
+
pick!
|
28
25
|
|
29
26
|
print_task(project, task)
|
30
27
|
|
31
28
|
return if flags[:"dry-run"]
|
32
29
|
|
33
|
-
config.
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
def project
|
39
|
-
project_assignment["project"]
|
40
|
-
end
|
41
|
-
|
42
|
-
def pick_task
|
43
|
-
cli.prompt.choice("Select a task", tasks)
|
44
|
-
end
|
30
|
+
unless config.local_available?
|
31
|
+
warn("No local configuration to update - will function as dry run")
|
32
|
+
return
|
33
|
+
end
|
45
34
|
|
46
|
-
|
47
|
-
@tasks ||= project_assignment["task_assignments"].map { |ta| ta["task"] }
|
35
|
+
config.path = path
|
48
36
|
end
|
49
37
|
|
50
|
-
|
51
|
-
@project_assignment ||= begin
|
52
|
-
project_assignments.find { |pa| pa["project"]["id"].to_s == project_id }
|
53
|
-
end
|
54
|
-
end
|
38
|
+
private
|
55
39
|
|
56
|
-
def
|
57
|
-
|
40
|
+
def pick!
|
41
|
+
prompt_project! if project_id.nil? || flags[:clean]
|
42
|
+
prompt_task!
|
58
43
|
end
|
59
44
|
end
|
60
45
|
end
|
@@ -22,17 +22,10 @@ module Abt
|
|
22
22
|
private
|
23
23
|
|
24
24
|
def projects
|
25
|
-
@projects ||=
|
26
|
-
|
27
|
-
project_assignments.map do |project_assignment|
|
28
|
-
project_assignment["project"].merge("client" => project_assignment["client"])
|
29
|
-
end
|
25
|
+
@projects ||= project_assignments.map do |project_assignment|
|
26
|
+
project_assignment["project"].merge("client" => project_assignment["client"])
|
30
27
|
end
|
31
28
|
end
|
32
|
-
|
33
|
-
def project_assignments
|
34
|
-
@project_assignments ||= api.get_paged("users/me/project_assignments")
|
35
|
-
end
|
36
29
|
end
|
37
30
|
end
|
38
31
|
end
|
@@ -14,7 +14,7 @@ module Abt
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
|
-
|
17
|
+
prompt_project! unless project_id
|
18
18
|
|
19
19
|
tasks.each do |task|
|
20
20
|
print_task(project, task)
|
@@ -23,25 +23,8 @@ module Abt
|
|
23
23
|
|
24
24
|
private
|
25
25
|
|
26
|
-
def project
|
27
|
-
project_assignment["project"]
|
28
|
-
end
|
29
|
-
|
30
26
|
def tasks
|
31
|
-
@tasks ||=
|
32
|
-
warn("Fetching tasks...")
|
33
|
-
project_assignment["task_assignments"].map { |ta| ta["task"] }
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def project_assignment
|
38
|
-
@project_assignment ||= begin
|
39
|
-
project_assignments.find { |pa| pa["project"]["id"].to_s == project_id }
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
def project_assignments
|
44
|
-
@project_assignments ||= api.get_paged("users/me/project_assignments")
|
27
|
+
@tasks ||= project_assignment["task_assignments"].map { |ta| ta["task"] }
|
45
28
|
end
|
46
29
|
end
|
47
30
|
end
|
@@ -4,7 +4,7 @@ module Abt
|
|
4
4
|
module Providers
|
5
5
|
module Harvest
|
6
6
|
module Commands
|
7
|
-
class Track < BaseCommand
|
7
|
+
class Track < BaseCommand # rubocop:disable Metrics/ClassLength
|
8
8
|
def self.usage
|
9
9
|
"abt track harvest[:<project-id>/<task-id>] [options]"
|
10
10
|
end
|
@@ -20,15 +20,21 @@ module Abt
|
|
20
20
|
["-s", "--set", "Set specified task as current"],
|
21
21
|
["-c", "--comment COMMENT", "Override comment"],
|
22
22
|
["-t", "--time HOURS",
|
23
|
-
"
|
24
|
-
["-
|
23
|
+
"Track amount of hours, this will create a stopped entry."],
|
24
|
+
["-i", "--since HH:MM",
|
25
|
+
"Start entry today at specified time. The computed duration will be deducted from the running entry if one exists."] # rubocop:disable Layout/LineLength
|
25
26
|
]
|
26
27
|
end
|
27
28
|
|
28
29
|
def perform
|
30
|
+
abort("Flags --time and --since cannot be used together") if flags[:time] && flags[:since]
|
31
|
+
|
29
32
|
require_task!
|
30
33
|
|
31
|
-
|
34
|
+
maybe_adjust_previous_entry
|
35
|
+
entry = create_entry!
|
36
|
+
|
37
|
+
print_task(entry["project"], entry["task"])
|
32
38
|
|
33
39
|
maybe_override_current_task
|
34
40
|
rescue Abt::HttpError::HttpError => _e
|
@@ -37,31 +43,41 @@ module Abt
|
|
37
43
|
|
38
44
|
private
|
39
45
|
|
40
|
-
def
|
41
|
-
|
46
|
+
def create_entry!
|
47
|
+
result = api.post("time_entries", Oj.dump(entry_data, mode: :json))
|
48
|
+
api.patch("time_entries/#{result['id']}/restart") if flags.key?(:since)
|
49
|
+
result
|
42
50
|
end
|
43
51
|
|
44
|
-
def
|
45
|
-
|
52
|
+
def maybe_adjust_previous_entry
|
53
|
+
return unless flags.key?(:since)
|
54
|
+
return unless since_flag_duration # Ensure --since flag is valid before fetching data
|
55
|
+
return unless previous_entry
|
56
|
+
|
57
|
+
adjust_previous_entry
|
58
|
+
end
|
46
59
|
|
47
|
-
|
60
|
+
def adjust_previous_entry
|
61
|
+
updated_hours = previous_entry["hours"] - since_flag_duration
|
62
|
+
abort("Cannot adjust previous entry to a negative duration") if updated_hours <= 0
|
48
63
|
|
49
|
-
api.patch("time_entries/#{
|
64
|
+
api.patch("time_entries/#{previous_entry['id']}", Oj.dump({ hours: updated_hours }, mode: :json))
|
50
65
|
|
51
|
-
|
66
|
+
subtracted_minutes = (since_flag_duration * 60).round
|
67
|
+
warn("~#{subtracted_minutes} minute(s) subtracted from previous entry")
|
52
68
|
end
|
53
69
|
|
54
|
-
def
|
55
|
-
body =
|
70
|
+
def entry_data
|
71
|
+
body = entry_base_data
|
56
72
|
|
57
73
|
maybe_add_external_link(body)
|
58
74
|
maybe_add_comment(body)
|
59
|
-
|
75
|
+
maybe_add_hours(body)
|
60
76
|
|
61
77
|
body
|
62
78
|
end
|
63
79
|
|
64
|
-
def
|
80
|
+
def entry_base_data
|
65
81
|
{
|
66
82
|
project_id: project_id,
|
67
83
|
task_id: task_id,
|
@@ -74,8 +90,8 @@ module Abt
|
|
74
90
|
if external_link_data
|
75
91
|
warn(<<~TXT)
|
76
92
|
Linking to:
|
77
|
-
|
78
|
-
|
93
|
+
#{external_link_data[:notes]}
|
94
|
+
#{external_link_data[:external_reference][:permalink]}
|
79
95
|
TXT
|
80
96
|
body.merge!(external_link_data)
|
81
97
|
else
|
@@ -83,38 +99,40 @@ module Abt
|
|
83
99
|
end
|
84
100
|
end
|
85
101
|
|
86
|
-
def maybe_add_comment(body)
|
87
|
-
body[:notes] = flags[:comment] if flags.key?(:comment)
|
88
|
-
body[:notes] ||= cli.prompt.text("Fill in comment (optional)")
|
89
|
-
end
|
90
|
-
|
91
|
-
def maybe_add_time(body)
|
92
|
-
body[:hours] = flags[:time] if flags.key?(:time)
|
93
|
-
end
|
94
|
-
|
95
102
|
def external_link_data
|
96
103
|
return @external_link_data if instance_variable_defined?(:@external_link_data)
|
104
|
+
return @external_link_data = nil if link_data_lines.empty?
|
97
105
|
|
98
|
-
|
99
|
-
|
100
|
-
return @external_link_data = nil if lines.empty?
|
101
|
-
|
102
|
-
if lines.length > 1
|
106
|
+
if link_data_lines.length > 1
|
103
107
|
abort("Got reference data from multiple scheme providers, only one is supported at a time")
|
104
108
|
end
|
105
109
|
|
106
|
-
@external_link_data = Oj.load(
|
110
|
+
@external_link_data = Oj.load(link_data_lines.first, symbol_keys: true)
|
107
111
|
end
|
108
112
|
|
109
|
-
def
|
110
|
-
|
111
|
-
|
113
|
+
def link_data_lines
|
114
|
+
@link_data_lines ||= begin
|
115
|
+
other_aris = cli.aris - [ari]
|
116
|
+
other_aris.map do |other_ari|
|
117
|
+
input = StringIO.new(other_ari.to_s)
|
118
|
+
output = StringIO.new
|
119
|
+
Abt::Cli.new(argv: ["harvest-time-entry-data"], output: output, input: input).perform
|
120
|
+
output.string.chomp
|
121
|
+
end.reject(&:empty?)
|
122
|
+
end
|
123
|
+
end
|
112
124
|
|
113
|
-
|
114
|
-
|
115
|
-
|
125
|
+
def maybe_add_comment(body)
|
126
|
+
body[:notes] = flags[:comment] if flags.key?(:comment)
|
127
|
+
body[:notes] ||= cli.prompt.text("Fill in comment (optional)")
|
128
|
+
end
|
116
129
|
|
117
|
-
|
130
|
+
def maybe_add_hours(body)
|
131
|
+
if flags[:time]
|
132
|
+
body[:hours] = flags[:time]
|
133
|
+
elsif flags[:since]
|
134
|
+
body[:hours] = since_flag_duration
|
135
|
+
end
|
118
136
|
end
|
119
137
|
|
120
138
|
def maybe_override_current_task
|
@@ -125,6 +143,21 @@ module Abt
|
|
125
143
|
config.path = path
|
126
144
|
warn("Current task updated")
|
127
145
|
end
|
146
|
+
|
147
|
+
def since_flag_duration
|
148
|
+
@since_flag_duration ||= begin
|
149
|
+
since_hours = HarvestHelpers.decimal_hours_from_string(flags[:since])
|
150
|
+
now_hours = HarvestHelpers.decimal_hours_from_string(Time.now.strftime("%T"))
|
151
|
+
|
152
|
+
abort("Specified \"since\" time (#{flags[:since]}) is in the future") if now_hours <= since_hours
|
153
|
+
|
154
|
+
now_hours - since_hours
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def previous_entry
|
159
|
+
@previous_entry ||= api.get_paged("time_entries", is_running: true, user_id: config.user_id).first
|
160
|
+
end
|
128
161
|
end
|
129
162
|
end
|
130
163
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Harvest
|
6
|
+
module Commands
|
7
|
+
class WriteConfig < BaseCommand
|
8
|
+
def self.usage
|
9
|
+
"abt write-config harvest[:<project-id>[/<task-id>]]"
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.description
|
13
|
+
"Write Harvest settings to .abt.yml"
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.flags
|
17
|
+
[
|
18
|
+
["-c", "--clean", "Don't reuse configuration"]
|
19
|
+
]
|
20
|
+
end
|
21
|
+
|
22
|
+
def perform
|
23
|
+
prompt_project! if project_id.nil? || flags[:clean]
|
24
|
+
prompt_task! if task_id.nil? || flags[:clean]
|
25
|
+
|
26
|
+
update_directory_config!
|
27
|
+
|
28
|
+
warn("Harvest configuration written to #{Abt::DirectoryConfig::FILE_NAME}")
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def update_directory_config!
|
34
|
+
cli.directory_config["harvest"] = { "path" => path.to_s }
|
35
|
+
cli.directory_config.save!
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|