pray-cli 1.25.0 → 1.26.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 817f233c7933ea4baf101ff50028641a039e3970951e83a1bfdda61f1ec55412
4
- data.tar.gz: 43b0d03df5ee524eefc819d5984b2ac01fa05377ffbdb60621ce195113339c45
3
+ metadata.gz: '087854aaa5f5bfead826eaa843615d2a52ba5f52236cc953b55d715c66c6f84b'
4
+ data.tar.gz: 2600fce7bf405cdcc5f9ef78da7e7026dd408f0babd026ecbf869dc09da2de6f
5
5
  SHA512:
6
- metadata.gz: 6ae47d928e86b7beb3c3b4009e836fe0a53d2f620a54eb891576acb8854931b51508bda2fbd4fe33565aab97047691561dc2ce49e332068f6235e1be0db34ba2
7
- data.tar.gz: 16e3dfa34136071143a0adcb85184c341f655fbcef9a6319c0a8249bd90c86bb0f61ace49ad16da2470660c23240a70c5ec343d3b14fec563fcffac741fbf9a3
6
+ metadata.gz: 1242427bff2198aafc4c74e2c6977128150dde7ba0d7fadecf9d92df8bcaf68662028f45e11419faf8e3a25b9b3c9c959034190dff054cb264c93b303ee1fe3a
7
+ data.tar.gz: 8d4b9ebc38d9fef0360ec9173b7e53ff92baff22f146e349f6ab049b48b1d0668883bfa0acb6bd3c8a8fd42993e91d06bc96b310b58df09d83dcfa0c377899f8
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.26.0 (2026-10-30)
6
+
7
+ - Add validated `related` references to prayspec parsing, rendering, and `pray explain`.
8
+ - Make `render --check`, frozen install, and `plan` compare destinations with the bytes the write path would produce. Remove unchanged managed spans retired by the previous lock while preserving edited or unknown spans for explicit review.
9
+ - Add `pray search` parity with the stable distribution-index contract. Search package names by substring through a declared source, local distribution root, or registry URL, and label catalog summaries as unverified claims in help.
10
+
5
11
  ## 1.25.0 (2026-10-26)
6
12
 
7
13
  - Bump the gem version to 1.25.0 with the coordinated language-registry cut.
data/README.md CHANGED
@@ -62,6 +62,7 @@ pray remove sample/base
62
62
  pray update
63
63
  pray unlock sample/base
64
64
  pray list
65
+ pray search base --source default
65
66
  pray tree
66
67
  pray explain sample/base
67
68
  pray outdated
@@ -85,6 +85,7 @@ module Pray
85
85
  lines << "source: #{package_source_summary(package)}"
86
86
  lines << "exports: #{format_list(package.selected_exports)}"
87
87
  lines << "dependencies: #{format_list(package.spec.dependencies.map(&:name))}"
88
+ lines << "related prayers: #{format_list(package.spec.related)}"
88
89
  lines << "tree hash: #{package.tree_hash}"
89
90
  lines << "artifact hash: #{package.artifact_hash}"
90
91
  if lockfile_package
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module CLI
5
+ def search_command(query:, source:, root:, url:)
6
+ hits = if root
7
+ RegistrySearch.search_local_registry(root, query)
8
+ elsif url
9
+ RegistrySearch.search_http_registry(url, query)
10
+ else
11
+ search_declared_source(query, source)
12
+ end
13
+ if hits.empty?
14
+ puts "no packages matched"
15
+ else
16
+ hits.each { |hit| puts(hit.summary ? "#{hit.name} — #{hit.summary}" : hit.name) }
17
+ end
18
+ end
19
+
20
+ def search_declared_source(query, source_name)
21
+ manifest = Pray.parse_manifest(Pray.read_manifest_text(manifest_path))
22
+ source = source_name ? manifest.sources.find { |entry| entry.name == source_name } : searchable_source(manifest)
23
+ raise Error.resolution("unknown source: #{source_name}") if source_name && !source
24
+ unless source
25
+ raise Error.resolution("Prayfile has no searchable sources; pass --root PATH or --url URL")
26
+ end
27
+
28
+ case source.kind
29
+ when "path"
30
+ root = File.expand_path(source.url, File.dirname(manifest_path))
31
+ RegistrySearch.search_local_registry(root, query)
32
+ when "registry", "static index"
33
+ RegistrySearch.search_http_registry(source.url, query)
34
+ else
35
+ raise Error.unsupported("search does not support source kind #{source.kind}")
36
+ end
37
+ end
38
+
39
+ def searchable_source(manifest)
40
+ manifest.sources.find { |source| %w[registry static\ index].include?(source.kind) } || manifest.sources.first
41
+ end
42
+ end
43
+ end
@@ -29,7 +29,7 @@ module Pray
29
29
  if flags[:check]
