mine-shipper 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/COPYING +674 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +43 -0
- data/README.md +49 -0
- data/Rakefile +11 -0
- data/bin/mine-shipper +21 -0
- data/lib/mine-shipper/app.rb +81 -0
- data/lib/mine-shipper/config.rb +45 -0
- data/lib/mine-shipper/github.rb +71 -0
- data/lib/mine-shipper/issue_comment.rb +78 -0
- data/lib/mine-shipper/redmine.rb +170 -0
- data/lib/mine-shipper/version.rb +19 -0
- data/mine-shipper.gemspec +24 -0
- data/sample.env +5 -0
- data/test/test_issue_comment.rb +64 -0
- data/test/test_redmine.rb +139 -0
- metadata +133 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
mine-shipper (0.0.1)
|
5
|
+
dotenv (~> 2.7)
|
6
|
+
octokit (~> 4.0)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
addressable (2.7.0)
|
12
|
+
public_suffix (>= 2.0.2, < 5.0)
|
13
|
+
dotenv (2.7.6)
|
14
|
+
faraday (1.0.1)
|
15
|
+
multipart-post (>= 1.2, < 3)
|
16
|
+
multipart-post (2.1.1)
|
17
|
+
octokit (4.18.0)
|
18
|
+
faraday (>= 0.9)
|
19
|
+
sawyer (~> 0.8.0, >= 0.5.3)
|
20
|
+
power_assert (1.2.0)
|
21
|
+
public_suffix (4.0.5)
|
22
|
+
rake (13.0.1)
|
23
|
+
rr (1.2.1)
|
24
|
+
sawyer (0.8.2)
|
25
|
+
addressable (>= 2.3.5)
|
26
|
+
faraday (> 0.8, < 2.0)
|
27
|
+
test-unit (3.3.6)
|
28
|
+
power_assert
|
29
|
+
test-unit-rr (1.0.5)
|
30
|
+
rr (>= 1.1.1)
|
31
|
+
test-unit (>= 2.5.2)
|
32
|
+
|
33
|
+
PLATFORMS
|
34
|
+
ruby
|
35
|
+
|
36
|
+
DEPENDENCIES
|
37
|
+
mine-shipper!
|
38
|
+
rake (~> 13.0)
|
39
|
+
test-unit (~> 3.3)
|
40
|
+
test-unit-rr (~> 1.0)
|
41
|
+
|
42
|
+
BUNDLED WITH
|
43
|
+
2.1.4
|
data/README.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# mine-shipper
|
2
|
+
|
3
|
+
A command to duplicate comments on a GitHub issue to an associated Redmine issue.
|
4
|
+
|
5
|
+
## Prepare on Redmine
|
6
|
+
|
7
|
+
1. Create API access key of your Redmine account
|
8
|
+
* My account -> API access key
|
9
|
+
2. Create a custom field with the following properties (Administration -> Custom fields)
|
10
|
+
* Format: `Link`
|
11
|
+
* Name: `GitHub`
|
12
|
+
* Regular expression: `\A([\w-]+)/([\w-]+)#(\d+)\z`
|
13
|
+
* Used as a filter: checked
|
14
|
+
3. Enable the custom field at your Redmine project
|
15
|
+
4. Create an issue on the project
|
16
|
+
* Fill "GitHub" custom fileld to associate a GitHub issue to the Redmine issue.
|
17
|
+
e.g.) groonga/groonga#1062
|
18
|
+
|
19
|
+
## Installation
|
20
|
+
|
21
|
+
```
|
22
|
+
$ bundle install
|
23
|
+
$ cp sample.env .env
|
24
|
+
```
|
25
|
+
|
26
|
+
Then edit .env to adopt to your environment.
|
27
|
+
`GITHUB_ACCESS_TOKEN` is optional but recommended to set to exceed [API rate limit](https://developer.github.com/v3/rate_limit/).
|
28
|
+
See https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token to know how to get it.
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
Run ./bin/mine-shipper command with `--github-issue` option to specify the issue.
|
33
|
+
|
34
|
+
e.g.)
|
35
|
+
|
36
|
+
```
|
37
|
+
$ bundle exec ./bin/mine-shipper --github-issue groonga/groonga#1062
|
38
|
+
```
|
39
|
+
|
40
|
+
The comments on the GitHub issue will be copied to the associated Redmine issue.
|
41
|
+
If no associated issue on Redmine is found or the comments are already copied, it does nothing.
|
42
|
+
|
43
|
+
## Plans
|
44
|
+
|
45
|
+
* Create a new issue when no associated issue is found
|
46
|
+
* Close a Redmine issue automatically if an associated GitHub issue is closed
|
47
|
+
* Synchronize updated comments ([need to add new API to Redmine](https://www.redmine.org/issues/10171))
|
48
|
+
* Work as Webhook
|
49
|
+
* ...
|
data/Rakefile
ADDED
data/bin/mine-shipper
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Copyright (C) 2020 Takuro Ashie <ashie@clear-code.com>
|
4
|
+
#
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
8
|
+
# (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
require_relative '../lib/mine-shipper/app'
|
19
|
+
|
20
|
+
app = MineShipper::App.new
|
21
|
+
app.run
|
@@ -0,0 +1,81 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2020 Takuro Ashie <ashie@clear-code.com>
|
3
|
+
#
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
7
|
+
# (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
16
|
+
|
17
|
+
require 'logger'
|
18
|
+
require_relative './config'
|
19
|
+
require_relative './github'
|
20
|
+
require_relative './redmine'
|
21
|
+
|
22
|
+
module MineShipper
|
23
|
+
class App
|
24
|
+
def initialize
|
25
|
+
@config = Config::new
|
26
|
+
@issue_key = @config[:github][:issue]
|
27
|
+
@logger = Logger.new(STDOUT)
|
28
|
+
@logger.level = @config[:log_level]
|
29
|
+
end
|
30
|
+
|
31
|
+
def run
|
32
|
+
begin
|
33
|
+
do_run
|
34
|
+
rescue Exception => e
|
35
|
+
@logger.error(e)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def do_run
|
42
|
+
@logger.info("Fetching #{@issue_key} comments on Redmine...")
|
43
|
+
redmine_issue = get_redmine_issue
|
44
|
+
if redmine_issue
|
45
|
+
@logger.info("Fetching #{@issue_key} comments on GitHub...")
|
46
|
+
github_issue = get_github_issue
|
47
|
+
dump_issue(github_issue)
|
48
|
+
dump_issue(redmine_issue)
|
49
|
+
redmine_issue.sync_comments(github_issue.comments)
|
50
|
+
@logger.info("Done synchronizing issue comments of #{@issue_key}")
|
51
|
+
else
|
52
|
+
@logger.info("Cannot find Redmine issue for #{@issue_key}")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_github_issue
|
57
|
+
project_name, issue_id = @issue_key.split('#', 2)
|
58
|
+
github = GitHub::new(@config[:github][:access_token])
|
59
|
+
github.issue(project_name, issue_id)
|
60
|
+
end
|
61
|
+
|
62
|
+
def get_redmine_issue
|
63
|
+
redmine = Redmine.new(@config[:redmine][:base_url],
|
64
|
+
@config[:redmine][:api_key])
|
65
|
+
redmine.issue_by_custom_field(@config[:redmine][:custom_field_id],
|
66
|
+
@issue_key)
|
67
|
+
end
|
68
|
+
|
69
|
+
def dump_issue(issue)
|
70
|
+
@logger.debug("#{issue.tracker} Issue \##{issue.identifier}: #{issue.title}")
|
71
|
+
issue.comments.each do |comment|
|
72
|
+
dump_comment(comment)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def dump_comment(comment)
|
77
|
+
time = comment.created_at.getlocal
|
78
|
+
@logger.debug("#{comment.tracker} Comment #{time}\n#{comment.body}")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2020 Takuro Ashie <ashie@clear-code.com>
|
3
|
+
#
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
7
|
+
# (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
16
|
+
|
17
|
+
require 'optparse'
|
18
|
+
require 'dotenv/load'
|
19
|
+
|
20
|
+
module MineShipper
|
21
|
+
class Config < Hash
|
22
|
+
DEFAULT_CONFIG = {
|
23
|
+
log_level: ENV["MINE_SHIPPER_LOG_LEVEL"] || 'WARN',
|
24
|
+
github: {
|
25
|
+
access_token: ENV["GITHUB_ACCESS_TOKEN"],
|
26
|
+
issue: nil
|
27
|
+
},
|
28
|
+
redmine: {
|
29
|
+
base_url: ENV["REDMINE_BASE_URL"],
|
30
|
+
custom_field_id: ENV["REDMINE_CUSTOM_FIELD_ID"],
|
31
|
+
api_key: ENV["REDMINE_API_KEY"],
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
def initialize(argv = ARGV)
|
36
|
+
self.merge!(DEFAULT_CONFIG)
|
37
|
+
OptionParser.new do |opts|
|
38
|
+
opts.on("--github-issue ISSUE") do |github_issue|
|
39
|
+
self[:github][:issue] = github_issue
|
40
|
+
end
|
41
|
+
opts.parse!(argv)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2020 Takuro Ashie <ashie@clear-code.com>
|
3
|
+
#
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
7
|
+
# (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
16
|
+
|
17
|
+
require 'octokit'
|
18
|
+
require_relative './issue_comment.rb'
|
19
|
+
|
20
|
+
module MineShipper
|
21
|
+
class GitHub
|
22
|
+
def initialize(access_token = nil)
|
23
|
+
@client = Octokit::Client.new(:access_token => access_token)
|
24
|
+
end
|
25
|
+
|
26
|
+
def issue(project_name, issue_id)
|
27
|
+
@issue = @client.issue(project_name, issue_id)
|
28
|
+
Issue.new(@client, project_name, issue_id)
|
29
|
+
end
|
30
|
+
|
31
|
+
class Issue
|
32
|
+
attr_reader :comments
|
33
|
+
|
34
|
+
def initialize(client, project_name, issue_id)
|
35
|
+
@project_name = project_name
|
36
|
+
@issue_id = issue_id
|
37
|
+
@issue = client.issue(@project_name, @issue_id)
|
38
|
+
@comments = [Comment.new(@issue)]
|
39
|
+
comments = client.issue_comments(@project_name, @issue_id)
|
40
|
+
comments.each do |comment|
|
41
|
+
@comments << Comment.new(comment)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def tracker
|
46
|
+
"GitHub"
|
47
|
+
end
|
48
|
+
|
49
|
+
def identifier
|
50
|
+
"#{@project_name}#{@issue_id}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def title
|
54
|
+
@issue.title
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class Comment < IssueComment
|
59
|
+
attr_reader :tracker, :created_at, :updated_at, :url, :user, :body
|
60
|
+
def initialize(comment)
|
61
|
+
@comment = comment
|
62
|
+
@tracker = "GitHub"
|
63
|
+
@created_at = @comment.created_at
|
64
|
+
@updated_at = @comment.updated_at
|
65
|
+
@url = @comment.html_url
|
66
|
+
@usr = @comment.user.login
|
67
|
+
@body = @comment.body.gsub(/\R/, "\n")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2020 Takuro Ashie <ashie@clear-code.com>
|
3
|
+
#
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
7
|
+
# (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
16
|
+
|
17
|
+
module MineShipper
|
18
|
+
class NotImplemented < StandardError
|
19
|
+
end
|
20
|
+
|
21
|
+
class IssueComment
|
22
|
+
def tracker
|
23
|
+
"Unknown"
|
24
|
+
end
|
25
|
+
|
26
|
+
def created_at
|
27
|
+
raise NotImplemented
|
28
|
+
end
|
29
|
+
|
30
|
+
def updated_at
|
31
|
+
raise NotImplemented
|
32
|
+
end
|
33
|
+
|
34
|
+
def url
|
35
|
+
raise NotImplemented
|
36
|
+
end
|
37
|
+
|
38
|
+
def user
|
39
|
+
raise NotImplemented
|
40
|
+
end
|
41
|
+
|
42
|
+
def body
|
43
|
+
raise NotImplemented
|
44
|
+
end
|
45
|
+
|
46
|
+
def render
|
47
|
+
title = "#{user} commented on #{created_at.getlocal}"
|
48
|
+
result = "### [#{title}](#{url})\n"
|
49
|
+
result += "{{collapse(More...)\n"
|
50
|
+
result += "* created_at: \"#{created_at.getlocal}\"\n"
|
51
|
+
result += "* updated_at: \"#{updated_at.getlocal}\"\n"
|
52
|
+
result += "}}\n"
|
53
|
+
result += "\n"
|
54
|
+
result += body
|
55
|
+
result
|
56
|
+
end
|
57
|
+
|
58
|
+
def corresponding?(comment)
|
59
|
+
escaped_url = Regexp.escape(comment.url)
|
60
|
+
escaped_time = Regexp.escape("#{comment.created_at.getlocal}")
|
61
|
+
if body.match(/^### \[#{comment.user} commented on #{escaped_time}\]\(#{escaped_url}\)\n/)
|
62
|
+
true
|
63
|
+
else
|
64
|
+
false
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def updated?(comment)
|
69
|
+
lines = body.split("\n", 6)
|
70
|
+
return false if lines[1] != "{{collapse(More...)"
|
71
|
+
return false if lines[4] != "}}"
|
72
|
+
timestr = lines[3].match(/^\* updated_at: \"(.*)\"$/).to_a[1]
|
73
|
+
return false if timestr.nil?
|
74
|
+
updated_time = Time.parse(timestr)
|
75
|
+
updated_time >= comment.updated_at
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2020 Takuro Ashie <ashie@clear-code.com>
|
3
|
+
#
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
7
|
+
# (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
16
|
+
|
17
|
+
require 'net/https'
|
18
|
+
require 'uri'
|
19
|
+
require 'json'
|
20
|
+
require_relative './issue_comment.rb'
|
21
|
+
|
22
|
+
module MineShipper
|
23
|
+
class Redmine
|
24
|
+
class Issue
|
25
|
+
attr_reader :tracker, :id, :identifier, :title, :comments
|
26
|
+
|
27
|
+
def initialize(redmine, json)
|
28
|
+
@redmine = redmine
|
29
|
+
@json = json
|
30
|
+
@tracker = "Redmine"
|
31
|
+
@id = @json["id"]
|
32
|
+
@identifier = "##{@json["id"]}"
|
33
|
+
@title = @json["subject"]
|
34
|
+
@comments = []
|
35
|
+
@json["journals"].each do |journal|
|
36
|
+
next if journal["notes"].empty?
|
37
|
+
@comments << Comment.new(journal)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def sync_comments(comments)
|
42
|
+
path = "issues/#{id}.json"
|
43
|
+
comments.each do |comment|
|
44
|
+
sync_comment(comment)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def sync_comment(comment)
|
49
|
+
my_comment = find_comment(comment)
|
50
|
+
if my_comment
|
51
|
+
my_comment.update(comment)
|
52
|
+
else
|
53
|
+
post_comment(comment)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def find_comment(comment)
|
58
|
+
@comments.each do |my_comment|
|
59
|
+
return my_comment if my_comment.corresponding?(comment)
|
60
|
+
end
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
|
64
|
+
def post_comment(comment)
|
65
|
+
path = "issues/#{id}.json"
|
66
|
+
params = {
|
67
|
+
issue: {
|
68
|
+
notes: comment.render
|
69
|
+
}
|
70
|
+
}
|
71
|
+
@redmine.api_request(path, params, :put)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class Comment < IssueComment
|
76
|
+
attr_reader :tracker, :body, :created_at
|
77
|
+
def initialize(json)
|
78
|
+
@json = json
|
79
|
+
@tracker = "Redmine"
|
80
|
+
@body = @json["notes"]
|
81
|
+
@created_at = Time.parse(@json["created_on"])
|
82
|
+
end
|
83
|
+
|
84
|
+
def update(comment)
|
85
|
+
return if updated?(comment)
|
86
|
+
# TODO: There is no API to update a comment
|
87
|
+
# https://www.redmine.org/issues/10171
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def initialize(base_url, api_key = nil)
|
92
|
+
@base_url = base_url
|
93
|
+
@api_key = api_key
|
94
|
+
end
|
95
|
+
|
96
|
+
def api_request(path, params = {}, method = :get)
|
97
|
+
url = "#{@base_url}/#{path}"
|
98
|
+
uri = URI.parse(url)
|
99
|
+
|
100
|
+
case method
|
101
|
+
when :get
|
102
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
103
|
+
when :post
|
104
|
+
req = Net::HTTP::Post.new(uri.request_uri)
|
105
|
+
when :put
|
106
|
+
req = Net::HTTP::Put.new(uri.request_uri)
|
107
|
+
end
|
108
|
+
req["Content-Type"] = "application/json"
|
109
|
+
req['X-Redmine-API-Key'] = @api_key
|
110
|
+
req.body = params.to_json
|
111
|
+
|
112
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
113
|
+
http.use_ssl = true
|
114
|
+
response = http.request(req)
|
115
|
+
if response.kind_of?(Net::HTTPSuccess)
|
116
|
+
response
|
117
|
+
else
|
118
|
+
raise "#{uri}: #{response.code} #{response.message}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def custom_fields(params = {})
|
123
|
+
response = api_request("custom_fields.json", params)
|
124
|
+
JSON.parse(response.body)["custom_fields"]
|
125
|
+
end
|
126
|
+
|
127
|
+
def custom_field_id(custom_field_name)
|
128
|
+
fields = custom_fields
|
129
|
+
field = fields.find do |field|
|
130
|
+
field["name"] == custom_field_name
|
131
|
+
end
|
132
|
+
field["id"]
|
133
|
+
end
|
134
|
+
|
135
|
+
def issues(params = {})
|
136
|
+
response = api_request("issues.json", params)
|
137
|
+
issues_json = JSON.parse(response.body)["issues"]
|
138
|
+
issues = []
|
139
|
+
issues_json.each do |issue_json|
|
140
|
+
id = issue_json["id"]
|
141
|
+
issues << issue(id)
|
142
|
+
end
|
143
|
+
issues
|
144
|
+
end
|
145
|
+
|
146
|
+
def issue(id)
|
147
|
+
params = {
|
148
|
+
include: "journals"
|
149
|
+
}
|
150
|
+
response = api_request("issues/#{id}.json", params)
|
151
|
+
issue_json = JSON.parse(response.body)["issue"]
|
152
|
+
Issue.new(self, issue_json)
|
153
|
+
end
|
154
|
+
|
155
|
+
def issues_by_custom_field(field_id, field_value, limit: nil)
|
156
|
+
search_options = {
|
157
|
+
"cf_#{field_id}".to_sym => field_value,
|
158
|
+
:status_id => '*',
|
159
|
+
:sort => 'id',
|
160
|
+
:limit => limit,
|
161
|
+
}
|
162
|
+
issues(search_options)
|
163
|
+
end
|
164
|
+
|
165
|
+
def issue_by_custom_field(field_id, field_value)
|
166
|
+
issues = issues_by_custom_field(field_id, field_value, limit: 1)
|
167
|
+
issues.empty? ? nil : issues.first
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|