pray-cli 1.19.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6df9a00a7c009e00fdd00987564489607a77bf48cb6472e06951c7709a91a32f
4
- data.tar.gz: 93ad851e71a69c13764f2a289d3f32dd7856a749aa341ef728b4c49ebc651149
3
+ metadata.gz: 4eb951f0ca0abc5ac0aa68ab27e63e4f778c92453fddb9a7cca0115dac1526d9
4
+ data.tar.gz: 28ba1073042c0c2a77138e35a9ae80688cb1a6075dd16cba5ee1b9aef2c9c3af
5
5
  SHA512:
6
- metadata.gz: 56060ecc2267d2f8a2c1b555591b3db551383d8350a9b3125a0647831ee2438dfb3f78b6424e2af98bbfc184ef8a91c181e1da5bf35835a160f34bcde865c1b0
7
- data.tar.gz: 55b5590131213f639bc63be84817d65c17297354a1387b363c94d8953192ca03f80c9245b47d5cc1d7d99cd51569c2a9647147fb0de124966dfb294e125bf68b
6
+ metadata.gz: 260a99edc903d99d235b2d9dd40904490fbcc392d67a07a4323d43b1359fead4428976745e438e17767948a1494262daa97b87650965ffc22366267c8faf5226
7
+ data.tar.gz: f031ff7ebaeceee86829abac7e2f99f060be902ebc586c00e5ad67135d25958248ac7c4fd5425a12068b67d1a01ebd747cacf53501f2c81cc68f3bdcbcfd838d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.20.0 (2026-09-18)
4
+
5
+ - Fetch a locked git catalog revision during `pray install` when the local cache does not have that commit yet. `pray install --offline` still refuses a missing commit.
6
+ - Refuse `pray install --offline` when a git catalog is not already in the local cache, instead of cloning it. A global cache seed still works.
7
+ - Honour a locked git revision for a local file catalog. Install does not read the origin worktree after that checkout fails.
8
+ - Keep git catalog objects in the shared cache. The project catalog is a file tree without a git directory.
9
+ - When a locked git revision is missing from a shallow cache, fetch the missing history instead of only that one commit.
10
+ - Keep an unchanged published version when a later `pray publish` uses a different publisher label.
11
+ - Keep `pray drift` clean after `pray install` when a compose block adds a local file after a package dest already had.
12
+
3
13
  ## 1.19.0 (2026-09-18)
4
14
 
5
15
  - Declare named `publish` remotes in Prayfile (RFC 0118). `pray publish` uses them when dest flags are omitted, and publishes only path-owned packages.
@@ -1,33 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fileutils"
4
- require "pathname"
5
3
  require_relative "error"
6
4
  require_relative "hashing"
7
5
  require_relative "git_run"
8
- require_relative "git_clone"
6
+ require_relative "git_store"
7
+ require_relative "git_materialize"
9
8
 
10
9
  module Pray
11
10
  module GitCache
12
11
  module_function
13
12
 
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)]
13
+ def ensure_git_repository(project_root, clone_url, refresh:, pinned_revision:, sparse_subdir:, offline: false)
14
+ db, revision = GitStore.ensure_global_git_db(
15
+ clone_url,
16
+ pinned_revision: pinned_revision,
17
+ refresh: refresh,
18
+ offline: offline,
19
+ working_directory: project_root
20
+ )
21
+ catalog = git_source_cache_directory(project_root, clone_url, sparse_subdir)
22
+ GitMaterialize.materialize_catalog_tree(db, catalog, revision, sparse_subdir, refresh)
23
+ [catalog, revision]
31
24
  end
32
25
 
33
26
  def git_source_cache_directory(project_root, clone_url, subdir = nil)
@@ -36,6 +29,9 @@ module Pray
36
29
  end
37
30
 
38
31
  def git_source_cached_repository(project_root, clone_url)
32
+ global_cache = GitStore.global_git_cache_directory(clone_url)
33
+ return global_cache if global_cache && GitStore.global_git_cache_ready?(global_cache)
34
+
39
35
  shared = git_source_cache_directory(project_root, clone_url)
40
36
  return shared if git_checkout?(shared)
41
37
 
@@ -57,60 +53,6 @@ module Pray
57
53
  Hashing.sha256_prefixed(text)[7, 16]
