abt-cli 0.0.7 → 0.0.12

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +1 -0
  3. data/lib/abt/cli.rb +5 -5
  4. data/lib/abt/cli/dialogs.rb +36 -14
  5. data/lib/abt/docs.rb +4 -0
  6. data/lib/abt/git_config.rb +13 -0
  7. data/lib/abt/providers/asana/base_command.rb +11 -0
  8. data/lib/abt/providers/asana/commands/add.rb +75 -0
  9. data/lib/abt/providers/asana/commands/current.rb +3 -3
  10. data/lib/abt/providers/asana/commands/finalize.rb +1 -1
  11. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +3 -4
  12. data/lib/abt/providers/asana/commands/init.rb +5 -0
  13. data/lib/abt/providers/asana/commands/pick.rb +17 -4
  14. data/lib/abt/providers/asana/commands/share.rb +3 -3
  15. data/lib/abt/providers/asana/commands/start.rb +1 -1
  16. data/lib/abt/providers/asana/commands/tasks.rb +2 -0
  17. data/lib/abt/providers/asana/configuration.rb +9 -3
  18. data/lib/abt/providers/devops.rb +19 -0
  19. data/lib/abt/providers/devops/api.rb +77 -0
  20. data/lib/abt/providers/devops/base_command.rb +97 -0
  21. data/lib/abt/providers/devops/commands/boards.rb +34 -0
  22. data/lib/abt/providers/devops/commands/clear.rb +24 -0
  23. data/lib/abt/providers/devops/commands/clear_global.rb +24 -0
  24. data/lib/abt/providers/devops/commands/current.rb +93 -0
  25. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +53 -0
  26. data/lib/abt/providers/devops/commands/init.rb +72 -0
  27. data/lib/abt/providers/devops/commands/pick.rb +78 -0
  28. data/lib/abt/providers/devops/commands/share.rb +26 -0
  29. data/lib/abt/providers/devops/commands/work-items.rb +46 -0
  30. data/lib/abt/providers/devops/configuration.rb +110 -0
  31. data/lib/abt/providers/harvest/base_command.rb +11 -0
  32. data/lib/abt/providers/harvest/commands/current.rb +3 -3
  33. data/lib/abt/providers/harvest/commands/pick.rb +1 -0
  34. data/lib/abt/providers/harvest/commands/start.rb +21 -65
  35. data/lib/abt/providers/harvest/commands/tasks.rb +2 -0
  36. data/lib/abt/providers/harvest/commands/track.rb +72 -0
  37. data/lib/abt/providers/harvest/configuration.rb +4 -3
  38. data/lib/abt/version.rb +1 -1
  39. metadata +17 -2
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class Pick < BaseCommand
8
+ def self.command
9
+ 'pick devops[:<organization-name>/<project-name>/<board-id>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Pick work item 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
+ require_board!
19
+
20
+ cli.warn "#{project_name} - #{board['name']}"
21
+
22
+ work_item = select_work_item
23
+
24
+ update_config!(work_item)
25
+
26
+ print_work_item(organization_name, project_name, board, work_item)
27
+ end
28
+
29
+ private
30
+
31
+ def update_config!(work_item)
32
+ config.organization_name = organization_name
33
+ config.project_name = project_name
34
+ config.board_id = board_id
35
+ config.work_item_id = work_item['id']
36
+ end
37
+
38
+ def select_work_item
39
+ loop do
40
+ column = cli.prompt_choice 'Which column?', columns
41
+ cli.warn 'Fetching work items...'
42
+ work_items = work_items_in_column(column)
43
+
44
+ if work_items.length.zero?
45
+ cli.warn 'Section is empty'
46
+ next
47
+ end
48
+
49
+ work_item = cli.prompt_choice 'Select a work item', work_items, true
50
+ return work_item if work_item
51
+ end
52
+ end
53
+
54
+ def work_items_in_column(column)
55
+ work_items = api.work_item_query(
56
+ <<~WIQL
57
+ SELECT [System.Id]
58
+ FROM WorkItems
59
+ WHERE [System.BoardColumn] = '#{column['name']}'
60
+ ORDER BY [Microsoft.VSTS.Common.BacklogPriority] ASC
61
+ WIQL
62
+ )
63
+
64
+ work_items.map { |work_item| sanitize_work_item(work_item) }
65
+ end
66
+
67
+ def columns
68
+ board['columns']
69
+ end
70
+
71
+ def board
72
+ @board ||= api.get("work/boards/#{board_id}")
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class Share < BaseCommand
8
+ def self.command
9
+ 'share devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]'
10
+ end
11
+
12
+ def self.description
13
+ 'Print DevOps config string'
14
+ end
15
+
16
+ def call
17
+ require_work_item!
18
+
19
+ args = [organization_name, project_name, board_id, work_item_id].compact
20
+ cli.print_provider_command('devops', args.join('/'))
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class WorkItems < BaseCommand
8
+ def self.command
9
+ 'work-items devops'
10
+ end
11
+
12
+ def self.description
13
+ 'List all work items on board - useful for piping into grep etc.'
14
+ end
15
+
16
+ def call
17
+ require_board!
18
+
19
+ work_items.each do |work_item|
20
+ print_work_item(organization_name, project_name, board, work_item)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def work_items
27
+ @work_items ||= begin
28
+ cli.warn 'Fetching work items...'
29
+ api.work_item_query(
30
+ <<~WIQL
31
+ SELECT [System.Id]
32
+ FROM WorkItems
33
+ ORDER BY [System.Title] ASC
34
+ WIQL
35
+ ).map { |work_item| sanitize_work_item(work_item) }
36
+ end
37
+ end
38
+
39
+ def board
40
+ @board ||= api.get("work/boards/#{board_id}")
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ class Configuration
7
+ attr_accessor :cli
8
+
9
+ def initialize(cli:)
10
+ @cli = cli
11
+ @git = GitConfig.new(namespace: 'abt.devops')
12
+ end
13
+
14
+ def local_available?
15
+ GitConfig.local_available?
16
+ end
17
+
18
+ def organization_name
19
+ local_available? ? git['organizationName'] : nil
20
+ end
21
+
22
+ def project_name
23
+ local_available? ? git['projectName'] : nil
24
+ end
25
+
26
+ def board_id
27
+ local_available? ? git['boardId'] : nil
28
+ end
29
+
30
+ def work_item_id
31
+ local_available? ? git['workItemId'] : nil
32
+ end
33
+
34
+ def organization_name=(value)
35
+ return if organization_name == value
36
+
37
+ clear_local
38
+ git['organizationName'] = value unless value.nil?
39
+ end
40
+
41
+ def project_name=(value)
42
+ return if project_name == value
43
+
44
+ git['projectName'] = value unless value.nil?
45
+ git['boardId'] = nil
46
+ git['workItemId'] = nil
47
+ end
48
+
49
+ def board_id=(value)
50
+ return if board_id == value
51
+
52
+ git['boardId'] = value unless value.nil?
53
+ git['workItemId'] = nil
54
+ end
55
+
56
+ def work_item_id=(value)
57
+ git['workItemId'] = value
58
+ end
59
+
60
+ def clear_local
61
+ cli.abort 'No local configuration was found' unless local_available?
62
+
63
+ git['organizationName'] = nil
64
+ git['projectName'] = nil
65
+ git['boardId'] = nil
66
+ git['workItemId'] = nil
67
+ end
68
+
69
+ def clear_global
70
+ git.global.keys.each do |key|
71
+ cli.puts 'Deleting configuration: ' + key
72
+ git.global[key] = nil
73
+ end
74
+ end
75
+
76
+ def username_for_organization(organization_name)
77
+ username_key = "organizations.#{organization_name}.username"
78
+
79
+ return git.global[username_key] unless git.global[username_key].nil?
80
+
81
+ git.global[username_key] = cli.prompt([
82
+ "Please provide your username for the DevOps organization (#{organization_name}).",
83
+ '',
84
+ 'Enter username'
85
+ ].join("\n"))
86
+ end
87
+
88
+ def access_token_for_organization(organization_name)
89
+ access_token_key = "organizations.#{organization_name}.accessToken"
90
+
91
+ return git.global[access_token_key] unless git.global[access_token_key].nil?
92
+
93
+ git.global[access_token_key] = cli.prompt([
94
+ "Please provide your personal access token for the DevOps organization (#{organization_name}).",
95
+ 'If you don\'t have one, follow the guide here: https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate',
96
+ '',
97
+ 'The token MUST have "Read" permission for Work Items',
98
+ 'Future features will likely require "Write" or "Manage"',
99
+ '',
100
+ 'Enter access token'
101
+ ].join("\n"))
102
+ end
103
+
104
+ private
105
+
106
+ attr_reader :git
107
+ end
108
+ end
109
+ end
110
+ end
@@ -20,6 +20,17 @@ module Abt
20
20
 
