pray-cli 1.21.0 → 1.22.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: cdfebfb1eb131a2329908805545aff6330a96381e7790251da11a615cb30174d
4
- data.tar.gz: 10dd49010364b5525e230ec4b567c9fe0fe036ed61e26397db2f6ab8370134cd
3
+ metadata.gz: 98ff5ea4c8847034ac02621b75593119ad7b5810fdde65e25998f0702b326a4a
4
+ data.tar.gz: a6f9245375fad4ae3b49eeb71e97cb51ccd4554dcd0e98e7acf64cc8b1d1864c
5
5
  SHA512:
6
- metadata.gz: 6a25b584cc6561c2454642f23e2e6a7245966e928d10c5c7ff903cdab564e82c0ec0b985918fdc0c5586451b6dce770b4cbb9e330e2a78476fb904fa6bf4e9fe
7
- data.tar.gz: a53cce034d8cca01ab38119f982515b715c72b70675e9d97999d00cbaa43bf5e804eb205a48fb64846d1b1069fc44d28f535ae9d34be75e945eadbc4426944ab
6
+ metadata.gz: a7cf4893383addd2ad24b02f36be80543e4fac33ef93b0f606de2ee4370b4be8998b361ed9739d43424aa872e9c1748009fd04f176aa28a64e2cddda15249b3b
7
+ data.tar.gz: f4c71dccd6a7f7faf2e4a266893ff693ffdd10827061c85213c87dacd167fc0482d7fac32e67db6ed868809d6a02dff1c9f4fc8b4275d8f74c511be394bb782e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 1.22.0 (2026-10-22)
6
+
7
+ - Keep and create catalog `derived_metadata` on publish. Unchanged republishes and version appends no longer strip historical summaries, topics, or embeddings.
8
+ - Ignore leftover project git full clones when resolving a shared bare or stub catalog. Install renames those leftovers so they cannot shadow the locked revision.
9
+ - Print a pray-level recovery hint when a locked git revision is missing from the shared bare or when catalog checkout fails.
10
+ - On path-fork upstream merge conflict, name the reset-then-rebrand workflow and tag the error as `upstream_merge_conflict`. `pray update --help` documents the same steps.
11
+ - Print `published` / `unchanged` lines and a count footer after `pray publish` (same shape as `--dry-run`). Add global `-q`/`--quiet` and `-v`/`--verbose` for non-error status stdout across publish, apply, install, plan, doctor, unlock, update, outdated, and would-conflict. Redact URL userinfo in publish and doctor lines.
12
+ - Conflict when a path fork deleted an upstream file and upstream later changes that file, instead of restoring it silently.
13
+ - Publish shared bare git clones through a temporary directory, `*.lock.d`, and `.pray-ready` marker. Prefer `git show` for lazy catalog fill; set `GIT_NO_LAZY_FETCH` when offline.
14
+ - Bound git catalog archive and `git show` blob fills to the same size ceilings as `.praypkg` unpack; probe sizes before buffering; unpack through the shared tar validators instead of system tar.
15
+ - Publish project git catalogs through staging, an advisory lock, and atomic rename; refuse stub `.pray-git-dir` paths that are not the expected shared bare.
16
+ - Recover stale git cache lock directories after ten minutes; wait up to about one minute for a live lock; name the lock path on timeout.
17
+ - Lock catalog file fill and publish blobs through a partial rename; re-check catalog match under the materialize lock.
18
+ - Refuse absolute federation and torrent artifact URLs; refuse URL-shaped registry and catalog artifact paths; validate prayspec member paths on parse and tree hash.
19
+ - Make `pray update --latest --dry-run` list would-conflict path-fork paths before write.
20
+
3
21
  ## 1.21.0 (2026-10-21)
4
22
 
5
23
  - Read package catalogs whose `published_at` is an RFC 3339 string or JSON null.
@@ -13,13 +13,34 @@ module Pray
13
13
  print_publish_plan(project, dests)
14
14
  return
15
15
  end
16
+ stats = {published: 0, unchanged: 0}
16
17
  dests.each do |dest|
17
18
  packages = selected_publish_packages(project, dest.packages)
18
19
  filtered = project.dup
19
20
  filtered.packages = packages
