ruby-jira-cli 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +47 -0
  3. data/bin/jira +26 -0
  4. data/lib/jira/api.rb +76 -0
  5. data/lib/jira/auth_api.rb +11 -0
  6. data/lib/jira/command.rb +60 -0
  7. data/lib/jira/commands/all.rb +72 -0
  8. data/lib/jira/commands/assign.rb +65 -0
  9. data/lib/jira/commands/attachments.rb +45 -0
  10. data/lib/jira/commands/checkout.rb +87 -0
  11. data/lib/jira/commands/comment/add.rb +52 -0
  12. data/lib/jira/commands/comment/delete.rb +80 -0
  13. data/lib/jira/commands/comment/list.rb +72 -0
  14. data/lib/jira/commands/comment/update.rb +88 -0
  15. data/lib/jira/commands/comment.rb +14 -0
  16. data/lib/jira/commands/delete.rb +92 -0
  17. data/lib/jira/commands/describe.rb +64 -0
  18. data/lib/jira/commands/install.rb +121 -0
  19. data/lib/jira/commands/link.rb +94 -0
  20. data/lib/jira/commands/log/add.rb +52 -0
  21. data/lib/jira/commands/log/delete.rb +80 -0
  22. data/lib/jira/commands/log/list.rb +69 -0
  23. data/lib/jira/commands/log/update.rb +89 -0
  24. data/lib/jira/commands/log.rb +13 -0
  25. data/lib/jira/commands/new.rb +174 -0
  26. data/lib/jira/commands/rename.rb +53 -0
  27. data/lib/jira/commands/sprint.rb +109 -0
  28. data/lib/jira/commands/tickets.rb +55 -0
  29. data/lib/jira/commands/transition.rb +97 -0
  30. data/lib/jira/commands/version.rb +10 -0
  31. data/lib/jira/commands/vote/add.rb +43 -0
  32. data/lib/jira/commands/vote/delete.rb +46 -0
  33. data/lib/jira/commands/vote/list.rb +59 -0
  34. data/lib/jira/commands/vote.rb +12 -0
  35. data/lib/jira/commands/watch/add.rb +43 -0
  36. data/lib/jira/commands/watch/delete.rb +46 -0
  37. data/lib/jira/commands/watch/list.rb +59 -0
  38. data/lib/jira/commands/watch.rb +12 -0
  39. data/lib/jira/constants.rb +7 -0
  40. data/lib/jira/core.rb +101 -0
  41. data/lib/jira/exceptions.rb +3 -0
  42. data/lib/jira/format.rb +78 -0
  43. data/lib/jira/sprint_api.rb +33 -0
  44. data/lib/jira.rb +19 -0
  45. metadata +202 -0
