abt-cli 0.0.22 → 0.0.27

Sign up to get free protection for your applications and to get access to all the features.
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