58
54
  end
59
55
 
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
56
  def origin_matches?(repository, clone_url)
115
57
  output, status = GitRun.capture_git(repository, "remote", "get-url", "origin")
116
58
  return false unless status.success?
@@ -118,75 +60,5 @@ module Pray
118
60
  origin = output.strip.delete_prefix("git+")
119
61
  origin == clone_url
120
62
  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
63
  end
192
64
  end
@@ -6,21 +6,17 @@ module Pray
6
6
  module GitClone
7
7
  module_function
8
8
 
9
- def clone_git_cache(working_directory, source, destination, quiet:)
10
- filtered = ["clone", "--depth", "1", "--filter=blob:none", "--sparse", source, destination]
9
+ def clone_bare_git_db(working_directory, source, destination, quiet:)
10
+ local = source.start_with?("file://", "/") ? ["--no-local"] : []
11
+ filtered = ["clone", "--bare", "--depth", "1", "--filter=blob:none", *local, source, destination]
11
12
  filtered.insert(1, "--quiet") if quiet
12
13
  return if GitRun.try_run_git(working_directory, *filtered)
13
14
 
14
- full = ["clone", "--depth", "1", source, destination]
15
+ full = ["clone", "--bare", "--depth", "1", *local, source, destination]
15
16
  full.insert(1, "--quiet") if quiet
16
17
  GitRun.run_git(working_directory, *full)
17
18
  end
18
19
 
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
20
  def catalog_sparse_cones(subdir)
25
21
  if subdir && !subdir.empty?
26
22
  ["#{subdir}/v1/packages"]
@@ -1,11 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "git_run"
3
+ require "fileutils"
4
+ require "open3"
4
5
  require_relative "error"
6
+ require_relative "git_clone"
7
+ require_relative "git_run"
5
8
  require_relative "path_safety"
6
9
 
7
10
  module Pray
8
11
  module GitMaterialize
12
+ REVISION_MARKER = ".pray-revision"
13
+ GIT_DIR_MARKER = ".pray-git-dir"
14
+
9
15
  module_function
10
16
 
11
17
  def read_local_artifact_bytes(source_root, artifact)
@@ -28,49 +34,95 @@ module Pray
28
34
  File.binread(path)
29
35
  end
30
36
 
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
37
+ def materialize_catalog_tree(git_dir, dest, revision, subdir, refresh)
38
+ return if !refresh && catalog_matches?(dest, git_dir, revision)
40
39
 
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
40
+ FileUtils.rm_rf(dest) if File.exist?(dest)
41
+ FileUtils.mkdir_p(dest)
42
+ unpacked = GitClone.catalog_sparse_cones(subdir).any? do |prefix|
43
+ unpack_archive_prefix(git_dir, dest, revision, prefix)
44
+ end
45
+ raise Error.resolution("no pray distribution root in git source at revision #{revision}") unless unpacked
45
46
 
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"
47
+ File.write(File.join(dest, REVISION_MARKER), revision)
48
+ File.write(File.join(dest, GIT_DIR_MARKER), git_dir)
49
49
  end
50
50
 
51
- def git_toplevel(source_root)
52
- output, status = GitRun.capture_git(source_root, "rev-parse", "--show-toplevel")
53
- return unless status.success?
51
+ def materialize_catalog_file(source_root, relative)
52
+ return if File.file?(File.join(source_root, relative))
54
53
 
55
- text = output.strip
56
- text.empty? ? nil : text
54
+ markers = find_catalog_markers(source_root)
55
+ return unless markers
56
+
57
+ full_path = File.join(source_root, relative)
58
+ repo_relative = full_path.delete_prefix(markers.fetch(:work_tree)).delete_prefix("/")
59
+ GitRun.run_git(
60
+ markers.fetch(:git_dir),
61
+ "--work-tree",
62
+ markers.fetch(:work_tree),
63
+ "checkout",
64
+ markers.fetch(:revision),
65
+ "--",
66
+ repo_relative
67
+ )
57
68
  end
