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