30
30
  ensure_rendered_outputs_current(project, rendered)
31
31
  else
32
- Render.write_rendered_targets(project, rendered)
32
+ Render.write_rendered_targets(project, rendered, project.previous_lockfile)
33
33
  end
34
34
  end
35
35
 
data/lib/pray/cli/help.rb CHANGED
@@ -36,6 +36,7 @@ module Pray
36
36
 
37
37
  INSPECT_COMMANDS = [
38
38
  "list list declared packages",
39
+ "search <query> [--source|--root|--url] find packages in a distribution index",
39
40
  "outdated [--remote] show constraint vs resolved versions",
40
41
  "explain <package> show why a package was selected",
41
42
  "tree print the dependency tree"
@@ -102,7 +103,7 @@ module Pray
102
103
 
103
104
  Without --check, render writes dest. It does not write Prayfile.lock.
104
105
  Use pray plan to preview a write.
105
- --check writes nothing. It fails unless dest already matches a fresh render from current inputs.
106
+ --check writes nothing. It fails when render would change dest.
106
107
  TEXT
107
108
  "format" => <<~TEXT.strip,
108
109
  rewrite Prayfile to recommended destination DSL
@@ -187,6 +188,7 @@ module Pray
187
188
  Subcommands: list, show, add-key, remove-key, set-signed, set-allow, import-repo, import-registry, check
188
189
  TEXT
189
190
  "list" => "list declared packages\n\nUsage: pray list",
191
+ "search" => "search a distribution index for package names\n\nUsage: pray search <query> [--source NAME | --root PATH | --url URL]\n\nMatches package names by case-insensitive substring. Summaries are unverified catalog claims.",
190
192
  "outdated" => <<~TEXT.strip,
191
193
  show constraint vs resolved versions
192
194
 
@@ -59,8 +59,8 @@ module Pray
59
59
  def ensure_rendered_outputs_current(project, rendered)
60
60
  rendered.each do |target|
61
61
  path = File.join(project.project_root, target.path)
62
- on_disk = File.read(path)
63
- if on_disk != target.content
62
+ retired = RenderReconcile.retired_span_checksums(project.previous_lockfile, target)
63
+ unless RenderReconcile.current?(path, target.path, target.content, retired)
64
64
  raise Error.render("#{path} is stale; rerun pray install to regenerate it or pray plan to inspect the diff")
65
65
  end
66
66
  end
@@ -64,6 +64,7 @@ module Pray
64
64
  when "serve" then [:serve, parse_serve_arguments(arguments)]
65
65
  when "confess" then [:confess, parse_confess_arguments(arguments)]
66
66
  when "list" then [:list]
67
+ when "search" then [:search, parse_search_arguments(arguments)]
67
68
  when "outdated" then [:outdated, arguments]
68
69
  when "explain" then [:explain, arguments.shift]
69
70
  when "vendor" then [:vendor]
@@ -121,6 +122,32 @@ module Pray
121
122
  {unused: unused}
122
123
  end
123
124
 
125
+ def parse_search_arguments(arguments)
126
+ options = {query: nil, source: nil, root: nil, url: nil}
127
+ while (argument = arguments.shift)
128
+ case argument
129
+ when "--source" then options[:source] = search_option_value(arguments, argument)
130
+ when "--root" then options[:root] = search_option_value(arguments, argument)
131
+ when "--url" then options[:url] = search_option_value(arguments, argument)
132
+ when /^--/ then raise Error.usage("unknown search flag: #{argument}")
133
+ else
134
+ raise Error.usage("unexpected search argument: #{argument}") if options[:query]
135
+
136
+ options[:query] = argument
137
+ end
138
+ end
139
+ raise Error.usage("search requires a query") unless options[:query]
140
+
141
+ options
142
+ end
143
+
144
+ def search_option_value(arguments, flag)
145
+ value = arguments.shift
146
+ raise Error.usage("#{flag} requires a value") unless value && !value.start_with?("--")
147
+
148
+ value
149
+ end
150
+
124
151
  def parse_add_arguments(arguments)
125
152
  name = arguments.shift
126
153
  raise Error.unsupported("add requires a package name") unless name
data/lib/pray/cli.rb CHANGED
@@ -14,6 +14,7 @@ require_relative "cli/commands/init_prayer"
14
14
  require_relative "cli/commands/workflow"
15
15
  require_relative "cli/commands/update_latest"
16
16
  require_relative "cli/commands/packages"
17
+ require_relative "cli/commands/search"
17
18
  require_relative "cli/commands/distribution"
18
19
  require_relative "cli/commands/trust"
19
20
  require_relative "cli/commands/auth"
@@ -119,6 +120,7 @@ module Pray
119
120
  in [:confess, confess_args] then confess_command(**confess_args)
120
121
  in [:sync, sync_args] then sync_command(**sync_args)
121
122
  in [:list] then list_command
123
+ in [:search, search_args] then search_command(**search_args)
122
124
  in [:outdated, arguments] then outdated_command(arguments)
123
125
  in [:explain, name] then explain_command(name)
124
126
  in [:vendor] then raise Error.unsupported("vendor is not implemented yet in pray-cli Ruby")
data/lib/pray/lockfile.rb CHANGED
@@ -169,7 +169,7 @@ module Pray
169
169
  end
170
170
 
171
171
  def build_lockfile(manifest_hash, environment, project_root, manifest_sources, manifest_targets, rendered, packages, source_revisions, source_host_keys, project = nil)
172
- rendered = Render.layout_rendered_targets(project, rendered) if project
172
+ rendered = Render.layout_rendered_targets(project, rendered, project.previous_lockfile) if project
173
173
  lockfile = Lockfile.new(
174
174
  manifest_hash: manifest_hash,
175
175
  environment: environment,
@@ -90,8 +90,8 @@ module Pray
90
90
  if frozen
91
91
  rendered.each do |target|
92
92
  path = File.join(project.project_root, target.path)
93
- on_disk = File.read(path)
94
- if on_disk != target.content
93
+ retired = RenderReconcile.retired_span_checksums(existing, target)
94
+ unless RenderReconcile.current?(path, target.path, target.content, retired)
95
95
  raise Error.render(
96
96
  "#{path} is stale; rerun install to regenerate it or plan to inspect the diff"
97
97
  )
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module PackageRelation
5
+ module_function
6
+
7
+ def canonicalize(references)
8
+ Array(references).map(&:strip).uniq.sort.tap do |canonical|
9
+ invalid = canonical.find { |reference| !valid?(reference) }
10
+ raise Error.parse("prayspec", "invalid related prayer reference") if invalid
11
+ end
12
+ end
13
+
14
+ def valid?(reference)
15
+ return false if reference.empty? || reference.match?(/[\\[:cntrl:]]/)
16
+ if reference.match?(/\A[A-Za-z][A-Za-z0-9+.-]*:.+\z/)
17
+ return !uri_has_userinfo?(reference)
18
+ end
19
+ if reference.start_with?("/")
20
+ path = locator_path(reference)
21
+ return path.length > 1 && !path.start_with?("//")
22
+ end
23
+ if reference.start_with?("./", "../")
24
+ return locator_path(reference).split("/").any? do |segment|
25
+ !segment.empty? && ![".", ".."].include?(segment)
26
+ end
27
+ end
28
+
29
+ return false if reference.match?(/[?#]/)
30
+
31
+ segments = reference.split("/", -1)
32
+ segments.length == 2 && segments.none? { |segment| segment.empty? || [".", ".."].include?(segment) }
33
+ end
34
+
35
+ def locator_path(reference)
36
+ reference.split(/[?#]/, 2).first
37
+ end
38
+
39
+ def uri_has_userinfo?(reference)
40
+ after_scheme = reference.split("://", 2)[1]
41
+ after_scheme && after_scheme.split(%r{[/?#]}, 2).first.include?("@")
42
+ end
43
+ end
44
+ end
@@ -14,13 +14,13 @@ module Pray
14
14
  PackageSpec = Struct.new(
15
15
  :name, :version, :summary, :description, :authors, :maintainers, :license, :homepage,
16
16
  :source_code_uri, :changelog_uri, :prayfile_version, :files, :exports,
17
- :skills, :templates, :adapters, :targets, :dependencies, :metadata, :upstream
17
+ :skills, :templates, :adapters, :targets, :related, :dependencies, :metadata, :upstream
18
18
  ) do
19
19
  def initialize(
20
20
  name: "", version: "", summary: nil, description: nil, authors: [], maintainers: [], license: nil,
21
21
  homepage: nil, source_code_uri: nil, changelog_uri: nil, prayfile_version: nil,
22
22
  files: [], exports: {}, skills: {}, templates: {}, adapters: {}, targets: [],
23
- dependencies: [], metadata: {}, upstream: nil
23
+ related: [], dependencies: [], metadata: {}, upstream: nil
24
24
  )
25
25
  super
26
26
  end
@@ -50,6 +50,7 @@ module Pray
50
50
  copy.authors = Array(authors).sort
51
51
  copy.maintainers = Array(maintainers).sort
52
52
  copy.targets = Array(targets).sort
53
+ copy.related = PackageRelation.canonicalize(related)
53
54
  copy.dependencies = dependencies.sort_by { |dependency| [dependency.name, dependency.constraint, dependency.optional] }
54
55
  end
55
56
  end
@@ -258,6 +259,7 @@ module Pray
258
259
  when "maintainers" then spec.maintainers = array_of_strings(value)
259
260
  when "files" then spec.files = array_of_strings(value)
260
261
  when "targets" then spec.targets = array_of_strings(value)
262
+ when "related" then spec.related = array_of_strings(value)
261
263
  when "exports" then spec.exports = parse_exports(value)
262
264
  when "skills" then spec.skills = parse_skills(value)
263
265
  when "templates" then spec.templates = parse_templates(value)
@@ -51,6 +51,9 @@ module Pray
51
51
  push_named_paths(lines, "templates", spec.templates)
52
52
  push_string_map(lines, "adapters", spec.adapters)
53
53
  push_array(lines, "targets", spec.targets) unless spec.targets.nil? || spec.targets.empty?
54
+ unless spec.related.nil? || spec.related.empty?
55
+ push_array(lines, "related", spec.related)
56
+ end
54
57
  end
55
58
 
56
59
  def push_relation_fields(lines, spec)
@@ -21,8 +21,10 @@ module Pray
21
21
  end
22
22
 
23
23
  def reject_unsafe_package_name!(package_name)
24
- if package_name.nil? || package_name.empty? || package_name.include?("\0") || package_name.include?("\\") ||
25
- package_name.include?("..")
24
+ segments = package_name&.split("/", -1)
25
+ if segments&.length != 2 || segments.any? do |segment|
26
+ segment.empty? || [".", ".."].include?(segment) || segment.include?("\0") || segment.include?("\\")
27
+ end
26
28
  raise Error.resolution("invalid package name: #{package_name.inspect}")
27
29
  end
28
30
 
data/lib/pray/plan.rb CHANGED
@@ -16,7 +16,7 @@ module Pray
16
16
  package_lines: ApplyReport.local_summary_lines(previous_lockfile, project) +
17
17
  package_summary_lines(previous_lockfile, lockfile, project),
18
18
  lockfile: lockfile_change_status(previous_lockfile, lockfile),
19
- targets: rendered.map { |target| target_change(project, target) },
19
+ targets: rendered.map { |target| target_change(project, target, previous_lockfile) },
20
20
  provisioned: destinations.map { |file, status| [file.path, status.to_s] },
21
21
  warnings: []
22
22
  )
@@ -57,11 +57,12 @@ module Pray
57
57
  Pray.lockfiles_equivalent?(lockfile, existing) ? "unchanged" : "update"
58
58
  end
59
59
 
60
- def target_change(project, target)
60
+ def target_change(project, target, previous_lockfile)
61
61
  path = File.join(project.project_root, target.path)
62
+ retired = RenderReconcile.retired_span_checksums(previous_lockfile, target)
62
63
  change = if !File.exist?(path)
63
64
  "write"
64
- elsif RenderDest.read_regular_bytes(path, target.path) == target.content.b
65
+ elsif RenderReconcile.current?(path, target.path, target.content, retired)
65
66
  "unchanged"
66
67
  else
67
68
  "update"
data/lib/pray/registry.rb CHANGED
@@ -276,7 +276,7 @@ module Pray
276
276
 
277
277
  def http_get(url)
278
278
  uri = URI(url)
279
- bounded_http_exchange(uri, Net::HTTP::Get.new(uri), "HTTP request failed for #{url}")
279
+ bounded_http_exchange(uri, Net::HTTP::Get.new(uri), "HTTP request failed for #{UrlRedaction.redact_userinfo(url)}")
280
280
  end
281
281
 
282
282
  def http_put(url, content_type, body)
@@ -284,7 +284,7 @@ module Pray
284
284
  request = Net::HTTP::Put.new(uri)
285
285
  request["Content-Type"] = content_type
286
286
  request.body = body
287
- bounded_http_exchange(uri, request, "HTTP upload failed for #{url}")
287
+ bounded_http_exchange(uri, request, "HTTP upload failed for #{UrlRedaction.redact_userinfo(url)}")
288
288
  end
289
289
 
290
290
  def http_post(url, content_type, body)
@@ -292,7 +292,7 @@ module Pray
292
292
  request = Net::HTTP::Post.new(uri)
293
293
  request["Content-Type"] = content_type
294
294
  request.body = body
295
- bounded_http_exchange(uri, request, "HTTP request failed for #{url}")
295
+ bounded_http_exchange(uri, request, "HTTP request failed for #{UrlRedaction.redact_userinfo(url)}")
296
296
  end
297
297
 
298
298
  def bounded_http_exchange(uri, request, failure_message)
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Pray
6
+ RegistrySearchHit = Struct.new(:name, :summary, keyword_init: true)
7
+
8
+ module RegistrySearch
9
+ module_function
10
+
11
+ def search_local_registry(root, query, include_summary: true)
12
+ index = parse_index(File.read(File.join(root, "v1", "index.json"), encoding: "UTF-8"))
13
+ search_index_names(index, query).map do |name|
14
+ metadata_path = Registry.registry_metadata_path(root, name)
15
+ metadata = Registry.parse_metadata(File.read(metadata_path, encoding: "UTF-8")) if include_summary && File.file?(metadata_path)
16
+ search_hit(name, metadata)
17
+ end
18
+ end
19
+
20
+ def search_http_registry(url, query, include_summary: true)
21
+ index = parse_index(Registry.http_get(Registry.join_url(url, "v1/index.json")))
22
+ search_index_names(index, query).map do |name|
23
+ PathSafety.reject_unsafe_package_name!(name)
24
+ metadata = fetch_http_metadata(url, name) if include_summary
25
+ search_hit(name, metadata)
26
+ end
27
+ end
28
+
29
+ def search_index_names(index, query)
30
+ needle = query.strip.downcase
31
+ index.fetch("packages").select do |name|
32
+ raise Error.parse("registry index", "package names must be strings") unless name.is_a?(String)
33
+
34
+ PathSafety.reject_unsafe_package_name!(name)
35
+ name.downcase.include?(needle)
36
+ end.sort
37
+ end
38
+
39
+ def latest_non_yanked_summary(metadata)
40
+ return unless metadata
41
+
42
+ latest = metadata.versions.reject(&:yanked).max_by { |version| Gem::Version.new(version.version) }
43
+ summary = latest&.derived_metadata&.fetch("summary", nil).to_s.strip
44
+ summary unless summary.empty?
45
+ end
46
+
47
+ def parse_index(text)
48
+ index = JSON.parse(text)
49
+ unless index["spec"] == "prayfile-distribution-1" && index["packages"].is_a?(Array)
50
+ raise Error.parse("registry index", "expected prayfile-distribution-1 with a packages array")
51
+ end
52
+ index
53
+ rescue JSON::ParserError => error
54
+ raise Error.parse("registry index", error.message)
55
+ end
56
+
57
+ def fetch_http_metadata(url, name)
58
+ path = "v1/packages/#{name}.json"
59
+ Registry.parse_metadata(Registry.http_get(Registry.join_url(url, path)))
60
+ rescue Error => error
61
+ raise unless error.category == :resolution
62
+
63
+ nil
64
+ end
65
+
66
+ def search_hit(name, metadata)
67
+ if metadata && metadata.name != name
68
+ raise Error.integrity("registry metadata name mismatch for #{name}")
69
+ end
70
+ RegistrySearchHit.new(name: name, summary: latest_non_yanked_summary(metadata))
71
+ end
72
+ end
73
+ end
@@ -9,13 +9,15 @@ module Pray
9
9
 
10
10
  def write_rendered_targets(project, rendered, previous_lockfile = nil)
11
11
  validate_destinations!(project, previous_lockfile)
12
+ RenderReconcile.reject_managed_span_conflicts!(project, rendered, previous_lockfile) if previous_lockfile
12
13
  rendered.each do |target|
13
14
  PathSafety.validate_destination_path!(target.path)
14
15
  ensure_safe_destination_ancestors!(project.project_root, target.path, target.path)
15
16
  path = File.join(project.project_root, target.path)
16
17
  FileUtils.mkdir_p(File.dirname(path))
17
18
  ensure_safe_destination_ancestors!(project.project_root, target.path, target.path)
18
- write_rendered_content(path, target.path, target.content)
19
+ retired = RenderReconcile.retired_span_checksums(previous_lockfile, target)
20
+ write_rendered_content(path, target.path, target.content, retired)
19
21
  end
20
22
  materialize(project, previous_lockfile)
21
23
  end
@@ -147,7 +149,7 @@ module Pray
147
149
  end
148
150
  end
149
151
 
150
- def layout_rendered_content(path, display, fresh)
152
+ def layout_rendered_content(path, display, fresh, retired = {})
151
153
  kind = destination_kind(path)
152
154
  return fresh if kind == :missing
153
155
 
@@ -155,15 +157,15 @@ module Pray
155
157
  unless kind == :regular
156
158
  raise Error.render("refusing to write `#{display}`; destination is not a regular file")
157
159
  end
158
- RenderPatch.patch_rendered_content(decode_utf8(read_regular_bytes(path, display), display), fresh)
160
+ RenderPatch.patch_rendered_content(decode_utf8(read_regular_bytes(path, display), display), fresh, retired)
159
161
  end
160
162
 
161
- def write_rendered_content(path, display, fresh)
163
+ def write_rendered_content(path, display, fresh, retired = {})
162
164
  return create_bytes(path, display, fresh) if destination_kind(path) == :missing
163
165
 
164
166
  open_regular(path, display, File::RDWR) do |file|
165
167
  existing = decode_utf8(read_destination_bytes(file, display), display)
166
- content = RenderPatch.patch_rendered_content(existing, fresh)
168
+ content = RenderPatch.patch_rendered_content(existing, fresh, retired)
167
169
  return if Transaction.replace(path, existing.b, content.b)
168
170
 
169
171
  file.rewind
@@ -4,12 +4,13 @@ module Pray
4
4
  module Render
5
5
  module_function
6
6
 
7
- def layout_rendered_targets(project, rendered)
7
+ def layout_rendered_targets(project, rendered, previous_lockfile = project.previous_lockfile)
8
8
  rendered.map do |target|
9
9
  PathSafety.validate_destination_path!(target.path)
10
10
  RenderDest.ensure_safe_destination_ancestors!(project.project_root, target.path, target.path)
11
11
  path = File.join(project.project_root, target.path)
12
- content = RenderDest.layout_rendered_content(path, target.path, target.content)
12
+ retired = RenderReconcile.retired_span_checksums(previous_lockfile, target)
13
+ content = RenderDest.layout_rendered_content(path, target.path, target.content, retired)
13
14
  RenderedTarget.new(
14
15
  path: target.path,
15
16
  content: content,
@@ -4,11 +4,11 @@ module Pray
4
4
  module RenderPatch
5
5
  module_function
6
6
 
7
- def patch_rendered_content(existing, fresh)
8
- existing_segments = split_segments(existing)
7
+ def patch_rendered_content(existing, fresh, retired = {})
9
8
  fresh_segments = split_segments(fresh)
10
9
  fresh_managed = managed_bodies(fresh_segments)
11
- return fresh unless overlapping_managed?(existing_segments, fresh_managed)
10
+ existing_segments = drop_unchanged_retired(split_segments(existing), fresh_managed, retired)
11
+ return preserve_unmatched_managed(fresh, existing_segments, fresh_managed) unless overlapping_managed?(existing_segments, fresh_managed)
12
12
 
13
13
  used = {}
14
14
  output = +""
@@ -24,6 +24,28 @@ module Pray
24
24
  output.end_with?("\n") ? output : "#{output}\n"
25
25
  end
26
26
 
27
+ def drop_unchanged_retired(segments, fresh_managed, retired)
28
+ segments.reject do |segment|
29
+ next false unless segment[:kind] == :managed
30
+ next false if fresh_managed.key?(segment[:id])
31
+
32
+ expected = retired[segment[:id]]
33
+ expected && Hashing.checksum_managed_span_content(segment[:body]) == expected
34
+ end
35
+ end
36
+
37
+ def preserve_unmatched_managed(fresh, segments, fresh_managed)
38
+ unmatched = segments.select do |segment|
39
+ segment[:kind] == :managed && !fresh_managed.key?(segment[:id])
40
+ end
41
+ return fresh if unmatched.empty?
42
+
43
+ output = fresh.dup
44
+ output << "\n" unless output.empty? || output.end_with?("\n")
45
+ unmatched.each { |segment| output << managed_segment(segment[:id], segment[:body]) }
46
+ output
47
+ end
48
+
27
49
  def managed_bodies(segments)
28
50
  segments.filter_map { |segment| [segment[:id], segment[:body]] if segment[:kind] == :managed }.to_h
29
51
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module RenderReconcile
5
+ module_function
6
+
7
+ def current?(path, display, fresh, retired = {})
8
+ kind = RenderDest.destination_kind(path)
9
+ return false if kind == :missing
10
+
11
+ RenderDest.fail_if_symlink!(path, display)
12
+ unless kind == :regular
13
+ raise Error.render("refusing to read `#{display}`; destination is not a regular file")
14
+ end
15
+ existing = RenderDest.decode_utf8(RenderDest.read_regular_bytes(path, display), display)
16
+ existing == RenderPatch.patch_rendered_content(existing, fresh, retired)
17
+ end
18
+
19
+ def retired_span_checksums(previous_lockfile, target)
20
+ current_ids = target.managed_spans.to_h { |span| [span.id, true] }
21
+ Array(previous_lockfile&.managed_span).filter_map do |span|
22
+ next unless span.target == target.path
23
+ next if current_ids[span.id]
24
+
25
+ [span.id, span.ideal_checksum]
26
+ end.to_h
27
+ end
28
+
29
+ def reject_managed_span_conflicts!(project, rendered, previous_lockfile)
30
+ active_by_target = rendered.to_h do |target|
31
+ [target.path, target.managed_spans.to_h { |span| [span.id, true] }]
32
+ end
33
+ Array(previous_lockfile.managed_span).group_by(&:target).each do |target_path, spans|
34
+ active = active_by_target[target_path]
35
+ next unless active
36
+
37
+ PathSafety.validate_destination_path!(target_path)
38
+ RenderDest.ensure_safe_destination_ancestors!(project.project_root, target_path, target_path)
39
+ path = File.join(project.project_root, target_path)
40
+ next unless File.file?(path)
41
+
42
+ content = RenderDest.decode_utf8(RenderDest.read_regular_bytes(path, target_path), target_path)
43
+ bodies = RenderPatch.managed_bodies(RenderPatch.split_segments(content))
44
+ spans.each do |span|
45
+ next unless active[span.id] && bodies.key?(span.id)
46
+ next if Hashing.checksum_managed_span_content(bodies[span.id]) == span.ideal_checksum
47
+
48
+ raise Error.render(
49
+ "conflict: managed span `#{span.id}` in `#{target_path}` was edited " \
50
+ "(`#{span.package}::#{span.export}`)"
51
+ )
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Pray
6
+ module UrlRedaction
7
+ module_function
8
+
9
+ def redact_userinfo(url)
10
+ uri = URI(url)
11
+ return url unless uri.user || uri.password
12
+
13
+ uri.user = "***"
14
+ uri.password = nil
15
+ uri.to_s
16
+ rescue URI::InvalidURIError
17
+ url
18
+ end
19
+ end
20
+ end
data/lib/pray/verify.rb CHANGED
@@ -177,7 +177,7 @@ module Pray
177
177
 
178
178
  VerificationFinding.new(
179
179
  kind: "orphan_marker",
180
- message: "`#{target_path}` contains marker `#{marker_id}` that is not tracked in `Prayfile.lock`. Remove the marker or run `pray install` to reconcile."
180
+ message: "`#{target_path}` contains marker `#{marker_id}` that is not tracked in `Prayfile.lock`. Inspect and remove the block explicitly."
181
181
  )
182
182
  end
183
183
  end
data/lib/pray/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pray
4
- VERSION = "1.25.0"
5
- GENERATED_BY = "pray 1.25.0"
4
+ VERSION = "1.26.0"
5
+ GENERATED_BY = "pray 1.26.0"
6
6
  end
data/lib/pray.rb CHANGED
@@ -15,6 +15,7 @@ require_relative "pray/destination"
15
15
  require_relative "pray/manifest"
16
16
  require_relative "pray/format_serialize"
17
17
  require_relative "pray/format_manifest"
18
+ require_relative "pray/package_relation"
18
19
  require_relative "pray/package_spec"
19
20
  require_relative "pray/package_spec_paths"
20
21
  require_relative "pray/package_spec_version"
@@ -52,6 +53,7 @@ require_relative "pray/render_content"
52
53
  require_relative "pray/render_managed"
53
54
  require_relative "pray/render"
54
55
  require_relative "pray/render_dest"
56
+ require_relative "pray/render_reconcile"
55
57
  require_relative "pray/transaction"
56
58
  require_relative "pray/render_layout"
57
59
  require_relative "pray/verify_position"
@@ -60,9 +62,11 @@ require_relative "pray/verify"
60
62
  require_relative "pray/verify_locked_dest"
61
63
  require_relative "pray/resource_limits"
62
64
  require_relative "pray/http_body"
65
+ require_relative "pray/url_redaction"
63
66
  require_relative "pray/registry_install"
64
67
  require_relative "pray/registry_integrity"
65
68
  require_relative "pray/registry"
69
+ require_relative "pray/registry_search"
66
70
  require_relative "pray/derived_metadata"
67
71
  require_relative "pray/archive_unpack"
68
72
  require_relative "pray/archive"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pray-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.25.0
4
+ version: 1.26.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -115,6 +115,7 @@ files:
115
115
  - lib/pray/cli/commands/init_prayer_manifest.rb
116
116
  - lib/pray/cli/commands/meta.rb
117
117
  - lib/pray/cli/commands/packages.rb
118
+ - lib/pray/cli/commands/search.rb
118
119
  - lib/pray/cli/commands/trust.rb
119
120
  - lib/pray/cli/commands/update_latest.rb
120
121
  - lib/pray/cli/commands/workflow.rb
@@ -161,6 +162,7 @@ files:
161
162
  - lib/pray/manifest_parser_helpers.rb
162
163
  - lib/pray/manifest_parser_publish.rb
163
164
  - lib/pray/materialize.rb
165
+ - lib/pray/package_relation.rb
164
166
  - lib/pray/package_spec.rb
165
167
  - lib/pray/package_spec_paths.rb
166
168
  - lib/pray/package_spec_render.rb
@@ -176,6 +178,7 @@ files:
176
178
  - lib/pray/registry_install.rb
177
179
  - lib/pray/registry_integrity.rb
178
180
  - lib/pray/registry_paths.rb
181
+ - lib/pray/registry_search.rb
179
182
  - lib/pray/render.rb
180
183
  - lib/pray/render_content.rb
181
184
  - lib/pray/render_dest.rb
@@ -183,6 +186,7 @@ files:
183
186
  - lib/pray/render_managed.rb
184
187
  - lib/pray/render_patch.rb
185
188
  - lib/pray/render_project.rb
189
+ - lib/pray/render_reconcile.rb
186
190
  - lib/pray/resolve.rb
187
191
  - lib/pray/resolve_context.rb
188
192
  - lib/pray/resolve_deps.rb
@@ -216,6 +220,7 @@ files:
216
220
  - lib/pray/upstream_merge.rb
217
221
  - lib/pray/upstream_plan.rb
218
222
  - lib/pray/upstream_refresh.rb
223
+ - lib/pray/url_redaction.rb
219
224
  - lib/pray/verify.rb
220
225
  - lib/pray/verify_locked_dest.rb
221
226
  - lib/pray/verify_position.rb
@@ -246,7 +251,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
246
251
  - !ruby/object:Gem::Version
247
252
  version: '0'
248
253
  requirements: []
249
- rubygems_version: 4.0.20
254
+ rubygems_version: 3.6.9
250
255
  specification_version: 4
251
256
  summary: Package manager for the language placed before inference (Prayfile)
252
257
  test_files: []