abt-cli 0.0.22 → 0.0.27

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/bin/abt +2 -2
  3. data/lib/abt.rb +5 -0
  4. data/lib/abt/cli.rb +28 -9
  5. data/lib/abt/cli/prompt.rb +37 -53
  6. data/lib/abt/directory_config.rb +25 -0
  7. data/lib/abt/docs.rb +10 -6
  8. data/lib/abt/docs/markdown.rb +5 -2
  9. data/lib/abt/helpers.rb +26 -8
  10. data/lib/abt/providers/asana.rb +1 -0
  11. data/lib/abt/providers/asana/base_command.rb +37 -3
  12. data/lib/abt/providers/asana/commands/add.rb +0 -4
  13. data/lib/abt/providers/asana/commands/branch_name.rb +0 -13
  14. data/lib/abt/providers/asana/commands/current.rb +1 -20
  15. data/lib/abt/providers/asana/commands/finalize.rb +6 -2
  16. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +7 -5
  17. data/lib/abt/providers/asana/commands/pick.rb +12 -46
  18. data/lib/abt/providers/asana/commands/start.rb +9 -3
  19. data/lib/abt/providers/asana/commands/tasks.rb +2 -7
  20. data/lib/abt/providers/asana/configuration.rb +28 -12
  21. data/lib/abt/providers/asana/path.rb +1 -1
  22. data/lib/abt/providers/asana/services/project_picker.rb +54 -0
  23. data/lib/abt/providers/asana/services/task_picker.rb +83 -0
  24. data/lib/abt/providers/devops.rb +1 -0
  25. data/lib/abt/providers/devops/api.rb +10 -0
  26. data/lib/abt/providers/devops/base_command.rb +38 -14
  27. data/lib/abt/providers/devops/commands/boards.rb +1 -2
  28. data/lib/abt/providers/devops/commands/branch_name.rb +10 -16
  29. data/lib/abt/providers/devops/commands/current.rb +1 -21
  30. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +16 -20
  31. data/lib/abt/providers/devops/commands/pick.rb +18 -39
  32. data/lib/abt/providers/devops/commands/work_items.rb +3 -6
  33. data/lib/abt/providers/devops/configuration.rb +10 -14
  34. data/lib/abt/providers/devops/path.rb +4 -4
  35. data/lib/abt/providers/devops/services/board_picker.rb +54 -0
  36. data/lib/abt/providers/devops/services/project_picker.rb +79 -0
  37. data/lib/abt/providers/devops/services/work_item_picker.rb +93 -0
  38. data/lib/abt/providers/git/commands/branch.rb +7 -3
  39. data/lib/abt/providers/harvest.rb +1 -0
  40. data/lib/abt/providers/harvest/base_command.rb +49 -3
  41. data/lib/abt/providers/harvest/commands/current.rb +1 -30
  42. data/lib/abt/providers/harvest/commands/pick.rb +12 -23
  43. data/lib/abt/providers/harvest/commands/projects.rb +0 -5
  44. data/lib/abt/providers/harvest/commands/tasks.rb +1 -16
  45. data/lib/abt/providers/harvest/commands/track.rb +33 -19
  46. data/lib/abt/providers/harvest/configuration.rb +1 -1
  47. data/lib/abt/providers/harvest/path.rb +1 -1
  48. data/lib/abt/providers/harvest/services/project_picker.rb +53 -0
  49. data/lib/abt/providers/harvest/services/task_picker.rb +50 -0
  50. data/lib/abt/version.rb +1 -1
  51. metadata +10 -5
  52. data/lib/abt/providers/asana/commands/init.rb +0 -42
  53. data/lib/abt/providers/devops/commands/init.rb +0 -76
  54. data/lib/abt/providers/harvest/commands/init.rb +0 -54
@@ -2,6 +2,7 @@
2
2
 
3
3
  Dir.glob("#{File.expand_path(__dir__)}/devops/*.rb").sort.each { |file| require file }
4
4
  Dir.glob("#{File.expand_path(__dir__)}/devops/commands/*.rb").sort.each { |file| require file }
5
+ Dir.glob("#{File.expand_path(__dir__)}/devops/services/*.rb").sort.each { |file| require file }
5
6
 
6
7
  module Abt
7
8
  module Providers
@@ -73,6 +73,16 @@ module Abt
73
73
  "#{base_url}/_boards/board/#{rfc_3986_encode_path_segment(board['name'])}"
74
74
  end
75
75
 