58
69
 
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)
70
+ def catalog_matches?(dest, git_dir, revision)
71
+ revision_path = File.join(dest, REVISION_MARKER)
72
+ git_dir_path = File.join(dest, GIT_DIR_MARKER)
73
+ return false unless File.file?(revision_path) && File.file?(git_dir_path)
74
+ return false unless File.read(revision_path).strip == revision
75
+ return false unless File.read(git_dir_path).strip == git_dir
76
+
77
+ File.directory?(File.join(dest, "v1", "packages")) ||
78
+ File.directory?(File.join(dest, "prayers", "v1", "packages")) ||
79
+ Dir.children(dest).any? { |name| File.directory?(File.join(dest, name, "v1", "packages")) }
80
+ rescue Errno::ENOENT
81
+ false
65
82
  end
66
83
 
67
- def artifact_sparse_cone(repo_relative)
68
- parts = repo_relative.split("/").reject(&:empty?)
69
- return repo_relative if parts.length < 2
84
+ def unpack_archive_prefix(git_dir, dest, revision, prefix)
85
+ env = ENV.to_h.merge("GIT_TERMINAL_PROMPT" => "0")
86
+ stdout, _stderr, status = Open3.capture3(
87
+ env,
88
+ "git",
89
+ "-c",
90
+ "protocol.file.allow=always",
91
+ "-C",
92
+ git_dir,
93
+ "archive",
94
+ "--format=tar",
95
+ revision,
96
+ "--",
97
+ prefix,
98
+ stdin_data: ""
99
+ )
100
+ return false unless status.success? && !stdout.empty?
101
+
102
+ _ignored, extract_status = Open3.capture2(env, "tar", "-x", "-C", dest, stdin_data: stdout)
103
+ raise Error.resolution("failed to unpack git catalog archive") unless extract_status.success?
104
+
105
+ true
106
+ end
70
107
 
71
- parts.pop
72
- parts.pop if parts.last != "artifacts" && parts.length > 3
73
- parts.join("/")
108
+ def find_catalog_markers(start)
109
+ current = start
110
+ 8.times do
111
+ revision_path = File.join(current, REVISION_MARKER)
112
+ git_dir_path = File.join(current, GIT_DIR_MARKER)
113
+ if File.file?(revision_path) && File.file?(git_dir_path)
114
+ revision = File.read(revision_path).strip
115
+ git_dir = File.read(git_dir_path).strip
116
+ return if revision.empty? || !File.exist?(git_dir)
117
+
118
+ return {git_dir: git_dir, revision: revision, work_tree: current}
119
+ end
120
+ parent = File.dirname(current)
121
+ return if parent == current
122
+
123
+ current = parent
124
+ end
125
+ nil
74
126
  end
75
127
  end
76
128
  end
@@ -7,10 +7,11 @@ module Pray
7
7
  GitSourceCheckout = Struct.new(:cache_directory, :revision, :subdir)
8
8
 
9
9
  class GitSourceSet
10
- def initialize(project_root, sources, lockfile, refresh:)
10
+ def initialize(project_root, sources, lockfile, refresh:, offline: false)
11
11
  @project_root = project_root
12
12
  @lockfile = lockfile
13
13
  @refresh = refresh
14
+ @offline = offline
14
15
  @sources = sources.select { |source| source.kind == "git" }.to_h { |source| [source.name, source] }
15
16
  @checkouts = {}
16
17
  end
@@ -21,7 +22,7 @@ module Pray
21
22
  source = @sources[name]
22
23
  return nil unless source
23
24
 
24
- checkout = GitSources.prepare_one_git_source(@project_root, source, @lockfile, refresh: @refresh)
25
+ checkout = GitSources.prepare_one_git_source(@project_root, source, @lockfile, refresh: @refresh, offline: @offline)
25
26
  @checkouts[name] = checkout if checkout
26
27
  checkout
27
28
  end
@@ -38,21 +39,21 @@ module Pray
38
39
  module GitSources
39
40
  module_function
40
41
 
41
- def prepare_git_sources(project_root, sources, lockfile, refresh: false)
42
- GitSourceSet.new(project_root, sources, lockfile, refresh: refresh)
42
+ def prepare_git_sources(project_root, sources, lockfile, refresh: false, offline: false)
43
+ GitSourceSet.new(project_root, sources, lockfile, refresh: refresh, offline: offline)
43
44
  end
44
45
 
45
- def prepare_one_git_source(project_root, source, lockfile, refresh:)
46
+ def prepare_one_git_source(project_root, source, lockfile, refresh:, offline: false)
46
47
  clone_url = source.url.delete_prefix("git+")
