abt-cli 0.0.26 → 0.0.31

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/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