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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//! §4 config resolution with provenance — resolve one [`Key`] across the §4
//! layers (innermost wins) and NAME the layer that answered. Lifted from
//! [`super`] so the `conf` file stays the key namespace + the read/dump dispatch;
//! this is the "which layer / what file" engine the dump and single-key read read.
use std::io;
use std::path::Path;
use crate::config;
use crate::edge::Edge;
use crate::hooks::Hooks;
use crate::layout::CloneDir;
use super::Key;
/// One resolved value plus the §4 layer that answered — what the dump and the
/// single-key read print.
pub(super) struct Resolved {
pub(super) value: String,
pub(super) layer: String,
}
/// Resolve one key across its §4 layers, innermost wins, naming the layer.
pub(super) fn resolve(edge: &Edge, clone: &CloneDir, key: &Key) -> io::Result<Resolved> {
let landing = clone.landing();
match key {
Key::TaskRemote => task_remote(edge, &landing, &clone.binding(), &clone.store()),
Key::TaskBranch => scalar(edge, &landing, "tasks_branch", crate::DEFAULT_TASKS_BRANCH, None),
Key::LogLevel => scalar(edge, &landing, "log_level", "info", edge.log_level.as_deref()),
Key::ClockProvider => Ok(clock_provider(edge, &clone.binding())),
Key::Hook(k) => {
let hooks = Hooks::effective(&landing, &edge.xdg.user_config(), &edge.machine_dirs())?;
let names = hooks
.entries()
.find(|(key, _)| *key == k)
.map(|(_, names)| names.join(", "))
.filter(|joined| !joined.is_empty());
Ok(Resolved {
value: names.unwrap_or_else(|| "(none)".into()),
layer: hook_layer(edge, clone, k)?,
})
}
}
}
/// The store remote per the §12 ladder's DURABLE tiers (`conf` takes no
/// `--remote`), through the SAME [`config::remote_ladder`] the ops bind with:
/// a NESTED invocation — one whose store an ENCLOSING `bl` holds open, read
/// from the held chain (`$BALLS_HELD_STORES`, bl-1266/bl-aac7) — reads `(none)`
/// from `nested` and PREEMPTS every durable tier, else the landing `task_remote` policy (declared stealth reads
/// `(none)` from `landing` — bl-9df0), else this clone's `binding` remote, else
/// the legacy global XDG remote, else the project repo's `origin` (a local `git
/// remote get-url` — naming, not contacting, §12), else `(none)` from `stealth`
/// — circumstantial, nothing set. A non-stealth URL is labelled by its tier:
/// `binding` (per-clone, this checkout's own) vs `xdg (global)` (per-machine,
/// shared by every repo — the bl-d081 disambiguation, so a global value is told
/// apart from a per-clone one).
///
/// **FOUR no-remote readouts, and the fourth is not a kind of stealth.**
/// Declared (bl-d234's `landing`), unset-with-origin, unset-without
/// (`stealth`) all say the same thing — *nothing will be published*. `nested`
/// says the OPPOSITE of nothing: the push is OWED, and the outermost `bl` in
/// this invocation tree pays it. It preempts the URL tiers deliberately, because
/// the question the line answers is *"will this op publish?"* and a perfectly
/// configured remote is the most misleading possible answer when the enclosing
/// op holds the anvil open. The predicate is STORE-SCOPED (bl-aac7): a nested
/// `bl -C` on a different store is not nested for this purpose and publishes.
/// Reading `nested` at top level means a `BALLS_HELD_STORES` naming this store
/// leaked into the shell (§6) — nothing is lost, the store publishes on the
/// next clean op.
fn task_remote(edge: &Edge, landing: &Path, binding: &Path, store: &Path) -> io::Result<Resolved> {
if edge.held.iter().any(|h| h == store) {
return Ok(Resolved { value: "(none)".into(), layer: "nested".into() });
}
let (remote, declared) = config::remote_ladder(None, landing, binding, &edge.xdg.user_config())?;
if declared {
return Ok(Resolved { value: "(none)".into(), layer: "landing".into() });
}
if let Some(url) = remote {
let layer = if config::binding_remote(binding).is_some() { "binding" } else { "xdg (global)" };
return Ok(Resolved { value: url, layer: layer.into() });
}
Ok(match origin(&edge.invocation_path) {
Some(url) => Resolved { value: url, layer: "origin".into() },
None => Resolved { value: "(none)".into(), layer: "stealth".into() },
})
}
/// The §8 op-clock provider per its LOCAL-TRUST tiers (bl-cfe3) — this clone's
/// `binding` `clock_provider` key, else the legacy per-machine XDG config, else
/// `(none)` (⇒ the system clock). A directly-set value (an absolute path or a
/// PATH-resolved name), NOT a landing field: it never travels on `install`, so
/// `conf` reads it from the same non-traveling homes as the store `remote` and
/// labels which tier answered — `binding` (per-clone) vs `xdg (global)`.
fn clock_provider(edge: &Edge, binding: &Path) -> Resolved {
let uc = edge.xdg.user_config();
if let Some(v) = config::string_key(binding, "clock_provider") {
return Resolved { value: v, layer: "binding".into() };
}
if let Some(v) = config::string_key(&uc, "clock_provider") {
return Resolved { value: v, layer: "xdg (global)".into() };
}
Resolved { value: "(none)".into(), layer: "default".into() }
}
/// `git remote get-url origin` on the PROJECT repo (the invocation path, §12) —
/// a local config read; absent origin or a non-repo path ⇒ `None`.
fn origin(project: &Path) -> Option<String> {
let url = crate::git::run(project, &["remote", "get-url", "origin"], None).ok()?;
Some(url.trim().to_string())
}
/// Resolve a scalar field across the §4 stack — `cli` (a flag the edge already
/// stripped) > `xdg` > `landing` > `default` — reading each layer table
/// directly so the ANSWERING layer is named, not just the folded value.
fn scalar(edge: &Edge, landing: &Path, field: &str, default: &str, cli: Option<&str>) -> io::Result<Resolved> {
if let Some(v) = cli {
return Ok(Resolved { value: v.to_string(), layer: "cli".into() });
}
let layers = [
(config::read_layer(&edge.xdg.user_config())?, "xdg"),
(config::read_layer(&landing.join("config").join("balls.toml"))?, "landing"),
];
for (table, layer) in layers {
if let Some(v) = table.as_ref().and_then(|t| t.get(field)).and_then(|v| v.as_str()) {
return Ok(Resolved { value: v.to_string(), layer: layer.into() });
}
}
Ok(Resolved { value: default.to_string(), layer: "default".into() })
}
/// Which layer(s) mention a `[hooks]` key — the landing `plugins.toml`, the XDG
/// overlay, or both (the §4 list compose can draw on both at once; a key in
/// neither file resolved through a directive-less default and reads `default`).
pub(super) fn hook_layer(edge: &Edge, clone: &CloneDir, key: &str) -> io::Result<String> {
let landing = hooks_mentions(&clone.landing().join("config").join("plugins.toml"), key)?;
let xdg = hooks_mentions(&edge.xdg.user_config().with_file_name("plugins.toml"), key)?;
Ok(match (landing, xdg) {
(true, true) => "landing+xdg".into(),
(true, false) => "landing".into(),
(false, true) => "xdg".into(),
(false, false) => "default".into(),
})
}
/// Does this `plugins.toml` layer's `[hooks]` table mention `key` — bare or via
/// any §4 compose directive? An absent file mentions nothing.
fn hooks_mentions(path: &Path, key: &str) -> io::Result<bool> {
let Some(root) = config::read_layer(path)? else {
return Ok(false);
};
let Some(toml::Value::Table(hooks)) = root.get("hooks") else {
return Ok(false);
};
Ok(hooks.contains_key(key)
|| ["_prepend", "_append", "_ban"].iter().any(|s| hooks.contains_key(&format!("{key}{s}"))))
}