pray-cli 1.16.0 → 1.18.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/lib/pray/apply_report.rb +49 -0
  4. data/lib/pray/cli/commands/init.rb +1 -20
  5. data/lib/pray/cli/commands/init_prayer.rb +76 -0
  6. data/lib/pray/cli/commands/init_prayer_manifest.rb +102 -0
  7. data/lib/pray/cli/commands/packages.rb +13 -2
  8. data/lib/pray/cli/commands/trust.rb +3 -3
  9. data/lib/pray/cli/commands/update_latest.rb +14 -16
  10. data/lib/pray/cli/commands/workflow.rb +5 -0
  11. data/lib/pray/cli/help.rb +52 -15
  12. data/lib/pray/cli/helpers.rb +13 -5
  13. data/lib/pray/cli/parse.rb +21 -1
  14. data/lib/pray/cli.rb +3 -1
  15. data/lib/pray/destination.rb +32 -2
  16. data/lib/pray/distribution.rb +94 -0
  17. data/lib/pray/format_serialize.rb +1 -4
  18. data/lib/pray/git_cache.rb +192 -0
  19. data/lib/pray/git_clone.rb +32 -0
  20. data/lib/pray/git_materialize.rb +76 -0
  21. data/lib/pray/git_run.rb +41 -0
  22. data/lib/pray/git_sources.rb +60 -145
  23. data/lib/pray/local_prayer.rb +38 -0
  24. data/lib/pray/lockfile.rb +8 -2
  25. data/lib/pray/manifest.rb +6 -3
  26. data/lib/pray/manifest_json.rb +1 -0
  27. data/lib/pray/manifest_parser_blocks.rb +15 -15
  28. data/lib/pray/materialize.rb +7 -1
  29. data/lib/pray/package_spec.rb +25 -13
  30. data/lib/pray/package_spec_render.rb +4 -3
  31. data/lib/pray/package_spec_version.rb +33 -0
  32. data/lib/pray/plan.rb +2 -1
  33. data/lib/pray/publish.rb +27 -2
  34. data/lib/pray/registry.rb +4 -15
  35. data/lib/pray/registry_http_cache.rb +17 -0
  36. data/lib/pray/render.rb +6 -36
  37. data/lib/pray/render_managed.rb +64 -0
  38. data/lib/pray/render_patch.rb +63 -12
  39. data/lib/pray/resolve.rb +6 -37
  40. data/lib/pray/resolve_local.rb +39 -0
  41. data/lib/pray/resolve_source.rb +5 -0
  42. data/lib/pray/serve.rb +15 -4
  43. data/lib/pray/serve_range.rb +36 -0
  44. data/lib/pray/torrent_manifest.rb +78 -0
  45. data/lib/pray/upstream_drift.rb +42 -0
  46. data/lib/pray/upstream_merge.rb +25 -0
  47. data/lib/pray/upstream_refresh.rb +28 -2
  48. data/lib/pray/verify.rb +2 -2
  49. data/lib/pray/version.rb +2 -2
  50. data/lib/pray.rb +10 -0
  51. metadata +17 -1
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module RegistryHttpCache
5
+ module_function
6
+
7
+ def fetch_package_metadata(source_url, package_name)
8
+ @cache ||= {}
9
+ key = [source_url, package_name]
10
+ return @cache[key] if @cache.key?(key)
11
+
12
+ PathSafety.reject_unsafe_package_name!(package_name)
13
+ response = Registry.http_get(Registry.join_url(source_url, "v1/packages/#{package_name}.json"))
14
+ @cache[key] = Registry.parse_metadata(response)
15
+ end
16
+ end
17
+ end
data/lib/pray/render.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "fileutils"
4
4
  require "pathname"
5
5
  require_relative "render_content"
6
+ require_relative "render_managed"
6
7
 
7
8
  module Pray
8
9
  RenderedTarget = Struct.new(:path, :content, :managed_spans) do
@@ -76,7 +77,7 @@ module Pray
76
77
 
77
78
  (target.entries || []).each do |entry|
78
79
  if entry.kind == "local"