47
48
  if local_filesystem_source?(clone_url) && !local_git_repo_path(project_root, clone_url)
48
49
  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: "",
54
- subdir: source.subdir
55
- )
50
+ if source_root
51
+ return GitSourceCheckout.new(
52
+ cache_directory: source_root,
53
+ revision: "",
54
+ subdir: source.subdir
55
+ )
56
+ end
56
57
  end
57
58
 
58
59
  pinned_revision = refresh ? nil : pinned_revision_for_source(lockfile, source)
@@ -61,7 +62,8 @@ module Pray
61
62
  clone_url,
62
63
  refresh: refresh,
63
64
  pinned_revision: pinned_revision,
64
- sparse_subdir: source.subdir
65
+ sparse_subdir: source.subdir,
66
+ offline: offline
65
67
  )
66
68
  GitSourceCheckout.new(
67
69
  cache_directory: cache_directory,
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require_relative "error"
5
+ require_relative "hashing"
6
+ require_relative "git_run"
7
+ require_relative "git_clone"
8
+
9
+ module Pray
10
+ module GitStore
11
+ module_function
12
+
13
+ def ensure_global_git_db(clone_url, pinned_revision:, refresh:, offline:, working_directory:)
14
+ db = global_git_cache_directory(clone_url)
15
+ raise Error.resolution("git object cache is not configured") unless db
16
+
17
+ unless global_git_cache_ready?(db)
18
+ raise Error.resolution(offline_git_source_uncached(clone_url)) if offline
19
+
20
+ FileUtils.rm_rf(db) if File.exist?(db)
21
+ FileUtils.mkdir_p(File.dirname(db))
22
+ GitClone.clone_bare_git_db(working_directory, clone_url, db, quiet: false)
23
+ ensure_git_remote_origin(db, clone_url)
24
+ end
25
+ fetch_origin_tip(db, clone_url) if refresh && !offline
26
+ if pinned_revision
27
+ ensure_revision_in_db(db, clone_url, pinned_revision, !offline)
28
+ return [db, pinned_revision]
29
+ end
30
+
31
+ [db, git_head_revision(db)]
32
+ end
33
+
34
+ def global_cache_root
35
+ return ENV["PRAY_CACHE"] if ENV["PRAY_CACHE"]
36
+ return File.join(ENV["PRAY_HOME"], "cache") if ENV["PRAY_HOME"]
37
+
38
+ home = ENV["HOME"]
39
+ home ? File.join(home, ".cache", "pray") : nil
40
+ end
41
+
42
+ def global_git_cache_directory(clone_url)
43
+ root = global_cache_root
44
+ root ? File.join(root, "git", Hashing.sha256_prefixed(clone_url)[7, 16]) : nil
45
+ end
46
+
47
+ def global_git_cache_ready?(global_cache)
48
+ File.file?(File.join(global_cache, "HEAD")) || File.directory?(File.join(global_cache, ".git"))
49
+ end
50
+
51
+ def offline_git_source_uncached(clone_url)
52
+ "git source #{clone_url} is not cached locally and offline mode is enabled"
53
+ end
54
+
55
+ def ensure_revision_in_db(db, clone_url, revision, allow_fetch)
56
+ return if GitRun.try_run_git(db, "cat-file", "-e", revision)
57
+ unless allow_fetch
58
+ raise Error.resolution(
59
+ "git source #{db.inspect} is locked to revision #{revision}, but that commit is not available locally and offline mode is enabled"
60
+ )
61
+ end
62
+
63
+ ensure_git_remote_origin(db, clone_url)
64
+ fetch_unshallow(db) if File.file?(File.join(db, "shallow"))
65
+ return if GitRun.try_run_git(db, "cat-file", "-e", revision)
66
+
67
+ fetch_revision(db, revision)
68
+ return if GitRun.try_run_git(db, "cat-file", "-e", revision)
69
+
70
+ raise Error.resolution(
71
+ "git source #{db.inspect} is locked to revision #{revision}, but that commit could not be fetched"
72
+ )
73
+ end
74
+
75
+ def ensure_git_remote_origin(repository, clone_url)
76
+ if GitRun.try_run_git(repository, "remote", "get-url", "origin")
77
+ GitRun.run_git(repository, "remote", "set-url", "origin", clone_url)
78
+ else
79
+ GitRun.run_git(repository, "remote", "add", "origin", clone_url)
80
+ end
81
+ end
82
+
83
+ def fetch_origin_tip(db, clone_url)
84
+ ensure_git_remote_origin(db, clone_url)
85
+ unless GitRun.try_run_git(db, "fetch", "--depth", "1", "--filter=blob:none", "origin")
86
+ GitRun.run_git(db, "fetch", "--depth", "1", "origin")
87
+ end
88
+ GitRun.run_git(db, "update-ref", "HEAD", "FETCH_HEAD")
89
+ end
90
+
91
+ def fetch_unshallow(db)
92
+ return if GitRun.try_run_git(db, "fetch", "--unshallow", "--filter=blob:none", "origin")
93
+
94
+ GitRun.run_git(db, "fetch", "--unshallow", "origin")
95
+ end
96
+
97
+ def fetch_revision(db, revision)
98
+ return if GitRun.try_run_git(db, "fetch", "--filter=blob:none", "origin", revision)
99
+
100
+ GitRun.run_git(db, "fetch", "origin", revision)
101
+ end
102
+
103
+ def git_head_revision(repository)
104
+ output, status = GitRun.capture_git(repository, "rev-parse", "HEAD")
105
+ raise Error.resolution(GitRun.command_error("git rev-parse HEAD", output)) unless status.success?
106
+
107
+ revision = output.strip
108
+ raise Error.resolution("git repository has no HEAD revision") if revision.empty?
109
+
110
+ revision
111
+ end
112
+ end
113
+ end
data/lib/pray/publish.rb CHANGED
@@ -28,9 +28,7 @@ module Pray
28
28
  metadata = load_registry_package_metadata(metadata_path, package.declaration.name)
29
29
  existing = metadata.versions.find { |entry| entry.version == package.spec.version }
30
30
  stored_artifact = stored_package_artifact(root, artifact_path, package, existing, distribution)
31
- if stored_artifact && stored_publish_matches?(
32
- stored_artifact, package, signer, signer_fingerprint, existing
33
- )
31
+ if stored_artifact && stored_publish_matches?(stored_artifact, package, existing)
34
32
  package_names << package.declaration.name
35
33
  write_registry_package_metadata(metadata_path, metadata)
36
34
  next
@@ -134,10 +132,17 @@ module Pray
134
132
  artifact_bytes
135
133
  end
136
134
 
137
- def stored_publish_matches?(artifact_bytes, package, signer, signer_fingerprint, existing)
138
- existing.signer == signer &&
139
- existing.signer_fingerprint == signer_fingerprint &&
140
- existing.signature == Registry.registry_artifact_signature(artifact_bytes, package.tree_hash, signer)
135
+ # A stored row is current when its own recorded signature still authenticates the
136
+ # stored artifact. The check deliberately ignores who is publishing now: a row signed
137
+ # by another publisher is still a valid attestation of identical bytes, and rewriting
138
+ # it would restate authorship without republishing anything. A row carrying no
139
+ # signature is not current, so publish repairs it.
140
+ def stored_publish_matches?(artifact_bytes, package, existing)
141
+ return false if existing.signer.nil? || existing.signature.nil?
142
+
143
+ existing.signature == Registry.registry_artifact_signature(
144
+ artifact_bytes, package.tree_hash, existing.signer
145
+ )
141
146
  end
142
147
 
143
148
  def stored_prayspec_matches?(artifact_bytes, package_root)
@@ -19,7 +19,7 @@ module Pray
19
19
  used,
20
20
  output
21
21
  )
