jira-cli 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/jira.rb +0 -14
- data/lib/jira/api.rb +51 -82
- data/lib/jira/command.rb +36 -0
- data/lib/jira/commands/all.rb +75 -0
- data/lib/jira/commands/assign.rb +53 -7
- data/lib/jira/commands/attachments.rb +27 -4
- data/lib/jira/commands/comment.rb +3 -3
- data/lib/jira/commands/delete.rb +81 -27
- data/lib/jira/commands/describe.rb +51 -68
- data/lib/jira/commands/install.rb +54 -16
- data/lib/jira/commands/link.rb +68 -38
- data/lib/jira/commands/log.rb +4 -4
- data/lib/jira/commands/new.rb +137 -79
- data/lib/jira/commands/rename.rb +44 -10
- data/lib/jira/commands/sprint.rb +80 -0
- data/lib/jira/commands/tickets.rb +28 -4
- data/lib/jira/commands/transition.rb +60 -24
- data/lib/jira/commands/vote.rb +89 -16
- data/lib/jira/commands/watch.rb +90 -16
- data/lib/jira/constants.rb +1 -1
- data/lib/jira/core.rb +25 -53
- data/lib/jira/legacy_api.rb +102 -0
- data/lib/jira/mixins.rb +4 -8
- data/lib/jira/sprint_api.rb +49 -0
- metadata +69 -11
- data/lib/jira/io.rb +0 -17
@@ -1,85 +1,68 @@
|
|
1
|
+
require_relative '../command'
|
2
|
+
|
1
3
|
module Jira
|
2
4
|
class CLI < Thor
|
3
5
|
|
4
6
|
desc "describe", "Describes the input ticket"
|
5
7
|
def describe(ticket=Jira::Core.ticket)
|
6
|
-
|
7
|
-
output = description(ticket.strip, false, true, true)
|
8
|
-
puts output if !output.strip.empty?
|
9
|
-
end
|
8
|
+
Command::Describe.new(ticket).run
|
10
9
|
end
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
branches.each do |branch|
|
22
|
-
ticket = branch.delete('*').strip
|
23
|
-
if Jira::Core.ticket?(ticket, false)
|
24
|
-
if branch.include?('*')
|
25
|
-
tickets[:current] = ticket
|
26
|
-
else
|
27
|
-
tickets[:others] << ticket
|
28
|
-
end
|
29
|
-
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Command
|
14
|
+
class Describe < Base
|
15
|
+
|
16
|
+
attr_accessor :ticket
|
17
|
+
|
18
|
+
def initialize(ticket)
|
19
|
+
self.ticket = ticket
|
30
20
|
end
|
31
21
|
|
22
|
+
def run
|
23
|
+
return if json.empty?
|
24
|
+
return if errored?
|
25
|
+
render_table(header, [row])
|
26
|
+
end
|
32
27
|
|
33
|
-
|
34
|
-
|
35
|
-
threads = []
|
36
|
-
if !tickets[:current].nil?
|
37
|
-
threads << Thread.new{ puts description(tickets[:current], true) }
|
28
|
+
def header
|
29
|
+
[ 'Ticket', 'Assignee', 'Status', 'Summary' ]
|
38
30
|
end
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
31
|
+
|
32
|
+
def row
|
33
|
+
[ ticket, assignee, status, summary ]
|
34
|
+
end
|
35
|
+
|
36
|
+
def errored?
|
37
|
+
return false if errors.empty?
|
38
|
+
puts errors
|
39
|
+
true
|
47
40
|
end
|
48
|
-
threads.each{ |thread| thread.join }
|
49
|
-
puts output if !output.empty?
|
50
|
-
end
|
51
41
|
|
52
|
-
|
53
|
-
|
54
|
-
#
|
55
|
-
# Returns a formatted description of the input ticket
|
56
|
-
#
|
57
|
-
# @param ticket [String] the ticket to describe
|
58
|
-
# @param star [Boolean] if true, adds a * indicator
|
59
|
-
#
|
60
|
-
# @return [String] formatted summary string
|
61
|
-
#
|
62
|
-
def description(ticket, star=false, verbose=false, describe=false)
|
63
|
-
self.api.get("issue/#{ticket}", nil, verbose) do |json|
|
64
|
-
summary = json['fields']['summary']
|
65
|
-
status = json['fields']['status']['name']
|
66
|
-
assignee = "nil"
|
67
|
-
if json['fields'].has_key?("assignee")
|
68
|
-
if !json['fields']['assignee'].nil?
|
69
|
-
assignee = json['fields']['assignee']['name']
|
70
|
-
end
|
71
|
-
end
|
72
|
-
description = describe ? "\n" + json['fields']['description'].to_s : ""
|
73
|
-
|
74
|
-
return Jira::Format.ticket(ticket) +
|
75
|
-
(star ? Jira::Format.star : " ") + " \t" +
|
76
|
-
("(" + Jira::Format.user(assignee) + ")").ljust(20) +
|
77
|
-
Jira::Format.status(status).ljust(26) +
|
78
|
-
Jira::Format.summary(summary) +
|
79
|
-
description
|
80
|
-
end
|
81
|
-
return ""
|
42
|
+
def errors
|
43
|
+
@errors ||= (json['errorMessages'] || []).join('. ')
|
82
44
|
end
|
83
45
|
|
46
|
+
def assignee
|
47
|
+
(fields['assignee'] || {})['name'] || 'Unassigned'
|
48
|
+
end
|
49
|
+
|
50
|
+
def status
|
51
|
+
(fields['status'] || {})['name'] || 'Unknown'
|
52
|
+
end
|
53
|
+
|
54
|
+
def summary
|
55
|
+
truncate(json['fields']['summary'], 45)
|
56
|
+
end
|
57
|
+
|
58
|
+
def fields
|
59
|
+
json['fields'] || {}
|
60
|
+
end
|
61
|
+
|
62
|
+
def json
|
63
|
+
@json ||= api.get "issue/#{ticket}"
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
84
67
|
end
|
85
68
|
end
|
@@ -1,29 +1,67 @@
|
|
1
|
-
|
2
|
-
# Installation Script
|
3
|
-
#
|
1
|
+
require_relative '../command'
|
4
2
|
|
5
3
|
module Jira
|
6
4
|
class CLI < Thor
|
7
5
|
|
8
|
-
desc "install", "Guides the user through JIRA installation"
|
6
|
+
desc "install", "Guides the user through JIRA CLI installation"
|
9
7
|
def install
|
8
|
+
Command::Install.new.run
|
9
|
+
end
|
10
10
|
|
11
|
-
|
11
|
+
end
|
12
12
|
|
13
|
-
|
13
|
+
module Command
|
14
|
+
class Install < Base
|
14
15
|
|
15
|
-
|
16
|
-
|
16
|
+
def run
|
17
|
+
io.say('Please enter your JIRA information.')
|
18
|
+
inifile[:global] = params
|
19
|
+
inifile.write
|
20
|
+
end
|
17
21
|
|
18
|
-
|
19
|
-
url: url,
|
20
|
-
username: username,
|
21
|
-
password: password
|
22
|
-
}
|
23
|
-
inifile.write
|
22
|
+
private
|
24
23
|
|
25
|
-
|
26
|
-
|
24
|
+
def params
|
25
|
+
args = {
|
26
|
+
url: url,
|
27
|
+
username: username,
|
28
|
+
}
|
29
|
+
response = io.select("Select an authentication type:", ["basic", "token"])
|
30
|
+
case response
|
31
|
+
when "basic"
|
32
|
+
args[:password] = password
|
33
|
+
when "token"
|
34
|
+
args[:token] = token
|
35
|
+
else
|
36
|
+
raise InstallationException
|
37
|
+
end
|
38
|
+
args
|
39
|
+
end
|
40
|
+
|
41
|
+
def url
|
42
|
+
io.ask("JIRA URL:")
|
43
|
+
end
|
44
|
+
|
45
|
+
def username
|
46
|
+
io.ask("JIRA username:")
|
47
|
+
end
|
27
48
|
|
49
|
+
def password
|
50
|
+
io.mask("JIRA password:")
|
51
|
+
end
|
52
|
+
|
53
|
+
def token
|
54
|
+
io.ask("JIRA token:")
|
55
|
+
end
|
56
|
+
|
57
|
+
def inifile
|
58
|
+
@inifile ||= IniFile.new(
|
59
|
+
comment: '#',
|
60
|
+
encoding: 'UTF-8',
|
61
|
+
filename: Jira::Core.cli_path
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
28
66
|
end
|
29
67
|
end
|
data/lib/jira/commands/link.rb
CHANGED
@@ -1,19 +1,44 @@
|
|
1
|
+
require_relative '../command'
|
2
|
+
|
1
3
|
module Jira
|
2
4
|
class CLI < Thor
|
3
5
|
|
4
6
|
desc "link", "Creates a link between two tickets in JIRA"
|
5
7
|
def link(ticket=Jira::Core.ticket)
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
8
|
+
Command::Link.new(ticket).run
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
module Command
|
14
|
+
class Link < Base
|
15
|
+
|
16
|
+
attr_accessor :ticket
|
17
|
+
|
18
|
+
def initialize(ticket)
|
19
|
+
self.ticket = ticket
|
20
|
+
end
|
21
|
+
|
22
|
+
def run
|
23
|
+
return if ticket.empty?
|
24
|
+
return if metadata.empty?
|
25
|
+
return if issue_link_type.empty?
|
26
|
+
return if outward_ticket.empty?
|
27
|
+
return unless invalid_ticket?
|
10
28
|
|
11
|
-
|
12
|
-
|
13
|
-
|
29
|
+
begin
|
30
|
+
api.post "issueLink",
|
31
|
+
params: params,
|
32
|
+
success: on_success,
|
33
|
+
failure: on_failure
|
34
|
+
rescue CommandException
|
35
|
+
end
|
36
|
+
end
|
14
37
|
|
15
|
-
|
16
|
-
|
38
|
+
private
|
39
|
+
|
40
|
+
def params
|
41
|
+
{
|
17
42
|
type: {
|
18
43
|
name: issue_link_type[:name]
|
19
44
|
},
|
@@ -24,41 +49,46 @@ module Jira
|
|
24
49
|
key: outward_ticket
|
25
50
|
}
|
26
51
|
}
|
27
|
-
|
28
|
-
self.api.post("issueLink", params) do |json|
|
29
|
-
puts "Successfully linked ticket #{ticket} to ticket #{outward_ticket}."
|
30
|
-
return
|
31
|
-
end
|
32
52
|
end
|
33
|
-
puts "No ticket linked."
|
34
|
-
end
|
35
53
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
# type hash
|
42
|
-
#
|
43
|
-
# @param json [Hash] issue link type metadata
|
44
|
-
#
|
45
|
-
# @return [Hash] selected issue link type metadata
|
46
|
-
#
|
47
|
-
def select_issue_link_type(json)
|
48
|
-
issue_link_types = {}
|
49
|
-
json['issueLinkTypes'].each do |issue_link_type|
|
54
|
+
def issue_link_type
|
55
|
+
return @issue_link_type unless @issue_link_type.nil?
|
56
|
+
|
57
|
+
types = {}
|
58
|
+
metadata['issueLinkTypes'].each do |type|
|
50
59
|
data = {
|
51
|
-
id:
|
52
|
-
name:
|
53
|
-
inward:
|
54
|
-
outward:
|
60
|
+
id: type['id'],
|
61
|
+
name: type['name'],
|
62
|
+
inward: type['inward'],
|
63
|
+
outward: type['outward']
|
55
64
|
}
|
56
|
-
|
65
|
+
types[type['name']] = data
|
57
66
|
end
|
58
|
-
|
59
|
-
|
60
|
-
|
67
|
+
choice = io.select("Select a link type:", types.keys)
|
68
|
+
@issue_link_type = types[choice]
|
69
|
+
end
|
70
|
+
|
71
|
+
def on_success
|
72
|
+
->{ puts "Successfully linked ticket #{ticket} to ticket #{outward_ticket}." }
|
61
73
|
end
|
62
74
|
|
75
|
+
def on_failure
|
76
|
+
->{ puts "No ticket linked." }
|
77
|
+
end
|
78
|
+
|
79
|
+
def outward_ticket
|
80
|
+
@outward_ticket ||= io.ask("Outward ticket:").strip
|
81
|
+
end
|
82
|
+
|
83
|
+
def invalid_ticket?
|
84
|
+
return true unless Jira::Core.ticket?(outward_ticket)
|
85
|
+
return false
|
86
|
+
end
|
87
|
+
|
88
|
+
def metadata
|
89
|
+
@metadata ||= api.get("issueLinkType")
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
63
93
|
end
|
64
94
|
end
|
data/lib/jira/commands/log.rb
CHANGED
@@ -3,7 +3,7 @@ module Jira
|
|
3
3
|
|
4
4
|
desc "log", "Logs work against the input ticket"
|
5
5
|
def log(ticket=Jira::Core.ticket)
|
6
|
-
time_spent = self.io.ask("Time spent on ticket #{ticket}")
|
6
|
+
time_spent = self.io.ask("Time spent on ticket #{ticket}:")
|
7
7
|
self.api.post("issue/#{ticket}/worklog", { timeSpent: time_spent }) do |json|
|
8
8
|
puts "Successfully logged #{time_spent} on ticket #{ticket}."
|
9
9
|
end
|
@@ -11,7 +11,7 @@ module Jira
|
|
11
11
|
|
12
12
|
desc "logd", "Deletes work against the input ticket"
|
13
13
|
def logd(ticket=Jira::Core.ticket)
|
14
|
-
logs(ticket) if self.io.
|
14
|
+
logs(ticket) if self.io.yes?("List worklogs for ticket #{ticket}?")
|
15
15
|
|
16
16
|
index = self.get_type_of_index("worklog", "delete")
|
17
17
|
puts "No worklog deleted." and return if index < 0
|
@@ -53,12 +53,12 @@ module Jira
|
|
53
53
|
|
54
54
|
desc "logu", "Updates work against the input ticket"
|
55
55
|
def logu(ticket=Jira::Core.ticket)
|
56
|
-
logs(ticket) if self.io.
|
56
|
+
logs(ticket) if self.io.yes?("List worklogs for ticket #{ticket}?")
|
57
57
|
|
58
58
|
index = self.get_type_of_index("worklog", "update")
|
59
59
|
puts "No worklog updated." and return if index < 0
|
60
60
|
|
61
|
-
time_spent = self.io.ask("Time spent on #{ticket}").strip
|
61
|
+
time_spent = self.io.ask("Time spent on #{ticket}:").strip
|
62
62
|
puts "No worklog updated." and return if time_spent.empty?
|
63
63
|
|
64
64
|
self.api.get("issue/#{ticket}/worklog") do |json|
|
data/lib/jira/commands/new.rb
CHANGED
@@ -1,97 +1,155 @@
|
|
1
|
+
require_relative '../command'
|
2
|
+
|
1
3
|
module Jira
|
2
4
|
class CLI < Thor
|
3
5
|
|
4
6
|
desc "new", "Creates a new ticket in JIRA and checks out the git branch"
|
5
|
-
def new
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
7
|
+
def new
|
8
|
+
Command::New.new.run
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
module Command
|
14
|
+
class New < Base
|
15
|
+
|
16
|
+
def run
|
17
|
+
return if metadata.empty?
|
18
|
+
return if project.empty?
|
19
|
+
return if project_metadata.empty?
|
20
|
+
components # Select components if any after a project
|
21
|
+
return if issue_type.empty?
|
22
|
+
return if assign_parent? && parent.empty?
|
23
|
+
return if summary.empty?
|
24
|
+
return if description.empty?
|
25
|
+
|
26
|
+
api.post 'issue',
|
27
|
+
params: params,
|
28
|
+
success: on_success,
|
29
|
+
failure: on_failure
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_accessor :ticket
|
35
|
+
|
36
|
+
def params
|
37
|
+
{
|
26
38
|
fields: {
|
27
|
-
project: { id: project[
|
39
|
+
project: { id: project['id'] },
|
28
40
|
issuetype: { id: issue_type['id'] },
|
29
41
|
summary: summary,
|
30
|
-
description: description
|
42
|
+
description: description,
|
43
|
+
parent: @parent.nil? ? {} : { key: @parent },
|
44
|
+
components: @components.nil? ? [] : @components
|
31
45
|
}
|
32
46
|
}
|
33
|
-
|
47
|
+
end
|
48
|
+
|
49
|
+
def on_success
|
50
|
+
->(json) do
|
51
|
+
self.ticket = json['key']
|
52
|
+
io.say("Ticket #{ticket} created.")
|
53
|
+
assign?
|
54
|
+
create_branch? && checkout_branch?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def assign?
|
59
|
+
Command::Assign.new(ticket).run if io.yes?('Assign?')
|
60
|
+
end
|
61
|
+
|
62
|
+
def create_branch?
|
63
|
+
return false if io.no?("Create branch?")
|
64
|
+
`git branch #{ticket} 2> /dev/null`
|
65
|
+
true
|
66
|
+
end
|
67
|
+
|
68
|
+
def checkout_branch?
|
69
|
+
return false if io.no?("Check-out branch?")
|
70
|
+
`git checkout #{ticket} 2> /dev/null`
|
71
|
+
true
|
72
|
+
end
|
73
|
+
|
74
|
+
def on_failure
|
75
|
+
->{ puts "No ticket created." }
|
76
|
+
end
|
77
|
+
|
78
|
+
def metadata
|
79
|
+
# TODO - {} on 200 but jira error
|
80
|
+
@metadata ||= api.get('issue/createmeta')
|
81
|
+
end
|
82
|
+
|
83
|
+
def project
|
84
|
+
@project ||= projects[
|
85
|
+
io.select("Select a project:", projects.keys)
|
86
|
+
]
|
87
|
+
end
|
34
88
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
89
|
+
def projects
|
90
|
+
@projects ||= (
|
91
|
+
projects = {}
|
92
|
+
metadata['projects'].each do |project|
|
93
|
+
projects[project['name']] = {
|
94
|
+
'id' => project['id'],
|
95
|
+
'issues' => project['issuetypes']
|
96
|
+
}
|
40
97
|
end
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
98
|
+
projects
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
def project_metadata
|
103
|
+
id = project['id']
|
104
|
+
@project_metadata ||= api.get("project/#{id}")
|
105
|
+
end
|
106
|
+
|
107
|
+
def components
|
108
|
+
@components ||= (
|
109
|
+
components = {}
|
110
|
+
project_metadata['components'].each do |component|
|
111
|
+
components[component['name']] = {
|
112
|
+
'id' => component['id']
|
113
|
+
}
|
46
114
|
end
|
47
|
-
|
48
|
-
|
115
|
+
io.multi_select("Select component(s):", components) unless components.empty?
|
116
|
+
)
|
49
117
|
end
|
50
|
-
puts "No ticket created."
|
51
|
-
end
|
52
118
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
# then return the project data hash
|
58
|
-
#
|
59
|
-
# @param json [Hash] creation metadata
|
60
|
-
#
|
61
|
-
# @return [Hash] selected project's metadata
|
62
|
-
#
|
63
|
-
def select_project(json)
|
64
|
-
projects = {}
|
65
|
-
json['projects'].each do |project|
|
66
|
-
data = {
|
67
|
-
id: project['id'],
|
68
|
-
issues: project['issuetypes']
|
69
|
-
}
|
70
|
-
projects[project['name']] = data
|
71
|
-
end
|
72
|
-
projects['Cancel'] = nil
|
73
|
-
choice = self.io.choose("Select a project", projects.keys)
|
74
|
-
return {} if choice == 'Cancel'
|
75
|
-
return projects[choice]
|
76
|
-
end
|
77
|
-
|
78
|
-
#
|
79
|
-
# Given the project metadata, prompts the user for the issue type to use
|
80
|
-
# and returns the selected issue type.
|
81
|
-
#
|
82
|
-
# @param project_data [Hash] project metadata
|
83
|
-
#
|
84
|
-
# @return [Hash] selected issue type
|
85
|
-
#
|
86
|
-
def select_issue_type(project_data)
|
87
|
-
issue_types = {}
|
88
|
-
project_data[:issues].each do |issue_type|
|
89
|
-
issue_types[issue_type['name']] = issue_type
|
90
|
-
end
|
91
|
-
issue_types['Cancel'] = nil
|
92
|
-
choice = self.io.choose("Select an issue type", issue_types.keys)
|
93
|
-
return issue_types[choice]
|
119
|
+
def assign_parent?
|
120
|
+
return false unless issue_type['subtask']
|
121
|
+
return false if io.no?('Set parent of subtask?')
|
122
|
+
true
|
94
123
|
end
|
95
124
|
|
125
|
+
def parent
|
126
|
+
@parent ||= io.ask('Subtask parent:', default: Jira::Core.ticket)
|
127
|
+
end
|
128
|
+
|
129
|
+
def issue_type
|
130
|
+
@issue_type ||= issue_types[
|
131
|
+
io.select("Select an issue type:", issue_types.keys)
|
132
|
+
]
|
133
|
+
end
|
134
|
+
|
135
|
+
def issue_types
|
136
|
+
@issue_types ||= (
|
137
|
+
issue_types = {}
|
138
|
+
project['issues'].each do |issue_type|
|
139
|
+
issue_types[issue_type['name']] = issue_type
|
140
|
+
end
|
141
|
+
issue_types
|
142
|
+
)
|
143
|
+
end
|
144
|
+
|
145
|
+
def summary
|
146
|
+
@summary ||= io.ask("Summary:")
|
147
|
+
end
|
148
|
+
|
149
|
+
def description
|
150
|
+
@description ||= io.ask("Description:")
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
96
154
|
end
|
97
155
|
end
|