abt-cli 0.0.26 → 0.0.31
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 +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
|