plan_my_stuff 0.19.0 → 0.21.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/CHANGELOG.md +38 -0
- data/lib/plan_my_stuff/attachment.rb +26 -20
- data/lib/plan_my_stuff/attachment_uploader.rb +9 -4
- data/lib/plan_my_stuff/comment_metadata.rb +48 -4
- data/lib/plan_my_stuff/issue.rb +72 -3
- data/lib/plan_my_stuff/issue_metadata.rb +61 -13
- data/lib/plan_my_stuff/metadata_parser.rb +40 -8
- data/lib/plan_my_stuff/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0d33c652f54d9d078276d12baac63134e8bba4064185f61626c66fd924ea958a
|
|
4
|
+
data.tar.gz: 54f5c69130890fa5ae807d642cd8d89d2f0d74c4d12b638ed892d8f9b2df22d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 36d2a9a1624279d818d6c1a16fcfd51cd8e047e4f02d51044673fecb0ef3433b4a953290eb51deb868b9a8959fe365abf4dcb0645883466941047c6915c070d3
|
|
7
|
+
data.tar.gz: fbadf2e2e8337a7014e148e898291de7ef4453729332f2c8d2609942487ebec7359a97a687e44340edac1a0d7c8e36fdeddcd3cc90c8fe30a108f76f7ba74a49
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.0
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
- `PMS::Attachment` no longer accepts/stores a single `url:` attribute. Construct with explicit
|
|
8
|
+
`filename:`, `owner:`, `repo:`, `sha:`, `path:` instead; `#url` is now a derived method that returns the
|
|
9
|
+
GitHub blob viewer URL (`https://github.com/{owner}/{repo}/blob/{sha}/{path}`) rather than the
|
|
10
|
+
`raw.githubusercontent.com` URL. `#to_h` emits the structured fields and no longer includes a `url` key.
|
|
11
|
+
Persisted comment metadata in the legacy `{filename, url}` shape is migrated on read and rewritten in the
|
|
12
|
+
new shape on next save (closes #70).
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Comment bodies now render a visible `<details><summary>attachments (N)</summary>` block listing each
|
|
17
|
+
attachment as a markdown link to its blob URL, between the `pms-metadata` block and the user body. The
|
|
18
|
+
parser strips this block on read so round-trips stay clean.
|
|
19
|
+
|
|
20
|
+
## 0.20.0
|
|
21
|
+
|
|
22
|
+
### Breaking
|
|
23
|
+
|
|
24
|
+
- `priority_list` and `priority_list_priority` keys removed from `IssueMetadata` serialization. Existing issues
|
|
25
|
+
migrate to the GitHub Issue Field equivalents (`Priority List`, `Priority List Priority`) on their next save.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- `Issue#priority_list?` and `Issue#priority_list_priority` read the corresponding issue field values directly.
|
|
30
|
+
- `Issue.priority_list(repo:, state:, labels:, page:, per_page:)` convenience method delegates to
|
|
31
|
+
`Issue.list(priority_list: true, ...)`.
|
|
32
|
+
- `Issue.list` and `Issue.count` accept `priority_list:` for server-side filtering via the GitHub issue fields
|
|
33
|
+
API (passes `priority_list: false` raises `ArgumentError` -- the API has no negation qualifier).
|
|
34
|
+
|
|
35
|
+
### Deprecated
|
|
36
|
+
|
|
37
|
+
- `IssueMetadata#priority_list`, `#priority_list=`, `#priority_list?`, `#priority_list_priority`, and
|
|
38
|
+
`#priority_list_priority=` emit deprecation warnings on each read/write. Legacy writes forward to the issue
|
|
39
|
+
field on `Issue#save!` / `#update!` / `Issue.create!`. The accessors will be removed in 1.0.0.
|
|
40
|
+
|
|
3
41
|
## 0.19.0
|
|
4
42
|
|
|
5
43
|
### Added
|
|
@@ -6,33 +6,40 @@ require 'tmpdir'
|
|
|
6
6
|
module PlanMyStuff
|
|
7
7
|
# Value object representing a single attachment record on a +Comment+.
|
|
8
8
|
# Persisted in +CommentMetadata#attachments+; the gem owns the upload
|
|
9
|
-
# (see +PlanMyStuff::AttachmentUploader+) and stores
|
|
10
|
-
# +
|
|
11
|
-
# across later branch rewrites.
|
|
9
|
+
# (see +PlanMyStuff::AttachmentUploader+) and stores the structured
|
|
10
|
+
# location (+owner+/+repo+/+sha+/+path+) of the uploaded file so the
|
|
11
|
+
# blob remains addressable across later branch rewrites.
|
|
12
12
|
#
|
|
13
13
|
# Mirrors +PlanMyStuff::Link+ / +PlanMyStuff::Approval+:
|
|
14
14
|
# +ActiveModel::Attributes+-backed, with +Serializers::JSON+ for
|
|
15
15
|
# round-trip through the metadata blob.
|
|
16
16
|
#
|
|
17
17
|
class Attachment
|
|
18
|
-
RAW_URL_REGEX =
|
|
19
|
-
%r{\Ahttps://raw\.githubusercontent\.com/(?<owner>[^/\s]+)/(?<repo>[^/\s]+)/(?<sha>[^/\s]+)/(?<path>.+)\z}
|
|
20
|
-
|
|
21
18
|
include ActiveModel::Model
|
|
22
19
|
include ActiveModel::Attributes
|
|
23
20
|
include ActiveModel::Serializers::JSON
|
|
24
21
|
|
|
25
22
|
# @return [String] display filename (user-provided original name)
|
|
26
23
|
attribute :filename, :string
|
|
27
|
-
# @return [String]
|
|
28
|
-
attribute :
|
|
24
|
+
# @return [String] owner of the attachment repo (e.g. +"BrandsInsurance"+)
|
|
25
|
+
attribute :owner, :string
|
|
26
|
+
# @return [String] attachment repo name
|
|
27
|
+
attribute :repo, :string
|
|
28
|
+
# @return [String] commit SHA pinning the file
|
|
29
|
+
attribute :sha, :string
|
|
30
|
+
# @return [String] path within the attachment repo (no leading slash)
|
|
31
|
+
attribute :path, :string
|
|
32
|
+
|
|
33
|
+
validates :filename, :owner, :repo, :sha, :path, presence: true
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
# @return [String] blob-viewer URL for the pinned file
|
|
36
|
+
def url
|
|
37
|
+
"https://github.com/#{owner}/#{repo}/blob/#{sha}/#{path}"
|
|
38
|
+
end
|
|
32
39
|
|
|
33
40
|
# @return [Hash]
|
|
34
41
|
def to_h
|
|
35
|
-
{ filename: filename,
|
|
42
|
+
{ filename: filename, owner: owner, repo: repo, sha: sha, path: path }
|
|
36
43
|
end
|
|
37
44
|
|
|
38
45
|
# @param other [Object]
|
|
@@ -56,22 +63,21 @@ module PlanMyStuff
|
|
|
56
63
|
# Defaults the destination to +Dir.tmpdir+ joined with +File.basename(filename)+ (directory components
|
|
57
64
|
# stripped to prevent path traversal).
|
|
58
65
|
#
|
|
59
|
-
# @param
|
|
66
|
+
# @param dest [String, nil] destination path; defaults to +File.join(Dir.tmpdir, File.basename(filename))+
|
|
60
67
|
#
|
|
61
68
|
# @return [String] the path the bytes were written to
|
|
62
69
|
#
|
|
63
|
-
def download_to(
|
|
64
|
-
|
|
65
|
-
match = RAW_URL_REGEX.match(url)
|
|
70
|
+
def download_to(dest = nil)
|
|
71
|
+
dest ||= File.join(Dir.tmpdir, File.basename(filename.to_s))
|
|
66
72
|
body = PlanMyStuff.client.rest(
|
|
67
73
|
:contents,
|
|
68
|
-
"#{
|
|
69
|
-
path:
|
|
70
|
-
ref:
|
|
74
|
+
"#{owner}/#{repo}",
|
|
75
|
+
path: path,
|
|
76
|
+
ref: sha,
|
|
71
77
|
accept: 'application/vnd.github.raw',
|
|
72
78
|
)
|
|
73
|
-
File.binwrite(
|
|
74
|
-
|
|
79
|
+
File.binwrite(dest, body)
|
|
80
|
+
dest
|
|
75
81
|
end
|
|
76
82
|
end
|
|
77
83
|
end
|
|
@@ -6,8 +6,8 @@ require 'securerandom'
|
|
|
6
6
|
|
|
7
7
|
module PlanMyStuff
|
|
8
8
|
# Uploads attachment files to a shared attachment repo (+config.attachment_repo+ under +config.organization+) using
|
|
9
|
-
# GitHub's Git Data API, returning
|
|
10
|
-
# +
|
|
9
|
+
# GitHub's Git Data API, returning +PlanMyStuff::Attachment+ instances populated with the structured
|
|
10
|
+
# +owner+/+repo+/+sha+/+path+ location of each uploaded file.
|
|
11
11
|
#
|
|
12
12
|
# One commit per batch (atomic): all files in a single +upload_all!+ call land in the same tree/commit so partial
|
|
13
13
|
# failures cannot leave +config.main_branch+ in an inconsistent state.
|
|
@@ -202,8 +202,13 @@ module PlanMyStuff
|
|
|
202
202
|
# @return [PlanMyStuff::Attachment]
|
|
203
203
|
#
|
|
204
204
|
def build_attachment(commit_sha:, slot:)
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
PlanMyStuff::Attachment.new(
|
|
206
|
+
filename: slot[:filename],
|
|
207
|
+
owner: PlanMyStuff.configuration.organization,
|
|
208
|
+
repo: PlanMyStuff.configuration.attachment_repo,
|
|
209
|
+
sha: commit_sha,
|
|
210
|
+
path: slot[:path],
|
|
211
|
+
)
|
|
207
212
|
end
|
|
208
213
|
|
|
209
214
|
# @return [String] +"<organization>/<attachment_repo>"+
|
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module PlanMyStuff
|
|
4
4
|
class CommentMetadata < PlanMyStuff::BaseMetadata
|
|
5
|
+
LEGACY_URL_REGEXES = [
|
|
6
|
+
%r{\Ahttps://raw\.githubusercontent\.com/(?<owner>[^/\s]+)/(?<repo>[^/\s]+)/(?<sha>[^/\s]+)/(?<path>.+)\z},
|
|
7
|
+
%r{\Ahttps://github\.com/(?<owner>[^/\s]+)/(?<repo>[^/\s]+)/blob/(?<sha>[^/\s]+)/(?<path>.+)\z},
|
|
8
|
+
].freeze
|
|
9
|
+
|
|
10
|
+
LEGACY_ATTACHMENT_DEPRECATION_MESSAGE =
|
|
11
|
+
'PlanMyStuff: legacy attachment metadata shape {filename, url} detected. It will continue to parse ' \
|
|
12
|
+
'until 1.0.0, at which point legacy detection will be removed. New writes use the structured ' \
|
|
13
|
+
'{filename, owner, repo, sha, path} shape introduced in #70; existing entries migrate on their ' \
|
|
14
|
+
'next write.'
|
|
15
|
+
|
|
5
16
|
# @return [Boolean] true if this comment holds the issue's body content
|
|
6
17
|
attr_accessor :issue_body
|
|
7
18
|
# @return [Array<PlanMyStuff::Attachment>] consuming-app attachment records associated with this comment
|
|
@@ -82,9 +93,14 @@ module PlanMyStuff
|
|
|
82
93
|
private
|
|
83
94
|
|
|
84
95
|
# Builds a +PlanMyStuff::Attachment+ from each parsed entry.
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
96
|
+
# Accepts:
|
|
97
|
+
# - an existing +PlanMyStuff::Attachment+
|
|
98
|
+
# - the new shape +{filename:, owner:, repo:, sha:, path:}+
|
|
99
|
+
# - the legacy shape +{filename:, url:}+ where +url+ is a raw
|
|
100
|
+
# +raw.githubusercontent.com+ permalink or a +github.com/.../blob/...+
|
|
101
|
+
# blob-viewer URL; parsed once into structured fields so the
|
|
102
|
+
# next write drops the +url+ key (migrate-on-write).
|
|
103
|
+
#
|
|
88
104
|
# Malformed hash entries are silently dropped so a single bad
|
|
89
105
|
# historical entry doesn't crash +Comment.find+. An already-
|
|
90
106
|
# constructed +Attachment+ that fails validation raises so the
|
|
@@ -102,7 +118,7 @@ module PlanMyStuff
|
|
|
102
118
|
if entry.is_a?(PlanMyStuff::Attachment)
|
|
103
119
|
entry
|
|
104
120
|
elsif entry.respond_to?(:transform_keys)
|
|
105
|
-
PlanMyStuff::Attachment.new(entry.transform_keys(&:to_sym))
|
|
121
|
+
PlanMyStuff::Attachment.new(structured_attrs(entry.transform_keys(&:to_sym)))
|
|
106
122
|
end
|
|
107
123
|
next if attachment.nil?
|
|
108
124
|
|
|
@@ -114,5 +130,33 @@ module PlanMyStuff
|
|
|
114
130
|
next
|
|
115
131
|
end
|
|
116
132
|
end
|
|
133
|
+
|
|
134
|
+
# Returns +attrs+ untouched when it already carries structured
|
|
135
|
+
# fields, otherwise parses a legacy +url+ into
|
|
136
|
+
# +owner+/+repo+/+sha+/+path+. Unparseable URLs yield a hash
|
|
137
|
+
# with no structured fields, leaving +Attachment+ validation to
|
|
138
|
+
# drop the entry.
|
|
139
|
+
#
|
|
140
|
+
# @param attrs [Hash{Symbol=>Object}]
|
|
141
|
+
#
|
|
142
|
+
# @return [Hash{Symbol=>Object}]
|
|
143
|
+
#
|
|
144
|
+
def structured_attrs(attrs)
|
|
145
|
+
return attrs if attrs.key?(:owner)
|
|
146
|
+
|
|
147
|
+
PlanMyStuff.deprecator.warn(LEGACY_ATTACHMENT_DEPRECATION_MESSAGE)
|
|
148
|
+
|
|
149
|
+
url = attrs[:url].to_s
|
|
150
|
+
match = LEGACY_URL_REGEXES.lazy.filter_map { |re| re.match(url) }.first
|
|
151
|
+
|
|
152
|
+
return attrs.except(:url) if match.nil?
|
|
153
|
+
|
|
154
|
+
attrs.except(:url).merge(
|
|
155
|
+
owner: match[:owner],
|
|
156
|
+
repo: match[:repo],
|
|
157
|
+
sha: match[:sha],
|
|
158
|
+
path: match[:path],
|
|
159
|
+
)
|
|
160
|
+
end
|
|
117
161
|
end
|
|
118
162
|
end
|
data/lib/plan_my_stuff/issue.rb
CHANGED
|
@@ -296,26 +296,46 @@ module PlanMyStuff
|
|
|
296
296
|
|
|
297
297
|
# Lists GitHub issues with optional filters and pagination.
|
|
298
298
|
#
|
|
299
|
+
# @raise [ArgumentError] when +priority_list: false+ is passed
|
|
300
|
+
#
|
|
299
301
|
# @param repo [Symbol, String, nil] defaults to config.default_repo
|
|
300
302
|
# @param state [Symbol] :open, :closed, or :all
|
|
301
303
|
# @param labels [Array<String>]
|
|
304
|
+
# @param priority_list [Boolean, nil] when +true+, restricts to issues whose +Priority List+ issue field is
|
|
305
|
+
# +Yes+ (server-side filter via the +issue_field_values+ query param). +false+ raises +ArgumentError+ -- GitHub
|
|
306
|
+
# has no negation qualifier. Silently dropped when +config.issue_fields_enabled+ is +false+.
|
|
302
307
|
# @param page [Integer]
|
|
303
308
|
# @param per_page [Integer]
|
|
304
309
|
#
|
|
305
310
|
# @return [Array<PlanMyStuff::Issue>]
|
|
306
311
|
#
|
|
307
|
-
def list(repo: nil, state: :open, labels: [], page: 1, per_page: 25)
|
|
312
|
+
def list(repo: nil, state: :open, labels: [], priority_list: nil, page: 1, per_page: 25)
|
|
313
|
+
if priority_list == false
|
|
314
|
+
raise(ArgumentError, 'priority_list: false is not supported (no GitHub negation qualifier)')
|
|
315
|
+
end
|
|
316
|
+
|
|
308
317
|
client = PlanMyStuff.client
|
|
309
318
|
resolved_repo = client.resolve_repo!(repo)
|
|
310
319
|
|
|
311
320
|
params = { state: state.to_s, page: page, per_page: per_page }
|
|
312
321
|
params[:labels] = labels.sort.join(',') if labels.present?
|
|
322
|
+
if priority_list && PlanMyStuff.configuration.issue_fields_enabled
|
|
323
|
+
params[:issue_field_values] = 'priority-list:Yes'
|
|
324
|
+
end
|
|
313
325
|
|
|
314
326
|
github_issues = client.rest(:list_issues, resolved_repo, **params)
|
|
315
327
|
filtered = github_issues.reject { |gi| gi.respond_to?(:pull_request) && gi.pull_request }
|
|
316
328
|
filtered.map { |gi| build(gi, repo: resolved_repo) }
|
|
317
329
|
end
|
|
318
330
|
|
|
331
|
+
# Convenience shortcut for +list(priority_list: true, ...)+. See +.list+ for parameter semantics.
|
|
332
|
+
#
|
|
333
|
+
# @return [Array<PlanMyStuff::Issue>]
|
|
334
|
+
#
|
|
335
|
+
def priority_list(**)
|
|
336
|
+
list(**, priority_list: true)
|
|
337
|
+
end
|
|
338
|
+
|
|
319
339
|
# Counts GitHub issues matching the given filters without paginating full payloads.
|
|
320
340
|
#
|
|
321
341
|
# Uses GitHub's Search API (+search/issues+), which returns +total_count+ in a single
|
|
@@ -327,13 +347,23 @@ module PlanMyStuff
|
|
|
327
347
|
# - The Search API has its own rate limit (30 req/min authenticated) separate from
|
|
328
348
|
# the core REST API.
|
|
329
349
|
#
|
|
350
|
+
# @raise [ArgumentError] when +priority_list: false+ is passed
|
|
351
|
+
#
|
|
330
352
|
# @param repo [Symbol, String, nil] defaults to config.default_repo
|
|
331
353
|
# @param state [Symbol] :open, :closed, or :all
|
|
332
354
|
# @param labels [Array<String>]
|
|
355
|
+
# @param priority_list [Boolean, nil] when +true+, restricts to issues whose +Priority List+ issue field is
|
|
356
|
+
# +Yes+ (server-side filter via the +field.priority-list:Yes+ Search qualifier). +false+ raises
|
|
357
|
+
# +ArgumentError+ -- GitHub has no negation qualifier. Silently dropped when
|
|
358
|
+
# +config.issue_fields_enabled+ is +false+.
|
|
333
359
|
#
|
|
334
360
|
# @return [Integer]
|
|
335
361
|
#
|
|
336
|
-
def count(repo: nil, state: :open, labels: [])
|
|
362
|
+
def count(repo: nil, state: :open, labels: [], priority_list: nil)
|
|
363
|
+
if priority_list == false
|
|
364
|
+
raise(ArgumentError, 'priority_list: false is not supported (no GitHub negation qualifier)')
|
|
365
|
+
end
|
|
366
|
+
|
|
337
367
|
client = PlanMyStuff.client
|
|
338
368
|
resolved_repo = client.resolve_repo!(repo)
|
|
339
369
|
|
|
@@ -344,7 +374,12 @@ module PlanMyStuff
|
|
|
344
374
|
qualifiers += labels_to_use.map do |label|
|
|
345
375
|
"label:\"#{label}\""
|
|
346
376
|
end
|
|
347
|
-
|
|
377
|
+
search_options = { per_page: 1 }
|
|
378
|
+
if priority_list && PlanMyStuff.configuration.issue_fields_enabled
|
|
379
|
+
qualifiers << 'field.priority-list:Yes'
|
|
380
|
+
search_options[:advanced_search] = true
|
|
381
|
+
end
|
|
382
|
+
client.rest(:search_issues, qualifiers.join(' '), **search_options).total_count
|
|
348
383
|
end
|
|
349
384
|
|
|
350
385
|
# Submits one or more pre-built payloads to GitHub's "Import Issues" preview endpoint
|
|
@@ -663,6 +698,8 @@ module PlanMyStuff
|
|
|
663
698
|
# @return [self]
|
|
664
699
|
#
|
|
665
700
|
def save!(user: nil, skip_notification: false)
|
|
701
|
+
forward_legacy_priority_list_metadata!
|
|
702
|
+
|
|
666
703
|
if new_record?
|
|
667
704
|
created = self.class.create!(
|
|
668
705
|
title: title,
|
|
@@ -795,6 +832,16 @@ module PlanMyStuff
|
|
|
795
832
|
@issue_fields ||= load_issue_fields!
|
|
796
833
|
end
|
|
797
834
|
|
|
835
|
+
# @return [Boolean]
|
|
836
|
+
def priority_list?
|
|
837
|
+
issue_fields['Priority List'] == 'Yes'
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
# @return [Integer, nil]
|
|
841
|
+
def priority_list_priority
|
|
842
|
+
issue_fields['Priority List Priority']
|
|
843
|
+
end
|
|
844
|
+
|
|
798
845
|
# Bulk-updates GitHub Issue Field values in a single +setIssueFieldValue+ mutation. Each key is the field display
|
|
799
846
|
# name; values are coerced to the right input fragment based on the field's type. Passing +nil+ as a value clears
|
|
800
847
|
# that field.
|
|
@@ -934,6 +981,28 @@ module PlanMyStuff
|
|
|
934
981
|
reload
|
|
935
982
|
end
|
|
936
983
|
|
|
984
|
+
# Forwards a pending legacy +metadata.priority_list+ /
|
|
985
|
+
# +#priority_list_priority+ write into +@pending_issue_fields+ so the next
|
|
986
|
+
# +save!+ persists the values into the +Priority List+ /
|
|
987
|
+
# +Priority List Priority+ GitHub Issue Fields. Caller-supplied
|
|
988
|
+
# +issue_fields:+ entries win on key collision. Silently skipped when
|
|
989
|
+
# +config.issue_fields_enabled+ is +false+.
|
|
990
|
+
#
|
|
991
|
+
# @return [void]
|
|
992
|
+
#
|
|
993
|
+
def forward_legacy_priority_list_metadata!
|
|
994
|
+
return unless PlanMyStuff.configuration.issue_fields_enabled
|
|
995
|
+
return unless metadata.instance_variable_get(:@legacy_priority_list_pending)
|
|
996
|
+
|
|
997
|
+
legacy_pl = metadata.instance_variable_get(:@priority_list)
|
|
998
|
+
legacy_plp = metadata.instance_variable_get(:@priority_list_priority)
|
|
999
|
+
|
|
1000
|
+
legacy_fields = { 'Priority List' => legacy_pl ? 'Yes' : nil }
|
|
1001
|
+
legacy_fields['Priority List Priority'] = legacy_plp unless legacy_plp == -1
|
|
1002
|
+
|
|
1003
|
+
@pending_issue_fields = legacy_fields.merge(@pending_issue_fields || {})
|
|
1004
|
+
end
|
|
1005
|
+
|
|
937
1006
|
# Applies in-memory updates from an +update!+ kwargs hash. Top-level scalars go through their setters so
|
|
938
1007
|
# +@body_dirty+ and friends stay in sync; +metadata:+ is merged into +@metadata+ (top-level attrs assigned
|
|
939
1008
|
# directly, custom_fields merged key-by-key).
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module PlanMyStuff
|
|
4
4
|
class IssueMetadata < PlanMyStuff::BaseMetadata
|
|
5
|
+
PRIORITY_LIST_METADATA_DEPRECATION =
|
|
6
|
+
'PlanMyStuff: IssueMetadata#priority_list / #priority_list_priority are deprecated. priority_list moved to ' \
|
|
7
|
+
"GitHub Issue Fields ('Priority List' single_select, 'Priority List Priority' number) in 0.20.0. Reads " \
|
|
8
|
+
'now come from issue_fields; legacy writes forward to set_issue_fields! on save. The metadata accessors ' \
|
|
9
|
+
'will be removed in 1.0.0.'
|
|
10
|
+
|
|
5
11
|
# @return [Time, nil] first support action timestamp, nil until set
|
|
6
12
|
attr_accessor :responded_at
|
|
7
13
|
# @return [String, nil] user-facing URL in the consuming app
|
|
8
14
|
attr_accessor :issues_url
|
|
9
|
-
# @return [Boolean] whether this issue appears on the priority dashboard
|
|
10
|
-
attr_accessor :priority_list
|
|
11
|
-
# @return [Integer] sort order on priority dashboard (-1 = unranked)
|
|
12
|
-
attr_accessor :priority_list_priority
|
|
13
15
|
# @return [Array<Integer>] user IDs of non-support users allowed to view internal comments
|
|
14
16
|
attr_accessor :visibility_allowlist
|
|
15
17
|
# @return [String, nil] merged PR commit SHA for release tracking
|
|
@@ -50,8 +52,14 @@ module PlanMyStuff
|
|
|
50
52
|
|
|
51
53
|
metadata.responded_at = parse_time(hash[:responded_at])
|
|
52
54
|
metadata.issues_url = hash[:issues_url]
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
if hash.key?(:priority_list) || hash.key?(:priority_list_priority)
|
|
56
|
+
PlanMyStuff.deprecator.warn(PRIORITY_LIST_METADATA_DEPRECATION)
|
|
57
|
+
metadata.instance_variable_set(:@priority_list, hash[:priority_list]) if hash.key?(:priority_list)
|
|
58
|
+
if hash.key?(:priority_list_priority)
|
|
59
|
+
metadata.instance_variable_set(:@priority_list_priority, hash[:priority_list_priority])
|
|
60
|
+
end
|
|
61
|
+
metadata.instance_variable_set(:@legacy_priority_list_pending, true)
|
|
62
|
+
end
|
|
55
63
|
metadata.visibility_allowlist = Array.wrap(hash[:visibility_allowlist])
|
|
56
64
|
metadata.commit_sha = hash[:commit_sha]
|
|
57
65
|
metadata.auto_complete = hash.fetch(:auto_complete, true)
|
|
@@ -86,8 +94,6 @@ module PlanMyStuff
|
|
|
86
94
|
|
|
87
95
|
metadata.responded_at = nil
|
|
88
96
|
metadata.issues_url = build_issues_url(PlanMyStuff.configuration)
|
|
89
|
-
metadata.priority_list = false
|
|
90
|
-
metadata.priority_list_priority = -1
|
|
91
97
|
metadata.visibility_allowlist = []
|
|
92
98
|
metadata.commit_sha = nil
|
|
93
99
|
metadata.auto_complete = true
|
|
@@ -169,8 +175,6 @@ module PlanMyStuff
|
|
|
169
175
|
|
|
170
176
|
def initialize
|
|
171
177
|
super
|
|
172
|
-
@priority_list = false
|
|
173
|
-
@priority_list_priority = -1
|
|
174
178
|
@visibility_allowlist = []
|
|
175
179
|
@auto_complete = true
|
|
176
180
|
@links = []
|
|
@@ -188,9 +192,55 @@ module PlanMyStuff
|
|
|
188
192
|
!!auto_complete
|
|
189
193
|
end
|
|
190
194
|
|
|
195
|
+
# @deprecated Use +Issue#priority_list?+. Removed in 1.0.0.
|
|
196
|
+
#
|
|
197
|
+
# @return [Object, nil]
|
|
198
|
+
#
|
|
199
|
+
def priority_list
|
|
200
|
+
PlanMyStuff.deprecator.warn(PRIORITY_LIST_METADATA_DEPRECATION)
|
|
201
|
+
@priority_list
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# @deprecated Use +Issue#update!(issue_fields: { 'Priority List' => 'Yes' })+. Removed in 1.0.0.
|
|
205
|
+
#
|
|
206
|
+
# @param value [Object]
|
|
207
|
+
#
|
|
208
|
+
# @return [Object]
|
|
209
|
+
#
|
|
210
|
+
def priority_list=(value)
|
|
211
|
+
PlanMyStuff.deprecator.warn(PRIORITY_LIST_METADATA_DEPRECATION)
|
|
212
|
+
@legacy_priority_list_pending = true
|
|
213
|
+
@priority_list = value
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# @deprecated Use +Issue#priority_list?+. Removed in 1.0.0.
|
|
217
|
+
#
|
|
191
218
|
# @return [Boolean]
|
|
219
|
+
#
|
|
192
220
|
def priority_list?
|
|
193
|
-
|
|
221
|
+
PlanMyStuff.deprecator.warn(PRIORITY_LIST_METADATA_DEPRECATION)
|
|
222
|
+
!!@priority_list
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# @deprecated Use +Issue#priority_list_priority+. Removed in 1.0.0.
|
|
226
|
+
#
|
|
227
|
+
# @return [Object, nil]
|
|
228
|
+
#
|
|
229
|
+
def priority_list_priority
|
|
230
|
+
PlanMyStuff.deprecator.warn(PRIORITY_LIST_METADATA_DEPRECATION)
|
|
231
|
+
@priority_list_priority
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# @deprecated Use +Issue#update!(issue_fields: { 'Priority List Priority' => N })+. Removed in 1.0.0.
|
|
235
|
+
#
|
|
236
|
+
# @param value [Object]
|
|
237
|
+
#
|
|
238
|
+
# @return [Object]
|
|
239
|
+
#
|
|
240
|
+
def priority_list_priority=(value)
|
|
241
|
+
PlanMyStuff.deprecator.warn(PRIORITY_LIST_METADATA_DEPRECATION)
|
|
242
|
+
@legacy_priority_list_pending = true
|
|
243
|
+
@priority_list_priority = value
|
|
194
244
|
end
|
|
195
245
|
|
|
196
246
|
# @return [Boolean]
|
|
@@ -220,8 +270,6 @@ module PlanMyStuff
|
|
|
220
270
|
super.merge(
|
|
221
271
|
responded_at: PlanMyStuff.format_time(responded_at),
|
|
222
272
|
issues_url: issues_url,
|
|
223
|
-
priority_list: priority_list,
|
|
224
|
-
priority_list_priority: priority_list_priority,
|
|
225
273
|
visibility_allowlist: visibility_allowlist,
|
|
226
274
|
commit_sha: commit_sha,
|
|
227
275
|
auto_complete: auto_complete,
|
|
@@ -18,6 +18,15 @@ module PlanMyStuff
|
|
|
18
18
|
</details>\n*
|
|
19
19
|
}mx
|
|
20
20
|
|
|
21
|
+
# Visible attachments block emitted after the metadata block when the
|
|
22
|
+
# metadata carries non-empty +attachments+ (issue #70). Parse strips it
|
|
23
|
+
# so round-trips stay clean.
|
|
24
|
+
ATTACHMENTS_PATTERN = %r{
|
|
25
|
+
\A<details><summary>attachments\ \(\d+\)</summary>\n\n
|
|
26
|
+
.*?\n\n
|
|
27
|
+
</details>\n*
|
|
28
|
+
}mx
|
|
29
|
+
|
|
21
30
|
# Legacy format kept for parsing only - existing issues serialized before 0.17.0
|
|
22
31
|
# used a hidden HTML comment. They migrate to the new format on the next write.
|
|
23
32
|
LEGACY_METADATA_PATTERN = /\A<!-- pms-metadata:(.*?) -->\n*/m
|
|
@@ -40,7 +49,7 @@ module PlanMyStuff
|
|
|
40
49
|
|
|
41
50
|
match = raw_body.match(pattern)
|
|
42
51
|
metadata = JSON.parse(match[1], symbolize_names: true)
|
|
43
|
-
body = raw_body.sub(pattern, '')
|
|
52
|
+
body = raw_body.sub(pattern, '').sub(ATTACHMENTS_PATTERN, '')
|
|
44
53
|
|
|
45
54
|
{ metadata: metadata, body: body }
|
|
46
55
|
rescue JSON::ParserError
|
|
@@ -61,14 +70,37 @@ module PlanMyStuff
|
|
|
61
70
|
raise(ArgumentError, "metadata must be a Hash or PlanMyStuff::CustomFields, got #{metadata.class}")
|
|
62
71
|
end
|
|
63
72
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
hash = metadata.is_a?(PlanMyStuff::CustomFields) ? metadata.to_h : metadata
|
|
74
|
+
json = JSON.pretty_generate(hash)
|
|
75
|
+
|
|
76
|
+
"<details><summary>pms-metadata</summary>\n\n```json\n#{json}\n```\n\n</details>\n\n" \
|
|
77
|
+
"#{attachments_block(hash)}#{body}"
|
|
78
|
+
end
|
|
70
79
|
|
|
71
|
-
|
|
80
|
+
# Renders the visible attachments block when +metadata+ carries
|
|
81
|
+
# non-empty +:attachments+, otherwise returns an empty string.
|
|
82
|
+
#
|
|
83
|
+
# @param metadata [Hash]
|
|
84
|
+
#
|
|
85
|
+
# @return [String]
|
|
86
|
+
#
|
|
87
|
+
def attachments_block(metadata)
|
|
88
|
+
attachments = metadata[:attachments]
|
|
89
|
+
return '' if attachments.blank?
|
|
90
|
+
|
|
91
|
+
lines = attachments.map { |a| attachment_line(a) }.join("\n")
|
|
92
|
+
"<details><summary>attachments (#{attachments.size})</summary>\n\n#{lines}\n\n</details>\n\n"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @param attachment [Hash{Symbol=>String}]
|
|
96
|
+
#
|
|
97
|
+
# @return [String]
|
|
98
|
+
#
|
|
99
|
+
def attachment_line(attachment)
|
|
100
|
+
url = "https://github.com/#{attachment[:owner]}/#{attachment[:repo]}" \
|
|
101
|
+
"/blob/#{attachment[:sha]}/#{attachment[:path]}"
|
|
102
|
+
safe_filename = attachment[:filename].to_s.gsub(']', '\]')
|
|
103
|
+
"- [#{safe_filename}](#{url})"
|
|
72
104
|
end
|
|
73
105
|
|
|
74
106
|
# @param raw_body [String]
|