ruby-jira-cli 0.0.3

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.
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