76
+ def sanitize_work_item(work_item)
77
+ return nil if work_item.nil?
78
+
79
+ work_item.merge(
80
+ "id" => work_item["id"].to_s,
81
+ "name" => work_item["fields"]["System.Title"],
82
+ "url" => url_for_work_item(work_item)
83
+ )
84
+ end
85
+
76
86
  def connection
77
87
  @connection ||= Faraday.new(api_endpoint) do |connection|
78
88
  connection.basic_auth(username, access_token)
@@ -19,30 +19,54 @@ module Abt
19
19
 
20
20
  private
21
21
 
22
+ def require_local_config!
23
+ abort("Must be run inside a git repository") unless config.local_available?
24
+ end
25
+
22
26
  def require_board!
23
- return if organization_name && project_name && board_id
27
+ return if board_id && organization_name && project_name
24
28
 
25
- abort("No current/specified board. Did you initialize DevOps?")
29
+ abort("No current/specified board. Did you forget to `pick`?")
26
30
  end
27
31
 
28
32
  def require_work_item!
29
- unless organization_name && project_name && board_id
30
- abort("No current/specified board. Did you initialize DevOps and pick a work item?")
31
- end
32
-
33
+ require_board!
33
34
  return if work_item_id
34
35
 
35
- abort("No current/specified work item. Did you pick a DevOps work item?")
36
+ abort("No current/specified work item. Did you forget to `pick`?")
36
37
  end
37
38
 
38
- def sanitize_work_item(work_item)
39
- return nil if work_item.nil?
39
+ def prompt_project!
40
+ @path = Services::ProjectPicker.call(cli: cli).path
41
+ end
42
+
43
+ def prompt_board!
44
+ result = Services::BoardPicker.call(cli: cli, path: path, config: config)
45
+ @path = result.path
46
+ @board = result.board
47
+ end
40
48
 
41
- work_item.merge(
42
- "id" => work_item["id"].to_s,
43
- "name" => work_item["fields"]["System.Title"],
44
- "url" => api.url_for_work_item(work_item)
45
- )
49
+ def prompt_work_item!
50
+ result = Services::WorkItemPicker.call(cli: cli, path: path, config: config, board: board)
51
+ @path = result.path
52
+ @work_item = result.work_item
53
+ end
54
+
55
+ def board
56
+ @board ||= begin
57
+ api.get("work/boards/#{board_id}")
58
+ rescue HttpError::NotFoundError
59
+ nil
60
+ end
61
+ end
62
+
63
+ def work_item
64
+ @work_item ||= begin
65
+ work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
66
+ api.sanitize_work_item(work_item)
67
+ rescue HttpError::NotFoundError
68
+ nil
69
+ end
46
70
  end
47
71
 
48
72
  def print_board(organization_name, project_name, board)
@@ -14,8 +14,7 @@ module Abt
14
14
  end
15
15
 
16
16
  def perform
17
- abort("No organization selected. Did you initialize DevOps?") if organization_name.nil?
18
- abort("No project selected. Did you initialize DevOps?") if project_name.nil?
17
+ prompt_project! unless project_name
19
18
 
20
19
  boards.map do |board|
21
20
  print_board(organization_name, project_name, board)
@@ -16,15 +16,16 @@ module Abt
16
16
  def perform
17
17
  require_work_item!
18
18
 
19
- puts name
20
- rescue HttpError::NotFoundError
21
- args = [organization_name, project_name, board_id, work_item_id].compact
22
-
23
- error_message = [
24
- "Unable to find work item for configuration:",
25
- "devops:#{args.join('/')}"
26
- ].join("\n")
27
- abort(error_message)
19
+ if work_item
20
+ puts name
21
+ else
22
+ args = [organization_name, project_name, board_id, work_item_id].compact
23
+
24
+ abort(<<~TXT)
25
+ Unable to find work item for configuration:
26
+ devops:#{args.join('/')}
27
+ TXT
28
+ end
28
29
  end
29
30
 
30
31
  private
@@ -35,13 +36,6 @@ module Abt
35
36
  str += work_item["name"].downcase.gsub(/[^\w]/, "-")
36
37
  str.squeeze("-").gsub(/(^-|-$)/, "")
37
38
  end
38
-
39
- def work_item
40
- @work_item ||= begin
41
- work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
42
- sanitize_work_item(work_item)
43
- end
44
- end
45
39
  end
46
40
  end
47
41
  end
@@ -14,8 +14,7 @@ module Abt
14
14
  end
15
15
 
16
16
  def perform
