samagotchi 0.149.0 → 0.151.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 +69 -1
- data/README.md +4 -2
- data/docs/architecture.md +4 -4
- data/docs/bundle-authoring.md +1 -1
- data/docs/cli.md +59 -24
- data/docs/configuration.md +6 -6
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/block-renderers.md +96 -0
- data/docs/internals/bridge.md +53 -9
- data/docs/internals/bundles.md +43 -14
- data/docs/internals/objectives.md +7 -2
- data/docs/internals/web-frontend.md +2 -2
- data/docs/internals/web-push.md +2 -2
- data/docs/internals/working-set.md +5 -2
- data/docs/memory.md +48 -10
- data/docs/plugins.md +49 -3
- data/docs/sessions.md +5 -4
- data/docs/testing.md +5 -0
- data/lib/samagotchi/atomic_file.rb +3 -0
- data/lib/samagotchi/block_renderers.rb +115 -0
- data/lib/samagotchi/bootstrap_command.rb +5 -2
- data/lib/samagotchi/bridge/card_places.rb +93 -0
- data/lib/samagotchi/bridge/card_store.rb +224 -82
- data/lib/samagotchi/bridge.rb +12 -4
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +91 -68
- data/lib/samagotchi/bundler_env.rb +24 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -1
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +3 -1
- data/lib/samagotchi/bundles/known-names/manifest.yml +3 -2
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +2 -2
- data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/chi_home.rb +4 -2
- data/lib/samagotchi/chi_shim.rb +2 -1
- data/lib/samagotchi/child_env.rb +10 -3
- data/lib/samagotchi/engine.rb +19 -1
- data/lib/samagotchi/git_env.rb +43 -0
- data/lib/samagotchi/guardrails/context.rb +2 -0
- data/lib/samagotchi/guardrails/targets.rb +4 -1
- data/lib/samagotchi/memory_bundle/asset_installer.rb +23 -3
- data/lib/samagotchi/memory_bundle/builder.rb +5 -3
- data/lib/samagotchi/memory_bundle/file_standing.rb +167 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +70 -6
- data/lib/samagotchi/memory_bundle/installed_copy.rb +1 -0
- data/lib/samagotchi/memory_bundle/installer.rb +162 -52
- data/lib/samagotchi/memory_bundle/listing.rb +4 -3
- data/lib/samagotchi/memory_bundle/manifest.rb +23 -2
- data/lib/samagotchi/memory_bundle/merger.rb +209 -8
- data/lib/samagotchi/memory_bundle/placeholder.rb +2 -1
- data/lib/samagotchi/memory_bundle/profile.rb +7 -3
- data/lib/samagotchi/memory_bundle/provenance.rb +182 -22
- data/lib/samagotchi/memory_bundle/remerge.rb +167 -0
- data/lib/samagotchi/memory_bundle/shipped_history.rb +79 -0
- data/lib/samagotchi/memory_bundle/shipped_update.rb +89 -24
- data/lib/samagotchi/memory_bundle/source.rb +3 -3
- data/lib/samagotchi/memory_bundle/status.rb +21 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +98 -33
- data/lib/samagotchi/memory_bundle/text_merge.rb +399 -0
- data/lib/samagotchi/memory_bundle/trash.rb +58 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +26 -6
- data/lib/samagotchi/objectives_check.rb +37 -7
- data/lib/samagotchi/project_env.rb +3 -2
- data/lib/samagotchi/self_report.rb +2 -0
- data/lib/samagotchi/session_chain.rb +2 -7
- data/lib/samagotchi/session_commands.rb +15 -9
- data/lib/samagotchi/session_manager.rb +10 -7
- data/lib/samagotchi/steer.rb +17 -0
- data/lib/samagotchi/terminal_ui.rb +4 -4
- data/lib/samagotchi/testing/boundary.rb +1 -0
- data/lib/samagotchi/testing/minitest.rb +3 -0
- data/lib/samagotchi/testing/rspec.rb +3 -0
- data/lib/samagotchi/testing/sandbox.rb +3 -1
- data/lib/samagotchi/tools/delegate.rb +2 -1
- data/lib/samagotchi/tools/task_runtime.rb +13 -10
- data/lib/samagotchi/turn_checkpoint.rb +11 -5
- data/lib/samagotchi/turn_flow.rb +2 -1
- data/lib/samagotchi/update_command.rb +76 -28
- data/lib/samagotchi/update_hint.rb +4 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +22 -12
- data/lib/samagotchi/web/auto_update.rb +8 -8
- data/lib/samagotchi/web/auto_update_record.rb +6 -4
- data/lib/samagotchi/web/block_renderer_routes.rb +32 -0
- data/lib/samagotchi/web/markdown_renderer.rb +39 -6
- data/lib/samagotchi/web/message_display.rb +3 -3
- data/lib/samagotchi/web/public/app.js +7 -3
- data/lib/samagotchi/web/public/block_frame.js +202 -0
- data/lib/samagotchi/web/public/block_host.js +502 -0
- data/lib/samagotchi/web/public/block_open.js +13 -0
- data/lib/samagotchi/web/public/block_renderers.css +20 -0
- data/lib/samagotchi/web/public/block_source.js +37 -0
- data/lib/samagotchi/web/public/bubbles.js +11 -5
- data/lib/samagotchi/web/public/card.js +17 -10
- data/lib/samagotchi/web/public/cards_place.js +10 -5
- data/lib/samagotchi/web/public/claim_link.js +4 -2
- data/lib/samagotchi/web/public/details_motion.js +2 -1
- data/lib/samagotchi/web/public/history_reveal.js +4 -1
- data/lib/samagotchi/web/public/index.html +20 -14
- data/lib/samagotchi/web/public/markdown_place.js +2 -3
- data/lib/samagotchi/web/public/stage_model.js +5 -6
- data/lib/samagotchi/web/public/stage_view.js +8 -7
- data/lib/samagotchi/web/public/text_roots.js +6 -0
- data/lib/samagotchi/web/public/turn_block.js +34 -19
- data/lib/samagotchi/web/public/turn_html.js +4 -1
- data/lib/samagotchi/web/public/turn_model.js +1 -1
- data/lib/samagotchi/web/public/update_sheet.js +8 -3
- data/lib/samagotchi/web/public/working_set_view.js +10 -7
- data/lib/samagotchi/web/push/notifier.rb +7 -9
- data/lib/samagotchi/web/session_hub.rb +2 -1
- data/lib/samagotchi/web/session_show.rb +1 -1
- data/lib/samagotchi/web/update_ops.rb +2 -1
- data/lib/samagotchi/worker.rb +4 -2
- metadata +17 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "index_updater"
|
|
4
|
+
require_relative "manifest"
|
|
5
|
+
require_relative "memory_files"
|
|
6
|
+
require_relative "merger"
|
|
7
|
+
require_relative "provenance"
|
|
8
|
+
require_relative "trash"
|
|
9
|
+
require_relative "../model_overlay"
|
|
10
|
+
|
|
11
|
+
module Samagotchi
|
|
12
|
+
module MemoryBundle
|
|
13
|
+
# The retry at the same version, memory files only: nothing else would come
|
|
14
|
+
# back to a file marked conflict until the bundle's next version. It merges
|
|
15
|
+
# what merges and settles those entries, leaving version, installed_at and
|
|
16
|
+
# the assets alone. Not the Installer: that rm_rfs guardrails, plugin and
|
|
17
|
+
# scripts, and GuardrailWiring#rules stats per tool call, so a live session
|
|
18
|
+
# could hit a transient deny.
|
|
19
|
+
#
|
|
20
|
+
# Each marked file, by what Merger::Upgrade says of it:
|
|
21
|
+
# - not shipped now: its mark, base and text stay, and it reads as behind
|
|
22
|
+
# unless level or missing, as every report has it;
|
|
23
|
+
# - the shipped text isn't the one the mark names (a checkout at the same
|
|
24
|
+
# VERSION can carry other text): left alone, behind as its mark says. A
|
|
25
|
+
# mark that names none (older chi) is tried too, the migration; a real
|
|
26
|
+
# conflict then records the digest and legacy_base, so
|
|
27
|
+
# Merger.merge_outcome keeps protecting its file (a merge there may not
|
|
28
|
+
# take a line out). A mark whose digest matches is tried on every run,
|
|
29
|
+
# since the user's file or git may have changed;
|
|
30
|
+
# - merged: replaced as the Installer does, base the incoming file;
|
|
31
|
+
# - noop, keep (a merge that adds nothing too): the file has nothing left
|
|
32
|
+
# to miss, so the mark goes;
|
|
33
|
+
# - conflict: behind, and the mark records why, only when that changed;
|
|
34
|
+
# - fast_forward (one Merger::Upgrade#outcome lets go ahead): the next
|
|
35
|
+
# upgrade's (save_marked_file), behind until then;
|
|
36
|
+
# - install: a missing file, which isn't behind (FileStanding.behind?): the
|
|
37
|
+
# next upgrade or a restore puts it back, and a warning says so;
|
|
38
|
+
# - one it can't read or replace: left as it is, behind, saying why.
|
|
39
|
+
#
|
|
40
|
+
# It can't loop: settling clears the mark, and a real conflict writes nothing.
|
|
41
|
+
# git runs once per marked file per run, and only for a marked file. A run
|
|
42
|
+
# is every chi start for the system bundle (SystemBundle.ensure!) and `chi
|
|
43
|
+
# update` for it and the other bundles chi ships; a bundle chi doesn't ship
|
|
44
|
+
# gets no Remerge, and `chi bundle upgrade` is its retry.
|
|
45
|
+
class Remerge
|
|
46
|
+
# merged: replaced (or, in a dry run, would be). settled: their mark gone
|
|
47
|
+
# with no merge. stamped: their mark was updated. behind: each file that
|
|
48
|
+
# stays behind, and why (FileStanding::Behind). work? says whether a real run
|
|
49
|
+
# would change anything.
|
|
50
|
+
Result = Data.define(:merged, :settled, :stamped, :behind, :warnings, :trash_copy) do
|
|
51
|
+
def work? = [merged, settled, stamped].any?(&:any?)
|
|
52
|
+
end
|
|
53
|
+
NOTHING = Result.new(merged: [], settled: [], stamped: [], behind: [], warnings: [], trash_copy: nil).freeze
|
|
54
|
+
|
|
55
|
+
# @param record [InstalledBundle, nil] the bundle's record (read when not given)
|
|
56
|
+
def initialize(name:, scope:, target_dir:, source_dir:, record: nil)
|
|
57
|
+
@name = name
|
|
58
|
+
@scope = scope
|
|
59
|
+
@target_dir = target_dir
|
|
60
|
+
@source_dir = source_dir
|
|
61
|
+
@provenance = Provenance.new(name: name)
|
|
62
|
+
@record = record || @provenance.record
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @param dry_run [Boolean] answer, writing nothing
|
|
66
|
+
# @return [Result]
|
|
67
|
+
def run(dry_run: false)
|
|
68
|
+
marked = @record ? @record.files.select { |_, entry| entry[:conflict] } : {}
|
|
69
|
+
return NOTHING if marked.empty?
|
|
70
|
+
|
|
71
|
+
@dry_run = dry_run
|
|
72
|
+
@merged = []
|
|
73
|
+
@settled = []
|
|
74
|
+
@stamped = []
|
|
75
|
+
@behind = []
|
|
76
|
+
@warnings = []
|
|
77
|
+
@trash = Trash.new(@name)
|
|
78
|
+
shipped = MemoryFiles.select(@source_dir, Manifest.read(dir: @source_dir))
|
|
79
|
+
marked.each { |key, entry| retry_file(key, entry, shipped) }
|
|
80
|
+
settle unless dry_run
|
|
81
|
+
Result.new(merged: @merged, settled: @settled, stamped: @stamped.map(&:file), behind: @behind, warnings: @warnings, trash_copy: @trash.copy_line)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def retry_file(key, entry, shipped)
|
|
87
|
+
behind = @record.behind_file?(key, view.paths(key)[:current])
|
|
88
|
+
unless shipped.include?(key)
|
|
89
|
+
not_shipped(key, entry) if behind
|
|
90
|
+
return
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
incoming_sha = Merger.digest(view.paths(key)[:incoming])
|
|
94
|
+
if entry[:incoming_sha] && entry[:incoming_sha] != incoming_sha
|
|
95
|
+
@behind << @record.behind_entry(key, entry) if behind
|
|
96
|
+
return
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
outcome = view.outcome(key)
|
|
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
|
|
107
|
+
rescue SystemCallError => e
|
|
108
|
+
stay(key, FileStanding.left_as_it_is(e.message))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def behind_for(key, why) = FileStanding::Behind.new(file: key, version: @record.version, why: why)
|
|
112
|
+
|
|
113
|
+
# The record says it too, so chi bundle status reads as the row does. There
|
|
114
|
+
# is no bundle text to save or compare with: only the words can change.
|
|
115
|
+
def not_shipped(key, entry)
|
|
116
|
+
behind = behind_for(key, FileStanding::NOT_SHIPPED)
|
|
117
|
+
@behind << behind
|
|
118
|
+
@stamped << behind unless @record.behind_entry(key, entry) == behind
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Behind for now, with a reason worth no write to the record.
|
|
122
|
+
def stay(key, why) = @behind << behind_for(key, why)
|
|
123
|
+
|
|
124
|
+
# Behind, and the record says so: a write only when it has something new,
|
|
125
|
+
# so a real conflict at each chi start writes nothing.
|
|
126
|
+
def mark(key, entry, behind)
|
|
127
|
+
@behind << behind
|
|
128
|
+
@stamped << behind unless entry[:incoming_sha] && @record.behind_entry(key, entry) == behind && File.exist?(@provenance.incoming_path(key))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def merge_file(key, entry, outcome)
|
|
132
|
+
return @merged << key if @dry_run
|
|
133
|
+
return @merged << key if Merger.write_merged(view.paths(key)[:current], outcome, @trash)
|
|
134
|
+
|
|
135
|
+
mark(key, entry, behind_for(key, FileStanding.no_merge(TextMerge::RACED)))
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def settle
|
|
139
|
+
files = @merged + @settled
|
|
140
|
+
@provenance.resolve_conflicts(files.to_h { |key| [key, view.paths(key)] }) unless files.empty?
|
|
141
|
+
@provenance.refresh_marks(@stamped, @source_dir) unless @stamped.empty?
|
|
142
|
+
@merged.each { |key| refresh_index(key) }
|
|
143
|
+
note_merges
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def note_merges
|
|
147
|
+
@provenance.note_merged(@merged, version: @record.version, trash: @trash.dir) unless @merged.empty?
|
|
148
|
+
rescue StandardError => e
|
|
149
|
+
@warnings << "merge not noted on the record (#{e.message})"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# The file's size moved; its description and source stay. Not fatal, as
|
|
153
|
+
# in the Installer: the file is in place. An overlay has no line.
|
|
154
|
+
def refresh_index(key)
|
|
155
|
+
return if ModelOverlay.bundle_overlay?(key, bundle_files: @record.files.keys, target_dir: @target_dir)
|
|
156
|
+
|
|
157
|
+
IndexUpdater.update_index(@scope, key.delete_suffix(".md"), File.size(view.paths(key)[:current]), nil, source: @name)
|
|
158
|
+
rescue StandardError => e
|
|
159
|
+
@warnings << "index.md: line for #{key.delete_suffix(".md")} not updated (#{e.message})"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def view
|
|
163
|
+
@view ||= Merger::Upgrade.new(provenance: @provenance, target_dir: @target_dir, source_dir: @source_dir)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require_relative "../chi_home"
|
|
5
|
+
|
|
6
|
+
module Samagotchi
|
|
7
|
+
module MemoryBundle
|
|
8
|
+
# Whether a file's bytes are what some installed samagotchi gem shipped for
|
|
9
|
+
# that file of that bundle. A mark from an older chi may have the user's own
|
|
10
|
+
# text as its base (Merger.merge_outcome); bytes a chi shipped can't be that.
|
|
11
|
+
#
|
|
12
|
+
# Asked only when the guard would otherwise fire, so an ordinary start pays
|
|
13
|
+
# nothing. Reads the gem dirs by hand: the running process's (Gem.path, which
|
|
14
|
+
# a chi home's boot string sets) and the chi home's (a checkout or a stray
|
|
15
|
+
# GEM_PATH runs on other gems than the home's), never loading a gem or asking
|
|
16
|
+
# Gem::Specification. Anything it can't read is no match, and no match keeps
|
|
17
|
+
# the guard: the fallback is the safe side.
|
|
18
|
+
module ShippedHistory
|
|
19
|
+
GEM_DIR = /\Asamagotchi-#{Gem::Version::VERSION_PATTERN}(?:-[a-z0-9_-]+)?\z/
|
|
20
|
+
BUNDLES = File.join("lib", "samagotchi", "bundles")
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
# @param bundle_name [String] the name in the manifest: a shipped dir may
|
|
25
|
+
# have another name in another version
|
|
26
|
+
# @param file_key [String] the file's name in the bundle
|
|
27
|
+
# @param path [String] the file holding the bytes asked about
|
|
28
|
+
# @return [Boolean]
|
|
29
|
+
def shipped?(bundle_name, file_key, path)
|
|
30
|
+
return false if file_key.include?("/")
|
|
31
|
+
|
|
32
|
+
bytes = File.binread(path)
|
|
33
|
+
gem_roots.any? { |root| holds?(root, bundle_name, file_key, bytes) }
|
|
34
|
+
rescue SystemCallError
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Each installed samagotchi gem's folder, wherever it is.
|
|
39
|
+
def gem_roots
|
|
40
|
+
gem_dirs.flat_map do |dir|
|
|
41
|
+
gems = File.join(dir, "gems")
|
|
42
|
+
Dir.children(gems).select { |name| name.match?(GEM_DIR) }.map { |name| File.join(gems, name) }
|
|
43
|
+
rescue SystemCallError
|
|
44
|
+
[]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def gem_dirs = (Gem.path + home_gem_dirs).uniq
|
|
49
|
+
|
|
50
|
+
def home_gem_dirs
|
|
51
|
+
ChiHome.read&.gem_dirs || []
|
|
52
|
+
rescue StandardError
|
|
53
|
+
[]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The size first, the bytes next and the manifest last: 55 gems of 22
|
|
57
|
+
# bundles would be a thousand YAML files, and few candidates get that far.
|
|
58
|
+
def holds?(root, bundle_name, file_key, bytes)
|
|
59
|
+
bundles = File.join(root, BUNDLES)
|
|
60
|
+
Dir.glob("*", base: bundles).any? do |dir|
|
|
61
|
+
file = File.join(bundles, dir, file_key)
|
|
62
|
+
File.file?(file) && File.size(file) == bytes.bytesize && File.binread(file) == bytes &&
|
|
63
|
+
manifest_name(File.dirname(file)) == bundle_name
|
|
64
|
+
rescue SystemCallError
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The name a bundle's manifest gives, nil when it doesn't read. Not
|
|
70
|
+
# Manifest.read: an older manifest needn't pass this chi's checks.
|
|
71
|
+
def manifest_name(dir)
|
|
72
|
+
data = YAML.safe_load_file(File.join(dir, "manifest.yml"))
|
|
73
|
+
data.is_a?(Hash) ? data["name"] : nil
|
|
74
|
+
rescue StandardError
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -6,6 +6,7 @@ require_relative "manifest"
|
|
|
6
6
|
require_relative "merger"
|
|
7
7
|
require_relative "profile"
|
|
8
8
|
require_relative "provenance"
|
|
9
|
+
require_relative "remerge"
|
|
9
10
|
require_relative "source"
|
|
10
11
|
require_relative "status"
|
|
11
12
|
require_relative "system_bundle"
|
|
@@ -22,8 +23,8 @@ module Samagotchi
|
|
|
22
23
|
# newer and this chi meets its requires_chi. It never installs a
|
|
23
24
|
# bundle that isn't installed and never downgrades.
|
|
24
25
|
#
|
|
25
|
-
# .md files take the Installer's 3-way merge: a
|
|
26
|
-
# user's
|
|
26
|
+
# .md files take the Installer's 3-way merge: a clean text merge replaces
|
|
27
|
+
# the file (merged), a real conflict keeps the user's (behind). Hooks, rules and the plugin are replaced; an
|
|
27
28
|
# edited one (which didn't load: its sha no longer matched) is listed
|
|
28
29
|
# in replaced.
|
|
29
30
|
#
|
|
@@ -32,15 +33,28 @@ module Samagotchi
|
|
|
32
33
|
# to install (Profile.to_install, less the ones this chi can't load),
|
|
33
34
|
# even at the same version, so a member that failed is retried. adds
|
|
34
35
|
# names them.
|
|
36
|
+
#
|
|
37
|
+
# At the same version a row retries the files marked conflict (Remerge):
|
|
38
|
+
# :would_merge when that would change anything, then :merged when a file
|
|
39
|
+
# merged; what stays behind reads as behind whichever it is. Not retried for
|
|
40
|
+
# a project-scope bundle: chi bundle upgrade in the project.
|
|
35
41
|
module ShippedUpdate
|
|
36
|
-
# status: :would_update (plan), :updated, :
|
|
37
|
-
# :failed. note says why for skipped/failed
|
|
38
|
-
#
|
|
42
|
+
# status: :would_update, :would_merge (plan), :updated, :merged,
|
|
43
|
+
# :up_to_date, :skipped, :failed. note says why for skipped/failed, and
|
|
44
|
+
# what a retry at the same version warned of. merged and replaced are file
|
|
45
|
+
# names, behind FileStanding::Behind; under plan they are what apply would
|
|
46
|
+
# merge, replace or leave behind.
|
|
39
47
|
# adds is nil but for a profile: the members it installs.
|
|
40
48
|
# kept_own is nil but for a profile that installed a member: its
|
|
41
49
|
# Profile.kept_own_line lines.
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
# trash_copy is nil but under apply when it saved edited files
|
|
51
|
+
# (Installer#trash_copy_line).
|
|
52
|
+
# earlier: the merge notes on its record when the plan read it
|
|
53
|
+
# (FileStanding::MergeNote): this run's own merges aren't among them.
|
|
54
|
+
Row = Struct.new(:name, :from, :to, :status, :note, :merged, :behind, :replaced, :source_dir, :scope, :adds, :kept_own,
|
|
55
|
+
:trash_copy, :earlier, keyword_init: true) do
|
|
56
|
+
def kept = Array(behind).map(&:file)
|
|
57
|
+
end
|
|
44
58
|
|
|
45
59
|
module_function
|
|
46
60
|
|
|
@@ -49,7 +63,11 @@ module Samagotchi
|
|
|
49
63
|
def plan(shipped_dir: SourceNormalizer::SHIPPED_DIR, chi_version: Samagotchi::VERSION)
|
|
50
64
|
shipped = Listing.shipped(dir: shipped_dir).to_h { |s| [s.name, s] }
|
|
51
65
|
installed.map do |name, bundle|
|
|
52
|
-
plan_row(name, bundle, shipped[name], shipped_dir, chi_version)
|
|
66
|
+
row = plan_row(name, bundle, shipped[name], shipped_dir, chi_version)
|
|
67
|
+
# A row that retried nothing still has its files that stay behind.
|
|
68
|
+
row.behind = bundle.behind_files if row.status == :skipped && !bundle.error?
|
|
69
|
+
row.earlier = bundle.merge_notes
|
|
70
|
+
row
|
|
53
71
|
end
|
|
54
72
|
end
|
|
55
73
|
|
|
@@ -69,11 +87,13 @@ module Samagotchi
|
|
|
69
87
|
installed.filter_map { |name, bundle| bundle unless bundle.error? || bundle.profile? || from_chi?(bundle, shipped[name]) }
|
|
70
88
|
end
|
|
71
89
|
|
|
72
|
-
# Runs the Installer for each :would_update row
|
|
73
|
-
# through. A failing bundle doesn't
|
|
90
|
+
# Runs the Installer for each :would_update row, Remerge for each
|
|
91
|
+
# :would_merge row; the others pass through. A failing bundle doesn't
|
|
92
|
+
# stop the rest.
|
|
74
93
|
# @return [Array<Row>]
|
|
75
94
|
def apply(rows)
|
|
76
95
|
rows.map do |row|
|
|
96
|
+
next apply_remerge(row) if row.status == :would_merge
|
|
77
97
|
next row unless row.status == :would_update
|
|
78
98
|
next apply_profile(row) if row.adds
|
|
79
99
|
|
|
@@ -82,16 +102,32 @@ module Samagotchi
|
|
|
82
102
|
installer.run
|
|
83
103
|
row.dup.tap do |r|
|
|
84
104
|
r.status = :updated
|
|
85
|
-
r.
|
|
105
|
+
r.merged = installer.merged_files
|
|
106
|
+
r.behind = installer.behind
|
|
107
|
+
r.trash_copy = installer.trash_copy_line
|
|
86
108
|
end
|
|
87
109
|
rescue StandardError => e
|
|
88
110
|
row.dup.tap do |r|
|
|
89
111
|
r.status = :failed
|
|
90
112
|
r.note = e.message.lines.first.to_s.strip
|
|
113
|
+
r.merged = installer&.merged_files || []
|
|
114
|
+
r.behind = installer&.behind || []
|
|
115
|
+
r.trash_copy = installer&.trash_copy_line
|
|
91
116
|
end
|
|
92
117
|
end
|
|
93
118
|
end
|
|
94
119
|
|
|
120
|
+
def apply_remerge(row)
|
|
121
|
+
done = remerge(row).run
|
|
122
|
+
row.dup.tap do |r|
|
|
123
|
+
r.status = done.merged.empty? ? :up_to_date : :merged
|
|
124
|
+
r.merged = done.merged
|
|
125
|
+
r.behind = done.behind
|
|
126
|
+
r.trash_copy = done.trash_copy
|
|
127
|
+
r.note = note_of(done)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
95
131
|
def apply_profile(row)
|
|
96
132
|
result = Profile.install(row.source_dir, shipped_dir: File.dirname(row.source_dir))
|
|
97
133
|
row.dup.tap do |r|
|
|
@@ -111,7 +147,7 @@ module Samagotchi
|
|
|
111
147
|
def from_chi?(bundle, ship) = !ship.nil? && bundle.source.to_s.match?(SourceNormalizer::SHIPPED_SOURCE)
|
|
112
148
|
|
|
113
149
|
def plan_row(name, bundle, ship, shipped_dir, chi_version)
|
|
114
|
-
row = Row.new(name: name,
|
|
150
|
+
row = Row.new(name: name, merged: [], behind: [], replaced: [])
|
|
115
151
|
return skip(row, "manifest.json unreadable") if bundle.error?
|
|
116
152
|
|
|
117
153
|
row.from = bundle.version.to_s
|
|
@@ -123,7 +159,7 @@ module Samagotchi
|
|
|
123
159
|
|
|
124
160
|
row.source_dir = File.join(shipped_dir, ship.source)
|
|
125
161
|
return profile_row(row, bundle, shipped_dir, chi_version) if Profile.shipped_meta?(row.source_dir, shipped_dir: shipped_dir)
|
|
126
|
-
return row
|
|
162
|
+
return same_version_row(row, bundle) unless Listing.newer?(row.to, row.from)
|
|
127
163
|
|
|
128
164
|
manifest = Manifest.read(dir: row.source_dir)
|
|
129
165
|
if Manifest.requires_chi_failure(manifest.requires_chi, chi_version)
|
|
@@ -132,12 +168,43 @@ module Samagotchi
|
|
|
132
168
|
return skip(row, "project scope: chi bundle upgrade #{ship.source} in the project") if row.scope == "project"
|
|
133
169
|
return skip(row, "unknown scope #{row.scope}: upgrade chi or reinstall") unless MemoryPaths.scope_dir(row.scope)
|
|
134
170
|
|
|
135
|
-
|
|
171
|
+
forecast = forecast(row, bundle)
|
|
172
|
+
row.merged = forecast.merged
|
|
173
|
+
row.behind = forecast.behind
|
|
136
174
|
row.replaced = edited_executables(name, bundle)
|
|
137
175
|
row.status = :would_update
|
|
138
176
|
row
|
|
177
|
+
rescue SystemCallError => e
|
|
178
|
+
# A file it can't read: the upgrade would fail on it, so this is that
|
|
179
|
+
# row, not a crash of the whole plan. Apply passes it through.
|
|
180
|
+
row.status = :failed
|
|
181
|
+
row.note = Trash.failure_message(e)
|
|
182
|
+
row
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Up to date, unless a file marked conflict could settle now or stays behind.
|
|
186
|
+
def same_version_row(row, bundle)
|
|
187
|
+
row.to = nil
|
|
188
|
+
row.status = :up_to_date
|
|
189
|
+
if row.scope == "project" || !MemoryPaths.scope_dir(row.scope)
|
|
190
|
+
row.behind = bundle.behind_files
|
|
191
|
+
return row
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
planned = remerge(row).run(dry_run: true)
|
|
195
|
+
row.merged = planned.merged
|
|
196
|
+
row.behind = planned.behind
|
|
197
|
+
row.note = note_of(planned)
|
|
198
|
+
row.status = :would_merge if planned.work?
|
|
199
|
+
row
|
|
139
200
|
end
|
|
140
201
|
|
|
202
|
+
def remerge(row)
|
|
203
|
+
Remerge.new(name: row.name, scope: row.scope, target_dir: MemoryPaths.scope_dir!(row.scope), source_dir: row.source_dir)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def note_of(remerged) = remerged.warnings.empty? ? nil : remerged.warnings.join("; ")
|
|
207
|
+
|
|
141
208
|
def profile_row(row, bundle, shipped_dir, chi_version)
|
|
142
209
|
manifest = Manifest.read(dir: row.source_dir)
|
|
143
210
|
newer = Listing.newer?(row.to, row.from)
|
|
@@ -162,16 +229,14 @@ module Samagotchi
|
|
|
162
229
|
row
|
|
163
230
|
end
|
|
164
231
|
|
|
165
|
-
#
|
|
166
|
-
# version (a same-name file it didn't install is
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
incoming_path: File.join(row.source_dir, key)) == :conflict
|
|
174
|
-
end
|
|
232
|
+
# What the upgrade would do to the bundle's .md files that local edits and
|
|
233
|
+
# the shipped version both changed (a same-name file it didn't install is
|
|
234
|
+
# skipped, not merged). Every shipped *.md, not MemoryFiles.select: see
|
|
235
|
+
# Merger::Upgrade#forecast.
|
|
236
|
+
def forecast(row, bundle)
|
|
237
|
+
shipped = Dir.glob(File.join(row.source_dir, "*.md")).map { |f| File.basename(f) }.sort
|
|
238
|
+
Merger::Upgrade.new(provenance: Provenance.new(name: row.name), target_dir: MemoryPaths.scope_dir!(row.scope),
|
|
239
|
+
source_dir: row.source_dir).forecast(shipped & bundle.files.keys, version: row.to)
|
|
175
240
|
end
|
|
176
241
|
|
|
177
242
|
# Installed hooks, rules and the plugin whose sha differs from the
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "tmpdir"
|
|
4
4
|
require "fileutils"
|
|
5
|
-
|
|
5
|
+
require_relative "../git_env"
|
|
6
6
|
|
|
7
7
|
module Samagotchi
|
|
8
8
|
module MemoryBundle
|
|
@@ -81,13 +81,13 @@ module Samagotchi
|
|
|
81
81
|
args = ["git", "clone", "--depth", "1"]
|
|
82
82
|
args += ["--branch", ref] if ref && !ref.empty?
|
|
83
83
|
args += [url, clone_dir]
|
|
84
|
-
result = system(*args)
|
|
84
|
+
result = system(GitEnv::UNSET, *args)
|
|
85
85
|
raise UnknownSourceError, "git clone failed for #{git_url}" unless result && File.directory?(clone_dir)
|
|
86
86
|
|
|
87
87
|
# Capture HEAD commit before stripping .git (audit trail)
|
|
88
88
|
commit = nil
|
|
89
89
|
begin
|
|
90
|
-
commit =
|
|
90
|
+
commit = IO.popen(GitEnv::UNSET, ["git", "-C", clone_dir, "rev-parse", "HEAD"], err: File::NULL, &:read).strip
|
|
91
91
|
commit = nil if commit.empty?
|
|
92
92
|
rescue StandardError
|
|
93
93
|
commit = nil
|
|
@@ -18,7 +18,9 @@ module Samagotchi
|
|
|
18
18
|
# disk, its installed plugin, why none of its hooks load, and its needs
|
|
19
19
|
# with what this process's PATH found. The +files+ entries and +plugin+
|
|
20
20
|
# stay Hashes: their fields differ per case (an unchecked file has no
|
|
21
|
-
# paths at all).
|
|
21
|
+
# paths at all). An entry's behind is its FileStanding::Behind, nil for a
|
|
22
|
+
# file that isn't; merge_note its FileStanding::MergeNote, nil without one;
|
|
23
|
+
# unreadable: the file is there, but this chi can't read it.
|
|
22
24
|
Report = Data.define(:provenance, :scope, :scope_error, :target_dir, :files, :plugin,
|
|
23
25
|
:hooks_requires_failure, :needs)
|
|
24
26
|
|
|
@@ -34,14 +36,19 @@ module Samagotchi
|
|
|
34
36
|
target_dir = MemoryPaths.scope_dir(scope)
|
|
35
37
|
files = bundle.files
|
|
36
38
|
details = {}
|
|
39
|
+
behind = bundle.behind_files(target_dir).to_h { |file| [file.file, file] }
|
|
40
|
+
notes = bundle.merge_notes.to_h { |note| [note.file, note] }
|
|
37
41
|
files.each do |file_key_str, meta|
|
|
38
42
|
next details[file_key_str] = unchecked_file(meta) unless target_dir
|
|
39
43
|
|
|
40
44
|
target_path = File.join(target_dir, file_key_str)
|
|
41
45
|
base_path = provenance.base_path(file_key_str)
|
|
42
46
|
stored_checksum = meta[:checksum]
|
|
43
|
-
current_checksum =
|
|
44
|
-
|
|
47
|
+
current_checksum = checksum(target_path)
|
|
48
|
+
unreadable = current_checksum == :unreadable
|
|
49
|
+
current_checksum = nil if unreadable
|
|
50
|
+
base_checksum = checksum(base_path)
|
|
51
|
+
base_checksum = nil if base_checksum == :unreadable
|
|
45
52
|
modified = base_checksum && current_checksum && base_checksum != current_checksum
|
|
46
53
|
missing = !File.exist?(target_path)
|
|
47
54
|
# A model overlay has no index line (Installer#note_overlay).
|
|
@@ -49,6 +56,9 @@ module Samagotchi
|
|
|
49
56
|
index_present = overlay ? nil : index_has_entry?(scope, file_key_str)
|
|
50
57
|
details[file_key_str] = {
|
|
51
58
|
conflict: meta[:conflict] == true,
|
|
59
|
+
behind: behind[file_key_str],
|
|
60
|
+
merge_note: notes[file_key_str],
|
|
61
|
+
unreadable: unreadable,
|
|
52
62
|
stored_checksum: stored_checksum,
|
|
53
63
|
current_checksum: current_checksum,
|
|
54
64
|
base_checksum: base_checksum,
|
|
@@ -65,6 +75,14 @@ module Samagotchi
|
|
|
65
75
|
hooks_requires_failure: hooks_requires_failure(bundle), needs: needs_status(bundle))
|
|
66
76
|
end
|
|
67
77
|
|
|
78
|
+
# The file's digest, nil when there is none, :unreadable when it can't be read.
|
|
79
|
+
def self.checksum(path)
|
|
80
|
+
File.exist?(path) ? Digest::SHA256.file(path).hexdigest : nil
|
|
81
|
+
rescue SystemCallError
|
|
82
|
+
:unreadable
|
|
83
|
+
end
|
|
84
|
+
private_class_method :checksum
|
|
85
|
+
|
|
68
86
|
# Why none of the bundle's hooks load (this chi doesn't meet its
|
|
69
87
|
# requires_chi, Hooks::BundleLoader), or nil: it has no hooks or meets it.
|
|
70
88
|
def self.hooks_requires_failure(bundle)
|