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
@@ -16,15 +16,14 @@ module Abt
16
16
  def perform
17
17
  require_work_item!
18
18
 
19
- puts Oj.dump(body, mode: :json)
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 Oj.dump(body, mode: :json)
21
+ else
22
+ abort(<<~TXT)
23
+ Unable to find work item for configuration:
24
+ devops:#{path}
25
+ TXT
26
+ end
28
27
  end
29
28
 
30
29
  private
@@ -49,13 +48,6 @@ module Abt
49
48
  work_item["name"]
50
49
  ].join(" ")
51
50
  end
52
-
53
- def work_item
54
- @work_item ||= begin
55
- work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
56
- sanitize_work_item(work_item)
57
- end
58
- end
59
51
  end
60
52
  end
61
53
  end
@@ -15,79 +15,30 @@ 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
- require_local_config!
24
- require_board!
24
+ pick!
25
25
 
26
- warn("#{project_name} - #{board['name']}")
27
-
28
- work_item = select_work_item
29
- print_work_item(organization_name, project_name, board, work_item)
26
+ print_work_item(organization_name, project_name, team_name, board, work_item)
30
27
 
31
28
  return if flags[:"dry-run"]
32
29
 
33
- update_config(work_item)
34
- end
35
-
36
- private
37
-
38
- def update_config(work_item)
39
- config.path = Path.from_ids(
40
- organization_name: organization_name,
41
- project_name: project_name,
42
- board_id: board_id,
43
- work_item_id: work_item["id"]
44
- )
45
- end
46
-
47
- def select_work_item
48
- column = cli.prompt.choice("Which column?", columns)
49
- warn("Fetching work items...")
50
- work_items = work_items_in_column(column)
51
-
52
- if work_items.length.zero?
53
- warn("Section is empty")
54
- select_work_item
30
+ if config.local_available?
31
+ config.path = path
55
32
  else
56
- prompt_work_item(work_items) || select_work_item
33
+ warn("No local configuration to update - will function as dry run")
57
34
  end
58
35
  end
59
36
 
60
- def prompt_work_item(work_items)
61
- options = work_items.map do |work_item|
62
- {
63
- "id" => work_item["id"],
64
- "name" => "##{work_item['id']} #{work_item['name']}"
65
- }
66
- end
67
-
68
- choice = cli.prompt.choice("Select a work item", options, nil_option: true)
69
- choice && work_items.find { |work_item| work_item["id"] == choice["id"] }
70
- end
71
-
72
- def work_items_in_column(column)
73
- work_items = api.work_item_query(
74
- <<~WIQL
75
- SELECT [System.Id]
76
- FROM WorkItems
77
- WHERE [System.BoardColumn] = '#{column['name']}'
78
- ORDER BY [Microsoft.VSTS.Common.BacklogPriority] ASC
79
- WIQL
80
- )
81
-
82
- work_items.map { |work_item| sanitize_work_item(work_item) }
83
- end
84
-
85
- def columns
86
- board["columns"]
87
- end
37
+ private
88
38
 
89
- def board
90
- @board ||= api.get("work/boards/#{board_id}")
39
+ def pick!
40
+ prompt_board! if board_name.nil? || flags[:clean]
41
+ prompt_work_item!
91
42
  end
92
43
  end
93
44
  end
@@ -14,10 +14,10 @@ module Abt
14
14
  end
15
15
 
16
16
  def perform
17
- require_board!
17
+ prompt_board! unless board_name
18
18
 
19
19
  work_items.each do |work_item|
20
- print_work_item(organization_name, project_name, board, work_item)
20
+ print_work_item(organization_name, project_name, team_name, board, work_item)
21
21
  end
22
22
  end
23
23
 
@@ -32,13 +32,9 @@ module Abt
32
32
  FROM WorkItems
33
33
  ORDER BY [System.Title] ASC
34
34
  WIQL
35
- ).map { |work_item| sanitize_work_item(work_item) }
35
+ ).map { |work_item| api.sanitize_work_item(work_item) }
36
36
  end
37
37
  end
38
-
39
- def board
40
- @board ||= api.get("work/boards/#{board_id}")
41
- end
42
38
  end
43
39
  end
44
40
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class WriteConfig < BaseCommand
8
+ def self.usage
9
+ "abt write-config devops[:<organization-name>/<project-name>/<board-id>]"
10
+ end
11
+
12
+ def self.description
13
+ "Write DevOps 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_board! if board_name.nil? || flags[:clean]
24
+
25
+ update_directory_config!
26
+
27
+ warn("DevOps configuration written to #{Abt::DirectoryConfig::FILE_NAME}")
28
+ end
29
+
30
+ private
31
+
32
+ def update_directory_config!
33
+ cli.directory_config["devops"] = {
34
+ "path" => Path.from_ids(
35
+ organization_name: organization_name,
36
+ project_name: project_name,
37
+ team_name: team_name,
38
+ board_name: board_name
39
+ ).to_s
40
+ }
41
+ cli.directory_config.save!
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -15,7 +15,7 @@ module Abt
15
15
  end