21
21
  private
22
22
 
23
+ def require_project!
24
+ cli.abort 'No current/specified project. Did you initialize Harvest?' if project_id.nil?
25
+ end
26
+
27
+ def require_task!
28
+ if project_id.nil?
29
+ cli.abort 'No current/specified project. Did you initialize Harvest and pick a task?'
30
+ end
31
+ cli.abort 'No current/specified task. Did you pick a Harvest task?' if task_id.nil?
32
+ end
33
+
23
34
  def same_args_as_config?
24
35
  project_id == config.project_id && task_id == config.task_id
25
36
  end
@@ -14,6 +14,8 @@ module Abt
14
14
  end
15
15
 
16
16
  def call
17
+ require_project!
18
+
17
19
  if same_args_as_config? || !config.local_available?
18
20
  show_current_configuration
19
21
  else
@@ -25,9 +27,7 @@ module Abt
25
27
  private
26
28
 
27
29
  def show_current_configuration
28
- if project_id.nil?
29
- cli.warn 'No project selected'
30
- elsif task_id.nil?
30
+ if task_id.nil?
31
31
  print_project(project)
32
32
  else
33
33
  print_task(project, task)
@@ -15,6 +15,7 @@ module Abt
15
15
 
16
16
  def call
17
17
  cli.abort 'Must be run inside a git repository' unless config.local_available?
