abt-cli 0.0.3 → 0.0.4

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +1 -0
  3. data/lib/abt/cli.rb +16 -7
  4. data/lib/abt/cli/dialogs.rb +18 -2
  5. data/lib/abt/cli/io.rb +8 -6
  6. data/lib/abt/docs.rb +12 -5
  7. data/lib/abt/docs/cli.rb +1 -1
  8. data/lib/abt/docs/markdown.rb +1 -1
  9. data/lib/abt/git_config.rb +55 -49
  10. data/lib/abt/providers/asana/api.rb +1 -1
  11. data/lib/abt/providers/asana/base_command.rb +9 -4
  12. data/lib/abt/providers/asana/commands/current.rb +10 -4
  13. data/lib/abt/providers/asana/commands/finalize.rb +71 -0
  14. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +2 -2
  15. data/lib/abt/providers/asana/commands/init.rb +8 -3
  16. data/lib/abt/providers/asana/commands/{pick_task.rb → pick.rb} +13 -6
  17. data/lib/abt/providers/asana/commands/projects.rb +9 -2
  18. data/lib/abt/providers/asana/commands/share.rb +29 -0
  19. data/lib/abt/providers/asana/commands/start.rb +51 -6
  20. data/lib/abt/providers/asana/commands/tasks.rb +4 -1
  21. data/lib/abt/providers/asana/configuration.rb +54 -34
  22. data/lib/abt/providers/harvest.rb +9 -51
  23. data/lib/abt/providers/harvest/api.rb +62 -0
  24. data/lib/abt/providers/harvest/base_command.rb +12 -16
  25. data/lib/abt/providers/harvest/commands/clear.rb +26 -0
  26. data/lib/abt/providers/harvest/commands/clear_global.rb +24 -0
  27. data/lib/abt/providers/harvest/commands/current.rb +81 -0
  28. data/lib/abt/providers/harvest/commands/init.rb +66 -0
  29. data/lib/abt/providers/harvest/commands/pick.rb +49 -0
  30. data/lib/abt/providers/harvest/commands/projects.rb +34 -0
  31. data/lib/abt/providers/harvest/commands/share.rb +29 -0
  32. data/lib/abt/providers/harvest/commands/start.rb +81 -0
  33. data/lib/abt/providers/harvest/commands/stop.rb +58 -0
  34. data/lib/abt/providers/harvest/commands/tasks.rb +38 -0
  35. data/lib/abt/providers/harvest/configuration.rb +90 -0
  36. data/lib/abt/version.rb +1 -1
  37. metadata +17 -14
  38. data/lib/abt/harvest_client.rb +0 -58
  39. data/lib/abt/providers/asana/commands/move.rb +0 -56
  40. data/lib/abt/providers/harvest/clear.rb +0 -24
  41. data/lib/abt/providers/harvest/clear_global.rb +0 -24
  42. data/lib/abt/providers/harvest/current.rb +0 -79
  43. data/lib/abt/providers/harvest/init.rb +0 -61
  44. data/lib/abt/providers/harvest/pick_task.rb +0 -45
  45. data/lib/abt/providers/harvest/projects.rb +0 -29
  46. data/lib/abt/providers/harvest/start.rb +0 -58
  47. data/lib/abt/providers/harvest/stop.rb +0 -51
  48. data/lib/abt/providers/harvest/tasks.rb +0 -36
@@ -2,12 +2,13 @@
2
2
 
3
3
  module Abt
4
4
  module Providers
5
- class Harvest
5
+ module Harvest
6
6
  class BaseCommand
7
- attr_reader :arg_str, :project_id, :task_id, :cli
7
+ attr_reader :arg_str, :project_id, :task_id, :cli, :config
8
8
 
9
9
  def initialize(arg_str:, cli:)
10
10
  @arg_str = arg_str
11
+ @config = Configuration.new(cli: cli)
11
12
 
12
13
  if arg_str.nil?
13
14
  use_current_args
@@ -19,6 +20,10 @@ module Abt
19
20
 
20
21
  private
21
22
 
23
+ def same_args_as_config?
24
+ project_id == config.project_id && task_id == config.task_id
25
+ end
26
+
22
27
  def print_project(project)
