abt-cli 0.0.20 → 0.0.25
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/abt +3 -3
- data/lib/abt.rb +6 -6
- data/lib/abt/ari.rb +2 -2
- data/lib/abt/ari_list.rb +1 -1
- data/lib/abt/base_command.rb +7 -7
- data/lib/abt/cli.rb +49 -47
- data/lib/abt/cli/arguments_parser.rb +6 -3
- data/lib/abt/cli/global_commands.rb +23 -0
- data/lib/abt/cli/global_commands/commands.rb +23 -0
- data/lib/abt/cli/global_commands/examples.rb +23 -0
- data/lib/abt/cli/global_commands/help.rb +23 -0
- data/lib/abt/cli/global_commands/readme.rb +23 -0
- data/lib/abt/cli/global_commands/share.rb +36 -0
- data/lib/abt/cli/global_commands/version.rb +23 -0
- data/lib/abt/cli/prompt.rb +71 -56
- data/lib/abt/docs.rb +48 -26
- data/lib/abt/docs/cli.rb +3 -3
- data/lib/abt/docs/markdown.rb +10 -7
- data/lib/abt/git_config.rb +4 -6
- data/lib/abt/helpers.rb +26 -8
- data/lib/abt/providers/asana/api.rb +9 -9
- data/lib/abt/providers/asana/base_command.rb +12 -10
- data/lib/abt/providers/asana/commands/add.rb +13 -12
- data/lib/abt/providers/asana/commands/branch_name.rb +8 -8
- data/lib/abt/providers/asana/commands/clear.rb +7 -8
- data/lib/abt/providers/asana/commands/current.rb +14 -15
- data/lib/abt/providers/asana/commands/finalize.rb +17 -14
- data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +18 -16
- data/lib/abt/providers/asana/commands/init.rb +8 -41
- data/lib/abt/providers/asana/commands/pick.rb +22 -26
- data/lib/abt/providers/asana/commands/projects.rb +5 -5
- data/lib/abt/providers/asana/commands/share.rb +7 -5
- data/lib/abt/providers/asana/commands/start.rb +28 -21
- data/lib/abt/providers/asana/commands/tasks.rb +6 -6
- data/lib/abt/providers/asana/configuration.rb +37 -29
- data/lib/abt/providers/asana/path.rb +6 -6
- data/lib/abt/providers/devops/api.rb +12 -12
- data/lib/abt/providers/devops/base_command.rb +14 -10
- data/lib/abt/providers/devops/commands/boards.rb +5 -7
- data/lib/abt/providers/devops/commands/branch_name.rb +9 -9
- data/lib/abt/providers/devops/commands/clear.rb +7 -8
- data/lib/abt/providers/devops/commands/current.rb +17 -18
- data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +21 -19
- data/lib/abt/providers/devops/commands/init.rb +21 -14
- data/lib/abt/providers/devops/commands/pick.rb +37 -19
- data/lib/abt/providers/devops/commands/share.rb +7 -5
- data/lib/abt/providers/devops/commands/{work-items.rb → work_items.rb} +3 -3
- data/lib/abt/providers/devops/configuration.rb +15 -15
- data/lib/abt/providers/devops/path.rb +7 -6
- data/lib/abt/providers/git/commands/branch.rb +23 -21
- data/lib/abt/providers/harvest/api.rb +8 -8
- data/lib/abt/providers/harvest/base_command.rb +10 -8
- data/lib/abt/providers/harvest/commands/clear.rb +7 -8
- data/lib/abt/providers/harvest/commands/current.rb +13 -14
- data/lib/abt/providers/harvest/commands/init.rb +10 -39
- data/lib/abt/providers/harvest/commands/pick.rb +15 -11
- data/lib/abt/providers/harvest/commands/projects.rb +5 -5
- data/lib/abt/providers/harvest/commands/share.rb +7 -5
- data/lib/abt/providers/harvest/commands/start.rb +5 -3
- data/lib/abt/providers/harvest/commands/stop.rb +12 -12
- data/lib/abt/providers/harvest/commands/tasks.rb +7 -7
- data/lib/abt/providers/harvest/commands/track.rb +52 -37
- data/lib/abt/providers/harvest/configuration.rb +18 -18
- data/lib/abt/providers/harvest/path.rb +6 -6
- data/lib/abt/version.rb +1 -1
- metadata +12 -5
@@ -4,9 +4,9 @@ module Abt
|
|
4
4
|
module Providers
|
5
5
|
module Devops
|
6
6
|
class Api
|
7
|
-
VERBS =
|
7
|
+
VERBS = [:get, :post, :put].freeze
|
8
8
|
|
9
|
-
CONDITIONAL_ACCESS_POLICY_ERROR_CODE =
|
9
|
+
CONDITIONAL_ACCESS_POLICY_ERROR_CODE = "VS403463"
|
10
10
|
|
11
11
|
attr_reader :organization_name, :project_name, :username, :access_token, :cli
|
12
12
|
|
@@ -26,18 +26,18 @@ module Abt
|
|
26
26
|
|
27
27
|
def get_paged(path, query = {})
|
28
28
|
result = request(:get, path, query)
|
29
|
-
result[
|
29
|
+
result["value"]
|
30
30
|
|
31
31
|
# TODO: Loop if necessary
|
32
32
|
end
|
33
33
|
|
34
34
|
def work_item_query(wiql)
|
35
|
-
response = post(
|
36
|
-
ids = response[
|
35
|
+
response = post("wit/wiql", Oj.dump({ query: wiql }, mode: :json))
|
36
|
+
ids = response["workItems"].map { |work_item| work_item["id"] }
|
37
37
|
|
38
38
|
work_items = []
|
39
39
|
ids.each_slice(200) do |page_ids|
|
40
|
-
work_items += get_paged(
|
40
|
+
work_items += get_paged("wit/workitems", ids: page_ids.join(","))
|
41
41
|
end
|
42
42
|
|
43
43
|
work_items
|
@@ -50,7 +50,7 @@ module Abt
|
|
50
50
|
Oj.load(response.body)
|
51
51
|
else
|
52
52
|
error_class = Abt::HttpError.error_class_for_status(response.status)
|
53
|
-
encoded_response_body = response.body.force_encoding(
|
53
|
+
encoded_response_body = response.body.force_encoding("utf-8")
|
54
54
|
raise error_class, "Code: #{response.status}, body: #{encoded_response_body}"
|
55
55
|
end
|
56
56
|
rescue Abt::HttpError::ForbiddenError => e
|
@@ -75,9 +75,9 @@ module Abt
|
|
75
75
|
|
76
76
|
def connection
|
77
77
|
@connection ||= Faraday.new(api_endpoint) do |connection|
|
78
|
-
connection.basic_auth
|
79
|
-
connection.headers[
|
80
|
-
connection.headers[
|
78
|
+
connection.basic_auth(username, access_token)
|
79
|
+
connection.headers["Content-Type"] = "application/json"
|
80
|
+
connection.headers["Accept"] = "application/json; api-version=6.0"
|
81
81
|
end
|
82
82
|
end
|
83
83
|
|
@@ -87,14 +87,14 @@ module Abt
|
|
87
87
|
# https://apidock.com/ruby/ERB/Util/url_encode
|
88
88
|
def rfc_3986_encode_path_segment(string)
|
89
89
|
string.to_s.b.gsub(/[^a-zA-Z0-9_\-.~]/) do |match|
|
90
|
-
format(
|
90
|
+
format("%%%02X", match.unpack1("C"))
|
91
91
|
end
|
92
92
|
end
|
93
93
|
|
94
94
|
def handle_denied_by_conditional_access_policy!(exception)
|
95
95
|
raise exception unless exception.message.include?(CONDITIONAL_ACCESS_POLICY_ERROR_CODE)
|
96
96
|
|
97
|
-
cli.abort
|
97
|
+
cli.abort(<<~TXT)
|
98
98
|
Access denied by conditional access policy.
|
99
99
|
Try logging into the board using the URL below, then retry the command.
|
100
100
|
|
@@ -19,44 +19,48 @@ module Abt
|
|
19
19
|
|
20
20
|
private
|
21
21
|
|
22
|
+
def require_local_config!
|
23
|
+
abort("Must be run inside a git repository") unless config.local_available?
|
24
|
+
end
|
25
|
+
|
22
26
|
def require_board!
|
23
27
|
return if organization_name && project_name && board_id
|
24
28
|
|
25
|
-
abort
|
29
|
+
abort("No current/specified board. Did you initialize DevOps?")
|
26
30
|
end
|
27
31
|
|
28
32
|
def require_work_item!
|
29
33
|
unless organization_name && project_name && board_id
|
30
|
-
abort
|
34
|
+
abort("No current/specified board. Did you initialize DevOps and pick a work item?")
|
31
35
|
end
|
32
36
|
|
33
37
|
return if work_item_id
|
34
38
|
|
35
|
-
abort
|
39
|
+
abort("No current/specified work item. Did you pick a DevOps work item?")
|
36
40
|
end
|
37
41
|
|
38
42
|
def sanitize_work_item(work_item)
|
39
43
|
return nil if work_item.nil?
|
40
44
|
|
41
45
|
work_item.merge(
|
42
|
-
|
43
|
-
|
44
|
-
|
46
|
+
"id" => work_item["id"].to_s,
|
47
|
+
"name" => work_item["fields"]["System.Title"],
|
48
|
+
"url" => api.url_for_work_item(work_item)
|
45
49
|
)
|
46
50
|
end
|
47
51
|
|
48
52
|
def print_board(organization_name, project_name, board)
|
49
53
|
path = "#{organization_name}/#{project_name}/#{board['id']}"
|
50
54
|
|
51
|
-
cli.print_ari(
|
52
|
-
warn
|
55
|
+
cli.print_ari("devops", path, board["name"])
|
56
|
+
warn(api.url_for_board(board)) if cli.output.isatty
|
53
57
|
end
|
54
58
|
|
55
59
|
def print_work_item(organization, project, board, work_item)
|
56
60
|
path = "#{organization}/#{project}/#{board['id']}/#{work_item['id']}"
|
57
61
|
|
58
|
-
cli.print_ari(
|
59
|
-
warn
|
62
|
+
cli.print_ari("devops", path, work_item["name"])
|
63
|
+
warn(work_item["url"]) if work_item.key?("url") && cli.output.isatty
|
60
64
|
end
|
61
65
|
|
62
66
|
def api
|
@@ -6,18 +6,16 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Boards < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
9
|
+
"abt boards devops"
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
|
13
|
+
"List all boards - useful for piping into grep etc"
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
|
-
if organization_name.nil?
|
18
|
-
|
19
|
-
end
|
20
|
-
abort 'No project selected. Did you initialize DevOps?' if project_name.nil?
|
17
|
+
abort("No organization selected. Did you initialize DevOps?") if organization_name.nil?
|
18
|
+
abort("No project selected. Did you initialize DevOps?") if project_name.nil?
|
21
19
|
|
22
20
|
boards.map do |board|
|
23
21
|
print_board(organization_name, project_name, board)
|
@@ -27,7 +25,7 @@ module Abt
|
|
27
25
|
private
|
28
26
|
|
29
27
|
def boards
|
30
|
-
@boards ||= api.get_paged(
|
28
|
+
@boards ||= api.get_paged("work/boards")
|
31
29
|
end
|
32
30
|
end
|
33
31
|
end
|
@@ -6,11 +6,11 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class BranchName < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
9
|
+
"abt branch-name devops[:<organization-name>/<project-name>/<board-id>/<work-item-id>]"
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
|
13
|
+
"Suggest a git branch name for the current/specified work-item."
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
@@ -21,24 +21,24 @@ module Abt
|
|
21
21
|
args = [organization_name, project_name, board_id, work_item_id].compact
|
22
22
|
|
23
23
|
error_message = [
|
24
|
-
|
24
|
+
"Unable to find work item for configuration:",
|
25
25
|
"devops:#{args.join('/')}"
|
26
26
|
].join("\n")
|
27
|
-
abort
|
27
|
+
abort(error_message)
|
28
28
|
end
|
29
29
|
|
30
30
|
private
|
31
31
|
|
32
32
|
def name
|
33
|
-
str = work_item[
|
34
|
-
str +=
|
35
|
-
str += work_item[
|
36
|
-
str.gsub(
|
33
|
+
str = work_item["id"]
|
34
|
+
str += "-"
|
35
|
+
str += work_item["name"].downcase.gsub(/[^\w]/, "-")
|
36
|
+
str.squeeze("-").gsub(/(^-|-$)/, "")
|
37
37
|
end
|
38
38
|
|
39
39
|
def work_item
|
40
40
|
@work_item ||= begin
|
41
|
-
work_item = api.get_paged(
|
41
|
+
work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
|
42
42
|
sanitize_work_item(work_item)
|
43
43
|
end
|
44
44
|
end
|
@@ -6,29 +6,28 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Clear < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
9
|
+
"abt clear devops"
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
|
13
|
+
"Clear DevOps configuration"
|
14
14
|
end
|
15
15
|
|
16
16
|
def self.flags
|
17
17
|
[
|
18
|
-
[
|
19
|
-
|
18
|
+
["-g", "--global",
|
19
|
+
"Clear global instead of local DevOp configuration (credentials etc.)"],
|
20
|
+
["-a", "--all", "Clear all DevOp configuration"]
|
20
21
|
]
|
21
22
|
end
|
22
23
|
|
23
24
|
def perform
|
24
|
-
if flags[:global] && flags[:all]
|
25
|
-
abort('Flags --global and --all cannot be used at the same time')
|
26
|
-
end
|
25
|
+
abort("Flags --global and --all cannot be used at the same time") if flags[:global] && flags[:all]
|
27
26
|
|
28
27
|
config.clear_local unless flags[:global]
|
29
28
|
config.clear_global if flags[:global] || flags[:all]
|
30
29
|
|
31
|
-
warn
|
30
|
+
warn("Configuration cleared")
|
32
31
|
end
|
33
32
|
end
|
34
33
|
end
|
@@ -6,22 +6,21 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Current < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
9
|
+
"abt current devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]"
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
|
13
|
+
"Get or set DevOps configuration for current git repository"
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
|
-
|
18
|
-
|
17
|
+
require_local_config!
|
19
18
|
require_board!
|
20
19
|
ensure_valid_configuration!
|
21
20
|
|
22
21
|
if path != config.path && config.local_available?
|
23
22
|
config.path = path
|
24
|
-
warn
|
23
|
+
warn("Configuration updated")
|
25
24
|
end
|
26
25
|
|
27
26
|
print_configuration
|
@@ -39,28 +38,28 @@ module Abt
|
|
39
38
|
|
40
39
|
def ensure_valid_configuration!
|
41
40
|
if board.nil?
|
42
|
-
abort
|
41
|
+
abort("Board could not be found, ensure that settings for organization, project, and board are correct")
|
43
42
|
end
|
44
|
-
abort
|
43
|
+
abort("No such work item: ##{work_item_id}") if work_item_id && work_item.nil?
|
45
44
|
end
|
46
45
|
|
47
46
|
def board
|
48
47
|
@board ||= begin
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
48
|
+
warn("Fetching board...")
|
49
|
+
api.get("work/boards/#{board_id}")
|
50
|
+
rescue HttpError::NotFoundError
|
51
|
+
nil
|
52
|
+
end
|
54
53
|
end
|
55
54
|
|
56
55
|
def work_item
|
57
56
|
@work_item ||= begin
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
57
|
+
warn("Fetching work item...")
|
58
|
+
work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
|
59
|
+
sanitize_work_item(work_item)
|
60
|
+
rescue HttpError::NotFoundError
|
61
|
+
nil
|
62
|
+
end
|
64
63
|
end
|
65
64
|
end
|
66
65
|
end
|
@@ -6,51 +6,53 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class HarvestTimeEntryData < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
9
|
+
"abt harvest-time-entry-data devops[:<organization-name>/<project-name>/<board-id>/<work-item-id>]"
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
|
13
|
+
"Print Harvest time entry data for DevOps work item as json. Used by harvest start script."
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
17
|
require_work_item!
|
18
18
|
|
19
|
-
body = {
|
20
|
-
notes: notes,
|
21
|
-
external_reference: {
|
22
|
-
id: work_item['id'],
|
23
|
-
group_id: 'AzureDevOpsWorkItem',
|
24
|
-
permalink: work_item['url']
|
25
|
-
}
|
26
|
-
}
|
27
|
-
|
28
19
|
puts Oj.dump(body, mode: :json)
|
29
20
|
rescue HttpError::NotFoundError
|
30
21
|
args = [organization_name, project_name, board_id, work_item_id].compact
|
31
22
|
|
32
23
|
error_message = [
|
33
|
-
|
24
|
+
"Unable to find work item for configuration:",
|
34
25
|
"devops:#{args.join('/')}"
|
35
26
|
].join("\n")
|
36
|
-
abort
|
27
|
+
abort(error_message)
|
37
28
|
end
|
38
29
|
|
39
30
|
private
|
40
31
|
|
32
|
+
def body
|
33
|
+
{
|
34
|
+
notes: notes,
|
35
|
+
external_reference: {
|
36
|
+
id: work_item["id"],
|
37
|
+
group_id: "AzureDevOpsWorkItem",
|
38
|
+
permalink: work_item["url"]
|
39
|
+
}
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
41
43
|
def notes
|
42
44
|
[
|
43
|
-
|
44
|
-
work_item[
|
45
|
+
"Azure DevOps",
|
46
|
+
work_item["fields"]["System.WorkItemType"],
|
45
47
|
"##{work_item['id']}",
|
46
|
-
|
47
|
-
work_item[
|
48
|
-
].join(
|
48
|
+
"-",
|
49
|
+
work_item["name"]
|
50
|
+
].join(" ")
|
49
51
|
end
|
50
52
|
|
51
53
|
def work_item
|
52
54
|
@work_item ||= begin
|
53
|
-
work_item = api.get_paged(
|
55
|
+
work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
|
54
56
|
sanitize_work_item(work_item)
|
55
57
|
end
|
56
58
|
end
|
@@ -9,26 +9,29 @@ module Abt
|
|
9
9
|
VS_URL_REGEX = %r{^https://(?<organization>[^.]+)\.visualstudio\.com/(?<project>[^/]+)}.freeze
|
10
10
|
|
11
11
|
def self.usage
|
12
|
-
|
12
|
+
"abt init devops"
|
13
13
|
end
|
14
14
|
|
15
15
|
def self.description
|
16
|
-
|
16
|
+
"Pick DevOps board for current git repository"
|
17
17
|
end
|
18
18
|
|
19
19
|
def perform
|
20
|
-
|
20
|
+
require_local_config!
|
21
|
+
board = cli.prompt.choice("Select a project work board", boards)
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
|
23
|
+
config.path = Path.from_ids(
|
24
|
+
organization_name: organization_name,
|
25
|
+
project_name: project_name,
|
26
|
+
board_id: board["id"]
|
27
|
+
)
|
25
28
|
print_board(organization_name, project_name, board)
|
26
29
|
end
|
27
30
|
|
28
31
|
private
|
29
32
|
|
30
33
|
def boards
|
31
|
-
@boards ||= api.get_paged(
|
34
|
+
@boards ||= api.get_paged("work/boards")
|
32
35
|
end
|
33
36
|
|
34
37
|
def project_name
|
@@ -52,19 +55,23 @@ module Abt
|
|
52
55
|
def project_url
|
53
56
|
@project_url ||= begin
|
54
57
|
loop do
|
55
|
-
url = cli.prompt.text(
|
56
|
-
'Please provide the URL for the devops project',
|
57
|
-
'For instance https://{organization}.visualstudio.com/{project} or https://dev.azure.com/{organization}/{project}',
|
58
|
-
'',
|
59
|
-
'Enter URL'
|
60
|
-
].join("\n"))
|
58
|
+
url = cli.prompt.text(project_url_prompt_text)
|
61
59
|
|
62
60
|
break url if AZURE_DEV_URL_REGEX =~ url || VS_URL_REGEX =~ url
|
63
61
|
|
64
|
-
warn
|
62
|
+
warn("Invalid URL")
|
65
63
|
end
|
66
64
|
end
|
67
65
|
end
|
66
|
+
|
67
|
+
def project_url_prompt_text
|
68
|
+
<<~TXT
|
69
|
+
Please provide the URL for the devops project
|
70
|
+
For instance https://{organization}.visualstudio.com/{project} or https://dev.azure.com/{organization}/{project}
|
71
|
+
|
72
|
+
Enter URL
|
73
|
+
TXT
|
74
|
+
end
|
68
75
|
end
|
69
76
|
end
|
70
77
|
end
|