17
- abort("Must be run inside a git repository") unless config.local_available?
18
-
17
+ require_local_config!
19
18
  require_board!
20
19
  ensure_valid_configuration!
21
20
 
@@ -43,25 +42,6 @@ module Abt
43
42
  end
44
43
  abort("No such work item: ##{work_item_id}") if work_item_id && work_item.nil?
45
44
  end
46
-
47
- def board
48
- @board ||= begin
49
- warn("Fetching board...")
50
- api.get("work/boards/#{board_id}")
51
- rescue HttpError::NotFoundError
52
- nil
53
- end
54
- end
55
-
56
- def work_item
57
- @work_item ||= begin
58
- warn("Fetching work item...")
59
- work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
60
- sanitize_work_item(work_item)
61
- rescue HttpError::NotFoundError
62
- nil
63
- end
64
- end
65
45
  end
66
46
  end
67
47
  end
@@ -16,7 +16,22 @@ module Abt
16
16
  def perform
17
17
  require_work_item!
18
18
 
19
- body = {
19
+ if work_item
20
+ puts Oj.dump(body, mode: :json)
21
+ else
22
+ args = [organization_name, project_name, board_id, work_item_id].compact
23
+
24
+ abort(<<~TXT)
25
+ Unable to find work item for configuration:
26
+ devops:#{args.join('/')}
27
+ TXT
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def body
34
+ {
20
35
  notes: notes,
21
36
  external_reference: {
22
37
  id: work_item["id"],
@@ -24,20 +39,8 @@ module Abt
24
39
  permalink: work_item["url"]
25
40
  }
26
41
  }
27
-
28
- puts Oj.dump(body, mode: :json)
29
- rescue HttpError::NotFoundError
30
- args = [organization_name, project_name, board_id, work_item_id].compact
31
-
32
- error_message = [
33
- "Unable to find work item for configuration:",
34
- "devops:#{args.join('/')}"
35
- ].join("\n")
36
- abort(error_message)
37
42
  end
38
43
 
39
- private
40
-
41
44
  def notes
42
45
  [
43
46
  "Azure DevOps",
@@ -47,13 +50,6 @@ module Abt
47
50
  work_item["name"]
48
51
  ].join(" ")
49
52
  end
50
-
51
- def work_item
52
- @work_item ||= begin
53
- work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
54
- sanitize_work_item(work_item)
55
- end
56
- end
57
53
  end
58
54
  end
59
55
  end
@@ -15,61 +15,40 @@ 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/board configuration"]
19
20
  ]
20
21
  end
21
22
 
22
23
  def perform
23
- abort("Must be run inside a git repository") unless config.local_available?
24
- require_board!
24
+ pick!
25
25
 
26
- warn("#{project_name} - #{board['name']}")
27
-
28
- work_item = select_work_item
29
26
  print_work_item(organization_name, project_name, board, work_item)
30
27
 
31
28
  return if flags[:"dry-run"]
32
29
 
33
- config.path = Path.from_ids(organization_name, project_name, board_id, work_item["id"])
30
+ if config.local_available?
31
+ update_config(work_item)
32
+ else
33
+ warn("No local configuration to update - will function as dry run")
34
+ end
34
35
  end
35
36
 
36
37
  private
37
38
 
38
- def select_work_item
39
- loop do
40
- column = cli.prompt.choice("Which column?", columns)
41
- warn("Fetching work items...")
42
- work_items = work_items_in_column(column)
43
-
44
- if work_items.length.zero?
45
- warn("Section is empty")
46
- next
47
- end
48
-
49
- work_item = cli.prompt.choice("Select a work item", work_items, nil_option: true)
50
- return work_item if work_item
51
- end
39
+ def pick!
40
+ prompt_project! if project_name.nil? || flags[:clean]
41
+ prompt_board! if board_id.nil? || flags[:clean]
42
+ prompt_work_item!
52
43
  end
53
44
 
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
45
+ def update_config(work_item)
46
+ config.path = Path.from_ids(
47
+ organization_name: organization_name,
48
+ project_name: project_name,
49
+ board_id: board_id,
50
+ work_item_id: work_item["id"]
62
51
  )
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
52
  end
74
53
  end
75
54
  end
@@ -14,7 +14,8 @@ module Abt
14
14
  end
15
15
 
16
16
  def perform
17
- require_board!
17
+ prompt_project! unless project_name
18
+ prompt_board! unless board_id
18
19
 
19
20
  work_items.each do |work_item|
20
21
  print_work_item(organization_name, project_name, board, work_item)