20
- Publish.publish_to_root(filtered, dest.root) if dest.root
21
- Publish.publish_to_server(filtered, dest.server) if dest.server
21
+ target = dest.root || dest.server || dest.name
22
+ if dest.root
23
+ result = Publish.publish_to_root(
24
+ filtered,
25
+ dest.root,
26
+ dest_name: dest.name,
27
+ target: target
28
+ )
29
+ stats[:published] += result[:published]
30
+ stats[:unchanged] += result[:unchanged]
31
+ end
32
+ if dest.server
33
+ result = Publish.publish_to_server(
34
+ filtered,
35
+ dest.server,
36
+ dest_name: dest.name,
37
+ target: target
38
+ )
39
+ stats[:published] += result[:published]
40
+ stats[:unchanged] += result[:unchanged]
41
+ end
22
42
  end
43
+ print_publish_footer(stats)
23
44
  end
24
45
 
25
46
  def selected_publish_packages(project, listed)
@@ -37,11 +58,21 @@ module Pray
37
58
  packages = selected_publish_packages(project, dest.packages)
38
59
  target = dest.root || dest.server || dest.name
39
60
  packages.each do |package|
40
- puts "publish #{package.declaration.name} #{package.spec.version} -> #{dest.name} (#{target})"
61
+ Terminal.println_stdout(
62
+ "publish #{package.declaration.name} #{package.spec.version} -> #{Publish.redact_publish_target(dest.name)} (#{Publish.redact_publish_target(target)})"
63
+ )
41
64
  end
42
65
  end
43
66
  end
44
67
 
68
+ def print_publish_footer(stats)
69
+ if stats[:published].zero? && stats[:unchanged].zero?
70
+ Terminal.println_stdout("nothing to publish")
71
+ return
72
+ end
73
+ Terminal.println_stdout("#{stats[:published]} published, #{stats[:unchanged]} unchanged")
74
+ end
75
+
45
76
  def serve_command(root:, host:, port:, stdio:, to: nil)
46
77
  raise Error.unsupported("serve --stdio is not implemented yet in pray-cli Ruby") if stdio
47
78
 
@@ -102,21 +102,23 @@ module Pray
102
102
  project = resolve_current_project
103
103
  rendered = Render.render_project(project)
104
104
  latest_lockfile = build_lockfile(project, rendered)
105
- puts "Outdated packages"
105
+ Terminal.println_stdout("Outdated packages")
106
106
  if previous_lockfile && Pray.lockfiles_equivalent?(latest_lockfile, previous_lockfile)
107
- puts "All packages up to date"
107
+ Terminal.println_stdout("All packages up to date")
108
108
  else
109
- Plan.package_summary_lines(previous_lockfile, latest_lockfile, project).each { |line| puts line }
109
+ Plan.package_summary_lines(previous_lockfile, latest_lockfile, project).each do |line|
110
+ Terminal.println_stdout(line)
111
+ end
110
112
  end
111
113
  local_lines = ApplyReport.outdated_local_lines(previous_lockfile, project)
112
114
  unless local_lines.empty?
113
- puts "Outdated local files"
114
- local_lines.each { |line| puts line }
115
+ Terminal.println_stdout("Outdated local files")
116
+ local_lines.each { |line| Terminal.println_stdout(line) }
115
117
  end
116
118
  fork_lines = Upstream.path_fork_drift_lines(project, previous_lockfile, ResolveOptions.new)
117
119
  unless fork_lines.empty?
118
- puts "Outdated path forks"
119
- fork_lines.each { |line| puts line }
120
+ Terminal.println_stdout("Outdated path forks")
121
+ fork_lines.each { |line| Terminal.println_stdout(line) }
120
122
  end
121
123
  end
122
124
  end
@@ -52,7 +52,10 @@ module Pray
52
52
  )
53
53
  print_latest_constraint_plans(manifest_updates, upstream_plans)
54
54
 
55
- return if dry_run
55
+ if dry_run
56
+ Upstream.report_path_upstream_would_conflicts(project, previous, package, options)
57
+ return
58
+ end
56
59
 
57
60
  if !manifest_updates.empty? || !upstream_plans.empty?
58
61
  Upstream.apply_path_upstream_latest_constraints(upstream_plans)
@@ -76,16 +79,20 @@ module Pray
76
79
 
77
80
  def print_latest_constraint_plans(manifest_updates, upstream_plans)
78
81
  if manifest_updates.empty? && upstream_plans.empty?