23
28
  cli.print_provider_command(
24
29
  'harvest',
@@ -36,10 +41,8 @@ module Abt
36
41
  end
37
42
 
38
43
  def use_current_args
39
- @project_id = Abt::GitConfig.local('abt.harvest.projectId').to_s
40
- @project_id = nil if project_id.empty?
41
- @task_id = Abt::GitConfig.local('abt.harvest.taskId').to_s
42
- @task_id = nil if task_id.empty?
44
+ @project_id = config.project_id
45
+ @task_id = config.task_id
43
46
  end
44
47
 
45
48
  def use_arg_str(arg_str)
@@ -53,16 +56,9 @@ module Abt
53
56
  @task_id = nil if @task_id.empty?
54
57
  end
55
58
 
56
- def remember_project_id(project_id)
57
- Abt::GitConfig.local('abt.harvest.projectId', project_id)
58
- end
59
-
60
- def remember_task_id(task_id)
61
- if task_id.nil?
62
- Abt::GitConfig.unset_local('abt.harvest.taskId')
63
- else
64
- Abt::GitConfig.local('abt.harvest.taskId', task_id)
65
- end
59
+ def api
60
+ @api ||= Abt::Providers::Harvest::Api.new(access_token: config.access_token,
61
+ account_id: config.account_id)
66
62
  end
67
63
  end
68
64
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Harvest
6
+ module Commands
7
+ class Clear < BaseCommand
8
+ def self.command
9
+ 'clear harvest'
10
+ end
11
+
12
+ def self.description
13
+ 'Clear project/task for current git repository'
14
+ end
15
+
16
+ def initialize(**); end
17
+
18
+ def call
19
+ cli.warn 'Clearing Harvest project configuration'
20
+ config.clear
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Harvest
6
+ module Commands
7
+ class ClearGlobal < BaseCommand
8
+ def self.command
9
+ 'clear-global harvest'
10
+ end
11
+
12
+ def self.description
13
+ 'Clear all global configuration'
14
+ end
15
+
16
+ def call
17
+ cli.warn 'Clearing Harvest project configuration'
18
+ config.clear_global
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Harvest
6
+ module Commands
7
+ class Current < BaseCommand
8
+ def self.command
9
+ 'current harvest[:<project-id>[/<task-id>]]'
10
+ end
11
+
12
+ def self.description
13
+ 'Get or set project and or task for current git repository'
14
+ end
15
+
16
+ def call
17
+ if same_args_as_config? || !config.local_available?
18
+ show_current_configuration
19
+ else
20
+ cli.warn 'Updating configuration'
21
+ update_configuration
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def show_current_configuration
28
+ if project_id.nil?
29
+ cli.warn 'No project selected'
30
+ elsif task_id.nil?
31
+ print_project(project)
32
+ else
33
+ print_task(project, task)
34
+ end
35
+ end
36
+
37
+ def update_configuration
38
+ ensure_project_is_valid!
39
+ config.project_id = project_id
40
+
41
+ if task_id.nil?
42
+ print_project(project)
43
+ config.task_id = nil
44
+ else
45
+ ensure_task_is_valid!
46
+ config.task_id = task_id
47
+
48
+ print_task(project, task)
49
+ end
50
+ end
51
+
52
+ def ensure_project_is_valid!
53
+ cli.abort "Invalid project: #{project_id}" if project.nil?
54
+ end
55
+
56
+ def ensure_task_is_valid!
57
+ cli.abort "Invalid task: #{task_id}" if task.nil?
58
+ end
59
+
60
+ def project
61
+ @project ||= api.get("projects/#{project_id}")
62
+ end
63
+
64
+ def task
65
+ project_task_assignments
66
+ .map { |assignment| assignment['task'] }
67
+ .find { |task| task['id'].to_s == task_id }
68
+ end
69
+
70
+ def project_task_assignments
71
+ @project_task_assignments ||= begin
72
+ api.get_paged("projects/#{project_id}/task_assignments", is_active: true)
73
+ rescue Abt::HttpError::HttpError # rubocop:disable Layout/RescueEnsureAlignment
74
+ []
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Harvest
6
+ module Commands
7
+ class Init < BaseCommand
8
+ def self.command
9
+ 'init harvest'
10
+ end
11
+
12
+ def self.description
13
+ 'Pick Harvest project for current git repository'
14
+ end
15
+
16
+ def call
17
+ cli.abort 'Must be run inside a git repository' unless config.local_available?
18
+
19
+ projects # Load projects up front to make it obvious that searches are instant
20
+ project = find_search_result
21
+
22
+ config.project_id = project['id']
23
+ config.task_id = nil
24
+
25
+ print_project(project)
26
+ end
27
+
28
+ private
29
+
30
+ def find_search_result
31
+ loop do
32
+ matches = matches_for_string cli.prompt('Enter search')
33
+ if matches.empty?
34
+ warn 'No matches'
35
+ next
36
+ end
37
+
38
+ cli.warn 'Showing the 10 first matches' if matches.size > 10
39
+ choice = cli.prompt_choice 'Select a project', matches[0...10], true
40
+ break choice unless choice.nil?
41
+ end
42
+ end
43
+
44
+ def matches_for_string(string)
45
+ search_string = sanitize_string(string)
46
+
47
+ projects.select do |project|
48
+ sanitize_string(project['name']).include?(search_string)
49
+ end
50
+ end
51
+
52
+ def sanitize_string(string)
53
+ string.downcase.gsub(/[^\w]/, '')
54
+ end
55
+
56
+ def projects
57
+ @projects ||= begin
58
+ cli.warn 'Fetching projects...'
59
+ api.get_paged('projects')
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Harvest
6
+ module Commands
7
+ class Pick < BaseCommand
8
+ def self.command
9
+ 'pick harvest[:<project-id>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Pick task for current git repository'
14
+ end
15
+
16
+ def call
17
+ cli.abort 'Must be run inside a git repository' unless config.local_available?
18
+
19
+ cli.warn project['name']
20
+ task = cli.prompt_choice 'Select a task', tasks
21
+
22
+ config.project_id = project_id # We might have gotten the project ID as an argument
23
+ config.task_id = task['id']
24
+
25
+ print_task(project, task)
26
+ end
27
+
28
+ private
29
+
30
+ def project
31
+ @project ||= api.get("projects/#{project_id}")
32
+ end
33
+
34
+ def tasks
35
+ project_task_assignments.map { |assignment| assignment['task'] }
36
+ end
37
+
38
+ def project_task_assignments
39
+ @project_task_assignments ||= begin
40
+ api.get_paged("projects/#{project_id}/task_assignments", is_active: true)
41
+ rescue Abt::HttpError::HttpError # rubocop:disable Layout/RescueEnsureAlignment
42
+ []
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Harvest
6
+ module Commands
7
+ class Projects < BaseCommand
8
+ def self.command
9
+ 'projects harvest'
10
+ end
11
+
12
+ def self.description
13
+ 'List all available projects - useful for piping into grep etc.'
14
+ end
15
+
16
+ def call
17
+ projects.map do |project|
18
+ print_project(project)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def projects
25
+ @projects ||= begin
26
+ cli.warn 'Fetching projects...'
27
+ api.get_paged('projects', is_active: true)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Harvest
6
+ module Commands
7
+ class Share < BaseCommand
8
+ def self.command
9
+ 'share harvest[:<project-id>[/<task-id>]]'
10
+ end
11
+
12
+ def self.description
13
+ 'Print project/task config string'
14
+ end
15
+
16
+ def call
17
+ if project_id.nil?
18
+ cli.warn 'No project selected'
19
+ elsif task_id.nil?
20
+ cli.print_provider_command('harvest', project_id)
21
+ else
22
+ cli.print_provider_command('harvest', "#{project_id}/#{task_id}")
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Harvest
6
+ module Commands
7
+ class Start < BaseCommand
8
+ def self.command
9
+ 'start harvest[:<project-id>/<task-id>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Start tracker for current or specified task. Add a relevant provider to link the time entry: E.g. `abt start harvest asana`' # rubocop:disable Layout/LineLength
14
+ end
15
+
16
+ def call
17
+ abort 'No current/provided task' if task_id.nil?
18
+
19
+ maybe_override_current_task
20
+
21
+ print_task(project, task)
22
+
23
+ cli.abort('No task selected') if task_id.nil?
24
+
25
+ create_time_entry
26
+
27
+ cli.warn 'Tracker successfully started'
28
+ rescue Abt::HttpError::HttpError => e
29
+ cli.warn e
30
+ cli.abort 'Unable to start tracker'
31
+ end
32
+
33
+ private
34
+
35
+ def maybe_override_current_task
36
+ return if arg_str.nil?
37
+ return if same_args_as_config?
38
+ return unless config.local_available?
39
+
40
+ should_override = cli.prompt_boolean 'Set selected task as current?'
41
+ Current.new(arg_str: arg_str, cli: cli).call if should_override
42
+ end
43
+
44
+ def create_time_entry
45
+ body = Oj.dump({
46
+ project_id: project_id,
47
+ task_id: task_id,
48
+ user_id: config.user_id,
49
+ spent_date: Date.today.iso8601
50
+ }.merge(external_link_data), mode: :json)
51
+ api.post('time_entries', body)
52
+ end
53
+
54
+ def project
55
+ @project ||= api.get("projects/#{project_id}")
56
+ end
57
+
58
+ def task
59
+ @task ||= api.get("tasks/#{task_id}")
60
+ end
61
+
62
+ def external_link_data
63
+ @external_link_data ||= begin
64
+ arg_strs = cli.args.join(' ')
65
+ lines = `#{$PROGRAM_NAME} harvest-time-entry-data #{arg_strs}`.split("\n")
66
+
67
+ return {} if lines.empty?
68
+
69
+ # TODO: Make user choose which reference to use by printing the urls
70
+ if lines.length > 1
71
+ cli.abort('Multiple providers had harvest reference data, only one is supported at a time') # rubocop:disable Layout/LineLength
72
+ end
73
+
74
+ Oj.load(lines.first)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end