samagotchi 0.156.0 → 0.157.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 +44 -1
- data/bin/chi +1 -0
- data/docs/architecture.md +12 -7
- data/docs/bundles.md +3 -1
- data/docs/cli.md +9 -8
- data/docs/configuration.md +36 -4
- data/docs/guardrails.md +9 -5
- data/docs/hooks.md +11 -5
- data/docs/internals/bridge.md +83 -6
- data/docs/internals/bundles.md +28 -18
- data/docs/internals/context-telemetry.md +4 -3
- data/docs/internals/engine.md +16 -2
- data/docs/internals/participants.md +3 -2
- data/docs/internals/remote-sessions.md +5 -2
- data/docs/internals/web-frontend.md +3 -2
- data/docs/internals/working-set.md +1 -1
- data/docs/memory.md +13 -2
- data/docs/plugins.md +35 -2
- data/docs/sessions.md +9 -9
- data/docs/sub-agent.md +17 -2
- data/docs/testing.md +53 -2
- data/lib/samagotchi/answer_command.rb +2 -1
- data/lib/samagotchi/archive_store.rb +4 -3
- data/lib/samagotchi/bootstrap_command.rb +3 -2
- data/lib/samagotchi/bridge/card_places.rb +10 -4
- data/lib/samagotchi/bridge/card_store.rb +3 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +109 -9
- data/lib/samagotchi/bridge/turn_drawings.rb +265 -0
- data/lib/samagotchi/bridge.rb +31 -6
- data/lib/samagotchi/broadcast/recipients.rb +8 -8
- data/lib/samagotchi/broadcast/triage_model.rb +1 -1
- data/lib/samagotchi/bundle_command/check.rb +18 -0
- data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +5 -2
- data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
- data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
- data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/child_move.rb +6 -6
- data/lib/samagotchi/child_reports.rb +2 -2
- data/lib/samagotchi/children_status.rb +15 -21
- data/lib/samagotchi/cli/parent_wait.rb +8 -22
- data/lib/samagotchi/config.rb +3 -0
- data/lib/samagotchi/context_sources.rb +5 -4
- data/lib/samagotchi/context_window.rb +9 -12
- data/lib/samagotchi/effective_model.rb +57 -22
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/recap.rb +8 -4
- data/lib/samagotchi/engine.rb +29 -3
- data/lib/samagotchi/events.rb +3 -2
- data/lib/samagotchi/extension_load.rb +6 -3
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/host_address.rb +52 -0
- data/lib/samagotchi/host_registry.rb +57 -45
- data/lib/samagotchi/kernel_loop.rb +7 -3
- data/lib/samagotchi/llm/chat_loop.rb +6 -3
- data/lib/samagotchi/llm_context_strategy.rb +27 -63
- data/lib/samagotchi/memory_bundle/asset_installer.rb +71 -76
- data/lib/samagotchi/memory_bundle/builder.rb +2 -1
- data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
- data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
- data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
- data/lib/samagotchi/memory_bundle/installed_copy.rb +2 -1
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +8 -1
- data/lib/samagotchi/memory_bundle/installer.rb +185 -169
- data/lib/samagotchi/memory_bundle/manifest.rb +14 -4
- data/lib/samagotchi/memory_bundle/merger.rb +69 -65
- data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
- data/lib/samagotchi/memory_bundle/profile.rb +5 -5
- data/lib/samagotchi/memory_bundle/provenance.rb +15 -1
- data/lib/samagotchi/memory_bundle/remerge.rb +25 -12
- data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -6
- data/lib/samagotchi/memory_bundle/status.rb +14 -20
- data/lib/samagotchi/memory_bundle/system_bundle.rb +24 -6
- data/lib/samagotchi/memory_bundle/trash.rb +23 -8
- data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
- data/lib/samagotchi/memory_usage.rb +1 -1
- data/lib/samagotchi/model_lookup.rb +75 -0
- data/lib/samagotchi/model_profile.rb +1 -0
- data/lib/samagotchi/models_command.rb +1 -1
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/participant_mode.rb +2 -1
- data/lib/samagotchi/plugin/api.rb +41 -6
- data/lib/samagotchi/plugin/loader.rb +33 -10
- data/lib/samagotchi/plugin/sessions.rb +1 -1
- data/lib/samagotchi/reasoning_history.rb +14 -20
- data/lib/samagotchi/recap_place.rb +27 -0
- data/lib/samagotchi/reply_wait/liveness.rb +38 -0
- data/lib/samagotchi/reply_wait/poll.rb +44 -15
- data/lib/samagotchi/reply_wait/target.rb +113 -0
- data/lib/samagotchi/reply_wait.rb +22 -2
- data/lib/samagotchi/sampling_settings.rb +9 -16
- data/lib/samagotchi/self_report.rb +34 -35
- data/lib/samagotchi/send_command.rb +17 -23
- data/lib/samagotchi/session.rb +321 -439
- data/lib/samagotchi/session_access/local.rb +5 -6
- data/lib/samagotchi/session_access/remote.rb +5 -3
- data/lib/samagotchi/session_field.rb +133 -0
- data/lib/samagotchi/session_inbox.rb +49 -1
- data/lib/samagotchi/session_manager/emptiness.rb +2 -1
- data/lib/samagotchi/session_manager/listing.rb +29 -100
- data/lib/samagotchi/session_manager.rb +2 -1
- data/lib/samagotchi/session_metrics.rb +7 -4
- data/lib/samagotchi/session_paths.rb +57 -0
- data/lib/samagotchi/session_retention.rb +2 -2
- data/lib/samagotchi/session_row.rb +200 -0
- data/lib/samagotchi/session_setup.rb +15 -12
- data/lib/samagotchi/sessions_command.rb +11 -129
- data/lib/samagotchi/steer_cut_after.rb +7 -14
- data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
- data/lib/samagotchi/terminal_ui/join_replay.rb +86 -8
- data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
- data/lib/samagotchi/terminal_ui.rb +3 -1
- data/lib/samagotchi/testing/install.rb +2 -1
- data/lib/samagotchi/thinking.rb +16 -21
- data/lib/samagotchi/tool_response.rb +5 -3
- data/lib/samagotchi/tool_runner.rb +18 -1
- data/lib/samagotchi/tools/delegate.rb +48 -43
- data/lib/samagotchi/tools/delegate_relay.rb +5 -5
- data/lib/samagotchi/tools/delegate_result.rb +1 -1
- data/lib/samagotchi/tools/delegate_wait.rb +41 -17
- data/lib/samagotchi/tools/list_sessions.rb +7 -7
- data/lib/samagotchi/update_command.rb +11 -7
- data/lib/samagotchi/verify_fresh.rb +7 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +11 -14
- data/lib/samagotchi/web/app.rb +31 -21
- data/lib/samagotchi/web/message_display.rb +57 -7
- data/lib/samagotchi/web/participant_access.rb +1 -1
- data/lib/samagotchi/web/public/activity.js +16 -7
- data/lib/samagotchi/web/public/app.js +29 -8
- data/lib/samagotchi/web/public/bubbles.js +8 -3
- data/lib/samagotchi/web/public/cards_place.js +15 -0
- data/lib/samagotchi/web/public/commands.js +5 -1
- data/lib/samagotchi/web/public/data.js +4 -2
- data/lib/samagotchi/web/public/history_shape.js +220 -0
- data/lib/samagotchi/web/public/recap_stamp.js +17 -0
- data/lib/samagotchi/web/public/reload_check.js +71 -0
- data/lib/samagotchi/web/public/stage_model.js +7 -5
- data/lib/samagotchi/web/public/stage_view.js +7 -3
- data/lib/samagotchi/web/public/stream_handlers.js +8 -2
- data/lib/samagotchi/web/public/timing.js +43 -2
- data/lib/samagotchi/web/public/turn_block.js +215 -30
- data/lib/samagotchi/web/public/turn_drawing.js +43 -0
- data/lib/samagotchi/web/public/turn_events.js +10 -1
- data/lib/samagotchi/web/public/turn_pieces.js +29 -0
- data/lib/samagotchi/web/reload_check_route.rb +95 -0
- data/lib/samagotchi/web/server.rb +4 -2
- data/lib/samagotchi/web/session_access_routes.rb +3 -3
- data/lib/samagotchi/web/session_control.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +9 -17
- data/lib/samagotchi/web/session_show.rb +64 -2
- data/lib/samagotchi/web/session_summary.rb +36 -24
- data/lib/samagotchi/worker.rb +28 -5
- data/lib/samagotchi/worker_link.rb +3 -0
- metadata +20 -4
- data/lib/samagotchi/send_command/wait_target.rb +0 -85
|
@@ -4,6 +4,7 @@ require "digest"
|
|
|
4
4
|
require_relative "../atomic_file"
|
|
5
5
|
require_relative "file_digest"
|
|
6
6
|
require_relative "installed_bundle"
|
|
7
|
+
require_relative "file_actions"
|
|
7
8
|
|
|
8
9
|
module Samagotchi
|
|
9
10
|
module MemoryBundle
|
|
@@ -13,8 +14,8 @@ module Samagotchi
|
|
|
13
14
|
|
|
14
15
|
# 3-way merge helper for bundle upgrades.
|
|
15
16
|
#
|
|
16
|
-
#
|
|
17
|
-
# and
|
|
17
|
+
# An upgrade's merge: what FileState and FileActions' UPGRADE table say of
|
|
18
|
+
# a file, as an Outcome, and the text merge where both sides changed it.
|
|
18
19
|
module Merger
|
|
19
20
|
# What an upgrade does to one file. status names the case; base is where
|
|
20
21
|
# the file's next base comes from:
|
|
@@ -26,15 +27,13 @@ module Samagotchi
|
|
|
26
27
|
# edits, so it must never become the base (the edit is what
|
|
27
28
|
# differs from the base, and the next upgrade would
|
|
28
29
|
# fast-forward over it).
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
# one), #next_base if its base is a new kind, and its status word in
|
|
32
|
-
# Installer#preview_memory, #verify_memory_checksums' list and #summary.
|
|
30
|
+
# The same words as FileActions::Action#next_base: an Outcome is what an
|
|
31
|
+
# UPGRADE cell stands for to a plan, and a :decide cell's answer.
|
|
33
32
|
#
|
|
34
33
|
# merge is Upgrade#outcome's TextMerge::Result, so a merge runs git once
|
|
35
34
|
# and its answer travels with the outcome: a :merged outcome's text, a
|
|
36
35
|
# :conflict's why (nil for a real one). current_sha is the digest of the
|
|
37
|
-
# file that merge read: write_merged checks it.
|
|
36
|
+
# file that merge read: write_merged checks it. The constants leave both nil.
|
|
38
37
|
Outcome = Data.define(:status, :base, :merge, :current_sha) do
|
|
39
38
|
def initialize(status:, base:, merge: nil, current_sha: nil) = super
|
|
40
39
|
|
|
@@ -65,39 +64,38 @@ module Samagotchi
|
|
|
65
64
|
def conflicts = behind.map(&:file)
|
|
66
65
|
end
|
|
67
66
|
|
|
67
|
+
# One file's upgrade, read once (Upgrade#decision): its FileState, the
|
|
68
|
+
# action the upgrade takes (a :decide cell already answered) and the
|
|
69
|
+
# Outcome that action stands for.
|
|
70
|
+
Decision = Data.define(:state, :action, :outcome)
|
|
71
|
+
|
|
68
72
|
# One bundle's upgrade as seen before it runs: what it does to a file,
|
|
69
73
|
# nothing written. The one way to ask: Installer's merge and dry run,
|
|
70
74
|
# ShippedUpdate's plan and SystemBundle's dry run. Which files to ask
|
|
71
75
|
# about is the caller's, and they differ (see #forecast).
|
|
72
76
|
Upgrade = Data.define(:provenance, :target_dir, :source_dir) do
|
|
73
|
-
# What the upgrade does to the file
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
# @return [
|
|
78
|
-
def
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
legacy = InstalledBundle.legacy_mark?(provenance.record&.files&.fetch(file_key, nil))
|
|
83
|
-
# Asked only when the guard would otherwise fire.
|
|
84
|
-
unproved = -> { legacy && !ShippedHistory.shipped?(provenance.name, file_key, paths(file_key)[:base]) }
|
|
85
|
-
return Merger.merge_outcome(paths(file_key), unproved: unproved) if classified.conflict?
|
|
86
|
-
|
|
87
|
-
# The guard's own case at its plainest: the file is the base, which
|
|
88
|
-
# may be the user's text. One that only gains lines loses nothing, and
|
|
89
|
-
# save_marked_file keeps a copy.
|
|
90
|
-
sides = paths(file_key)
|
|
91
|
-
return classified unless legacy && Merger.removes_lines?(File.binread(sides[:current]), File.binread(sides[:incoming]))
|
|
92
|
-
|
|
93
|
-
unproved.call ? CONFLICT.with(merge: TextMerge.failed(FileStanding::LEGACY_BASE)) : classified
|
|
77
|
+
# What the upgrade does to the file, with what it was read from: its
|
|
78
|
+
# FileState, the UPGRADE action (a :decide cell answered by #outcome's
|
|
79
|
+
# merge or legacy guard, through FileActions.decided) and the Outcome.
|
|
80
|
+
# @param state [FileState] the file's, when the caller has read it
|
|
81
|
+
# @return [Decision]
|
|
82
|
+
def decision(file_key, state = state(file_key))
|
|
83
|
+
action = FileActions.for(:upgrade, state)
|
|
84
|
+
outcome = outcome_of(file_key, state, action)
|
|
85
|
+
Decision.new(state: state, action: action.do == :decide ? FileActions.decided(outcome) : action, outcome: outcome)
|
|
94
86
|
end
|
|
95
87
|
|
|
96
|
-
#
|
|
88
|
+
# What the upgrade does to the file: its FileState's UPGRADE cell, and
|
|
89
|
+
# where both sides changed it, the text merge (git runs here, once per
|
|
90
|
+
# call). A mark from an older chi fast-forwards only if no line of the
|
|
91
|
+
# file goes or its base is proved: see .merge_outcome.
|
|
97
92
|
# @return [Outcome]
|
|
98
|
-
def
|
|
99
|
-
|
|
100
|
-
|
|
93
|
+
def outcome(file_key) = decision(file_key).outcome
|
|
94
|
+
|
|
95
|
+
# The file as the upgrade meets it (FileState, with the source).
|
|
96
|
+
# @return [FileState]
|
|
97
|
+
def state(file_key)
|
|
98
|
+
FileState.of(file_key, record: provenance.record, provenance: provenance, target_dir: target_dir, source_dir: source_dir)
|
|
101
99
|
end
|
|
102
100
|
|
|
103
101
|
# The ones among +file_keys+ that would merge or end in a conflict. The
|
|
@@ -138,38 +136,50 @@ module Samagotchi
|
|
|
138
136
|
{ base: provenance.base_path(file_key), current: File.join(target_dir, file_key),
|
|
139
137
|
incoming: File.join(source_dir, file_key) }
|
|
140
138
|
end
|
|
141
|
-
end
|
|
142
139
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
# The Outcome a cell stands for; a :decide cell's is the text merge
|
|
143
|
+
# (:diverged) or the legacy guard ([:pristine, :legacy]).
|
|
144
|
+
def outcome_of(file_key, state, action)
|
|
145
|
+
return NO_INCOMING if state.digests.incoming.nil?
|
|
146
|
+
|
|
147
|
+
case action.do
|
|
148
|
+
when :decide then decide(file_key, state)
|
|
149
|
+
when :write_incoming then action.word == :installed ? INSTALL : FAST_FORWARD
|
|
150
|
+
when :keep
|
|
151
|
+
return no_base(state) if action.behind?
|
|
152
|
+
|
|
153
|
+
action.word == :kept ? KEEP : NOOP
|
|
154
|
+
# A file the bundle doesn't own, the user's: there is no base to merge from.
|
|
155
|
+
when :skip then no_base(state)
|
|
156
|
+
# It can't be read: the read raises, as the merge would.
|
|
157
|
+
when :refuse then FileDigest.hex(paths(file_key)[:current])
|
|
158
|
+
else raise ArgumentError, "no outcome for #{action.do.inspect} (#{file_key})"
|
|
159
|
+
end
|
|
154
160
|
end
|
|
155
161
|
|
|
156
|
-
# No base
|
|
157
|
-
|
|
158
|
-
|
|
162
|
+
# No base to merge from: the file stays behind. With none on disk
|
|
163
|
+
# TextMerge::NO_BASE says why; one the checksum contradicts (the file
|
|
164
|
+
# equals it, yet is edited) may be the user's own text, as an older
|
|
165
|
+
# chi's can (FileStanding::LEGACY_BASE).
|
|
166
|
+
def no_base(state)
|
|
167
|
+
why = state.digests.base.nil? ? TextMerge::NO_BASE : FileStanding::LEGACY_BASE
|
|
168
|
+
CONFLICT.with(merge: TextMerge.failed(why))
|
|
159
169
|
end
|
|
160
170
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
CONFLICT
|
|
171
|
+
def decide(file_key, state)
|
|
172
|
+
# Asked only when the guard would otherwise fire.
|
|
173
|
+
unproved = -> { state.mark == :legacy && !ShippedHistory.shipped?(provenance.name, file_key, paths(file_key)[:base]) }
|
|
174
|
+
return Merger.merge_outcome(paths(file_key), unproved: unproved) unless state.kind == :pristine
|
|
175
|
+
|
|
176
|
+
# The guard's own case at its plainest: the file is the base, which
|
|
177
|
+
# may be the user's text. One that only gains lines loses nothing, and
|
|
178
|
+
# its save_first keeps a copy (Installer#save_first).
|
|
179
|
+
sides = paths(file_key)
|
|
180
|
+
return FAST_FORWARD unless Merger.removes_lines?(File.binread(sides[:current]), File.binread(sides[:incoming]))
|
|
181
|
+
|
|
182
|
+
unproved.call ? CONFLICT.with(merge: TextMerge.failed(FileStanding::LEGACY_BASE)) : FAST_FORWARD
|
|
173
183
|
end
|
|
174
184
|
end
|
|
175
185
|
|
|
@@ -243,12 +253,6 @@ module Samagotchi
|
|
|
243
253
|
def self.digest(path)
|
|
244
254
|
FileDigest.hex(path)
|
|
245
255
|
end
|
|
246
|
-
|
|
247
|
-
def self.current_modified?(base_path, current_path)
|
|
248
|
-
return true unless base_path && File.exist?(base_path) && File.exist?(current_path)
|
|
249
|
-
|
|
250
|
-
FileDigest.hex(base_path) != FileDigest.hex(current_path)
|
|
251
|
-
end
|
|
252
256
|
end
|
|
253
257
|
end
|
|
254
258
|
end
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
module Samagotchi
|
|
4
4
|
module MemoryBundle
|
|
5
5
|
# The plugin an installed bundle's record (manifest.json) names: its
|
|
6
|
-
# file under plugin
|
|
6
|
+
# file under plugin/, the sha256 recorded at install ("" when none
|
|
7
7
|
# is; either "sha256:<hex>" or a bare hex, Provenance.recorded_sha
|
|
8
|
-
# reads both)
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
# reads both), and whether its manifest called it required (a guardrail
|
|
9
|
+
# plugin: plugin: required: true; an older chi ignores the key).
|
|
10
|
+
PluginRef = Data.define(:file, :sha256, :required) do
|
|
11
|
+
def initialize(file:, sha256: "", required: false)
|
|
11
12
|
super
|
|
12
13
|
end
|
|
13
14
|
|
|
@@ -20,7 +21,7 @@ module Samagotchi
|
|
|
20
21
|
|
|
21
22
|
h = raw.transform_keys(&:to_s)
|
|
22
23
|
file = h["file"].to_s
|
|
23
|
-
file.empty? ? nil : new(file: file, sha256: h["sha256"].to_s)
|
|
24
|
+
file.empty? ? nil : new(file: file, sha256: h["sha256"].to_s, required: h["required"] == true)
|
|
24
25
|
end
|
|
25
26
|
end
|
|
26
27
|
end
|
|
@@ -27,7 +27,7 @@ module Samagotchi
|
|
|
27
27
|
# installed: members installed now; already: members found installed
|
|
28
28
|
# (recorded, not reinstalled); skipped and failed: name => why;
|
|
29
29
|
# kept_own: member installed now => its memory files the user already
|
|
30
|
-
# had, left as they are and not owned (
|
|
30
|
+
# had, left as they are and not owned (the INSTALL rows of a file not owned);
|
|
31
31
|
# trash_copies: "member: line" for what --force copied to the trash
|
|
32
32
|
# (Installer#trash_copy_line).
|
|
33
33
|
InstallResult = Struct.new(:name, :version, :installed, :already, :skipped, :failed, :kept_own, :trash_copies, keyword_init: true) do
|
|
@@ -140,11 +140,11 @@ module Samagotchi
|
|
|
140
140
|
private_class_method :install_member
|
|
141
141
|
|
|
142
142
|
# An Installer result for a file the user already had, with other
|
|
143
|
-
# bytes:
|
|
144
|
-
# with the bundle's bytes (
|
|
145
|
-
# of the bundle's is missing (it stays the user's all the same).
|
|
143
|
+
# bytes: kept as the user's own (:kept_own), or a dry run's
|
|
144
|
+
# :would_skip. One with the bundle's bytes (:up_to_date) isn't named:
|
|
145
|
+
# nothing of the bundle's is missing (it stays the user's all the same).
|
|
146
146
|
def kept_own?(entry)
|
|
147
|
-
|
|
147
|
+
%i[would_skip kept_own].include?(entry.word)
|
|
148
148
|
end
|
|
149
149
|
private_class_method :kept_own?
|
|
150
150
|
|
|
@@ -159,6 +159,16 @@ module Samagotchi
|
|
|
159
159
|
unchecked?(recorded) || file_sha(path) == recorded_sha(recorded)
|
|
160
160
|
end
|
|
161
161
|
|
|
162
|
+
# Whether a memory on disk is what the install wrote: the entry's
|
|
163
|
+
# recorded checksum, else its base copy. Unknown counts as edited. The
|
|
164
|
+
# one rule a prune, an uninstall's refusal and chi bundle status read.
|
|
165
|
+
def self.unchanged?(path, entry, base_path)
|
|
166
|
+
recorded = entry[:checksum]
|
|
167
|
+
return sha_matches?(path, recorded) unless recorded_sha(recorded).empty?
|
|
168
|
+
|
|
169
|
+
File.exist?(base_path) && file_sha(path) == file_sha(base_path)
|
|
170
|
+
end
|
|
171
|
+
|
|
162
172
|
# The record's mirror of the manifest keys (InstalledBundle::MIRROR_KEYS)
|
|
163
173
|
# as #write records them from its kwargs: what an older chi reads, and
|
|
164
174
|
# what #adopt_manifest holds a source's manifest against.
|
|
@@ -191,6 +201,9 @@ module Samagotchi
|
|
|
191
201
|
# @param plugin_file [String, nil] the installed plugin file; its name
|
|
192
202
|
# and sha256 are recorded (the Engine checks it at load) with a base
|
|
193
203
|
# snapshot
|
|
204
|
+
# @param plugin_required [Boolean] the manifest's plugin: required: true
|
|
205
|
+
# (a guardrail plugin), recorded with it as "required": true, so the
|
|
206
|
+
# Engine knows it whatever becomes of the kept manifest.yml
|
|
194
207
|
# @param requires_chi [String, nil] the manifest's requirement
|
|
195
208
|
# @param needs [Array<Hash>] the manifest's needs ({command:, why:, hint:})
|
|
196
209
|
# @param marks [Array<FileStanding::Behind>] files an upgrade left behind;
|
|
@@ -217,7 +230,7 @@ module Samagotchi
|
|
|
217
230
|
# an older chi sharing this memories dir reads only them
|
|
218
231
|
# (docs/internals/bundles.md); a key a later manifest gains is never added
|
|
219
232
|
def write(files:, scope:, version:, source_path:, hooks: {}, trust_level: nil, source_commit: nil, hooks_files: {},
|
|
220
|
-
guardrails_files: {}, plugin_file: nil, requires_chi: nil, needs: nil, marks: [], incoming_dir: nil, includes: nil,
|
|
233
|
+
guardrails_files: {}, plugin_file: nil, plugin_required: false, requires_chi: nil, needs: nil, marks: [], incoming_dir: nil, includes: nil,
|
|
221
234
|
scripts_files: {}, context_providers: [], block_renderers: [], file_descriptions: {}, session_settings: [], session_setting_types: {}, settings: nil, setting_types: {},
|
|
222
235
|
dev: false, manifest_from: nil)
|
|
223
236
|
FileUtils.mkdir_p(@bundle_dir)
|
|
@@ -318,6 +331,7 @@ module Samagotchi
|
|
|
318
331
|
content = File.binread(plugin_file.to_s) # the base snapshot's bytes
|
|
319
332
|
manifest_data["plugin"] = { "file" => File.basename(plugin_file.to_s),
|
|
320
333
|
"sha256" => dev ? DEV_SHA : FileDigest.sha256(plugin_file.to_s) }
|
|
334
|
+
manifest_data["plugin"]["required"] = true if plugin_required
|
|
321
335
|
FileUtils.mkdir_p(File.join(bases_dir, "plugin"))
|
|
322
336
|
File.binwrite(plugin_base_path(File.basename(plugin_file.to_s)), content)
|
|
323
337
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "index_updater"
|
|
4
4
|
require_relative "manifest"
|
|
5
5
|
require_relative "memory_files"
|
|
6
|
+
require_relative "file_actions"
|
|
6
7
|
require_relative "merger"
|
|
7
8
|
require_relative "provenance"
|
|
8
9
|
require_relative "trash"
|
|
@@ -17,7 +18,8 @@ module Samagotchi
|
|
|
17
18
|
# scripts, and GuardrailWiring#rules stats per tool call, so a live session
|
|
18
19
|
# could hit a transient deny.
|
|
19
20
|
#
|
|
20
|
-
# Each marked file, by
|
|
21
|
+
# Each marked file, by its FileState and REMERGE row (FileActions; a
|
|
22
|
+
# :decide cell by what Merger::Upgrade says of it):
|
|
21
23
|
# - not shipped now: its mark, base and text stay, and it reads as behind
|
|
22
24
|
# unless level or missing, as every report has it;
|
|
23
25
|
# - the shipped text isn't the one the mark names (a checkout at the same
|
|
@@ -32,7 +34,7 @@ module Samagotchi
|
|
|
32
34
|
# to miss, so the mark goes;
|
|
33
35
|
# - conflict: behind, and the mark records why, only when that changed;
|
|
34
36
|
# - fast_forward (one Merger::Upgrade#outcome lets go ahead): the next
|
|
35
|
-
# upgrade's (
|
|
37
|
+
# upgrade's (its save_first), behind until then;
|
|
36
38
|
# - install: a missing file, which isn't behind (FileStanding.behind?): the
|
|
37
39
|
# next upgrade or a restore puts it back, and a warning says so;
|
|
38
40
|
# - one it can't read or replace: left as it is, behind, saying why.
|
|
@@ -90,24 +92,35 @@ module Samagotchi
|
|
|
90
92
|
return
|
|
91
93
|
end
|
|
92
94
|
|
|
93
|
-
|
|
94
|
-
if
|
|
95
|
+
state = view.state(key)
|
|
96
|
+
if state.source_moved?
|
|
95
97
|
@behind << @record.behind_entry(key, entry) if behind
|
|
96
98
|
return
|
|
97
99
|
end
|
|
98
100
|
|
|
99
|
-
|
|
100
|
-
case outcome.status
|
|
101
|
-
when :merged then merge_file(key, entry, outcome)
|
|
102
|
-
when :conflict then mark(key, entry, view.behind(key, outcome, @record.version))
|
|
103
|
-
when :noop, :keep then @settled << key
|
|
104
|
-
when :fast_forward then mark(key, entry, behind_for(key, FileStanding::WAITS_FOR_UPGRADE))
|
|
105
|
-
when :install then @warnings << "#{key}: #{FileStanding::FILE_MISSING}"
|
|
106
|
-
end
|
|
101
|
+
retry_row(key, entry, state, FileActions.for(:remerge, state))
|
|
107
102
|
rescue SystemCallError => e
|
|
108
103
|
stay(key, FileStanding.left_as_it_is(e.message))
|
|
109
104
|
end
|
|
110
105
|
|
|
106
|
+
# The file's REMERGE row, done. A :decide cell, and a behind one, asks
|
|
107
|
+
# Merger::Upgrade#decision for the Outcome (the text merge, the legacy
|
|
108
|
+
# guard, or why no merge is possible); a fast-forward waits for the
|
|
109
|
+
# upgrade. A missing file is put back by the next upgrade or a restore;
|
|
110
|
+
# one it can't read raises here (left as it is, saying why).
|
|
111
|
+
def retry_row(key, entry, state, action)
|
|
112
|
+
decision = view.decision(key, state) if action.do == :decide || action.behind?
|
|
113
|
+
action = FileActions.decided(decision.outcome, :remerge) if action.do == :decide
|
|
114
|
+
if action.do == :merge then merge_file(key, entry, decision.outcome)
|
|
115
|
+
elsif action.behind?
|
|
116
|
+
waits = decision.outcome.status == :fast_forward
|
|
117
|
+
mark(key, entry, waits ? behind_for(key, FileStanding::WAITS_FOR_UPGRADE) : view.behind(key, decision.outcome, @record.version))
|
|
118
|
+
elsif action.word == :settled then @settled << key
|
|
119
|
+
elsif state.kind == :missing then @warnings << "#{key}: #{FileStanding::FILE_MISSING}"
|
|
120
|
+
elsif state.kind == :unreadable then FileDigest.hex(state.paths[:current])
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
111
124
|
def behind_for(key, why) = FileStanding::Behind.new(file: key, version: @record.version, why: why)
|
|
112
125
|
|
|
113
126
|
# The record says it too, so chi bundle status reads as the row does. There
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "asset_installer"
|
|
3
4
|
require_relative "installer"
|
|
4
5
|
require_relative "listing"
|
|
5
6
|
require_relative "manifest"
|
|
@@ -11,6 +12,7 @@ require_relative "remerge"
|
|
|
11
12
|
require_relative "source"
|
|
12
13
|
require_relative "status"
|
|
13
14
|
require_relative "system_bundle"
|
|
15
|
+
require_relative "trash"
|
|
14
16
|
require_relative "../memory_paths"
|
|
15
17
|
require_relative "../version"
|
|
16
18
|
|
|
@@ -25,9 +27,11 @@ module Samagotchi
|
|
|
25
27
|
# bundle that isn't installed and never downgrades.
|
|
26
28
|
#
|
|
27
29
|
# .md files take the Installer's 3-way merge: a clean text merge replaces
|
|
28
|
-
# the file (merged), a real conflict keeps the user's (behind). Hooks, rules
|
|
29
|
-
#
|
|
30
|
-
#
|
|
30
|
+
# the file (merged), a real conflict keeps the user's (behind). Hooks, rules,
|
|
31
|
+
# the plugin and scripts are replaced; an edited one (Trash.edit?: its
|
|
32
|
+
# bytes neither the recorded sha, none recorded counting as edited, nor
|
|
33
|
+
# the incoming file's) is copied to the trash first and listed in
|
|
34
|
+
# replaced (#trash_bound).
|
|
31
35
|
#
|
|
32
36
|
# A profile (a shipped meta bundle) is updated through Profile: its row
|
|
33
37
|
# is :would_update when the shipped version is newer or it has members
|
|
@@ -41,8 +45,9 @@ module Samagotchi
|
|
|
41
45
|
# a project-scope bundle: chi bundle upgrade in the project.
|
|
42
46
|
module ShippedUpdate
|
|
43
47
|
# status: :would_update, :would_merge (plan), :updated, :merged,
|
|
44
|
-
# :up_to_date, :skipped, :failed. note says why for skipped/failed,
|
|
45
|
-
# what a retry at the same version warned of
|
|
48
|
+
# :up_to_date, :skipped, :failed. note says why for skipped/failed,
|
|
49
|
+
# what a retry at the same version warned of, and an update's Checksum
|
|
50
|
+
# mismatch warnings. merged and replaced are file
|
|
46
51
|
# names, behind FileStanding::Behind; under plan they are what apply would
|
|
47
52
|
# merge, replace or leave behind.
|
|
48
53
|
# adds is nil but for a profile: the members it installs.
|
|
@@ -114,6 +119,10 @@ module Samagotchi
|
|
|
114
119
|
r.merged = installer.merged_files
|
|
115
120
|
r.behind = installer.behind
|
|
116
121
|
r.trash_copy = installer.trash_copy_line
|
|
122
|
+
# The integrity check's warnings; the rest of the Installer's say
|
|
123
|
+
# what the row's merged and behind already do.
|
|
124
|
+
mismatches = installer.warnings.grep(/\AChecksum mismatch/)
|
|
125
|
+
r.note = mismatches.join("; ") unless mismatches.empty?
|
|
117
126
|
end
|
|
118
127
|
rescue StandardError => e
|
|
119
128
|
row.dup.tap do |r|
|
|
@@ -199,7 +208,7 @@ module Samagotchi
|
|
|
199
208
|
forecast = forecast(row, bundle)
|
|
200
209
|
row.merged = forecast.merged
|
|
201
210
|
row.behind = forecast.behind
|
|
202
|
-
row.replaced =
|
|
211
|
+
row.replaced = trash_bound(name, bundle, manifest, row.source_dir)
|
|
203
212
|
row.status = :would_update
|
|
204
213
|
row
|
|
205
214
|
rescue SystemCallError => e
|
|
@@ -267,6 +276,42 @@ module Samagotchi
|
|
|
267
276
|
source_dir: row.source_dir).forecast(shipped & bundle.files.keys, version: row.to)
|
|
268
277
|
end
|
|
269
278
|
|
|
279
|
+
# What the upgrade from +source_dir+ copies to the trash, as
|
|
280
|
+
# hooks/F, guardrails/F, plugin/F, scripts/F: Trash.edit? over the files
|
|
281
|
+
# AssetInstaller replaces or removes, each against the file it puts in
|
|
282
|
+
# its place. Hooks: the ones install_hooks copies (listed, else every
|
|
283
|
+
# hooks/*.rb; present, not links) and the ones prune_dropped_hooks
|
|
284
|
+
# removes (recorded, no longer shipped; no incoming). Rules, the plugin
|
|
285
|
+
# and scripts: their whole folder, which the upgrade replaces.
|
|
286
|
+
def trash_bound(name, bundle, manifest, source_dir)
|
|
287
|
+
provenance = Provenance.new(name: name)
|
|
288
|
+
rules = Dir.glob(File.join(source_dir, "guardrails", "*.yml")).sort
|
|
289
|
+
.reject { |file| AssetInstaller.linked?(file, source_dir) }
|
|
290
|
+
plugin = manifest.plugin ? { manifest.plugin[:file] => File.join(source_dir, manifest.plugin[:file]) } : {}
|
|
291
|
+
scripts = manifest.scripts.keys.to_h { |file| [file, File.join(source_dir, "scripts", file)] }
|
|
292
|
+
trash_bound_hooks(provenance, bundle, manifest, source_dir) +
|
|
293
|
+
Trash.edits(provenance.guardrails_dir, "guardrails", bundle.guardrails, rules.to_h { |file| [File.basename(file), file] }) +
|
|
294
|
+
Trash.edits(provenance.plugin_dir, "plugin", bundle.plugin_shas, plugin) +
|
|
295
|
+
Trash.edits(provenance.scripts_dir, "scripts", bundle.scripts, scripts)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# #trash_bound's hooks: AssetInstaller#install_hooks' set (incoming: the
|
|
299
|
+
# source's file), then #prune_dropped_hooks' (incoming: none). A stray
|
|
300
|
+
# file in hooks/ is in neither: the upgrade leaves it.
|
|
301
|
+
def trash_bound_hooks(provenance, bundle, manifest, source_dir)
|
|
302
|
+
src = ->(file) { File.join(source_dir, "hooks", file) }
|
|
303
|
+
listed = manifest.hooks.keys.select { |file| file.is_a?(String) && !file.empty? }
|
|
304
|
+
listed = Dir.glob(src.call("*.rb")).map { |file| File.basename(file) } if manifest.hooks.empty?
|
|
305
|
+
shipped = listed.reject do |file|
|
|
306
|
+
(File.symlink?(src.call(file)) || File.exist?(src.call(file))) && AssetInstaller.linked?(src.call(file), source_dir)
|
|
307
|
+
end
|
|
308
|
+
incoming = shipped.select { |file| File.exist?(src.call(file)) }.to_h { |file| [file, src.call(file)] }
|
|
309
|
+
incoming.merge!((bundle.hooks.keys - shipped).to_h { |file| [file, nil] })
|
|
310
|
+
incoming.filter_map do |file, from|
|
|
311
|
+
"hooks/#{file}" if Trash.edit?(File.join(provenance.hooks_dir, file), recorded: bundle.hooks[file]&.sha256, incoming: from)
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
270
315
|
# Installed hooks, rules and the plugin whose sha differs from the
|
|
271
316
|
# recorded one (they didn't load), as hooks/F, guardrails/F, plugin/F.
|
|
272
317
|
def edited_executables(name, bundle)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "file_digest"
|
|
4
4
|
require_relative "provenance"
|
|
5
|
+
require_relative "file_state"
|
|
5
6
|
require_relative "manifest"
|
|
6
7
|
require_relative "../version"
|
|
7
8
|
require_relative "index_updater"
|
|
@@ -41,16 +42,12 @@ module Samagotchi
|
|
|
41
42
|
files.each do |file_key_str, meta|
|
|
42
43
|
next details[file_key_str] = unchecked_file(meta) unless target_dir
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
base_checksum = checksum(base_path)
|
|
51
|
-
base_checksum = nil if base_checksum == :unreadable
|
|
52
|
-
modified = base_checksum && current_checksum && base_checksum != current_checksum
|
|
53
|
-
missing = !File.exist?(target_path)
|
|
45
|
+
state = FileState.of(file_key_str, record: bundle, provenance: provenance, target_dir: target_dir, source_dir: nil)
|
|
46
|
+
target_path = state.paths[:current]
|
|
47
|
+
base_path = state.paths[:base]
|
|
48
|
+
unreadable = state.kind == :unreadable
|
|
49
|
+
missing = state.kind == :missing
|
|
50
|
+
modified = %i[edited edited_no_base].include?(state.kind)
|
|
54
51
|
# A model overlay has no index line (Installer#note_overlay).
|
|
55
52
|
overlay = ModelOverlay.bundle_overlay?(file_key_str, bundle_files: files.keys, target_dir: target_dir)
|
|
56
53
|
index_present = overlay ? nil : index_has_entry?(scope, file_key_str)
|
|
@@ -59,9 +56,9 @@ module Samagotchi
|
|
|
59
56
|
behind: behind[file_key_str],
|
|
60
57
|
merge_note: notes[file_key_str],
|
|
61
58
|
unreadable: unreadable,
|
|
62
|
-
stored_checksum:
|
|
63
|
-
current_checksum:
|
|
64
|
-
base_checksum:
|
|
59
|
+
stored_checksum: meta[:checksum],
|
|
60
|
+
current_checksum: digest_or_nil(state.digests.disk),
|
|
61
|
+
base_checksum: digest_or_nil(state.digests.base),
|
|
65
62
|
modified: modified,
|
|
66
63
|
missing: missing,
|
|
67
64
|
index_present: index_present,
|
|
@@ -75,13 +72,10 @@ module Samagotchi
|
|
|
75
72
|
hooks_requires_failure: hooks_requires_failure(bundle), needs: needs_status(bundle))
|
|
76
73
|
end
|
|
77
74
|
|
|
78
|
-
#
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
:unreadable
|
|
83
|
-
end
|
|
84
|
-
private_class_method :checksum
|
|
75
|
+
# A FileState digest as the Report has it: nil for none, or one that
|
|
76
|
+
# can't be read (unreadable says which).
|
|
77
|
+
def self.digest_or_nil(digest) = digest == :unreadable ? nil : digest
|
|
78
|
+
private_class_method :digest_or_nil
|
|
85
79
|
|
|
86
80
|
# Why none of the bundle's hooks load (this chi doesn't meet its
|
|
87
81
|
# requires_chi, Hooks::BundleLoader), or nil: it has no hooks or meets it.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require_relative "installer"
|
|
5
|
+
require_relative "file_digest"
|
|
5
6
|
require_relative "provenance"
|
|
6
7
|
require_relative "manifest"
|
|
7
8
|
require_relative "index_updater"
|
|
@@ -204,29 +205,46 @@ module Samagotchi
|
|
|
204
205
|
# A file from the bundle missing on disk (the user deleted it) is put
|
|
205
206
|
# back, and nothing else: at the same version a checkout can carry other
|
|
206
207
|
# text, so no other file is merged here (Remerge has its guard), and a live
|
|
207
|
-
# session's guardrails, plugin and scripts stay where they are.
|
|
208
|
+
# session's guardrails, plugin and scripts stay where they are. Which
|
|
209
|
+
# missing file is put back is its RESTORE row (FileActions): one the
|
|
210
|
+
# record doesn't own too. A put-back file is checked against the
|
|
211
|
+
# manifest's files: sha, as an installed one is (a bad copy warns).
|
|
208
212
|
def verify_files_present(gem_manifest, version, dry_run)
|
|
209
213
|
target_dir = MemoryPaths.system_dir
|
|
210
214
|
missing = MemoryFiles.select(GEM_BUNDLE_DIR, gem_manifest).reject { |key| File.exist?(File.join(target_dir, key)) }
|
|
215
|
+
missing = restored_rows(missing, target_dir) unless missing.empty?
|
|
211
216
|
return result(:up_to_date, from: version, to: version) if missing.empty?
|
|
212
217
|
return result(:restored, from: version, to: version) if dry_run
|
|
213
218
|
|
|
214
|
-
warnings = missing.
|
|
219
|
+
warnings = missing.flat_map { |key| restore_file(key, gem_manifest) }
|
|
215
220
|
result(:restored, from: version, to: version, warnings: warnings)
|
|
216
221
|
end
|
|
217
222
|
|
|
218
|
-
#
|
|
219
|
-
def
|
|
223
|
+
# The ones among +keys+ whose FileState's RESTORE row restores.
|
|
224
|
+
def restored_rows(keys, target_dir)
|
|
225
|
+
provenance = Provenance.new(name: BUNDLE_NAME)
|
|
226
|
+
record = provenance.record
|
|
227
|
+
keys.select do |key|
|
|
228
|
+
state = FileState.of(key, record: record, provenance: provenance, target_dir: target_dir, source_dir: GEM_BUNDLE_DIR)
|
|
229
|
+
FileActions.for(:restore, state)&.do == :restore
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# @return [Array<String>] warnings: its bytes don't match the manifest's
|
|
234
|
+
# sha, or the file is in place and its index line isn't
|
|
235
|
+
def restore_file(key, gem_manifest)
|
|
220
236
|
source = File.join(GEM_BUNDLE_DIR, key)
|
|
221
237
|
target = File.join(MemoryPaths.system_dir, key)
|
|
238
|
+
description = gem_manifest.file_descriptions[key]
|
|
222
239
|
FileUtils.cp(source, target)
|
|
223
240
|
Provenance.new(name: BUNDLE_NAME).record_restored(key, source, description: description)
|
|
241
|
+
warnings = [FileDigest.mismatch(key, target, gem_manifest.checksum_for(key))].compact
|
|
224
242
|
begin
|
|
225
243
|
IndexUpdater.update_index(SCOPE, key.delete_suffix(".md"), File.size(target), description, source: BUNDLE_NAME)
|
|
226
|
-
nil
|
|
227
244
|
rescue StandardError => e
|
|
228
|
-
"index.md: line for #{key.delete_suffix(".md")} not updated (#{e.message})"
|
|
245
|
+
warnings << "index.md: line for #{key.delete_suffix(".md")} not updated (#{e.message})"
|
|
229
246
|
end
|
|
247
|
+
warnings
|
|
230
248
|
end
|
|
231
249
|
|
|
232
250
|
# An up-to-date bundle's marked files, retried. No marks: nothing is read.
|
|
@@ -105,11 +105,7 @@ module Samagotchi
|
|
|
105
105
|
# +incoming+'s. A link (a dev install's) points at its source, which
|
|
106
106
|
# nothing here touches.
|
|
107
107
|
def copy_edit(path, as:, recorded: nil, incoming: nil)
|
|
108
|
-
|
|
109
|
-
return if incoming && File.file?(incoming) && FileUtils.identical?(incoming, path)
|
|
110
|
-
return if !Provenance.recorded_sha(recorded).empty? && Provenance.sha_matches?(path, recorded)
|
|
111
|
-
|
|
112
|
-
copy(path, as: as)
|
|
108
|
+
copy(path, as: as) if self.class.edit?(path, recorded: recorded, incoming: incoming)
|
|
113
109
|
end
|
|
114
110
|
|
|
115
111
|
# copy_edit for every file under +dir+, which is about to go, at any depth
|
|
@@ -117,10 +113,29 @@ module Samagotchi
|
|
|
117
113
|
# @param recorded [Hash, nil] path in dir => its recorded sha256
|
|
118
114
|
# @param incoming [Hash] path in dir => the source replacing it
|
|
119
115
|
def copy_edits(dir, label, recorded = nil, incoming = {})
|
|
120
|
-
|
|
116
|
+
self.class.edited_in(dir, recorded, incoming).each { |file| copy(File.join(dir, file), as: "#{label}/#{file}") }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Whether #copy_edit would copy the file at +path+ (read-only): the rule
|
|
120
|
+
# ShippedUpdate's plan names its replaced files by.
|
|
121
|
+
def self.edit?(path, recorded: nil, incoming: nil)
|
|
122
|
+
return false if File.symlink?(path) || !File.file?(path)
|
|
123
|
+
return false if incoming && File.file?(incoming) && FileUtils.identical?(incoming, path)
|
|
124
|
+
|
|
125
|
+
Provenance.recorded_sha(recorded).empty? || !Provenance.sha_matches?(path, recorded)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# What #copy_edits would copy (read-only), as <label>/<its path in dir>.
|
|
129
|
+
def self.edits(dir, label, recorded = nil, incoming = {})
|
|
130
|
+
edited_in(dir, recorded, incoming).map { |file| "#{label}/#{file}" }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# The files under +dir+ (paths in it) #copy_edits copies.
|
|
134
|
+
def self.edited_in(dir, recorded, incoming)
|
|
135
|
+
return [] if File.symlink?(dir) || !File.directory?(dir)
|
|
121
136
|
|
|
122
|
-
Dir.glob("**/*", File::FNM_DOTMATCH, base: dir).sort.
|
|
123
|
-
|
|
137
|
+
Dir.glob("**/*", File::FNM_DOTMATCH, base: dir).sort.select do |file|
|
|
138
|
+
edit?(File.join(dir, file), recorded: recorded&.[](file), incoming: incoming[file])
|
|
124
139
|
end
|
|
125
140
|
end
|
|
126
141
|
|