abt-cli 0.0.22 → 0.0.27
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 +2 -2
- data/lib/abt.rb +5 -0
- data/lib/abt/cli.rb +28 -9
- data/lib/abt/cli/prompt.rb +37 -53
- data/lib/abt/directory_config.rb +25 -0
- data/lib/abt/docs.rb +10 -6
- data/lib/abt/docs/markdown.rb +5 -2
- data/lib/abt/helpers.rb +26 -8
- data/lib/abt/providers/asana.rb +1 -0
- data/lib/abt/providers/asana/base_command.rb +37 -3
- data/lib/abt/providers/asana/commands/add.rb +0 -4
- data/lib/abt/providers/asana/commands/branch_name.rb +0 -13
- data/lib/abt/providers/asana/commands/current.rb +1 -20
- data/lib/abt/providers/asana/commands/finalize.rb +6 -2
- data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +7 -5
- data/lib/abt/providers/asana/commands/pick.rb +12 -46
- data/lib/abt/providers/asana/commands/start.rb +9 -3
- data/lib/abt/providers/asana/commands/tasks.rb +2 -7
- data/lib/abt/providers/asana/configuration.rb +28 -12
- data/lib/abt/providers/asana/path.rb +1 -1
- data/lib/abt/providers/asana/services/project_picker.rb +54 -0
- data/lib/abt/providers/asana/services/task_picker.rb +83 -0
- data/lib/abt/providers/devops.rb +1 -0
- data/lib/abt/providers/devops/api.rb +10 -0
- data/lib/abt/providers/devops/base_command.rb +38 -14
- data/lib/abt/providers/devops/commands/boards.rb +1 -2
- data/lib/abt/providers/devops/commands/branch_name.rb +10 -16
- data/lib/abt/providers/devops/commands/current.rb +1 -21
- data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +16 -20
- data/lib/abt/providers/devops/commands/pick.rb +18 -39
- data/lib/abt/providers/devops/commands/work_items.rb +3 -6
- data/lib/abt/providers/devops/configuration.rb +10 -14
- data/lib/abt/providers/devops/path.rb +4 -4
- data/lib/abt/providers/devops/services/board_picker.rb +54 -0
- data/lib/abt/providers/devops/services/project_picker.rb +79 -0
- data/lib/abt/providers/devops/services/work_item_picker.rb +93 -0
- data/lib/abt/providers/git/commands/branch.rb +7 -3
- data/lib/abt/providers/harvest.rb +1 -0
- data/lib/abt/providers/harvest/base_command.rb +49 -3
- data/lib/abt/providers/harvest/commands/current.rb +1 -30
- data/lib/abt/providers/harvest/commands/pick.rb +12 -23
- data/lib/abt/providers/harvest/commands/projects.rb +0 -5
- data/lib/abt/providers/harvest/commands/tasks.rb +1 -16
- data/lib/abt/providers/harvest/commands/track.rb +33 -19
- data/lib/abt/providers/harvest/configuration.rb +1 -1
- data/lib/abt/providers/harvest/path.rb +1 -1
- data/lib/abt/providers/harvest/services/project_picker.rb +53 -0
- data/lib/abt/providers/harvest/services/task_picker.rb +50 -0
- data/lib/abt/version.rb +1 -1
- metadata +10 -5
- data/lib/abt/providers/asana/commands/init.rb +0 -42
- data/lib/abt/providers/devops/commands/init.rb +0 -76
- data/lib/abt/providers/harvest/commands/init.rb +0 -54
data/lib/abt/providers/devops.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
Dir.glob("#{File.expand_path(__dir__)}/devops/*.rb").sort.each { |file| require file }
|
4
4
|
Dir.glob("#{File.expand_path(__dir__)}/devops/commands/*.rb").sort.each { |file| require file }
|
5
|
+
Dir.glob("#{File.expand_path(__dir__)}/devops/services/*.rb").sort.each { |file| require file }
|
5
6
|
|
6
7
|
module Abt
|
7
8
|
module Providers
|
@@ -73,6 +73,16 @@ module Abt
|
|
73
73
|
"#{base_url}/_boards/board/#{rfc_3986_encode_path_segment(board['name'])}"
|
74
74
|
end
|
75
75
|
|
76
|
+
def sanitize_work_item(work_item)
|
77
|
+
return nil if work_item.nil?
|
78
|
+
|
79
|
+
work_item.merge(
|
80
|
+
"id" => work_item["id"].to_s,
|
81
|
+
"name" => work_item["fields"]["System.Title"],
|
82
|
+
"url" => url_for_work_item(work_item)
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
76
86
|
def connection
|
77
87
|
@connection ||= Faraday.new(api_endpoint) do |connection|
|
78
88
|
connection.basic_auth(username, access_token)
|
@@ -19,30 +19,54 @@ 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
|
-
return if
|
27
|
+
return if board_id && organization_name && project_name
|
24
28
|
|
25
|
-
abort("No current/specified board. Did you
|
29
|
+
abort("No current/specified board. Did you forget to `pick`?")
|
26
30
|
end
|
27
31
|
|
28
32
|
def require_work_item!
|
29
|
-
|
30
|
-
abort("No current/specified board. Did you initialize DevOps and pick a work item?")
|
31
|
-
end
|
32
|
-
|
33
|
+
require_board!
|
33
34
|
return if work_item_id
|
34
35
|
|
35
|
-
abort("No current/specified work item. Did you
|
36
|
+
abort("No current/specified work item. Did you forget to `pick`?")
|
36
37
|
end
|
37
38
|
|
38
|
-
def
|
39
|
-
|
39
|
+
def prompt_project!
|
40
|
+
@path = Services::ProjectPicker.call(cli: cli).path
|
41
|
+
end
|
42
|
+
|
43
|
+
def prompt_board!
|
44
|
+
result = Services::BoardPicker.call(cli: cli, path: path, config: config)
|
45
|
+
@path = result.path
|
46
|
+
@board = result.board
|
47
|
+
end
|
40
48
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
49
|
+
def prompt_work_item!
|
50
|
+
result = Services::WorkItemPicker.call(cli: cli, path: path, config: config, board: board)
|
51
|
+
@path = result.path
|
52
|
+
@work_item = result.work_item
|
53
|
+
end
|
54
|
+
|
55
|
+
def board
|
56
|
+
@board ||= begin
|
57
|
+
api.get("work/boards/#{board_id}")
|
58
|
+
rescue HttpError::NotFoundError
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def work_item
|
64
|
+
@work_item ||= begin
|
65
|
+
work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
|
66
|
+
api.sanitize_work_item(work_item)
|
67
|
+
rescue HttpError::NotFoundError
|
68
|
+
nil
|
69
|
+
end
|
46
70
|
end
|
47
71
|
|
48
72
|
def print_board(organization_name, project_name, board)
|
@@ -14,8 +14,7 @@ module Abt
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
|
-
|
18
|
-
abort("No project selected. Did you initialize DevOps?") if project_name.nil?
|
17
|
+
prompt_project! unless project_name
|
19
18
|
|
20
19
|
boards.map do |board|
|
21
20
|
print_board(organization_name, project_name, board)
|
@@ -16,15 +16,16 @@ module Abt
|
|
16
16
|
def perform
|
17
17
|
require_work_item!
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
19
|
+
if work_item
|
20
|
+
puts name
|
21
|
+
else
|
22
|
+
args = [organization_name, project_name, board_id, work_item_id].compact
|
23
|
+
|
24
|
+
abort(<<~TXT)
|
25
|
+
Unable to find work item for configuration:
|
26
|
+
devops:#{args.join('/')}
|
27
|
+
TXT
|
28
|
+
end
|
28
29
|
end
|
29
30
|
|
30
31
|
private
|
@@ -35,13 +36,6 @@ module Abt
|
|
35
36
|
str += work_item["name"].downcase.gsub(/[^\w]/, "-")
|
36
37
|
str.squeeze("-").gsub(/(^-|-$)/, "")
|
37
38
|
end
|
38
|
-
|
39
|
-
def work_item
|
40
|
-
@work_item ||= begin
|
41
|
-
work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
|
42
|
-
sanitize_work_item(work_item)
|
43
|
-
end
|
44
|
-
end
|
45
39
|
end
|
46
40
|
end
|
47
41
|
end
|
@@ -14,8 +14,7 @@ module Abt
|
|
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
|
|
@@ -43,25 +42,6 @@ module Abt
|
|
43
42
|
end
|
44
43
|
abort("No such work item: ##{work_item_id}") if work_item_id && work_item.nil?
|
45
44
|
end
|
46
|
-
|
47
|
-
def board
|
48
|
-
@board ||= begin
|
49
|
-
warn("Fetching board...")
|
50
|
-
api.get("work/boards/#{board_id}")
|
51
|
-
rescue HttpError::NotFoundError
|
52
|
-
nil
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
def work_item
|
57
|
-
@work_item ||= begin
|
58
|
-
warn("Fetching work item...")
|
59
|
-
work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
|
60
|
-
sanitize_work_item(work_item)
|
61
|
-
rescue HttpError::NotFoundError
|
62
|
-
nil
|
63
|
-
end
|
64
|
-
end
|
65
45
|
end
|
66
46
|
end
|
67
47
|
end
|
@@ -16,7 +16,22 @@ module Abt
|
|
16
16
|
def perform
|
17
17
|
require_work_item!
|
18
18
|
|
19
|
-
|
19
|
+
if work_item
|
20
|
+
puts Oj.dump(body, mode: :json)
|
21
|
+
else
|
22
|
+
args = [organization_name, project_name, board_id, work_item_id].compact
|
23
|
+
|
24
|
+
abort(<<~TXT)
|
25
|
+
Unable to find work item for configuration:
|
26
|
+
devops:#{args.join('/')}
|
27
|
+
TXT
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def body
|
34
|
+
{
|
20
35
|
notes: notes,
|
21
36
|
external_reference: {
|
22
37
|
id: work_item["id"],
|
@@ -24,20 +39,8 @@ module Abt
|
|
24
39
|
permalink: work_item["url"]
|
25
40
|
}
|
26
41
|
}
|
27
|
-
|
28
|
-
puts Oj.dump(body, mode: :json)
|
29
|
-
rescue HttpError::NotFoundError
|
30
|
-
args = [organization_name, project_name, board_id, work_item_id].compact
|
31
|
-
|
32
|
-
error_message = [
|
33
|
-
"Unable to find work item for configuration:",
|
34
|
-
"devops:#{args.join('/')}"
|
35
|
-
].join("\n")
|
36
|
-
abort(error_message)
|
37
42
|
end
|
38
43
|
|
39
|
-
private
|
40
|
-
|
41
44
|
def notes
|
42
45
|
[
|
43
46
|
"Azure DevOps",
|
@@ -47,13 +50,6 @@ module Abt
|
|
47
50
|
work_item["name"]
|
48
51
|
].join(" ")
|
49
52
|
end
|
50
|
-
|
51
|
-
def work_item
|
52
|
-
@work_item ||= begin
|
53
|
-
work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
|
54
|
-
sanitize_work_item(work_item)
|
55
|
-
end
|
56
|
-
end
|
57
53
|
end
|
58
54
|
end
|
59
55
|
end
|
@@ -15,61 +15,40 @@ module Abt
|
|
15
15
|
|
16
16
|
def self.flags
|
17
17
|
[
|
18
|
-
["-d", "--dry-run", "Keep existing configuration"]
|
18
|
+
["-d", "--dry-run", "Keep existing configuration"],
|
19
|
+
["-c", "--clean", "Don't reuse project/board configuration"]
|
19
20
|
]
|
20
21
|
end
|
21
22
|
|
22
23
|
def perform
|
23
|
-
|
24
|
-
require_board!
|
24
|
+
pick!
|
25
25
|
|
26
|
-
warn("#{project_name} - #{board['name']}")
|
27
|
-
|
28
|
-
work_item = select_work_item
|
29
26
|
print_work_item(organization_name, project_name, board, work_item)
|
30
27
|
|
31
28
|
return if flags[:"dry-run"]
|
32
29
|
|
33
|
-
config.
|
30
|
+
if config.local_available?
|
31
|
+
update_config(work_item)
|
32
|
+
else
|
33
|
+
warn("No local configuration to update - will function as dry run")
|
34
|
+
end
|
34
35
|
end
|
35
36
|
|
36
37
|
private
|
37
38
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
work_items = work_items_in_column(column)
|
43
|
-
|
44
|
-
if work_items.length.zero?
|
45
|
-
warn("Section is empty")
|
46
|
-
next
|
47
|
-
end
|
48
|
-
|
49
|
-
work_item = cli.prompt.choice("Select a work item", work_items, nil_option: true)
|
50
|
-
return work_item if work_item
|
51
|
-
end
|
39
|
+
def pick!
|
40
|
+
prompt_project! if project_name.nil? || flags[:clean]
|
41
|
+
prompt_board! if board_id.nil? || flags[:clean]
|
42
|
+
prompt_work_item!
|
52
43
|
end
|
53
44
|
|
54
|
-
def
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
ORDER BY [Microsoft.VSTS.Common.BacklogPriority] ASC
|
61
|
-
WIQL
|
45
|
+
def update_config(work_item)
|
46
|
+
config.path = Path.from_ids(
|
47
|
+
organization_name: organization_name,
|
48
|
+
project_name: project_name,
|
49
|
+
board_id: board_id,
|
50
|
+
work_item_id: work_item["id"]
|
62
51
|
)
|
63
|
-
|
64
|
-
work_items.map { |work_item| sanitize_work_item(work_item) }
|
65
|
-
end
|
66
|
-
|
67
|
-
def columns
|
68
|
-
board["columns"]
|
69
|
-
end
|
70
|
-
|
71
|
-
def board
|
72
|
-
@board ||= api.get("work/boards/#{board_id}")
|
73
52
|
end
|
74
53
|
end
|
75
54
|
end
|
@@ -14,7 +14,8 @@ module Abt
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
|
-
|
17
|
+
prompt_project! unless project_name
|
18
|
+
prompt_board! unless board_id
|
18
19
|
|
19
20
|
work_items.each do |work_item|
|
20
21
|
print_work_item(organization_name, project_name, board, work_item)
|
@@ -32,13 +33,9 @@ module Abt
|
|
32
33
|
FROM WorkItems
|
33
34
|
ORDER BY [System.Title] ASC
|
34
35
|
WIQL
|
35
|
-
).map { |work_item| sanitize_work_item(work_item) }
|
36
|
+
).map { |work_item| api.sanitize_work_item(work_item) }
|
36
37
|
end
|
37
38
|
end
|
38
|
-
|
39
|
-
def board
|
40
|
-
@board ||= api.get("work/boards/#{board_id}")
|
41
|
-
end
|
42
39
|
end
|
43
40
|
end
|
44
41
|
end
|
@@ -15,7 +15,7 @@ module Abt
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def path
|
18
|
-
Path.new(local_available? && git["path"] || "")
|
18
|
+
Path.new(local_available? && git["path"] || Abt.directory_config.dig("devops", "path") || "")
|
19
19
|
end
|
20
20
|
|
21
21
|
def path=(new_path)
|
@@ -47,7 +47,15 @@ module Abt
|
|
47
47
|
|
48
48
|
return git_global[access_token_key] unless git_global[access_token_key].nil?
|
49
49
|
|
50
|
-
git_global[access_token_key] = cli.prompt.text(
|
50
|
+
git_global[access_token_key] = cli.prompt.text(<<~TXT)
|
51
|
+
Please provide your personal access token for the DevOps organization (#{organization_name}).
|
52
|
+
If you don't have one, follow the guide here: https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate
|
53
|
+
|
54
|
+
The token MUST have "Read" permission for Work Items
|
55
|
+
Future features will likely require "Write" or "Manage
|
56
|
+
|
57
|
+
Enter access token"
|
58
|
+
TXT
|
51
59
|
end
|
52
60
|
|
53
61
|
private
|
@@ -59,18 +67,6 @@ module Abt
|
|
59
67
|
def git_global
|
60
68
|
@git_global ||= GitConfig.new("global", "abt.devops")
|
61
69
|
end
|
62
|
-
|
63
|
-
def access_token_prompt_text
|
64
|
-
<<~TXT
|
65
|
-
Please provide your personal access token for the DevOps organization (#{organization_name}).
|
66
|
-
If you don't have one, follow the guide here: https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate
|
67
|
-
|
68
|
-
The token MUST have "Read" permission for Work Items
|
69
|
-
Future features will likely require "Write" or "Manage
|
70
|
-
|
71
|
-
Enter access token"
|
72
|
-
TXT
|
73
|
-
end
|
74
70
|
end
|
75
71
|
end
|
76
72
|
end
|
@@ -10,12 +10,12 @@ module Abt
|
|
10
10
|
WORK_ITEM_ID_REGEX = /(?<work_item_id>\d+)/.freeze
|
11
11
|
|
12
12
|
PATH_REGEX =
|
13
|
-
%r{^(#{ORGANIZATION_NAME_REGEX}/#{PROJECT_NAME_REGEX}/#{BOARD_ID_REGEX}
|
13
|
+
%r{^(#{ORGANIZATION_NAME_REGEX}/#{PROJECT_NAME_REGEX}(/#{BOARD_ID_REGEX}(/#{WORK_ITEM_ID_REGEX})?)?)?}.freeze
|
14
14
|
|
15
|
-
def self.from_ids(
|
16
|
-
return new unless
|
15
|
+
def self.from_ids(organization_name: nil, project_name: nil, board_id: nil, work_item_id: nil)
|
16
|
+
return new unless organization_name && project_name
|
17
17
|
|
18
|
-
new([
|
18
|
+
new([organization_name, project_name, *board_id, *work_item_id].join("/"))
|
19
19
|
end
|
20
20
|
|
21
21
|
def initialize(path = "")
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Devops
|
6
|
+
module Services
|
7
|
+
class BoardPicker
|
8
|
+
class Result
|
9
|
+
attr_reader :board, :path
|
10
|
+
|
11
|
+
def initialize(board:, path:)
|
12
|
+
@board = board
|
13
|
+
@path = path
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.call(**args)
|
18
|
+
new(**args).call
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :cli, :config, :path
|
22
|
+
|
23
|
+
def initialize(cli:, path:, config:)
|
24
|
+
@cli = cli
|
25
|
+
@config = config
|
26
|
+
@path = path
|
27
|
+
end
|
28
|
+
|
29
|
+
def call
|
30
|
+
board = cli.prompt.choice("Select a project work board", boards)
|
31
|
+
|
32
|
+
path_with_board = Path.new([path, board["id"]].join("/"))
|
33
|
+
|
34
|
+
Result.new(board: board, path: path_with_board)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def boards
|
40
|
+
@boards ||= api.get_paged("work/boards")
|
41
|
+
end
|
42
|
+
|
43
|
+
def api
|
44
|
+
Abt::Providers::Devops::Api.new(organization_name: path.organization_name,
|
45
|
+
project_name: path.project_name,
|
46
|
+
username: config.username_for_organization(path.organization_name),
|
47
|
+
access_token: config.access_token_for_organization(path.organization_name),
|
48
|
+
cli: cli)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|