jira-cli 0.2.1 → 0.2.2

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.
@@ -1,85 +1,68 @@
1
+ require_relative '../command'
2
+
1
3
  module Jira
2
4
  class CLI < Thor
3
5
 
4
6
  desc "describe", "Describes the input ticket"
5
7
  def describe(ticket=Jira::Core.ticket)
6
- if Jira::Core.ticket?(ticket)
7
- output = description(ticket.strip, false, true, true)
8
- puts output if !output.strip.empty?
9
- end
8
+ Command::Describe.new(ticket).run
10
9
  end
11
10
 
12
- desc "all", "Describes all local branches that match JIRA ticketing syntax"
13
- def all
14
- # determine which local branches match JIRA ticket syntax
15
- # TODO - move to Jira::Git
16
- tickets = {
17
- current: nil,
18
- others: []
19
- }
20
- branches = `git branch`.strip.split("\n")
21
- branches.each do |branch|
22
- ticket = branch.delete('*').strip
23
- if Jira::Core.ticket?(ticket, false)
24
- if branch.include?('*')
25
- tickets[:current] = ticket
26
- else
27
- tickets[:others] << ticket
28
- end
29
- end
11
+ end
12
+
13
+ module Command
14
+ class Describe < Base
15
+
16
+ attr_accessor :ticket
17
+
18
+ def initialize(ticket)
19
+ self.ticket = ticket
30
20
  end
31
21
 
22
+ def run
23
+ return if json.empty?
24
+ return if errored?
25
+ render_table(header, [row])
26
+ end
32
27
 
33
- # asynchronously fetch and describe tickets
34
- output = ""
35
- threads = []
36
- if !tickets[:current].nil?
37
- threads << Thread.new{ puts description(tickets[:current], true) }
28
+ def header
29
+ [ 'Ticket', 'Assignee', 'Status', 'Summary' ]
38
30
  end
39
- mutex = Mutex.new
40
- tickets[:others].each do |ticket|
41
- threads << Thread.new do
42
- out = description(ticket) + "\n"
43
- if !out.strip.empty?
44
- mutex.synchronize{ output << out }
45
- end
46
- end
31
+
32
+ def row
33
+ [ ticket, assignee, status, summary ]
34
+ end
35
+
36
+ def errored?
37
+ return false if errors.empty?
38
+ puts errors
39
+ true
47
40
  end
48
- threads.each{ |thread| thread.join }
49
- puts output if !output.empty?
50
- end
51
41
 
52
- protected
53
-
54
- #
55
- # Returns a formatted description of the input ticket
56
- #
57
- # @param ticket [String] the ticket to describe
58
- # @param star [Boolean] if true, adds a * indicator
59
- #
60
- # @return [String] formatted summary string
61
- #
62
- def description(ticket, star=false, verbose=false, describe=false)
63
- self.api.get("issue/#{ticket}", nil, verbose) do |json|
64
- summary = json['fields']['summary']
65
- status = json['fields']['status']['name']
66
- assignee = "nil"
67
- if json['fields'].has_key?("assignee")
68
- if !json['fields']['assignee'].nil?
69
- assignee = json['fields']['assignee']['name']
70
- end
71
- end
72
- description = describe ? "\n" + json['fields']['description'].to_s : ""
73
-
74
- return Jira::Format.ticket(ticket) +
75
- (star ? Jira::Format.star : " ") + " \t" +
76
- ("(" + Jira::Format.user(assignee) + ")").ljust(20) +
77
- Jira::Format.status(status).ljust(26) +
78
- Jira::Format.summary(summary) +
79
- description
80
- end
81
- return ""
42
+ def errors
43
+ @errors ||= (json['errorMessages'] || []).join('. ')
82
44
  end
83
45
 
46
+ def assignee
47
+ (fields['assignee'] || {})['name'] || 'Unassigned'
48
+ end
49
+
50
+ def status
51
+ (fields['status'] || {})['name'] || 'Unknown'
52
+ end
53
+
54
+ def summary
55
+ truncate(json['fields']['summary'], 45)
56
+ end
57
+
58
+ def fields
59
+ json['fields'] || {}
60
+ end
61
+
62
+ def json
63
+ @json ||= api.get "issue/#{ticket}"
64
+ end
65
+
66
+ end
84
67
  end
85
68
  end
@@ -1,29 +1,67 @@
1
- #
2
- # Installation Script
3
- #
1
+ require_relative '../command'
4
2
 
5
3
  module Jira
6
4
  class CLI < Thor
7
5
 
8
- desc "install", "Guides the user through JIRA installation"
6
+ desc "install", "Guides the user through JIRA CLI installation"
9
7
  def install
8
+ Command::Install.new.run
9
+ end
10
10
 
11
- inifile = IniFile.new(:comment => '#', :encoding => 'UTF-8', :filename => Jira::Core.cli_path)
11
+ end
12
12
 
