pray-cli 1.21.0 → 1.23.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/lib/pray/cli/commands/distribution.rb +34 -3
  4. data/lib/pray/cli/commands/packages.rb +9 -7
  5. data/lib/pray/cli/commands/update_latest.rb +11 -4
  6. data/lib/pray/cli/commands/workflow.rb +4 -2
  7. data/lib/pray/cli/help.rb +7 -0
  8. data/lib/pray/cli.rb +14 -0
  9. data/lib/pray/constraint.rb +1 -1
  10. data/lib/pray/derived_metadata.rb +194 -0
  11. data/lib/pray/destination.rb +28 -8
  12. data/lib/pray/git_cache.rb +46 -19
  13. data/lib/pray/git_legacy.rb +36 -0
  14. data/lib/pray/git_materialize.rb +204 -38
  15. data/lib/pray/git_store.rb +101 -14
  16. data/lib/pray/literal.rb +4 -5
  17. data/lib/pray/lockfile_serialize.rb +6 -1
  18. data/lib/pray/manifest_json.rb +3 -27
  19. data/lib/pray/manifest_parser.rb +25 -3
  20. data/lib/pray/manifest_parser_blocks.rb +7 -7
  21. data/lib/pray/package_spec.rb +6 -1
  22. data/lib/pray/package_spec_paths.rb +18 -0
  23. data/lib/pray/publish.rb +50 -6
  24. data/lib/pray/registry.rb +10 -7
  25. data/lib/pray/render.rb +26 -32
  26. data/lib/pray/render_content.rb +4 -4
  27. data/lib/pray/render_project.rb +22 -0
  28. data/lib/pray/resolve.rb +17 -23
  29. data/lib/pray/resolve_local.rb +1 -0
  30. data/lib/pray/serve.rb +15 -1
  31. data/lib/pray/serve_federation.rb +3 -0
  32. data/lib/pray/source_path.rb +35 -0
  33. data/lib/pray/statement_surface.rb +3 -1
  34. data/lib/pray/sync.rb +1 -4
  35. data/lib/pray/tar_validation.rb +2 -0
  36. data/lib/pray/terminal.rb +28 -0
  37. data/lib/pray/trust.rb +4 -3
  38. data/lib/pray/upstream_merge.rb +6 -2
  39. data/lib/pray/upstream_plan.rb +84 -0
  40. data/lib/pray/version.rb +2 -2
  41. data/lib/pray.rb +4 -0
  42. metadata +8 -2
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "path_safety"
4
+
5
+ module Pray
6
+ class PackageSpec
7
+ def validate_relative_paths!
8
+ files.each { |file| PathSafety.validate_archive_member_path!(file) }
9
+ exports.each_value do |export|
10
+ PathSafety.validate_archive_member_path!(export.path)
11
+ PathSafety.validate_archive_member_path!(export.default_path) if export.default_path
12
+ end
13
+ skills.each_value { |skill| PathSafety.validate_archive_member_path!(skill.path) }
14
+ templates.each_value { |template| PathSafety.validate_archive_member_path!(template.path) }
15
+ self
16
+ end
17
+ end
18
+ end
data/lib/pray/publish.rb CHANGED
@@ -4,6 +4,7 @@ require "json"
4
4
  require "fileutils"
5
5
  require "time"
6
6
  require "tmpdir"
7
+ require_relative "terminal"
7
8
 
8
9
  module Pray
9
10
  RegistryIndex = Struct.new(:spec, :packages) do
@@ -15,11 +16,14 @@ module Pray
15
16
  module Publish
16
17
  module_function
17
18
 
18
- def publish_to_root(project, root, signer: "local", signer_fingerprint: nil)
19
+ def publish_to_root(project, root, signer: "local", signer_fingerprint: nil, dest_name: nil, target: nil)
19
20
  root = File.expand_path(root)
21
+ dest_name ||= root
22
+ target ||= root
20
23
  index = load_registry_index(root)
