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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b229a4bc96644bd3e819673aa12a382530c9ed74
4
+ data.tar.gz: 0273cb796258b0e39599830647b9dedff8519479
5
+ SHA512:
6
+ metadata.gz: e14df53c00913ae286f0cd053f4f8d92adbb8526f2271d4201e217b72ba166e1a967796988c38b4851d40b0da1797f14a2609b8dcf4aee041aa3932ca6c8cee5
7
+ data.tar.gz: 6ee3d17a550cf5f974440e032af391edd58b5e108c4f60e610c96958e7a9c4890c4807086151350ec1fd05e60459f48e003324bd984e302ce88881f55423dede
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # JIRA CLI
2
+
3
+ Ruby gem CLI tool used to manage JIRA workflows leveraging git
4
+
5
+ * * *
6
+
7
+ ### Available Commands
8
+
9
+ jira all # Describes all local branches that match JIRA ticketing syntax
10
+ jira assign # Assign a ticket to a user
11
+ jira attachments # View ticket attachments
12
+ jira checkout <ticket> # Checks out a ticket from JIRA in the git branch
13
+ jira comment <command> # Commands for comment operations in JIRA
14
+ jira delete # Deletes a ticket in JIRA and the git branch
15
+ jira describe # Describes the input ticket
16
+ jira help [COMMAND] # Describe available commands or one specific command
17
+ jira install # Guides the user through JIRA CLI installation
18
+ jira link # Creates a link between two tickets in JIRA
19
+ jira log <command> # Commands for logging operations in JIRA
20
+ jira new # Creates a new ticket in JIRA and checks out the git branch
21
+ jira rename # Updates the summary of the input ticket
22
+ jira sprint # Lists sprint info
23
+ jira tickets [jql] # List the in progress tickets of the input username (or jql)
24
+ jira transition # Transitions the input ticket to the next state
25
+ jira version # Displays the version
26
+ jira vote <command> # Commands for voting operations in JIRA
27
+ jira watch <command> # Commands for watching tickets in JIRA
28
+
29
+ ### Gem Installation
30
+
31
+ Rubygems:
32
+
33
+ gem install ruby-jira-cli
34
+
35
+ Manually:
36
+
37
+ git clone git@github.com:ajmyers01/jira-cli.git
38
+ cd jira-cli
39
+ ./scripts/install
40
+
41
+ ### Project Installation
42
+
43
+ In order to use this tool, you'll need to run the installation script in the
44
+ git repository that you're managing via JIRA.
45
+
46
+ cd path/to/jira/repo
47
+ jira install
data/bin/jira ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'jira'
4
+ begin
5
+ Jira::CLI.start
6
+ File.delete(Jira::Core.rescue_cookie_path) if File.exist?(Jira::Core.rescue_cookie_path)
7
+ rescue Faraday::Error, UnauthorizedException
8
+ if Jira::CLI.new.try_install_cookie
9
+ if File.read(Jira::Core.rescue_cookie_path).count('r') < 3
10
+ puts "Re-running: jira #{ARGV.join(' ')}"
11
+ Process.waitpid(
12
+ Process.fork do
13
+ Process.exec("jira #{ARGV.join(' ')}")
14
+ end
15
+ )
16
+ exit
17
+ end
18
+ end
19
+ puts "JIRA failed connect, you may need to rerun 'jira install'"
20
+ rescue GitException
21
+ puts "JIRA commands can only be run within a git repository."
22
+ rescue InstallationException
23
+ puts "Please run #{Jira::Format.summary('jira install')} before "\
24
+ "running this command."
25
+ rescue Interrupt
26
+ end
data/lib/jira/api.rb ADDED
@@ -0,0 +1,76 @@
1
+ module Jira
2
+ class API
3
+
4
+ def get(path, options={})
5
+ response = client.get(path, options[:params] || {}, headers)
6
+ process(response, options)
7
+ end
8
+
9
+ def post(path, options={})
10
+ response = client.post(path, options[:params] || {}, headers)
11
+ process(response, options)
12
+ end
13
+
14
+ def patch(path, options={})
15
+ response = client.put(path, options[:params] || {}, headers)
16
+ process(response, options)
17
+ end
18
+
19
+ def delete(path, options={})
20
+ response = client.delete(path, options[:params] || {}, headers)
21
+ process(response, options)
22
+ end
23
+
24
+ protected
25
+
26
+ def process(response, options)
27
+ raise UnauthorizedException if response.status == 401
28
+ body = response.body || {}
29
+ json = (body if body.class == Hash) || {}
30
+ if response.success? && json['errorMessages'].nil?
31
+ respond_to(options[:success], body)
32
+ else
33
+ puts json['errorMessages'].join('. ') unless json['errorMessages'].nil?
34
+ respond_to(options[:failure], body)
35
+ end
36
+ body
37
+ end
38
+
39
+ def respond_to(block, body)
40
+ return if block.nil?
41
+ case block.arity
42
+ when 0
43
+ block.call
44
+ when 1
45
+ block.call(body)
46
+ end
47
+ end
48
+
49
+ def client
50
+ @client ||= Faraday.new(endpoint) do |faraday|
51
+ faraday.request :basic_auth, Jira::Core.username, Jira::Core.password unless Jira::Core.password.nil?
52
+ faraday.request :token_auth, Jira::Core.token unless Jira::Core.token.nil?
53
+ faraday.request :json
54
+ faraday.response :json
55
+ faraday.adapter :net_http
56
+ end
57
+ end
58
+
59
+ def endpoint
60
+ "#{Jira::Core.url}/rest/api/2"
61
+ end
62
+
63
+ def headers
64
+ { 'Content-Type' => 'application/json' }.merge(cookies)
65
+ end
66
+
67
+ def cookies
68
+ cookie = Jira::Core.cookie
69
+ unless cookie.empty?
70
+ return { 'cookie' => "#{cookie[:name]}=#{cookie[:value]}" }
71
+ end
72
+ {}
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,11 @@
1
+ module Jira
2
+ class AuthAPI < API
3
+
4
+ protected
5
+
6
+ def endpoint
7
+ "#{Jira::Core.url}/rest/auth/1"
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,60 @@
1
+ # external dependencies
2
+ require 'time'
3
+
4
+ # internal dependencies
5
+ require 'jira/api'
6
+ require 'jira/sprint_api'
7
+ require 'jira/auth_api'
8
+ require 'jira/core'
9
+
10
+ module Jira
11
+ module Command
12
+ class Base
13
+
14
+ def run
15
+ raise NotImplementedError
16
+ end
17
+
18
+ protected
19
+
20
+ def api
21
+ @api ||= Jira::API.new
22
+ end
23
+
24
+ def auth_api
25
+ @auth_api ||= Jira::AuthAPI.new
26
+ end
27
+
28
+ # TODO: Move this to relevant subcommand Base
29
+ def body(text=nil)
30
+ @body ||= (
31
+ comment = text || io.ask("Leave a comment for ticket #{ticket}:", default: 'Empty comment').strip
32
+ comment = comment.gsub(/\@[a-zA-Z]+/, '[~\0]') || comment
33
+ comment.gsub('[~@', '[~') || comment
34
+ )
35
+ end
36
+
37
+ def sprint_api
38
+ @sprint_api ||= Jira::SprintAPI.new
39
+ end
40
+
41
+ def io
42
+ @io ||= TTY::Prompt.new
43
+ end
44
+
45
+ def render_table(header, rows)
46
+ puts TTY::Table.new(header, rows).render(:unicode, padding: [0, 1], multiline: true)
47
+ end
48
+
49
+ def truncate(string, limit=80)
50
+ return string if string.length < limit
51
+ string[0..limit-3] + '...'
52
+ end
53
+
54
+ end
55
+ end
56
+ end
57
+
58
+ # load commands
59
+ commands_directory = File.join(File.dirname(__FILE__), 'commands', '*.rb')
60
+ Dir[commands_directory].each { |file| require file }
@@ -0,0 +1,72 @@
1
+ module Jira
2
+ class CLI < Thor
3
+
4
+ desc "all", "Describes all local branches that match JIRA ticketing syntax"
5
+ def all
6
+ Command::All.new.run
7
+ end
8
+
9
+ end
10
+
11
+ module Command
12
+ class All < Base
13
+
14
+ def run
15
+ if tickets.empty?
16
+ puts 'No tickets'
17
+ return
18
+ end
19
+ return if json.empty?
20
+ return unless errors.empty?
21
+ render_table(header, rows)
22
+ end
23
+
24
+ private
25
+
26
+ def header
27
+ [ 'Ticket', 'Assignee', 'Status', 'Summary' ]
28
+ end
29
+
30
+ def rows
31
+ json['issues'].map do |issue|
32
+ [
33
+ issue['key'],
34
+ issue['fields']['assignee']['name'] || 'Unassigned',
35
+ issue['fields']['status']['name'] || 'Unknown',
36
+ truncate(issue['fields']['summary'], 45)
37
+ ]
38
+ end
39
+ end
40
+
41
+ def errors
42
+ @errors ||= (json['errorMessages'] || []).join('. ')
43
+ end
44
+
45
+ def json
46
+ @json ||= api.get "search", params: params
47
+ end
48
+
49
+ def params
50
+ {
51
+ jql: "key in (#{tickets.join(',')})"
52
+ }
53
+ end
54
+
55
+ def tickets
56
+ @tickets ||= (
57
+ tickets = []
58
+ branches.each do |branch|
59
+ ticket = branch.delete('*').strip
60
+ tickets << ticket if Jira::Core.ticket?(ticket, false)
61
+ end
62
+ tickets
63
+ )
64
+ end
65
+
66
+ def branches
67
+ `git branch`.strip.split("\n")
68
+ end
69
+
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,65 @@
1
+ module Jira
2
+ class CLI < Thor
3
+
4
+ desc "assign", "Assign a ticket to a user"
5
+ method_option :assignee, aliases: "-a", type: :string, default: nil, lazy_default: "auto", banner: "ASSIGNEE"
6
+ def assign(ticket=Jira::Core.ticket)
7
+ Command::Assign.new(ticket, options).run
8
+ end
9
+
10
+ end
11
+
12
+ module Command
13
+ class Assign < 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
+ api.patch path,
24
+ params: params,
25
+ success: on_success,
26
+ failure: on_failure
27
+ end
28
+
29
+ private
30
+
31
+ def on_success
32
+ -> do
33
+ puts "Ticket #{ticket} assigned to #{name}."
34
+ end
35
+ end
36
+
37
+ def on_failure
38
+ ->(json) do
39
+ message = (json['errors'] || {})['assignee']
40
+ puts message || "Ticket #{ticket} was not assigned."
41
+ end
42
+ end
43
+
44
+ def path
45
+ "issue/#{ticket}/assignee"
46
+ end
47
+
48
+ def params
49
+ { name: assignee }
50
+ end
51
+
52
+ def name
53
+ assignee == '-1' ? 'default user' : "'#{assignee}'"
54
+ end
55
+
56
+ def assignee
57
+ @assignee ||= (
58
+ assignee = options['assignee'] || io.ask('Assignee?', default: 'auto')
59
+ assignee == 'auto' ? '-1' : assignee
60
+ )
61
+ end
62
+
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,45 @@
1
+ module Jira
2
+ class CLI < Thor
3
+
4
+ desc "attachments", "View ticket attachments"
5
+ def attachments(ticket=Jira::Core.ticket)
6
+ Command::Attachments.new(ticket).run
7
+ end
8
+
9
+ end
10
+
11
+ module Command
12
+ class Attachments < Base
13
+
14
+ attr_accessor :ticket
15
+
16
+ def initialize(ticket)
17
+ self.ticket = ticket
18
+ end
19
+
20
+ def run
21
+ return if ticket.empty?
22
+ return if metadata.empty?
23
+ return if metadata['fields'].nil?
24
+
25
+ attachments=metadata['fields']['attachment']
26
+ if !attachments.nil? and attachments.count > 0
27
+ attachments.each do |attachment|
28
+ name=attachment['filename']
29
+ url=attachment['content']
30
+
31
+ puts "#{Jira::Format.user(name)} #{url}"
32
+ end
33
+ else
34
+ puts "No attachments found for ticket #{ticket}."
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def metadata
41
+ @metadata ||= api.get("issue/#{ticket}")
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,87 @@
1
+ module Jira
2
+ class CLI < Thor
3
+
4
+ desc "checkout <ticket>", "Checks out a ticket from JIRA in the git branch"
5
+ method_option :remote, aliases: "-r", type: :string, default: nil, lazy_default: "", banner: "REMOTE"
6
+ def checkout(ticket)
7
+ Command::Checkout.new(ticket, options).run
8
+ end
9
+
10
+ end
11
+
12
+ module Command
13
+ class Checkout < 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 unless Jira::Core.ticket?(ticket)
24
+ return if metadata.empty?
25
+ unless metadata['errorMessages'].nil?
26
+ on_failure
27
+ return
28
+ end
29
+ unless remote?
30
+ on_failure
31
+ return
32
+ end
33
+
34
+ create_branch unless branches.include?(ticket)
35
+ checkout_branch
36
+ reset_branch unless branches.include?(ticket)
37
+ on_success
38
+ end
39
+
40
+ private
41
+
42
+ def on_success
43
+ puts "Ticket #{ticket} checked out."
44
+ end
45
+
46
+ def on_failure
47
+ puts "No ticket checked out."
48
+ end
49
+
50
+ def branches
51
+ @branches ||= `git branch --list 2> /dev/null`.split(' ')
52
+ @branches.delete("*")
53
+ @branches
54
+ end
55
+
56
+ def create_branch
57
+ `git branch #{ticket} 2> /dev/null`
58
+ end
59
+
60
+ def checkout_branch
61
+ `git checkout #{ticket} 2> /dev/null`
62
+ end
63
+
64
+ def metadata
65
+ @metadata ||= api.get("issue/#{ticket}")
66
+ end
67
+
68
+ def remote
69
+ @remote ||= options['remote'] || io.select('Remote?', remotes)
70
+ end
71
+
72
+ def remotes
73
+ @remotes ||= `git remote 2> /dev/null`.split(' ')
74
+ end
75
+
76
+ def remote?
77
+ return true if remotes.include?(remote)
78
+ false
79
+ end
80
+
81
+ def reset_branch
82
+ `git reset --hard #{remote} 2> /dev/null`
83
+ end
84
+
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,52 @@
1
+ module Jira
2
+ class Comment < Thor
3
+
4
+ desc 'add', 'Add a comment to the input ticket'
5
+ method_option :text, aliases: "-t", type: :string, default: nil, lazy_default: "", banner: "TEXT"
6
+ def add(ticket=Jira::Core.ticket)
7
+ Command::Comment::Add.new(ticket, options).run
8
+ end
9
+
10
+ end
11
+
12
+ module Command
13
+ module Comment
14
+ class Add < 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 text.empty?
25
+ api.post "issue/#{ticket}/comment",
26
+ params: params,
27
+ success: on_success,
28
+ failure: on_failure
29
+ end
30
+
31
+ private
32
+
33
+ def params
34
+ { body: text }
35
+ end
36
+
37
+ def text
38
+ body(options['text'])
39
+ end
40
+
41
+ def on_success
42
+ ->{ puts "Successfully posted your comment." }
43
+ end
44
+
45
+ def on_failure
46
+ ->{ puts "No comment posted." }
47
+ end
48
+
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,80 @@
1
+ module Jira
2
+ class Comment < Thor
3
+
4
+ desc 'delete', 'Delete a comment to the input ticket'
5
+ def delete(ticket=Jira::Core.ticket)
6
+ Command::Comment::Delete.new(ticket).run
7
+ end
8
+
9
+ end
10
+
11
+ module Command
12
+ module Comment
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 unless comments?
23
+ api.delete endpoint,
24
+ success: on_success,
25
+ failure: on_failure
26
+ end
27
+
28
+ private
29
+
30
+ def comments?
31
+ if json.empty?
32
+ puts "Ticket #{ticket} has no comments."
33
+ return false
34
+ end
35
+ true
36
+ end
37
+
38
+ def endpoint
39
+ "issue/#{ticket}/comment/#{to_delete['id']}"
40
+ end
41
+
42
+ def on_success
43
+ ->{ puts "Successfully deleted comment from #{to_delete['updateAuthor']['displayName']}" }
44
+ end
45
+
46
+ def on_failure
47
+ ->{ puts "No comment deleted." }
48
+ end
49
+
50
+ def to_delete
51
+ @to_delete ||= comments[
52
+ io.select("Select a comment to delete:", comments.keys)
53
+ ]
54
+ end
55
+
56
+ def comments
57
+ @comments ||= (
58
+ comments = {}
59
+ json.each do |comment|
60
+ comments[description_for(comment)] = comment
61
+ end
62
+ comments
63
+ )
64
+ end
65
+
66
+ def description_for(comment)
67
+ author = comment['updateAuthor']['displayName']
68
+ updated_at = Jira::Format.time(Time.parse(comment['updated']))
69
+ body = comment['body'].split.join(" ")
70
+ truncate("#{author} @ #{updated_at}: #{body}", 160)
71
+ end
72
+
73
+ def json
74
+ @json ||= api.get("issue/#{ticket}/comment")['comments'] || {}
75
+ end
76
+
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,72 @@
1
+ module Jira
2
+ class Comment < Thor
3
+
4
+ desc 'list', 'Lists the comments of the input ticket'
5
+ def list(ticket=Jira::Core.ticket)
6
+ Command::Comment::List.new(ticket).run
7
+ end
8
+
9
+ end
10
+
11
+ module Command
12
+ module Comment
13
+ class List < Base
14
+
15
+ attr_accessor :ticket
16
+
17
+ def initialize(ticket)
18
+ self.ticket = ticket
19
+ end
20
+
21
+ def run
22
+ return if comments.nil?
23
+
24
+ if comments.empty?
25
+ puts "Ticket #{ticket} has no comments."
26
+ return
27
+ end
28
+ render_table(header, rows)
29
+ end
30
+
31
+ private
32
+
33
+ attr_accessor :comment
34
+
35
+ def header
36
+ [ 'Author', 'Updated At', 'Body' ]
37
+ end
38
+
39
+ def rows
40
+ rows = []
41
+ comments.each do |comment|
42
+ self.comment = comment
43
+ rows << row
44
+ end
45
+ rows
46
+ end
47
+
48
+ def row
49
+ [ author, updated_at, body ]
50
+ end
51
+
52
+ def author
53
+ comment['updateAuthor']['displayName']
54
+ end
55
+
56
+ def updated_at
57
+ Jira::Format.time(Time.parse(comment['updated']))
58
+ end
59
+
60
+ def body
61
+ body = comment['body'].gsub("\r\n|\r|\n", ";")
62
+ truncate(body, 45)
63
+ end
64
+
65
+ def comments
66
+ @comments ||= api.get("issue/#{ticket}/comment")['comments']
67
+ end
68
+
69
+ end
70
+ end
71
+ end
72
+ end