abt-cli 0.0.19 → 0.0.24
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/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 +64 -52
- 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 -18
- 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 +25 -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,15 +4,15 @@ module Abt
|
|
4
4
|
module Providers
|
5
5
|
module Asana
|
6
6
|
class Path < String
|
7
|
-
PATH_REGEX = %r{^(?<project_gid>\d+)
|
7
|
+
PATH_REGEX = %r{^(?<project_gid>\d+)?/?(?<task_gid>\d+)?$}.freeze
|
8
8
|
|
9
|
-
def self.from_ids(project_gid
|
10
|
-
path = project_gid ? [project_gid, *task_gid].join(
|
11
|
-
new
|
9
|
+
def self.from_ids(project_gid: nil, task_gid: nil)
|
10
|
+
path = project_gid ? [project_gid, *task_gid].join("/") : ""
|
11
|
+
new(path)
|
12
12
|
end
|
13
13
|
|
14
|
-
def initialize(path =
|
15
|
-
raise Abt::Cli::Abort, "Invalid path: #{path}" unless path
|
14
|
+
def initialize(path = "")
|
15
|
+
raise Abt::Cli::Abort, "Invalid path: #{path}" unless PATH_REGEX.match?(path)
|
16
16
|
|
17
17
|
super
|
18
18
|
end
|
@@ -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
|