abt-cli 0.0.29 → 0.0.30

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1fbc512b85932a7c8cfa8e6d9ec10176f3e2ac8711df78ad4ce67100ac79f003
4
- data.tar.gz: 7dc1c727ced7d316c84075458b75839bd931050e50c9ffe39f925324c6a8e63d
3
+ metadata.gz: 7f0389f2300f5dc3f3143f6672d770a7dc50cce27b77774915eba3873cdfc89c
4
+ data.tar.gz: bc7952525895e85a0b2d0b0e679ebf98a687647e86bf2b4250d9dcb0abc1e4ac
5
5
  SHA512:
6
- metadata.gz: 74f84e028cf84f156ee18f46177fae4e17f857eb538362daf2baf3b017f1ace428a8b9c16aa912ba0c486fdc13cbf2a41402fa64917fa74ee9578c6e041c6d7b
7
- data.tar.gz: fe06b4314f9a119950979b7a8c966758a56b4a3619dcabc9328209beaca94f317851bc9dedbc986a955a120afe29da0707225e78896e1517854c24864bba2efc
6
+ metadata.gz: 484a395a41009899bb56e42049f0acb33e764ff8b4f54fbabdb5e7a468e663717f537a9b6e29934b02e6d668b7b6f3218fca35f6de3983ff32dd3e51a48ba7b3
7
+ data.tar.gz: 0d1651b94be6bbc6c93fb68fee3b1ca2dab62aa5160994b29b0009ee68f77090c3303b145cb3aae49305c5742ace9153a7392c637c566422cdec088923de61df
@@ -4,15 +4,22 @@ module Abt
4
4
  module Providers
5
5
  module Devops
6
6
  class Api
7
+ # Shamelessly copied from ERB::Util.url_encode
8
+ # https://apidock.com/ruby/ERB/Util/url_encode
9
+ def self.rfc_3986_encode_path_segment(string)
10
+ string.to_s.b.gsub(/[^a-zA-Z0-9_\-.~]/) do |match|
11
+ format("%%%02X", match.unpack1("C"))
12
+ end
13
+ end
14
+
7
15
  VERBS = [:get, :post, :put].freeze
8
16
 
9
17
  CONDITIONAL_ACCESS_POLICY_ERROR_CODE = "VS403463"
10
18
 
11
19
  attr_reader :organization_name, :project_name, :username, :access_token, :cli
12
20
 
13
- def initialize(organization_name:, project_name:, username:, access_token:, cli:)
21
+ def initialize(organization_name:, username:, access_token:, cli:)
14
22
  @organization_name = organization_name
15
- @project_name = project_name
16
23
  @username = username
17
24
  @access_token = access_token
18
25
  @cli = cli
@@ -32,12 +39,12 @@ module Abt
32
39
  end
33
40
 
34
41
  def work_item_query(wiql)
35
- response = post("wit/wiql", Oj.dump({ query: wiql }, mode: :json))
42
+ response = post("_apis/wit/wiql", Oj.dump({ query: wiql }, mode: :json))
36
43
  ids = response["workItems"].map { |work_item| work_item["id"] }
37
44
 
38
45
  work_items = []
39
46
  ids.each_slice(200) do |page_ids|
40
- work_items += get_paged("wit/workitems", ids: page_ids.join(","))
47
+ work_items += get_paged("_apis/wit/workitems", ids: page_ids.join(","))
41
48
  end
42
49
 
43
50
  work_items
@@ -58,19 +65,17 @@ module Abt
58
65
  end
59
66
 
60
67
  def base_url
61
- "https://#{organization_name}.visualstudio.com/#{project_name}"
62
- end
63
-
64
- def api_endpoint
65
- "#{base_url}/_apis"
68
+ "https://#{organization_name}.visualstudio.com"
66
69
  end
67
70
 
68
71
  def url_for_work_item(work_item)
69
- "#{base_url}/_workitems/edit/#{work_item['id']}"
72
+ project_name = self.class.rfc_3986_encode_path_segment(work_item["fields"]["System.TeamProject"])
73
+ "#{base_url}/#{project_name}/_workitems/edit/#{work_item['id']}"
70
74
  end
71
75
 
