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