issue-db 1.1.0 → 1.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/README.md +34 -6
- data/issue-db.gemspec +1 -1
- data/lib/issue_db/authentication.rb +36 -28
- data/lib/issue_db/cache.rb +27 -25
- data/lib/issue_db/database.rb +191 -189
- data/lib/issue_db/models/record.rb +25 -23
- data/lib/issue_db/models/repository.rb +15 -13
- data/lib/issue_db/utils/generate.rb +38 -36
- data/lib/issue_db/utils/github.rb +282 -280
- data/lib/issue_db/utils/init.rb +19 -17
- data/lib/issue_db/utils/parse.rb +33 -31
- data/lib/issue_db.rb +59 -47
- data/lib/version.rb +4 -2
- metadata +1 -1
data/lib/issue_db/database.rb
CHANGED
@@ -4,218 +4,220 @@ require_relative "cache"
|
|
4
4
|
require_relative "models/record"
|
5
5
|
require_relative "utils/generate"
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
7
|
+
module IssueDB
|
8
|
+
class RecordNotFound < StandardError; end
|
9
|
+
|
10
|
+
class Database
|
11
|
+
include Cache
|
12
|
+
include Generate
|
13
|
+
|
14
|
+
# :param: log [Logger] a logger object to use for logging
|
15
|
+
# :param: client [Octokit::Client] an Octokit::Client object to use for interacting with the GitHub API
|
16
|
+
# :param: repo [Repository] a Repository object that represents the GitHub repository to use as the datastore
|
17
|
+
# :param: label [String] the label to use for issues managed in the datastore by this library
|
18
|
+
# :param: cache_expiry [Integer] the number of seconds to cache issues in memory (default: 60)
|
19
|
+
# :return: A new Database object
|
20
|
+
def initialize(log, client, repo, label, cache_expiry)
|
21
|
+
@log = log
|
22
|
+
@client = client
|
23
|
+
@repo = repo
|
24
|
+
@label = label
|
25
|
+
@cache_expiry = cache_expiry
|
26
|
+
@issues = nil
|
27
|
+
@issues_last_updated = nil
|
28
|
+
end
|
28
29
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
30
|
+
# Create a new issue/record in the database
|
31
|
+
# This will return the newly created issue as a Record object (parsed)
|
32
|
+
# :param: key [String] the key (issue title) to create
|
33
|
+
# :param: data [Hash] the data to use for the issue body
|
34
|
+
# :param: options [Hash] a hash of options containing extra data such as body_before and body_after
|
35
|
+
# :return: The newly created issue as a Record object
|
36
|
+
# usage example:
|
37
|
+
# data = { color: "blue", cool: true, popularity: 100, tags: ["tag1", "tag2"] }
|
38
|
+
# options = { body_before: "some text before the data", body_after: "some text after the data", include_closed: true }
|
39
|
+
# db.create("event123", {cool: true, data: "here"}, options)
|
40
|
+
def create(key, data, options = {})
|
41
|
+
@log.debug("attempting to create: #{key}")
|
42
|
+
issue = find_issue_by_key(key, options, create_mode: true)
|
43
|
+
if issue
|
44
|
+
@log.warn("skipping issue creation and returning existing issue - an issue already exists with the key: #{key}")
|
45
|
+
return Record.new(issue)
|
46
|
+
end
|
47
|
+
|
48
|
+
# if we make it here, no existing issues were found so we can safely create one
|
49
|
+
|
50
|
+
body = generate(data, body_before: options[:body_before], body_after: options[:body_after])
|
51
|
+
|
52
|
+
# if we make it here, no existing issues were found so we can safely create one
|
53
|
+
issue = @client.create_issue(@repo.full_name, key, body, { labels: @label })
|
54
|
+
|
55
|
+
# ensure the cache is initialized before appending and handle race conditions
|
56
|
+
current_issues = issues
|
57
|
+
if current_issues && !current_issues.include?(issue)
|
58
|
+
@issues << issue
|
59
|
+
end
|
60
|
+
|
61
|
+
@log.debug("issue created: #{key}")
|
44
62
|
return Record.new(issue)
|
45
63
|
end
|
46
64
|
|
47
|
-
#
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
#
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
@issues << issue
|
65
|
+
# Read an issue/record from the database
|
66
|
+
# This will return the issue as a Record object (parsed)
|
67
|
+
# :param: key [String] the key (issue title) to read
|
68
|
+
# :param: options [Hash] a hash of options to pass through to the search method
|
69
|
+
# :return: The issue as a Record object
|
70
|
+
def read(key, options = {})
|
71
|
+
@log.debug("attempting to read: #{key}")
|
72
|
+
issue = find_issue_by_key(key, options)
|
73
|
+
@log.debug("issue found: #{key}")
|
74
|
+
return Record.new(issue)
|
58
75
|
end
|
59
76
|
|
60
|
-
|
61
|
-
return Record
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
body = generate(data, body_before: options[:body_before], body_after: options[:body_after])
|
91
|
-
|
92
|
-
updated_issue = @client.update_issue(@repo.full_name, issue.number, key, body)
|
93
|
-
|
94
|
-
# update the issue in the cache using the reference we have
|
95
|
-
index = @issues.index(issue)
|
96
|
-
if index
|
97
|
-
@issues[index] = updated_issue
|
98
|
-
else
|
99
|
-
@log.warn("issue not found in cache during update: #{key}")
|
100
|
-
# Force a cache refresh to ensure consistency
|
101
|
-
update_issue_cache!
|
77
|
+
# Update an issue/record in the database
|
78
|
+
# This will return the updated issue as a Record object (parsed)
|
79
|
+
# :param: key [String] the key (issue title) to update
|
80
|
+
# :param: data [Hash] the data to use for the issue body
|
81
|
+
# :param: options [Hash] a hash of options containing extra data such as body_before and body_after
|
82
|
+
# :return: The updated issue as a Record object
|
83
|
+
# usage example:
|
84
|
+
# data = { color: "blue", cool: true, popularity: 100, tags: ["tag1", "tag2"] }
|
85
|
+
# options = { body_before: "some text before the data", body_after: "some text after the data", include_closed: true }
|
86
|
+
# db.update("event123", {cool: true, data: "here"}, options)
|
87
|
+
def update(key, data, options = {})
|
88
|
+
@log.debug("attempting to update: #{key}")
|
89
|
+
issue = find_issue_by_key(key, options)
|
90
|
+
|
91
|
+
body = generate(data, body_before: options[:body_before], body_after: options[:body_after])
|
92
|
+
|
93
|
+
updated_issue = @client.update_issue(@repo.full_name, issue.number, key, body)
|
94
|
+
|
95
|
+
# update the issue in the cache using the reference we have
|
96
|
+
index = @issues.index(issue)
|
97
|
+
if index
|
98
|
+
@issues[index] = updated_issue
|
99
|
+
else
|
100
|
+
@log.warn("issue not found in cache during update: #{key}")
|
101
|
+
# Force a cache refresh to ensure consistency
|
102
|
+
update_issue_cache!
|
103
|
+
end
|
104
|
+
|
105
|
+
@log.debug("issue updated: #{key}")
|
106
|
+
return Record.new(updated_issue)
|
102
107
|
end
|
103
108
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
#
|
125
|
-
|
109
|
+
# Delete an issue/record from the database - in this context, "delete" means to close the issue as "completed"
|
110
|
+
# :param: key [String] the key (issue title) to delete
|
111
|
+
# :param: options [Hash] a hash of options to pass through to the search method
|
112
|
+
# :return: The deleted issue as a Record object (parsed) - it may contain useful data
|
113
|
+
def delete(key, options = {})
|
114
|
+
@log.debug("attempting to delete: #{key}")
|
115
|
+
issue = find_issue_by_key(key, options)
|
116
|
+
|
117
|
+
deleted_issue = @client.close_issue(@repo.full_name, issue.number)
|
118
|
+
|
119
|
+
# update the issue in the cache using the reference we have
|
120
|
+
index = @issues.index(issue)
|
121
|
+
if index
|
122
|
+
@issues[index] = deleted_issue
|
123
|
+
else
|
124
|
+
@log.warn("issue not found in cache during delete: #{key}")
|
125
|
+
# Force a cache refresh to ensure consistency
|
126
|
+
update_issue_cache!
|
127
|
+
end
|
128
|
+
|
129
|
+
# return the deleted issue as a Record object as it may contain useful data
|
130
|
+
return Record.new(deleted_issue)
|
126
131
|
end
|
127
132
|
|
128
|
-
#
|
129
|
-
return
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
issue[:title]
|
133
|
+
# List all keys in the database
|
134
|
+
# This will return an array of strings that represent the issue titles that are "keys" in the database
|
135
|
+
# :param: options [Hash] a hash of options to pass through to the search method
|
136
|
+
# :return: An array of strings that represent the issue titles that are "keys" in the database
|
137
|
+
# usage example:
|
138
|
+
# options = {include_closed: true}
|
139
|
+
# keys = db.list_keys(options)
|
140
|
+
def list_keys(options = {})
|
141
|
+
current_issues = issues
|
142
|
+
return [] if current_issues.nil?
|
143
|
+
|
144
|
+
keys = current_issues.select do |issue|
|
145
|
+
options[:include_closed] || issue[:state] == "open"
|
146
|
+
end.map do |issue|
|
147
|
+
issue[:title]
|
148
|
+
end
|
149
|
+
|
150
|
+
return keys
|
147
151
|
end
|
148
152
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
153
|
+
# List all issues/record in the database as Record objects (parsed)
|
154
|
+
# This will return an array of Record objects that represent the issues in the database
|
155
|
+
# :param: options [Hash] a hash of options to pass through to the search method
|
156
|
+
# :return: An array of Record objects that represent the issues in the database
|
157
|
+
# usage example:
|
158
|
+
# options = {include_closed: true}
|
159
|
+
# records = db.list(options)
|
160
|
+
def list(options = {})
|
161
|
+
current_issues = issues
|
162
|
+
return [] if current_issues.nil?
|
163
|
+
|
164
|
+
records = current_issues.select do |issue|
|
165
|
+
options[:include_closed] || issue[:state] == "open"
|
166
|
+
end.map do |issue|
|
167
|
+
Record.new(issue)
|
168
|
+
end
|
169
|
+
|
170
|
+
return records
|
167
171
|
end
|
168
172
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
def refresh!
|
176
|
-
update_issue_cache!
|
177
|
-
end
|
173
|
+
# Force a refresh of the issues cache
|
174
|
+
# This will update the issues cache with the latest issues from the repo
|
175
|
+
# :return: The updated issue cache as a list of issues (Hash objects not parsed)
|
176
|
+
def refresh!
|
177
|
+
update_issue_cache!
|
178
|
+
end
|
178
179
|
|
179
180
|
protected
|
180
181
|
|
181
|
-
|
182
|
-
|
183
|
-
end
|
184
|
-
|
185
|
-
# A helper method to search through the issues cache and return the first issue that matches the given key
|
186
|
-
# :param: key [String] the key (issue title) to search for
|
187
|
-
# :param: options [Hash] a hash of options to pass through to the search method
|
188
|
-
# :param: create_mode [Boolean] a flag to indicate whether or not we are in create mode
|
189
|
-
# :return: A direct reference to the issue as a Hash object if found, otherwise throws a RecordNotFound error
|
190
|
-
# ... unless create_mode is true, in which case it returns nil as a signal to proceed with creating the issue
|
191
|
-
def find_issue_by_key(key, options = {}, create_mode: false)
|
192
|
-
issue = issues.find do |issue|
|
193
|
-
issue[:title] == key && (options[:include_closed] || issue[:state] == "open")
|
182
|
+
def not_found!(key)
|
183
|
+
raise RecordNotFound, "no record found for key: #{key}"
|
194
184
|
end
|
195
185
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
186
|
+
# A helper method to search through the issues cache and return the first issue that matches the given key
|
187
|
+
# :param: key [String] the key (issue title) to search for
|
188
|
+
# :param: options [Hash] a hash of options to pass through to the search method
|
189
|
+
# :param: create_mode [Boolean] a flag to indicate whether or not we are in create mode
|
190
|
+
# :return: A direct reference to the issue as a Hash object if found, otherwise throws a RecordNotFound error
|
191
|
+
# ... unless create_mode is true, in which case it returns nil as a signal to proceed with creating the issue
|
192
|
+
def find_issue_by_key(key, options = {}, create_mode: false)
|
193
|
+
issue = issues.find do |issue|
|
194
|
+
issue[:title] == key && (options[:include_closed] || issue[:state] == "open")
|
195
|
+
end
|
196
|
+
|
197
|
+
if issue.nil?
|
198
|
+
@log.debug("no issue found in cache for: #{key}")
|
199
|
+
return nil if create_mode
|
200
|
+
|
201
|
+
not_found!(key)
|
202
|
+
end
|
203
|
+
|
204
|
+
@log.debug("issue found in cache for: #{key}")
|
205
|
+
return issue
|
201
206
|
end
|
202
207
|
|
203
|
-
|
204
|
-
|
205
|
-
|
208
|
+
# A helper method to fetch all issues from the repo and update the issues cache
|
209
|
+
# It is cache aware
|
210
|
+
def issues
|
211
|
+
# update the issues cache if it is nil
|
212
|
+
update_issue_cache! if @issues.nil?
|
206
213
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
214
|
+
# update the cache if it has expired (with nil safety)
|
215
|
+
if !@issues_last_updated.nil? && (Time.now - @issues_last_updated) > @cache_expiry
|
216
|
+
@log.debug("issue cache expired - last updated: #{@issues_last_updated} - refreshing now")
|
217
|
+
update_issue_cache!
|
218
|
+
end
|
212
219
|
|
213
|
-
|
214
|
-
if !@issues_last_updated.nil? && (Time.now - @issues_last_updated) > @cache_expiry
|
215
|
-
@log.debug("issue cache expired - last updated: #{@issues_last_updated} - refreshing now")
|
216
|
-
update_issue_cache!
|
220
|
+
return @issues
|
217
221
|
end
|
218
|
-
|
219
|
-
return @issues
|
220
222
|
end
|
221
223
|
end
|
@@ -2,34 +2,36 @@
|
|
2
2
|
|
3
3
|
require_relative "../utils/parse"
|
4
4
|
|
5
|
-
|
5
|
+
module IssueDB
|
6
|
+
class IssueParseError < StandardError; end
|
6
7
|
|
7
|
-
class Record
|
8
|
-
|
8
|
+
class Record
|
9
|
+
include Parse
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
11
|
+
attr_reader :body_before, :data, :body_after, :source_data, :key
|
12
|
+
def initialize(data)
|
13
|
+
@key = data.title
|
14
|
+
@source_data = data
|
15
|
+
parse!
|
16
|
+
end
|
16
17
|
|
17
18
|
protected
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
20
|
+
def parse!
|
21
|
+
if @source_data.body.nil? || @source_data.body.strip == ""
|
22
|
+
raise IssueParseError, "issue body is empty for issue number #{@source_data.number}"
|
23
|
+
end
|
24
|
+
|
25
|
+
begin
|
26
|
+
parsed = parse(@source_data.body)
|
27
|
+
rescue JSON::ParserError => e
|
28
|
+
message = "failed to parse issue body data contents for issue number: #{@source_data.number} - #{e.message}"
|
29
|
+
raise IssueParseError, message
|
30
|
+
end
|
31
|
+
|
32
|
+
@body_before = parsed[:body_before]
|
33
|
+
@data = parsed[:data]
|
34
|
+
@body_after = parsed[:body_after]
|
22
35
|
end
|
23
|
-
|
24
|
-
begin
|
25
|
-
parsed = parse(@source_data.body)
|
26
|
-
rescue JSON::ParserError => e
|
27
|
-
message = "failed to parse issue body data contents for issue number: #{@source_data.number} - #{e.message}"
|
28
|
-
raise IssueParseError, message
|
29
|
-
end
|
30
|
-
|
31
|
-
@body_before = parsed[:body_before]
|
32
|
-
@data = parsed[:data]
|
33
|
-
@body_after = parsed[:body_after]
|
34
36
|
end
|
35
37
|
end
|
@@ -1,22 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
module IssueDB
|
4
|
+
class RepoFormatError < StandardError; end
|
4
5
|
|
5
|
-
class Repository
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
class Repository
|
7
|
+
attr_reader :owner, :repo, :full_name
|
8
|
+
def initialize(repo)
|
9
|
+
@repo = repo
|
10
|
+
validate!
|
11
|
+
end
|
11
12
|
|
12
13
|
protected
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
def validate!
|
16
|
+
if @repo.nil? || !@repo.include?("/")
|
17
|
+
raise RepoFormatError, "repository #{@repo} is invalid - valid format: <owner>/<repo>"
|
18
|
+
end
|
18
19
|
|
19
|
-
|
20
|
-
|
20
|
+
@full_name = @repo.strip
|
21
|
+
@owner, @repo = @full_name.split("/")
|
22
|
+
end
|
21
23
|
end
|
22
24
|
end
|
@@ -1,45 +1,47 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
module IssueDB
|
4
|
+
class GenerateError < StandardError; end
|
4
5
|
|
5
|
-
module Generate
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
6
|
+
module Generate
|
7
|
+
# Generates the issue body with embedded data
|
8
|
+
# :param data [Hash] the data to embed in the issue body
|
9
|
+
# :param body_before [String] the body of the issue before the data (optional)
|
10
|
+
# :param body_after [String] the body of the issue after the data (optional)
|
11
|
+
# :param guard_start [String] the guard start string which is used to identify the start of the data
|
12
|
+
# :param guard_end [String] the guard end string which is used to identify the end of the data
|
13
|
+
# :return [String] the issue body with the embedded data
|
14
|
+
def generate(
|
15
|
+
data,
|
16
|
+
body_before: nil,
|
17
|
+
body_after: nil,
|
18
|
+
guard_start: "<!--- issue-db-start -->",
|
19
|
+
guard_end: "<!--- issue-db-end -->"
|
20
|
+
)
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
22
|
+
# json formatting options
|
23
|
+
opts = {
|
24
|
+
indent: " ",
|
25
|
+
space: " ",
|
26
|
+
object_nl: "\n",
|
27
|
+
array_nl: "\n",
|
28
|
+
allow_nan: true,
|
29
|
+
max_nesting: false
|
30
|
+
}
|
30
31
|
|
31
|
-
|
32
|
+
json_data = JSON.pretty_generate(data, opts)
|
32
33
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
34
|
+
# construct the body
|
35
|
+
body = ""
|
36
|
+
body += "#{body_before}\n" unless body_before.nil? # the first part of the body
|
37
|
+
body += "#{guard_start}\n" # the start of the data
|
38
|
+
body += "```json\n" # the start of the json codeblock
|
39
|
+
body += "#{json_data}\n" # the data
|
40
|
+
body += "```\n" # the end of the json codeblock
|
41
|
+
body += "#{guard_end}\n" # the end of the data
|
42
|
+
body += body_after unless body_after.nil? # the last part of the body
|
42
43
|
|
43
|
-
|
44
|
+
return body
|
45
|
+
end
|
44
46
|
end
|
45
47
|
end
|