79
- append_scoped_local(builder, project, entry.path, symbols)
80
+ append_scoped_local(builder, managed_spans, project, target, output, entry.path, symbols)
80
81
  else
81
82
  append_scoped_package(builder, managed_spans, project, target, output, entry.name, symbols)
82
83
  end
@@ -99,18 +100,17 @@ module Pray
99
100
  builder.append_empty_line
100
101
  end
101
102
  symbols = project.manifest.symbols || {}
103
+ managed_spans = []
102
104
  unbound_locals.each do |local|
103
105
  next if local.content.empty? && local.optional
104
106
 
105
107
  builder.append_line("### #{local.manifest_path}")
106
- builder.append_body(Substitute.substitute_pray_symbols(local.content, symbols))
107
- builder.append_empty_line
108
+ append_managed_local(builder, managed_spans, local, target, output, symbols)
108
109
  end
109
110
 
110
111
  builder.append_line("## Shared instructions")
111
112
  builder.append_empty_line
112
113
 
113
- managed_spans = []
114
114
  project.packages.each do |package|
115
115
  next unless Environment.package_matches_environment?(package.declaration.groups, project.environment)
116
116
  next unless Destination.package_bound_to_compose?(package.declaration, target)
@@ -133,13 +133,11 @@ module Pray
133
133
  builder.append_empty_line
134
134
  end
135
135
 
136
- def append_scoped_local(builder, project, path, symbols)
136
+ def append_scoped_local(builder, managed_spans, project, target, output, path, symbols)
137
137
  local = project.local_files.find { |candidate| candidate.manifest_path == path }
138
138
  return unless local
139
- return if local.content.empty? && local.optional
140
139
 
141
- builder.append_body(Substitute.substitute_pray_symbols(local.content, symbols))
142
- builder.append_empty_line
140
+ append_managed_local(builder, managed_spans, local, target, output, symbols)
143
141
  end
144
142
 
145
143
  def append_scoped_package(builder, managed_spans, project, target, output, package_name, symbols)
@@ -154,34 +152,6 @@ module Pray
154
152
  end
155
153
  end
156
154
 
157
- def append_managed_export(builder, managed_spans, package, export, target, output, symbols)
158
- body = package.export_bodies[export]
159
- unless body
160
- raise Error.integrity("compose cannot write binary export #{export}; use file: for unmarked bytes") if package.spec.exports[export]&.kind == "file"
161
- raise Error.render("package #{package.declaration.name} is missing cached export #{export}")
162
- end
163
-
164
- body = Substitute.substitute_pray_symbols(body, symbols)
165
- identifier = Hashing.marker_id("#{package.declaration.name}:#{export}:#{target.name}")
166
- open_line = builder.next_line_number
167
- builder.append_line("<!-- pray:#{identifier} -->")
168
- builder.append_body(body)
169
- close_line = builder.next_line_number
170
- builder.append_line("<!-- pray:#{identifier} -->")
171
- managed_spans << ManagedSpanRecord.new(
172
- id: identifier,
173
- target: output,
174
- open_line: open_line,
175
- close_line: close_line,
176
- ideal_checksum: Hashing.checksum_managed_span_content(body),
177
- package: package.declaration.name,
178
- export: export,
179
- source_checksum: package.source_checksum,
180
- silenced: false
181
- )
182
- builder.append_empty_line
183
- end
184
-
185
155
  def should_inline_export?(package, export_name)
186
156
  export = package.spec.exports[export_name]