72
- def url_for_board(board)
73
- "#{base_url}/_boards/board/#{rfc_3986_encode_path_segment(board['name'])}"
76
+ def url_for_board(project_name, team_name, board)
77
+ board_name = self.class.rfc_3986_encode_path_segment(board["name"])
78
+ "#{base_url}/#{project_name}/_boards/board/t/#{team_name}/#{board_name}"
74
79
  end
75
80
 
76
81
  def sanitize_work_item(work_item)
@@ -84,7 +89,7 @@ module Abt
84
89
  end
85
90
 
86
91
  def connection
87
- @connection ||= Faraday.new(api_endpoint) do |connection|
92
+ @connection ||= Faraday.new(base_url) do |connection|
88
93
  connection.basic_auth(username, access_token)
89
94
  connection.headers["Content-Type"] = "application/json"
90
95
  connection.headers["Accept"] = "application/json; api-version=6.0"
@@ -93,14 +98,6 @@ module Abt
93
98
 
94
99
  private
95
100
 
96
- # Shamelessly copied from ERB::Util.url_encode
97
- # https://apidock.com/ruby/ERB/Util/url_encode
98
- def rfc_3986_encode_path_segment(string)
99
- string.to_s.b.gsub(/[^a-zA-Z0-9_\-.~]/) do |match|
100
- format("%%%02X", match.unpack1("C"))
101
- end
102
- end
103
-
104
101
  def handle_denied_by_conditional_access_policy!(exception)
105
102
  raise exception unless exception.message.include?(CONDITIONAL_ACCESS_POLICY_ERROR_CODE)
106
103
 
@@ -8,7 +8,7 @@ module Abt
8
8
 
9
9
  attr_reader :config, :path
10
10
 
11
- def_delegators(:@path, :organization_name, :project_name, :board_id, :work_item_id)
11
+ def_delegators(:@path, :organization_name, :project_name, :team_name, :board_name, :work_item_id)
12
12
 
13
13
  def initialize(ari:, cli:)
14
14
  super
@@ -24,7 +24,7 @@ module Abt
24
24
  end
25
25
 
26
26
  def require_board!
27
- return if board_id && organization_name && project_name
27
+ return if board_name && organization_name && project_name && team_name
28
28
 
29
29
  abort("No current/specified board. Did you forget to `pick`?")
30
30
  end
@@ -36,12 +36,8 @@ module Abt
36
36
  abort("No current/specified work item. Did you forget to `pick`?")
37
37
  end
38
38
 
39
- def prompt_project!
40
- @path = Services::ProjectPicker.call(cli: cli).path
41
- end
42
-
43
39
  def prompt_board!
44
- result = Services::BoardPicker.call(cli: cli, path: path, config: config)
40
+ result = Services::BoardPicker.call(cli: cli, config: config)
45
41
  @path = result.path
46
42
  @board = result.board
47
43
  end
@@ -54,7 +50,7 @@ module Abt
54
50
 
55
51
  def board
56
52
  @board ||= begin
57
- api.get("work/boards/#{board_id}")
53
+ api.get("#{project_name}/#{team_name}/_apis/work/boards/#{board_name}")
58
54
  rescue HttpError::NotFoundError
59
55
  nil
60
56
  end
@@ -62,33 +58,34 @@ module Abt
62
58
 
63
59
  def work_item
64
60
  @work_item ||= begin
65
- work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
61
+ work_item = api.get_paged("_apis/wit/workitems", ids: work_item_id)[0]
66
62
  api.sanitize_work_item(work_item)
67
63
  rescue HttpError::NotFoundError
68
64
  nil
69
65
  end
70
66
  end
71
67
 
72
- def print_board(organization_name, project_name, board)
73
- path = "#{organization_name}/#{project_name}/#{board['id']}"
68
+ def print_board(organization_name, project_name, team_name, board)
69
+ board_name = Api.rfc_3986_encode_path_segment(board["name"])
70
+ path = "#{organization_name}/#{project_name}/#{team_name}/#{board_name}"
74
71
 
75
72
  cli.print_ari("devops", path, board["name"])
76
- warn(api.url_for_board(board)) if cli.output.isatty
73
+ warn(api.url_for_board(project_name, team_name, board)) if cli.output.isatty
77
74
  end
78
75
 
