plan_my_stuff 0.20.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 +17 -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/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,22 @@
|
|
|
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
|
+
|
|
3
20
|
## 0.20.0
|
|
4
21
|
|
|
5
22
|
### Breaking
|
|
@@ -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
|
|
@@ -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]
|