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
|
@@ -34,6 +34,9 @@ module Samagotchi
|
|
|
34
34
|
|
|
35
35
|
attr_reader :results, :warnings, :placeholder_warnings, :conflicts
|
|
36
36
|
|
|
37
|
+
# Each file this run left behind (FileStanding::Behind).
|
|
38
|
+
attr_reader :behind
|
|
39
|
+
|
|
37
40
|
# prepared: the source already normalized (a PreparedSource, which the
|
|
38
41
|
# Installer then owns and cleans up as its own); nil normalizes source.
|
|
39
42
|
# shipped_dir: the bundles this chi ships (a bundle of their names
|
|
@@ -55,6 +58,9 @@ module Samagotchi
|
|
|
55
58
|
@warnings = []
|
|
56
59
|
@placeholder_warnings = []
|
|
57
60
|
@conflicts = {}
|
|
61
|
+
@behind = []
|
|
62
|
+
@replaced = []
|
|
63
|
+
@outcomes = {}
|
|
58
64
|
@overlays = []
|
|
59
65
|
@trash = Trash.new(name)
|
|
60
66
|
end
|
|
@@ -63,10 +69,16 @@ module Samagotchi
|
|
|
63
69
|
# source (Installer#check_source), else nil.
|
|
64
70
|
attr_reader :source_change
|
|
65
71
|
|
|
66
|
-
# Where
|
|
67
|
-
# none
|
|
72
|
+
# Where this install moved the files the bundle no longer ships and
|
|
73
|
+
# copied the ones it overwrote (nil: none).
|
|
68
74
|
def trash_dir = @trash.dir
|
|
69
75
|
|
|
76
|
+
# What it copied to the trash before overwriting, said once (nil: nothing).
|
|
77
|
+
def trash_copy_line = @trash.copy_line
|
|
78
|
+
|
|
79
|
+
# The files this run merged, the bundle's changes joined to the user's.
|
|
80
|
+
def merged_files = @results.select { |_, r| r[:status] == "merged" }.keys
|
|
81
|
+
|
|
70
82
|
# The normalized source dir. An owned one (a git/zip/tar extract) is
|
|
71
83
|
# removed after #run, or kept until #cleanup_source! when an upgrade
|
|
72
84
|
# has conflicts (the agent step reads the incoming files); nil once
|
|
@@ -94,16 +106,7 @@ module Samagotchi
|
|
|
94
106
|
check_source
|
|
95
107
|
# A plain install over an installed bundle: summary points at upgrade.
|
|
96
108
|
@reinstall = @existing && !@upgrade && !@force && !@dry_run
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
installed = AssetInstaller.new(
|
|
100
|
-
name: @name, source_dir: @bundle_dir, manifest: @manifest, provenance: @provenance, existing: @existing,
|
|
101
|
-
results: @results, warnings: @warnings, conflicts: @conflicts,
|
|
102
|
-
force: @force, strict: @strict, upgrade: @upgrade, dry_run: @dry_run, dev: @dev
|
|
103
|
-
).run
|
|
104
|
-
|
|
105
|
-
# Files the previous version shipped and this one doesn't.
|
|
106
|
-
prune_dropped_files(@existing.files, @bundle_md, @target_dir, @target_scope, @provenance) if @upgrade && @existing
|
|
109
|
+
installed = replace_files
|
|
107
110
|
verify_memory_checksums if @manifest && @strict
|
|
108
111
|
warn_missing_needs(@manifest) if @manifest
|
|
109
112
|
if @manifest && !@dry_run
|
|
@@ -113,6 +116,7 @@ module Samagotchi
|
|
|
113
116
|
|
|
114
117
|
[@bundle_dir, @manifest]
|
|
115
118
|
ensure
|
|
119
|
+
note_merges
|
|
116
120
|
# An owned source (git/zip/tar) is cleaned up here, unless an upgrade
|
|
117
121
|
# kept conflicts: the agent step still needs the incoming files, so
|
|
118
122
|
# the caller runs #cleanup_source! when it is done.
|
|
@@ -131,9 +135,10 @@ module Samagotchi
|
|
|
131
135
|
lines << "Source changes: #{@source_change.join(" → ")}" if @source_change
|
|
132
136
|
installed = @results.select { |_, r| r[:status].to_s == "installed" }.keys
|
|
133
137
|
updated = @results.select { |_, r| r[:status].to_s == "updated" }.keys
|
|
134
|
-
|
|
138
|
+
merged = merged_files
|
|
139
|
+
would_merge = @results.select { |_, r| r[:status].to_s == "would_merge" }.keys
|
|
140
|
+
kept = @results.select { |_, r| %w[kept keep kept_pruned].include?(r[:status].to_s) }.keys
|
|
135
141
|
skipped = @results.select { |_, r| %w[skipped noop would_skip].include?(r[:status].to_s) }.keys
|
|
136
|
-
conflicts = @results.select { |_, r| r[:status].to_s == "conflict" }.keys
|
|
137
142
|
would_install = @results.select { |_, r| r[:status].to_s == "would_install" }.keys
|
|
138
143
|
fast_forward = @results.select { |_, r| r[:status].to_s == "fast_forward" }.keys
|
|
139
144
|
removed = @results.select { |_, r| r[:status].to_s == "removed" }.keys
|
|
@@ -143,11 +148,13 @@ module Samagotchi
|
|
|
143
148
|
lines << "Updated: #{updated.join(", ")}" unless updated.empty?
|
|
144
149
|
lines << "Would install: #{would_install.join(", ")}" unless would_install.empty?
|
|
145
150
|
lines << "Fast-forward: #{fast_forward.join(", ")}" unless fast_forward.empty?
|
|
151
|
+
lines << FileStanding.merged_line(merged, @manifest&.version) unless merged.empty?
|
|
152
|
+
lines << FileStanding.merged_line(would_merge, @manifest&.version, dry_run: true) unless would_merge.empty?
|
|
146
153
|
lines << "Removed (no longer in the bundle): #{removed.join(", ")}#{" (moved to #{trash_dir})" if trash_dir}" unless removed.empty?
|
|
147
154
|
lines << "Would remove (no longer in the bundle): #{would_remove.join(", ")}" unless would_remove.empty?
|
|
148
155
|
lines << "Kept (local edits preserved): #{kept.join(", ")}" unless kept.empty?
|
|
149
156
|
lines << "Skipped: #{skipped.join(", ")}" unless skipped.empty?
|
|
150
|
-
lines <<
|
|
157
|
+
lines << trash_copy_line if trash_copy_line
|
|
151
158
|
lines.concat(@warnings) unless @warnings.empty?
|
|
152
159
|
lines.concat(@placeholder_warnings) unless @placeholder_warnings.empty?
|
|
153
160
|
lines << "#{@name} is already installed; `chi bundle upgrade #{@name}` updates it and keeps local edits" if @reinstall
|
|
@@ -157,6 +164,28 @@ module Samagotchi
|
|
|
157
164
|
|
|
158
165
|
private
|
|
159
166
|
|
|
167
|
+
# The steps that replace files: memories, the bundle's own folder, then
|
|
168
|
+
# the files it dropped. The record isn't written yet, so a failed run is
|
|
169
|
+
# retried as the same upgrade.
|
|
170
|
+
# @return [AssetInstaller::Installed]
|
|
171
|
+
# @raise [InstallError] a file system error (a file it can't read, a
|
|
172
|
+
# .trash it can't write)
|
|
173
|
+
def replace_files
|
|
174
|
+
install_memories
|
|
175
|
+
|
|
176
|
+
installed = AssetInstaller.new(
|
|
177
|
+
name: @name, source_dir: @bundle_dir, manifest: @manifest, provenance: @provenance, existing: @existing,
|
|
178
|
+
results: @results, warnings: @warnings, conflicts: @conflicts, trash: @trash,
|
|
179
|
+
force: @force, strict: @strict, upgrade: @upgrade, dry_run: @dry_run, dev: @dev
|
|
180
|
+
).run
|
|
181
|
+
|
|
182
|
+
# Files the previous version shipped and this one doesn't.
|
|
183
|
+
prune_dropped_files(@existing.files, @bundle_md, @target_dir, @target_scope, @provenance) if @upgrade && @existing
|
|
184
|
+
installed
|
|
185
|
+
rescue SystemCallError => e
|
|
186
|
+
raise InstallError, Trash.failure_message(e)
|
|
187
|
+
end
|
|
188
|
+
|
|
160
189
|
# Who may install under this name (a manifest can take any name: a
|
|
161
190
|
# repo calling itself guardrails). Sources compare by SourceIdentity
|
|
162
191
|
# (a shipped dir by name, in any gem or checkout; a git URL without
|
|
@@ -314,8 +343,12 @@ module Samagotchi
|
|
|
314
343
|
@owned << file_key if owned_before.include?(file_key)
|
|
315
344
|
skip_existing(file_key, file_path, target_path, @target_scope, owned: owned_before.include?(file_key))
|
|
316
345
|
else
|
|
346
|
+
# Only --force gets here with a file in place: a copy keeps it.
|
|
347
|
+
@replaced << file_key if File.exist?(target_path)
|
|
348
|
+
@trash.copy(target_path) if File.exist?(target_path) && !FileUtils.identical?(file_path, target_path)
|
|
317
349
|
FileUtils.cp(file_path, target_path)
|
|
318
350
|
@owned << file_key
|
|
351
|
+
@outcomes[file_key] = Merger::INSTALL
|
|
319
352
|
@results[file_key] = { status: "installed" }
|
|
320
353
|
update_target_index(@target_scope, target_path, file_key)
|
|
321
354
|
end
|
|
@@ -338,30 +371,71 @@ module Samagotchi
|
|
|
338
371
|
MemoryFiles.symlinks(@bundle_dir, @manifest).each { |name| @warnings << "#{name} is a symlink (not installed)" }
|
|
339
372
|
end
|
|
340
373
|
|
|
341
|
-
# An upgrade's 3-way merge of one file (Merger
|
|
342
|
-
# base the last install recorded); a
|
|
343
|
-
# and is recorded in @conflicts for the agent step.
|
|
374
|
+
# An upgrade's 3-way merge of one file (Merger::Upgrade against the
|
|
375
|
+
# base the last install recorded); a clean text merge replaces the file;
|
|
376
|
+
# a conflict leaves it and is recorded in @conflicts for the agent step.
|
|
344
377
|
def merge_memory(file_key, file_path, target_path)
|
|
345
|
-
|
|
346
|
-
case
|
|
378
|
+
outcome = @outcomes[file_key] = upgrade_view.outcome(file_key)
|
|
379
|
+
case outcome.status
|
|
347
380
|
when :install
|
|
348
381
|
FileUtils.cp(file_path, target_path)
|
|
349
382
|
@results[file_key] = { status: "installed" }
|
|
350
383
|
when :fast_forward
|
|
384
|
+
save_marked_file(file_key, target_path)
|
|
351
385
|
FileUtils.cp(file_path, target_path)
|
|
352
386
|
@results[file_key] = { status: "updated", reason: "auto-merged (not edited)" }
|
|
353
387
|
when :keep
|
|
354
|
-
@results[file_key] = { status: "kept", reason: "bundle unchanged, local edits preserved" }
|
|
388
|
+
@results[file_key] = { status: "kept", reason: outcome == Merger::KEEP ? "bundle unchanged, local edits preserved" : "local edits already hold the bundle's changes" }
|
|
355
389
|
when :noop
|
|
356
390
|
@results[file_key] = { status: "skipped", reason: "already up to date" }
|
|
391
|
+
when :merged
|
|
392
|
+
if Merger.write_merged(target_path, outcome, @trash)
|
|
393
|
+
@results[file_key] = { status: "merged", reason: "the bundle's changes joined to local edits" }
|
|
394
|
+
else
|
|
395
|
+
note_conflict(file_key, @outcomes[file_key] = Merger::CONFLICT.with(merge: TextMerge.failed(TextMerge::RACED)))
|
|
396
|
+
end
|
|
357
397
|
when :conflict
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
@warnings << "Conflict in #{file_key}: local edits conflict with bundle update (use --force to overwrite or resolve interactively)"
|
|
398
|
+
note_conflict(file_key, outcome)
|
|
399
|
+
else raise ArgumentError, "unknown outcome status #{outcome.status.inspect} for #{file_key}" # the base would move with nothing written
|
|
361
400
|
end
|
|
362
401
|
update_target_index(@target_scope, target_path, file_key)
|
|
363
402
|
end
|
|
364
403
|
|
|
404
|
+
# The file stays as it is, with the old base and the mark, in FileStanding's
|
|
405
|
+
# words.
|
|
406
|
+
def note_conflict(file_key, outcome)
|
|
407
|
+
behind = upgrade_view.behind(file_key, outcome, @manifest&.version)
|
|
408
|
+
@behind << behind
|
|
409
|
+
@results[file_key] = { status: "conflict", reason: behind.state(dry_run: @dry_run) }
|
|
410
|
+
@conflicts[file_key] = upgrade_view.paths(file_key)
|
|
411
|
+
@warnings << "#{behind.line(dry_run: @dry_run)} (#{FileStanding::FORCE_TAKES})"
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# What merged, on the record, a run that failed later too: the file holds the
|
|
415
|
+
# merge whichever way the run ended. Never fails the run.
|
|
416
|
+
def note_merges
|
|
417
|
+
return if merged_files.empty?
|
|
418
|
+
|
|
419
|
+
@provenance.note_merged(merged_files, version: @manifest&.version, trash: trash_dir)
|
|
420
|
+
rescue StandardError => e
|
|
421
|
+
@warnings << "merge not noted on the record (#{e.message})"
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def upgrade_view
|
|
425
|
+
@upgrade_view ||= Merger::Upgrade.new(provenance: @provenance, target_dir: @target_dir, source_dir: @bundle_dir)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# A marked file that equals its base, and the update goes ahead: the base
|
|
429
|
+
# is known to be real (this chi's mark, or ShippedHistory), or no line of the
|
|
430
|
+
# file goes. A copy is kept, and it says so. (Merger::Upgrade#outcome stops
|
|
431
|
+
# an older chi's mark otherwise.)
|
|
432
|
+
def save_marked_file(file_key, target_path)
|
|
433
|
+
return unless @existing.files.dig(file_key, :conflict)
|
|
434
|
+
|
|
435
|
+
@trash.copy(target_path)
|
|
436
|
+
@warnings << "#{file_key} was marked as kept but matched its recorded base, so it took the update; your copy is in the trash"
|
|
437
|
+
end
|
|
438
|
+
|
|
365
439
|
# A dry run's verdict for one file, nothing written: an upgrade
|
|
366
440
|
# classifies as merge_memory would (its status is the classification).
|
|
367
441
|
def preview_memory(file_key, file_path, target_path)
|
|
@@ -371,10 +445,10 @@ module Samagotchi
|
|
|
371
445
|
end
|
|
372
446
|
|
|
373
447
|
if @upgrade && @existing
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
@
|
|
448
|
+
outcome = upgrade_view.outcome(file_key)
|
|
449
|
+
return note_conflict(file_key, outcome) if outcome.conflict?
|
|
450
|
+
|
|
451
|
+
@results[file_key] = { status: outcome.merged? ? "would_merge" : outcome.status.to_s }
|
|
378
452
|
elsif FileUtils.identical?(file_path, target_path)
|
|
379
453
|
@results[file_key] = { status: "skipped", reason: "already up to date" }
|
|
380
454
|
else
|
|
@@ -383,14 +457,15 @@ module Samagotchi
|
|
|
383
457
|
end
|
|
384
458
|
|
|
385
459
|
# Strict mode: a memory written from the bundle whose sha256 differs
|
|
386
|
-
# from its manifest's warns. One that wasn't copied (kept, skipped
|
|
387
|
-
# a local edit isn't a bad download) or
|
|
460
|
+
# from its manifest's warns. One that wasn't copied (kept, skipped, a
|
|
461
|
+
# dry run's keep and would_skip: a local edit isn't a bad download) or
|
|
462
|
+
# was merged (merged, a dry run's would_merge) isn't checked.
|
|
388
463
|
def verify_memory_checksums
|
|
389
464
|
@bundle_md.each do |file_key|
|
|
390
465
|
next if @conflicts.key?(file_key)
|
|
391
466
|
|
|
392
467
|
status = @results[file_key] ? @results[file_key][:status].to_s : nil
|
|
393
|
-
next if %w[kept kept_pruned fast_forward updated skipped].include?(status)
|
|
468
|
+
next if %w[kept keep kept_pruned fast_forward updated skipped would_skip merged would_merge].include?(status)
|
|
394
469
|
|
|
395
470
|
target_path = File.join(@target_dir, file_key)
|
|
396
471
|
next unless File.exist?(target_path)
|
|
@@ -437,9 +512,11 @@ module Samagotchi
|
|
|
437
512
|
plugin_file: installed.plugin_file,
|
|
438
513
|
requires_chi: @manifest.requires_chi,
|
|
439
514
|
needs: @manifest.needs,
|
|
440
|
-
|
|
515
|
+
marks: @behind + held_conflict_marks,
|
|
516
|
+
incoming_dir: @bundle_dir,
|
|
441
517
|
scripts_files: installed.script_files,
|
|
442
518
|
context_providers: @manifest.context_providers,
|
|
519
|
+
block_renderers: @manifest.block_renderers,
|
|
443
520
|
file_descriptions: @manifest.file_descriptions,
|
|
444
521
|
session_settings: @manifest.session_settings,
|
|
445
522
|
session_setting_types: @manifest.session_setting_types,
|
|
@@ -447,34 +524,69 @@ module Samagotchi
|
|
|
447
524
|
setting_types: @manifest.setting_types,
|
|
448
525
|
dev: @dev
|
|
449
526
|
)
|
|
527
|
+
# Replaced by --force: an earlier merge into it no longer says anything true.
|
|
528
|
+
@provenance.clear_merge_notes([], files: @replaced)
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# The files a plain install skipped that are still marked conflict:
|
|
532
|
+
# Provenance#write rebuilds their entries, and a file it left alone has
|
|
533
|
+
# missed the bundle's change as much as before (its version may move on
|
|
534
|
+
# all the same). One with the incoming bytes has nothing left to miss.
|
|
535
|
+
# @return [Array<FileStanding::Behind>]
|
|
536
|
+
def held_conflict_marks
|
|
537
|
+
return [] if @upgrade || @existing.nil?
|
|
538
|
+
|
|
539
|
+
kept = @owned.select { |key| @results.dig(key, :reason) == "already exists" && @existing.files.dig(key, :conflict) }
|
|
540
|
+
kept.map { |key| held_mark(key) }
|
|
450
541
|
end
|
|
451
542
|
|
|
452
|
-
#
|
|
453
|
-
#
|
|
454
|
-
#
|
|
455
|
-
|
|
456
|
-
|
|
543
|
+
# The mark now speaks of this source's text, so what it says comes from that
|
|
544
|
+
# text too: what was missed recomputed, an old reason dropped. No merge ran,
|
|
545
|
+
# so one from an older chi, which can't name sections, keeps its own words.
|
|
546
|
+
def held_mark(key)
|
|
547
|
+
entry = @existing.files.fetch(key)
|
|
548
|
+
return @existing.behind_entry(key, entry).with(version: @manifest.version) if InstalledBundle.legacy_mark?(entry)
|
|
549
|
+
|
|
550
|
+
upgrade_view.behind(key, Merger::CONFLICT, @manifest.version)
|
|
551
|
+
rescue SystemCallError
|
|
552
|
+
FileStanding::Behind.new(file: key, version: @manifest.version)
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# file key => the file whose content becomes its base: the one its
|
|
556
|
+
# Merger::Outcome names (next_base). A file the bundle dropped but the
|
|
557
|
+
# user kept stays recorded too, with the base it has.
|
|
457
558
|
def upgrade_provenance_files
|
|
458
559
|
files = {}
|
|
459
560
|
@owned.each do |file_key|
|
|
460
|
-
|
|
461
|
-
next unless File.exist?(target_path)
|
|
561
|
+
next unless File.exist?(File.join(@target_dir, file_key))
|
|
462
562
|
|
|
463
|
-
|
|
464
|
-
base_path = @provenance.base_path(file_key)
|
|
465
|
-
files[file_key] = %w[kept noop conflict kept_pruned].include?(status) && File.exist?(base_path) ? base_path : target_path
|
|
563
|
+
files[file_key] = next_base(file_key, @outcomes.fetch(file_key))
|
|
466
564
|
end
|
|
467
565
|
@existing.files.each_key do |old_key_str|
|
|
468
566
|
next if @bundle_md.include?(old_key_str)
|
|
469
567
|
next unless @results[old_key_str] && @results[old_key_str][:status].to_s == "kept_pruned"
|
|
470
568
|
|
|
471
|
-
|
|
472
|
-
src = File.exist?(base_path) ? base_path : File.join(@target_dir, old_key_str)
|
|
473
|
-
files[old_key_str] = src if File.exist?(src)
|
|
569
|
+
files[old_key_str] = recorded_base(@provenance.base_path(old_key_str))
|
|
474
570
|
end
|
|
475
571
|
files
|
|
476
572
|
end
|
|
477
573
|
|
|
574
|
+
# The file an Outcome's base names. nil for a kept file with no base:
|
|
575
|
+
# the user's file must not become one, or the next upgrade fast-forwards
|
|
576
|
+
# over it.
|
|
577
|
+
def next_base(file_key, outcome)
|
|
578
|
+
case outcome.base
|
|
579
|
+
when :recorded then recorded_base(@provenance.base_path(file_key))
|
|
580
|
+
when :on_disk then File.join(@target_dir, file_key)
|
|
581
|
+
when :incoming then File.join(@bundle_dir, file_key)
|
|
582
|
+
else raise ArgumentError, "unknown base #{outcome.base.inspect} for #{file_key}" # nil would drop the base silently
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
# +base_path+, nil when there is no base (Provenance#write's "keep the
|
|
587
|
+
# entry, without one").
|
|
588
|
+
def recorded_base(base_path) = File.exist?(base_path) ? base_path : nil
|
|
589
|
+
|
|
478
590
|
# file key => its base's content source for a plain install. Skipped
|
|
479
591
|
# for a local edit: keep the installed base, so the next upgrade still
|
|
480
592
|
# sees the edit (not a fast-forward over it).
|
|
@@ -483,9 +595,8 @@ module Samagotchi
|
|
|
483
595
|
target_path = File.join(@target_dir, file_key)
|
|
484
596
|
next unless File.exist?(target_path)
|
|
485
597
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
map[file_key] = kept ? base_path : target_path
|
|
598
|
+
kept = @existing && @results.dig(file_key, :reason) == "already exists"
|
|
599
|
+
map[file_key] = kept ? recorded_base(@provenance.base_path(file_key)) : target_path
|
|
489
600
|
end
|
|
490
601
|
files.merge(dropped_owned_files)
|
|
491
602
|
end
|
|
@@ -503,8 +614,7 @@ module Samagotchi
|
|
|
503
614
|
next if @bundle_md.include?(file_key)
|
|
504
615
|
next unless File.exist?(File.join(@target_dir, file_key))
|
|
505
616
|
|
|
506
|
-
|
|
507
|
-
files[file_key] = base_path if File.exist?(base_path)
|
|
617
|
+
files[file_key] = recorded_base(@provenance.base_path(file_key))
|
|
508
618
|
end
|
|
509
619
|
end
|
|
510
620
|
|
|
@@ -520,7 +630,7 @@ module Samagotchi
|
|
|
520
630
|
return
|
|
521
631
|
else
|
|
522
632
|
@results[file_key] = { status: "skipped", reason: "already exists" }
|
|
523
|
-
@warnings << "Skipped #{file_key} (already exists;
|
|
633
|
+
@warnings << "Skipped #{file_key} (already exists; #{FileStanding::FORCE_TAKES})"
|
|
524
634
|
end
|
|
525
635
|
update_target_index(scope, target_path, file_key, owned: owned) unless @dry_run
|
|
526
636
|
end
|
|
@@ -548,7 +658,7 @@ module Samagotchi
|
|
|
548
658
|
remove_target_index(scope, key)
|
|
549
659
|
else
|
|
550
660
|
@results[key] = { status: "kept_pruned", reason: "local edits preserved" }
|
|
551
|
-
@warnings << "Kept #{key}: no longer in the bundle but edited locally (
|
|
661
|
+
@warnings << "Kept #{key}: no longer in the bundle but edited locally (#{FileStanding::FORCE_MOVES})"
|
|
552
662
|
end
|
|
553
663
|
end
|
|
554
664
|
end
|
|
@@ -15,8 +15,9 @@ module Samagotchi
|
|
|
15
15
|
# its own line, and they aren't listed again.
|
|
16
16
|
module Listing
|
|
17
17
|
# error is set (and the rest nil) when the provenance manifest.json
|
|
18
|
-
# can't be read; includes is a profile's recorded members, else nil
|
|
19
|
-
|
|
18
|
+
# can't be read; includes is a profile's recorded members, else nil;
|
|
19
|
+
# behind is its files that stay behind (InstalledBundle#behind_files).
|
|
20
|
+
Installed = Struct.new(:name, :version, :scope, :files, :installed_at, :upgrade, :error, :includes, :behind, keyword_init: true)
|
|
20
21
|
# source is the name `chi bundle install` takes (the shipped dir's
|
|
21
22
|
# name); includes is a profile's members ([] for a plain bundle).
|
|
22
23
|
Shipped = Struct.new(:name, :source, :version, :description, :includes, keyword_init: true)
|
|
@@ -50,7 +51,7 @@ module Samagotchi
|
|
|
50
51
|
Installed.new(name: name, version: bundle.version, scope: bundle.scope,
|
|
51
52
|
files: bundle.files.size, installed_at: bundle.installed_at,
|
|
52
53
|
upgrade: ship && newer?(ship.version, bundle.version) ? ship : nil,
|
|
53
|
-
includes: bundle.includes)
|
|
54
|
+
includes: bundle.includes, behind: bundle.behind_files)
|
|
54
55
|
end
|
|
55
56
|
end
|
|
56
57
|
|
|
@@ -7,6 +7,8 @@ require_relative "../context_providers"
|
|
|
7
7
|
require_relative "bundle_hook"
|
|
8
8
|
|
|
9
9
|
module Samagotchi
|
|
10
|
+
autoload :BlockRenderers, File.expand_path("../block_renderers", __dir__)
|
|
11
|
+
|
|
10
12
|
module MemoryBundle
|
|
11
13
|
# Parses and validates a bundle manifest.yml.
|
|
12
14
|
#
|
|
@@ -41,6 +43,9 @@ module Samagotchi
|
|
|
41
43
|
# cmd: '{ruby} {bundle_dir}/scripts/pr_context.rb {url}'
|
|
42
44
|
# why: GitHub PR # optional
|
|
43
45
|
# every_seconds: 300 # optional
|
|
46
|
+
# block_renderers: # optional — a fence's language → the scripts/
|
|
47
|
+
# - lang: mermaid # files chi web's page loads to draw it
|
|
48
|
+
# load: [lib.js, draw.js] # (BlockRenderers); each one a scripts: key
|
|
44
49
|
# session_settings: [cap] # optional — the settings a session may set
|
|
45
50
|
# # (chi --set cost-cap.cap=3), read by the
|
|
46
51
|
# # plugin from ctx.settings at call time;
|
|
@@ -71,7 +76,7 @@ module Samagotchi
|
|
|
71
76
|
# the description a mapping gave, for the files that have one; hooks:
|
|
72
77
|
# name → BundleHook.
|
|
73
78
|
attr_reader :name, :version, :scope, :description, :files, :file_descriptions, :hooks, :trust_level, :plugin, :requires_chi, :needs, :includes,
|
|
74
|
-
:scripts, :context_providers, :session_settings, :session_setting_types, :settings, :setting_types
|
|
79
|
+
:scripts, :context_providers, :block_renderers, :session_settings, :session_setting_types, :settings, :setting_types
|
|
75
80
|
|
|
76
81
|
def initialize(path:)
|
|
77
82
|
@path = path
|
|
@@ -94,6 +99,7 @@ module Samagotchi
|
|
|
94
99
|
@needs = self.class.parse_needs(raw["needs"])
|
|
95
100
|
@scripts = parse_scripts(raw["scripts"])
|
|
96
101
|
@context_providers = parse_context_providers(raw["context_providers"])
|
|
102
|
+
@block_renderers = parse_block_renderers(raw["block_renderers"])
|
|
97
103
|
@includes = parse_includes(raw["includes"])
|
|
98
104
|
@session_settings = self.class.parse_session_settings(raw["session_settings"])
|
|
99
105
|
@session_setting_types = self.class.parse_session_setting_types(raw["session_settings"])
|
|
@@ -163,7 +169,7 @@ module Samagotchi
|
|
|
163
169
|
# Writes a fresh manifest.yml from computed checksums at `dir`.
|
|
164
170
|
# +file_descriptions+ (name → text) writes those files as mappings.
|
|
165
171
|
def self.write(dir:, name:, version:, files:, scope: nil, description: "", hooks: nil, trust_level: nil,
|
|
166
|
-
plugin: nil, requires_chi: nil, needs: nil, scripts: {}, context_providers: [], file_descriptions: {},
|
|
172
|
+
plugin: nil, requires_chi: nil, needs: nil, scripts: {}, context_providers: [], block_renderers: [], file_descriptions: {},
|
|
167
173
|
session_settings: [], session_setting_types: {}, settings: nil, setting_types: {})
|
|
168
174
|
FileUtils.mkdir_p(dir)
|
|
169
175
|
manifest = {
|
|
@@ -187,6 +193,7 @@ module Samagotchi
|
|
|
187
193
|
manifest["needs"] = needs.map { |n| n.transform_keys(&:to_s).compact } unless needs.empty?
|
|
188
194
|
manifest["scripts"] = scripts.transform_keys(&:to_s) unless scripts.empty?
|
|
189
195
|
manifest["context_providers"] = context_providers.map(&:to_h) unless context_providers.empty?
|
|
196
|
+
manifest["block_renderers"] = block_renderers.map(&:to_h) unless block_renderers.empty?
|
|
190
197
|
manifest["session_settings"] = session_settings_entry(session_settings, session_setting_types) unless Array(session_settings).empty?
|
|
191
198
|
manifest["settings"] = session_settings_entry(settings, setting_types) unless settings.nil?
|
|
192
199
|
File.write(File.join(dir, "manifest.yml"), YAML.dump(manifest))
|
|
@@ -362,6 +369,20 @@ module Samagotchi
|
|
|
362
369
|
raise ValidationError, e.message
|
|
363
370
|
end
|
|
364
371
|
|
|
372
|
+
# @return [Array<BlockRenderers::Renderer>] each load is a scripts: key
|
|
373
|
+
def parse_block_renderers(raw)
|
|
374
|
+
return [] if raw.nil?
|
|
375
|
+
|
|
376
|
+
renderers = BlockRenderers.parse_list(raw)
|
|
377
|
+
renderers.each do |renderer|
|
|
378
|
+
missing = renderer.load - @scripts.keys
|
|
379
|
+
raise ValidationError, "block_renderers: #{renderer.lang} loads #{missing.join(", ")}, not under scripts:" unless missing.empty?
|
|
380
|
+
end
|
|
381
|
+
renderers
|
|
382
|
+
rescue BlockRenderers::Invalid => e
|
|
383
|
+
raise ValidationError, e.message
|
|
384
|
+
end
|
|
385
|
+
|
|
365
386
|
# includes: → the names in order, duplicates dropped; [] when absent.
|
|
366
387
|
# A meta ships nothing of its own, so files, hooks or a plugin next
|
|
367
388
|
# to it is a ValidationError.
|