79
- puts "All package constraints already allow latest versions"
82
+ Terminal.println_stdout("All package constraints already allow latest versions")
80
83
  return
81
84
  end
82
85
 
83
86
  manifest_updates.each do |update|
84
- puts "Prayfile: #{update[:name]} constraint #{update[:from_constraint]} -> #{update[:to_constraint]} " \
87
+ Terminal.println_stdout(
88
+ "Prayfile: #{update[:name]} constraint #{update[:from_constraint]} -> #{update[:to_constraint]} " \
85
89
  "(registry latest #{update[:registry_latest_version]})"
90
+ )
86
91
  end
87
92
  upstream_plans.each do |plan|
88
- puts "#{plan.package_name} upstream #{plan.current_constraint} -> #{plan.new_constraint} (latest #{plan.latest_version})"
93
+ Terminal.println_stdout(
94
+ "#{plan.package_name} upstream #{plan.current_constraint} -> #{plan.new_constraint} (latest #{plan.latest_version})"
95
+ )
89
96
  end
90
97
  end
91
98
  end
@@ -18,7 +18,9 @@ module Pray
18
18
  return if flags[:frozen] || flags[:locked]
19
19
 
20
20
  project = resolve_current_project
21
- ApplyReport.local_summary_lines(previous_lockfile, project).each { |line| puts line }
21
+ ApplyReport.local_summary_lines(previous_lockfile, project).each do |line|
22
+ Terminal.println_stdout(line)
23
+ end
22
24
  end
23
25
 
24
26
  def render_command(flags)
@@ -146,7 +148,7 @@ module Pray
146
148
  merged_lockfile = merge_selected_package_update(previous_lockfile, updated_lockfile, name)
147
149
  Render.write_rendered_targets(project, rendered, previous_lockfile)
148
150
  Pray.write_lockfile(lockfile_path, merged_lockfile)
149
- puts "Unlocked #{name}"
151
+ Terminal.println_stdout("Unlocked #{name}")
150
152
  end
151
153
 
152
154
  def merge_selected_package_update(previous, updated, selected_package)
data/lib/pray/cli/help.rb CHANGED
@@ -52,6 +52,8 @@ module Pray
52
52
 
53
53
  GLOBAL_OPTIONS = [
54
54
  "--no-input disable prompts",
55
+ "-q, --quiet suppress non-error status stdout",
56
+ "-v, --verbose extra status detail on stdout",
55
57
  "--rm use an ephemeral home directory",
56
58
  "--trust [--global] import trust on first use"
57
59
  ].freeze
@@ -122,6 +124,8 @@ module Pray
122
124
  With no package name, update re-resolves the whole graph within constraints
123
125
  and refreshes git source revisions. A package name unlocks that package only.
124
126
  Path-fork packages can refresh from upstream.
127
+ When a branded path fork conflicts, reset conflicting content to the locked upstream,
128
+ run update --latest, re-apply overlays, then bump the fork version.
125
129
  Listed local compose files are re-embedded by pray install, not by update.
126
130
 
127
131
  --latest adjusts constraints to allow the latest package versions.
@@ -151,6 +155,9 @@ module Pray
151
155
  Usage: pray publish [--root PATH] [--server URL ...] [--to NAME] [--dry-run]
152
156
 
153
157
  Prayfile publish remotes supply dests when flags are omitted.
158
+ Live publish prints published or unchanged lines and a short count footer.
159
+ --dry-run prints the planned lines without writing.
160
+ Global -q/--quiet suppresses this stdout; -v/--verbose adds artifact paths.
154
161
  TEXT
155
162
  "login" => <<~TEXT.strip,
156
163
  authenticate to a registry server
data/lib/pray/cli.rb CHANGED
@@ -30,6 +30,20 @@ module Pray
30
30
  if arguments.delete("--no-input")
31
31
  ENV["PRAY_NO_INPUT"] = "1"
32
32
  end
33
+ quiet = false
34
+ verbose = 0
35
+ arguments.reject! do |argument|
36
+ if argument == "--quiet" || argument == "-q"
37
+ quiet = true
38
+ true
39
+ elsif argument == "--verbose" || argument == "-v"
40
+ verbose += 1
41
+ true
42
+ else
43
+ false
44
+ end
45
+ end
46
+ Terminal.configure_output!(quiet: quiet, verbose: verbose)
33
47
  arguments = Invocation.initialize(arguments)
