jira-ruby 2.3.0 → 3.2.0
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.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE/bug_report.md +20 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/CI.yml +30 -0
- data/.github/workflows/codeql.yml +96 -0
- data/.github/workflows/rubocop.yml +18 -0
- data/.gitignore +3 -1
- data/.rubocop.yml +123 -0
- data/.yardopts +4 -0
- data/Gemfile +11 -3
- data/Guardfile +2 -0
- data/README.md +94 -19
- data/Rakefile +3 -4
- data/jira-ruby.gemspec +12 -17
- data/lib/jira/atlassian/jwt.rb +76 -0
- data/lib/jira/base.rb +37 -36
- data/lib/jira/base_factory.rb +4 -1
- data/lib/jira/client.rb +132 -51
- data/lib/jira/has_many_proxy.rb +32 -28
- data/lib/jira/http_client.rb +80 -13
- data/lib/jira/http_error.rb +4 -0
- data/lib/jira/jwt_client.rb +19 -43
- data/lib/jira/oauth_client.rb +68 -3
- data/lib/jira/railtie.rb +2 -0
- data/lib/jira/request_client.rb +31 -2
- data/lib/jira/resource/agile.rb +7 -9
- data/lib/jira/resource/applinks.rb +5 -3
- data/lib/jira/resource/attachment.rb +144 -4
- data/lib/jira/resource/board.rb +5 -3
- data/lib/jira/resource/board_configuration.rb +2 -0
- data/lib/jira/resource/comment.rb +14 -0
- data/lib/jira/resource/component.rb +2 -0
- data/lib/jira/resource/createmeta.rb +7 -5
- data/lib/jira/resource/field.rb +13 -12
- data/lib/jira/resource/filter.rb +2 -0
- data/lib/jira/resource/issue.rb +148 -54
- data/lib/jira/resource/issue_picker_suggestions.rb +4 -1
- data/lib/jira/resource/issue_picker_suggestions_issue.rb +2 -0
- data/lib/jira/resource/issuelink.rb +6 -3
- data/lib/jira/resource/issuelinktype.rb +2 -0
- data/lib/jira/resource/issuetype.rb +2 -0
- data/lib/jira/resource/priority.rb +2 -0
- data/lib/jira/resource/project.rb +4 -2
- data/lib/jira/resource/properties.rb +56 -0
- data/lib/jira/resource/rapidview.rb +5 -3
- data/lib/jira/resource/remotelink.rb +2 -0
- data/lib/jira/resource/resolution.rb +2 -0
- data/lib/jira/resource/serverinfo.rb +2 -0
- data/lib/jira/resource/sprint.rb +14 -23
- data/lib/jira/resource/status.rb +7 -1
- data/lib/jira/resource/status_category.rb +10 -0
- data/lib/jira/resource/suggested_issue.rb +2 -0
- data/lib/jira/resource/transition.rb +2 -0
- data/lib/jira/resource/user.rb +3 -1
- data/lib/jira/resource/version.rb +2 -0
- data/lib/jira/resource/watcher.rb +3 -2
- data/lib/jira/resource/webhook.rb +9 -3
- data/lib/jira/resource/worklog.rb +15 -2
- data/lib/jira/version.rb +3 -1
- data/lib/jira-ruby.rb +6 -3
- data/lib/tasks/generate.rake +3 -1
- data/spec/data/files/jwt-signed-urls.json +317 -0
- data/spec/data/files/short.txt +1 -0
- data/spec/integration/attachment_spec.rb +3 -3
- data/spec/integration/comment_spec.rb +8 -8
- data/spec/integration/component_spec.rb +7 -7
- data/spec/integration/field_spec.rb +3 -3
- data/spec/integration/issue_spec.rb +21 -18
- data/spec/integration/issuelinktype_spec.rb +3 -3
- data/spec/integration/issuetype_spec.rb +3 -3
- data/spec/integration/priority_spec.rb +3 -3
- data/spec/integration/project_spec.rb +8 -8
- data/spec/integration/properties_spec.rb +45 -0
- data/spec/integration/rapidview_spec.rb +10 -10
- data/spec/integration/resolution_spec.rb +3 -3
- data/spec/integration/status_category_spec.rb +20 -0
- data/spec/integration/status_spec.rb +4 -8
- data/spec/integration/transition_spec.rb +5 -4
- data/spec/integration/user_spec.rb +34 -11
- data/spec/integration/version_spec.rb +7 -7
- data/spec/integration/watcher_spec.rb +21 -18
- data/spec/integration/webhook_spec.rb +35 -0
- data/spec/integration/worklog_spec.rb +8 -8
- data/spec/jira/atlassian/jwt_spec.rb +60 -0
- data/spec/jira/base_factory_spec.rb +13 -3
- data/spec/jira/base_spec.rb +135 -98
- data/spec/jira/client_spec.rb +72 -56
- data/spec/jira/has_many_proxy_spec.rb +3 -3
- data/spec/jira/http_client_spec.rb +94 -27
- data/spec/jira/http_error_spec.rb +2 -2
- data/spec/jira/oauth_client_spec.rb +14 -8
- data/spec/jira/request_client_spec.rb +4 -4
- data/spec/jira/resource/agile_spec.rb +32 -32
- data/spec/jira/resource/attachment_spec.rb +170 -57
- data/spec/jira/resource/board_spec.rb +24 -23
- data/spec/jira/resource/createmeta_spec.rb +48 -48
- data/spec/jira/resource/field_spec.rb +44 -27
- data/spec/jira/resource/filter_spec.rb +7 -6
- data/spec/jira/resource/issue_picker_suggestions_spec.rb +17 -17
- data/spec/jira/resource/issue_spec.rb +159 -93
- data/spec/jira/resource/jira_picker_suggestions_issue_spec.rb +3 -3
- data/spec/jira/resource/project_factory_spec.rb +3 -2
- data/spec/jira/resource/project_spec.rb +16 -16
- data/spec/jira/resource/sprint_spec.rb +88 -9
- data/spec/jira/resource/status_spec.rb +21 -0
- data/spec/jira/resource/user_factory_spec.rb +5 -5
- data/spec/jira/resource/worklog_spec.rb +4 -4
- data/spec/mock_responses/board/1_issues.json +2 -1
- data/spec/mock_responses/issue/10002/properties/foo.json +4 -0
- data/spec/mock_responses/issue/10002/properties/xyz.json +4 -0
- data/spec/mock_responses/issue/10002/properties/xyz.put.json +4 -0
- data/spec/mock_responses/issue/10002/properties.json +12 -0
- data/spec/mock_responses/issue.json +1 -0
- data/spec/mock_responses/rapidview/SAMPLEPROJECT.issues.full.json +2 -1
- data/spec/mock_responses/rapidview/SAMPLEPROJECT.issues.json +2 -1
- data/spec/mock_responses/sprint/1.json +13 -0
- data/spec/mock_responses/status/1.json +8 -1
- data/spec/mock_responses/status.json +40 -5
- data/spec/mock_responses/statuscategory/1.json +7 -0
- data/spec/mock_responses/statuscategory.json +30 -0
- data/spec/mock_responses/{user_username=admin.json → user_accountId=1234567890abcdef01234567.json} +2 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/support/clients_helper.rb +5 -7
- data/spec/support/mock_client.rb +9 -0
- data/spec/support/mock_response.rb +8 -0
- data/spec/support/shared_examples/integration.rb +51 -58
- metadata +45 -257
- data/.travis.yml +0 -9
- data/example.rb +0 -232
- data/http-basic-example.rb +0 -113
- data/lib/jira/resource/sprint_report.rb +0 -8
- data/lib/jira/tasks.rb +0 -0
- data/spec/integration/webhook.rb +0 -25
- data/spec/jira/jwt_uri_builder_spec.rb +0 -59
- data/spec/mock_responses/jira/rest/webhooks/1.0/webhook.json +0 -11
- data/spec/mock_responses/webhook/webhook.json +0 -11
- /data/spec/mock_responses/{jira/rest/webhooks/1.0/webhook → webhook}/2.json +0 -0
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'net/http/post/multipart'
|
|
4
|
+
require 'open-uri'
|
|
2
5
|
|
|
3
6
|
module JIRA
|
|
4
7
|
module Resource
|
|
@@ -6,27 +9,153 @@ module JIRA
|
|
|
6
9
|
delegate_to_target_class :meta
|
|
7
10
|
end
|
|
8
11
|
|
|
12
|
+
# This class provides the Attachment object <-> REST mapping for JIRA::Resource::Attachment derived class,
|
|
13
|
+
# i.e. the Create, Retrieve, Update, Delete lifecycle methods.
|
|
14
|
+
#
|
|
15
|
+
# == Lifecycle methods
|
|
16
|
+
#
|
|
17
|
+
# === Retrieving a single attachment by Issue and attachment id
|
|
18
|
+
#
|
|
19
|
+
# # Find attachment with id 30076 on issue SUP-3000.
|
|
20
|
+
# issue = JIRA::Resource::Issue.find(client, 'SUP-3000', { fields: 'attachment' } )
|
|
21
|
+
# attachment = issue.attachments.find do |attachment_curr|
|
|
22
|
+
# 30076 == attachment_curr.id.to_i
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# === Retrieving meta information for an attachments
|
|
26
|
+
#
|
|
27
|
+
# attachment.meta
|
|
28
|
+
#
|
|
29
|
+
# === Retrieving file contents of attachment
|
|
30
|
+
#
|
|
31
|
+
# content = URI.open(attachment.content).read
|
|
32
|
+
# content = attachment.download_contents
|
|
33
|
+
# content = attachment.download_file { |file| file.read }
|
|
34
|
+
#
|
|
35
|
+
# === Adding an attachment to an issue
|
|
36
|
+
#
|
|
37
|
+
# Dir.mktmpdir do |dir|
|
|
38
|
+
# path = File.join(dir, filename)
|
|
39
|
+
# IO.copy_stream(file.path, path)
|
|
40
|
+
#
|
|
41
|
+
# issue = JIRA::Resource::Issue.find(client, 'SUP-3000', { fields: 'attachment' } )
|
|
42
|
+
# attachment = issue.attachments.build
|
|
43
|
+
# attachment.save!( { file: path, mimeType: content_type } )
|
|
44
|
+
# end
|
|
45
|
+
#
|
|
46
|
+
# === Deleting an attachment
|
|
47
|
+
#
|
|
48
|
+
# attachment.delete
|
|
49
|
+
#
|
|
50
|
+
# @!method save(attrs, path = url)
|
|
51
|
+
# Uploads a file as an attachment to its issue.
|
|
52
|
+
#
|
|
53
|
+
# Filename used will be the basename of the given file.
|
|
54
|
+
#
|
|
55
|
+
# @param [Hash] attrs the options to create a message with.
|
|
56
|
+
# @option attrs [IO,String] :file The file to upload, either a file object or a file path to find the file.
|
|
57
|
+
# @option attrs [String] :mimeType The MIME type of the file.
|
|
58
|
+
# @return [Boolean] True if successful, false if failed.
|
|
59
|
+
#
|
|
60
|
+
# @!attribute [r] self
|
|
61
|
+
# @return [String] URL to JSON of this attachment
|
|
62
|
+
# @!attribute [r] filename
|
|
63
|
+
# @return [String] the filename
|
|
64
|
+
# @!attribute [r] author
|
|
65
|
+
# @return [JIRA::Resource::User] the user who created the attachment
|
|
66
|
+
# @!attribute [r] created
|
|
67
|
+
# @return [String] timestamp when the attachment was created
|
|
68
|
+
# @!attribute [r] size
|
|
69
|
+
# @return [Integer] the file size
|
|
70
|
+
# @!attribute [r] mimeType
|
|
71
|
+
# @return [String] MIME of the content type
|
|
72
|
+
# @!attribute [r] content
|
|
73
|
+
# @return [String] URL (not the contents) to download the contents of the attachment
|
|
74
|
+
# @!attribute [r] thumbnail
|
|
75
|
+
# @return [String] URL to download the thumbnail of the attachment
|
|
76
|
+
#
|
|
9
77
|
class Attachment < JIRA::Base
|
|
10
78
|
belongs_to :issue
|
|
11
79
|
has_one :author, class: JIRA::Resource::User
|
|
12
80
|
|
|
81
|
+
# @private
|
|
13
82
|
def self.endpoint_name
|
|
14
83
|
'attachments'
|
|
15
84
|
end
|
|
16
85
|
|
|
86
|
+
# Gets metadata about attachments on the server.
|
|
87
|
+
# @example Return metadata
|
|
88
|
+
# Attachment.meta(client)
|
|
89
|
+
# => { "enabled" => true, "uploadLimit" => 1000000 }
|
|
90
|
+
# @return [Hash] The metadata for attachments. (See example.)
|
|
17
91
|
def self.meta(client)
|
|
18
|
-
response = client.get(client.options[:rest_base_path]
|
|
92
|
+
response = client.get("#{client.options[:rest_base_path]}/attachment/meta")
|
|
19
93
|
parse_json(response.body)
|
|
20
94
|
end
|
|
21
95
|
|
|
96
|
+
# Opens a file streaming the download of the attachment.
|
|
97
|
+
# @example Write file contents to a file.
|
|
98
|
+
# File.open('some-filename', 'wb') do |output|
|
|
99
|
+
# download_file do |file|
|
|
100
|
+
# IO.copy_stream(file, output)
|
|
101
|
+
# end
|
|
102
|
+
# end
|
|
103
|
+
# @example Stream file contents for an HTTP response.
|
|
104
|
+
# response.headers[ "Content-Type" ] = "application/octet-stream"
|
|
105
|
+
# download_file do |file|
|
|
106
|
+
# chunk = file.read(8000)
|
|
107
|
+
# while chunk.present? do
|
|
108
|
+
# response.stream.write(chunk)
|
|
109
|
+
# chunk = file.read(8000)
|
|
110
|
+
# end
|
|
111
|
+
# end
|
|
112
|
+
# response.stream.close
|
|
113
|
+
# @param [Hash] headers Any additional headers to call Jira.
|
|
114
|
+
# @yield |file|
|
|
115
|
+
# @yieldparam [IO] file The IO object streaming the download.
|
|
116
|
+
def download_file(headers = {}, &)
|
|
117
|
+
default_headers = client.options[:default_headers]
|
|
118
|
+
URI.parse(content).open(default_headers.merge(headers), &)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Downloads the file contents as a string object.
|
|
122
|
+
#
|
|
123
|
+
# Note that this reads the contents into a ruby string in memory.
|
|
124
|
+
# A file might be very large so it is recommended to avoid this unless you are certain about doing so.
|
|
125
|
+
# Use the download_file method instead and avoid calling the read method without a limit.
|
|
126
|
+
#
|
|
127
|
+
# @example Save file contents to a string.
|
|
128
|
+
# content = download_contents
|
|
129
|
+
# @param [Hash] headers Any additional headers to call Jira.
|
|
130
|
+
# @return [String,NilClass] The file contents.
|
|
131
|
+
def download_contents(headers = {})
|
|
132
|
+
download_file(headers, &:read)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Uploads a file as an attachment to its issue.
|
|
136
|
+
#
|
|
137
|
+
# Filename used will be the basename of the given file.
|
|
138
|
+
#
|
|
139
|
+
# @example Save a file as an attachment
|
|
140
|
+
# issue = JIRA::Resource::Issue.find(client, 'SUP-3000', { fields: 'attachment' } )
|
|
141
|
+
# attachment = issue.attachments.build
|
|
142
|
+
# attachment.save!( { file: path, mimeType: 'text/plain' } )
|
|
143
|
+
# @param [Hash] attrs the options to create a message with.
|
|
144
|
+
# @option attrs [IO,String] :file The file to upload, either a file object or a file path to find the file.
|
|
145
|
+
# @option attrs [String] :mimeType The MIME type of the file.
|
|
146
|
+
# @raise [JIRA::HTTPError] if failed
|
|
22
147
|
def save!(attrs, path = url)
|
|
23
148
|
file = attrs['file'] || attrs[:file] # Keep supporting 'file' parameter as a string for backward compatibility
|
|
24
|
-
|
|
149
|
+
# If :filename does not exist or is nil, that is fine as it will force
|
|
150
|
+
# Upload to determine the filename automatically from file.
|
|
151
|
+
# Breaking the filename out allows this to support any IO-based file parameter.
|
|
152
|
+
fname = attrs['filename'] || attrs[:filename]
|
|
153
|
+
mime_type = attrs['mimeType'] || attrs[:mimeType] || 'application/binary'
|
|
25
154
|
|
|
26
155
|
headers = { 'X-Atlassian-Token' => 'nocheck' }
|
|
27
|
-
data = { 'file' => UploadIO.new(file, mime_type,
|
|
156
|
+
data = { 'file' => Multipart::Post::UploadIO.new(file, mime_type, fname) }
|
|
28
157
|
|
|
29
|
-
response = client.post_multipart(path, data
|
|
158
|
+
response = client.post_multipart(path, data, headers)
|
|
30
159
|
|
|
31
160
|
set_attributes(attrs, response)
|
|
32
161
|
|
|
@@ -34,6 +163,17 @@ module JIRA
|
|
|
34
163
|
true
|
|
35
164
|
end
|
|
36
165
|
|
|
166
|
+
def http_download
|
|
167
|
+
# Actually fetch the attachment
|
|
168
|
+
# Note: Jira handles attachment's weird!
|
|
169
|
+
# Typically, they respond with a redirect location that should not have the same authentication
|
|
170
|
+
client.get(attrs['content'])
|
|
171
|
+
rescue JIRA::HTTPError => e
|
|
172
|
+
raise e unless e.response.code =~ /\A3\d\d\z/ && e.response['location'].present?
|
|
173
|
+
|
|
174
|
+
Net::HTTP.get_response(URI(e.response['location']))
|
|
175
|
+
end
|
|
176
|
+
|
|
37
177
|
private
|
|
38
178
|
|
|
39
179
|
def set_attributes(attributes, response)
|
data/lib/jira/resource/board.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'cgi'
|
|
2
4
|
|
|
3
5
|
module JIRA
|
|
@@ -7,7 +9,7 @@ module JIRA
|
|
|
7
9
|
|
|
8
10
|
class Board < JIRA::Base
|
|
9
11
|
def self.all(client)
|
|
10
|
-
path = path_base(client)
|
|
12
|
+
path = "#{path_base(client)}/board"
|
|
11
13
|
response = client.get(path)
|
|
12
14
|
json = parse_json(response.body)
|
|
13
15
|
results = json['values']
|
|
@@ -74,13 +76,13 @@ module JIRA
|
|
|
74
76
|
end
|
|
75
77
|
|
|
76
78
|
def add_issue_to_backlog(issue)
|
|
77
|
-
client.post(path_base(client)
|
|
79
|
+
client.post("#{path_base(client)}/backlog/issue", { issues: [issue.id] }.to_json)
|
|
78
80
|
end
|
|
79
81
|
|
|
80
82
|
private
|
|
81
83
|
|
|
82
84
|
def self.path_base(client)
|
|
83
|
-
client.options[:context_path]
|
|
85
|
+
"#{client.options[:context_path]}/rest/agile/1.0"
|
|
84
86
|
end
|
|
85
87
|
|
|
86
88
|
def path_base(client)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module JIRA
|
|
2
4
|
module Resource
|
|
3
5
|
class CommentFactory < JIRA::BaseFactory # :nodoc:
|
|
@@ -7,6 +9,18 @@ module JIRA
|
|
|
7
9
|
belongs_to :issue
|
|
8
10
|
|
|
9
11
|
nested_collections true
|
|
12
|
+
|
|
13
|
+
def self.all(client, options = {})
|
|
14
|
+
issue = options[:issue]
|
|
15
|
+
raise ArgumentError, 'parent issue is required' unless issue
|
|
16
|
+
|
|
17
|
+
response = client.get("#{issue.url}/#{endpoint_name}")
|
|
18
|
+
json = parse_json(response.body)
|
|
19
|
+
json = json[endpoint_name.pluralize]
|
|
20
|
+
json.map do |attrs|
|
|
21
|
+
new(client, { attrs: }.merge(options))
|
|
22
|
+
end
|
|
23
|
+
end
|
|
10
24
|
end
|
|
11
25
|
end
|
|
12
26
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module JIRA
|
|
2
4
|
module Resource
|
|
3
5
|
class CreatemetaFactory < JIRA::BaseFactory # :nodoc:
|
|
@@ -10,22 +12,22 @@ module JIRA
|
|
|
10
12
|
|
|
11
13
|
def self.all(client, params = {})
|
|
12
14
|
if params.key?(:projectKeys)
|
|
13
|
-
values = Array(params[:projectKeys]).map { |i|
|
|
15
|
+
values = Array(params[:projectKeys]).map { |i| i.is_a?(JIRA::Resource::Project) ? i.key : i }
|
|
14
16
|
params[:projectKeys] = values.join(',')
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
if params.key?(:projectIds)
|
|
18
|
-
values = Array(params[:projectIds]).map { |i|
|
|
20
|
+
values = Array(params[:projectIds]).map { |i| i.is_a?(JIRA::Resource::Project) ? i.id : i }
|
|
19
21
|
params[:projectIds] = values.join(',')
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
if params.key?(:issuetypeNames)
|
|
23
|
-
values = Array(params[:issuetypeNames]).map { |i|
|
|
25
|
+
values = Array(params[:issuetypeNames]).map { |i| i.is_a?(JIRA::Resource::Issuetype) ? i.name : i }
|
|
24
26
|
params[:issuetypeNames] = values.join(',')
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
if params.key?(:issuetypeIds)
|
|
28
|
-
values = Array(params[:issuetypeIds]).map { |i|
|
|
30
|
+
values = Array(params[:issuetypeIds]).map { |i| i.is_a?(JIRA::Resource::Issuetype) ? i.id : i }
|
|
29
31
|
params[:issuetypeIds] = values.join(',')
|
|
30
32
|
end
|
|
31
33
|
|
|
@@ -36,7 +38,7 @@ module JIRA
|
|
|
36
38
|
|
|
37
39
|
json = parse_json(response.body)
|
|
38
40
|
json['projects'].map do |attrs|
|
|
39
|
-
new(client, attrs:
|
|
41
|
+
new(client, attrs:)
|
|
40
42
|
end
|
|
41
43
|
end
|
|
42
44
|
end
|
data/lib/jira/resource/field.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module JIRA
|
|
2
4
|
module Resource
|
|
3
5
|
class FieldFactory < JIRA::BaseFactory # :nodoc:
|
|
@@ -19,43 +21,42 @@ module JIRA
|
|
|
19
21
|
|
|
20
22
|
def self.map_fields(client)
|
|
21
23
|
field_map = {}
|
|
22
|
-
field_map_reverse = {}
|
|
23
24
|
fields = client.Field.all
|
|
24
25
|
|
|
25
26
|
# two pass approach, so that a custom field with the same name
|
|
26
27
|
# as a system field can't take precedence
|
|
27
28
|
fields.each do |f|
|
|
28
29
|
next if f.custom
|
|
30
|
+
|
|
29
31
|
name = safe_name(f.name)
|
|
30
|
-
field_map_reverse[f.id] = [f.name, name] # capture both the official name, and the mapped name
|
|
31
32
|
field_map[name] = f.id
|
|
32
33
|
end
|
|
33
34
|
|
|
34
|
-
fields.each do |f|
|
|
35
|
+
fields.each do |f| # rubocop:disable Style/CombinableLoops
|
|
35
36
|
next unless f.custom
|
|
37
|
+
|
|
36
38
|
name = if field_map.key? f.name
|
|
37
39
|
renamed = safer_name(f.name, f.id)
|
|
38
40
|
warn "Duplicate Field name #{f.name} #{f.id} - renaming as #{renamed}"
|
|
39
41
|
renamed
|
|
40
42
|
else
|
|
41
43
|
safe_name(f.name)
|
|
42
|
-
|
|
43
|
-
field_map_reverse[f.id] = [f.name, name] # capture both the official name, and the mapped name
|
|
44
|
+
end
|
|
44
45
|
field_map[name] = f.id
|
|
45
46
|
end
|
|
46
47
|
|
|
47
|
-
client.
|
|
48
|
-
client.cache.field_map = field_map
|
|
48
|
+
client.field_map_cache = field_map
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def self.field_map(client)
|
|
52
|
-
client.
|
|
52
|
+
client.field_map_cache
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def self.name_to_id(client, field_name)
|
|
56
56
|
field_name = field_name.to_s
|
|
57
|
-
return field_name unless client.
|
|
58
|
-
|
|
57
|
+
return field_name unless client.field_map_cache && client.field_map_cache[field_name]
|
|
58
|
+
|
|
59
|
+
client.field_map_cache[field_name]
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
def respond_to?(method_name, _include_all = false)
|
|
@@ -66,7 +67,7 @@ module JIRA
|
|
|
66
67
|
end
|
|
67
68
|
end
|
|
68
69
|
|
|
69
|
-
def method_missing(method_name, *args, &
|
|
70
|
+
def method_missing(method_name, *args, &)
|
|
70
71
|
if attrs.key?(method_name.to_s)
|
|
71
72
|
attrs[method_name.to_s]
|
|
72
73
|
else
|
|
@@ -74,7 +75,7 @@ module JIRA
|
|
|
74
75
|
if attrs.key?(official_name)
|
|
75
76
|
attrs[official_name]
|
|
76
77
|
else
|
|
77
|
-
super
|
|
78
|
+
super
|
|
78
79
|
end
|
|
79
80
|
end
|
|
80
81
|
end
|
data/lib/jira/resource/filter.rb
CHANGED
data/lib/jira/resource/issue.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'cgi'
|
|
2
4
|
require 'json'
|
|
3
5
|
|
|
@@ -6,100 +8,168 @@ module JIRA
|
|
|
6
8
|
class IssueFactory < JIRA::BaseFactory # :nodoc:
|
|
7
9
|
end
|
|
8
10
|
|
|
11
|
+
# This class provides the Issue object <-> REST mapping for JIRA::Resource::Issue derived class,
|
|
12
|
+
# i.e. the Create, Retrieve, Update, Delete lifecycle methods.
|
|
13
|
+
#
|
|
14
|
+
# == Lifecycle methods
|
|
15
|
+
#
|
|
16
|
+
# === Retrieving all issues
|
|
17
|
+
#
|
|
18
|
+
# client.Issue.all
|
|
19
|
+
#
|
|
20
|
+
# === Retrieving a single issue
|
|
21
|
+
#
|
|
22
|
+
# options = { expand: 'editmeta' }
|
|
23
|
+
# issue = client.Issue.find("SUP-3000", options)
|
|
24
|
+
#
|
|
25
|
+
# === Creating a new issue
|
|
26
|
+
#
|
|
27
|
+
# issue = client.Issue.build(fields: { summary: 'New issue', project: { key: 'SUP' }, issuetype: { name: 'Bug' } })
|
|
28
|
+
# issue.save
|
|
29
|
+
#
|
|
30
|
+
# === Updating an issue
|
|
31
|
+
#
|
|
32
|
+
# issue = client.Issue.find("SUP-3000")
|
|
33
|
+
# issue.save(fields: { summary: 'Updated issue' })
|
|
34
|
+
#
|
|
35
|
+
# === Deleting an issue
|
|
36
|
+
#
|
|
37
|
+
# issue = client.Issue.find("SUP-3000")
|
|
38
|
+
# issue.delete
|
|
39
|
+
#
|
|
9
40
|
class Issue < JIRA::Base
|
|
10
|
-
has_one :reporter,
|
|
11
|
-
|
|
12
|
-
has_one :
|
|
13
|
-
nested_under: 'fields'
|
|
14
|
-
has_one :project, nested_under: 'fields'
|
|
15
|
-
|
|
41
|
+
has_one :reporter, class: JIRA::Resource::User, nested_under: 'fields'
|
|
42
|
+
has_one :assignee, class: JIRA::Resource::User, nested_under: 'fields'
|
|
43
|
+
has_one :project, nested_under: 'fields'
|
|
16
44
|
has_one :issuetype, nested_under: 'fields'
|
|
17
|
-
|
|
18
|
-
has_one :
|
|
19
|
-
|
|
20
|
-
has_one :status, nested_under: 'fields'
|
|
21
|
-
|
|
45
|
+
has_one :priority, nested_under: 'fields'
|
|
46
|
+
has_one :status, nested_under: 'fields'
|
|
47
|
+
has_one :resolution, nested_under: 'fields'
|
|
22
48
|
has_many :transitions
|
|
23
|
-
|
|
24
49
|
has_many :components, nested_under: 'fields'
|
|
25
|
-
|
|
26
50
|
has_many :comments, nested_under: %w[fields comment]
|
|
27
|
-
|
|
28
|
-
has_many :
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
has_many :versions, nested_under: 'fields'
|
|
32
|
-
has_many :fixVersions, class: JIRA::Resource::Version,
|
|
33
|
-
nested_under: 'fields'
|
|
34
|
-
|
|
51
|
+
has_many :attachments, nested_under: 'fields', attribute_key: 'attachment'
|
|
52
|
+
has_many :versions, nested_under: 'fields'
|
|
53
|
+
has_many :fixVersions, class: JIRA::Resource::Version, nested_under: 'fields'
|
|
35
54
|
has_many :worklogs, nested_under: %w[fields worklog]
|
|
36
|
-
has_one :sprint, class: JIRA::Resource::Sprint,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
has_many :closed_sprints, class: JIRA::Resource::Sprint,
|
|
40
|
-
nested_under: 'fields', attribute_key: 'closedSprints'
|
|
41
|
-
|
|
55
|
+
has_one :sprint, class: JIRA::Resource::Sprint, nested_under: 'fields'
|
|
56
|
+
has_many :closed_sprints, class: JIRA::Resource::Sprint, nested_under: 'fields', attribute_key: 'closedSprints'
|
|
42
57
|
has_many :issuelinks, nested_under: 'fields'
|
|
43
|
-
|
|
44
58
|
has_many :remotelink, class: JIRA::Resource::Remotelink
|
|
59
|
+
has_many :watchers, attribute_key: 'watches', nested_under: %w[fields watches]
|
|
60
|
+
has_many :properties, class: JIRA::Resource::Properties
|
|
45
61
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
62
|
+
# Get collection of issues.
|
|
63
|
+
# @param client [JIRA::Client]
|
|
64
|
+
# @return [Array<JIRA::Resource::Issue>]
|
|
49
65
|
def self.all(client)
|
|
50
66
|
start_at = 0
|
|
51
67
|
max_results = 1000
|
|
52
68
|
result = []
|
|
53
69
|
loop do
|
|
54
70
|
url = client.options[:rest_base_path] +
|
|
55
|
-
"/search?expand=transitions.fields&maxResults=#{max_results}&startAt=#{start_at}"
|
|
71
|
+
"/search/jql?expand=transitions.fields&maxResults=#{max_results}&startAt=#{start_at}"
|
|
56
72
|
response = client.get(url)
|
|
57
73
|
json = parse_json(response.body)
|
|
58
74
|
json['issues'].map do |issue|
|
|
59
75
|
result.push(client.Issue.build(issue))
|
|
60
76
|
end
|
|
61
77
|
break if json['issues'].empty?
|
|
78
|
+
|
|
62
79
|
start_at += json['issues'].size
|
|
63
80
|
end
|
|
64
81
|
result
|
|
65
82
|
end
|
|
66
83
|
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
# Get issues using JQL query.
|
|
85
|
+
# @param client [JIRA::Client]
|
|
86
|
+
# @param jql [String] the JQL query string to search with
|
|
87
|
+
# @param options [Hash] Jira API options for the search
|
|
88
|
+
# @return [Array<JIRA::Resource::Issue>] or [Integer] total count if max_results is 0
|
|
89
|
+
def self.jql(client, jql, options = { fields: nil, max_results: nil, expand: nil, reconcile_issues: nil })
|
|
90
|
+
issues = []
|
|
91
|
+
total = nil
|
|
92
|
+
next_page_token = nil
|
|
93
|
+
is_last = false
|
|
94
|
+
|
|
95
|
+
until is_last
|
|
96
|
+
result = jql_paged(client, jql, options.merge(next_page_token:))
|
|
97
|
+
|
|
98
|
+
issues.concat(result[:issues])
|
|
99
|
+
total = result[:total]
|
|
100
|
+
next_page_token = result[:next_page_token]
|
|
101
|
+
is_last = next_page_token.nil?
|
|
102
|
+
end
|
|
103
|
+
options[:max_results]&.zero? ? total : issues
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Get paged issues using JQL query.
|
|
107
|
+
# @param jql [String] the JQL query string to search with
|
|
108
|
+
# @param options [Hash] Jira API options for the search, including next_page_token
|
|
109
|
+
# @return [Hash] with format { issues: [JIRA::Resource::Issue], next_page_token: [String], total: [Integer] }
|
|
110
|
+
def self.jql_paged(client, jql, options = { fields: nil, max_results: nil, expand: nil, reconcile_issues: nil, next_page_token: nil })
|
|
111
|
+
url = jql_url(client, jql, options)
|
|
112
|
+
next_page_token = options[:next_page_token]
|
|
113
|
+
max_results = options[:max_results]
|
|
114
|
+
|
|
115
|
+
issues = []
|
|
69
116
|
|
|
70
|
-
|
|
71
|
-
|
|
117
|
+
page_url = url.dup
|
|
118
|
+
page_url << "&nextPageToken=#{next_page_token}" if next_page_token
|
|
119
|
+
|
|
120
|
+
response = client.get(page_url)
|
|
121
|
+
json = parse_json(response.body)
|
|
122
|
+
total = json['total']
|
|
123
|
+
|
|
124
|
+
unless max_results&.zero?
|
|
125
|
+
next_page_token = json['nextPageToken']
|
|
126
|
+
json['issues'].map do |issue|
|
|
127
|
+
issues << client.Issue.build(issue)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
{ issues:, next_page_token:, total: }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def self.jql_url(client, jql, options)
|
|
135
|
+
url = client.options[:rest_base_path] + "/search/jql?jql=#{CGI.escape(jql)}"
|
|
136
|
+
|
|
137
|
+
if options[:fields]
|
|
138
|
+
url << "&fields=#{options[:fields].map do |value|
|
|
139
|
+
CGI.escape(client.Field.name_to_id(value))
|
|
140
|
+
end.join(',')}"
|
|
141
|
+
end
|
|
72
142
|
url << "&maxResults=#{CGI.escape(options[:max_results].to_s)}" if options[:max_results]
|
|
73
|
-
url <<
|
|
143
|
+
url << "&reconcileIssues=#{CGI.escape(options[:reconcile_issues].to_s)}" if options[:reconcile_issues]
|
|
74
144
|
|
|
75
145
|
if options[:expand]
|
|
76
146
|
options[:expand] = [options[:expand]] if options[:expand].is_a?(String)
|
|
77
147
|
url << "&expand=#{options[:expand].to_a.map { |value| CGI.escape(value.to_s) }.join(',')}"
|
|
78
148
|
end
|
|
79
|
-
|
|
80
|
-
response = client.get(url)
|
|
81
|
-
json = parse_json(response.body)
|
|
82
|
-
if options[:max_results] && (options[:max_results] == 0)
|
|
83
|
-
return json['total']
|
|
84
|
-
end
|
|
85
|
-
json['issues'].map do |issue|
|
|
86
|
-
client.Issue.build(issue)
|
|
87
|
-
end
|
|
149
|
+
url
|
|
88
150
|
end
|
|
89
151
|
|
|
90
152
|
# Fetches the attributes for the specified resource from JIRA unless
|
|
91
153
|
# the resource is already expanded and the optional force reload flag
|
|
92
154
|
# is not set
|
|
155
|
+
# @param [Boolean] reload
|
|
156
|
+
# @param [Hash] query_params
|
|
157
|
+
# @option query_params [String] :fields
|
|
158
|
+
# @option query_params [String] :expand
|
|
159
|
+
# @option query_params [Integer] :startAt
|
|
160
|
+
# @option query_params [Integer] :maxResults
|
|
161
|
+
# @return [void]
|
|
93
162
|
def fetch(reload = false, query_params = {})
|
|
94
163
|
return if expanded? && !reload
|
|
164
|
+
|
|
95
165
|
response = client.get(url_with_query_params(url, query_params))
|
|
96
166
|
set_attrs_from_response(response)
|
|
97
|
-
if @attrs && @attrs['fields'] &&
|
|
167
|
+
if @attrs && @attrs['fields'] &&
|
|
168
|
+
@attrs['fields']['worklog'] &&
|
|
169
|
+
(@attrs['fields']['worklog']['total'] > @attrs['fields']['worklog']['maxResults'])
|
|
98
170
|
worklog_url = client.options[:rest_base_path] + "/#{self.class.endpoint_name}/#{id}/worklog"
|
|
99
171
|
response = client.get(worklog_url)
|
|
100
|
-
unless response.body.nil? || (response.body.length < 2)
|
|
101
|
-
set_attrs({ 'fields' => { 'worklog' => self.class.parse_json(response.body) } }, false)
|
|
102
|
-
end
|
|
172
|
+
set_attrs({ 'fields' => { 'worklog' => self.class.parse_json(response.body) } }, false) unless response.body.nil? || (response.body.length < 2)
|
|
103
173
|
end
|
|
104
174
|
@expanded = true
|
|
105
175
|
end
|
|
@@ -112,15 +182,19 @@ module JIRA
|
|
|
112
182
|
json['fields']
|
|
113
183
|
end
|
|
114
184
|
|
|
185
|
+
# @private
|
|
115
186
|
def respond_to?(method_name, _include_all = false)
|
|
116
|
-
if attrs.key?('fields') && [method_name.to_s, client.Field.name_to_id(method_name)].any?
|
|
187
|
+
if attrs.key?('fields') && [method_name.to_s, client.Field.name_to_id(method_name)].any? do |k|
|
|
188
|
+
attrs['fields'].key?(k)
|
|
189
|
+
end
|
|
117
190
|
true
|
|
118
191
|
else
|
|
119
192
|
super(method_name)
|
|
120
193
|
end
|
|
121
194
|
end
|
|
122
195
|
|
|
123
|
-
|
|
196
|
+
# @private
|
|
197
|
+
def method_missing(method_name, *args, &)
|
|
124
198
|
if attrs.key?('fields')
|
|
125
199
|
if attrs['fields'].key?(method_name.to_s)
|
|
126
200
|
attrs['fields'][method_name.to_s]
|
|
@@ -129,13 +203,33 @@ module JIRA
|
|
|
129
203
|
if attrs['fields'].key?(official_name)
|
|
130
204
|
attrs['fields'][official_name]
|
|
131
205
|
else
|
|
132
|
-
super
|
|
206
|
+
super
|
|
133
207
|
end
|
|
134
208
|
end
|
|
135
209
|
else
|
|
136
|
-
super
|
|
210
|
+
super
|
|
137
211
|
end
|
|
138
212
|
end
|
|
213
|
+
|
|
214
|
+
# @!method self.find(client, key, options = {})
|
|
215
|
+
# Gets/fetches an issue from JIRA.
|
|
216
|
+
#
|
|
217
|
+
# Note: attachments are not fetched by default.
|
|
218
|
+
#
|
|
219
|
+
# @param [JIRA::Client] client
|
|
220
|
+
# @param [String] key the key of the issue to find
|
|
221
|
+
# @param [Hash] options the options to find the issue with
|
|
222
|
+
# @option options [String] :fields the fields to include in the response
|
|
223
|
+
# @return [JIRA::Resource::Issue] the found issue
|
|
224
|
+
# @example Find an issue
|
|
225
|
+
# JIRA::Resource::Issue.find(client, "SUP-3000", { fields: %w[summary description attachment created ] } )
|
|
226
|
+
#
|
|
227
|
+
# @!method self.build(attrs = {})
|
|
228
|
+
# Constructs a new issue object.
|
|
229
|
+
# @param [Hash] attrs the attributes to initialize the issue with
|
|
230
|
+
# @return [JIRA::Resource::Issue] the new issue
|
|
231
|
+
#
|
|
232
|
+
# .
|
|
139
233
|
end
|
|
140
234
|
end
|
|
141
235
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module JIRA
|
|
2
4
|
module Resource
|
|
3
5
|
class IssuePickerSuggestionsFactory < JIRA::BaseFactory # :nodoc:
|
|
@@ -6,7 +8,8 @@ module JIRA
|
|
|
6
8
|
class IssuePickerSuggestions < JIRA::Base
|
|
7
9
|
has_many :sections, class: JIRA::Resource::IssuePickerSuggestionsIssue
|
|
8
10
|
|
|
9
|
-
def self.all(client, query = '', options = { current_jql: nil, current_issue_key: nil, current_project_id: nil,
|
|
11
|
+
def self.all(client, query = '', options = { current_jql: nil, current_issue_key: nil, current_project_id: nil,
|
|
12
|
+
show_sub_tasks: nil, show_sub_tasks_parent: nil })
|
|
10
13
|
url = client.options[:rest_base_path] + "/issue/picker?query=#{CGI.escape(query)}"
|
|
11
14
|
|
|
12
15
|
url << "¤tJQL=#{CGI.escape(options[:current_jql])}" if options[:current_jql]
|