79
- def print_work_item(organization, project, board, work_item)
80
- path = "#{organization}/#{project}/#{board['id']}/#{work_item['id']}"
76
+ def print_work_item(organization, project, team_name, board, work_item)
77
+ board_name = Api.rfc_3986_encode_path_segment(board["name"])
78
+ path = "#{organization}/#{project}/#{team_name}/#{board_name}/#{work_item['id']}"
81
79
 
82
80
  cli.print_ari("devops", path, work_item["name"])
83
81
  warn(work_item["url"]) if work_item.key?("url") && cli.output.isatty
84
82
  end
85
83
 
86
84
  def api
87
- Abt::Providers::Devops::Api.new(organization_name: organization_name,
88
- project_name: project_name,
89
- username: config.username_for_organization(organization_name),
90
- access_token: config.access_token_for_organization(organization_name),
91
- cli: cli)
85
+ Api.new(organization_name: organization_name,
86
+ username: config.username_for_organization(organization_name),
87
+ access_token: config.access_token_for_organization(organization_name),
88
+ cli: cli)
92
89
  end
93
90
  end
94
91
  end
@@ -19,11 +19,9 @@ module Abt
19
19
  if work_item
20
20
  puts name
21
21
  else
22
- args = [organization_name, project_name, board_id, work_item_id].compact
23
-
24
22
  abort(<<~TXT)
25
23
  Unable to find work item for configuration:
26
- devops:#{args.join('/')}
24
+ devops:#{path}
27
25
  TXT
28
26
  end
29
27
  end
@@ -30,9 +30,9 @@ module Abt
30
30
 
31
31
  def print_configuration
32
32
  if work_item_id.nil?
33
- print_board(organization_name, project_name, board)
33
+ print_board(organization_name, project_name, team_name, board)
34
34
  else
35
- print_work_item(organization_name, project_name, board, work_item)
35
+ print_work_item(organization_name, project_name, team_name, board, work_item)
36
36
  end
37
37
  end
38
38
 
@@ -19,11 +19,9 @@ module Abt
19
19
  if work_item
20
20
  puts Oj.dump(body, mode: :json)
21
21
  else
22
- args = [organization_name, project_name, board_id, work_item_id].compact
23
-
24
22
  abort(<<~TXT)
25
23
  Unable to find work item for configuration:
26
- devops:#{args.join('/')}
24
+ devops:#{path}
27
25
  TXT
28
26
  end
29
27
  end
@@ -23,7 +23,7 @@ module Abt
23
23
  def perform
24
24
  pick!
25
25
 
26
- print_work_item(organization_name, project_name, board, work_item)
26
+ print_work_item(organization_name, project_name, team_name, board, work_item)
27
27
 
28
28
  return if flags[:"dry-run"]
29
29
 
@@ -37,8 +37,7 @@ module Abt
37
37
  private
38
38
 
39
39
  def pick!
40
- prompt_project! if project_name.nil? || flags[:clean]
41
- prompt_board! if board_id.nil? || flags[:clean]
40
+ prompt_board! if board_name.nil? || flags[:clean]
42
41
  prompt_work_item!
43
42
  end
44
43
  end
@@ -14,11 +14,10 @@ module Abt
14
14
  end
15
15
 
16
16
  def perform
17
- prompt_project! unless project_name
18
- prompt_board! unless board_id
17
+ prompt_board! unless board_name
19
18
 
20
19
  work_items.each do |work_item|
21
- print_work_item(organization_name, project_name, board, work_item)
20
+ print_work_item(organization_name, project_name, team_name, board, work_item)
22
21
  end
23
22
  end
24
23
 
@@ -20,8 +20,7 @@ module Abt
20
20
  end
21
21
 
22
22
  def perform
23
- prompt_project! if project_name.nil? || flags[:clean]
24
- prompt_board! if board_id.nil? || flags[:clean]
23
+ prompt_board! if board_name.nil? || flags[:clean]
25
24
 
26
25
  update_directory_config!
27
26
 
@@ -35,7 +34,8 @@ module Abt
35
34
  "path" => Path.from_ids(
36
35
  organization_name: organization_name,
37
36
  project_name: project_name,
38
- board_id: board_id
37
+ team_name: team_name,
38
+ board_name: board_name
39
39
  ).to_s
40
40
  }
41
41
  cli.directory_config.save!