21
24
  distribution = Distribution.read_settings(root)
22
25
  package_names = index.packages.to_set
26
+ stats = {published: 0, unchanged: 0}
23
27
 
24
28
  project.packages.each do |package|
25
29
  package.spec.require_release_version!
@@ -30,7 +34,9 @@ module Pray
30
34
  stored_artifact = stored_package_artifact(root, artifact_path, package, existing, distribution)
31
35
  if stored_artifact && stored_publish_matches?(stored_artifact, package, existing)
32
36
  package_names << package.declaration.name
33
- write_registry_package_metadata(metadata_path, metadata)
37
+ # Intent: unchanged packages keep on-disk catalog bytes (newline, key order).
38
+ report_unchanged(package, dest_name, target)
39
+ stats[:unchanged] += 1
34
40
  next
35
41
  end
36
42
 
@@ -56,14 +62,20 @@ module Pray
56
62
  metadata.versions << version_entry
57
63
  write_registry_package_metadata(metadata_path, metadata)
58
64
  package_names << package.declaration.name
65
+ report_published(package, dest_name, target, artifact_path)
66
+ stats[:published] += 1
59
67
  end
60
68
 
61
69
  index.packages = package_names.sort
62
70
  write_registry_index(root, index)
71
+ stats
63
72
  end
64
73
 
65
- def publish_to_server(project, server_url, signer: "local", signer_fingerprint: nil)
74
+ def publish_to_server(project, server_url, signer: "local", signer_fingerprint: nil, dest_name: nil, target: nil)
75
+ dest_name ||= server_url
76
+ target ||= server_url
66
77
  distribution = Distribution.fetch_settings(server_url)
78
+ stats = {published: 0, unchanged: 0}
67
79
  project.packages.each do |package|
68
80
  archive_bytes = Archive.build_package_archive_bytes(package)
69
81
  artifact_path = registry_artifact_path(package.declaration.name, package.spec.version)
@@ -99,9 +111,38 @@ module Pray
99
111
  "application/json",
100
112
  JSON.pretty_generate(metadata_to_hash(metadata))
101
113
  )
114
+ report_published(package, dest_name, target, artifact_path)
115
+ stats[:published] += 1
102
116
  end
117
+ stats
103
118
  end
104
119
 
120
+ def report_published(package, dest_name, target, artifact_path)
121
+ Terminal.println_stdout(
122
+ "published #{package.declaration.name} #{package.spec.version} -> #{redact_publish_target(dest_name)} (#{redact_publish_target(target)})"
123
+ )
124
+ Terminal.println_verbose(" artifact #{artifact_path}")
125
+ end
126
+
127
+ def report_unchanged(package, dest_name, target)
128
+ Terminal.println_stdout(
129
+ "unchanged #{package.declaration.name} #{package.spec.version} -> #{redact_publish_target(dest_name)} (#{redact_publish_target(target)})"
130
+ )
131
+ Terminal.println_verbose(" skip reason: stored publish matches")
132
+ end
133
+
134
+ def redact_publish_target(target)
135
+ text = target.to_s
136
+ return text unless text.include?("://")
137
+
138
+ scheme, remainder = text.split("://", 2)
139
+ return text unless remainder&.include?("@")
140
+
141
+ _userinfo, host_and_path = remainder.split("@", 2)
142
+ "#{scheme}://***@#{host_and_path}"
143
+ end
144
+ private_class_method :report_published, :report_unchanged
145
+
105
146
  def published_registry_package_version(package, signer, signer_fingerprint, archive_bytes, artifact_path)
106
147
  RegistryPackageVersion.new(
107
148
  version: package.spec.version,
@@ -114,7 +155,8 @@ module Pray
114
155
  signer: signer,
115
156
  signer_fingerprint: signer_fingerprint,
116
157
  published_at: Time.now.to_i,
117
- signature: Registry.registry_artifact_signature(archive_bytes, package.tree_hash, signer)
158
+ signature: Registry.registry_artifact_signature(archive_bytes, package.tree_hash, signer),
159
+ derived_metadata: DerivedMetadata.from_package_root(package.root, package.spec)
118
160
  )