34
48
  return if maybe_print_help(arguments)
35
49
 
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "tmpdir"
5
+ require_relative "hashing"
6
+
7
+ module Pray
8
+ module DerivedMetadata
9
+ module_function
10
+
11
+ EMBEDDING_MODEL = "pray-hash-bucket-v1"
12
+ EMBEDDING_DIMENSIONS = 16
13
+ MAX_SUMMARY_PARTS = 2
14
+ MAX_TOKEN_COUNT = 512
15
+ MAX_SNIPPET_LENGTH = 120
16
+ STOPWORDS = %w[
17
+ a an and are as at be by for from has have in is it its of on or package the this to was we
18
+ with you your
19
+ ].freeze
20
+
21
+ def from_package_root(root, spec)
22
+ analysis = analyze_package_files(root, spec)
23
+ summary_candidates = collect_spec_summary_candidates(spec)
24
+ analysis.fetch(:sample_lines).each { |line| push_candidate(summary_candidates, line) }
25
+ analyzed_text = analysis.fetch(:analyzed_text)
26
+ if summary_candidates.empty?
27
+ top_topics(analyzed_text).each { |token| push_candidate(summary_candidates, token) }
28
+ end
29
+
30
+ tokens = tokenize(analyzed_text)
31
+ topics = top_topics(analyzed_text)
32
+ categories = infer_categories("#{summary_candidates.join(" \n")} #{topics.join(" ")}")
33
+ {
34
+ "summary" => build_summary(summary_candidates),
35
+ "topics" => topics,
36
+ "categories" => categories,
37
+ "possible_effects" => infer_possible_effects(categories),
38
+ "possible_side_effects" => infer_possible_side_effects(analyzed_text, topics),
39
+ "embeddings" => [{"model" => EMBEDDING_MODEL, "vector" => hashed_embedding(tokens)}],
40
+ "file_count" => analysis.fetch(:file_count),
41
+ "character_count" => analysis.fetch(:character_count),
42
+ "token_count" => tokens.length
43
+ }
44
+ end
45
+
46
+ def collect_spec_summary_candidates(spec)
47
+ summary_candidates = []
48
+ push_candidate(summary_candidates, spec.summary)
49
+ push_candidate(summary_candidates, spec.description)
50
+ spec.exports.each_value { |export| push_candidate(summary_candidates, export.summary) }
51
+ summary_candidates
52
+ end
53
+ private_class_method :collect_spec_summary_candidates
54
+
55
+ def analyze_package_files(root, spec)
56
+ analyzed_text = +""
57
+ sample_lines = []
58
+ file_count = 0
59
+ character_count = 0
60
+ Array(spec.files).each do |relative|
61
+ path = File.join(root, relative)
62
+ next unless File.file?(path)
63
+
64
+ normalized = Hashing.normalize_line_endings(File.read(path, encoding: "UTF-8"))
65
+ line = first_meaningful_line(normalized)
66
+ push_candidate(sample_lines, line) if line
67
+ file_count += 1
68
+ character_count += normalized.chars.count
69
+ analyzed_text << normalized << "\n"
70
+ end
71
+ {analyzed_text: analyzed_text, sample_lines: sample_lines, file_count: file_count,
72
+ character_count: character_count}
73
+ end
74
+ private_class_method :analyze_package_files
75
+
76
+ def from_archive_bytes(archive_bytes)
77
+ Dir.mktmpdir("pray-derived-metadata-") do |directory|
78
+ Archive.unpack_praypkg(archive_bytes, directory)
79
+ spec_path = Resolve.find_prayspec_file(directory)
80
+ spec = Pray.parse_package_spec(File.read(spec_path, encoding: "UTF-8")).canonicalized
81
+ from_package_root(directory, spec)
82
+ end
83
+ end
84
+
85
+ def build_summary(candidates)
86
+ summary = []
87
+ seen = {}
88
+ candidates.each do |candidate|
89
+ normalized = candidate.to_s.strip
90
+ next if normalized.empty?
91
+
92
+ key = normalized.downcase
93
+ next if seen[key]
94
+
95
+ seen[key] = true
96
+ summary << truncate(normalized, MAX_SNIPPET_LENGTH)
97
+ break if summary.length == MAX_SUMMARY_PARTS
98
+ end
99
+ return "Package metadata" if summary.empty?
100
+
101
+ summary.join(" — ")
102
+ end
103
+ private_class_method :build_summary
104
+
105
+ def top_topics(text)
106
+ counts = Hash.new(0)
107
+ tokenize(text).each { |token| counts[token] += 1 }
108
+ counts.sort_by { |token, count| [-count, token] }.first(5).map(&:first)
109
+ end
110
+ private_class_method :top_topics
111
+
112
+ def infer_categories(text)
113
+ lowered = text.downcase
114
+ categories = []
115
+ categories << "documentation" if contains_any?(lowered, %w[guide guidance readme documentation doc])
116
+ categories << "testing" if contains_any?(lowered, %w[test testing fixture spec])
117
+ categories << "security" if contains_any?(lowered, %w[auth security ssh key sign trust])
118
+ categories << "automation" if contains_any?(lowered, %w[automation workflow render pipeline agent])
119
+ categories << "general" if categories.empty?
120
+ categories.sort.uniq
121
+ end
122
+ private_class_method :infer_categories
123
+
124
+ def infer_possible_effects(categories)
125
+ effects = []
126
+ effects << "clarifies usage" if categories.include?("documentation")
127
+ effects << "improves validation" if categories.include?("testing")
128
+ effects << "surfaces trust-sensitive behavior" if categories.include?("security")
129
+ effects << "reduces manual steps" if categories.include?("automation")
130
+ effects << "improves package discovery" if effects.empty?
131
+ effects
132
+ end
133
+ private_class_method :infer_possible_effects
134
+
135
+ def infer_possible_side_effects(text, topics)
136
+ side_effects = []
137
+ if contains_any?(text, %w[write overwrite replace render inject delete])
138
+ side_effects << "may change generated output or managed files"
139
+ end
140
+ if contains_any?(topics.join(" "), %w[publish release deploy])
141
+ side_effects << "may affect downstream package distribution"
142
+ end
143
+ side_effects.sort.uniq
144
+ end
145
+ private_class_method :infer_possible_side_effects
146
+
147
+ def hashed_embedding(tokens)
148
+ vector = Array.new(EMBEDDING_DIMENSIONS, 0)
149
+ tokens.first(MAX_TOKEN_COUNT).each do |token|
150
+ digest = Digest::SHA256.digest(token)
151
+ bucket = digest.getbyte(0) % EMBEDDING_DIMENSIONS
152
+ vector[bucket] += 1
153
+ end
154
+ vector
155
+ end
156
+ private_class_method :hashed_embedding
157
+
158
+ def tokenize(text)
159
+ tokens = []
160
+ text.to_s.split(/[^[:alnum:]]+/).each do |raw|
161
+ token = raw.strip.downcase
162
+ next if token.length < 4 || STOPWORDS.include?(token)
163
+
164
+ tokens << token
165
+ break if tokens.length == MAX_TOKEN_COUNT
166
+ end
167
+ tokens
168
+ end
169
+ private_class_method :tokenize
170
+
171
+ def contains_any?(text, needles)
172
+ needles.any? { |needle| text.include?(needle) }
173
+ end
174
+ private_class_method :contains_any?
175
+
176
+ def push_candidate(candidates, candidate)
177
+ trimmed = candidate.to_s.strip
178
+ return if trimmed.empty?
179
+
180
+ candidates << truncate(trimmed, MAX_SNIPPET_LENGTH)
181
+ end
182
+ private_class_method :push_candidate
183
+
184
+ def first_meaningful_line(text)
185
+ text.each_line.map(&:strip).find { |line| !line.empty? }&.then { |line| truncate(line, MAX_SNIPPET_LENGTH) }
186
+ end
187
+ private_class_method :first_meaningful_line
188
+
189
+ def truncate(text, limit)
190
+ text[0...limit].chars.join
191
+ end
192
+ private_class_method :truncate
193
+ end
194
+ end
@@ -5,6 +5,7 @@ require_relative "hashing"
5
5
  require_relative "git_run"