16
16
 
17
17
  def path
18
- Path.new(local_available? && git["path"] || Abt.directory_config.dig("devops", "path") || "")
18
+ Path.new(local_available? && git["path"] || cli.directory_config.dig("devops", "path") || "")
19
19
  end
20
20
 
21
21
  def path=(new_path)
@@ -6,16 +6,28 @@ module Abt
6
6
  class Path < String
7
7
  ORGANIZATION_NAME_REGEX = %r{(?<organization_name>[^/ ]+)}.freeze
8
8
  PROJECT_NAME_REGEX = %r{(?<project_name>[^/ ]+)}.freeze
9
- BOARD_ID_REGEX = /(?<board_id>[a-z0-9\-]+)/.freeze
9
+ TEAM_NAME_REGEX = %r{(?<team_name>[^/ ]+)}.freeze
10
+ BOARD_NAME_REGEX = %r{(?<board_name>[^/ ]+)}.freeze
10
11
  WORK_ITEM_ID_REGEX = /(?<work_item_id>\d+)/.freeze
11
12
 
12
13
  PATH_REGEX =
13
- %r{^(#{ORGANIZATION_NAME_REGEX}/#{PROJECT_NAME_REGEX}(/#{BOARD_ID_REGEX}(/#{WORK_ITEM_ID_REGEX})?)?)?}.freeze
14
+ %r{^(#{ORGANIZATION_NAME_REGEX}/#{PROJECT_NAME_REGEX}(/#{TEAM_NAME_REGEX}(/#{BOARD_NAME_REGEX}(/#{WORK_ITEM_ID_REGEX})?)?)?)?}.freeze # rubocop:disable Layout/LineLength
14
15
 
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 && board_id
16
+ def self.from_ids(organization_name: nil, project_name: nil, team_name: nil, board_name: nil, work_item_id: nil)
17
+ return new unless organization_name && project_name
17
18
 
18
- new([organization_name, project_name, board_id, *work_item_id].join("/"))
19
+ parts = [organization_name, project_name]
20
+
21
+ if team_name
22
+ parts << team_name
23
+
24
+ if board_name
25
+ parts << board_name
26
+ parts << work_item_id if work_item_id
27
+ end
28
+ end
29
+
30
+ new(parts.join("/"))
19
31
  end
20
32
 
21
33
  def initialize(path = "")
@@ -32,8 +44,12 @@ module Abt
32
44
  match[:project_name]
33
45
  end
34
46
 
35
- def board_id
36
- match[:board_id]
47
+ def team_name
48
+ match[:team_name]
49
+ end
50
+
51
+ def board_name
52
+ match[:board_name]
37
53
  end
38
54
 
39
55
  def work_item_id
@@ -43,7 +59,7 @@ module Abt
43
59
  private
44
60
 
45
61
  def match
46
- @match ||= PATH_REGEX.match(self)
62
+ @match ||= PATH_REGEX.match(to_s)
47
63
  end
48
64
  end
49
65
  end
@@ -0,0 +1,69 @@
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:, config:)
24
+ @cli = cli
25
+ @config = config
26
+ end
27
+
28
+ def call
29
+ @path = ProjectPicker.call(cli: cli).path
30
+ board = cli.prompt.choice("Select a project work board", boards)
31
+
32
+ Result.new(board: board, path: path_with_board(team, board))
33
+ end
34
+
35
+ private
36
+
37
+ def path_with_board(team, board)
38
+ Path.from_ids(
39
+ organization_name: path.organization_name,
40
+ project_name: path.project_name,
41
+ team_name: Api.rfc_3986_encode_path_segment(team["name"]),
42
+ board_name: Api.rfc_3986_encode_path_segment(board["name"])
43
+ )
44
+ end
45
+
46
+ def team
47
+ @team ||= cli.prompt.choice("Select a team", teams)
48
+ end
49
+
50
+ def teams
51
+ @teams ||= api.get_paged("/_apis/projects/#{path.project_name}/teams")
52
+ end
53
+
54
+ def boards
55
+ team_name = Api.rfc_3986_encode_path_segment(team["name"])
56
+ @boards ||= api.get_paged("#{path.project_name}/#{team_name}/_apis/work/boards")
57
+ end
58
+
59
+ def api
60
+ Api.new(organization_name: path.organization_name,
61
+ username: config.username_for_organization(path.organization_name),
62
+ access_token: config.access_token_for_organization(path.organization_name),
63
+ cli: cli)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Services
7
+ class ProjectPicker
8
+ class Result
9
+ attr_reader :board, :path
10
+
11
+ def initialize(path:)
12
+ @path = path
13
+ end
14
+ end
15
+
16
+ AZURE_DEV_URL_REGEX = %r{^https://dev\.azure\.com/(?<organization>[^/]+)/(?<project>[^/]+)}.freeze
17
+ VS_URL_REGEX = %r{^https://(?<organization>[^.]+)\.visualstudio\.com/(?<project>[^/]+)}.freeze
18
+
19
+ extend Forwardable
20
+
21
+ def self.call(**args)
22
+ new(**args).call
23
+ end
24
+
25
+ attr_reader :cli
26
+
27
+ def initialize(cli:)
28
+ @cli = cli
29
+ end
30
+
31
+ def call
32
+ Result.new(
33
+ path: Path.from_ids(organization_name: organization_name, project_name: project_name)
34
+ )
35
+ end
36
+
37
+ private
38
+
39
+ def project_name
40
+ @project_name ||= project_url_match && project_url_match[:project]
41
+ end
42
+
43
+ def organization_name
44
+ @organization_name ||= project_url_match && project_url_match[:organization]
45
+ end
46
+
47
+ def project_url_match
48
+ AZURE_DEV_URL_REGEX.match(project_url) || VS_URL_REGEX.match(project_url)
49
+ end
50
+
51
+ def project_url
52
+ @project_url ||= loop do
53
+ url = prompt_url
54
+
55
+ break url if AZURE_DEV_URL_REGEX =~ url || VS_URL_REGEX =~ url
56
+
57
+ cli.warn("Invalid URL")
58
+ end
59
+ end
60
+
61
+ def prompt_url
62
+ cli.prompt.text(<<~TXT)
63
+ Please provide the URL for the devops project
64
+ For instance https://{organization}.visualstudio.com/{project} or https://dev.azure.com/{organization}/{project}
65
+
66
+ Enter URL
67
+ TXT
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Services
7
+ class WorkItemPicker
8
+ class Result
9
+ attr_reader :work_item, :path
10
+
11
+ def initialize(work_item:, path:)
12
+ @work_item = work_item
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, :board
22
+
23
+ def initialize(cli:, path:, config:, board:)
24
+ @cli = cli
25
+ @config = config
26
+ @path = path
27
+ @board = board
28
+ end
29
+
30
+ def call
31
+ work_item = select_work_item
32
+
33
+ path_with_work_item = Path.from_ids(
34
+ organization_name: path.organization_name,
35
+ project_name: path.project_name,
36
+ team_name: path.team_name,
37
+ board_name: path.board_name,
38
+ work_item_id: work_item["id"]
39
+ )
40
+
41
+ Result.new(work_item: work_item, path: path_with_work_item)
42
+ end
43
+
44
+ def select_work_item
45
+ column = cli.prompt.choice("Which column in #{board['name']}?", columns)
46
+ cli.warn("Fetching work items...")
47
+ work_items = work_items_in_column(column)
48
+
49
+ if work_items.length.zero?
50
+ cli.warn("Section is empty")
51
+ select_work_item
52
+ else
53
+ prompt_work_item(work_items) || select_work_item
54
+ end
55
+ end
56
+
57
+ def prompt_work_item(work_items)
58
+ options = work_items.map do |work_item|
59
+ {
60
+ "id" => work_item["id"],
61
+ "name" => "##{work_item['id']} #{work_item['name']}"
62
+ }
63
+ end
64
+
65
+ choice = cli.prompt.choice("Select a work item", options, nil_option: true)
66
+ choice && work_items.find { |work_item| work_item["id"] == choice["id"] }
67
+ end
68
+
69
+ def work_items_in_column(column)
70
+ work_items = api.work_item_query(
71
+ <<~WIQL
72
+ SELECT [System.Id]
73
+ FROM WorkItems
74
+ WHERE [System.BoardColumn] = '#{column['name']}'
75
+ ORDER BY [Microsoft.VSTS.Common.BacklogPriority] ASC
76
+ WIQL
77
+ )
78
+
79
+ work_items.map { |work_item| api.sanitize_work_item(work_item) }
80
+ end
81
+
82
+ def columns
83
+ board["columns"] ||
84
+ api.get("#{path.project_name}/#{path.team_name}/_apis/work/boards/#{path.board_name}")["columns"]
85
+ end
86
+
87
+ private
88
+
89
+ def api
90
+ Abt::Providers::Devops::Api.new(organization_name: path.organization_name,
91
+ username: config.username_for_organization(path.organization_name),
92
+ access_token: config.access_token_for_organization(path.organization_name),
93
+ cli: cli)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end