@@ -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
+ def self.from_ids(organization_name: nil, project_name: nil, team_name: nil, board_name: nil, work_item_id: nil)
16
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
@@ -20,36 +20,47 @@ module Abt
20
20
 
21
21
  attr_reader :cli, :config, :path
22
22
 
23
- def initialize(cli:, path:, config:)
23
+ def initialize(cli:, config:)
24
24
  @cli = cli
25
25
  @config = config
26
- @path = path
27
26
  end
28
27
 
29
28
  def call
29
+ @path = ProjectPicker.call(cli: cli).path
30
30
  board = cli.prompt.choice("Select a project work board", boards)
31
31
 
32
- path_with_board = Path.from_ids(
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(
33
39
  organization_name: path.organization_name,
34
40
  project_name: path.project_name,
35
- board_id: board["id"]
41
+ team_name: Api.rfc_3986_encode_path_segment(team["name"]),
42
+ board_name: Api.rfc_3986_encode_path_segment(board["name"])
36
43
  )
44
+ end
37
45
 
38
- Result.new(board: board, path: path_with_board)
46
+ def team
47
+ @team ||= cli.prompt.choice("Select a team", teams)
39
48
  end
40
49
 
41
- private
50
+ def teams
51
+ @teams ||= api.get_paged("/_apis/projects/#{path.project_name}/teams")
52
+ end
42
53
 
43
54
  def boards
44
- @boards ||= api.get_paged("work/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")
45
57
  end
46
58
 
47
59
  def api
48
- Abt::Providers::Devops::Api.new(organization_name: path.organization_name,
49
- project_name: path.project_name,
50
- username: config.username_for_organization(path.organization_name),
51
- access_token: config.access_token_for_organization(path.organization_name),
52
- cli: cli)
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)
53
64
  end
54
65
  end
55
66
  end
@@ -33,7 +33,8 @@ module Abt
33
33
  path_with_work_item = Path.from_ids(
34
34
  organization_name: path.organization_name,
35
35
  project_name: path.project_name,
36
- board_id: path.board_id,
36
+ team_name: path.team_name,
37
+ board_name: path.board_name,
37
38
  work_item_id: work_item["id"]
38
39
  )
39
40
 
@@ -79,14 +80,14 @@ module Abt
79
80
  end
80
81
 
81
82
  def columns
82
- board["columns"] || api.get("work/boards/#{path.board_id}")["columns"]
83
+ board["columns"] ||
84
+ api.get("#{path.project_name}/#{path.team_name}/_apis/work/boards/#{path.board_name}")["columns"]
83
85
  end
84
86
 
85
87
  private
86
88
 
87
89
  def api
88
90
  Abt::Providers::Devops::Api.new(organization_name: path.organization_name,
89
- project_name: path.project_name,
90
91
  username: config.username_for_organization(path.organization_name),
91
92
  access_token: config.access_token_for_organization(path.organization_name),
92
93
  cli: cli)
data/lib/abt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Abt
4
- VERSION = "0.0.29"
4
+ VERSION = "0.0.30"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: abt-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.29
4
+ version: 0.0.30
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jesper Sørensen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-28 00:00:00.000000000 Z
11
+ date: 2021-03-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-inflector
@@ -118,7 +118,6 @@ files:
118
118
  - "./lib/abt/providers/devops.rb"
119
119
  - "./lib/abt/providers/devops/api.rb"
120
120
  - "./lib/abt/providers/devops/base_command.rb"
121
- - "./lib/abt/providers/devops/commands/boards.rb"
122
121
  - "./lib/abt/providers/devops/commands/branch_name.rb"
123
122
  - "./lib/abt/providers/devops/commands/clear.rb"
124
123
  - "./lib/abt/providers/devops/commands/current.rb"
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Abt
4
- module Providers
5
- module Devops
6
- module Commands
7
- class Boards < BaseCommand
8
- def self.usage
9
- "abt boards devops"
10
- end
11
-
12
- def self.description
13
- "List all boards - useful for piping into grep etc"
14
- end
15
-
16
- def perform
17
- prompt_project! unless project_name
18
-
19
- boards.map do |board|
20
- print_board(organization_name, project_name, board)
21
- end
22
- end
23
-
24
- private
25
-
26
- def boards
27
- @boards ||= api.get_paged("work/boards")
28
- end
29
- end
30
- end
31
- end
32
- end
33
- end