abt-cli 0.0.22 → 0.0.23
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/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
|