13
- url = self.io.ask("Enter your JIRA URL")
13
+ module Command
14
+ class Install < Base
14
15
 
15
- username = self.io.ask("Enter your JIRA username")
16
- password = self.io.ask("Enter your JIRA password", password: true)
16
+ def run
17
+ io.say('Please enter your JIRA information.')
18
+ inifile[:global] = params
19
+ inifile.write
20
+ end
17
21
 
18
- inifile[:global] = {
19
- url: url,
20
- username: username,
21
- password: password
22
- }
23
- inifile.write
22
+ private
24
23
 
25
- Jira::Core.send(:discard_memoized)
26
- end
24
+ def params
25
+ args = {
26
+ url: url,
27
+ username: username,
28
+ }
29
+ response = io.select("Select an authentication type:", ["basic", "token"])
30
+ case response
31
+ when "basic"
32
+ args[:password] = password
33
+ when "token"
34
+ args[:token] = token
35
+ else
36
+ raise InstallationException
37
+ end
38
+ args
39
+ end
40
+
41
+ def url
42
+ io.ask("JIRA URL:")
43
+ end
44
+
45
+ def username
46
+ io.ask("JIRA username:")
47
+ end
27
48
 
49
+ def password
50
+ io.mask("JIRA password:")
51
+ end
52
+
53
+ def token
54
+ io.ask("JIRA token:")
55
+ end
56
+
57
+ def inifile
58
+ @inifile ||= IniFile.new(
59
+ comment: '#',
60
+ encoding: 'UTF-8',
61
+ filename: Jira::Core.cli_path
62
+ )
63
+ end
64
+
65
+ end
28
66
  end
29
67
  end
@@ -1,19 +1,44 @@
1
+ require_relative '../command'
2
+
1
3
  module Jira
2
4
  class CLI < Thor
3
5
 
4
6
  desc "link", "Creates a link between two tickets in JIRA"
5
7
  def link(ticket=Jira::Core.ticket)
6
- self.api.get("issueLinkType") do |json|
7
- # determine issue link type
8
- issue_link_type = select_issue_link_type(json)
9
- break if issue_link_type.nil?
8
+ Command::Link.new(ticket).run
9
+ end
10
+
11
+ end
12
+
13
+ module Command
14
+ class Link < Base
15
+
16
+ attr_accessor :ticket
17
+
18
+ def initialize(ticket)
19
+ self.ticket = ticket
20
+ end
21
+
22
+ def run
23
+ return if ticket.empty?
24
+ return if metadata.empty?
25
+ return if issue_link_type.empty?
26
+ return if outward_ticket.empty?
27
+ return unless invalid_ticket?
10
28
 
11
- # determine outward ticket
12
- outward_ticket = self.io.ask("Outward ticket").strip
13
- break if !Jira::Core.ticket?(outward_ticket)
29
+ begin
30
+ api.post "issueLink",
31
+ params: params,
32
+ success: on_success,
33
+ failure: on_failure
34
+ rescue CommandException
35
+ end
36
+ end
14
37
 
15
- # determine api parameters
16
- params = {
38
+ private
39
+
40
+ def params
41
+ {
17
42
  type: {
18
43
  name: issue_link_type[:name]
19
44
  },
@@ -24,41 +49,46 @@ module Jira
24
49
  key: outward_ticket
25
50
  }
26
51
  }
27
-
28
- self.api.post("issueLink", params) do |json|
29
- puts "Successfully linked ticket #{ticket} to ticket #{outward_ticket}."
30
- return
31
- end
32
52
  end
33
- puts "No ticket linked."
34
- end
35
53
 
36
- protected
37
-
38
- #
39
- # Given the issue link type metadata, prompts the user for
40
- # the issue link type to use, then return the issue link
41
- # type hash
42
- #
43
- # @param json [Hash] issue link type metadata
44
- #
45
- # @return [Hash] selected issue link type metadata
46
- #
47
- def select_issue_link_type(json)
48
- issue_link_types = {}
49
- json['issueLinkTypes'].each do |issue_link_type|
54
+ def issue_link_type
55
+ return @issue_link_type unless @issue_link_type.nil?
56
+
57
+ types = {}
58
+ metadata['issueLinkTypes'].each do |type|
50
59
  data = {
51
- id: issue_link_type['id'],
52
- name: issue_link_type['name'],
53
- inward: issue_link_type['inward'],
54
- outward: issue_link_type['outward']
60
+ id: type['id'],
61
+ name: type['name'],
62
+ inward: type['inward'],
63
+ outward: type['outward']
55
64
  }
56
- issue_link_types[issue_link_type['name']] = data
65
+ types[type['name']] = data
57
66
  end
