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 +4 -4
- data/lib/abt/providers/devops/api.rb +18 -21
- data/lib/abt/providers/devops/base_command.rb +16 -19
- data/lib/abt/providers/devops/commands/branch_name.rb +1 -3
- data/lib/abt/providers/devops/commands/current.rb +2 -2
- data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +1 -3
- data/lib/abt/providers/devops/commands/pick.rb +2 -3
- data/lib/abt/providers/devops/commands/work_items.rb +2 -3
- data/lib/abt/providers/devops/commands/write_config.rb +3 -3
- data/lib/abt/providers/devops/path.rb +22 -6
- data/lib/abt/providers/devops/services/board_picker.rb +23 -12
- data/lib/abt/providers/devops/services/work_item_picker.rb +4 -3
- data/lib/abt/version.rb +1 -1
- metadata +2 -3
- data/lib/abt/providers/devops/commands/boards.rb +0 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7f0389f2300f5dc3f3143f6672d770a7dc50cce27b77774915eba3873cdfc89c
|
4
|
+
data.tar.gz: bc7952525895e85a0b2d0b0e679ebf98a687647e86bf2b4250d9dcb0abc1e4ac
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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:,
|
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
|
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
|
-
|
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
|
-
|
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(
|
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, :
|
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
|
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,
|
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/#{
|
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
|
-
|
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
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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:#{
|
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:#{
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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}(/#{
|
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,
|
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
|
-
|
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
|
36
|
-
match[:
|
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:,
|
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
|
-
|
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
|
-
|
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
|
-
|
46
|
+
def team
|
47
|
+
@team ||= cli.prompt.choice("Select a team", teams)
|
39
48
|
end
|
40
49
|
|
41
|
-
|
50
|
+
def teams
|
51
|
+
@teams ||= api.get_paged("/_apis/projects/#{path.project_name}/teams")
|
52
|
+
end
|
42
53
|
|
43
54
|
def boards
|
44
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
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"] ||
|
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
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.
|
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-
|
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
|