abt-cli 0.0.26 → 0.0.31
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/lib/abt.rb +0 -4
- data/lib/abt/cli.rb +5 -1
- data/lib/abt/directory_config.rb +28 -10
- data/lib/abt/docs.rb +10 -6
- data/lib/abt/providers/asana.rb +1 -0
- data/lib/abt/providers/asana/base_command.rb +33 -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/clear.rb +1 -1
- data/lib/abt/providers/asana/commands/current.rb +0 -18
- data/lib/abt/providers/asana/commands/finalize.rb +2 -4
- data/lib/abt/providers/asana/commands/pick.rb +11 -41
- data/lib/abt/providers/asana/commands/tasks.rb +2 -7
- data/lib/abt/providers/asana/commands/write_config.rb +73 -0
- data/lib/abt/providers/asana/configuration.rb +1 -1
- data/lib/abt/providers/asana/path.rb +2 -2
- 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 +27 -20
- data/lib/abt/providers/devops/base_command.rb +42 -25
- data/lib/abt/providers/devops/commands/branch_name.rb +8 -16
- data/lib/abt/providers/devops/commands/clear.rb +1 -1
- data/lib/abt/providers/devops/commands/current.rb +2 -21
- data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +8 -16
- data/lib/abt/providers/devops/commands/pick.rb +11 -60
- data/lib/abt/providers/devops/commands/work_items.rb +3 -7
- data/lib/abt/providers/devops/commands/write_config.rb +47 -0
- data/lib/abt/providers/devops/configuration.rb +1 -1
- data/lib/abt/providers/devops/path.rb +24 -8
- data/lib/abt/providers/devops/services/board_picker.rb +69 -0
- data/lib/abt/providers/devops/services/project_picker.rb +73 -0
- data/lib/abt/providers/devops/services/work_item_picker.rb +99 -0
- data/lib/abt/providers/harvest.rb +1 -0
- data/lib/abt/providers/harvest/base_command.rb +45 -3
- data/lib/abt/providers/harvest/commands/clear.rb +1 -1
- data/lib/abt/providers/harvest/commands/current.rb +0 -28
- data/lib/abt/providers/harvest/commands/pick.rb +12 -27
- data/lib/abt/providers/harvest/commands/projects.rb +2 -9
- data/lib/abt/providers/harvest/commands/tasks.rb +2 -19
- data/lib/abt/providers/harvest/commands/track.rb +72 -39
- data/lib/abt/providers/harvest/commands/write_config.rb +41 -0
- data/lib/abt/providers/harvest/configuration.rb +1 -1
- data/lib/abt/providers/harvest/harvest_helpers.rb +25 -0
- 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 +13 -6
- data/lib/abt/providers/asana/commands/init.rb +0 -42
- data/lib/abt/providers/devops/commands/boards.rb +0 -34
- data/lib/abt/providers/devops/commands/init.rb +0 -79
- data/lib/abt/providers/harvest/commands/init.rb +0 -53
@@ -16,15 +16,14 @@ module Abt
|
|
16
16
|
def perform
|
17
17
|
require_work_item!
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
abort(error_message)
|
19
|
+
if work_item
|
20
|
+
puts Oj.dump(body, mode: :json)
|
21
|
+
else
|
22
|
+
abort(<<~TXT)
|
23
|
+
Unable to find work item for configuration:
|
24
|
+
devops:#{path}
|
25
|
+
TXT
|
26
|
+
end
|
28
27
|
end
|
29
28
|
|
30
29
|
private
|
@@ -49,13 +48,6 @@ module Abt
|
|
49
48
|
work_item["name"]
|
50
49
|
].join(" ")
|
51
50
|
end
|
52
|
-
|
53
|
-
def work_item
|
54
|
-
@work_item ||= begin
|
55
|
-
work_item = api.get_paged("wit/workitems", ids: work_item_id)[0]
|
56
|
-
sanitize_work_item(work_item)
|
57
|
-
end
|
58
|
-
end
|
59
51
|
end
|
60
52
|
end
|
61
53
|
end
|
@@ -15,79 +15,30 @@ 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
|
-
|
27
|
-
|
28
|
-
work_item = select_work_item
|
29
|
-
print_work_item(organization_name, project_name, board, work_item)
|
26
|
+
print_work_item(organization_name, project_name, team_name, board, work_item)
|
30
27
|
|
31
28
|
return if flags[:"dry-run"]
|
32
29
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
private
|
37
|
-
|
38
|
-
def update_config(work_item)
|
39
|
-
config.path = Path.from_ids(
|
40
|
-
organization_name: organization_name,
|
41
|
-
project_name: project_name,
|
42
|
-
board_id: board_id,
|
43
|
-
work_item_id: work_item["id"]
|
44
|
-
)
|
45
|
-
end
|
46
|
-
|
47
|
-
def select_work_item
|
48
|
-
column = cli.prompt.choice("Which column?", columns)
|
49
|
-
warn("Fetching work items...")
|
50
|
-
work_items = work_items_in_column(column)
|
51
|
-
|
52
|
-
if work_items.length.zero?
|
53
|
-
warn("Section is empty")
|
54
|
-
select_work_item
|
30
|
+
if config.local_available?
|
31
|
+
config.path = path
|
55
32
|
else
|
56
|
-
|
33
|
+
warn("No local configuration to update - will function as dry run")
|
57
34
|
end
|
58
35
|
end
|
59
36
|
|
60
|
-
|
61
|
-
options = work_items.map do |work_item|
|
62
|
-
{
|
63
|
-
"id" => work_item["id"],
|
64
|
-
"name" => "##{work_item['id']} #{work_item['name']}"
|
65
|
-
}
|
66
|
-
end
|
67
|
-
|
68
|
-
choice = cli.prompt.choice("Select a work item", options, nil_option: true)
|
69
|
-
choice && work_items.find { |work_item| work_item["id"] == choice["id"] }
|
70
|
-
end
|
71
|
-
|
72
|
-
def work_items_in_column(column)
|
73
|
-
work_items = api.work_item_query(
|
74
|
-
<<~WIQL
|
75
|
-
SELECT [System.Id]
|
76
|
-
FROM WorkItems
|
77
|
-
WHERE [System.BoardColumn] = '#{column['name']}'
|
78
|
-
ORDER BY [Microsoft.VSTS.Common.BacklogPriority] ASC
|
79
|
-
WIQL
|
80
|
-
)
|
81
|
-
|
82
|
-
work_items.map { |work_item| sanitize_work_item(work_item) }
|
83
|
-
end
|
84
|
-
|
85
|
-
def columns
|
86
|
-
board["columns"]
|
87
|
-
end
|
37
|
+
private
|
88
38
|
|
89
|
-
def
|
90
|
-
|
39
|
+
def pick!
|
40
|
+
prompt_board! if board_name.nil? || flags[:clean]
|
41
|
+
prompt_work_item!
|
91
42
|
end
|
92
43
|
end
|
93
44
|
end
|
@@ -14,10 +14,10 @@ module Abt
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
|
-
|
17
|
+
prompt_board! unless board_name
|
18
18
|
|
19
19
|
work_items.each do |work_item|
|
20
|
-
print_work_item(organization_name, project_name, board, work_item)
|
20
|
+
print_work_item(organization_name, project_name, team_name, board, work_item)
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
@@ -32,13 +32,9 @@ module Abt
|
|
32
32
|
FROM WorkItems
|
33
33
|
ORDER BY [System.Title] ASC
|
34
34
|
WIQL
|
35
|
-
).map { |work_item| sanitize_work_item(work_item) }
|
35
|
+
).map { |work_item| api.sanitize_work_item(work_item) }
|
36
36
|
end
|
37
37
|
end
|
38
|
-
|
39
|
-
def board
|
40
|
-
@board ||= api.get("work/boards/#{board_id}")
|
41
|
-
end
|
42
38
|
end
|
43
39
|
end
|
44
40
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Devops
|
6
|
+
module Commands
|
7
|
+
class WriteConfig < BaseCommand
|
8
|
+
def self.usage
|
9
|
+
"abt write-config devops[:<organization-name>/<project-name>/<board-id>]"
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.description
|
13
|
+
"Write DevOps settings to .abt.yml"
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.flags
|
17
|
+
[
|
18
|
+
["-c", "--clean", "Don't reuse configuration"]
|
19
|
+
]
|
20
|
+
end
|
21
|
+
|
22
|
+
def perform
|
23
|
+
prompt_board! if board_name.nil? || flags[:clean]
|
24
|
+
|
25
|
+
update_directory_config!
|
26
|
+
|
27
|
+
warn("DevOps configuration written to #{Abt::DirectoryConfig::FILE_NAME}")
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def update_directory_config!
|
33
|
+
cli.directory_config["devops"] = {
|
34
|
+
"path" => Path.from_ids(
|
35
|
+
organization_name: organization_name,
|
36
|
+
project_name: project_name,
|
37
|
+
team_name: team_name,
|
38
|
+
board_name: board_name
|
39
|
+
).to_s
|
40
|
+
}
|
41
|
+
cli.directory_config.save!
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -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
|
-
return new unless organization_name && project_name
|
16
|
+
def self.from_ids(organization_name: nil, project_name: nil, team_name: nil, board_name: nil, work_item_id: nil)
|
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
|
@@ -43,7 +59,7 @@ module Abt
|
|
43
59
|
private
|
44
60
|
|
45
61
|
def match
|
46
|
-
@match ||= PATH_REGEX.match(
|
62
|
+
@match ||= PATH_REGEX.match(to_s)
|
47
63
|
end
|
48
64
|
end
|
49
65
|
end
|
@@ -0,0 +1,69 @@
|
|
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:, config:)
|
24
|
+
@cli = cli
|
25
|
+
@config = config
|
26
|
+
end
|
27
|
+
|
28
|
+
def call
|
29
|
+
@path = ProjectPicker.call(cli: cli).path
|
30
|
+
board = cli.prompt.choice("Select a project work board", boards)
|
31
|
+
|
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(
|
39
|
+
organization_name: path.organization_name,
|
40
|
+
project_name: path.project_name,
|
41
|
+
team_name: Api.rfc_3986_encode_path_segment(team["name"]),
|
42
|
+
board_name: Api.rfc_3986_encode_path_segment(board["name"])
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def team
|
47
|
+
@team ||= cli.prompt.choice("Select a team", teams)
|
48
|
+
end
|
49
|
+
|
50
|
+
def teams
|
51
|
+
@teams ||= api.get_paged("/_apis/projects/#{path.project_name}/teams")
|
52
|
+
end
|
53
|
+
|
54
|
+
def boards
|
55
|
+
team_name = Api.rfc_3986_encode_path_segment(team["name"])
|
56
|
+
@boards ||= api.get_paged("#{path.project_name}/#{team_name}/_apis/work/boards")
|
57
|
+
end
|
58
|
+
|
59
|
+
def api
|
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)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Devops
|
6
|
+
module Services
|
7
|
+
class ProjectPicker
|
8
|
+
class Result
|
9
|
+
attr_reader :board, :path
|
10
|
+
|
11
|
+
def initialize(path:)
|
12
|
+
@path = path
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
AZURE_DEV_URL_REGEX = %r{^https://dev\.azure\.com/(?<organization>[^/]+)/(?<project>[^/]+)}.freeze
|
17
|
+
VS_URL_REGEX = %r{^https://(?<organization>[^.]+)\.visualstudio\.com/(?<project>[^/]+)}.freeze
|
18
|
+
|
19
|
+
extend Forwardable
|
20
|
+
|
21
|
+
def self.call(**args)
|
22
|
+
new(**args).call
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :cli
|
26
|
+
|
27
|
+
def initialize(cli:)
|
28
|
+
@cli = cli
|
29
|
+
end
|
30
|
+
|
31
|
+
def call
|
32
|
+
Result.new(
|
33
|
+
path: Path.from_ids(organization_name: organization_name, project_name: project_name)
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def project_name
|
40
|
+
@project_name ||= project_url_match && project_url_match[:project]
|
41
|
+
end
|
42
|
+
|
43
|
+
def organization_name
|
44
|
+
@organization_name ||= project_url_match && project_url_match[:organization]
|
45
|
+
end
|
46
|
+
|
47
|
+
def project_url_match
|
48
|
+
AZURE_DEV_URL_REGEX.match(project_url) || VS_URL_REGEX.match(project_url)
|
49
|
+
end
|
50
|
+
|
51
|
+
def project_url
|
52
|
+
@project_url ||= loop do
|
53
|
+
url = prompt_url
|
54
|
+
|
55
|
+
break url if AZURE_DEV_URL_REGEX =~ url || VS_URL_REGEX =~ url
|
56
|
+
|
57
|
+
cli.warn("Invalid URL")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def prompt_url
|
62
|
+
cli.prompt.text(<<~TXT)
|
63
|
+
Please provide the URL for the devops project
|
64
|
+
For instance https://{organization}.visualstudio.com/{project} or https://dev.azure.com/{organization}/{project}
|
65
|
+
|
66
|
+
Enter URL
|
67
|
+
TXT
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Devops
|
6
|
+
module Services
|
7
|
+
class WorkItemPicker
|
8
|
+
class Result
|
9
|
+
attr_reader :work_item, :path
|
10
|
+
|
11
|
+
def initialize(work_item:, path:)
|
12
|
+
@work_item = work_item
|
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, :board
|
22
|
+
|
23
|
+
def initialize(cli:, path:, config:, board:)
|
24
|
+
@cli = cli
|
25
|
+
@config = config
|
26
|
+
@path = path
|
27
|
+
@board = board
|
28
|
+
end
|
29
|
+
|
30
|
+
def call
|
31
|
+
work_item = select_work_item
|
32
|
+
|
33
|
+
path_with_work_item = Path.from_ids(
|
34
|
+
organization_name: path.organization_name,
|
35
|
+
project_name: path.project_name,
|
36
|
+
team_name: path.team_name,
|
37
|
+
board_name: path.board_name,
|
38
|
+
work_item_id: work_item["id"]
|
39
|
+
)
|
40
|
+
|
41
|
+
Result.new(work_item: work_item, path: path_with_work_item)
|
42
|
+
end
|
43
|
+
|
44
|
+
def select_work_item
|
45
|
+
column = cli.prompt.choice("Which column in #{board['name']}?", columns)
|
46
|
+
cli.warn("Fetching work items...")
|
47
|
+
work_items = work_items_in_column(column)
|
48
|
+
|
49
|
+
if work_items.length.zero?
|
50
|
+
cli.warn("Section is empty")
|
51
|
+
select_work_item
|
52
|
+
else
|
53
|
+
prompt_work_item(work_items) || select_work_item
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def prompt_work_item(work_items)
|
58
|
+
options = work_items.map do |work_item|
|
59
|
+
{
|
60
|
+
"id" => work_item["id"],
|
61
|
+
"name" => "##{work_item['id']} #{work_item['name']}"
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
choice = cli.prompt.choice("Select a work item", options, nil_option: true)
|
66
|
+
choice && work_items.find { |work_item| work_item["id"] == choice["id"] }
|
67
|
+
end
|
68
|
+
|
69
|
+
def work_items_in_column(column)
|
70
|
+
work_items = api.work_item_query(
|
71
|
+
<<~WIQL
|
72
|
+
SELECT [System.Id]
|
73
|
+
FROM WorkItems
|
74
|
+
WHERE [System.BoardColumn] = '#{column['name']}'
|
75
|
+
ORDER BY [Microsoft.VSTS.Common.BacklogPriority] ASC
|
76
|
+
WIQL
|
77
|
+
)
|
78
|
+
|
79
|
+
work_items.map { |work_item| api.sanitize_work_item(work_item) }
|
80
|
+
end
|
81
|
+
|
82
|
+
def columns
|
83
|
+
board["columns"] ||
|
84
|
+
api.get("#{path.project_name}/#{path.team_name}/_apis/work/boards/#{path.board_name}")["columns"]
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def api
|
90
|
+
Abt::Providers::Devops::Api.new(organization_name: path.organization_name,
|
91
|
+
username: config.username_for_organization(path.organization_name),
|
92
|
+
access_token: config.access_token_for_organization(path.organization_name),
|
93
|
+
cli: cli)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|