pray-cli 1.20.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 +4 -4
- data/CHANGELOG.md +25 -0
- data/lib/pray/cli/commands/distribution.rb +34 -3
- data/lib/pray/cli/commands/packages.rb +9 -7
- data/lib/pray/cli/commands/update_latest.rb +11 -4
- data/lib/pray/cli/commands/workflow.rb +4 -2
- data/lib/pray/cli/help.rb +16 -1
- data/lib/pray/cli.rb +14 -0
- data/lib/pray/derived_metadata.rb +194 -0
- data/lib/pray/git_cache.rb +46 -19
- data/lib/pray/git_legacy.rb +36 -0
- data/lib/pray/git_materialize.rb +204 -38
- data/lib/pray/git_run.rb +30 -1
- data/lib/pray/git_store.rb +125 -14
- data/lib/pray/package_spec.rb +6 -1
- data/lib/pray/package_spec_paths.rb +18 -0
- data/lib/pray/publish.rb +49 -5
- data/lib/pray/registry.rb +12 -9
- data/lib/pray/sync.rb +1 -4
- data/lib/pray/tar_validation.rb +2 -0
- data/lib/pray/terminal.rb +28 -0
- data/lib/pray/trust_ops.rb +2 -1
- data/lib/pray/upstream_merge.rb +6 -2
- data/lib/pray/upstream_plan.rb +84 -0
- data/lib/pray/version.rb +2 -2
- data/lib/pray.rb +3 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 98ff5ea4c8847034ac02621b75593119ad7b5810fdde65e25998f0702b326a4a
|
|
4
|
+
data.tar.gz: a6f9245375fad4ae3b49eeb71e97cb51ccd4554dcd0e98e7acf64cc8b1d1864c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a7cf4893383addd2ad24b02f36be80543e4fac33ef93b0f606de2ee4370b4be8998b361ed9739d43424aa872e9c1748009fd04f176aa28a64e2cddda15249b3b
|
|
7
|
+
data.tar.gz: f4c71dccd6a7f7faf2e4a266893ff693ffdd10827061c85213c87dacd167fc0482d7fac32e67db6ed868809d6a02dff1c9f4fc8b4275d8f74c511be394bb782e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
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
|
+
|
|
21
|
+
## 1.21.0 (2026-10-21)
|
|
22
|
+
|
|
23
|
+
- Read package catalogs whose `published_at` is an RFC 3339 string or JSON null.
|
|
24
|
+
- Keep prayer sources under `prayers/<name>/`, beside `prayers/v1/` when this project publishes. `prayers/v1/packages/` is catalog metadata. A root `packages/` folder is not a pray source convention.
|
|
25
|
+
- Prevent commands from changing the calling repository when a Git hook passes repository variables such as `GIT_DIR` or `GIT_WORK_TREE`. Explicit Git configuration, including CI credentials and URL rewrites, still applies.
|
|
26
|
+
- Refuse a damaged Git cache that resolves to a parent repository before changing its remote.
|
|
27
|
+
|
|
3
28
|
## 1.20.0 (2026-09-18)
|
|
4
29
|
|
|
5
30
|
- 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.
|
|
@@ -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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
105
|
+
Terminal.println_stdout("Outdated packages")
|
|
106
106
|
if previous_lockfile && Pray.lockfiles_equivalent?(latest_lockfile, previous_lockfile)
|
|
107
|
-
|
|
107
|
+
Terminal.println_stdout("All packages up to date")
|
|
108
108
|
else
|
|
109
|
-
Plan.package_summary_lines(previous_lockfile, latest_lockfile, project).each
|
|
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
|
-
|
|
114
|
-
local_lines.each { |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
|
-
|
|
119
|
-
fork_lines.each { |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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
@@ -207,11 +214,19 @@ module Pray
|
|
|
207
214
|
The default directory is prayers/. Use --path to choose another.
|
|
208
215
|
Declares pray "<source>/<name>" once, such as pray "local/project", inside compose when a compose block exists.
|
|
209
216
|
The default name is project. The name v1 is reserved for the distribution layout.
|
|
217
|
+
Named prayers may sit beside prayers/v1/. Keep sources under prayers/<name>/.
|
|
210
218
|
Without a Prayfile, writes a versioned package spec in the current directory.
|
|
211
219
|
Add spec.version before pray package or pray publish.
|
|
212
220
|
A compose file such as .agents/project.md remains a shortcut for one local file.
|
|
213
221
|
TEXT
|
|
214
|
-
"repo" =>
|
|
222
|
+
"repo" => <<~TEXT.strip,
|
|
223
|
+
scaffold a distribution root
|
|
224
|
+
|
|
225
|
+
Usage: pray repo init
|
|
226
|
+
|
|
227
|
+
Writes prayers/v1/. Git install discovers that catalog. Named prayers may sit beside v1/.
|
|
228
|
+
Keep sources under prayers/<name>/.
|
|
229
|
+
TEXT
|
|
215
230
|
"manifest" => "print canonical Prayfile JSON\n\nUsage: pray manifest",
|
|
216
231
|
"package" => "build a distributable prayer archive\n\nUsage: pray package",
|
|
217
232
|
"version" => <<~TEXT.strip,
|
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
|
data/lib/pray/git_cache.rb
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
33
|
-
return
|
|
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
|
-
|
|
42
|
-
|
|
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
|
|
57
|
-
|
|
58
|
-
return
|
|
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
|
-
|
|
61
|
-
|
|
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
|