pray-cli 1.17.0 → 1.18.1
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 +15 -0
- data/lib/pray/archive.rb +1 -0
- data/lib/pray/cli/commands/trust.rb +3 -3
- data/lib/pray/cli/commands/update_latest.rb +14 -16
- data/lib/pray/git_cache.rb +192 -0
- data/lib/pray/git_clone.rb +32 -0
- data/lib/pray/git_materialize.rb +76 -0
- data/lib/pray/git_run.rb +41 -0
- data/lib/pray/git_sources.rb +60 -145
- data/lib/pray/publish.rb +1 -0
- data/lib/pray/registry.rb +4 -15
- data/lib/pray/registry_http_cache.rb +17 -0
- data/lib/pray/resolve.rb +1 -1
- data/lib/pray/version.rb +2 -2
- 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: 91a549f989bb5872cf7307c16101a89c479bda9f919dd4f969917a80f4bf5958
|
|
4
|
+
data.tar.gz: 01a7042cad4e383a95ca2b15d698b9e00a46707d27b2885381ce57f6775cb15b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 52f904573df3f97eb3ad240388db1ac7d81d36cd66fe24b43be8cf32f392d59abf6283fc496a92b9844e40cec4d7058bf8db54fb97716164635c395c95a27796
|
|
7
|
+
data.tar.gz: ccc7110154721dbc019d02c37ecdbeaaac225ccf702e8d1007cfe3af4b3f54f35627876fb78f15eb5617e691c7c7ca7016c9c05853b1719655545a087baf8eef
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 1.18.1 (2026-09-17)
|
|
4
|
+
|
|
5
|
+
- Fix `pray package` and `pray publish` when the CLI cannot create a staging directory.
|
|
6
|
+
|
|
7
|
+
## 1.18.0 (2026-09-17)
|
|
8
|
+
|
|
9
|
+
- Clone a git catalog without unused package blobs, and fetch a used package file when install needs it.
|
|
10
|
+
- Skip cloning a git catalog when no package uses that source.
|
|
11
|
+
- Fetch a used git catalog once on `pray update` instead of fetching it twice.
|
|
12
|
+
- Keep packages from two git sources that share a clone URL when each source names a different `subdir`.
|
|
13
|
+
- Share one git object store for those subdirectory checkouts.
|
|
14
|
+
- Import signing keys with `pray trust import-repo` after a catalog was cloned only through `subdir` sources.
|
|
15
|
+
- Reuse registry package metadata during one command so `pray update --latest` does not download the same package JSON twice.
|
|
16
|
+
- Skip a second resolve when `pray update --latest` does not rewrite constraints or upstream pins, and still refresh path-fork files when needed.
|
|
17
|
+
|
|
3
18
|
## 1.17.0 (2026-09-16)
|
|
4
19
|
|
|
5
20
|
- Locate local prayers through a path source of any directory name (RFC 0117). `pray prayer init` adds `source "local"` when needed and declares `pray "local/project"` once. Version is optional until `pray package` or `pray publish`. `.agents/project.md` remains a compose shortcut for one local file.
|
data/lib/pray/archive.rb
CHANGED
|
@@ -28,10 +28,10 @@ module Pray
|
|
|
28
28
|
|
|
29
29
|
def trust_import_repo_command(source_url, match_prefix: nil)
|
|
30
30
|
clone_url = source_url.delete_prefix("git+")
|
|
31
|
-
repository = GitSources.
|
|
32
|
-
unless
|
|
31
|
+
repository = GitSources.git_source_cached_repository(Dir.pwd, clone_url)
|
|
32
|
+
unless repository
|
|
33
33
|
raise Error.resolution(
|
|
34
|
-
"no cached git repository for #{clone_url}
|
|
34
|
+
"no cached git repository for #{clone_url}"
|
|
35
35
|
)
|
|
36
36
|
end
|
|
37
37
|
|
|
@@ -6,13 +6,15 @@ module Pray
|
|
|
6
6
|
path = manifest_path
|
|
7
7
|
original_text = Pray.read_manifest_text(path)
|
|
8
8
|
manifest_text = original_text
|
|
9
|
-
|
|
9
|
+
unlocked = package ? Set[package] : Set.new
|
|
10
|
+
options = ResolveOptions.new(
|
|
10
11
|
offline: offline,
|
|
11
12
|
refresh: true,
|
|
12
13
|
refresh_source_revisions: true,
|
|
13
|
-
ignore_locked_versions:
|
|
14
|
+
ignore_locked_versions: package.nil?,
|
|
15
|
+
unlocked_packages: unlocked
|
|
14
16
|
)
|
|
15
|
-
project = resolve_current_project(
|
|
17
|
+
project = resolve_current_project(options)
|
|
16
18
|
if package && project.manifest.packages.none? { |entry| entry.name == package }
|
|
17
19
|
raise Error.manifest("package #{package} not found")
|
|
18
20
|
end
|
|
@@ -46,24 +48,20 @@ module Pray
|
|
|
46
48
|
|
|
47
49
|
previous = File.exist?(lockfile_path) ? Pray.read_lockfile(lockfile_path) : nil
|
|
48
50
|
upstream_plans = Upstream.plan_path_upstream_latest_constraints(
|
|
49
|
-
project, previous, package,
|
|
51
|
+
project, previous, package, options
|
|
50
52
|
)
|
|
51
53
|
print_latest_constraint_plans(manifest_updates, upstream_plans)
|
|
52
54
|
|
|
53
55
|
return if dry_run
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
unlocked_packages: unlocked
|
|
64
|
-
)
|
|
65
|
-
current = resolve_current_project(options)
|
|
66
|
-
Upstream.apply_path_upstream_refreshes(current, previous, package, options)
|
|
57
|
+
if !manifest_updates.empty? || !upstream_plans.empty?
|
|
58
|
+
Upstream.apply_path_upstream_latest_constraints(upstream_plans)
|
|
59
|
+
Transaction.write_file(path, manifest_text) if manifest_text != original_text
|
|
60
|
+
current = resolve_current_project(options)
|
|
61
|
+
Upstream.apply_path_upstream_refreshes(current, previous, package, options)
|
|
62
|
+
else
|
|
63
|
+
Upstream.apply_path_upstream_refreshes(project, previous, package, options)
|
|
64
|
+
end
|
|
67
65
|
install_command(
|
|
68
66
|
{
|
|
69
67
|
locked: false,
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require_relative "error"
|
|
6
|
+
require_relative "hashing"
|
|
7
|
+
require_relative "git_run"
|
|
8
|
+
require_relative "git_clone"
|
|
9
|
+
|
|
10
|
+
module Pray
|
|
11
|
+
module GitCache
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def ensure_git_repository(project_root, clone_url, refresh:, pinned_revision:, sparse_subdir:)
|
|
15
|
+
shared = git_source_cache_directory(project_root, clone_url)
|
|
16
|
+
ensure_shared_git_repository(project_root, clone_url, shared, refresh: refresh, pinned_revision: pinned_revision)
|
|
17
|
+
checkout = git_source_cache_directory(project_root, clone_url, sparse_subdir)
|
|
18
|
+
if checkout != shared
|
|
19
|
+
ensure_linked_worktree(shared, checkout)
|
|
20
|
+
GitClone.apply_sparse_checkout(checkout, sparse_subdir)
|
|
21
|
+
if pinned_revision
|
|
22
|
+
checkout_git_revision(checkout, clone_url, pinned_revision, refresh)
|
|
23
|
+
elsif refresh
|
|
24
|
+
GitRun.run_git(checkout, "reset", "--hard", git_head_revision(shared))
|
|
25
|
+
end
|
|
26
|
+
return [checkout, git_head_revision(checkout)]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
GitClone.apply_sparse_checkout(shared)
|
|
30
|
+
[shared, git_head_revision(shared)]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def git_source_cache_directory(project_root, clone_url, subdir = nil)
|
|
34
|
+
identity = (subdir && !subdir.empty?) ? "#{clone_url}\n#{subdir}" : clone_url
|
|
35
|
+
File.join(project_root, ".pray", "cache", "git", cache_key(identity))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def git_source_cached_repository(project_root, clone_url)
|
|
39
|
+
shared = git_source_cache_directory(project_root, clone_url)
|
|
40
|
+
return shared if git_checkout?(shared)
|
|
41
|
+
|
|
42
|
+
cache_root = File.join(project_root, ".pray", "cache", "git")
|
|
43
|
+
return nil unless File.directory?(cache_root)
|
|
44
|
+
|
|
45
|
+
Dir.children(cache_root).each do |name|
|
|
46
|
+
path = File.join(cache_root, name)
|
|
47
|
+
return path if git_checkout?(path) && origin_matches?(path, clone_url)
|
|
48
|
+
end
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def git_checkout?(path)
|
|
53
|
+
File.exist?(File.join(path, ".git"))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def cache_key(text)
|
|
57
|
+
Hashing.sha256_prefixed(text)[7, 16]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def ensure_shared_git_repository(project_root, clone_url, shared, refresh:, pinned_revision:)
|
|
61
|
+
if File.directory?(File.join(shared, ".git"))
|
|
62
|
+
if pinned_revision
|
|
63
|
+
checkout_git_revision(shared, clone_url, pinned_revision, refresh)
|
|
64
|
+
elsif refresh
|
|
65
|
+
refresh_git_worktree(shared, clone_url)
|
|
66
|
+
end
|
|
67
|
+
refresh_global_from_project(clone_url, shared) if refresh
|
|
68
|
+
return
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
FileUtils.rm_rf(shared) if File.exist?(shared)
|
|
72
|
+
FileUtils.mkdir_p(File.dirname(shared))
|
|
73
|
+
seeded = seed_git_cache_from_global(clone_url, shared, project_root)
|
|
74
|
+
if seeded
|
|
75
|
+
GitRun.run_git(shared, "remote", "set-url", "origin", clone_url)
|
|
76
|
+
else
|
|
77
|
+
GitClone.clone_git_cache(project_root, clone_url, shared, quiet: false)
|
|
78
|
+
mirror_git_cache_to_global(clone_url, shared)
|
|
79
|
+
end
|
|
80
|
+
GitClone.apply_sparse_checkout(shared)
|
|
81
|
+
if pinned_revision
|
|
82
|
+
checkout_git_revision(shared, clone_url, pinned_revision, true)
|
|
83
|
+
elsif refresh && seeded
|
|
84
|
+
refresh_git_worktree(shared, clone_url)
|
|
85
|
+
end
|
|
86
|
+
refresh_global_from_project(clone_url, shared) if refresh && seeded
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def ensure_linked_worktree(shared, checkout)
|
|
90
|
+
return if same_object_store?(shared, checkout)
|
|
91
|
+
|
|
92
|
+
FileUtils.rm_rf(checkout) if File.exist?(checkout)
|
|
93
|
+
FileUtils.mkdir_p(File.dirname(checkout))
|
|
94
|
+
return if GitRun.try_run_git(shared, "worktree", "add", "--detach", "--no-checkout", checkout)
|
|
95
|
+
|
|
96
|
+
GitRun.run_git(shared, "worktree", "add", "--detach", checkout)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def same_object_store?(shared, checkout)
|
|
100
|
+
return false unless git_checkout?(checkout)
|
|
101
|
+
|
|
102
|
+
git_common_dir(shared) == git_common_dir(checkout)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def git_common_dir(repository)
|
|
106
|
+
output, status = GitRun.capture_git(repository, "rev-parse", "--git-common-dir")
|
|
107
|
+
raise Error.resolution(GitRun.command_error("git rev-parse --git-common-dir", output)) unless status.success?
|
|
108
|
+
|
|
109
|
+
reported = output.strip
|
|
110
|
+
path = Pathname.new(reported).absolute? ? reported : File.expand_path(reported, repository)
|
|
111
|
+
File.realpath(path)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def origin_matches?(repository, clone_url)
|
|
115
|
+
output, status = GitRun.capture_git(repository, "remote", "get-url", "origin")
|
|
116
|
+
return false unless status.success?
|
|
117
|
+
|
|
118
|
+
origin = output.strip.delete_prefix("git+")
|
|
119
|
+
origin == clone_url
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def global_cache_root
|
|
123
|
+
return ENV["PRAY_CACHE"] if ENV["PRAY_CACHE"]
|
|
124
|
+
return File.join(ENV["PRAY_HOME"], "cache") if ENV["PRAY_HOME"]
|
|
125
|
+
|
|
126
|
+
home = ENV["HOME"]
|
|
127
|
+
home ? File.join(home, ".cache", "pray") : nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def global_git_cache_directory(clone_url)
|
|
131
|
+
root = global_cache_root
|
|
132
|
+
root ? File.join(root, "git", cache_key(clone_url)) : nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def global_git_cache_ready?(global_cache)
|
|
136
|
+
File.directory?(File.join(global_cache, ".git")) || File.file?(File.join(global_cache, "HEAD"))
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def seed_git_cache_from_global(clone_url, destination, working_directory)
|
|
140
|
+
global_cache = global_git_cache_directory(clone_url)
|
|
141
|
+
return false unless global_cache && global_git_cache_ready?(global_cache)
|
|
142
|
+
|
|
143
|
+
GitClone.clone_git_cache(working_directory, global_cache, destination, quiet: true)
|
|
144
|
+
true
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def mirror_git_cache_to_global(clone_url, project_cache)
|
|
148
|
+
global_cache = global_git_cache_directory(clone_url)
|
|
149
|
+
return unless global_cache
|
|
150
|
+
return if global_git_cache_ready?(global_cache)
|
|
151
|
+
|
|
152
|
+
FileUtils.mkdir_p(File.dirname(global_cache))
|
|
153
|
+
FileUtils.rm_rf(global_cache) if File.exist?(global_cache)
|
|
154
|
+
GitRun.run_git(File.dirname(project_cache), "clone", "--bare", "--quiet", File.basename(project_cache), global_cache)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def fetch_origin(repository, revision = nil)
|
|
158
|
+
extra = revision ? [revision] : []
|
|
159
|
+
return if GitRun.try_run_git(repository, "fetch", "--depth", "1", "--filter=blob:none", "origin", *extra)
|
|
160
|
+
|
|
161
|
+
GitRun.run_git(repository, "fetch", "--depth", "1", "origin", *extra)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def checkout_git_revision(repository, _clone_url, revision, refresh)
|
|
165
|
+
fetch_origin(repository, revision) if refresh
|
|
166
|
+
GitRun.run_git(repository, "checkout", "--force", revision)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def refresh_git_worktree(repository, _clone_url)
|
|
170
|
+
fetch_origin(repository)
|
|
171
|
+
GitRun.run_git(repository, "reset", "--hard", "origin/HEAD")
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def refresh_global_from_project(clone_url, project_cache)
|
|
175
|
+
global_cache = global_git_cache_directory(clone_url)
|
|
176
|
+
return unless global_cache
|
|
177
|
+
|
|
178
|
+
FileUtils.rm_rf(global_cache) if global_git_cache_ready?(global_cache) || File.exist?(global_cache)
|
|
179
|
+
mirror_git_cache_to_global(clone_url, project_cache)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def git_head_revision(repository)
|
|
183
|
+
output, status = GitRun.capture_git(repository, "rev-parse", "HEAD")
|
|
184
|
+
raise Error.resolution(GitRun.command_error("git rev-parse HEAD", output)) unless status.success?
|
|
185
|
+
|
|
186
|
+
revision = output.strip
|
|
187
|
+
raise Error.resolution("git repository has no HEAD revision") if revision.empty?
|
|
188
|
+
|
|
189
|
+
revision
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "git_run"
|
|
4
|
+
|
|
5
|
+
module Pray
|
|
6
|
+
module GitClone
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def clone_git_cache(working_directory, source, destination, quiet:)
|
|
10
|
+
filtered = ["clone", "--depth", "1", "--filter=blob:none", "--sparse", source, destination]
|
|
11
|
+
filtered.insert(1, "--quiet") if quiet
|
|
12
|
+
return if GitRun.try_run_git(working_directory, *filtered)
|
|
13
|
+
|
|
14
|
+
full = ["clone", "--depth", "1", source, destination]
|
|
15
|
+
full.insert(1, "--quiet") if quiet
|
|
16
|
+
GitRun.run_git(working_directory, *full)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def apply_sparse_checkout(repository, subdir = nil)
|
|
20
|
+
GitRun.run_git(repository, "sparse-checkout", "init", "--cone")
|
|
21
|
+
GitRun.run_git(repository, "sparse-checkout", "set", *catalog_sparse_cones(subdir))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def catalog_sparse_cones(subdir)
|
|
25
|
+
if subdir && !subdir.empty?
|
|
26
|
+
["#{subdir}/v1/packages"]
|
|
27
|
+
else
|
|
28
|
+
["v1/packages", "prayers/v1/packages"]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "git_run"
|
|
4
|
+
require_relative "error"
|
|
5
|
+
require_relative "path_safety"
|
|
6
|
+
|
|
7
|
+
module Pray
|
|
8
|
+
module GitMaterialize
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def read_local_artifact_bytes(source_root, artifact)
|
|
12
|
+
if artifact.start_with?("file://")
|
|
13
|
+
path = PathSafety.join_under_root(source_root, artifact.delete_prefix("file://"))
|
|
14
|
+
raise Error.resolution("package artifact path escapes distribution root") unless path
|
|
15
|
+
|
|
16
|
+
return File.binread(path)
|
|
17
|
+
end
|
|
18
|
+
if artifact.match?(%r{\A[a-z][a-z0-9+.-]*:}i)
|
|
19
|
+
raise Error.integrity("remote artifact path must be relative: #{artifact}")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
path = PathSafety.join_under_root(source_root, artifact)
|
|
23
|
+
raise Error.resolution("package artifact path escapes distribution root") unless path
|
|
24
|
+
|
|
25
|
+
materialize_catalog_file(source_root, artifact) unless File.exist?(path)
|
|
26
|
+
raise Error.resolution("package artifact missing at #{path}") unless File.exist?(path)
|
|
27
|
+
|
|
28
|
+
File.binread(path)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def materialize_catalog_file(source_root, relative)
|
|
32
|
+
return if File.file?(File.join(source_root, relative))
|
|
33
|
+
return unless sparse_checkout_enabled?(source_root)
|
|
34
|
+
|
|
35
|
+
toplevel = git_toplevel(source_root)
|
|
36
|
+
return unless toplevel
|
|
37
|
+
|
|
38
|
+
repo_relative = repo_relative_text(source_root, relative)
|
|
39
|
+
return unless repo_relative
|
|
40
|
+
|
|
41
|
+
cone = artifact_sparse_cone(repo_relative)
|
|
42
|
+
GitRun.run_git(toplevel, "sparse-checkout", "add", cone) if cone
|
|
43
|
+
GitRun.run_git(toplevel, "checkout", "HEAD", "--", repo_relative)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def sparse_checkout_enabled?(source_root)
|
|
47
|
+
output, status = GitRun.capture_git(source_root, "config", "--get", "core.sparseCheckout")
|
|
48
|
+
status.success? && output.strip == "true"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def git_toplevel(source_root)
|
|
52
|
+
output, status = GitRun.capture_git(source_root, "rev-parse", "--show-toplevel")
|
|
53
|
+
return unless status.success?
|
|
54
|
+
|
|
55
|
+
text = output.strip
|
|
56
|
+
text.empty? ? nil : text
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def repo_relative_text(source_root, relative)
|
|
60
|
+
output, status = GitRun.capture_git(source_root, "rev-parse", "--show-prefix")
|
|
61
|
+
return unless status.success?
|
|
62
|
+
|
|
63
|
+
prefix = output.strip
|
|
64
|
+
prefix.empty? ? relative : File.join(prefix, relative)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def artifact_sparse_cone(repo_relative)
|
|
68
|
+
parts = repo_relative.split("/").reject(&:empty?)
|
|
69
|
+
return repo_relative if parts.length < 2
|
|
70
|
+
|
|
71
|
+
parts.pop
|
|
72
|
+
parts.pop if parts.last != "artifacts" && parts.length > 3
|
|
73
|
+
parts.join("/")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/pray/git_run.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require_relative "error"
|
|
5
|
+
|
|
6
|
+
module Pray
|
|
7
|
+
module GitRun
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def run_git(cwd, *arguments)
|
|
11
|
+
output, status = capture_git(cwd, *arguments)
|
|
12
|
+
return if status.success?
|
|
13
|
+
|
|
14
|
+
raise Error.resolution(command_error("git #{arguments.join(" ")}", output))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def try_run_git(cwd, *arguments)
|
|
18
|
+
_output, status = capture_git(cwd, *arguments)
|
|
19
|
+
status.success?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def capture_git(cwd, *arguments)
|
|
23
|
+
env = ENV.to_h.merge("GIT_TERMINAL_PROMPT" => "0")
|
|
24
|
+
Open3.capture2e(
|
|
25
|
+
env,
|
|
26
|
+
"git",
|
|
27
|
+
"-c",
|
|
28
|
+
"protocol.file.allow=always",
|
|
29
|
+
"-C",
|
|
30
|
+
cwd,
|
|
31
|
+
*arguments,
|
|
32
|
+
stdin_data: ""
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def command_error(program, output)
|
|
37
|
+
message = output.strip
|
|
38
|
+
message.empty? ? "#{program} failed" : "#{program} failed: #{message}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/pray/git_sources.rb
CHANGED
|
@@ -1,48 +1,73 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "open3"
|
|
4
|
-
require "fileutils"
|
|
5
3
|
require "pathname"
|
|
4
|
+
require_relative "git_cache"
|
|
6
5
|
|
|
7
6
|
module Pray
|
|
8
7
|
GitSourceCheckout = Struct.new(:cache_directory, :revision, :subdir)
|
|
9
8
|
|
|
9
|
+
class GitSourceSet
|
|
10
|
+
def initialize(project_root, sources, lockfile, refresh:)
|
|
11
|
+
@project_root = project_root
|
|
12
|
+
@lockfile = lockfile
|
|
13
|
+
@refresh = refresh
|
|
14
|
+
@sources = sources.select { |source| source.kind == "git" }.to_h { |source| [source.name, source] }
|
|
15
|
+
@checkouts = {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def [](name)
|
|
19
|
+
return @checkouts[name] if @checkouts.key?(name)
|
|
20
|
+
|
|
21
|
+
source = @sources[name]
|
|
22
|
+
return nil unless source
|
|
23
|
+
|
|
24
|
+
checkout = GitSources.prepare_one_git_source(@project_root, source, @lockfile, refresh: @refresh)
|
|
25
|
+
@checkouts[name] = checkout if checkout
|
|
26
|
+
checkout
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def revisions
|
|
30
|
+
@checkouts.each_with_object({}) do |(name, checkout), collected|
|
|
31
|
+
next if checkout.revision.to_s.empty?
|
|
32
|
+
|
|
33
|
+
collected[name] = checkout.revision
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
10
38
|
module GitSources
|
|
11
39
|
module_function
|
|
12
40
|
|
|
13
41
|
def prepare_git_sources(project_root, sources, lockfile, refresh: false)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
)
|
|
27
|
-
end
|
|
28
|
-
next
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
pinned_revision = refresh ? nil : pinned_revision_for_source(lockfile, source)
|
|
32
|
-
cache_directory, revision = ensure_git_repository(
|
|
33
|
-
project_root,
|
|
34
|
-
clone_url,
|
|
35
|
-
refresh: refresh,
|
|
36
|
-
pinned_revision: pinned_revision,
|
|
37
|
-
sparse_subdir: source.subdir
|
|
38
|
-
)
|
|
39
|
-
checkouts[source.name] = GitSourceCheckout.new(
|
|
40
|
-
cache_directory: cache_directory,
|
|
41
|
-
revision: revision,
|
|
42
|
+
GitSourceSet.new(project_root, sources, lockfile, refresh: refresh)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def prepare_one_git_source(project_root, source, lockfile, refresh:)
|
|
46
|
+
clone_url = source.url.delete_prefix("git+")
|
|
47
|
+
if local_filesystem_source?(clone_url) && !local_git_repo_path(project_root, clone_url)
|
|
48
|
+
source_root = local_git_source_root(project_root, clone_url)
|
|
49
|
+
return unless source_root
|
|
50
|
+
|
|
51
|
+
return GitSourceCheckout.new(
|
|
52
|
+
cache_directory: source_root,
|
|
53
|
+
revision: "",
|
|
42
54
|
subdir: source.subdir
|
|
43
55
|
)
|
|
44
56
|
end
|
|
45
|
-
|
|
57
|
+
|
|
58
|
+
pinned_revision = refresh ? nil : pinned_revision_for_source(lockfile, source)
|
|
59
|
+
cache_directory, revision = GitCache.ensure_git_repository(
|
|
60
|
+
project_root,
|
|
61
|
+
clone_url,
|
|
62
|
+
refresh: refresh,
|
|
63
|
+
pinned_revision: pinned_revision,
|
|
64
|
+
sparse_subdir: source.subdir
|
|
65
|
+
)
|
|
66
|
+
GitSourceCheckout.new(
|
|
67
|
+
cache_directory: cache_directory,
|
|
68
|
+
revision: revision,
|
|
69
|
+
subdir: source.subdir
|
|
70
|
+
)
|
|
46
71
|
end
|
|
47
72
|
|
|
48
73
|
def resolve_distribution_root(repo_root, subdir)
|
|
@@ -83,39 +108,12 @@ module Pray
|
|
|
83
108
|
discover_distribution_root(path)
|
|
84
109
|
end
|
|
85
110
|
|
|
86
|
-
def
|
|
87
|
-
|
|
88
|
-
if File.directory?(File.join(cache_directory, ".git"))
|
|
89
|
-
refresh_global_git_cache(clone_url) if refresh
|
|
90
|
-
if pinned_revision
|
|
91
|
-
checkout_git_revision(cache_directory, clone_url, pinned_revision, refresh)
|
|
92
|
-
elsif refresh
|
|
93
|
-
refresh_git_worktree(cache_directory, clone_url)
|
|
94
|
-
end
|
|
95
|
-
apply_sparse_checkout(cache_directory, sparse_subdir) if sparse_subdir
|
|
96
|
-
revision = git_head_revision(cache_directory)
|
|
97
|
-
return [cache_directory, revision]
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
FileUtils.rm_rf(cache_directory) if File.exist?(cache_directory)
|
|
101
|
-
FileUtils.mkdir_p(File.dirname(cache_directory))
|
|
102
|
-
if seed_git_cache_from_global(clone_url, cache_directory, project_root)
|
|
103
|
-
run_git_in_repo(cache_directory, "remote", "set-url", "origin", clone_url)
|
|
104
|
-
else
|
|
105
|
-
run_git(project_root, "clone", "--depth", "1", clone_url, cache_directory)
|
|
106
|
-
mirror_git_cache_to_global(clone_url, cache_directory)
|
|
107
|
-
end
|
|
108
|
-
checkout_git_revision(cache_directory, clone_url, pinned_revision, true) if pinned_revision
|
|
109
|
-
apply_sparse_checkout(cache_directory, sparse_subdir) if sparse_subdir
|
|
110
|
-
[cache_directory, git_head_revision(cache_directory)]
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def git_source_cache_directory(project_root, clone_url)
|
|
114
|
-
File.join(project_root, ".pray", "cache", "git", cache_key(clone_url))
|
|
111
|
+
def git_source_cache_directory(project_root, clone_url, subdir = nil)
|
|
112
|
+
GitCache.git_source_cache_directory(project_root, clone_url, subdir)
|
|
115
113
|
end
|
|
116
114
|
|
|
117
|
-
def
|
|
118
|
-
|
|
115
|
+
def git_source_cached_repository(project_root, clone_url)
|
|
116
|
+
GitCache.git_source_cached_repository(project_root, clone_url)
|
|
119
117
|
end
|
|
120
118
|
|
|
121
119
|
def pinned_revision_for_source(lockfile, source)
|
|
@@ -142,88 +140,5 @@ module Pray
|
|
|
142
140
|
path = clone_url.delete_prefix("file://")
|
|
143
141
|
Pathname.new(path).absolute? ? path : File.expand_path(path, project_root)
|
|
144
142
|
end
|
|
145
|
-
|
|
146
|
-
def global_cache_root
|
|
147
|
-
return ENV["PRAY_CACHE"] if ENV["PRAY_CACHE"]
|
|
148
|
-
return File.join(ENV["PRAY_HOME"], "cache") if ENV["PRAY_HOME"]
|
|
149
|
-
|
|
150
|
-
home = ENV["HOME"]
|
|
151
|
-
home ? File.join(home, ".cache", "pray") : nil
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def global_git_cache_directory(clone_url)
|
|
155
|
-
root = global_cache_root
|
|
156
|
-
root ? File.join(root, "git", cache_key(clone_url)) : nil
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def global_git_cache_ready?(global_cache)
|
|
160
|
-
File.directory?(File.join(global_cache, ".git")) || File.file?(File.join(global_cache, "HEAD"))
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def seed_git_cache_from_global(clone_url, destination, working_directory)
|
|
164
|
-
global_cache = global_git_cache_directory(clone_url)
|
|
165
|
-
return false unless global_cache && global_git_cache_ready?(global_cache)
|
|
166
|
-
|
|
167
|
-
run_git(working_directory, "clone", "--depth", "1", "--quiet", global_cache, destination)
|
|
168
|
-
true
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def mirror_git_cache_to_global(clone_url, project_cache)
|
|
172
|
-
global_cache = global_git_cache_directory(clone_url)
|
|
173
|
-
return unless global_cache
|
|
174
|
-
return if global_git_cache_ready?(global_cache)
|
|
175
|
-
|
|
176
|
-
FileUtils.mkdir_p(File.dirname(global_cache))
|
|
177
|
-
FileUtils.rm_rf(global_cache) if File.exist?(global_cache)
|
|
178
|
-
run_git(File.dirname(project_cache), "clone", "--bare", "--quiet", File.basename(project_cache), global_cache)
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def apply_sparse_checkout(repository, subdir)
|
|
182
|
-
run_git_in_repo(repository, "sparse-checkout", "init", "--cone")
|
|
183
|
-
run_git_in_repo(repository, "sparse-checkout", "set", subdir)
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def checkout_git_revision(repository, _clone_url, revision, refresh)
|
|
187
|
-
run_git_in_repo(repository, "fetch", "--depth", "1", "origin", revision) if refresh
|
|
188
|
-
run_git_in_repo(repository, "checkout", "--force", revision)
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def refresh_git_worktree(repository, _clone_url)
|
|
192
|
-
run_git_in_repo(repository, "fetch", "--depth", "1", "origin")
|
|
193
|
-
run_git_in_repo(repository, "reset", "--hard", "origin/HEAD")
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def refresh_global_git_cache(clone_url)
|
|
197
|
-
global_cache = global_git_cache_directory(clone_url)
|
|
198
|
-
return unless global_cache && global_git_cache_ready?(global_cache)
|
|
199
|
-
|
|
200
|
-
run_git_in_repo(global_cache, "fetch", "--depth", "1", "origin")
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def git_head_revision(repository)
|
|
204
|
-
output, status = Open3.capture2e("git", "-C", repository, "rev-parse", "HEAD")
|
|
205
|
-
raise Error.resolution(command_error("git rev-parse HEAD", output)) unless status.success?
|
|
206
|
-
|
|
207
|
-
revision = output.strip
|
|
208
|
-
raise Error.resolution("git repository has no HEAD revision") if revision.empty?
|
|
209
|
-
|
|
210
|
-
revision
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def run_git(cwd, *arguments)
|
|
214
|
-
output, status = Open3.capture2e("git", "-C", cwd, *arguments)
|
|
215
|
-
return if status.success?
|
|
216
|
-
|
|
217
|
-
raise Error.resolution(command_error("git #{arguments.join(" ")}", output))
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def run_git_in_repo(repository, *arguments)
|
|
221
|
-
run_git(repository, *arguments)
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def command_error(program, output)
|
|
225
|
-
message = output.strip
|
|
226
|
-
message.empty? ? "#{program} failed" : "#{program} failed: #{message}"
|
|
227
|
-
end
|
|
228
143
|
end
|
|
229
144
|
end
|
data/lib/pray/publish.rb
CHANGED
data/lib/pray/registry.rb
CHANGED
|
@@ -9,6 +9,8 @@ require "time"
|
|
|
9
9
|
require_relative "path_safety"
|
|
10
10
|
require_relative "http_body"
|
|
11
11
|
require_relative "registry_paths"
|
|
12
|
+
require_relative "registry_http_cache"
|
|
13
|
+
require_relative "git_materialize"
|
|
12
14
|
|
|
13
15
|
module Pray
|
|
14
16
|
RegistryPackageVersion = Struct.new(
|
|
@@ -134,9 +136,7 @@ module Pray
|
|
|
134
136
|
return parse_metadata(File.read(metadata_path, encoding: "UTF-8"))
|
|
135
137
|
end
|
|
136
138
|
|
|
137
|
-
|
|
138
|
-
response = http_get(join_url(source_url, "v1/packages/#{package_name}.json"))
|
|
139
|
-
parse_metadata(response)
|
|
139
|
+
RegistryHttpCache.fetch_package_metadata(source_url, package_name)
|
|
140
140
|
end
|
|
141
141
|
|
|
142
142
|
def parse_metadata(text)
|
|
@@ -207,18 +207,7 @@ module Pray
|
|
|
207
207
|
end
|
|
208
208
|
|
|
209
209
|
def read_local_artifact_bytes(source_root, artifact)
|
|
210
|
-
|
|
211
|
-
path = PathSafety.join_under_root(source_root, artifact.delete_prefix("file://"))
|
|
212
|
-
raise Error.resolution("package artifact path escapes distribution root") unless path
|
|
213
|
-
return File.binread(path)
|
|
214
|
-
end
|
|
215
|
-
reject_absolute_artifact!(artifact)
|
|
216
|
-
|
|
217
|
-
path = PathSafety.join_under_root(source_root, artifact)
|
|
218
|
-
raise Error.resolution("package artifact path escapes distribution root") unless path
|
|
219
|
-
raise Error.resolution("package artifact missing at #{path}") unless File.exist?(path)
|
|
220
|
-
|
|
221
|
-
File.binread(path)
|
|
210
|
+
GitMaterialize.read_local_artifact_bytes(source_root, artifact)
|
|
222
211
|
end
|
|
223
212
|
|
|
224
213
|
def read_artifact_bytes(source_url, artifact)
|
|
@@ -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/resolve.rb
CHANGED
|
@@ -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 = []
|
|
@@ -100,6 +99,7 @@ module Pray
|
|
|
100
99
|
raise Error.resolution(local_errors.join("\n")) unless local_errors.empty?
|
|
101
100
|
|
|
102
101
|
ResolveDeps.reject_dependency_cycles(packages)
|
|
102
|
+
source_revisions = git_sources.revisions
|
|
103
103
|
|
|
104
104
|
ResolvedProject.new(
|
|
105
105
|
manifest_path: manifest_path,
|
data/lib/pray/version.rb
CHANGED
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.18.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Makarov
|
|
@@ -136,7 +136,11 @@ files:
|
|
|
136
136
|
- lib/pray/error.rb
|
|
137
137
|
- lib/pray/format_manifest.rb
|
|
138
138
|
- lib/pray/format_serialize.rb
|
|
139
|
+
- lib/pray/git_cache.rb
|
|
140
|
+
- lib/pray/git_clone.rb
|
|
141
|
+
- lib/pray/git_materialize.rb
|
|
139
142
|
- lib/pray/git_refresh.rb
|
|
143
|
+
- lib/pray/git_run.rb
|
|
140
144
|
- lib/pray/git_sources.rb
|
|
141
145
|
- lib/pray/hashing.rb
|
|
142
146
|
- lib/pray/http_body.rb
|
|
@@ -161,6 +165,7 @@ files:
|
|
|
161
165
|
- lib/pray/project_context.rb
|
|
162
166
|
- lib/pray/publish.rb
|
|
163
167
|
- lib/pray/registry.rb
|
|
168
|
+
- lib/pray/registry_http_cache.rb
|
|
164
169
|
- lib/pray/registry_install.rb
|
|
165
170
|
- lib/pray/registry_integrity.rb
|
|
166
171
|
- lib/pray/registry_paths.rb
|