187
157
  export.nil? || %w[fragment file].include?(export.kind)
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module Render
5
+ module_function
6
+
7
+ def append_managed_local(builder, managed_spans, local, target, output, symbols)
8
+ return if local.content.empty? && local.optional
9
+
10
+ body = Substitute.substitute_pray_symbols(local.content, symbols)
11
+ append_managed_span(
12
+ builder,
13
+ managed_spans,
14
+ "local:#{local.manifest_path}:#{target.name}",
15
+ body,
16
+ output,
17
+ LOCAL_EMBED_PACKAGE,
18
+ local.manifest_path,
19
+ local.source_checksum
20
+ )
21
+ end
22
+
23
+ def append_managed_export(builder, managed_spans, package, export, target, output, symbols)
24
+ body = package.export_bodies[export]
25
+ unless body
26
+ raise Error.integrity("compose cannot write binary export #{export}; use file: for unmarked bytes") if package.spec.exports[export]&.kind == "file"
27
+ raise Error.render("package #{package.declaration.name} is missing cached export #{export}")
28
+ end
29
+
30
+ body = Substitute.substitute_pray_symbols(body, symbols)
31
+ append_managed_span(
32
+ builder,
33
+ managed_spans,
34
+ "#{package.declaration.name}:#{export}:#{target.name}",
35
+ body,
36
+ output,
37
+ package.declaration.name,
38
+ export,
39
+ package.source_checksum
40
+ )
41
+ end
42
+
43
+ def append_managed_span(builder, managed_spans, seed, body, output, package, export, source_checksum)
44
+ identifier = Hashing.marker_id(seed)
45
+ open_line = builder.next_line_number
46
+ builder.append_line("<!-- pray:#{identifier} -->")
47
+ builder.append_body(body)
48
+ close_line = builder.next_line_number
49
+ builder.append_line("<!-- pray:#{identifier} -->")
50
+ managed_spans << ManagedSpanRecord.new(
51
+ id: identifier,
52
+ target: output,
53
+ open_line: open_line,
54
+ close_line: close_line,
55
+ ideal_checksum: Hashing.checksum_managed_span_content(body),
56
+ package: package,
57
+ export: export,
58
+ source_checksum: source_checksum,
59
+ silenced: false
60
+ )
61
+ builder.append_empty_line
62
+ end
63
+ end
64
+ end
@@ -7,28 +7,74 @@ module Pray
7
7
  def patch_rendered_content(existing, fresh)
8
8
  existing_segments = split_segments(existing)
9
9
  fresh_segments = split_segments(fresh)
10
- fresh_managed = fresh_segments.filter_map do |segment|
11
- [segment[:id], segment[:body]] if segment[:kind] == :managed
12
- end.to_h
13
- overlap = existing_segments.any? do |segment|
14
- segment[:kind] == :managed && fresh_managed.key?(segment[:id])
15
- end
16
- return fresh unless overlap
10
+ fresh_managed = managed_bodies(fresh_segments)
11
+ return fresh unless overlapping_managed?(existing_segments, fresh_managed)
17
12
 
18
13
  used = {}
19
- output = existing_segments.map do |segment|
20
- next segment[:text] if segment[:kind] == :text
14
+ output = +""
15
+ remaining = prepend_new_leading_spans(
16
+ existing_segments,
17
+ fresh_segments,
18
+ managed_id_set(existing_segments),
19
+ used,
20
+ output
21
+ )
22
+ splice_existing_managed(remaining, fresh_managed, used, output)
23
+ append_unused_fresh_managed(fresh_segments, used, output)
24
+ output.end_with?("\n") ? output : "#{output}\n"
25
+ end
26
+
27
+ def managed_bodies(segments)
28
+ segments.filter_map { |segment| [segment[:id], segment[:body]] if segment[:kind] == :managed }.to_h
29
+ end
30
+
31
+ def overlapping_managed?(existing_segments, fresh_managed)
32
+ existing_segments.any? { |segment| segment[:kind] == :managed && fresh_managed.key?(segment[:id]) }
33
+ end
34
+
35
+ def managed_id_set(segments)
36
+ segments.filter_map { |segment| segment[:id] if segment[:kind] == :managed }.to_h { |id| [id, true] }
37
+ end
38
+
39
+ def splice_existing_managed(remaining, fresh_managed, used, output)
40
+ remaining.each do |segment|
41
+ if segment[:kind] == :text
42
+ output << segment[:text]
43
+ next
44
+ end
21
45
 
22
46
  used[segment[:id]] = true
23
- managed_segment(segment[:id], fresh_managed.fetch(segment[:id], segment[:body]))
24
- end.join
47
+ output << managed_segment(segment[:id], fresh_managed.fetch(segment[:id], segment[:body]))
48
+ end
49
+ end
50
+
51
+ def append_unused_fresh_managed(fresh_segments, used, output)
25
52
  fresh_segments.each do |segment|