58
- issue_link_types['Cancel'] = nil
59
- choice = self.io.choose("Select a link type", issue_link_types.keys)
60
- return issue_link_types[choice]
67
+ choice = io.select("Select a link type:", types.keys)
68
+ @issue_link_type = types[choice]
69
+ end
70
+
71
+ def on_success
72
+ ->{ puts "Successfully linked ticket #{ticket} to ticket #{outward_ticket}." }
61
73
  end
62
74
 
75
+ def on_failure
76
+ ->{ puts "No ticket linked." }
77
+ end
78
+
79
+ def outward_ticket
80
+ @outward_ticket ||= io.ask("Outward ticket:").strip
81
+ end
82
+
83
+ def invalid_ticket?
84
+ return true unless Jira::Core.ticket?(outward_ticket)
85
+ return false
86
+ end
87
+
88
+ def metadata
89
+ @metadata ||= api.get("issueLinkType")
90
+ end
91
+
92
+ end
63
93
  end
64
94
  end
@@ -3,7 +3,7 @@ module Jira
3
3
 
4
4
  desc "log", "Logs work against the input ticket"
5
5
  def log(ticket=Jira::Core.ticket)
6
- time_spent = self.io.ask("Time spent on ticket #{ticket}")
6
+ time_spent = self.io.ask("Time spent on ticket #{ticket}:")
7
7
  self.api.post("issue/#{ticket}/worklog", { timeSpent: time_spent }) do |json|
8
8
  puts "Successfully logged #{time_spent} on ticket #{ticket}."
9
9
  end
@@ -11,7 +11,7 @@ module Jira
11
11
 
12
12
  desc "logd", "Deletes work against the input ticket"
13
13
  def logd(ticket=Jira::Core.ticket)
14
- logs(ticket) if self.io.agree("List worklogs for ticket #{ticket}")
14
+ logs(ticket) if self.io.yes?("List worklogs for ticket #{ticket}?")
15
15
 
16
16
  index = self.get_type_of_index("worklog", "delete")
17
17
  puts "No worklog deleted." and return if index < 0
@@ -53,12 +53,12 @@ module Jira
53
53
 
54
54
  desc "logu", "Updates work against the input ticket"
55
55
  def logu(ticket=Jira::Core.ticket)
56
- logs(ticket) if self.io.agree("List worklogs for ticket #{ticket}")
56
+ logs(ticket) if self.io.yes?("List worklogs for ticket #{ticket}?")
57
57
 
58
58
  index = self.get_type_of_index("worklog", "update")
59
59
  puts "No worklog updated." and return if index < 0
60
60
 
61
- time_spent = self.io.ask("Time spent on #{ticket}").strip
61
+ time_spent = self.io.ask("Time spent on #{ticket}:").strip
62
62
  puts "No worklog updated." and return if time_spent.empty?
63
63
 
64
64
  self.api.get("issue/#{ticket}/worklog") do |json|
@@ -1,97 +1,155 @@
1
+ require_relative '../command'
2
+
1
3
  module Jira
2
4
  class CLI < Thor
3
5
 
4
6
  desc "new", "Creates a new ticket in JIRA and checks out the git branch"