119
161
  end
120
162
 
@@ -163,6 +205,7 @@ module Pray
163
205
 
164
206
  version_entry.yanked = existing.yanked
165
207
  version_entry.published_at = existing.published_at if stored_artifact
208
+ version_entry.derived_metadata ||= existing.derived_metadata
166
209
  end
167
210
 
168
211
  def load_registry_index(root)
@@ -181,7 +224,7 @@ module Pray
181
224
 
182
225
  def load_registry_package_metadata(path, package_name)
183
226
  if File.exist?(path)
184
- data = JSON.parse(File.read(path))
227
+ data = JSON.parse(File.read(path, encoding: "UTF-8"))
185
228
  RegistryPackageMetadata.new(
186
229
  name: data["name"],
187
230
  versions: Array(data["versions"]).map { |entry| Registry.version_from_hash(entry) }
@@ -193,7 +236,7 @@ module Pray
193
236
 
194
237
  def write_registry_package_metadata(path, metadata)
195
238
  FileUtils.mkdir_p(File.dirname(path))
196
- File.write(path, JSON.pretty_generate(metadata_to_hash(metadata)))
239
+ File.write(path, JSON.pretty_generate(metadata_to_hash(metadata)), encoding: "UTF-8")
197
240
  end
198
241
 
199
242
  def metadata_to_hash(metadata)
@@ -217,6 +260,7 @@ module Pray
217
260
  hash["signer_fingerprint"] = entry.signer_fingerprint if entry.signer_fingerprint
218
261
  hash["published_at"] = entry.published_at if entry.published_at
219
262
  hash["signature"] = entry.signature if entry.signature
263
+ hash["derived_metadata"] = entry.derived_metadata if entry.derived_metadata
220
264
  hash
221
265
  end
222
266
 
data/lib/pray/registry.rb CHANGED
@@ -15,15 +15,17 @@ require_relative "git_materialize"
15
15
  module Pray
16
16
  RegistryPackageVersion = Struct.new(
17
17
  :version, :artifact, :artifact_hash, :tree_hash, :yanked, :targets, :exports,
18
- :signer, :signer_fingerprint, :published_at, :signature
18
+ :signer, :signer_fingerprint, :published_at, :signature, :derived_metadata
19
19
  ) do
20
20
  def initialize(
21
21
  artifact_hash: nil, tree_hash: nil, yanked: false, targets: [], exports: [],
22
- signer: nil, signer_fingerprint: nil, published_at: nil, signature: nil, **kwargs
22
+ signer: nil, signer_fingerprint: nil, published_at: nil, signature: nil,
23
+ derived_metadata: nil, **kwargs
23
24
  )
24
25
  super(**kwargs, artifact_hash: artifact_hash, tree_hash: tree_hash, yanked: yanked,
25
26
  targets: targets, exports: exports, signer: signer,
26
- signer_fingerprint: signer_fingerprint, published_at: published_at, signature: signature)
27
+ signer_fingerprint: signer_fingerprint, published_at: published_at,
28
+ signature: signature, derived_metadata: derived_metadata)
27
29
  end
28
30
  end
29
31
 
@@ -118,7 +120,7 @@ module Pray
118
120
 
119
121
  raise Error.resolution(offline_package_error(declaration.name, selected.version)) if offline
120
122
 
121
- artifact_bytes = read_local_artifact_bytes(source_root, selected.artifact)
123
+ artifact_bytes = read_local_artifact_bytes(source_root, selected.artifact, offline: offline)
122
124
  RegistryInstall.install_artifact_to_cache(
123
125
  cache_directory, declaration, selected, artifact_bytes, source_url: source_root
124
126
  )
@@ -159,7 +161,8 @@ module Pray
159
161
  signer: entry["signer"],