22
- splice_existing_managed(remaining, fresh_managed, used, output)
22
+ splice_existing_managed(remaining, fresh_segments, fresh_managed, used, output)
23
23
  append_unused_fresh_managed(fresh_segments, used, output)
24
24
  output.end_with?("\n") ? output : "#{output}\n"
25
25
  end
@@ -36,8 +36,8 @@ module Pray
36
36
  segments.filter_map { |segment| segment[:id] if segment[:kind] == :managed }.to_h { |id| [id, true] }
37
37
  end
38
38
 
39
- def splice_existing_managed(remaining, fresh_managed, used, output)
40
- remaining.each do |segment|
39
+ def splice_existing_managed(remaining, fresh_segments, fresh_managed, used, output)
40
+ remaining.each_with_index do |segment, index|
41
41
  if segment[:kind] == :text
42
42
  output << segment[:text]
43
43
  next
@@ -45,15 +45,47 @@ module Pray
45
45
 
46
46
  used[segment[:id]] = true
47
47
  output << managed_segment(segment[:id], fresh_managed.fetch(segment[:id], segment[:body]))
48
+ next_segment = remaining[index + 1]
49
+ next unless next_segment && next_segment[:kind] == :managed
50
+
51
+ output << text_between(fresh_segments, segment[:id], next_segment[:id])
48
52
  end
