1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
//! §6/§13 `prime --install CENTER` config adoption — copy a center's committed
//! `config/` into this landing, with the remote fetch done by the TRACKER, never
//! core.
//!
//! The invariant (§0): core talks to no remote — the tracker plugin is the only
//! remote-talker. So adoption splits cleanly in two:
//!
//! 1. [`fetch_config`] runs the `install.pre` hook chain. The tracker's
//! `install/pre` handler fetches the center's `balls/config` branch into the
//! LANDING repo, leaving it at `FETCH_HEAD` (a git-standard ref — no invented
//! core↔plugin convention). This is the ONE remote read; it rides a plugin.
//! 2. [`install_local`] is pure LOCAL git: SEAL the `config/` path-copy from
//! `FETCH_HEAD` onto the landing through the shared §8 engine spine
//! ([`crate::install::seal_copy`] — folder = mirror; config is single-owner,
//! install-replaced, so adoption is destructive, §2/§6), then validate-and-bind
//! the now-referenced plugins to this box's binaries. The engine's chains run
//! EMPTY here: the `install.pre` chain already ran in step 1 (the staging
//! reads the `FETCH_HEAD` it leaves, so the fetch cannot ride the engine's own
//! `pre`) — that fetch leg is the one piece still outside the §14 trace.
//! Nothing fetched ⇒ nothing to adopt (bl-3b82): a stock hub carries no
//! `balls/config` (bl never publishes the landing, §4 single-owner), so the
//! tracker's §13 "upstream, if any" fetch leaves no `FETCH_HEAD` and the
//! adopt is a no-op — `--center` still binds and primes, never a git fatal.
//!
//! Config "crosses into a landing only by the explicit copy `install` performs"
//! (§0); here that copy is local and the read that feeds it is the tracker's. The
//! ADOPT direction always seals to the local landing — install is purely local in
//! core, every seal target is (transport is the tracker's; §6, bl-b8d6). The
//! surrounding [`crate::checkout::prime`] then drives prime+sync against the
//! just-adopted `tasks_branch` — a SINGLE hop, no recursion (a center's config
//! names its own store, never another config, §4). Idempotent so a failed adopt
//! RESUMES: the worktrees are force-recreated, the mirror re-copies identical
//! bytes, and an unchanged tree seals to the existing tip (the no-op seal).
use crate::checkout;
use crate::config::EffectiveConfig;
use crate::edge::Edge;
use crate::hooks::Hooks;
use crate::install;
use crate::lifecycle::Plugins;
use crate::log::{self, Level, Log};
use crate::op::Phase;
use crate::plugin::Subprocess;
use crate::registry::Registry;
use crate::verb::Verb;
use crate::wire::OpContext;
use crate::LANDING_BRANCH;
use std::io;
use std::path::Path;
/// Adopt `center`'s committed `config/` into `landing` (§6/§13): the tracker
/// fetches (remote), then core copies (local) — or, when the fetch left nothing
/// (a hub with no `balls/config`), notes that and adopts nothing. `store` is the
/// sibling store checkout the §7 binding names; `actor` rides the wire.
pub fn adopt(edge: &Edge, landing: &Path, store: &Path, actor: &str, center: &str) -> io::Result<()> {
fetch_config(edge, landing, store, actor, center)?;
if !install::fetched(landing) {
eprintln!("install: {center} carries no {LANDING_BRANCH} — nothing to adopt");
return Ok(());
}
install_local(edge, landing)
}
/// Run the `install.pre` chain so the tracker fetches `center`'s `balls/config`
/// into the landing repo (§13). Builds the §7 binding with `center` as the remote
/// and the config branch as the target, then invokes each `install.pre` plugin
/// with cwd = the landing (where the fetch — and core's later materialize — both
/// read `FETCH_HEAD`). An EMPTY chain is a hard error: `prime --install` cannot
/// reach a remote without a fetch plugin (the tracker), so adopting is impossible
/// rather than silently producing nothing — the auto-safe degrade is *plain*
/// prime, not `--install`.
fn fetch_config(edge: &Edge, landing: &Path, store: &Path, actor: &str, center: &str) -> io::Result<()> {
let cfg = EffectiveConfig::resolve(landing, &edge.xdg.user_config())?;
let level = Level::parse(edge.log_level.as_deref().unwrap_or(&cfg.log_level))?;
let binding = checkout::binding(landing, store, &edge.invocation_path, Some(center.to_string()), false, LANDING_BRANCH.to_string());
let pre = Hooks::effective(landing, &edge.xdg.user_config(), &edge.machine_dirs())?
.resolve(&Registry::at(landing), Verb::Install.token(), "pre");
if pre.is_empty() {
return Err(io::Error::other(
"prime --install: no install.pre plugin (e.g. the tracker) is installed to fetch the center's config",
));
}
let log = Log::new(edge.xdg.clone_dir(&edge.invocation_path).op_log(), level, Verb::Install, log::wall);
let plugins = Subprocess::new(OpContext::diffless(actor.to_string(), binding), &log, edge.depth, edge.held.clone());
for plugin in &pre {
plugins.run(plugin, Verb::Install, Phase::Pre, landing, None)?;
}
Ok(())
}
/// SEAL the fetched config (`FETCH_HEAD`, left by [`fetch_config`]) onto the
/// landing through the shared §8 spine ([`install::seal_copy`] — the same
/// materialize/copy/seal `bl install` runs, §14 rollback included), then bind
/// the referenced plugins and print the §6 change summary. The chains run
/// empty — the `install.pre` fetch already ran (see the module doc) — so the
/// subprocess seam is constructed but never invoked.
pub(crate) fn install_local(edge: &Edge, landing: &Path) -> io::Result<()> {
let clone = edge.xdg.clone_dir(&edge.invocation_path);
let (binding, level) = checkout::bind(edge, landing, &clone.store(), None, None)?;
let log = Log::new(clone.op_log(), level, Verb::Install, log::wall);
let plugins = Subprocess::new(OpContext::diffless(edge.default_actor.clone(), binding), &log, edge.depth, edge.held.clone());
let chain = install::Chain { plugins: &plugins, log: &log, pre: Vec::new(), post: Vec::new() };
// Canonicalize a stale center's retired plugin names as they land (§12.1): the
// copy-in rewrite makes `prime --install`/`--center` converge to the no-op
// seal instead of re-injecting the old name — and re-committing prime's
// rewrite — every adopt cycle. The guard reads THIS landing's registry.
let reg = Registry::at(landing);
let converge = |dir: &Path| crate::converge::rewrite_config(dir, ®);
let summary = install::seal_copy(&clone, install::DEFAULT_PATH, "FETCH_HEAD", landing, &chain, &edge.default_actor, Some(&converge))?;
install::bind_referenced(landing, edge, &std::collections::BTreeMap::new(), &log)?;
println!("install: {summary}");
Ok(())
}
#[cfg(test)]
#[path = "adopt_tests.rs"]
mod tests;