160
162
  signer_fingerprint: entry["signer_fingerprint"],
161
163
  published_at: entry.key?("published_at") ? publish_timestamp_from_value(entry["published_at"]) : nil,
162
- signature: entry["signature"]
164
+ signature: entry["signature"],
165
+ derived_metadata: entry["derived_metadata"]
163
166
  )
164
167
  end
165
168
 
@@ -206,8 +209,8 @@ module Pray
206
209
  Gem::Version.new(left) <=> Gem::Version.new(right)
207
210
  end
208
211
 
209
- def read_local_artifact_bytes(source_root, artifact)
210
- GitMaterialize.read_local_artifact_bytes(source_root, artifact)
212
+ def read_local_artifact_bytes(source_root, artifact, offline: false)
213
+ GitMaterialize.read_local_artifact_bytes(source_root, artifact, offline: offline)
211
214
  end
212
215
 
213
216
  def read_artifact_bytes(source_url, artifact)
data/lib/pray/render.rb CHANGED
@@ -4,6 +4,7 @@ require "fileutils"
4
4
  require "pathname"
5
5
  require_relative "render_content"
6
6
  require_relative "render_managed"
7
+ require_relative "render_project"
7
8
 
8
9
  module Pray
9
10
  RenderedTarget = Struct.new(:path, :content, :managed_spans) do
@@ -15,16 +16,6 @@ module Pray
15
16
  module Render
16
17
  module_function
17
18
 
18
- def render_project(project)
19
- rendered = []
20
- project.manifest.targets.each do |target|
21
- output = target.outputs.first
22
- next unless output
23
- rendered << render_target(project, target, output)
24
- end
25
- rendered
26
- end
27
-
28
19
  def write_rendered_targets(project, rendered, previous_lockfile = nil)
29
20
  Transaction.run(project.project_root) { RenderDest.write_rendered_targets(project, rendered, previous_lockfile) }
30
21
  end
@@ -56,7 +47,18 @@ module Pray
56
47
  end
57
48
  end
58
49
  end
59
- planned.sort_by(&:path).uniq(&:path)
50
+ planned.sort_by! { |file| [file.path, file.package, file.export] }
51
+ planned.each_cons(2) do |left, right|
52
+ next unless left.path == right.path
53
+ next if left.package == right.package && left.export == right.export
54
+
55
+ raise Error.render(
56
+ "provisioned destination `#{left.path}` has multiple owners: " \
57
+ "package `#{left.package}` export `#{left.export}` and " \
58
+ "package `#{right.package}` export `#{right.export}`"
59
+ )
60
+ end
61
+ planned.uniq(&:path)
60
62
  end
61
63
 
62
64
  PlannedProvisionedFile = Struct.new(:path, :source, :package, :export, keyword_init: true)
@@ -74,12 +76,23 @@ module Pray
74
76
  append_header(builder, project, target, output)
75
77
  symbols = project.manifest.symbols || {}
76
78
  managed_spans = []
79
+ locals_by_path = project.local_files.to_h { |local| [local.manifest_path, local] }
80
+ packages_by_name = project.packages.to_h { |package| [package.declaration.name, package] }
77
81
 
78
82
  (target.entries || []).each do |entry|
79
83
  if entry.kind == "local"
80
- append_scoped_local(builder, managed_spans, project, target, output, entry.path, symbols)
84
+ local = locals_by_path[entry.path]
85
+ append_managed_local(builder, managed_spans, local, target, output, symbols) if local
81
86
  else
82
- append_scoped_package(builder, managed_spans, project, target, output, entry.name, symbols)
87
+ package = packages_by_name[entry.name]
88
+ next unless package
89
+ next unless Environment.package_matches_environment?(package.declaration.groups, project.environment)
90
+
91
+ package.selected_exports.each do |export|
92
+ next unless should_inline_export?(package, export)
93
+
94
+ append_managed_export(builder, managed_spans, package, export, target, output, symbols)
95
+ end
83
96
  end