6
6
  require_relative "git_store"
7
7
  require_relative "git_materialize"
8
+ require_relative "git_legacy"
8
9
 
9
10
  module Pray
10
11
  module GitCache
@@ -18,8 +19,16 @@ module Pray
18
19
  offline: offline,
19
20
  working_directory: project_root
20
21
  )
22
+ GitLegacy.quarantine_legacy_project_git_checkouts(project_root, clone_url)
21
23
  catalog = git_source_cache_directory(project_root, clone_url, sparse_subdir)
22
- GitMaterialize.materialize_catalog_tree(db, catalog, revision, sparse_subdir, refresh)
24
+ GitMaterialize.materialize_catalog_tree(
25
+ db,
26
+ catalog,
27
+ revision,
28
+ sparse_subdir,
29
+ refresh,
30
+ offline: offline
31
+ )
23
32
  [catalog, revision]
24
33
  end
25
34
 
@@ -29,20 +38,12 @@ module Pray
29
38
  end
30
39
 
31
40
  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
-
35
- shared = git_source_cache_directory(project_root, clone_url)
36
- return shared if git_checkout?(shared)
37
-
38
- cache_root = File.join(project_root, ".pray", "cache", "git")
39
- return nil unless File.directory?(cache_root)
41
+ expected = GitStore.global_git_cache_directory(clone_url)
42
+ return nil unless expected
43
+ return expected if GitStore.global_git_cache_ready?(expected)
40
44
 
