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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/abt.rb +0 -4
  3. data/lib/abt/cli.rb +5 -1
  4. data/lib/abt/directory_config.rb +28 -10
  5. data/lib/abt/docs.rb +10 -6
  6. data/lib/abt/providers/asana.rb +1 -0
  7. data/lib/abt/providers/asana/base_command.rb +33 -3
  8. data/lib/abt/providers/asana/commands/add.rb +0 -4
  9. data/lib/abt/providers/asana/commands/branch_name.rb +0 -13
  10. data/lib/abt/providers/asana/commands/clear.rb +1 -1
  11. data/lib/abt/providers/asana/commands/current.rb +0 -18
  12. data/lib/abt/providers/asana/commands/finalize.rb +2 -4
  13. data/lib/abt/providers/asana/commands/pick.rb +11 -41
  14. data/lib/abt/providers/asana/commands/tasks.rb +2 -7
  15. data/lib/abt/providers/asana/commands/write_config.rb +73 -0
  16. data/lib/abt/providers/asana/configuration.rb +1 -1
  17. data/lib/abt/providers/asana/path.rb +2 -2
  18. data/lib/abt/providers/asana/services/project_picker.rb +54 -0
  19. data/lib/abt/providers/asana/services/task_picker.rb +83 -0
  20. data/lib/abt/providers/devops.rb +1 -0
  21. data/lib/abt/providers/devops/api.rb +27 -20
  22. data/lib/abt/providers/devops/base_command.rb +42 -25
  23. data/lib/abt/providers/devops/commands/branch_name.rb +8 -16
  24. data/lib/abt/providers/devops/commands/clear.rb +1 -1
  25. data/lib/abt/providers/devops/commands/current.rb +2 -21
  26. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +8 -16
  27. data/lib/abt/providers/devops/commands/pick.rb +11 -60
  28. data/lib/abt/providers/devops/commands/work_items.rb +3 -7
  29. data/lib/abt/providers/devops/commands/write_config.rb +47 -0
  30. data/lib/abt/providers/devops/configuration.rb +1 -1
  31. data/lib/abt/providers/devops/path.rb +24 -8
  32. data/lib/abt/providers/devops/services/board_picker.rb +69 -0
  33. data/lib/abt/providers/devops/services/project_picker.rb +73 -0
  34. data/lib/abt/providers/devops/services/work_item_picker.rb +99 -0
  35. data/lib/abt/providers/harvest.rb +1 -0
  36. data/lib/abt/providers/harvest/base_command.rb +45 -3
  37. data/lib/abt/providers/harvest/commands/clear.rb +1 -1
  38. data/lib/abt/providers/harvest/commands/current.rb +0 -28
  39. data/lib/abt/providers/harvest/commands/pick.rb +12 -27
  40. data/lib/abt/providers/harvest/commands/projects.rb +2 -9
  41. data/lib/abt/providers/harvest/commands/tasks.rb +2 -19
  42. data/lib/abt/providers/harvest/commands/track.rb +72 -39
  43. data/lib/abt/providers/harvest/commands/write_config.rb +41 -0
  44. data/lib/abt/providers/harvest/configuration.rb +1 -1
  45. data/lib/abt/providers/harvest/harvest_helpers.rb +25 -0
  46. data/lib/abt/providers/harvest/path.rb +1 -1
  47. data/lib/abt/providers/harvest/services/project_picker.rb +53 -0
  48. data/lib/abt/providers/harvest/services/task_picker.rb +50 -0
  49. data/lib/abt/version.rb +1 -1
  50. metadata +13 -6
  51. data/lib/abt/providers/asana/commands/init.rb +0 -42
  52. data/lib/abt/providers/devops/commands/boards.rb +0 -34
  53. data/lib/abt/providers/devops/commands/init.rb +0 -79
  54. 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 initialize Harvest?")
29
+ abort("No current/specified project. Did you forget to run `pick`?")
30
30
  end
31
31
 
32
32
  def require_task!
33
- abort("No current/specified project. Did you initialize Harvest and pick a task?") unless project_id
33
+ require_project!
34
+ return if task_id
34
35
 
35
- abort("No current/specified task. Did you pick a Harvest task?") if task_id.nil?
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 at the same time") if flags[:global] && flags[:all]
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
- require_local_config!
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.path = Path.from_ids(project_id: project_id, task_id: task["id"])
34
- end
35
-
36
- private
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
- def tasks
47
- @tasks ||= project_assignment["task_assignments"].map { |ta| ta["task"] }
35
+ config.path = path
48
36
  end
49
37
 
50
- def project_assignment
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 project_assignments
57
- @project_assignments ||= api.get_paged("users/me/project_assignments")
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 ||= begin
26
- warn("Fetching projects...")
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
- require_project!
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 ||= begin
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
- "Set hours. Creates a stopped entry unless used with --running"],
24
- ["-r", "--running", "Used with --time, starts the created time entry"]
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
- print_task(created_time_entry["project"], created_time_entry["task"])
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 created_time_entry
41
- @created_time_entry ||= create_time_entry
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 create_time_entry
45
- body = time_entry_data
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
- result = api.post("time_entries", Oj.dump(body, mode: :json))
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/#{result['id']}/restart") if flags.key?(:time) && flags[:running]
64
+ api.patch("time_entries/#{previous_entry['id']}", Oj.dump({ hours: updated_hours }, mode: :json))
50
65
 
51
- result
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 time_entry_data
55
- body = time_entry_base_data
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
- maybe_add_time(body)
75
+ maybe_add_hours(body)
60
76
 
61
77
  body
62
78
  end
63
79
 
64
- def time_entry_base_data
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
- #{external_link_data[:notes]}
78
- #{external_link_data[:external_reference][:permalink]}
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
- lines = fetch_link_data_lines
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(lines.first, symbol_keys: true)
110
+ @external_link_data = Oj.load(link_data_lines.first, symbol_keys: true)
107
111
  end
108
112
 
109
- def fetch_link_data_lines
110
- other_aris = cli.aris - [ari]
111
- return [] if other_aris.empty?
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
- input = StringIO.new(other_aris.to_s)
114
- output = StringIO.new
115
- Abt::Cli.new(argv: ["harvest-time-entry-data"], output: output, input: input).perform
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
- output.string.strip.lines
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