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
@@ -14,8 +14,7 @@ module Abt
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
|
-
|
18
|
-
|
17
|
+
require_local_config!
|
19
18
|
require_project!
|
20
19
|
ensure_valid_configuration!
|
21
20
|
|
@@ -37,24 +36,6 @@ module Abt
|
|
37
36
|
abort("Invalid project: #{project_gid}") if project.nil?
|
38
37
|
abort("Invalid task: #{task_gid}") if task_gid && task.nil?
|
39
38
|
end
|
40
|
-
|
41
|
-
def project
|
42
|
-
@project ||= begin
|
43
|
-
warn("Fetching project...")
|
44
|
-
api.get("projects/#{project_gid}", opt_fields: "name,permalink_url")
|
45
|
-
rescue Abt::HttpError::NotFoundError
|
46
|
-
nil
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def task
|
51
|
-
@task ||= begin
|
52
|
-
warn("Fetching task...")
|
53
|
-
api.get("tasks/#{task_gid}", opt_fields: "name,permalink_url")
|
54
|
-
rescue Abt::HttpError::NotFoundError
|
55
|
-
nil
|
56
|
-
end
|
57
|
-
end
|
58
39
|
end
|
59
40
|
end
|
60
41
|
end
|
@@ -18,6 +18,12 @@ module Abt
|
|
18
18
|
require_task!
|
19
19
|
print_task(project_gid, task)
|
20
20
|
|
21
|
+
maybe_move_task
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def maybe_move_task
|
21
27
|
if task_already_in_finalized_section?
|
22
28
|
warn("Task already in section: #{current_task_section['name']}")
|
23
29
|
else
|
@@ -26,8 +32,6 @@ module Abt
|
|
26
32
|
end
|
27
33
|
end
|
28
34
|
|
29
|
-
private
|
30
|
-
|
31
35
|
def task_already_in_finalized_section?
|
32
36
|
!task_section_membership.nil?
|
33
37
|
end
|
@@ -17,7 +17,13 @@ module Abt
|
|
17
17
|
require_task!
|
18
18
|
ensure_current_is_valid!
|
19
19
|
|
20
|
-
body
|
20
|
+
puts Oj.dump(body, mode: :json)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def body
|
26
|
+
{
|
21
27
|
notes: task["name"],
|
22
28
|
external_reference: {
|
23
29
|
id: task_gid.to_i,
|
@@ -25,12 +31,8 @@ module Abt
|
|
25
31
|
permalink: task["permalink_url"]
|
26
32
|
}
|
27
33
|
}
|
28
|
-
|
29
|
-
puts Oj.dump(body, mode: :json)
|
30
34
|
end
|
31
35
|
|
32
|
-
private
|
33
|
-
|
34
36
|
def ensure_current_is_valid!
|
35
37
|
abort("Invalid task gid: #{task_gid}") if task.nil?
|
36
38
|
|
@@ -10,69 +10,35 @@ module Abt
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
"Pick task
|
13
|
+
"Pick a task and - unless told not to - make it current"
|
14
14
|
end
|
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 configuration"]
|
19
20
|
]
|
20
21
|
end
|
21
22
|
|
22
23
|
def perform
|
23
|
-
|
24
|
-
require_project!
|
25
|
-
|
26
|
-
warn(project["name"])
|
27
|
-
|
28
|
-
task = select_task
|
24
|
+
pick!
|
29
25
|
|
30
26
|
print_task(project, task)
|
31
27
|
|
32
28
|
return if flags[:"dry-run"]
|
33
29
|
|
34
|
-
config.
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
def project
|
40
|
-
@project ||= api.get("projects/#{project_gid}", opt_fields: "name")
|
41
|
-
end
|
42
|
-
|
43
|
-
def select_task
|
44
|
-
loop do
|
45
|
-
section = cli.prompt.choice("Which section?", sections)
|
46
|
-
warn("Fetching tasks...")
|
47
|
-
tasks = tasks_in_section(section)
|
48
|
-
|
49
|
-
if tasks.length.zero?
|
50
|
-
warn("Section is empty")
|
51
|
-
next
|
52
|
-
end
|
53
|
-
|
54
|
-
task = cli.prompt.choice("Select a task", tasks, nil_option: true)
|
55
|
-
return task if task
|
30
|
+
if config.local_available?
|
31
|
+
config.path = Path.from_gids(project_gid: project["gid"], task_gid: task["gid"])
|
32
|
+
else
|
33
|
+
warn("No local configuration to update - will function as dry run")
|
56
34
|
end
|
57
35
|
end
|
58
36
|
|
59
|
-
|
60
|
-
tasks = api.get_paged(
|
61
|
-
"tasks",
|
62
|
-
section: section["gid"],
|
63
|
-
opt_fields: "name,completed,permalink_url"
|
64
|
-
)
|
65
|
-
|
66
|
-
# The below filtering is the best we can do with Asanas api, see this:
|
67
|
-
# https://forum.asana.com/t/tasks-query-completed-since-is-broken-for-sections/21461
|
68
|
-
tasks.reject { |task| task["completed"] }
|
69
|
-
end
|
37
|
+
private
|
70
38
|
|
71
|
-
def
|
72
|
-
|
73
|
-
|
74
|
-
api.get_paged("projects/#{project_gid}/sections", opt_fields: "name")
|
75
|
-
end
|
39
|
+
def pick!
|
40
|
+
prompt_project! if project_gid.nil? || flags[:clean]
|
41
|
+
prompt_task!
|
76
42
|
end
|
77
43
|
end
|
78
44
|
end
|
@@ -42,19 +42,25 @@ module Abt
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def update_assignee_if_needed
|
45
|
-
current_assignee = task["assignee"]
|
46
|
-
|
47
45
|
if current_assignee.nil?
|
48
46
|
warn("Assigning task to user: #{current_user['name']}")
|
49
47
|
update_assignee
|
50
48
|
elsif current_assignee["gid"] == current_user["gid"]
|
51
49
|
warn("You are already assigned to this task")
|
52
|
-
elsif
|
50
|
+
elsif should_reassign?
|
53
51
|
warn("Reassigning task to user: #{current_user['name']}")
|
54
52
|
update_assignee
|
55
53
|
end
|
56
54
|
end
|
57
55
|
|
56
|
+
def current_assignee
|
57
|
+
task["assignee"]
|
58
|
+
end
|
59
|
+
|
60
|
+
def should_reassign?
|
61
|
+
cli.prompt.boolean("Task is assigned to: #{current_assignee['name']}, take over?")
|
62
|
+
end
|
63
|
+
|
58
64
|
def move_if_needed
|
59
65
|
unless project_gid == config.path.project_gid
|
60
66
|
warn("Task was not moved, this is not implemented for tasks outside current project")
|
@@ -14,7 +14,7 @@ module Abt
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
|
-
|
17
|
+
prompt_project! unless project_gid
|
18
18
|
|
19
19
|
tasks.each do |task|
|
20
20
|
print_task(project, task)
|
@@ -23,14 +23,9 @@ module Abt
|
|
23
23
|
|
24
24
|
private
|
25
25
|
|
26
|
-
def project
|
27
|
-
@project ||= begin
|
28
|
-
api.get("projects/#{project_gid}", opt_fields: "name")
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
26
|
def tasks
|
33
27
|
@tasks ||= begin
|
28
|
+
project
|
34
29
|
warn("Fetching tasks...")
|
35
30
|
tasks = api.get_paged("tasks", project: project["gid"], opt_fields: "name,completed")
|
36
31
|
tasks.reject { |task| task["completed"] }
|
@@ -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)
|
@@ -26,7 +26,7 @@ module Abt
|
|
26
26
|
@workspace_gid ||= begin
|
27
27
|
current = git_global["workspaceGid"]
|
28
28
|
if current.nil?
|
29
|
-
|
29
|
+
prompt_workspace_gid
|
30
30
|
else
|
31
31
|
current
|
32
32
|
end
|
@@ -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
|
+
Abt.directory_config.fetch("asana", {})
|
75
|
+
end
|
76
|
+
|
69
77
|
def git
|
70
78
|
@git ||= GitConfig.new("local", "abt.asana")
|
71
79
|
end
|
@@ -92,20 +100,28 @@ module Abt
|
|
92
100
|
cli.prompt.choice(message, sections)
|
93
101
|
end
|
94
102
|
|
95
|
-
def
|
96
|
-
cli.
|
97
|
-
|
98
|
-
if workspaces.
|
99
|
-
cli.abort("Your asana access token does not have access to any workspaces")
|
100
|
-
elsif workspaces.one?
|
103
|
+
def prompt_workspace_gid
|
104
|
+
cli.abort("Your asana access token does not have access to any workspaces") if workspaces.empty?
|
105
|
+
|
106
|
+
if workspaces.one?
|
101
107
|
workspace = workspaces.first
|
102
108
|
cli.warn("Selected Asana workspace: #{workspace['name']}")
|
103
109
|
else
|
104
|
-
workspace =
|
110
|
+
workspace = pick_workspace
|
105
111
|
end
|
106
112
|
|
107
113
|
git_global["workspaceGid"] = workspace["gid"]
|
108
|
-
|
114
|
+
end
|
115
|
+
|
116
|
+
def pick_workspace
|
117
|
+
cli.prompt.choice("Select Asana workspace", workspaces)
|
118
|
+
end
|
119
|
+
|
120
|
+
def workspaces
|
121
|
+
@workspaces ||= begin
|
122
|
+
cli.warn("Fetching workspaces...")
|
123
|
+
api.get_paged("workspaces", opt_fields: "name")
|
124
|
+
end
|
109
125
|
end
|
110
126
|
|
111
127
|
def api
|
@@ -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
|
@@ -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.new([path, task["gid"]].join("/"))
|
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
|