5
- def new(ticket = Jira::Core.ticket)
6
- self.api.get("issue/createmeta") do |meta|
7
- # determine project
8
- project = self.select_project(meta)
9
- break if project.empty?
10
-
11
- # determine issue type
12
- issue_type = self.select_issue_type(project)
13
- break if issue_type.nil?
14
-
15
- # determine parent (ticket)
16
- parent = nil
17
- parent = ticket if issue_type['subtask']
18
- break if !parent.nil? and !Jira::Core.ticket?(parent)
19
-
20
- # determine summary and description
21
- summary = self.io.ask("Summary")
22
- description = self.io.ask("Description")
23
-
24
- # determine api parameters
25
- params = {
7
+ def new
8
+ Command::New.new.run
9
+ end
10
+
11
+ end
12
+
13
+ module Command
14
+ class New < Base
15
+
16
+ def run
17
+ return if metadata.empty?
18
+ return if project.empty?
19
+ return if project_metadata.empty?
20
+ components # Select components if any after a project
21
+ return if issue_type.empty?
22
+ return if assign_parent? && parent.empty?
23
+ return if summary.empty?
24
+ return if description.empty?
25
+
26
+ api.post 'issue',
27
+ params: params,
28
+ success: on_success,
29
+ failure: on_failure
30
+ end
31
+
32
+ private
33
+
34
+ attr_accessor :ticket
35
+
36
+ def params
37
+ {
26
38
  fields: {
27
- project: { id: project[:id] },
39
+ project: { id: project['id'] },
28
40
  issuetype: { id: issue_type['id'] },
29
41
  summary: summary,
30
- description: description
42
+ description: description,
43
+ parent: @parent.nil? ? {} : { key: @parent },
44
+ components: @components.nil? ? [] : @components
31
45
  }
32
46
  }
33
- params[:fields][:parent] = { key: parent } if !parent.nil?
47
+ end
48
+
49
+ def on_success
50
+ ->(json) do
51
+ self.ticket = json['key']
52
+ io.say("Ticket #{ticket} created.")
53
+ assign?
54
+ create_branch? && checkout_branch?
55
+ end
56
+ end
57
+
58
+ def assign?
59
+ Command::Assign.new(ticket).run if io.yes?('Assign?')
60
+ end
61
+
62
+ def create_branch?
63
+ return false if io.no?("Create branch?")
64
+ `git branch #{ticket} 2> /dev/null`
65
+ true
66
+ end
67
+
68
+ def checkout_branch?
69
+ return false if io.no?("Check-out branch?")
70
+ `git checkout #{ticket} 2> /dev/null`
71
+ true
72
+ end
73
+
74
+ def on_failure
75
+ ->{ puts "No ticket created." }
76
+ end
77
+
78
+ def metadata
79
+ # TODO - {} on 200 but jira error
80
+ @metadata ||= api.get('issue/createmeta')
81
+ end
82
+
83
+ def project
84
+ @project ||= projects[
85
+ io.select("Select a project:", projects.keys)
86
+ ]
87
+ end
34
88
 
35
- # post issue to server
36
- self.api.post("issue", params) do |json|
37
- ticket = json['key']
38
- if self.io.agree("Assign")
39
- self.assign(ticket)
89
+ def projects
90
+ @projects ||= (
91
+ projects = {}
92
+ metadata['projects'].each do |project|
93
+ projects[project['name']] = {
94
+ 'id' => project['id'],
95
+ 'issues' => project['issuetypes']
96
+ }
40
97
  end
41
- if self.io.agree("Create branch")
42
- `git branch #{ticket} 2> /dev/null`
43
- if self.io.agree("Check-out branch")
44
- `git checkout #{ticket} 2> /dev/null`
45
- end
98
+ projects
99
+ )
100
+ end
101
+
102
+ def project_metadata
103
+ id = project['id']
104
+ @project_metadata ||= api.get("project/#{id}")
105
+ end
106
+
107
+ def components
108
+ @components ||= (
109
+ components = {}
110
+ project_metadata['components'].each do |component|
111
+ components[component['name']] = {
112
+ 'id' => component['id']
113
+ }
46
114
  end
47
- return
48
- end
115
+ io.multi_select("Select component(s):", components) unless components.empty?
116
+ )
49
117
  end
50
- puts "No ticket created."
51
- end
52
118
 
53
- protected
54
-
55
- #
56
- # Given the creation metadata, prompts the user for the project to use,
57
- # then return the project data hash
58
- #
59
- # @param json [Hash] creation metadata
60
- #
61
- # @return [Hash] selected project's metadata
62
- #
63
- def select_project(json)
64
- projects = {}
65
- json['projects'].each do |project|
66
- data = {
67
- id: project['id'],
68
- issues: project['issuetypes']
69
- }
70
- projects[project['name']] = data
71
- end
72
- projects['Cancel'] = nil
73
- choice = self.io.choose("Select a project", projects.keys)
74
- return {} if choice == 'Cancel'
75
- return projects[choice]
76
- end
77
-
78
- #
79
- # Given the project metadata, prompts the user for the issue type to use
80
- # and returns the selected issue type.
81
- #
82
- # @param project_data [Hash] project metadata
83
- #
84
- # @return [Hash] selected issue type
85
- #
86
- def select_issue_type(project_data)
87
- issue_types = {}
88
- project_data[:issues].each do |issue_type|
89
- issue_types[issue_type['name']] = issue_type
90
- end
91
- issue_types['Cancel'] = nil
92
- choice = self.io.choose("Select an issue type", issue_types.keys)
93
- return issue_types[choice]
119
+ def assign_parent?
120
+ return false unless issue_type['subtask']
121
+ return false if io.no?('Set parent of subtask?')
122
+ true
94
123
  end
95
124
 
125
+ def parent
126
+ @parent ||= io.ask('Subtask parent:', default: Jira::Core.ticket)
127
+ end
128
+
129
+ def issue_type
130
+ @issue_type ||= issue_types[
131
+ io.select("Select an issue type:", issue_types.keys)
132
+ ]
133
+ end
134
+
135
+ def issue_types
136
+ @issue_types ||= (
137
+ issue_types = {}
138
+ project['issues'].each do |issue_type|
139
+ issue_types[issue_type['name']] = issue_type
140
+ end
141
+ issue_types
142
+ )
143
+ end
144
+
145
+ def summary
146
+ @summary ||= io.ask("Summary:")
147
+ end
148
+
149
+ def description
150
+ @description ||= io.ask("Description:")
151
+ end
152
+
153
+ end
96
154
  end
97
155
  end