abt-cli 0.0.22 → 0.0.23
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/abt +1 -1
- data/lib/abt/cli/prompt.rb +26 -45
- data/lib/abt/docs/markdown.rb +5 -2
- data/lib/abt/helpers.rb +26 -8
- data/lib/abt/providers/asana/base_command.rb +4 -0
- data/lib/abt/providers/asana/commands/current.rb +1 -2
- 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/init.rb +2 -2
- data/lib/abt/providers/asana/commands/pick.rb +11 -15
- data/lib/abt/providers/asana/commands/start.rb +9 -3
- data/lib/abt/providers/asana/configuration.rb +17 -9
- data/lib/abt/providers/asana/path.rb +1 -1
- data/lib/abt/providers/devops/base_command.rb +4 -0
- data/lib/abt/providers/devops/commands/current.rb +1 -2
- data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +11 -9
- data/lib/abt/providers/devops/commands/init.rb +6 -3
- data/lib/abt/providers/devops/commands/pick.rb +20 -14
- data/lib/abt/providers/devops/configuration.rb +9 -13
- data/lib/abt/providers/devops/path.rb +4 -4
- data/lib/abt/providers/git/commands/branch.rb +4 -2
- data/lib/abt/providers/harvest/base_command.rb +4 -0
- data/lib/abt/providers/harvest/commands/current.rb +1 -2
- data/lib/abt/providers/harvest/commands/init.rb +2 -3
- data/lib/abt/providers/harvest/commands/pick.rb +7 -3
- data/lib/abt/providers/harvest/commands/track.rb +33 -19
- data/lib/abt/providers/harvest/path.rb +1 -1
- data/lib/abt/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0a7665ad954a2011f10df120e0dd6ca13a0ab4ef9e0aea0338be4ec2e6cc62a3
|
4
|
+
data.tar.gz: 7c50793f7c8cf9d3d4fb88090dec2803691a4acfc931d39135aed85d0226b00f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1db215088d055c4857f37ac01295afa0551d9ed88b11917971ff64ff8b8761db833422b9a52f83751e3b6cf476813fff205160d04443d7a8d7b0af21ce4d755c
|
7
|
+
data.tar.gz: 1e89308cc2a26401cfb5e06fd37dc2a80aef260a5bad6c0da0473e61d1469b92457bf14299dc823eca526326c3239ef1b060f0fd747d206ca55eb25a9ad6cf52
|
data/bin/abt
CHANGED
data/lib/abt/cli/prompt.rb
CHANGED
@@ -11,7 +11,7 @@ module Abt
|
|
11
11
|
|
12
12
|
def text(question)
|
13
13
|
output.print("#{question.strip}: ")
|
14
|
-
read_user_input
|
14
|
+
Abt::Helpers.read_user_input
|
15
15
|
end
|
16
16
|
|
17
17
|
def boolean(text)
|
@@ -20,12 +20,10 @@ module Abt
|
|
20
20
|
loop do
|
21
21
|
output.print("(y / n): ")
|
22
22
|
|
23
|
-
case read_user_input
|
23
|
+
case Abt::Helpers.read_user_input
|
24
24
|
when "y", "Y" then return true
|
25
25
|
when "n", "N" then return false
|
26
|
-
else
|
27
|
-
output.puts "Invalid choice"
|
28
|
-
end
|
26
|
+
else output.puts "Invalid choice" end
|
29
27
|
end
|
30
28
|
end
|
31
29
|
|
@@ -40,7 +38,7 @@ module Abt
|
|
40
38
|
end
|
41
39
|
|
42
40
|
print_options(options)
|
43
|
-
|
41
|
+
select_option(options, nil_option)
|
44
42
|
end
|
45
43
|
|
46
44
|
def search(text, options)
|
@@ -60,40 +58,37 @@ module Abt
|
|
60
58
|
end
|
61
59
|
end
|
62
60
|
|
63
|
-
def
|
64
|
-
|
65
|
-
number = read_option_number(options.length, nil_option)
|
66
|
-
if number.nil?
|
67
|
-
return nil if nil_option
|
68
|
-
|
69
|
-
next
|
70
|
-
end
|
61
|
+
def select_option(options, nil_option)
|
62
|
+
number = prompt_valid_option_number(options, nil_option)
|
71
63
|
|
72
|
-
|
64
|
+
return nil if number.nil?
|
73
65
|
|
74
|
-
|
75
|
-
|
76
|
-
|
66
|
+
option = options[number - 1]
|
67
|
+
output.puts "Selected: (#{number}) #{option['name']}"
|
68
|
+
option
|
77
69
|
end
|
78
70
|
|
79
|
-
def
|
80
|
-
|
81
|
-
|
82
|
-
str += nil_option_string(nil_option)
|
83
|
-
str += "): "
|
84
|
-
output.print(str)
|
85
|
-
|
86
|
-
input = read_user_input
|
71
|
+
def prompt_valid_option_number(options, nil_option)
|
72
|
+
output.print(options_info(options, nil_option))
|
73
|
+
input = Abt::Helpers.read_user_input
|
87
74
|
|
88
75
|
return nil if nil_option && input == nil_option_character(nil_option)
|
89
76
|
|
90
77
|
option_number = input.to_i
|
91
|
-
|
92
|
-
output.puts "Invalid selection"
|
93
|
-
return nil
|
94
|
-
end
|
78
|
+
return option_number if (1..options.length).cover?(option_number)
|
95
79
|
|
96
|
-
|
80
|
+
output.puts "Invalid selection"
|
81
|
+
|
82
|
+
# Prompt again if the selection was invalid
|
83
|
+
prompt_valid_option_number(options, nil_option)
|
84
|
+
end
|
85
|
+
|
86
|
+
def options_info(options, nil_option)
|
87
|
+
str = "("
|
88
|
+
str += options.length > 1 ? "1-#{options.length}" : "1"
|
89
|
+
str += nil_option_string(nil_option)
|
90
|
+
str += "): "
|
91
|
+
str
|
97
92
|
end
|
98
93
|
|
99
94
|
def nil_option_string(nil_option)
|
@@ -115,10 +110,6 @@ module Abt
|
|
115
110
|
nil_option[1]
|
116
111
|
end
|
117
112
|
|
118
|
-
def read_user_input
|
119
|
-
open(tty_path, &:gets).strip # rubocop:disable Security/Open
|
120
|
-
end
|
121
|
-
|
122
113
|
def get_search_result(options)
|
123
114
|
matches = matches_for_string(text("Enter search"), options)
|
124
115
|
if matches.empty?
|
@@ -141,16 +132,6 @@ module Abt
|
|
141
132
|
def sanitize_string(string)
|
142
133
|
string.downcase.gsub(/[^\w]/, "")
|
143
134
|
end
|
144
|
-
|
145
|
-
def tty_path
|
146
|
-
@tty_path ||= begin
|
147
|
-
candidates = ["/dev/tty", "CON:"] # Unix: '/dev/tty', Windows: 'CON:'
|
148
|
-
selected = candidates.find { |candidate| File.exist?(candidate) }
|
149
|
-
raise Abort, "Unable to prompt for user input" if selected.nil?
|
150
|
-
|
151
|
-
selected
|
152
|
-
end
|
153
|
-
end
|
154
135
|
end
|
155
136
|
end
|
156
137
|
end
|
data/lib/abt/docs/markdown.rb
CHANGED
@@ -50,8 +50,7 @@ module Abt
|
|
50
50
|
def example_commands
|
51
51
|
lines = []
|
52
52
|
|
53
|
-
|
54
|
-
examples.each_with_index do |(title, commands), index|
|
53
|
+
complete_examples.each_with_index do |(title, commands), index|
|
55
54
|
lines << "" unless index.zero?
|
56
55
|
lines << title
|
57
56
|
|
@@ -84,6 +83,10 @@ module Abt
|
|
84
83
|
lines.join("\n")
|
85
84
|
end
|
86
85
|
|
86
|
+
def complete_examples
|
87
|
+
Docs.basic_examples.merge(Docs.extended_examples)
|
88
|
+
end
|
89
|
+
|
87
90
|
def inflector
|
88
91
|
Dry::Inflector.new
|
89
92
|
end
|
data/lib/abt/helpers.rb
CHANGED
@@ -2,15 +2,33 @@
|
|
2
2
|
|
3
3
|
module Abt
|
4
4
|
module Helpers
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
class << self
|
6
|
+
def const_to_command(string)
|
7
|
+
string = string.to_s.dup
|
8
|
+
string[0] = string[0].downcase
|
9
|
+
string.gsub(/([A-Z])/, '-\1').downcase
|
10
|
+
end
|
11
|
+
|
12
|
+
def command_to_const(string)
|
13
|
+
inflector = Dry::Inflector.new
|
14
|
+
inflector.camelize(inflector.underscore(string))
|
15
|
+
end
|
16
|
+
|
17
|
+
def read_user_input
|
18
|
+
open(tty_path, &:gets).strip # rubocop:disable Security/Open
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def tty_path
|
24
|
+
@tty_path ||= begin
|
25
|
+
candidates = ["/dev/tty", "CON:"] # Unix: '/dev/tty', Windows: 'CON:'
|
26
|
+
selected = candidates.find { |candidate| File.exist?(candidate) }
|
27
|
+
raise Abort, "Unable to prompt for user input" if selected.nil?
|
10
28
|
|
11
|
-
|
12
|
-
|
13
|
-
|
29
|
+
selected
|
30
|
+
end
|
31
|
+
end
|
14
32
|
end
|
15
33
|
end
|
16
34
|
end
|
@@ -20,6 +20,10 @@ module Abt
|
|
20
20
|
|
21
21
|
private
|
22
22
|
|
23
|
+
def require_local_config!
|
24
|
+
abort("Must be run inside a git repository") unless config.local_available?
|
25
|
+
end
|
26
|
+
|
23
27
|
def require_project!
|
24
28
|
abort("No current/specified project. Did you initialize Asana?") if project_gid.nil?
|
25
29
|
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
|
|
@@ -14,12 +14,12 @@ module Abt
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
|
-
|
17
|
+
require_local_config!
|
18
18
|
|
19
19
|
projects # Load projects up front to make it obvious that searches are instant
|
20
20
|
project = cli.prompt.search("Select a project", projects)
|
21
21
|
|
22
|
-
config.path = Path.from_ids(project["gid"])
|
22
|
+
config.path = Path.from_ids(project_gid: project["gid"])
|
23
23
|
|
24
24
|
print_project(project)
|
25
25
|
end
|
@@ -20,18 +20,17 @@ module Abt
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def perform
|
23
|
-
|
23
|
+
require_local_config!
|
24
24
|
require_project!
|
25
25
|
|
26
26
|
warn(project["name"])
|
27
|
-
|
28
27
|
task = select_task
|
29
28
|
|
30
29
|
print_task(project, task)
|
31
30
|
|
32
31
|
return if flags[:"dry-run"]
|
33
32
|
|
34
|
-
config.path = Path.from_ids(project_gid, task["gid"])
|
33
|
+
config.path = Path.from_ids(project_gid: project_gid, task_gid: task["gid"])
|
35
34
|
end
|
36
35
|
|
37
36
|
private
|
@@ -41,18 +40,15 @@ module Abt
|
|
41
40
|
end
|
42
41
|
|
43
42
|
def select_task
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
task = cli.prompt.choice("Select a task", tasks, nil_option: true)
|
55
|
-
return task if task
|
43
|
+
section = cli.prompt.choice("Which section?", sections)
|
44
|
+
warn("Fetching tasks...")
|
45
|
+
tasks = tasks_in_section(section)
|
46
|
+
|
47
|
+
if tasks.length.zero?
|
48
|
+
warn("Section is empty")
|
49
|
+
select_task
|
50
|
+
else
|
51
|
+
cli.prompt.choice("Select a task", tasks, nil_option: true) || select_task
|
56
52
|
end
|
57
53
|
end
|
58
54
|
|
@@ -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")
|
@@ -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
|
@@ -92,20 +92,28 @@ module Abt
|
|
92
92
|
cli.prompt.choice(message, sections)
|
93
93
|
end
|
94
94
|
|
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?
|
95
|
+
def prompt_workspace_gid
|
96
|
+
cli.abort("Your asana access token does not have access to any workspaces") if workspaces.empty?
|
97
|
+
|
98
|
+
if workspaces.one?
|
101
99
|
workspace = workspaces.first
|
102
100
|
cli.warn("Selected Asana workspace: #{workspace['name']}")
|
103
101
|
else
|
104
|
-
workspace =
|
102
|
+
workspace = pick_workspace
|
105
103
|
end
|
106
104
|
|
107
105
|
git_global["workspaceGid"] = workspace["gid"]
|
108
|
-
|
106
|
+
end
|
107
|
+
|
108
|
+
def pick_workspace
|
109
|
+
cli.prompt.choice("Select Asana workspace", workspaces)
|
110
|
+
end
|
111
|
+
|
112
|
+
def workspaces
|
113
|
+
@workspaces ||= begin
|
114
|
+
cli.warn("Fetching workspaces...")
|
115
|
+
api.get_paged("workspaces", opt_fields: "name")
|
116
|
+
end
|
109
117
|
end
|
110
118
|
|
111
119
|
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.from_ids(project_gid
|
9
|
+
def self.from_ids(project_gid: nil, task_gid: nil)
|
10
10
|
path = project_gid ? [project_gid, *task_gid].join("/") : ""
|
11
11
|
new(path)
|
12
12
|
end
|
@@ -16,15 +16,6 @@ module Abt
|
|
16
16
|
def perform
|
17
17
|
require_work_item!
|
18
18
|
|
19
|
-
body = {
|
20
|
-
notes: notes,
|
21
|
-
external_reference: {
|
22
|
-
id: work_item["id"],
|
23
|
-
group_id: "AzureDevOpsWorkItem",
|
24
|
-
permalink: work_item["url"]
|
25
|
-
}
|
26
|
-
}
|
27
|
-
|
28
19
|
puts Oj.dump(body, mode: :json)
|
29
20
|
rescue HttpError::NotFoundError
|
30
21
|
args = [organization_name, project_name, board_id, work_item_id].compact
|
@@ -38,6 +29,17 @@ module Abt
|
|
38
29
|
|
39
30
|
private
|
40
31
|
|
32
|
+
def body
|
33
|
+
{
|
34
|
+
notes: notes,
|
35
|
+
external_reference: {
|
36
|
+
id: work_item["id"],
|
37
|
+
group_id: "AzureDevOpsWorkItem",
|
38
|
+
permalink: work_item["url"]
|
39
|
+
}
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
41
43
|
def notes
|
42
44
|
[
|
43
45
|
"Azure DevOps",
|
@@ -17,11 +17,14 @@ module Abt
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def perform
|
20
|
-
|
21
|
-
|
20
|
+
require_local_config!
|
22
21
|
board = cli.prompt.choice("Select a project work board", boards)
|
23
22
|
|
24
|
-
config.path = Path.from_ids(
|
23
|
+
config.path = Path.from_ids(
|
24
|
+
organization_name: organization_name,
|
25
|
+
project_name: project_name,
|
26
|
+
board_id: board["id"]
|
27
|
+
)
|
25
28
|
print_board(organization_name, project_name, board)
|
26
29
|
end
|
27
30
|
|
@@ -20,7 +20,7 @@ module Abt
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def perform
|
23
|
-
|
23
|
+
require_local_config!
|
24
24
|
require_board!
|
25
25
|
|
26
26
|
warn("#{project_name} - #{board['name']}")
|
@@ -30,24 +30,30 @@ module Abt
|
|
30
30
|
|
31
31
|
return if flags[:"dry-run"]
|
32
32
|
|
33
|
-
|
33
|
+
update_config(work_item)
|
34
34
|
end
|
35
35
|
|
36
36
|
private
|
37
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
|
+
|
38
47
|
def select_work_item
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
work_item = cli.prompt.choice("Select a work item", work_items, nil_option: true)
|
50
|
-
return work_item if 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
|
55
|
+
else
|
56
|
+
cli.prompt.choice("Select a work item", work_items, nil_option: true) || select_work_item
|
51
57
|
end
|
52
58
|
end
|
53
59
|
|
@@ -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 && board_id
|
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 = "")
|
@@ -61,8 +61,6 @@ module Abt
|
|
61
61
|
end
|
62
62
|
|
63
63
|
def branch_names_from_aris
|
64
|
-
other_aris = cli.aris - [ari]
|
65
|
-
|
66
64
|
abort("You must provide an additional ARI that responds to: branch-name. E.g., asana") if other_aris.empty?
|
67
65
|
|
68
66
|
input = StringIO.new(cli.aris.to_s)
|
@@ -71,6 +69,10 @@ module Abt
|
|
71
69
|
|
72
70
|
output.string.lines.map(&:strip).compact
|
73
71
|
end
|
72
|
+
|
73
|
+
def other_aris
|
74
|
+
@other_aris ||= cli.aris - [ari]
|
75
|
+
end
|
74
76
|
end
|
75
77
|
end
|
76
78
|
end
|
@@ -14,12 +14,11 @@ module Abt
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
|
-
|
18
|
-
|
17
|
+
require_local_config!
|
19
18
|
projects # Load projects up front to make it obvious that searches are instant
|
20
19
|
project = cli.prompt.search("Select a project", searchable_projects)["project"]
|
21
20
|
|
22
|
-
config.path = Path.from_ids(project["id"])
|
21
|
+
config.path = Path.from_ids(project_id: project["id"])
|
23
22
|
|
24
23
|
print_project(project)
|
25
24
|
end
|
@@ -20,17 +20,17 @@ module Abt
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def perform
|
23
|
-
|
23
|
+
require_local_config!
|
24
24
|
require_project!
|
25
25
|
|
26
26
|
warn(project["name"])
|
27
|
-
task =
|
27
|
+
task = pick_task
|
28
28
|
|
29
29
|
print_task(project, task)
|
30
30
|
|
31
31
|
return if flags[:"dry-run"]
|
32
32
|
|
33
|
-
config.path = Path.from_ids(project_id, task["id"])
|
33
|
+
config.path = Path.from_ids(project_id: project_id, task_id: task["id"])
|
34
34
|
end
|
35
35
|
|
36
36
|
private
|
@@ -39,6 +39,10 @@ module Abt
|
|
39
39
|
project_assignment["project"]
|
40
40
|
end
|
41
41
|
|
42
|
+
def pick_task
|
43
|
+
cli.prompt.choice("Select a task", tasks)
|
44
|
+
end
|
45
|
+
|
42
46
|
def tasks
|
43
47
|
@tasks ||= project_assignment["task_assignments"].map { |ta| ta["task"] }
|
44
48
|
end
|
@@ -42,8 +42,7 @@ module Abt
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def create_time_entry
|
45
|
-
body =
|
46
|
-
body[:hours] = flags[:time] if flags.key?(:time)
|
45
|
+
body = time_entry_data
|
47
46
|
|
48
47
|
result = api.post("time_entries", Oj.dump(body, mode: :json))
|
49
48
|
|
@@ -52,47 +51,62 @@ module Abt
|
|
52
51
|
result
|
53
52
|
end
|
54
53
|
|
54
|
+
def time_entry_data
|
55
|
+
body = time_entry_base_data
|
56
|
+
|
57
|
+
maybe_add_external_link(body)
|
58
|
+
maybe_add_comment(body)
|
59
|
+
maybe_add_time(body)
|
60
|
+
|
61
|
+
body
|
62
|
+
end
|
63
|
+
|
55
64
|
def time_entry_base_data
|
56
|
-
|
65
|
+
{
|
57
66
|
project_id: project_id,
|
58
67
|
task_id: task_id,
|
59
68
|
user_id: config.user_id,
|
60
69
|
spent_date: Date.today.iso8601
|
61
70
|
}
|
71
|
+
end
|
62
72
|
|
73
|
+
def maybe_add_external_link(body)
|
63
74
|
if external_link_data
|
64
75
|
warn(<<~TXT)
|
65
76
|
Linking to:
|
66
|
-
|
67
|
-
|
77
|
+
#{external_link_data[:notes]}
|
78
|
+
#{external_link_data[:external_reference][:permalink]}
|
68
79
|
TXT
|
69
80
|
body.merge!(external_link_data)
|
70
81
|
else
|
71
82
|
warn("No external link provided")
|
72
83
|
end
|
84
|
+
end
|
73
85
|
|
86
|
+
def maybe_add_comment(body)
|
74
87
|
body[:notes] = flags[:comment] if flags.key?(:comment)
|
75
88
|
body[:notes] ||= cli.prompt.text("Fill in comment (optional)")
|
76
|
-
|
89
|
+
end
|
90
|
+
|
91
|
+
def maybe_add_time(body)
|
92
|
+
body[:hours] = flags[:time] if flags.key?(:time)
|
77
93
|
end
|
78
94
|
|
79
95
|
def external_link_data
|
80
|
-
@external_link_data
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
end
|
89
|
-
|
90
|
-
Oj.load(lines.first, symbol_keys: true)
|
91
|
-
end
|
96
|
+
return @external_link_data if instance_variable_defined?(:@external_link_data)
|
97
|
+
|
98
|
+
lines = fetch_link_data_lines
|
99
|
+
|
100
|
+
return @external_link_data = nil if lines.empty?
|
101
|
+
|
102
|
+
if lines.length > 1
|
103
|
+
abort("Got reference data from multiple scheme providers, only one is supported at a time")
|
92
104
|
end
|
105
|
+
|
106
|
+
@external_link_data = Oj.load(lines.first, symbol_keys: true)
|
93
107
|
end
|
94
108
|
|
95
|
-
def
|
109
|
+
def fetch_link_data_lines
|
96
110
|
other_aris = cli.aris - [ari]
|
97
111
|
return [] if other_aris.empty?
|
98
112
|
|
@@ -6,7 +6,7 @@ module Abt
|
|
6
6
|
class Path < String
|
7
7
|
PATH_REGEX = %r{^(?<project_id>\d+)?/?(?<task_id>\d+)?$}.freeze
|
8
8
|
|
9
|
-
def self.from_ids(project_id
|
9
|
+
def self.from_ids(project_id: nil, task_id: nil)
|
10
10
|
path = project_id ? [project_id, *task_id].join("/") : ""
|
11
11
|
new(path)
|
12
12
|
end
|
data/lib/abt/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: abt-cli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.23
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jesper Sørensen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-03-
|
11
|
+
date: 2021-03-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-inflector
|