26
53
  next unless segment[:kind] == :managed
27
54
  next if used[segment[:id]]
28
55
 
29
56
  output << managed_segment(segment[:id], segment[:body])
30
57
  end
31
- output.end_with?("\n") ? output : "#{output}\n"
58
+ end
59
+
60
+ def prepend_new_leading_spans(existing_segments, fresh_segments, existing_ids, used, output)
61
+ missing = fresh_segments.any? { |segment| segment[:kind] == :managed && !existing_ids[segment[:id]] }
62
+ return existing_segments unless missing
63
+
64
+ shared_id = fresh_segments.find { |segment| segment[:kind] == :managed && existing_ids[segment[:id]] }&.[](:id)
65
+ return existing_segments unless shared_id
66
+
67
+ fresh_segments.each do |segment|
68
+ break if segment[:kind] == :managed && segment[:id] == shared_id
69
+
70
+ if segment[:kind] == :text
71
+ output << segment[:text]
72
+ else
73
+ used[segment[:id]] = true
74
+ output << managed_segment(segment[:id], segment[:body])
75
+ end
76
+ end
77
+ skip_until_managed(existing_segments, shared_id)
32
78
  end
33
79
 
34
80
  def relocate_managed_spans(content, spans)
@@ -68,6 +114,11 @@ module Pray
68
114
  segments
69
115
  end
70
116
 
117
+ def skip_until_managed(segments, identifier)
118
+ index = segments.index { |segment| segment[:kind] == :managed && segment[:id] == identifier }
119
+ index ? segments[index..] : segments
120
+ end
121
+
71
122
  def lines_of(content)
72
123
  content.lines(chomp: true)
73
124
  end
data/lib/pray/resolve.rb CHANGED
@@ -17,7 +17,7 @@ module Pray
17
17
  )
18
18
 
19
19
  ResolvedLocalFile = Struct.new(
20
- :path, :manifest_path, :content, :position, :optional
20
+ :path, :manifest_path, :content, :source_checksum, :position, :optional
21
21
  )
22
22
 
23
23
  module Resolve
@@ -61,7 +61,6 @@ module Pray
61
61
  lockfile_hints,
62
62
  refresh: options.refresh || options.refresh_source_revisions
63
63
  )
64
- source_revisions = git_sources.transform_values(&:revision)
65
64
  source_host_keys = Trust.prepare_source_host_keys(manifest.sources)
66
65
 
67
66
  packages = []
@@ -92,6 +91,7 @@ module Pray
92
91
  local_files = []
93
92
  local_errors = []
94
93
  manifest.local.each do |local|
94
+ next unless Destination.local_compose_embed?(manifest, local)
95
95
  local_files << resolve_local_file(project_root, local)
96
96
  rescue Error => error
97
97
  local_errors << "local #{local.path}: #{error.message}"
@@ -99,6 +99,7 @@ module Pray
99
99
  raise Error.resolution(local_errors.join("\n")) unless local_errors.empty?
100
100
 
101
101
  ResolveDeps.reject_dependency_cycles(packages)
102
+ source_revisions = git_sources.revisions
102
103
 