84
97
  end
85
98
 
@@ -133,25 +146,6 @@ module Pray
133
146
  builder.append_empty_line
134
147
  end
135
148
 
136
- def append_scoped_local(builder, managed_spans, project, target, output, path, symbols)
137
- local = project.local_files.find { |candidate| candidate.manifest_path == path }
138
- return unless local
139
-
140
- append_managed_local(builder, managed_spans, local, target, output, symbols)
141
- end
142
-
143
- def append_scoped_package(builder, managed_spans, project, target, output, package_name, symbols)
144
- package = project.packages.find { |candidate| candidate.declaration.name == package_name }
145
- return unless package
146
- return unless Environment.package_matches_environment?(package.declaration.groups, project.environment)
147
-
148
- package.selected_exports.each do |export|
149
- next unless should_inline_export?(package, export)
150
-
151
- append_managed_export(builder, managed_spans, package, export, target, output, symbols)
152
- end
153
- end
154
-
155
149
  def should_inline_export?(package, export_name)
156
150
  export = package.spec.exports[export_name]
157
151
  export.nil? || %w[fragment file].include?(export.kind)
@@ -3,16 +3,16 @@
3
3
  module Pray
4
4
  module Render
5
5
  class ContentBuilder
6
+ attr_reader :next_line_number
7
+
6
8
  def initialize
7
9
  @content = +""
8
- end
9
-
10
- def next_line_number
11
- @content.count("\n") + 1
10
+ @next_line_number = 1
12
11
  end
13
12
 
14
13
  def append_line(line)
15
14
  @content << line << "\n"
15
+ @next_line_number += 1
16
16
  end
17
17
 
18
18
  def append_empty_line
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module Render
5
+ module_function
6
+
7
+ def render_project(project)
8
+ project.manifest.targets.flat_map do |target|
9
+ target.outputs.map do |output|
10
+ rendered = render_target(project, target, output)
11
+ if target.max_bytes && rendered.content.bytesize > target.max_bytes
12
+ raise Error.render(
13
+ "target `#{target.name}` output `#{output}` is #{rendered.content.bytesize} bytes; " \
14
+ "max_bytes is #{target.max_bytes}"
15
+ )
16
+ end
17
+ rendered
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
data/lib/pray/resolve.rb CHANGED
@@ -154,8 +154,8 @@ module Pray
154
154
  project_root, sources, git_sources, user_config, declaration, lockfile, options
155
155
  )
156
156
  spec_path = find_prayspec_file(root)
157
- spec_text = File.read(spec_path)
158
- spec = Pray.parse_package_spec(spec_text).canonicalized
157
+ spec_text = SourcePath.read(root, File.basename(spec_path), "package specification").force_encoding(Encoding::UTF_8)
158
+ spec = Pray.parse_package_spec(spec_text)
159
159
  if spec.name != declaration.name
