mine-shipper 0.0.1
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 +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
|