pray-cli 1.14.0 → 1.15.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 +7 -0
- data/lib/pray/archive.rb +21 -2
- data/lib/pray/cli/commands/update_latest.rb +94 -0
- data/lib/pray/cli/commands/workflow.rb +28 -6
- data/lib/pray/cli/help.rb +5 -1
- data/lib/pray/cli.rb +4 -0
- data/lib/pray/materialize.rb +6 -1
- data/lib/pray/package_spec_render.rb +143 -0
- data/lib/pray/path_safety.rb +8 -4
- data/lib/pray/transaction.rb +7 -0
- data/lib/pray/upstream.rb +0 -13
- data/lib/pray/upstream_latest.rb +88 -0
- data/lib/pray/upstream_merge.rb +81 -0
- data/lib/pray/upstream_refresh.rb +144 -0
- data/lib/pray/version.rb +2 -2
- data/lib/pray.rb +4 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4754a8028d48136e6617f016890123bddb43a4fe27e87a05cc0da63ee5a22106
|
|
4
|
+
data.tar.gz: 5293f312b5cf2b5adb17d2955b21c447aa7ec1e91b374636b475eef30ad68770
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4905aa110a4304e404dfe10e763ec4dee9c30e158659a2390e1af4e9be6aaf07e4cfb909c56023c7c622fdfb4689c250cdb40a632596f4033492bb1cbc7ecdb6
|
|
7
|
+
data.tar.gz: 59a29e48784dd55363e8c2f63683fccc388029f4dbd18599a44f8efedb6b939e123e7203f96b1bc751edf2dbed8f49f30ca40842b3d13bdb3304a4d2a6e6e129
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 1.15.0 (2026-09-15)
|
|
4
|
+
|
|
5
|
+
- Refresh path-fork trees with `pray update` (RFC 0114). Rewrite `spec.upstream` pins with `pray update --latest` when the pin does not admit the latest upstream version. Rewrite Prayfile constraints with `pray update --latest` when they do not admit the registry latest version. `pray update --latest --dry-run` prints the planned rewrite and does not write.
|
|
6
|
+
- Keep `spec.upstream` in the packaged prayspec. Published registry metadata does not copy that pin.
|
|
7
|
+
- Keep a listed package spec in `spec.files` when packing; refuse a repeated or aliased content path in the package archive.
|
|
8
|
+
- Use the current project for a later install after an earlier in-process command in another directory.
|
|
9
|
+
|
|
3
10
|
## 1.14.0 (2026-09-14)
|
|
4
11
|
|
|
5
12
|
- Keep an unchanged package version's artifact, first-publish time, and yank when `pray publish` runs again (RFC 0061).
|
data/lib/pray/archive.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require "open3"
|
|
5
5
|
require "fileutils"
|
|
6
6
|
require_relative "archive_unpack"
|
|
7
|
+
require_relative "path_safety"
|
|
7
8
|
|
|
8
9
|
module Pray
|
|
9
10
|
module Archive
|
|
@@ -11,14 +12,23 @@ module Pray
|
|
|
11
12
|
|
|
12
13
|
def build_package_archive_bytes(package)
|
|
13
14
|
prayspec_path = Resolve.find_prayspec_file(package.root)
|
|
14
|
-
prayspec_name = File.basename(prayspec_path)
|
|
15
|
+
prayspec_name = PathSafety.validate_archive_member_path!(File.basename(prayspec_path))
|
|
15
16
|
metadata = package_metadata_json(package)
|
|
17
|
+
written_paths = Set.new
|
|
18
|
+
record_archive_path(written_paths, "metadata.json", prayspec_name)
|
|
19
|
+
record_archive_path(written_paths, prayspec_name, prayspec_name)
|
|
20
|
+
package.spec.files.each do |file|
|
|
21
|
+
record_archive_path(written_paths, file, prayspec_name)
|
|
22
|
+
end
|
|
16
23
|
|
|
17
24
|
Dir.mktmpdir("pray-package-") do |staging|
|
|
18
25
|
File.write(File.join(staging, "metadata.json"), metadata)
|
|
19
26
|
File.write(File.join(staging, prayspec_name), File.binread(prayspec_path))
|
|
20
27
|
package.spec.files.each do |file|
|
|
21
|
-
|
|
28
|
+
member = PathSafety.validate_archive_member_path!(file)
|
|
29
|
+
next if member == prayspec_name
|
|
30
|
+
|
|
31
|
+
destination = File.join(staging, member)
|
|
22
32
|
FileUtils.mkdir_p(File.dirname(destination))
|
|
23
33
|
File.binwrite(destination, File.binread(File.join(package.root, file)))
|
|
24
34
|
end
|
|
@@ -66,5 +76,14 @@ module Pray
|
|
|
66
76
|
def with_binary_process_encoding(&block)
|
|
67
77
|
ArchiveUnpack.with_binary_process_encoding(&block)
|
|
68
78
|
end
|
|
79
|
+
|
|
80
|
+
def record_archive_path(written_paths, path, auto_included_prayspec)
|
|
81
|
+
normalized = PathSafety.validate_archive_member_path!(path)
|
|
82
|
+
return if written_paths.add?(normalized)
|
|
83
|
+
return if normalized == auto_included_prayspec
|
|
84
|
+
|
|
85
|
+
raise Error.integrity("duplicate package archive path: #{normalized}")
|
|
86
|
+
end
|
|
87
|
+
private_class_method :record_archive_path
|
|
69
88
|
end
|
|
70
89
|
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pray
|
|
4
|
+
module CLI
|
|
5
|
+
def update_latest_command(package, dry_run:, offline:)
|
|
6
|
+
path = manifest_path
|
|
7
|
+
original_text = Pray.read_manifest_text(path)
|
|
8
|
+
manifest_text = original_text
|
|
9
|
+
preview_options = ResolveOptions.new(
|
|
10
|
+
offline: offline,
|
|
11
|
+
refresh: true,
|
|
12
|
+
refresh_source_revisions: true,
|
|
13
|
+
ignore_locked_versions: true
|
|
14
|
+
)
|
|
15
|
+
project = resolve_current_project(preview_options)
|
|
16
|
+
if package && project.manifest.packages.none? { |entry| entry.name == package }
|
|
17
|
+
raise Error.manifest("package #{package} not found")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
manifest_updates = []
|
|
21
|
+
project.packages.each do |resolved|
|
|
22
|
+
next if package && resolved.declaration.name != package
|
|
23
|
+
next unless resolved.registry_latest_version
|
|
24
|
+
next if Constraint.version_satisfies(resolved.registry_latest_version, resolved.declaration.constraint)
|
|
25
|
+
|
|
26
|
+
new_constraint = Constraint.latest_constraint_for_package(
|
|
27
|
+
resolved.declaration.constraint,
|
|
28
|
+
resolved.registry_latest_version
|
|
29
|
+
)
|
|
30
|
+
unless Constraint.version_satisfies(resolved.registry_latest_version, new_constraint)
|
|
31
|
+
raise Error.resolution(
|
|
32
|
+
"derived constraint #{new_constraint} does not admit registry latest #{resolved.registry_latest_version} for #{resolved.declaration.name}"
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
manifest_updates << {
|
|
37
|
+
name: resolved.declaration.name,
|
|
38
|
+
from_constraint: resolved.declaration.constraint,
|
|
39
|
+
to_constraint: new_constraint,
|
|
40
|
+
registry_latest_version: resolved.registry_latest_version
|
|
41
|
+
}
|
|
42
|
+
updated = resolved.declaration.dup
|
|
43
|
+
updated.constraint = new_constraint
|
|
44
|
+
manifest_text = Pray.replace_package_declaration(manifest_text, updated)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
previous = File.exist?(lockfile_path) ? Pray.read_lockfile(lockfile_path) : nil
|
|
48
|
+
upstream_plans = Upstream.plan_path_upstream_latest_constraints(
|
|
49
|
+
project, previous, package, preview_options
|
|
50
|
+
)
|
|
51
|
+
print_latest_constraint_plans(manifest_updates, upstream_plans)
|
|
52
|
+
|
|
53
|
+
return if dry_run
|
|
54
|
+
|
|
55
|
+
Upstream.apply_path_upstream_latest_constraints(upstream_plans)
|
|
56
|
+
Transaction.write_file(path, manifest_text) if manifest_text != original_text
|
|
57
|
+
unlocked = package ? Set[package] : Set.new
|
|
58
|
+
options = ResolveOptions.new(
|
|
59
|
+
offline: offline,
|
|
60
|
+
refresh: true,
|
|
61
|
+
refresh_source_revisions: true,
|
|
62
|
+
ignore_locked_versions: package.nil?,
|
|
63
|
+
unlocked_packages: unlocked
|
|
64
|
+
)
|
|
65
|
+
current = resolve_current_project(options)
|
|
66
|
+
Upstream.apply_path_upstream_refreshes(current, previous, package, options)
|
|
67
|
+
install_command(
|
|
68
|
+
{
|
|
69
|
+
locked: false,
|
|
70
|
+
frozen: false,
|
|
71
|
+
offline: offline,
|
|
72
|
+
refresh: true,
|
|
73
|
+
ignore_locked_versions: package.nil?,
|
|
74
|
+
unlocked_packages: unlocked
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def print_latest_constraint_plans(manifest_updates, upstream_plans)
|
|
80
|
+
if manifest_updates.empty? && upstream_plans.empty?
|
|
81
|
+
puts "All package constraints already allow latest versions"
|
|
82
|
+
return
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
manifest_updates.each do |update|
|
|
86
|
+
puts "Prayfile: #{update[:name]} constraint #{update[:from_constraint]} -> #{update[:to_constraint]} " \
|
|
87
|
+
"(registry latest #{update[:registry_latest_version]})"
|
|
88
|
+
end
|
|
89
|
+
upstream_plans.each do |plan|
|
|
90
|
+
puts "#{plan.package_name} upstream #{plan.current_constraint} -> #{plan.new_constraint} (latest #{plan.latest_version})"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -10,7 +10,9 @@ module Pray
|
|
|
10
10
|
frozen: flags[:frozen],
|
|
11
11
|
locked: flags[:locked],
|
|
12
12
|
offline: flags[:offline],
|
|
13
|
-
refresh: flags[:refresh]
|
|
13
|
+
refresh: flags[:refresh],
|
|
14
|
+
ignore_locked_versions: flags[:ignore_locked_versions],
|
|
15
|
+
unlocked_packages: flags[:unlocked_packages]
|
|
14
16
|
)
|
|
15
17
|
end
|
|
16
18
|
|
|
@@ -84,10 +86,10 @@ module Pray
|
|
|
84
86
|
end
|
|
85
87
|
|
|
86
88
|
def update_command(arguments)
|
|
87
|
-
if arguments.
|
|
89
|
+
if arguments.include?("--json") || (arguments.include?("--dry-run") && !arguments.include?("--latest"))
|
|
88
90
|
raise Error.unsupported("this Ruby CLI does not support --dry-run or --json for update; use `pray plan` to preview the current Prayfile")
|
|
89
91
|
end
|
|
90
|
-
if arguments.
|
|
92
|
+
if arguments.include?("--major")
|
|
91
93
|
raise Error.unsupported(
|
|
92
94
|
"to update beyond the current constraint, edit the package version in Prayfile, then run `pray update`"
|
|
93
95
|
)
|
|
@@ -100,9 +102,29 @@ module Pray
|
|
|
100
102
|
raise Error.manifest("package #{package} not found")
|
|
101
103
|
end
|
|
102
104
|
end
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
if arguments.include?("--latest")
|
|
106
|
+
return update_latest_command(package, dry_run: arguments.include?("--dry-run"), offline: offline)
|
|
107
|
+
end
|
|
108
|
+
unlocked = package ? Set[package] : Set.new
|
|
109
|
+
options = ResolveOptions.new(
|
|
110
|
+
offline: offline,
|
|
111
|
+
refresh: true,
|
|
112
|
+
refresh_source_revisions: true,
|
|
113
|
+
ignore_locked_versions: package.nil?,
|
|
114
|
+
unlocked_packages: unlocked
|
|
115
|
+
)
|
|
116
|
+
current = resolve_current_project(options)
|
|
117
|
+
Upstream.apply_path_upstream_refreshes(current, current.previous_lockfile, package, options)
|
|
118
|
+
install_command(
|
|
119
|
+
{
|
|
120
|
+
locked: false,
|
|
121
|
+
frozen: false,
|
|
122
|
+
offline: offline,
|
|
123
|
+
refresh: true,
|
|
124
|
+
ignore_locked_versions: package.nil?,
|
|
125
|
+
unlocked_packages: unlocked
|
|
126
|
+
}
|
|
127
|
+
)
|
|
106
128
|
end
|
|
107
129
|
|
|
108
130
|
def unlock_command(name)
|
data/lib/pray/cli/help.rb
CHANGED
|
@@ -101,7 +101,11 @@ module Pray
|
|
|
101
101
|
"update" => <<~TEXT.strip,
|
|
102
102
|
refresh package versions within constraints
|
|
103
103
|
|
|
104
|
-
Usage: pray update [package]
|
|
104
|
+
Usage: pray update [package] [--latest] [--latest --dry-run]
|
|
105
|
+
|
|
106
|
+
--latest adjusts constraints to allow the latest package versions.
|
|
107
|
+
--latest also rewrites exact spec.upstream pins in path packages, then refreshes those trees.
|
|
108
|
+
--latest --dry-run prints the planned rewrite and does not write.
|
|
105
109
|
TEXT
|
|
106
110
|
"plan" => <<~TEXT.strip,
|
|
107
111
|
preview install/apply changes
|
data/lib/pray/cli.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative "cli/suggest"
|
|
|
10
10
|
require_relative "cli/helpers"
|
|
11
11
|
require_relative "cli/commands/init"
|
|
12
12
|
require_relative "cli/commands/workflow"
|
|
13
|
+
require_relative "cli/commands/update_latest"
|
|
13
14
|
require_relative "cli/commands/packages"
|
|
14
15
|
require_relative "cli/commands/distribution"
|
|
15
16
|
require_relative "cli/commands/trust"
|
|
@@ -22,6 +23,7 @@ module Pray
|
|
|
22
23
|
LOCKFILE_PATH = "Prayfile.lock"
|
|
23
24
|
|
|
24
25
|
def run(arguments)
|
|
26
|
+
previous_context = Invocation.context
|
|
25
27
|
arguments = arguments.dup
|
|
26
28
|
if arguments.delete("--no-input")
|
|
27
29
|
ENV["PRAY_NO_INPUT"] = "1"
|
|
@@ -35,6 +37,8 @@ module Pray
|
|
|
35
37
|
else
|
|
36
38
|
dispatch(command)
|
|
37
39
|
end
|
|
40
|
+
ensure
|
|
41
|
+
Invocation.context = previous_context
|
|
38
42
|
end
|
|
39
43
|
|
|
40
44
|
def maybe_print_help(arguments)
|
data/lib/pray/materialize.rb
CHANGED
|
@@ -13,7 +13,9 @@ module Pray
|
|
|
13
13
|
frozen: false,
|
|
14
14
|
locked: false,
|
|
15
15
|
offline: false,
|
|
16
|
-
refresh: false
|
|
16
|
+
refresh: false,
|
|
17
|
+
ignore_locked_versions: false,
|
|
18
|
+
unlocked_packages: nil
|
|
17
19
|
)
|
|
18
20
|
context = Invocation.invocation_context
|
|
19
21
|
manifest_path = File.expand_path(manifest_path || context.manifest_path)
|
|
@@ -25,6 +27,9 @@ module Pray
|
|
|
25
27
|
options = ResolveOptions.new(
|
|
26
28
|
offline: offline,
|
|
27
29
|
refresh: refresh,
|
|
30
|
+
refresh_source_revisions: refresh,
|
|
31
|
+
ignore_locked_versions: ignore_locked_versions,
|
|
32
|
+
unlocked_packages: unlocked_packages ? Set.new(unlocked_packages) : Set.new,
|
|
28
33
|
environment: context.environment
|
|
29
34
|
)
|
|
30
35
|
allow_git_refresh_fallback = !locked && !frozen
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pray
|
|
4
|
+
module PackageSpecRender
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def fork_spec_after_refresh(local, new_upstream, local_prayspec_file, clean_replica, merged_content_paths)
|
|
8
|
+
spec = local.dup
|
|
9
|
+
spec.files = [local_prayspec_file, *merged_content_paths]
|
|
10
|
+
if clean_replica
|
|
11
|
+
spec.exports = new_upstream.exports.dup
|
|
12
|
+
spec.templates = new_upstream.templates.dup
|
|
13
|
+
end
|
|
14
|
+
if spec.upstream
|
|
15
|
+
spec.upstream = PackageUpstream.new(
|
|
16
|
+
name: new_upstream.name,
|
|
17
|
+
constraint: Upstream.next_upstream_constraint(spec.upstream.constraint, new_upstream.version)
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
spec
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def render_package_spec(spec)
|
|
24
|
+
lines = ["Package::Specification.new do |spec|"]
|
|
25
|
+
push_identity_fields(lines, spec)
|
|
26
|
+
push_content_fields(lines, spec)
|
|
27
|
+
push_relation_fields(lines, spec)
|
|
28
|
+
lines << "end"
|
|
29
|
+
lines << ""
|
|
30
|
+
lines.join("\n")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def push_identity_fields(lines, spec)
|
|
34
|
+
push_assignment(lines, "name", spec.name)
|
|
35
|
+
push_assignment(lines, "version", spec.version)
|
|
36
|
+
push_optional(lines, "summary", spec.summary)
|
|
37
|
+
push_optional(lines, "description", spec.description)
|
|
38
|
+
push_array(lines, "authors", spec.authors) unless spec.authors.nil? || spec.authors.empty?
|
|
39
|
+
push_optional(lines, "license", spec.license)
|
|
40
|
+
push_optional(lines, "homepage", spec.homepage)
|
|
41
|
+
push_optional(lines, "source_code_uri", spec.source_code_uri)
|
|
42
|
+
push_optional(lines, "changelog_uri", spec.changelog_uri)
|
|
43
|
+
push_optional(lines, "prayfile_version", spec.prayfile_version)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def push_content_fields(lines, spec)
|
|
47
|
+
push_array(lines, "files", spec.files)
|
|
48
|
+
push_exports(lines, spec.exports)
|
|
49
|
+
push_named_paths(lines, "skills", spec.skills)
|
|
50
|
+
push_named_paths(lines, "templates", spec.templates)
|
|
51
|
+
push_string_map(lines, "adapters", spec.adapters)
|
|
52
|
+
push_array(lines, "targets", spec.targets) unless spec.targets.nil? || spec.targets.empty?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def push_relation_fields(lines, spec)
|
|
56
|
+
(spec.dependencies || []).each do |dependency|
|
|
57
|
+
method = dependency.optional ? "add_optional_dependency" : "add_dependency"
|
|
58
|
+
lines << " spec.#{method} #{quote(dependency.name)}, #{quote(dependency.constraint)}"
|
|
59
|
+
end
|
|
60
|
+
unless spec.metadata.nil? || spec.metadata.empty?
|
|
61
|
+
lines << " spec.metadata = #{literal_map(spec.metadata)}"
|
|
62
|
+
end
|
|
63
|
+
return unless spec.upstream
|
|
64
|
+
|
|
65
|
+
lines << " spec.upstream #{quote(spec.upstream.name)}, #{quote(spec.upstream.constraint)}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def push_assignment(lines, field, value)
|
|
69
|
+
lines << " spec.#{field} = #{quote(value)}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def push_optional(lines, field, value)
|
|
73
|
+
push_assignment(lines, field, value) unless value.nil?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def push_array(lines, field, values)
|
|
77
|
+
lines << " spec.#{field} = [#{string_array(values)}]"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def push_exports(lines, exports)
|
|
81
|
+
return if exports.nil? || exports.empty?
|
|
82
|
+
|
|
83
|
+
lines << " spec.exports = {"
|
|
84
|
+
exports.each do |name, export|
|
|
85
|
+
fields = ["type: #{quote(export.kind)}", "path: #{quote(export.path)}"]
|
|
86
|
+
fields << "summary: #{quote(export.summary)}" if export.summary
|
|
87
|
+
fields << "only: [#{string_array(export.only)}]" unless export.only.nil? || export.only.empty?
|
|
88
|
+
fields << "except: [#{string_array(export.except)}]" unless export.except.nil? || export.except.empty?
|
|
89
|
+
fields << "default_path: #{quote(export.default_path)}" if export.default_path
|
|
90
|
+
lines << " #{quote(name)} => { #{fields.join(", ")} },"
|
|
91
|
+
end
|
|
92
|
+
lines << " }"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def push_named_paths(lines, field, entries)
|
|
96
|
+
return if entries.nil? || entries.empty?
|
|
97
|
+
|
|
98
|
+
lines << " spec.#{field} = {"
|
|
99
|
+
entries.each do |name, entry|
|
|
100
|
+
summary = entry.summary ? ", summary: #{quote(entry.summary)}" : ""
|
|
101
|
+
lines << " #{quote(name)} => { path: #{quote(entry.path)}#{summary} },"
|
|
102
|
+
end
|
|
103
|
+
lines << " }"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def push_string_map(lines, field, entries)
|
|
107
|
+
return if entries.nil? || entries.empty?
|
|
108
|
+
|
|
109
|
+
values = entries.map { |name, value| "#{quote(name)} => #{quote(value)}" }.join(", ")
|
|
110
|
+
lines << " spec.#{field} = { #{values} }"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def string_array(values)
|
|
114
|
+
Array(values).map { |value| quote(value) }.join(", ")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def quote(value)
|
|
118
|
+
escaped = value.to_s.gsub("\\", "\\\\").gsub('"', '\\"').gsub("\n", "\\n").gsub("\r", "\\r").gsub("\t", "\\t")
|
|
119
|
+
%("#{escaped}")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def literal_map(entries)
|
|
123
|
+
values = entries.map { |name, value| "#{quote(name)} => #{literal(value)}" }.join(", ")
|
|
124
|
+
"{ #{values} }"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def literal(value)
|
|
128
|
+
case value.kind
|
|
129
|
+
when :string then quote(value.value)
|
|
130
|
+
when :symbol then ":#{value.value}"
|
|
131
|
+
when :bool then value.value.to_s
|
|
132
|
+
when :null then "nil"
|
|
133
|
+
when :integer then value.value.to_s
|
|
134
|
+
when :array then "[#{value.value.map { |entry| literal(entry) }.join(", ")}]"
|
|
135
|
+
when :map then literal_map(value.value)
|
|
136
|
+
else quote(value.value)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
private_class_method :push_identity_fields, :push_content_fields, :push_relation_fields,
|
|
140
|
+
:push_assignment, :push_optional, :push_array, :push_exports,
|
|
141
|
+
:push_named_paths, :push_string_map, :string_array, :quote, :literal_map, :literal
|
|
142
|
+
end
|
|
143
|
+
end
|
data/lib/pray/path_safety.rb
CHANGED
|
@@ -39,20 +39,24 @@ module Pray
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def validate_archive_member_path!(path)
|
|
42
|
-
|
|
43
|
-
if
|
|
42
|
+
text = path.to_s.tr("\\", "/")
|
|
43
|
+
if text.empty? || Pathname.new(text).absolute? || text.start_with?("/")
|
|
44
44
|
raise Error.integrity("package path must be relative: #{path}")
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
parts = []
|
|
48
|
+
text.split("/").each do |part|
|
|
48
49
|
next if part.empty? || part == "."
|
|
49
50
|
|
|
50
51
|
if part == ".." || part.include?("\0")
|
|
51
52
|
raise Error.integrity("package path escapes package root: #{path}")
|
|
52
53
|
end
|
|
54
|
+
|
|
55
|
+
parts << part
|
|
53
56
|
end
|
|
57
|
+
raise Error.integrity("package path must be relative: #{path}") if parts.empty?
|
|
54
58
|
|
|
55
|
-
|
|
59
|
+
parts.join("/")
|
|
56
60
|
end
|
|
57
61
|
|
|
58
62
|
def validate_project_relative_path!(value)
|
data/lib/pray/transaction.rb
CHANGED
|
@@ -57,5 +57,12 @@ module Pray
|
|
|
57
57
|
|
|
58
58
|
journal.replace(path, TransactionJournal.snapshot(path), bytes.b)
|
|
59
59
|
end
|
|
60
|
+
|
|
61
|
+
def remove_file(path)
|
|
62
|
+
journal = Thread.current[:pray_write_transaction]
|
|
63
|
+
return File.delete(path) unless journal
|
|
64
|
+
|
|
65
|
+
journal.replace(path, TransactionJournal.snapshot(path), nil)
|
|
66
|
+
end
|
|
60
67
|
end
|
|
61
68
|
end
|
data/lib/pray/upstream.rb
CHANGED
|
@@ -4,19 +4,6 @@ module Pray
|
|
|
4
4
|
module Upstream
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
|
-
def ensure_update_supported!(packages, selected = nil)
|
|
8
|
-
unsupported = packages.find do |package|
|
|
9
|
-
package.declaration.path && package.spec.upstream &&
|
|
10
|
-
(selected.nil? || package.declaration.name == selected)
|
|
11
|
-
end
|
|
12
|
-
return unless unsupported
|
|
13
|
-
|
|
14
|
-
raise Error.unsupported(
|
|
15
|
-
"this installation cannot refresh upstream package #{unsupported.declaration.name}; " \
|
|
16
|
-
"install pray with Cargo and retry"
|
|
17
|
-
)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
7
|
def to_lock_hash(entry)
|
|
21
8
|
hash = {
|
|
22
9
|
"name" => entry.name,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pray
|
|
4
|
+
module Upstream
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
PathUpstreamLatestConstraint = Struct.new(
|
|
8
|
+
:package_name, :upstream_name, :current_constraint, :latest_version, :new_constraint, :package_root,
|
|
9
|
+
keyword_init: true
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
def plan_path_upstream_latest_constraints(project, previous, selected, options)
|
|
13
|
+
user_config = Config.load_user_config
|
|
14
|
+
git_sources = GitSources.prepare_git_sources(
|
|
15
|
+
project.project_root,
|
|
16
|
+
project.manifest.sources,
|
|
17
|
+
previous,
|
|
18
|
+
refresh: options.refresh || options.refresh_source_revisions
|
|
19
|
+
)
|
|
20
|
+
sources = Resolve.source_map(project.manifest.sources)
|
|
21
|
+
plans = []
|
|
22
|
+
project.packages.each do |package|
|
|
23
|
+
next if selected && package.declaration.name != selected
|
|
24
|
+
|
|
25
|
+
plan = plan_one_path_upstream(project, package, sources, git_sources, user_config, previous, options)
|
|
26
|
+
plans << plan if plan
|
|
27
|
+
end
|
|
28
|
+
plans
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def apply_path_upstream_latest_constraints(plans)
|
|
32
|
+
plans.each do |plan|
|
|
33
|
+
spec_path = Resolve.find_prayspec_file(plan.package_root)
|
|
34
|
+
spec = Pray.parse_package_spec(File.read(spec_path))
|
|
35
|
+
unless spec.upstream
|
|
36
|
+
raise Error.resolution("package #{plan.package_name} has no upstream pin")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
spec.upstream = PackageUpstream.new(
|
|
40
|
+
name: spec.upstream.name,
|
|
41
|
+
constraint: plan.new_constraint
|
|
42
|
+
)
|
|
43
|
+
Transaction.write_file(spec_path, PackageSpecRender.render_package_spec(spec))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def latest_spec_upstream_constraint(current, latest_version)
|
|
48
|
+
derived = Constraint.latest_constraint_for_package(current, latest_version)
|
|
49
|
+
return "= #{latest_version}" if derived.start_with?("=")
|
|
50
|
+
|
|
51
|
+
derived
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def plan_one_path_upstream(project, package, sources, git_sources, user_config, lockfile, options)
|
|
55
|
+
upstream = package.spec.upstream
|
|
56
|
+
return unless upstream && package.declaration.path
|
|
57
|
+
|
|
58
|
+
source = ResolveSource.implied_source_name(ManifestPackage.new(name: upstream.name), sources)
|
|
59
|
+
resolved = Resolve.resolve_package(
|
|
60
|
+
project.project_root,
|
|
61
|
+
sources,
|
|
62
|
+
git_sources,
|
|
63
|
+
user_config,
|
|
64
|
+
ManifestPackage.new(name: upstream.name, constraint: "*", source: source),
|
|
65
|
+
lockfile,
|
|
66
|
+
options: options
|
|
67
|
+
)
|
|
68
|
+
return if Constraint.version_satisfies(resolved.spec.version, upstream.constraint)
|
|
69
|
+
|
|
70
|
+
new_constraint = latest_spec_upstream_constraint(upstream.constraint, resolved.spec.version)
|
|
71
|
+
unless Constraint.version_satisfies(resolved.spec.version, new_constraint)
|
|
72
|
+
raise Error.resolution(
|
|
73
|
+
"derived constraint #{new_constraint} does not admit latest #{resolved.spec.version} for #{package.declaration.name}"
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
PathUpstreamLatestConstraint.new(
|
|
78
|
+
package_name: package.declaration.name,
|
|
79
|
+
upstream_name: upstream.name,
|
|
80
|
+
current_constraint: upstream.constraint,
|
|
81
|
+
latest_version: resolved.spec.version,
|
|
82
|
+
new_constraint: new_constraint,
|
|
83
|
+
package_root: package.root
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
private_class_method :plan_one_path_upstream
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pray
|
|
4
|
+
module Upstream
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def identity_path?(path)
|
|
8
|
+
File.extname(path.to_s) == ".prayspec"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def content_paths(files)
|
|
12
|
+
files.reject { |path| identity_path?(path) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def clean_replica?(old_content, local_content)
|
|
16
|
+
local_content == old_content
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def try_merge_content_files(old_content, new_content, local_content)
|
|
20
|
+
return [new_content.dup, []] if clean_replica?(old_content, local_content)
|
|
21
|
+
|
|
22
|
+
merged = {}
|
|
23
|
+
conflicts = []
|
|
24
|
+
(old_content.keys + new_content.keys + local_content.keys).uniq.sort.each do |path|
|
|
25
|
+
result = merge_one_content_path(old_content[path], new_content[path], local_content[path])
|
|
26
|
+
if result == :conflict
|
|
27
|
+
conflicts << path
|
|
28
|
+
elsif result != :omit
|
|
29
|
+
merged[path] = result
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
[merged, conflicts]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def merge_one_content_path(old, new, local)
|
|
36
|
+
case [old.nil?, new.nil?, local.nil?]
|
|
37
|
+
when [false, false, false]
|
|
38
|
+
merge_all_present(old, new, local)
|
|
39
|
+
when [false, true, false]
|
|
40
|
+
(local == old) ? :omit : :conflict
|
|
41
|
+
when [false, false, true]
|
|
42
|
+
(old == new) ? :omit : new
|
|
43
|
+
when [true, false, true]
|
|
44
|
+
new
|
|
45
|
+
when [true, true, false]
|
|
46
|
+
local
|
|
47
|
+
when [true, false, false]
|
|
48
|
+
(local == new) ? new : :conflict
|
|
49
|
+
else
|
|
50
|
+
:conflict
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def merge_all_present(old, new, local)
|
|
55
|
+
return new if local == new || local == old
|
|
56
|
+
return local if old == new
|
|
57
|
+
|
|
58
|
+
:conflict
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def merge_content_files(old_content, new_content, local_content)
|
|
62
|
+
merged, conflicts = try_merge_content_files(old_content, new_content, local_content)
|
|
63
|
+
return merged if conflicts.empty?
|
|
64
|
+
|
|
65
|
+
raise Error.resolution("upstream merge conflict in #{conflicts.join(", ")}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def merge_conflict_message(fork, upstream_name, old_version, new_version, paths)
|
|
69
|
+
"upstream merge conflict in #{fork} while refreshing #{upstream_name} " \
|
|
70
|
+
"#{old_version} to #{new_version}: #{paths.join(", ")}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def next_upstream_constraint(current, new_version)
|
|
74
|
+
trimmed = current.to_s.strip
|
|
75
|
+
return current if trimmed == "*" || trimmed.start_with?("~>", "^", ">=", ">", "<=", "<")
|
|
76
|
+
|
|
77
|
+
"= #{new_version}"
|
|
78
|
+
end
|
|
79
|
+
private_class_method :merge_one_content_path, :merge_all_present
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pray
|
|
4
|
+
module Upstream
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def apply_path_upstream_refreshes(project, previous, selected, 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
|
+
changed = false
|
|
17
|
+
project.packages.each do |package|
|
|
18
|
+
next if selected && package.declaration.name != selected
|
|
19
|
+
next unless apply_one_path_upstream(project, package, previous, sources, git_sources, user_config, options)
|
|
20
|
+
|
|
21
|
+
changed = true
|
|
22
|
+
end
|
|
23
|
+
changed
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def apply_one_path_upstream(project, package, previous, sources, git_sources, user_config, options)
|
|
27
|
+
new_upstream = package.upstream
|
|
28
|
+
return false unless new_upstream && package.declaration.path
|
|
29
|
+
|
|
30
|
+
old_upstream = previous&.package&.find { |entry| entry.name == package.declaration.name }&.upstream
|
|
31
|
+
return false unless old_upstream
|
|
32
|
+
return false if old_upstream.version == new_upstream.version && old_upstream.tree_hash == new_upstream.tree_hash
|
|
33
|
+
|
|
34
|
+
old_package, new_package = resolve_upstream_pair(
|
|
35
|
+
project, previous, sources, git_sources, user_config, options, old_upstream, new_upstream
|
|
36
|
+
)
|
|
37
|
+
old_content, local_content, merged = merge_fork_content(package, old_upstream, new_upstream, old_package, new_package)
|
|
38
|
+
write_content_files(package.root, old_content, merged)
|
|
39
|
+
write_refreshed_spec(package, new_package, old_content, local_content, merged)
|
|
40
|
+
true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def resolve_upstream_pair(project, previous, sources, git_sources, user_config, options, old_upstream, new_upstream)
|
|
44
|
+
old_package = resolve_named(
|
|
45
|
+
project.project_root, sources, git_sources, user_config, previous, options,
|
|
46
|
+
old_upstream.name, "= #{old_upstream.version}", old_upstream.source
|
|
47
|
+
)
|
|
48
|
+
resolved_old = LockedUpstream.new(
|
|
49
|
+
name: old_upstream.name,
|
|
50
|
+
version: old_package.spec.version,
|
|
51
|
+
source: old_upstream.source,
|
|
52
|
+
tree_hash: old_package.tree_hash,
|
|
53
|
+
artifact_hash: old_package.artifact_hash
|
|
54
|
+
)
|
|
55
|
+
ensure_locked_match!(old_upstream, resolved_old)
|
|
56
|
+
new_package = resolve_named(
|
|
57
|
+
project.project_root, sources, git_sources, user_config, previous, options,
|
|
58
|
+
new_upstream.name, "= #{new_upstream.version}", new_upstream.source
|
|
59
|
+
)
|
|
60
|
+
[old_package, new_package]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def merge_fork_content(package, old_upstream, new_upstream, old_package, new_package)
|
|
64
|
+
old_content = content_file_bytes(old_package.root, old_package.spec)
|
|
65
|
+
new_content = content_file_bytes(new_package.root, new_package.spec)
|
|
66
|
+
local_content = content_file_bytes(package.root, package.spec)
|
|
67
|
+
merged, conflicts = try_merge_content_files(old_content, new_content, local_content)
|
|
68
|
+
unless conflicts.empty?
|
|
69
|
+
raise Error.resolution(
|
|
70
|
+
merge_conflict_message(
|
|
71
|
+
package.declaration.name, old_upstream.name, old_upstream.version, new_upstream.version, conflicts
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
[old_content, local_content, merged]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def write_refreshed_spec(package, new_package, old_content, local_content, merged)
|
|
79
|
+
spec_path = Resolve.find_prayspec_file(package.root)
|
|
80
|
+
updated = PackageSpecRender.fork_spec_after_refresh(
|
|
81
|
+
package.spec,
|
|
82
|
+
new_package.spec,
|
|
83
|
+
File.basename(spec_path),
|
|
84
|
+
clean_replica?(old_content, local_content),
|
|
85
|
+
merged.keys
|
|
86
|
+
)
|
|
87
|
+
Transaction.write_file(spec_path, PackageSpecRender.render_package_spec(updated))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def resolve_named(project_root, sources, git_sources, user_config, lockfile, options, name, constraint, source)
|
|
91
|
+
Resolve.resolve_package(
|
|
92
|
+
project_root,
|
|
93
|
+
sources,
|
|
94
|
+
git_sources,
|
|
95
|
+
user_config,
|
|
96
|
+
ManifestPackage.new(name: name, constraint: constraint, source: source),
|
|
97
|
+
lockfile,
|
|
98
|
+
options: options
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def content_file_bytes(root, spec)
|
|
103
|
+
paths = content_paths(spec.files)
|
|
104
|
+
if paths.length > ResourceLimits::MAX_ARCHIVE_ENTRIES
|
|
105
|
+
raise Error.integrity("package content exceeds #{ResourceLimits::MAX_ARCHIVE_ENTRIES} files")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
files = {}
|
|
109
|
+
total_bytes = 0
|
|
110
|
+
paths.each do |relative|
|
|
111
|
+
PathSafety.validate_archive_member_path!(relative)
|
|
112
|
+
path = File.join(root, relative)
|
|
113
|
+
raise Error.integrity("package file missing: #{relative}") unless File.file?(path)
|
|
114
|
+
|
|
115
|
+
size = File.size(path)
|
|
116
|
+
if size > ResourceLimits::MAX_ARCHIVE_ENTRY_BYTES
|
|
117
|
+
raise Error.integrity("package file exceeds #{ResourceLimits::MAX_ARCHIVE_ENTRY_BYTES} bytes: #{relative}")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
total_bytes += size
|
|
121
|
+
if total_bytes > ResourceLimits::MAX_ARCHIVE_TOTAL_BYTES
|
|
122
|
+
raise Error.integrity("package content exceeds #{ResourceLimits::MAX_ARCHIVE_TOTAL_BYTES} bytes")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
files[relative] = File.binread(path)
|
|
126
|
+
end
|
|
127
|
+
files
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def write_content_files(root, old_content, merged)
|
|
131
|
+
(old_content.keys + merged.keys).each { |path| PathSafety.validate_archive_member_path!(path) }
|
|
132
|
+
old_content.each_key do |path|
|
|
133
|
+
next if merged.key?(path)
|
|
134
|
+
|
|
135
|
+
Transaction.remove_file(File.join(root, path))
|
|
136
|
+
end
|
|
137
|
+
merged.each do |relative, bytes|
|
|
138
|
+
Transaction.write_file(File.join(root, relative), bytes)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
private_class_method :apply_one_path_upstream, :resolve_upstream_pair, :merge_fork_content,
|
|
142
|
+
:write_refreshed_spec, :resolve_named
|
|
143
|
+
end
|
|
144
|
+
end
|
data/lib/pray/version.rb
CHANGED
data/lib/pray.rb
CHANGED
|
@@ -25,6 +25,10 @@ require_relative "pray/invocation"
|
|
|
25
25
|
require_relative "pray/git_refresh"
|
|
26
26
|
require_relative "pray/resolve"
|
|
27
27
|
require_relative "pray/upstream"
|
|
28
|
+
require_relative "pray/upstream_merge"
|
|
29
|
+
require_relative "pray/package_spec_render"
|
|
30
|
+
require_relative "pray/upstream_refresh"
|
|
31
|
+
require_relative "pray/upstream_latest"
|
|
28
32
|
require_relative "pray/compose_dest"
|
|
29
33
|
require_relative "pray/render_patch"
|
|
30
34
|
require_relative "pray/render"
|
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.
|
|
4
|
+
version: 1.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Makarov
|
|
@@ -113,6 +113,7 @@ files:
|
|
|
113
113
|
- lib/pray/cli/commands/meta.rb
|
|
114
114
|
- lib/pray/cli/commands/packages.rb
|
|
115
115
|
- lib/pray/cli/commands/trust.rb
|
|
116
|
+
- lib/pray/cli/commands/update_latest.rb
|
|
116
117
|
- lib/pray/cli/commands/workflow.rb
|
|
117
118
|
- lib/pray/cli/help.rb
|
|
118
119
|
- lib/pray/cli/helpers.rb
|
|
@@ -147,6 +148,7 @@ files:
|
|
|
147
148
|
- lib/pray/manifest_parser_helpers.rb
|
|
148
149
|
- lib/pray/materialize.rb
|
|
149
150
|
- lib/pray/package_spec.rb
|
|
151
|
+
- lib/pray/package_spec_render.rb
|
|
150
152
|
- lib/pray/path_safety.rb
|
|
151
153
|
- lib/pray/plan.rb
|
|
152
154
|
- lib/pray/project_context.rb
|
|
@@ -181,6 +183,9 @@ files:
|
|
|
181
183
|
- lib/pray/trust_feed.rb
|
|
182
184
|
- lib/pray/trust_ops.rb
|
|
183
185
|
- lib/pray/upstream.rb
|
|
186
|
+
- lib/pray/upstream_latest.rb
|
|
187
|
+
- lib/pray/upstream_merge.rb
|
|
188
|
+
- lib/pray/upstream_refresh.rb
|
|
184
189
|
- lib/pray/verify.rb
|
|
185
190
|
- lib/pray/verify_position.rb
|
|
186
191
|
- lib/pray/verify_provisioned.rb
|