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,88 @@
1
+ module Jira
2
+ class Comment < Thor
3
+
4
+ desc 'update', 'Update a comment to the input ticket'
5
+ def update(ticket=Jira::Core.ticket)
6
+ Command::Comment::Update.new(ticket).run
7
+ end
8
+
9
+ end
10
+
11
+ module Command
12
+ module Comment
13
+ class Update < 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.patch endpoint,
24
+ params: params,
25
+ success: on_success,
26
+ failure: on_failure
27
+ end
28
+
29
+ private
30
+
31
+ def params
32
+ { body: body }
33
+ end
34
+
35
+ def comments?
36
+ if json.empty?
37
+ puts "Ticket #{ticket} has no comments."
38
+ return false
39
+ end
40
+ true
41
+ end
42
+
43
+ def endpoint
44
+ "issue/#{ticket}/comment/#{to_update['id']}"
45
+ end
46
+
47
+ def on_success
48
+ ->{
49
+ puts "Successfully updated comment originally from"\
50
+ " #{to_update['updateAuthor']['displayName']}."
51
+ }
52
+ end
53
+
54
+ def on_failure
55
+ ->{ puts "No comment updated." }
56
+ end
57
+
58
+ def to_update
59
+ @to_update ||= comments[
60
+ io.select("Select a comment to update:", comments.keys)
61
+ ]
62
+ end
63
+
64
+ def comments
65
+ @comments ||= (
66
+ comments = {}
67
+ json.each do |comment|
68
+ comments[description_for(comment)] = comment
69
+ end
70
+ comments
71
+ )
72
+ end
73
+
74
+ def description_for(comment)
75
+ author = comment['updateAuthor']['displayName']
76
+ updated_at = Jira::Format.time(Time.parse(comment['updated']))
77
+ body = comment['body'].split.join(" ")
78
+ truncate("#{author} @ #{updated_at}: #{body}", 160)
79
+ end
80
+
81
+ def json
82
+ @json ||= api.get("issue/#{ticket}/comment")['comments']
83
+ end
84
+
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,14 @@
1
+ require 'jira/commands/comment/add'
2
+ require 'jira/commands/comment/delete'
3
+ require 'jira/commands/comment/list'
4
+ require 'jira/commands/comment/update'
5
+
6
+ module Jira
7
+ class CLI < Thor
8
+
9
+ desc 'comment <command>', 'Commands for comment operations in JIRA'
10
+ subcommand 'comment', Comment
11
+
12
+ end
13
+
14
+ end
@@ -0,0 +1,92 @@
1
+ module Jira
2
+ class CLI < Thor
3
+
4
+ desc "delete", "Deletes a ticket in JIRA and the git branch"
5
+ method_option :force, type: :boolean, default: false
6
+ def delete(ticket=Jira::Core.ticket)
7
+ Command::Delete.new(ticket, options[:force]).run
8
+ end
9
+
10
+ end
11
+
12
+ module Command
13
+ class Delete < Base
14
+
15
+ attr_accessor :ticket, :force
16
+
17
+ def initialize(ticket, force)
18
+ self.ticket = ticket
19
+ self.force = force
20
+ end
21
+
22
+ def run
23
+ return if ticket.empty?
24
+ return if metadata.empty?
25
+ return if metadata['fields'].nil?
26
+ return if subtasks_failure?
27
+
28
+ api.delete "issue/#{ticket}?deleteSubtasks=#{force}",
29
+ success: on_success,
30
+ failure: on_failure
31
+ end
32
+
33
+ private
34
+
35
+ def on_success
36
+ -> do
37
+ on_failure and return unless create_branch?
38
+ on_failure and return unless delete_branch?
39
+ end
40
+ end
41
+
42
+ def on_failure
43
+ -> { puts "No change made to ticket #{ticket}." }
44
+ end
45
+
46
+ def branches
47
+ branches = `git branch --list 2> /dev/null`.split(' ')
48
+ branches.delete("*")
49
+ branches.delete(ticket.to_s)
50
+ branches
51
+ end
52
+
53
+ def create_branch?
54
+ response = io.yes?("Create branch?")
55
+
56
+ if branches.count == 1 or response
57
+ io.say("Creating a new branch.")
58
+ new_branch = io.ask("Branch?").strip
59
+ new_branch.delete!(" ")
60
+ on_failure and return false if new_branch.empty?
61
+ `git branch #{new_branch} 2> /dev/null`
62
+ end
63
+ true
64
+ end
65
+
66
+ def delete_branch?
67
+ response = self.io.select("Select a branch:", branches)
68
+ `git checkout #{response} 2> /dev/null`
69
+ `git branch -D #{ticket} 2> /dev/null`
70
+ true
71
+ end
72
+
73
+ def subtasks_failure?
74
+ return false unless subtask?
75
+ if !metadata['fields']['subtasks'].empty? && !force
76
+ self.force = io.yes?("Delete all sub-tasks for ticket #{ticket}?")
77
+ return true unless force
78
+ end
79
+ false
80
+ end
81
+
82
+ def subtask?
83
+ metadata['fields']['issuetype']['subtask']
84
+ end
85
+
86
+ def metadata
87
+ @metadata ||= api.get("issue/#{ticket}")
88
+ end
89
+
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,64 @@
1
+ module Jira
2
+ class CLI < Thor
3
+
4
+ desc "describe", "Describes the input ticket"
5
+ def describe(ticket=Jira::Core.ticket)
6
+ Command::Describe.new(ticket).run
7
+ end
8
+
9
+ end
10
+
11
+ module Command
12
+ class Describe < Base
13
+
14
+ attr_accessor :ticket
15
+
16
+ def initialize(ticket)
17
+ self.ticket = ticket
18
+ end
19
+
20
+ def run
21
+ return if json.empty?
22
+ return unless errors.empty?
23
+ render_table(header, [row])
24
+ end
25
+
26
+ def header
27
+ [ 'Ticket', 'Assignee', 'Status', 'Summary', 'Description' ]
28
+ end
29
+
30
+ def row
31
+ [ ticket, assignee, status, summary, description ]
32
+ end
33
+
34
+ def errors
35
+ @errors ||= (json['errorMessages'] || []).join('. ')
36
+ end
37
+
38
+ def assignee
39
+ (fields['assignee'] || {})['name'] || 'Unassigned'
40
+ end
41
+
42
+ def status
43
+ (fields['status'] || {})['name'] || 'Unknown'
44
+ end
45
+
46
+ def summary
47
+ truncate(json['fields']['summary'], 45)
48
+ end
49
+
50
+ def description
51
+ json['fields']['description']
52
+ end
53
+
54
+ def fields
55
+ json['fields'] || {}
56
+ end
57
+
58
+ def json
59
+ @json ||= api.get "issue/#{ticket}"
60
+ end
61
+
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,121 @@
1
+ module Jira
2
+ class CLI < Thor
3
+
4
+ desc "install", "Guides the user through JIRA CLI installation"
5
+ def install
6
+ Command::Install.new.run
7
+ end
8
+
9
+ no_tasks do
10
+ def try_install_cookie
11
+ return false if Jira::Core.cookie.empty?
12
+ puts " ... cookie expired, renewing your cookie"
13
+ Command::Install.new.run_rescue_cookie
14
+ puts "Cookie renewed, updating .jira-rescue-cookie"
15
+ File.open(Jira::Core.rescue_cookie_path, "a") do |f|
16
+ f << "r"
17
+ end
18
+ puts " ... updated .jira-rescue-cookie"
19
+ return true
20
+ rescue Interrupt, StandardError
21
+ false
22
+ end
23
+ end
24
+
25
+ end
26
+
27
+ module Command
28
+ class Install < Base
29
+
30
+ def run
31
+ io.say('Please enter your JIRA information.')
32
+ inifile[:global] = base_params
33
+ inifile.write # Do this now because cookie authentication uses api calls
34
+
35
+ inifile.delete_section("cookie") if inifile.has_section?("cookie")
36
+ case authentication
37
+ when "basic"
38
+ inifile[:global][:password] = password
39
+ when "token"
40
+ inifile[:global][:token] = token
41
+ when "cookie"
42
+ response = cookie(session_params)
43
+ inifile[:cookie] = {}
44
+ inifile[:cookie][:name] = response['name']
45
+ inifile[:cookie][:value] = response['value']
46
+ end
47
+ inifile.write
48
+ end
49
+
50
+ def run_rescue_cookie
51
+ response = cookie(rescue_cookie_params)
52
+ config = Jira::Core.config
53
+ config[:cookie] = {}
54
+ config[:cookie][:name] = response['name']
55
+ config[:cookie][:value] = response['value']
56
+ config.write
57
+ end
58
+
59
+ private
60
+
61
+ def base_params
62
+ {
63
+ url: url,
64
+ username: username
65
+ }
66
+ end
67
+
68
+ def rescue_cookie_params
69
+ {
70
+ username: Jira::Core.username,
71
+ password: password
72
+ }
73
+ end
74
+
75
+ def session_params
76
+ {
77
+ username: username,
78
+ password: password
79
+ }
80
+ end
81
+
82
+ def authentication
83
+ @authentication ||= io.select(
84
+ "Select an authentication type:",
85
+ ["basic", "cookie", "token"]
86
+ )
87
+ end
88
+
89
+ def url
90
+ @url ||= io.ask("JIRA URL:")
91
+ end
92
+
93
+ def username
94
+ @username ||= io.ask("JIRA username:")
95
+ end
96
+
97
+ def password
98
+ io.mask("JIRA password:")
99
+ end
100
+
101
+ def token
102
+ io.ask("JIRA token:")
103
+ end
104
+
105
+ def cookie(params)
106
+ response = auth_api.post('session', params: params)
107
+ return {} unless response['errorMessages'].nil?
108
+ response['session']
109
+ end
110
+
111
+ def inifile
112
+ @inifile ||= IniFile.new(
113
+ comment: '#',
114
+ encoding: 'UTF-8',
115
+ filename: Jira::Core.cli_path
116
+ )
117
+ end
118
+
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,94 @@
1
+ module Jira
2
+ class CLI < Thor
3
+
4
+ desc "link", "Creates a link between two tickets in JIRA"
5
+ def link(ticket=Jira::Core.ticket)
6
+ Command::Link.new(ticket).run
7
+ end
8
+
9
+ end
10
+
11
+ module Command
12
+ class Link < 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 issue_link_type.empty?
24
+ return if outward_ticket.empty?
25
+ return unless invalid_ticket?
26
+
27
+ begin
28
+ api.post "issueLink",
29
+ params: params,
30
+ success: on_success,
31
+ failure: on_failure
32
+ rescue CommandException
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def params
39
+ {
40
+ type: {
41
+ name: issue_link_type[:name]
42
+ },
43
+ inwardIssue: {
44
+ key: ticket
45
+ },
46
+ outwardIssue: {
47
+ key: outward_ticket
48
+ }
49
+ }
50
+ end
51
+
52
+ def issue_link_type
53
+ return @issue_link_type unless @issue_link_type.nil?
54
+
55
+ types = {}
56
+ metadata['issueLinkTypes'].each do |type|
57
+ data = {
58
+ id: type['id'],
59
+ name: type['name'],
60
+ inward: type['inward'],
61
+ outward: type['outward']
62
+ }
63
+ types[type['name']] = data
64
+ end
65
+ choice = io.select("Select a link type:", types.keys)
66
+ @issue_link_type = types[choice]
67
+ end
68
+
69
+ def on_success
70
+ ->{
71
+ puts "Successfully linked ticket #{ticket} to"\
72
+ " ticket #{outward_ticket}."
73
+ }
74
+ end
75
+
76
+ def on_failure
77
+ ->{ puts "No ticket linked." }
78
+ end
79
+
80
+ def outward_ticket
81
+ @outward_ticket ||= io.ask("Outward ticket:").strip
82
+ end
83
+
84
+ def invalid_ticket?
85
+ !Jira::Core.ticket?(outward_ticket)
86
+ end
87
+
88
+ def metadata
89
+ @metadata ||= api.get("issueLinkType")
90
+ end
91
+
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,52 @@
1
+ module Jira
2
+ class Log < Thor
3
+
4
+ desc 'add', 'Logs work against the input ticket'
5
+ method_option :time, aliases: "-t", type: :string, default: nil, lazy_default: "", banner: "TIME"
6
+ def add(ticket=Jira::Core.ticket)
7
+ Command::Log::Add.new(ticket, options).run
8
+ end
9
+
10
+ end
11
+
12
+ module Command
13
+ module Log
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 time_spent.empty?
25
+ api.post "issue/#{ticket}/worklog",
26
+ params: params,
27
+ success: on_success,
28
+ failure: on_failure
29
+ end
30
+
31
+ private
32
+
33
+ def params
34
+ { timeSpent: time_spent }
35
+ end
36
+
37
+ def time_spent
38
+ @time_spent ||= options['time'] || io.ask("Time spent on ticket #{ticket}:")
39
+ end
40
+
41
+ def on_success
42
+ ->{ puts "Successfully logged #{time_spent} on ticket #{ticket}." }
43
+ end
44
+
45
+ def on_failure
46
+ ->{ puts "No work was logged on ticket #{ticket}." }
47
+ end
48
+
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,80 @@
1
+ module Jira
2
+ class Log < Thor
3
+
4
+ desc 'delete', 'Deletes logged work against the input ticket'
5
+ def delete(ticket=Jira::Core.ticket)
6
+ Command::Log::Delete.new(ticket).run
7
+ end
8
+
9
+ end
10
+
11
+ module Command
12
+ module Log
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 logs?
23
+ api.delete endpoint,
24
+ success: on_success,
25
+ failure: on_failure
26
+ end
27
+
28
+ private
29
+
30
+ def logs?
31
+ if json.empty?
32
+ puts "Ticket #{ticket} has no work logged."
33
+ return false
34
+ end
35
+ true
36
+ end
37
+
38
+ def endpoint
39
+ "issue/#{ticket}/worklog/#{to_delete['id']}"
40
+ end
41
+
42
+ def on_success
43
+ ->{ puts "Successfully deleted #{to_delete['timeSpent']}." }
44
+ end
45
+
46
+ def on_failure
47
+ ->{ puts "No logged work deleted." }
48
+ end
49
+
50
+ def to_delete
51
+ @to_delete ||= logs[
52
+ io.select("Select a worklog to delete:", logs.keys)
53
+ ]
54
+ end
55
+
56
+ def logs
57
+ @logs ||= (
58
+ logs = {}
59
+ json.each do |log|
60
+ logs[description_for(log)] = log
61
+ end
62
+ logs
63
+ )
64
+ end
65
+
66
+ def description_for(log)
67
+ author = log['updateAuthor']['displayName']
68
+ updated_at = Jira::Format.time(Time.parse(log['updated']))
69
+ time_spent = log['timeSpent']
70
+ "#{author} @ #{updated_at}: #{time_spent}"
71
+ end
72
+
73
+ def json
74
+ @json ||= api.get("issue/#{ticket}/worklog")['worklogs']
75
+ end
76
+
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,69 @@
1
+ module Jira
2
+ class Log < Thor
3
+
4
+ desc 'list', 'Lists work logged on the input ticket'
5
+ def list(ticket=Jira::Core.ticket)
6
+ Command::Log::List.new(ticket).run
7
+ end
8
+
9
+ end
10
+
11
+ module Command
12
+ module Log
13
+ class List < 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
+ if logs.empty?
23
+ puts "Ticket #{ticket} has no work logged."
24
+ return
25
+ end
26
+ render_table(header, rows)
27
+ end
28
+
29
+ private
30
+
31
+ attr_accessor :log
32
+
33
+ def header
34
+ [ 'Author', 'Updated At', 'Time Spent' ]
35
+ end
36
+
37
+ def rows
38
+ rows = []
39
+ logs.each do |log|
40
+ self.log = log
41
+ rows << row
42
+ end
43
+ rows
44
+ end
45
+
46
+ def row
47
+ [ author, updated_at, time_spent ]
48
+ end
49
+
50
+ def author
51
+ log['updateAuthor']['displayName']
52
+ end
53
+
54
+ def updated_at
55
+ Jira::Format.time(Time.parse(log['updated']))
56
+ end
57
+
58
+ def time_spent
59
+ log['timeSpent']
60
+ end
61
+
62
+ def logs
63
+ @logs ||= api.get("issue/#{ticket}/worklog")['worklogs']
64
+ end
65
+
66
+ end
67
+ end
68
+ end
69
+ end