41
- Dir.children(cache_root).each do |name|
42
- path = File.join(cache_root, name)
43
- return path if git_checkout?(path) && origin_matches?(path, clone_url)
44
- end
45
- nil
45
+ stub_git_dir(git_source_cache_directory(project_root, clone_url), expected) ||
46
+ find_stub_git_dir(project_root, expected)
46
47
  end
47
48
 
48
49
  def git_checkout?(path)
@@ -53,12 +54,38 @@ module Pray
53
54
  Hashing.sha256_prefixed(text)[7, 16]
54
55
  end
55
56
 
56
- def origin_matches?(repository, clone_url)
57
- output, status = GitRun.capture_git(repository, "remote", "get-url", "origin")
58
- return false unless status.success?
57
+ def stub_git_dir(catalog, expected)
58
+ marker = File.join(catalog, GitMaterialize::GIT_DIR_MARKER)
59
+ return nil unless File.file?(marker)
60
+
61
+ git_dir = File.read(marker).strip
62
+ return nil if git_dir.empty? || !File.exist?(git_dir)
63
+ return nil unless paths_equal?(git_dir, expected)
64
+
65
+ git_dir
66
+ end
67
+
68
+ def find_stub_git_dir(project_root, expected)
69
+ cache_root = File.join(project_root, ".pray", "cache", "git")
70
+ return nil unless File.directory?(cache_root)
71
+
72
+ Dir.children(cache_root).each do |name|
73
+ path = File.join(cache_root, name)
74
+ next if git_checkout?(path)
75
+
76
+ git_dir = stub_git_dir(path, expected)
77
+ return git_dir if git_dir
78
+ end
79
+ nil
80
+ end
81
+
82
+ def paths_equal?(left, right)
83
+ return true if left == right
59
84
 
60
- origin = output.strip.delete_prefix("git+")
61
- origin == clone_url
85
+ File.realpath(left) == File.realpath(right)
86
+ rescue Errno::ENOENT
87
+ false
62
88
  end
89
+ private_class_method :stub_git_dir, :find_stub_git_dir, :paths_equal?
63
90
  end
64
91
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require_relative "git_run"
5
+
6
+ module Pray
7
+ module GitLegacy
8
+ module_function
9
+
10
+ def quarantine_legacy_project_git_checkouts(project_root, clone_url)
11
+ cache_root = File.join(project_root, ".pray", "cache", "git")
12
+ return unless File.directory?(cache_root)
13
+
14
+ Dir.children(cache_root).each do |name|
15
+ next if name.end_with?(".legacy-git")
16
+
17
+ path = File.join(cache_root, name)
18
+ next unless File.exist?(File.join(path, ".git"))
19
+ next unless origin_matches?(path, clone_url)
20
+
21
+ legacy = File.join(cache_root, "#{name}.legacy-git")
22
+ FileUtils.rm_rf(legacy) if File.exist?(legacy)
23
+ FileUtils.mv(path, legacy)
24
+ end
25
+ end
26
+
27
+ def origin_matches?(repository, clone_url)
28
+ output, status = GitRun.capture_git(repository, "remote", "get-url", "origin")
29
+ return false unless status.success?
30
+
31
+ origin = output.strip.delete_prefix("git+")
32
+ origin == clone_url
33
+ end
34
+ private_class_method :origin_matches?
35
+ end
36
+ end