49
53
  end
50
54
 
55
+ def text_between(segments, left, right)
56
+ copying = false
57
+ text = +""
58
+ segments.each do |segment|
59
+ if segment[:kind] == :managed
60
+ if segment[:id] == left
61
+ copying = true
62
+ text = +""
63
+ elsif copying
64
+ return (segment[:id] == right) ? text : ""
65
+ end
66
+ elsif copying
67
+ text << segment[:text]
68
+ end
69
+ end
70
+ ""
71
+ end
72
+
51
73
  def append_unused_fresh_managed(fresh_segments, used, output)
74
+ pending = +""
75
+ seen_used = false
52
76
  fresh_segments.each do |segment|
53
- next unless segment[:kind] == :managed
54
- next if used[segment[:id]]
55
-
56
- output << managed_segment(segment[:id], segment[:body])
77
+ if segment[:kind] == :managed
78
+ if used[segment[:id]]
79
+ seen_used = true
80
+ pending = +""
81
+ next
82
+ end
83
+ output << pending
84
+ pending = +""
85
+ output << managed_segment(segment[:id], segment[:body])
86
+ elsif seen_used
87
+ pending << segment[:text]
88
+ end
57
89
  end
58
90
  end
59
91
 
data/lib/pray/resolve.rb CHANGED
@@ -59,7 +59,8 @@ module Pray
59
59
  project_root,
60
60
  manifest.sources,
61
61
  lockfile_hints,
62
- refresh: options.refresh || options.refresh_source_revisions
62
+ refresh: options.refresh || options.refresh_source_revisions,
63
+ offline: options.offline
63
64
  )
64
65
  source_host_keys = Trust.prepare_source_host_keys(manifest.sources)
65
66
 
@@ -10,7 +10,8 @@ module Pray
10
10
  project.project_root,
11
11
  project.manifest.sources,
12
12
  previous,
13
- refresh: options.refresh || options.refresh_source_revisions
13
+ refresh: options.refresh || options.refresh_source_revisions,
14
+ offline: options.offline
14
15
  )
15
16
  sources = Resolve.source_map(project.manifest.sources)
16
17
  lines = []
@@ -15,7 +15,8 @@ module Pray
15
15
  project.project_root,
16
16
  project.manifest.sources,
17
17
  previous,
18
- refresh: options.refresh || options.refresh_source_revisions
18
+ refresh: options.refresh || options.refresh_source_revisions,
19
+ offline: options.offline
19
20
  )
20
21
  sources = Resolve.source_map(project.manifest.sources)
21
22
  plans = []
@@ -10,7 +10,8 @@ module Pray
10
10
  project.project_root,
11
11
  project.manifest.sources,
12
12
  previous,
13
- refresh: options.refresh || options.refresh_source_revisions
13
+ refresh: options.refresh || options.refresh_source_revisions,
14
+ offline: options.offline
14
15
  )
15
16
  sources = Resolve.source_map(project.manifest.sources)
16
17
  changed = false
data/lib/pray/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pray
4
- VERSION = "1.19.0"
5
- GENERATED_BY = "pray 1.19.0"
4
+ VERSION = "1.20.0"
5
+ GENERATED_BY = "pray 1.20.0"
6
6
  end
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.19.0
4
+ version: 1.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -142,6 +142,7 @@ files:
142
142
  - lib/pray/git_refresh.rb
143
143
  - lib/pray/git_run.rb
144
144
  - lib/pray/git_sources.rb
145
+ - lib/pray/git_store.rb
145
146
  - lib/pray/hashing.rb
146
147
  - lib/pray/http_body.rb
147
148
  - lib/pray/invocation.rb