@@ -0,0 +1,89 @@
1
+ module Jira
2
+ class Log < Thor
3
+
4
+ desc 'update', 'Updates work logged on the input ticket'
5
+ def update(ticket=Jira::Core.ticket)
6
+ Command::Log::Update.new(ticket).run
7
+ end
8
+
9
+ end
10
+
11
+ module Command
12
+ module Log
13
+ class Update < Base
14
+
15
+ attr_accessor :ticket
16
+
17
+ def initialize(ticket=Jira::Core.ticket)
18
+ self.ticket = ticket
19
+ end
20
+
21
+ def run
22
+ return unless logs?
23
+ api.patch endpoint,
24
+ params: params,
25
+ success: on_success,
26
+ failure: on_failure
27
+ end
28
+
29
+ private
30
+
31
+ def params
32
+ { timeSpent: updated_time_spent }
33
+ end
34
+
35
+ def updated_time_spent
36
+ io.ask('Updated time spent?')
37
+ end
38
+
39
+ def logs?
40
+ if json.empty?
41
+ puts "Ticket #{ticket} has no work logged."
42
+ return false
43
+ end
44
+ true
45
+ end
46
+
47
+ def endpoint
48
+ "issue/#{ticket}/worklog/#{to_update['id']}"
49
+ end
50
+
51
+ def on_success
52
+ ->{ puts "Successfully updated #{to_update['timeSpent']}." }
53
+ end
54
+
55
+ def on_failure
56
+ ->{ puts "No logged work updated." }
57
+ end
58
+
59
+ def to_update
60
+ @to_delete ||= logs[
61
+ io.select("Select a worklog to update:", logs.keys)
62
+ ]
63
+ end
64
+
65
+ def logs
66
+ @logs ||= (
67
+ logs = {}
68
+ json.each do |log|
69
+ logs[description_for(log)] = log
70
+ end
71
+ logs
72
+ )
73
+ end
74
+
75
+ def description_for(log)
76
+ author = log['updateAuthor']['displayName']
77
+ updated_at = Jira::Format.time(Time.parse(log['updated']))
78
+ time_spent = log['timeSpent']
79
+ "#{author} @ #{updated_at}: #{time_spent}"
80
+ end
81
+
82
+ def json
83
+ @json ||= api.get("issue/#{ticket}/worklog")['worklogs']
84
+ end
85
+
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,13 @@
1
+ require 'jira/commands/log/add'
2
+ require 'jira/commands/log/delete'
3
+ require 'jira/commands/log/list'
4
+ require 'jira/commands/log/update'
5
+
6
+ module Jira
7
+ class CLI < Thor
8
+
9
+ desc 'log <command>', 'Commands for logging operations in JIRA'
10
+ subcommand 'log', Log
11
+
12
+ end
13
+ end
@@ -0,0 +1,174 @@
1
+ module Jira
2
+ class CLI < Thor
3
+
4
+ desc "new", "Creates a new ticket in JIRA and checks out the git branch"
5
+ method_option :project, aliases: "-p", type: :string, default: nil, banner: "PROJECT"
6
+ method_option :components, aliases: "-c", type: :array, default: nil, lazy_default: [], banner: "COMPONENTS"
7
+ method_option :issuetype, aliases: "-i", type: :string, default: nil, banner: "ISSUETYPE"
8
+ method_option :parent, type: :string, default: nil, lazy_default: "", banner: "PARENT"
9
+ method_option :summary, aliases: "-s", type: :string, default: nil, banner: "SUMMARY"
10
+ method_option :description, aliases: "-d", type: :string, default: nil, lazy_default: "", banner: "DESCRIPTION"
11
+ method_option :assignee, aliases: "-a", type: :string, default: nil, lazy_default: "auto", banner: "ASSIGNEE"
12
+ def new
13
+ Command::New.new(options).run
14
+ end
15
+
16
+ end
17
+
18
+ module Command
19
+ class New < Base
20
+
21
+ attr_accessor :options
22
+
23
+ def initialize(options)
24
+ self.options = options
25
+ end
26
+
27
+ def run
28
+ return if metadata.empty?
29
+ return if project.nil? || project.empty?
30
+ return if project_metadata.empty?
31
+ components # Select components if any after a project
32
+ return if issue_type.nil? || issue_type.empty?
33
+ return if assign_parent? && parent.empty?
34
+ return if summary.empty?
35
+
36
+ api.post 'issue',
37
+ params: params,
38
+ success: on_success,
39
+ failure: on_failure
40
+ end
41
+
42
+ private
43
+
44
+ attr_accessor :ticket
45
+
46
+ def params
47
+ {
48
+ fields: {
49
+ project: { id: project['id'] },
50
+ issuetype: { id: issue_type['id'] },
51
+ summary: summary,
52
+ description: description,
53
+ parent: @parent.nil? ? {} : { key: @parent },
54
+ components: @components.nil? ? [] : @components
55
+ }
56
+ }
57
+ end
58
+
59
+ def on_success
60
+ ->(json) do
61
+ self.ticket = json['key']
62
+ io.say("Ticket #{ticket} created.")
63
+ assign? if options.empty? || !options['assignee'].nil?
64
+ create_branch? && checkout_branch? if options.empty?
65
+ end
66
+ end
67
+
68
+ def assign?
69
+ Command::Assign.new(ticket, options).run if !options['assignee'].nil? || io.yes?('Assign?')
70
+ end
71
+
72
+ def create_branch?
73
+ return false if io.no?("Create branch?")
74
+ `git branch #{ticket} 2> /dev/null`
75
+ true
76
+ end
77
+
78
+ def checkout_branch?
79
+ return false if io.no?("Check-out branch?")
80
+ `git checkout #{ticket} 2> /dev/null`
81
+ true
82
+ end
83
+
84
+ def on_failure
85
+ ->{ puts "No ticket created." }
86
+ end
87
+
88
+ def metadata
89
+ # TODO: {} on 200 but jira error
90
+ @metadata ||= api.get('issue/createmeta')
91
+ end
92
+
93
+ def project
94
+ @project ||= projects[
95
+ options['project'] || io.select("Select a project:", projects.keys)
96
+ ]
97
+ end
98
+
99
+ def projects
100
+ @projects ||= (
101
+ projects = {}
102
+ metadata['projects'].each do |project|
103
+ projects[project['name']] = {
104
+ 'id' => project['id'],
105
+ 'issues' => project['issuetypes']
106
+ }
107
+ end
108
+ projects
109
+ )
110
+ end
111
+
112
+ def project_metadata
113
+ id = project['id']
114
+ @project_metadata ||= api.get("project/#{id}")
115
+ end
116
+
117
+ def components
118
+ @components ||= (
119
+ components = {}
120
+ project_metadata['components'].each do |component|
121
+ components[component['name']] = { 'id' => component['id'] }
122
+ end
123
+ unless components.empty?
124
+ if options['components'].nil?
125
+ components = io.multi_select("Select component(s):", components)
126
+ else
127
+ components.select! { |k| options['components'].include?(k) }
128
+ components = components.values
129
+ end
130
+ end
131
+ components.to_a
132
+ )
133
+ end
134
+
135
+ def assign_parent?
136
+ return false unless issue_type['subtask']
137
+ return false if options['parent'].nil? && io.no?('Set parent of subtask?')
138
+ true
139
+ end
140
+
141
+ def parent
142
+ @parent ||= options['parent'] || io.ask('Subtask parent:', default: Jira::Core.ticket)
143
+ end
144
+
145
+ def issue_type
146
+ @issue_type ||= issue_types[
147
+ options['issuetype'] || io.select("Select an issue type:", issue_types.keys)
148
+ ]
149
+ end
150
+
151
+ def issue_types
152
+ @issue_types ||= (
153
+ issue_types = {}
154
+ project['issues'].each do |issue_type|
155
+ issue_types[issue_type['name']] = issue_type
156
+ end
157
+ issue_types
158
+ )
159
+ end
160
+
161
+ def summary
162
+ @summary ||= options['summary'] || io.ask("Summary:", default: '')
163
+ end
164
+
165
+ def description
166
+ @description ||= (
167
+ description = options['description'] || (io.ask("Description:", default: '') if options['summary'].nil?)
168
+ description ||= ""
169
+ )
170
+ end
171
+
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,53 @@
1
+ module Jira
2
+ class CLI < Thor
3
+
4
+ desc "rename", "Updates the summary of the input ticket"
5
+ method_option :summary, aliases: "-s", type: :string, default: nil, lazy_default: "", banner: "SUMMARY"
6
+ def rename(ticket=Jira::Core.ticket)
7
+ Command::Rename.new(ticket, options).run
8
+ end
9
+
10
+ end
11
+
12
+ module Command
13
+ class Rename < Base
14
+
15
+ attr_accessor :ticket, :options
16
+
17
+ def initialize(ticket, options)
18
+ self.ticket = ticket
19
+ self.options = options
20
+ end
21
+
22
+ def run
23
+ return if ticket.empty?
24
+ return if summary.empty?
25
+ api.patch "issue/#{ticket}",
26
+ params: params,
27
+ success: on_success,
28
+ failure: on_failure
29
+ end
30
+
31
+ def params
32
+ {
33
+ fields: {
34
+ summary: summary
35
+ }
36
+ }
37
+ end
38
+
39
+ def on_success
40
+ ->{ puts "Successfully updated ticket #{ticket}'s summary." }
41
+ end
42
+
43
+ def on_failure
44
+ ->{ puts "No change made to ticket #{ticket}." }
45
+ end
46
+
47
+ def summary
48
+ @summary ||= options['summary'] || io.ask("New summary for ticket #{ticket}:", default: '')
49
+ end
50
+
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,109 @@
1
+ module Jira
2
+ class CLI < Thor
3
+
4
+ desc "sprint", "Lists sprint info"
5
+ def sprint(active = false)
6
+ Command::Sprint.new(active).run
7
+ end
8
+
9
+ end
10
+
11
+ module Command
12
+ class Sprint < Base
13
+
14
+ attr_accessor :active
15
+
16
+ def initialize(active)
17
+ self.active = active
18
+ end
19
+
20
+
21
+ def run
22
+ return if rapid_view.empty?
23
+ return if no_sprints?
24
+ return if sprint.empty?
25
+ if active == active
26
+ sprint_id = info['sprint']['id']
27
+ jql = "sprint = #{sprint_id}"
28
+ Command::Tickets.new(jql).run
29
+ else
30
+ render_table(
31
+ [ 'Sprint', 'State' ],
32
+ [ [ info['sprint']['name'], info['sprint']['state'] ] ]
33
+ )
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def no_sprints?
40
+ if sprints.empty?
41
+ puts "The #{rapid_view['name']} board has no sprints."
42
+ return true
43
+ end
44
+ false
45
+ end
46
+
47
+ def info
48
+ @info ||= sprint_api.sprint(rapid_view['id'], sprint['id'])
49
+ end
50
+
51
+ def sprint
52
+ @sprint ||= active_sprint || sprints[
53
+ io.select("Select a sprint:", sprints.keys[-10..-1])
54
+ ]
55
+ end
56
+
57
+ def sprints
58
+ @sprints ||= (
59
+ sprints = {}
60
+ sprint_api.sprints(rapid_view['id'])['sprints'].each do |sprint|
61
+ sprints["#{sprint['name']}"] = {
62
+ 'id' => sprint['id'],
63
+ 'name' => sprint['name'],
64
+ 'state' => sprint['state']
65
+ }
66
+ end
67
+ sprints
68
+ )
69
+ end
70
+
71
+ def active_sprint
72
+ sprints = {}
73
+ sprint_api.sprints(rapid_view['id'])['sprints'].each do |sprint|
74
+ if sprint['state'] == "ACTIVE"
75
+ sprints = {
76
+ 'id' => sprint['id'],
77
+ 'name' => sprint['name'],
78
+ 'state' => sprint['state']
79
+ }
80
+ end
81
+ end
82
+ sprints
83
+ end
84
+
85
+ def rapid_view
86
+ keys = rapid_views.keys
87
+ return '' if keys.empty?
88
+ # @rapid_view ||= rapid_views[
89
+ # io.select("Select a rapid view:", keys)
90
+ # ]
91
+ @rapid_view = {"id"=>15, "name"=>"Ruby Team"}
92
+ end
93
+
94
+ def rapid_views
95
+ @rapid_views ||= (
96
+ rapid_views = {}
97
+ sprint_api.rapid_views.each do |rapid_view|
98
+ rapid_views[rapid_view['name']] = {
99
+ 'id' => rapid_view['id'],
100
+ 'name' => rapid_view['name']
101
+ }
102
+ end
103
+ rapid_views
104
+ )
105
+ end
106
+
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,55 @@
1
+ module Jira
2
+ class CLI < Thor
3
+
4
+ desc "tickets [jql]", "List the tickets of the input jql"
5
+ def tickets(jql="assignee=#{Jira::Core.username} AND status = \"In Progress\"")
6
+ Command::Tickets.new(jql).run
7
+ end
8
+
9
+ end
10
+
11
+ module Command
12
+ class Tickets < Base
13
+
14
+ attr_accessor :jql
15
+
16
+ def initialize(jql)
17
+ self.jql = jql
18
+ end
19
+
20
+ def run
21
+ return if jql.empty?
22
+ return if metadata.empty?
23
+ return unless metadata['errorMessages'].nil?
24
+
25
+ if rows.empty?
26
+ puts "There are no tickets for jql=#{jql}."
27
+ return
28
+ end
29
+ render_table(header, rows)
30
+ end
31
+
32
+ private
33
+
34
+ def header
35
+ [ 'Ticket', 'Assignee', 'Status', 'Summary']
36
+ end
37
+
38
+ def rows
39
+ metadata['issues'].map do |issue|
40
+ [
41
+ issue['key'],
42
+ (issue['fields']['assignee']['name'] unless issue['fields']['assignee'].nil?) || 'Unassigned',
43
+ (issue['fields']['status']['name'] unless issue['fields']['status'].nil?) || 'Unknown',
44
+ truncate(issue['fields']['summary'] || '', 45)
45
+ ]
46
+ end
47
+ end
48
+
49
+ def metadata
50
+ @metadata ||= api.get("search?jql=#{jql}")
51
+ end
52
+
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,97 @@
1
+ module Jira
2
+ class CLI < Thor
3
+
4
+ desc "transition", "Transitions the input ticket to the next state"
5
+ method_option :transition, aliases: "-t", type: :string, default: nil, lazy_default: "", banner: "TRANSITION"
6
+ method_option :resolution, aliases: "-r", type: :string, default: nil, lazy_default: "", banner: "RESOLUTION"
7
+ def transition(ticket=Jira::Core.ticket)
8
+ Command::Transition.new(ticket, options).run
9
+ end
10
+
11
+ end
12
+
13
+ module Command
14
+ class Transition < Base
15
+
16
+ attr_accessor :ticket, :options
17
+
18
+ def initialize(ticket, options)
19
+ self.ticket = ticket
20
+ self.options = options
21
+ end
22
+
23
+ def run
24
+ return if ticket.empty?
25
+ return if metadata.empty?
26
+ return unless metadata['errorMessages'].nil?
27
+ return if transition.nil? || transition.empty?
28
+
29
+ api.post "issue/#{ticket}/transitions",
30
+ params: params,
31
+ success: on_success,
32
+ failure: on_failure
33
+ end
34
+
35
+ private
36
+
37
+ def params
38
+ {
39
+ transition: { id: transition[:id] },
40
+ fields: transition[:resolution?] ? { resolution: { name: resolution } } : {}
41
+ }
42
+ end
43
+
44
+ def on_success
45
+ ->{ puts "Transitioned ticket #{ticket} to #{transition_name}." }
46
+ end
47
+
48
+ def on_failure
49
+ ->{ puts "Failed to transition ticket #{ticket}." }
50
+ end
51
+
52
+ def transition_name
53
+ transitions.invert[transition]
54
+ end
55
+
56
+ def transition
57
+ @transition ||= transitions[
58
+ options['transition'] || io.select("Transition #{ticket} to:", transitions.keys)
59
+ ]
60
+ end
61
+
62
+ def transitions
63
+ @transitions ||= (
64
+ transitions = {}
65
+ metadata['transitions'].each do |transition|
66
+ transitions[transition['to']['name']] = {
67
+ id: transition['id'],
68
+ resolution?: !!transition['fields'].fetch('resolution', {})['required']
69
+ }
70
+ end
71
+ transitions
72
+ )
73
+ end
74
+
75
+ def metadata
76
+ @metadata ||= api.get("issue/#{ticket}/transitions?expand=transitions.fields")
77
+ end
78
+
79
+ def resolution
80
+ @resolution ||= resolutions[
81
+ options['resolution'] || io.select("Resolve #{ticket} as:", resolutions.keys)
82
+ ]
83
+ end
84
+
85
+ def resolutions
86
+ @resolutions ||= (
87
+ resolutions = {}
88
+ api.get("resolution").each do |resolution|
89
+ resolutions[resolution['name']] = resolution['name']
90
+ end
91
+ resolutions
92
+ )
93
+ end
94
+
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,10 @@
1
+ module Jira
2
+ class CLI < Thor
3
+
4
+ desc "version", "Displays the version"
5
+ def version
6
+ say "jira #{Jira::VERSION}"
7
+ end
8
+
9
+ end
10
+ end
@@ -0,0 +1,43 @@
1
+ module Jira
2
+ class Vote < Thor
3
+
4
+ desc 'add', 'Vote for the input ticket'
5
+ def add(ticket=Jira::Core.ticket)
6
+ Command::Vote::Add.new(ticket).run
7
+ end
8
+
9
+ end
10
+
11
+ module Command
12
+ module Vote
13
+ class Add < Base
14
+
15
+ attr_accessor :ticket
16
+
17
+ def initialize(ticket)
18
+ self.ticket = ticket
19
+ end
20
+
21
+ def run
22
+ return if ticket.empty?
23
+
24
+ api.post "issue/#{ticket}/votes",
25
+ params: "\"#{Jira::Core.username}\"",
26
+ success: on_success,
27
+ failure: on_failure
28
+ end
29
+
30
+ private
31
+
32
+ def on_success
33
+ ->{ puts "Successfully voted for ticket #{ticket}" }
34
+ end
35
+
36
+ def on_failure
37
+ ->{ puts "No vote cast." }
38
+ end
39
+
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,46 @@
1
+ module Jira
2
+ class Vote < Thor
3
+
4
+ desc 'delete', 'Delete vote for the input ticket'
5
+ def delete(ticket=Jira::Core.ticket)
6
+ Command::Vote::Delete.new(ticket).run
7
+ end
8
+
9
+ end
10
+
11
+ module Command
12
+ module Vote
13
+ class Delete < Base
14
+
15
+ attr_accessor :ticket
16
+
17
+ def initialize(ticket)
18
+ self.ticket = ticket
19
+ end
20
+
21
+ def run
22
+ return if ticket.empty?
23
+
24
+ api.delete endpoint,
25
+ success: on_success,
26
+ failure: on_failure
27
+ end
28
+
29
+ private
30
+
31
+ def endpoint
32
+ "issue/#{ticket}/votes?username=#{Jira::Core.username}"
33
+ end
34
+
35
+ def on_success
36
+ ->{ puts "Successfully removed vote from ticket #{ticket}" }
37
+ end
38
+
39
+ def on_failure
40
+ ->{ puts "No unvote." }
41
+ end
42
+
43
+ end
44
+ end
45
+ end
46
+ end