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
@@ -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"] || directory_config["path"] || "")
|
19
19
|
end
|
20
20
|
|
21
21
|
def path=(new_path)
|
@@ -36,13 +36,17 @@ module Abt
|
|
36
36
|
def wip_section_gid
|
37
37
|
return nil unless local_available?
|
38
38
|
|
39
|
-
@wip_section_gid ||= git["wipSectionGid"] ||
|
39
|
+
@wip_section_gid ||= git["wipSectionGid"] ||
|
40
|
+
directory_config["wip_section_gid"] ||
|
41
|
+
prompt_wip_section["gid"]
|
40
42
|
end
|
41
43
|
|
42
44
|
def finalized_section_gid
|
43
45
|
return nil unless local_available?
|
44
46
|
|
45
|
-
@finalized_section_gid ||= git["finalizedSectionGid"] ||
|
47
|
+
@finalized_section_gid ||= git["finalizedSectionGid"] ||
|
48
|
+
directory_config["finalized_section_gid"] ||
|
49
|
+
prompt_finalized_section["gid"]
|
46
50
|
end
|
47
51
|
|
48
52
|
def clear_local(verbose: true)
|
@@ -66,6 +70,10 @@ module Abt
|
|
66
70
|
|
67
71
|
private
|
68
72
|
|
73
|
+
def directory_config
|
74
|
+
cli.directory_config.fetch("asana", {})
|
75
|
+
end
|
76
|
+
|
69
77
|
def git
|
70
78
|
@git ||= GitConfig.new("local", "abt.asana")
|
71
79
|
end
|
@@ -6,7 +6,7 @@ module Abt
|
|
6
6
|
class Path < String
|
7
7
|
PATH_REGEX = %r{^(?<project_gid>\d+)?/?(?<task_gid>\d+)?$}.freeze
|
8
8
|
|
9
|
-
def self.
|
9
|
+
def self.from_gids(project_gid: nil, task_gid: nil)
|
10
10
|
path = project_gid ? [project_gid, *task_gid].join("/") : ""
|
11
11
|
new(path)
|
12
12
|
end
|
@@ -28,7 +28,7 @@ module Abt
|
|
28
28
|
private
|
29
29
|
|
30
30
|
def match
|
31
|
-
@match ||= PATH_REGEX.match(
|
31
|
+
@match ||= PATH_REGEX.match(to_s)
|
32
32
|
end
|
33
33
|
end
|
34
34
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Asana
|
6
|
+
module Services
|
7
|
+
class ProjectPicker
|
8
|
+
class Result
|
9
|
+
attr_reader :project, :path
|
10
|
+
|
11
|
+
def initialize(project:, path:)
|
12
|
+
@project = project
|
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
|
22
|
+
|
23
|
+
def initialize(cli:, config:)
|
24
|
+
@cli = cli
|
25
|
+
@config = config
|
26
|
+
end
|
27
|
+
|
28
|
+
def call
|
29
|
+
project = cli.prompt.search("Select a project", projects)
|
30
|
+
path = Path.from_gids(project_gid: project["gid"])
|
31
|
+
|
32
|
+
Result.new(project: project, path: path)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def projects
|
38
|
+
@projects ||= begin
|
39
|
+
cli.warn("Fetching projects...")
|
40
|
+
api.get_paged("projects",
|
41
|
+
workspace: config.workspace_gid,
|
42
|
+
archived: false,
|
43
|
+
opt_fields: "name,permalink_url")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def api
|
48
|
+
Abt::Providers::Asana::Api.new(access_token: config.access_token)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Asana
|
6
|
+
module Services
|
7
|
+
class TaskPicker
|
8
|
+
class Result
|
9
|
+
attr_reader :task, :path
|
10
|
+
|
11
|
+
def initialize(task:, path:)
|
12
|
+
@task = task
|
13
|
+
@path = path
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.call(**args)
|
18
|
+
new(**args).call
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :cli, :path, :config, :project
|
22
|
+
|
23
|
+
def initialize(cli:, path:, config:, project:)
|
24
|
+
@cli = cli
|
25
|
+
@path = path
|
26
|
+
@config = config
|
27
|
+
@project = project
|
28
|
+
end
|
29
|
+
|
30
|
+
def call
|
31
|
+
task = select_task
|
32
|
+
|
33
|
+
path_with_task = Path.from_gids(project_gid: path.project_gid, task_gid: task["gid"])
|
34
|
+
|
35
|
+
Result.new(task: task, path: path_with_task)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def select_task
|
41
|
+
section = prompt_section
|
42
|
+
tasks = tasks_in_section(section)
|
43
|
+
|
44
|
+
if tasks.length.zero?
|
45
|
+
cli.warn("Section is empty")
|
46
|
+
select_task
|
47
|
+
else
|
48
|
+
cli.prompt.choice("Select a task", tasks, nil_option: true) || select_task
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def prompt_section
|
53
|
+
cli.prompt.choice("Which section in #{project['name']}?", sections)
|
54
|
+
end
|
55
|
+
|
56
|
+
def tasks_in_section(section)
|
57
|
+
cli.warn("Fetching tasks...")
|
58
|
+
tasks = api.get_paged(
|
59
|
+
"tasks",
|
60
|
+
section: section["gid"],
|
61
|
+
opt_fields: "name,completed,permalink_url"
|
62
|
+
)
|
63
|
+
|
64
|
+
# The below filtering is the best we can do with Asanas api, see this:
|
65
|
+
# https://forum.asana.com/t/tasks-query-completed-since-is-broken-for-sections/21461
|
66
|
+
tasks.reject { |task| task["completed"] }
|
67
|
+
end
|
68
|
+
|
69
|
+
def sections
|
70
|
+
@sections ||= begin
|
71
|
+
cli.warn("Fetching sections...")
|
72
|
+
api.get_paged("projects/#{project['gid']}/sections", opt_fields: "name")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def api
|
77
|
+
Abt::Providers::Asana::Api.new(access_token: config.access_token)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
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)
|
@@ -24,29 +24,49 @@ module Abt
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def require_board!
|
27
|
-
return if
|
27
|
+
return if board_id && organization_name && project_name
|
28
28
|
|
29
|
-
abort("No current/specified board. Did you
|
29
|
+
abort("No current/specified board. Did you forget to `pick`?")
|
30
30
|
end
|
31
31
|
|
32
32
|
def require_work_item!
|
33
|
-
|
34
|
-
abort("No current/specified board. Did you initialize DevOps and pick a work item?")
|
35
|
-
end
|
36
|
-
|
33
|
+
require_board!
|
37
34
|
return if work_item_id
|
38
35
|
|
39
|
-
abort("No current/specified work item. Did you
|
36
|
+
abort("No current/specified work item. Did you forget to `pick`?")
|
37
|
+
end
|
38
|
+
|
39
|
+
def prompt_project!
|
40
|
+
@path = Services::ProjectPicker.call(cli: cli).path
|
40
41
|
end
|
41
42
|
|
42
|
-
def
|
43
|
-
|
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
|
44
48
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
50
70
|
end
|
51
71
|
|
52
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
|
@@ -42,25 +42,6 @@ module Abt
|
|
42
42
|
end
|
43
43
|
abort("No such work item: ##{work_item_id}") if work_item_id && work_item.nil?
|
44
44
|
end
|
45
|
-
|
46
|
-
def board
|
47
|
-
@board ||= begin
|
48
|
-
warn("Fetching board...")
|
49
|
-
api.get("work/boards/#{board_id}")
|
50
|
-
rescue HttpError::NotFoundError
|
51
|
-
nil
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def work_item
|
56
|
-
@work_item ||= begin
|
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
|
63
|
-
end
|
64
45
|
end
|
65
46
|
end
|
66
47
|
end
|
@@ -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 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
|
28
29
|
end
|
29
30
|
|
30
31
|
private
|
@@ -49,13 +50,6 @@ module Abt
|
|
49
50
|
work_item["name"]
|
50
51
|
].join(" ")
|
51
52
|
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
53
|
end
|
60
54
|
end
|
61
55
|
end
|
@@ -15,67 +15,31 @@ 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
|
-
|
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
|
-
work_items = api.work_item_query(
|
62
|
-
<<~WIQL
|
63
|
-
SELECT [System.Id]
|
64
|
-
FROM WorkItems
|
65
|
-
WHERE [System.BoardColumn] = '#{column['name']}'
|
66
|
-
ORDER BY [Microsoft.VSTS.Common.BacklogPriority] ASC
|
67
|
-
WIQL
|
68
|
-
)
|
69
|
-
|
70
|
-
work_items.map { |work_item| sanitize_work_item(work_item) }
|
71
|
-
end
|
72
|
-
|
73
|
-
def columns
|
74
|
-
board["columns"]
|
75
|
-
end
|
37
|
+
private
|
76
38
|
|
77
|
-
def
|
78
|
-
|
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!
|
79
43
|
end
|
80
44
|
end
|
81
45
|
end
|