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