103
104
  ResolvedProject.new(
104
105
  manifest_path: manifest_path,
@@ -146,11 +147,6 @@ module Pray
146
147
  GitRefresh.resolution_may_benefit_from_git_source_refresh?(error)
147
148
  end
148
149
 
149
- def missing_local_embed_guidance(path)
150
- "Prayfile lists `local \"#{path}\"` but the file does not exist. " \
151
- "Create the file or remove the entry from Prayfile, then run `pray install`."
152
- end
153
-
154
150
  def resolve_package(project_root, sources, git_sources, user_config, declaration, lockfile, offline: false, options: nil)
155
151
  options ||= ResolveOptions.new(offline: offline)
156
152
  root, registry_latest_version = resolve_package_root_with_metadata(
@@ -164,11 +160,7 @@ module Pray
164
160
  "package path #{root.inspect} declares #{spec.name.inspect}, expected #{declaration.name.inspect}"
165
161
  )
166
162
  end
167
- unless Constraint.version_satisfies(spec.version, declaration.constraint)
168
- raise Error.resolution(
169
- "package #{declaration.name} version #{spec.version} does not satisfy constraint #{declaration.constraint}"
170
- )
171
- end
163
+ spec.satisfy_constraint!(declaration.constraint)
172
164
 
173
165
  selected_exports = select_exports(declaration, spec)
174
166
  file_bytes = load_package_file_bytes(root, spec)
@@ -226,7 +218,8 @@ module Pray
226
218
 
227
219
  case source.kind
228
220
  when "path"
229
- return [File.join(project_root, source.url, declaration.name.tr("/", "-")), nil]
221
+ directory = LocalPrayer.path_source_package_directory(source_name, declaration.name)
222
+ return [File.join(project_root, source.url, directory), nil]
230
223
  when "registry", "static index"
231
224
  resolved = Registry.resolve_registry_package_root(
232
225
  project_root,
@@ -277,30 +270,6 @@ module Pray
277
270
  resolve_package_root_with_metadata(project_root, sources, git_sources, user_config, declaration, lockfile).first
278
271
  end
279
272
 
280
- def resolve_local_file(project_root, declaration)
281
- path = File.join(project_root, declaration.path)
282
- unless File.exist?(path)
283
- if declaration.optional
284
- return ResolvedLocalFile.new(
285
- path: path,
286
- manifest_path: declaration.path,
287
- content: "",
288
- position: declaration.position,
289
- optional: true
290
- )
291
- end
292
- raise Error.resolution(missing_local_embed_guidance(declaration.path))
293
- end
294
-
295
- ResolvedLocalFile.new(
296
- path: path,
297
- manifest_path: declaration.path,
298
- content: Hashing.normalize_line_endings(File.read(path)),
299
- position: declaration.position,
300
- optional: declaration.optional
301
- )
302
- end
303
-
304
273
  def find_prayspec_file(root)
305
274
  files = Dir.children(root).filter_map do |entry|
306
275
  path = File.join(root, entry)
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module Resolve
5
+ module_function
6
+
7
+ def missing_local_embed_guidance(path)
8
+ "Prayfile lists `local \"#{path}\"` but the file does not exist. " \
9
+ "Create the file or remove the entry from Prayfile, then run `pray install`."
10
+ end
11
+
12
+ def resolve_local_file(project_root, declaration)
13
+ path = File.join(project_root, declaration.path)
14
+ unless File.exist?(path)
15
+ if declaration.optional
16
+ return ResolvedLocalFile.new(
17
+ path: path,
18
+ manifest_path: declaration.path,
19
+ content: "",
20
+ source_checksum: Hashing.sha256_prefixed(""),
21
+ position: declaration.position,
22
+ optional: true
23
+ )
24
+ end
25
+ raise Error.resolution(missing_local_embed_guidance(declaration.path))
26
+ end
27
+
28
+ content = Hashing.normalize_line_endings(File.read(path))
29
+ ResolvedLocalFile.new(
30
+ path: path,
31
+ manifest_path: declaration.path,
32
+ content: content,
33
+ source_checksum: Hashing.sha256_prefixed(content),
34
+ position: declaration.position,
35
+ optional: declaration.optional
36
+ )
37
+ end
38
+ end
39
+ end
@@ -10,6 +10,11 @@ module Pray
10
10
  namespace = package_namespace(declaration.name)
11
11
  return namespace if namespace && sources.key?(namespace)
12
12
 
13
+ unless declaration.name.include?("/")
14
+ path_sources = sources.select { |_name, source| source.kind == "path" }
15
+ return path_sources.keys.first if path_sources.length == 1
16
+ end
17
+
13
18
  case sources.length
14
19
  when 0 then nil
15
20
  when 1 then sources.keys.first
data/lib/pray/serve.rb CHANGED
@@ -65,7 +65,7 @@ module Pray
65
65
  validate_body_length!(body_length)
66
66
  body = body_length.positive? ? socket.read(body_length) : ""
67
67
 
68
- response = dispatch_request(root, method, path, body)
68
+ response = dispatch_request(root, method, path, body, headers)
69
69
  socket.print(response)
70
70
  end
71
71
 
@@ -94,7 +94,7 @@ module Pray
94
94
  )
95
95
  end
96
96
 
97
- def dispatch_request(root, method, path, body = "")
97
+ def dispatch_request(root, method, path, body = "", headers = {})
98
98
  path = path.split("?", 2).first
99
99
 
100
100
  case [method, path]
@@ -125,7 +125,8 @@ module Pray
125
125
 
126
126
  content_type = content_type_for(file_path)
127
127
  file_body = File.binread(file_path)
128
- ok_response(content_type, file_body)
128
+ status, ranged_body, content_range = ServeRange.apply(200, file_body, headers["range"])
129
+ ranged_response(status, content_type, ranged_body, content_range)
129
130
  end
130
131
 
131
132
  def content_type_for(path)
@@ -137,7 +138,17 @@ module Pray
137
138
  end
138
139
 
139
140
  def ok_response(content_type, body)
140
- "HTTP/1.1 200 OK\r\nContent-Type: #{content_type}\r\nContent-Length: #{body.bytesize}\r\nConnection: close\r\n\r\n#{body}"
141
+ ranged_response(200, content_type, body, nil)
142
+ end
143
+
144
+ def ranged_response(status, content_type, body, content_range)
145
+ reason = {
146
+ 200 => "OK",
147
+ 206 => "Partial Content",
148
+ 416 => "Range Not Satisfiable"
149
+ }.fetch(status, "OK")
150
+ range_header = content_range ? "Content-Range: #{content_range}\r\n" : ""
151
+ "HTTP/1.1 #{status} #{reason}\r\nContent-Type: #{content_type}\r\n#{range_header}Content-Length: #{body.bytesize}\r\nConnection: close\r\n\r\n#{body}"
141
152
  end
142
153
 
143
154
  def html_response(body)
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module ServeRange
5
+ module_function
6
+
7
+ def apply(status, body, range_header)
8
+ return [status, body, nil] if range_header.nil? || range_header.empty?
9
+ return [status, body, nil] unless status == 200 && !body.empty?
10
+
11
+ start_offset, end_offset = parse_byte_range(range_header, body.bytesize)
12
+ unless start_offset
13
+ return [416, "".b, "bytes */#{body.bytesize}"]
14
+ end
15
+
16
+ [
17
+ 206,
18
+ body.b[start_offset..end_offset],
19
+ "bytes #{start_offset}-#{end_offset}/#{body.bytesize}"
20
+ ]
21
+ end
22
+
23
+ def parse_byte_range(header, total)
24
+ range = header.delete_prefix("bytes=")
25
+ start_text, end_text = range.split("-", 2)
26
+ return unless start_text && end_text
27
+
28
+ start_offset = Integer(start_text, exception: false)
29
+ end_offset = Integer(end_text, exception: false)
30
+ return if start_offset.nil? || end_offset.nil?
31
+ return unless start_offset <= end_offset && end_offset < total
32
+
33
+ [start_offset, end_offset]
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+
6
+ module Pray
7
+ TORRENT_MANIFEST_SPEC = "pray-torrent-v1"
8
+ DEFAULT_TORRENT_PIECE_SIZE = 16 * 1024
9
+
10
+ module TorrentManifest
11
+ module_function
12
+
13
+ def path_for(artifact_path)
14
+ "#{artifact_path}.praytorrent.json"
15
+ end
16
+
17
+ def payload(name:, version:, artifact_path:, archive_bytes:, trackers: [])
18
+ {
19
+ "spec" => TORRENT_MANIFEST_SPEC,
20
+ "name" => name,
21
+ "version" => version,
22
+ "artifact_url" => artifact_path,
23
+ "artifact_hash" => Hashing.sha256_prefixed(archive_bytes),
24
+ "piece_size" => DEFAULT_TORRENT_PIECE_SIZE,
25
+ "length" => archive_bytes.bytesize,
26
+ "pieces" => piece_hashes(archive_bytes),
27
+ "sources" => [artifact_path],
28
+ "trackers" => Array(trackers)
29
+ }
30
+ end
31
+
32
+ def bytes(name:, version:, artifact_path:, archive_bytes:, trackers: [])
33
+ JSON.pretty_generate(
34
+ payload(
35
+ name: name,
36
+ version: version,
37
+ artifact_path: artifact_path,
38
+ archive_bytes: archive_bytes,
39
+ trackers: trackers
40
+ )
41
+ )
42
+ end
43
+
44
+ def write(root, name:, version:, artifact_path:, archive_bytes:, settings:)
45
+ return unless settings.allows_torrent?
46
+
47
+ path = File.join(root, path_for(artifact_path))
48
+ FileUtils.mkdir_p(File.dirname(path))
49
+ File.write(
50
+ path,
51
+ bytes(
52
+ name: name,
53
+ version: version,
54
+ artifact_path: artifact_path,
55
+ archive_bytes: archive_bytes,
56
+ trackers: settings.bootstrap_trackers
57
+ )
58
+ )
59
+ end
60
+
61
+ def piece_hashes(archive_bytes)
62
+ length = archive_bytes.bytesize
63
+ return [] if length.zero?
64
+
65
+ hashes = []
66
+ start = 0
67
+ while start < length
68
+ hashes << Hashing.sha256_prefixed(archive_bytes.byteslice(start, DEFAULT_TORRENT_PIECE_SIZE))
69
+ start += DEFAULT_TORRENT_PIECE_SIZE
70
+ end
71
+ hashes
72
+ end
73
+
74
+ def descriptor_present?(root, artifact_path, settings)
75
+ !settings.allows_torrent? || File.file?(File.join(root, path_for(artifact_path)))
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module Upstream
5
+ module_function
6
+
7
+ def path_fork_drift_lines(project, previous, options)
8
+ user_config = Config.load_user_config
9
+ git_sources = GitSources.prepare_git_sources(
10
+ project.project_root,
11
+ project.manifest.sources,
12
+ previous,
13
+ refresh: options.refresh || options.refresh_source_revisions
14
+ )
15
+ sources = Resolve.source_map(project.manifest.sources)
16
+ lines = []
17
+ project.packages.each do |package|
18
+ next unless package.declaration.path
19
+ next unless package.upstream
20
+
21
+ upstream = package.upstream
22
+ resolved = resolve_named(
23
+ project.project_root, sources, git_sources, user_config, previous, options,
24
+ upstream.name, "= #{upstream.version}", upstream.source
25
+ )
26
+ upstream_content = content_file_bytes(resolved.root, resolved.spec)
27
+ local_content = local_content_for_refresh(package.root, package.spec)
28
+ if local_content.empty?
29
+ lines << "#{package.declaration.name} has no content files; run pray install to copy #{upstream.name} #{upstream.version}"
30
+ next
31
+ end
32
+
33
+ overlay_file_changes(local_content, upstream_content).each do |path, change|
34
+ lines << overlay_drift_line(
35
+ package.declaration.name, upstream.name, upstream.version, path, change
36
+ )
37
+ end
38
+ end
39
+ lines
40
+ end
41
+ end
42
+ end
@@ -76,6 +76,31 @@ module Pray
76
76
 
77
77
  "= #{new_version}"
78
78
  end
79
+
80
+ def overlay_file_changes(local_content, upstream_content)
81
+ (local_content.keys + upstream_content.keys).uniq.sort.filter_map do |path|
82
+ local = local_content[path]
83
+ upstream = upstream_content[path]
84
+ if local && upstream && local != upstream
85
+ [path, :changed]
86
+ elsif local && upstream.nil?
87
+ [path, :local_only]
88
+ elsif local.nil? && upstream
89
+ [path, :missing]
90
+ end
91
+ end
92
+ end
93
+
94
+ def overlay_drift_line(fork, upstream_name, upstream_version, path, change)
95
+ case change
96
+ when :changed
97
+ "#{fork} #{path} differs from #{upstream_name} #{upstream_version}"
98
+ when :local_only
99
+ "#{fork} #{path} local"
100
+ else
101
+ "#{fork} #{path} missing from fork"
102
+ end
103
+ end
79
104
  private_class_method :merge_one_content_path, :merge_all_present
80
105
  end
81
106
  end