18
+ require_project!
18
19
 
19
20
  cli.warn project['name']
20
21
  task = cli.prompt_choice 'Select a task', tasks
@@ -10,21 +10,16 @@ module Abt
10
10
  end
11
11
 
12
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
13
+ 'As track, but also lets the user override the current task and triggers `start` commands for other providers ' # rubocop:disable Layout/LineLength
14
14
  end
15
15
 
16
16
  def call
17
- abort 'No current/provided task' if task_id.nil?
17
+ track_output = call_track
18
+ puts track_output
18
19
 
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
20
+ use_arg_str(arg_str_from_track_output(track_output))
26
21
 
27
- cli.warn 'Tracker successfully started'
22
+ maybe_override_current_task
28
23
  rescue Abt::HttpError::HttpError => e
29
24
  cli.warn e
30
25
  cli.abort 'Unable to start tracker'
@@ -32,67 +27,28 @@ module Abt
32
27
 
33
28
  private
34
29
 
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
30
+ def arg_str_from_track_output(output)
31
+ output = output.split(' # ').first
32
+ output.split(':')[1]
42
33
  end
43
34
 
44
- def create_time_entry
45
- body = {
46
- project_id: project_id,
47
- task_id: task_id,
48
- user_id: config.user_id,
49
- spent_date: Date.today.iso8601
50
- }
51
-
52
- if external_link_data
53
- body.merge! external_link_data
54
- else
55
- cli.warn 'No external link provided'
56
- body[:notes] ||= cli.prompt('Fill in comment (optional)')
57
- end
58
-
59
- api.post('time_entries', Oj.dump(body, mode: :json))
60
- end
35
+ def call_track
36
+ input = StringIO.new(cli.args.join(' '))
37
+ output = StringIO.new
38
+ Abt::Cli.new(argv: ['track'], output: output, input: input).perform
61
39
 