160
160
  raise Error.resolution(
161
161
  "package path #{root.inspect} declares #{spec.name.inspect}, expected #{declaration.name.inspect}"
@@ -195,9 +195,11 @@ module Pray
195
195
  offline = options.offline
196
196
  preferred_version = options.preferred_lock_version(lockfile, declaration.name)
197
197
  if (local_path = user_config.local.package[declaration.name])
198
- return [File.expand_path(local_path, project_root), nil]
198
+ return [SourcePath.declared_directory(project_root, local_path, "package directory"), nil]
199
+ end
200
+ if declaration.path
201
+ return [SourcePath.declared_directory(project_root, declaration.path, "package directory"), nil]
199
202
  end
200
- return [File.expand_path(declaration.path, project_root), nil] if declaration.path
201
203
  return [ResolveTarball.package_root(project_root, declaration.tarball, offline: offline), nil] if declaration.tarball
202
204
  source_name = ResolveSource.implied_source_name(declaration, sources)
203
205
  if source_name
@@ -220,7 +222,8 @@ module Pray
220
222
  case source.kind
221
223
  when "path"
222
224
  directory = LocalPrayer.path_source_package_directory(source_name, declaration.name)
223
- return [File.join(project_root, source.url, directory), nil]
225
+ relative = File.join(source.url, directory)
226
+ return [SourcePath.declared_directory(project_root, relative, "package directory"), nil]
224
227
  when "registry", "static index"
225
228
  resolved = Registry.resolve_registry_package_root(
226
229
  project_root,
@@ -264,7 +267,8 @@ module Pray
264
267
 
265
268
  raise Error.unsupported("remote sources are not implemented yet") if declaration.git || declaration.oci
266
269
 
267
- [File.join(project_root, declaration.name.tr("/", "-")), nil]
270
+ slug = declaration.name.tr("/", "-")
271
+ [SourcePath.declared_directory(project_root, slug, "package directory"), nil]
268
272
  end
269
273
 
270
274
  def resolve_package_root(project_root, sources, git_sources, user_config, declaration, lockfile = nil)
@@ -272,9 +276,12 @@ module Pray
272
276
  end
273
277
 
274
278
  def find_prayspec_file(root)
275
- files = Dir.children(root).filter_map do |entry|
279
+ files = []
280
+ Dir.each_child(root) do |entry|
281
+ next unless File.extname(entry) == ".prayspec"
282
+
276
283
  path = File.join(root, entry)
277
- (File.file?(path) && File.extname(entry) == ".prayspec") ? path : nil
284
+ files << path if File.file?(path)
278
285
  end
279
286
  case files.length
280
287
  when 1 then files.first
@@ -296,15 +303,7 @@ module Pray
296
303
  end
297
304
 
298
305
  def load_package_file_bytes(root, spec)
299
- file_bytes = {}
300
- spec.files.each do |file|
301
- path = File.join(root, file)
302
- raise Error.integrity("package file missing: #{file}") unless File.exist?(path)
303
- raise Error.integrity("package file is a directory: #{file}") if File.directory?(path)
304
-
305
- file_bytes[file] = File.binread(path)
306
- end
307
- file_bytes
306
+ spec.files.to_h { |file| [file, SourcePath.read(root, file, "package file")] }
308
307
  end
309
308
 
310
309
  def build_skill_file_index(spec)
@@ -331,12 +330,7 @@ module Pray
331
330
  end
332
331
 
333
332
  def skill_relative_file(file, skill_prefix)
334
- return nil unless file.start_with?(skill_prefix)
335
-
336
- relative = file.delete_prefix(skill_prefix).delete_prefix("/")
337
- return nil if relative.empty? || file == skill_prefix
338
-
339
- relative
333
+ file.delete_prefix("#{skill_prefix.delete_suffix("/")}/") if file.start_with?("#{skill_prefix.delete_suffix("/")}/")
340
334
  end
341
335
  end
342
336
  end
@@ -11,6 +11,7 @@ module Pray
11
11
 
12
12
  def resolve_local_file(project_root, declaration)
13
13
  path = File.join(project_root, declaration.path)
14
+ SourcePath.reject_symlinks!(project_root, declaration.path, "local source")
14
15
  unless File.exist?(path)
15
16
  if declaration.optional
16
17
  return ResolvedLocalFile.new(
data/lib/pray/serve.rb CHANGED
@@ -118,7 +118,7 @@ module Pray
118
118
  return html_response("<h1>Pray distribution</h1>")
119
119
  end
120
120
 
121
- file_path = PathSafety.join_under_root(root, path.delete_prefix("/"))
121
+ file_path = public_distribution_path(root, path)
122
122
  return not_found unless file_path
123
123
  return not_found unless File.file?(file_path)
124
124
  return payload_too_large if File.size(file_path) > ResourceLimits::MAX_HTTP_RESPONSE_BYTES
@@ -129,6 +129,20 @@ module Pray
129
129
  ranged_response(status, content_type, ranged_body, content_range)
130
130
  end
131
131
 
132
+ def public_distribution_path(root, request_path)
133
+ relative = PathSafety.sanitize_relative_path(request_path)
134
+ exact = %w[v1/index.json v1/distribution.json v1/trust.json]
135
+ return nil unless exact.include?(relative) || relative.start_with?("v1/packages/", "v1/artifacts/")
136
+
137
+ file_path = PathSafety.join_under_root(root, relative)
138
+ return nil unless file_path
139
+
140
+ SourcePath.reject_symlinks!(root, relative, "distribution file")
141
+ file_path
142
+ rescue Error
143
+ nil
144
+ end
145
+
132
146
  def content_type_for(path)
133
147
  case File.extname(path)
134
148
  when ".json" then "application/json"
@@ -51,6 +51,7 @@ module Pray
51
51
 
52
52
  def package_response(root, path)
53
53
  name = path.delete_prefix("/v1/sync/package/")
54
+ PathSafety.validate_registry_cache_identity!(name, "metadata")
54
55
  metadata_path = Publish.registry_metadata_path(root, name)
55
56
  return Serve.not_found unless File.file?(metadata_path)
56
57
 
@@ -61,6 +62,8 @@ module Pray
61
62
  "versions" => metadata.versions.map { |version| transport_version(version) }
62
63
  }
63
64
  Serve.ok_response("application/json", JSON.pretty_generate(body))
65
+ rescue Error
66
+ Serve.not_found
64
67
  end
65
68
 
66
69
  def append_confession(root, body)
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module Pray
6
+ module SourcePath
7
+ module_function
8
+
9
+ def declared_directory(root, relative, label)
10
+ reject_symlinks!(root, relative, label)
11
+ File.expand_path(relative, root)
12
+ end
13
+
14
+ def reject_symlinks!(root, relative, label)
15
+ current = Pathname(relative).absolute? ? File::SEPARATOR : root
16
+ status = nil
17
+ Pathname(relative).each_filename do |component|
18
+ current = File.join(current, component)
19
+ status = File.lstat(current)
20
+ raise Error.integrity("#{label} uses a symbolic link: #{relative}") if status.symlink?
21
+ rescue Errno::ENOENT
22
+ return nil
23
+ end
24
+ status
25
+ end
26
+
27
+ def read(root, relative, label)
28
+ status = reject_symlinks!(root, relative, label)
29
+ raise Error.integrity("#{label} missing: #{relative}") unless status
30
+ raise Error.integrity("#{label} is a directory: #{relative}") if status.directory?
31
+
32
+ File.binread(File.join(root, relative))
33
+ end
34
+ end
35
+ end
@@ -7,6 +7,7 @@ module Pray
7
7
  def expand_statement_surface(statement)
8
8
  trimmed = statement.strip
9
9
  return [] if trimmed.empty?
10
+ return [normalize_keyword_call(trimmed)] unless trimmed.include?(";") || trimmed.include?("{")
10
11
 
11
12
  Literal.split_top_level(trimmed, ";").flat_map { |segment| expand_one_surface(segment) }
12
13
  end
@@ -15,7 +16,7 @@ module Pray
15
16
  trimmed = statement.strip
16
17
  return [] if trimmed.empty?
17
18
 
18
- braced = expand_brace_block(trimmed)
19
+ braced = expand_brace_block(trimmed) if trimmed.include?("{")
19
20
  return braced if braced
20
21
 
21
22
  [normalize_keyword_call(trimmed)]
@@ -71,6 +72,7 @@ module Pray
71
72
  def normalize_keyword_call(statement)
72
73
  spaced = normalize_spaced_block_opener(statement)
73
74
  return spaced if spaced
75
+ return statement unless statement.include?("(")
74
76
 
75
77
  keyword = leading_identifier(statement)
76
78
  return statement unless keyword
data/lib/pray/sync.rb CHANGED
@@ -205,10 +205,7 @@ module Pray
205
205
  end
206
206
 
207
207
  def fetch_bytes(peer_source, artifact)
208
- if artifact.start_with?("http://", "https://")
209
- return Registry.http_get(artifact).b
210
- end
211
-
208
+ Registry.reject_absolute_artifact!(artifact)
212
209
  Registry.http_get("#{trim_slash(peer_source)}/#{artifact.delete_prefix("/")}").b
213
210
  end
214
211
 
@@ -38,6 +38,8 @@ module Pray
38
38
 
39
39
  def process_entry(header, data, size, state, output_directory)
40
40
  type = header.byteslice(156, 1)
41
+ # git archive emits a pax global header (`g`); ignore without applying path.
42
+ return if type == "g"
41
43
  if type == "x"
42
44
  state.pending_path = pax_path(data)
43
45
  return
data/lib/pray/terminal.rb CHANGED
@@ -20,6 +20,34 @@ module Pray
20
20
  env_truthy?("PRAY_NO_INPUT")
21
21
  end
22
22
 
23
+ def quiet?
24
+ env_truthy?("PRAY_QUIET")
25
+ end
26
+
27
+ def verbose?
28
+ level = ENV.fetch("PRAY_VERBOSE", "0").to_i
29
+ level > 0
30
+ end
31
+
32
+ def configure_output!(quiet:, verbose:)
33
+ ENV["PRAY_QUIET"] = "1" if quiet
34
+ if verbose > 0
35
+ ENV["PRAY_VERBOSE"] = verbose.to_s
36
+ end
37
+ end
38
+
39
+ def println_stdout(message)
40
+ return if quiet?
41
+
42
+ puts message
43
+ end
44
+
45
+ def println_verbose(message)
46
+ return if quiet? || !verbose?
47
+
48
+ puts message
49
+ end
50
+
23
51
  def env_truthy?(name)
24
52
  value = ENV.fetch(name, nil)
25
53
  return false if value.nil?
data/lib/pray/trust.rb CHANGED
@@ -110,10 +110,11 @@ module Pray
110
110
  end
111
111
 
112
112
  def prepare_source_host_keys(sources)
113
- policy = load_policy_or_default
114
- sources.each_with_object({}) do |source, host_keys|
115
- next unless source.kind == "pray_ssh"
113
+ ssh_sources = sources.select { |source| source.kind == "pray_ssh" }
114
+ return {} if ssh_sources.empty?
116
115
 
116
+ policy = load_policy_or_default
117
+ ssh_sources.each_with_object({}) do |source, host_keys|
117
118
  rule = best_rule(policy, source.url)
118
119
  fingerprint = rule.allowed_host_keys.first
119
120
  host_keys[source.name] = fingerprint if fingerprint
@@ -39,13 +39,15 @@ module Pray
39
39
  when [false, true, false]
40
40
  (local == old) ? :omit : :conflict
41
41
  when [false, false, true]
42
- (old == new) ? :omit : new
42
+ (old == new) ? :omit : :conflict
43
43
  when [true, false, true]
44
44
  new
45
45
  when [true, true, false]
46
46
  local
47
47
  when [true, false, false]
48
48
  (local == new) ? new : :conflict
49
+ when [false, true, true]
50
+ :omit
49
51
  else
50
52
  :conflict
51
53
  end
@@ -67,7 +69,9 @@ module Pray
67
69
 
68
70
  def merge_conflict_message(fork, upstream_name, old_version, new_version, paths)
69
71
  "upstream merge conflict in #{fork} while refreshing #{upstream_name} " \
70
- "#{old_version} to #{new_version}: #{paths.join(", ")}"
72
+ "#{old_version} to #{new_version}: #{paths.join(", ")}. " \
73
+ "Reset those content files to the locked upstream tree, run `pray update --latest`, " \
74
+ "re-apply intentional overlays, then bump the fork version (upstream_merge_conflict)."
71
75
  end
72
76
 
73
77
  def next_upstream_constraint(current, new_version)