@@ -32,13 +33,9 @@ module Abt
32
33
  FROM WorkItems
33
34
  ORDER BY [System.Title] ASC
34
35
  WIQL
35
- ).map { |work_item| sanitize_work_item(work_item) }
36
+ ).map { |work_item| api.sanitize_work_item(work_item) }
36
37
  end
37
38
  end
38
-
39
- def board
40
- @board ||= api.get("work/boards/#{board_id}")
41
- end
42
39
  end
43
40
  end
44
41
  end
@@ -15,7 +15,7 @@ module Abt
15
15
  end
16
16
 
17
17
  def path
18
- Path.new(local_available? && git["path"] || "")
18
+ Path.new(local_available? && git["path"] || Abt.directory_config.dig("devops", "path") || "")
19
19
  end
20
20
 
21
21
  def path=(new_path)
@@ -47,7 +47,15 @@ module Abt
47
47
 
48
48
  return git_global[access_token_key] unless git_global[access_token_key].nil?
49
49
 
50
- git_global[access_token_key] = cli.prompt.text(access_token_prompt_text)
50
+ git_global[access_token_key] = cli.prompt.text(<<~TXT)
51
+ Please provide your personal access token for the DevOps organization (#{organization_name}).
52
+ 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
53
+
54
+ The token MUST have "Read" permission for Work Items
55
+ Future features will likely require "Write" or "Manage
56
+
57
+ Enter access token"
58
+ TXT
51
59
  end
52
60
 
53
61
  private
@@ -59,18 +67,6 @@ module Abt
59
67
  def git_global
60
68
  @git_global ||= GitConfig.new("global", "abt.devops")
61
69
  end
62
-
63
- def access_token_prompt_text
64
- <<~TXT
65
- Please provide your personal access token for the DevOps organization (#{organization_name}).
66
- 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
67
-
68
- The token MUST have "Read" permission for Work Items
69
- Future features will likely require "Write" or "Manage
70
-
71
- Enter access token"
72
- TXT
73
- end
74
70
  end
75
71
  end
76
72
  end
@@ -10,12 +10,12 @@ module Abt
10
10
  WORK_ITEM_ID_REGEX = /(?<work_item_id>\d+)/.freeze
11
11
 
12
12
  PATH_REGEX =
13
- %r{^(#{ORGANIZATION_NAME_REGEX}/#{PROJECT_NAME_REGEX}/#{BOARD_ID_REGEX})?(/#{WORK_ITEM_ID_REGEX})?}.freeze
13
+ %r{^(#{ORGANIZATION_NAME_REGEX}/#{PROJECT_NAME_REGEX}(/#{BOARD_ID_REGEX}(/#{WORK_ITEM_ID_REGEX})?)?)?}.freeze
14
14
 
15
- def self.from_ids(organization_id = nil, project_name = nil, board_id = nil, work_item_id = nil)
16
- return new unless organization_id && project_name && board_id
15
+ def self.from_ids(organization_name: nil, project_name: nil, board_id: nil, work_item_id: nil)
16
+ return new unless organization_name && project_name
17
17
 
18
- new([organization_id, project_name, board_id, *work_item_id].join("/"))
18
+ new([organization_name, project_name, *board_id, *work_item_id].join("/"))
19
19
  end
20
20
 
21
21
  def initialize(path = "")
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Services
7
+ class BoardPicker
8
+ class Result
9
+ attr_reader :board, :path
10
+
11
+ def initialize(board:, path:)
12
+ @board = board
13
+ @path = path
14
+ end
15
+ end
16
+
17
+ def self.call(**args)
18
+ new(**args).call
19
+ end
20
+
21
+ attr_reader :cli, :config, :path
22
+
23
+ def initialize(cli:, path:, config:)
24
+ @cli = cli
25
+ @config = config
26
+ @path = path
27
+ end
28
+
29
+ def call
30
+ board = cli.prompt.choice("Select a project work board", boards)
31
+
32
+ path_with_board = Path.new([path, board["id"]].join("/"))
33
+
34
+ Result.new(board: board, path: path_with_board)
35
+ end
36
+
37
+ private
38
+
39
+ def boards
40
+ @boards ||= api.get_paged("work/boards")
41
+ end
42
+
43
+ def api
44
+ Abt::Providers::Devops::Api.new(organization_name: path.organization_name,
45
+ project_name: path.project_name,
46
+ username: config.username_for_organization(path.organization_name),
47
+ access_token: config.access_token_for_organization(path.organization_name),
48
+ cli: cli)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end