62
- def project
63
- project_assignment['project']
40
+ output.string.strip
64
41
  end
65
42
 
66
- def task
67
- @task ||= project_assignment['task_assignments'].map { |ta| ta['task'] }.find do |task|
68
- task['id'].to_s == task_id
69
- end
70
- end
71
-
72
- def project_assignment
73
- @project_assignment ||= begin
74
- project_assignments.find { |pa| pa['project']['id'].to_s == project_id }
75
- end
76
- end
77
-
78
- def project_assignments
79
- @project_assignments ||= api.get_paged('users/me/project_assignments')
80
- end
81
-
82
- def external_link_data
83
- @external_link_data ||= begin
84
- arg_strs = cli.args.join(' ')
85
- lines = `#{$PROGRAM_NAME} harvest-time-entry-data #{arg_strs}`.split("\n")
86
-
87
- return if lines.empty?
88
-
89
- # TODO: Make user choose which reference to use by printing the urls
90
- if lines.length > 1
91
- cli.abort('Multiple providers had harvest reference data, only one is supported at a time') # rubocop:disable Layout/LineLength
92
- end
43
+ def maybe_override_current_task
44
+ return if arg_str.nil?
45
+ return if same_args_as_config?
46
+ return unless config.local_available?
47
+ return unless cli.prompt_boolean 'Set selected task as current?'
93
48
 
94
- Oj.load(lines.first)
95
- end
49
+ input = StringIO.new("harvest:#{project_id}/#{task_id}")
50
+ output = StringIO.new
51
+ Abt::Cli.new(argv: ['current'], output: output, input: input).perform
96
52
  end
97
53
  end
98
54
  end
@@ -14,6 +14,8 @@ module Abt
14
14
  end
15
15
 
16
16
  def call
17
+ require_project!
18
+
17
19
  tasks.each do |task|
18
20
  print_task(project, task)
19
21
  end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Harvest
6
+ module Commands
7
+ class Track < BaseCommand
8
+ def self.command
9
+ 'track 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
+ require_task!
18
+
19
+ print_task(created_time_entry['project'], created_time_entry['task'])
20
+
21
+ cli.warn 'Tracker successfully started'
22
+ rescue Abt::HttpError::HttpError => e
23
+ cli.abort 'Invalid task'
24
+ end
25
+
26
+ private
27
+
28
+ def created_time_entry
29
+ @created_time_entry ||= create_time_entry
30
+ end
31
+
32
+ def create_time_entry
33
+ body = {
34
+ project_id: project_id,
35
+ task_id: task_id,
36
+ user_id: config.user_id,
37
+ spent_date: Date.today.iso8601
38
+ }
39
+
40
+ if external_link_data
41
+ body.merge! external_link_data
42
+ else
43
+ cli.warn 'No external link provided'
44
+ body[:notes] ||= cli.prompt('Fill in comment (optional)')
45
+ end
46
+
47
+ api.post('time_entries', Oj.dump(body, mode: :json))
48
+ end
49
+
50
+ def external_link_data
51
+ @external_link_data ||= begin
52
+ input = StringIO.new(cli.args.join(' '))
53
+ output = StringIO.new
54
+ Abt::Cli.new(argv: ['harvest-time-entry-data'], output: output, input: input).perform
55
+
56
+ lines = output.string.strip.lines
57
+
58
+ return if lines.empty?
59
+
60
+ # TODO: Make user choose which reference to use by printing the urls
61
+ if lines.length > 1
62
+ cli.abort('Multiple providers had harvest reference data, only one is supported at a time') # rubocop:disable Layout/LineLength
63
+ end
64
+
65
+ Oj.load(lines.first)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end