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