abt-cli 0.0.24 → 0.0.29
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 +1 -1
- data/lib/abt.rb +1 -0
- data/lib/abt/cli.rb +32 -9
- data/lib/abt/cli/prompt.rb +12 -9
- data/lib/abt/directory_config.rb +43 -0
- 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/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 +11 -3
- 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 +10 -0
- data/lib/abt/providers/devops/base_command.rb +34 -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 +0 -19
- data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +10 -16
- data/lib/abt/providers/devops/commands/pick.rb +11 -47
- data/lib/abt/providers/devops/commands/work_items.rb +3 -6
- 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 +3 -3
- data/lib/abt/providers/devops/services/board_picker.rb +58 -0
- data/lib/abt/providers/devops/services/project_picker.rb +73 -0
- data/lib/abt/providers/devops/services/work_item_picker.rb +98 -0
- data/lib/abt/providers/git/commands/branch.rb +1 -1
- data/lib/abt/providers/harvest.rb +1 -0
- data/lib/abt/providers/harvest/base_command.rb +45 -3
- 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/write_config.rb +41 -0
- 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 +13 -5
- data/lib/abt/providers/asana/commands/init.rb +0 -42
- data/lib/abt/providers/devops/commands/init.rb +0 -79
- data/lib/abt/providers/harvest/commands/init.rb +0 -53
@@ -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
|
@@ -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_project! if project_name.nil? || flags[:clean]
|
24
|
+
prompt_board! if board_id.nil? || flags[:clean]
|
25
|
+
|
26
|
+
update_directory_config!
|
27
|
+
|
28
|
+
warn("DevOps configuration written to #{Abt::DirectoryConfig::FILE_NAME}")
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def update_directory_config!
|
34
|
+
cli.directory_config["devops"] = {
|
35
|
+
"path" => Path.from_ids(
|
36
|
+
organization_name: organization_name,
|
37
|
+
project_name: project_name,
|
38
|
+
board_id: board_id
|
39
|
+
).to_s
|
40
|
+
}
|
41
|
+
cli.directory_config.save!
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -13,9 +13,9 @@ module Abt
|
|
13
13
|
%r{^(#{ORGANIZATION_NAME_REGEX}/#{PROJECT_NAME_REGEX}(/#{BOARD_ID_REGEX}(/#{WORK_ITEM_ID_REGEX})?)?)?}.freeze
|
14
14
|
|
15
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
|
16
|
+
return new unless organization_name && project_name
|
17
17
|
|
18
|
-
new([organization_name, project_name, board_id, *work_item_id].join("/"))
|
18
|
+
new([organization_name, project_name, *board_id, *work_item_id].join("/"))
|
19
19
|
end
|
20
20
|
|
21
21
|
def initialize(path = "")
|
@@ -43,7 +43,7 @@ module Abt
|
|
43
43
|
private
|
44
44
|
|
45
45
|
def match
|
46
|
-
@match ||= PATH_REGEX.match(
|
46
|
+
@match ||= PATH_REGEX.match(to_s)
|
47
47
|
end
|
48
48
|
end
|
49
49
|
end
|
@@ -0,0 +1,58 @@
|
|
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.from_ids(
|
33
|
+
organization_name: path.organization_name,
|
34
|
+
project_name: path.project_name,
|
35
|
+
board_id: board["id"]
|
36
|
+
)
|
37
|
+
|
38
|
+
Result.new(board: board, path: path_with_board)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def boards
|
44
|
+
@boards ||= api.get_paged("work/boards")
|
45
|
+
end
|
46
|
+
|
47
|
+
def api
|
48
|
+
Abt::Providers::Devops::Api.new(organization_name: path.organization_name,
|
49
|
+
project_name: path.project_name,
|
50
|
+
username: config.username_for_organization(path.organization_name),
|
51
|
+
access_token: config.access_token_for_organization(path.organization_name),
|
52
|
+
cli: cli)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
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,98 @@
|
|
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
|
+
board_id: path.board_id,
|
37
|
+
work_item_id: work_item["id"]
|
38
|
+
)
|
39
|
+
|
40
|
+
Result.new(work_item: work_item, path: path_with_work_item)
|
41
|
+
end
|
42
|
+
|
43
|
+
def select_work_item
|
44
|
+
column = cli.prompt.choice("Which column in #{board['name']}?", columns)
|
45
|
+
cli.warn("Fetching work items...")
|
46
|
+
work_items = work_items_in_column(column)
|
47
|
+
|
48
|
+
if work_items.length.zero?
|
49
|
+
cli.warn("Section is empty")
|
50
|
+
select_work_item
|
51
|
+
else
|
52
|
+
prompt_work_item(work_items) || select_work_item
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def prompt_work_item(work_items)
|
57
|
+
options = work_items.map do |work_item|
|
58
|
+
{
|
59
|
+
"id" => work_item["id"],
|
60
|
+
"name" => "##{work_item['id']} #{work_item['name']}"
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
choice = cli.prompt.choice("Select a work item", options, nil_option: true)
|
65
|
+
choice && work_items.find { |work_item| work_item["id"] == choice["id"] }
|
66
|
+
end
|
67
|
+
|
68
|
+
def work_items_in_column(column)
|
69
|
+
work_items = api.work_item_query(
|
70
|
+
<<~WIQL
|
71
|
+
SELECT [System.Id]
|
72
|
+
FROM WorkItems
|
73
|
+
WHERE [System.BoardColumn] = '#{column['name']}'
|
74
|
+
ORDER BY [Microsoft.VSTS.Common.BacklogPriority] ASC
|
75
|
+
WIQL
|
76
|
+
)
|
77
|
+
|
78
|
+
work_items.map { |work_item| api.sanitize_work_item(work_item) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def columns
|
82
|
+
board["columns"] || api.get("work/boards/#{path.board_id}")["columns"]
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def api
|
88
|
+
Abt::Providers::Devops::Api.new(organization_name: path.organization_name,
|
89
|
+
project_name: path.project_name,
|
90
|
+
username: config.username_for_organization(path.organization_name),
|
91
|
+
access_token: config.access_token_for_organization(path.organization_name),
|
92
|
+
cli: cli)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -30,7 +30,7 @@ module Abt
|
|
30
30
|
|
31
31
|
def create_and_switch
|
32
32
|
warn("No such branch: #{branch_name}")
|
33
|
-
abort("Aborting") unless cli.prompt.boolean("Create branch?")
|
33
|
+
abort("Aborting") unless cli.prompt.boolean("Create branch?", default: true)
|
34
34
|
|
35
35
|
Open3.popen3("git switch -c #{branch_name}") do |_i, _o, _e, thread|
|
36
36
|
thread.value
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
Dir.glob("#{File.expand_path(__dir__)}/harvest/*.rb").sort.each { |file| require file }
|
4
4
|
Dir.glob("#{File.expand_path(__dir__)}/harvest/commands/*.rb").sort.each { |file| require file }
|
5
|
+
Dir.glob("#{File.expand_path(__dir__)}/harvest/services/*.rb").sort.each { |file| require file }
|
5
6
|
|
6
7
|
module Abt
|
7
8
|
module Providers
|
@@ -26,13 +26,55 @@ module Abt
|
|
26
26
|
def require_project!
|
27
27
|
return if project_id
|
28
28
|
|
29
|
-
abort("No current/specified project. Did you
|
29
|
+
abort("No current/specified project. Did you forget to run `pick`?")
|
30
30
|
end
|
31
31
|
|
32
32
|
def require_task!
|
33
|
-
|
33
|
+
require_project!
|
34
|
+
return if task_id
|
34
35
|
|
35
|
-
abort("No current/specified task. Did you
|
36
|
+
abort("No current/specified task. Did you forget to run `pick`?")
|
37
|
+
end
|
38
|
+
|
39
|
+
def prompt_project!
|
40
|
+
result = Services::ProjectPicker.call(cli: cli, project_assignments: project_assignments)
|
41
|
+
@path = result.path
|
42
|
+
@project = result.project
|
43
|
+
end
|
44
|
+
|
45
|
+
def prompt_task!
|
46
|
+
result = Services::TaskPicker.call(cli: cli, path: path, project_assignment: project_assignment)
|
47
|
+
@path = result.path
|
48
|
+
@task = result.task
|
49
|
+
end
|
50
|
+
|
51
|
+
def task
|
52
|
+
return @task if instance_variable_defined?(:@task)
|
53
|
+
|
54
|
+
@task = if project_assignment
|
55
|
+
project_assignment["task_assignments"].map { |ta| ta["task"] }.find do |task|
|
56
|
+
task["id"].to_s == task_id
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def project
|
62
|
+
return @project if instance_variable_defined?(:@project)
|
63
|
+
|
64
|
+
@project = if project_assignment
|
65
|
+
project_assignment["project"].merge("client" => project_assignment["client"])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def project_assignment
|
70
|
+
@project_assignment ||= project_assignments.find { |pa| pa["project"]["id"].to_s == path.project_id }
|
71
|
+
end
|
72
|
+
|
73
|
+
def project_assignments
|
74
|
+
@project_assignments ||= begin
|
75
|
+
warn("Fetching Harvest data...")
|
76
|
+
api.get_paged("users/me/project_assignments")
|
77
|
+
end
|
36
78
|
end
|
37
79
|
|
38
80
|
def print_project(project)
|
@@ -40,34 +40,6 @@ module Abt
|
|
40
40
|
abort("Invalid project: #{project_id}") if project.nil?
|
41
41
|
abort("Invalid task: #{task_id}") if task_id && task.nil?
|
42
42
|
end
|
43
|
-
|
44
|
-
def project
|
45
|
-
return @project if instance_variable_defined?(:@project)
|
46
|
-
|
47
|
-
@project = if project_assignment
|
48
|
-
project_assignment["project"].merge("client" => project_assignment["client"])
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def task
|
53
|
-
return @task if instance_variable_defined?(:@task)
|
54
|
-
|
55
|
-
@task = if project_assignment
|
56
|
-
project_assignment["task_assignments"].map { |ta| ta["task"] }.find do |task|
|
57
|
-
task["id"].to_s == task_id
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def project_assignment
|
63
|
-
@project_assignment ||= begin
|
64
|
-
project_assignments.find { |pa| pa["project"]["id"].to_s == project_id }
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
def project_assignments
|
69
|
-
@project_assignments ||= api.get_paged("users/me/project_assignments")
|
70
|
-
end
|
71
43
|
end
|
72
44
|
end
|
73
45
|
end
|