src/ws/relock.bend
ezx@1.10.0.0 · 428 lines · 0 definitions
1# ws/relock: `ez add` and `ez remove` in a workspace (EZ-WS-10). Outside a
2# workspace each runs its command's plan joined to a plain lock over the
3# ledger it writes (add/relock.bend, remove/relock.bend, EZ-DOC-6). In a
4# workspace the lock that ledger belongs to is the root's, so the command's
5# own plan is joined instead to the plan a plain `ez lock` at the root makes
6# (`U.plan`, ws/up.bend) over the workspace the command leaves: the root's
7# ledger and every member's as git lists them, with the one the command
8# writes in place of its own, joined (ws/joint.bend), the `.bend` files git
9# lists under the root, and the lock's answers. The command runs where it
10# is, so every path that lock writes is taken from the root's directory
11# (`at.effect`), and no lock is written in a member.
12#
13# A ledger the add or remove writes at the root is written through the
14# ledger model, which holds no `[workspace]` table, so it is written back with
15# the table it had (`U.root.text`), as an upgrade writes the root's.
16import Base
17import ezx@1.10.0.0/src/lock/plan.bend as LP
18import ezx@1.10.0.0/src/lock/world.bend as LW
19import ezx@1.10.0.0/src/lock/lock.bend as L
20import ezx@1.10.0.0/src/ledger/manifest.bend as M
21import ezx@1.10.0.0/src/ledger/render.bend as Rend
22import ezx@1.10.0.0/src/pkg/path.bend as P
23import ezx@1.10.0.0/src/relock/relock.bend as R
24import ezx@1.10.0.0/src/add/world.bend as A
25import ezx@1.10.0.0/src/add/plan.bend as AP
26import ezx@1.10.0.0/src/add/relock.bend as AR
27import ezx@1.10.0.0/src/remove/plan.bend as RP
28import ezx@1.10.0.0/src/remove/relock.bend as RR
29import ezx@1.10.0.0/src/ws/workspace.bend as W
30import ezx@1.10.0.0/src/ws/joint.bend as J
31import ezx@1.10.0.0/src/ws/up.bend as U
32
33# what a command run in a workspace reads of its root: the root's directory,
34# the path from it to where the command runs, the root's ledger, every other
35# ez.toml git lists under it, the `.bend` files git lists under it, each path
36# from the root, and the root's lock
37type Rooted is Data:
38 Rooted{
39 at: String,
40 rel: List<&2, String>,
41 text: String,
42 found: List<&2, J.Found>,
43 listing: LW.Listing,
44 lock: String
45 }
46
47# where a command runs: in a project of its own, or in a workspace
48type Ws is Data:
49 Own{}
50 Inside{root: Rooted}
51
52# the root's directory
53def rooted.at(root: Rooted) -> String:
54 Rooted{at, _rel, _text, _found, _listing, _lock} = root
55 at
56
57# ---------------------------------------------------------------------------
58# the root's lock, run from where the command runs
59
60# a place a tree is laid in, taken from the root: the root's committed
61# `.ez/lib` for a committed tree, BEND_LIB as it is, and a directory under the
62# root from the root
63def at.place(+at: String, place: LP.Place) -> LP.Place:
64 match place:
65 case LP.Committed{}:
66 LP.Under{at}
67 case LP.Cache{}:
68 LP.Cache{}
69 case LP.Under{dir}:
70 LP.Under{P.join(at, dir)}
71
72# an effect of the root's lock as it is run from where the command runs:
73# every path it writes taken from the root. Removing a committed tree is
74# laying it with no file, which removes what was there and writes nothing.
75def at.effect(+at: String, effect: LP.Effect) -> LP.Effect:
76 match effect:
77 case LP.Write{path, text}:
78 LP.Write{P.join(at, path), text}
79 case LP.Lay{place, hash, files}:
80 LP.Lay{at.place(at, place), hash, files}
81 case LP.Drop{hash}:
82 LP.Lay{LP.Under{at}, hash, []}
83 case LP.Say{text}:
84 LP.Say{text}
85 case LP.Name{nv, hash}:
86 LP.Name{nv, hash}
87
88# every effect of the root's lock, so
89def at.effects(+at: String, es: List<&2, LP.Effect>) -> List<&2, LP.Effect>:
90 match es:
91 case []:
92 []
93 case hd <> tl:
94 at.effect(at, hd) <> at.effects(at, tl)
95
96# the root's lock plan, so
97def at.plan(+at: String, pl: LP.Plan) -> LP.Plan:
98 LP.Plan{es, outcome} = pl
99 LP.Plan{at.effects(at, es), outcome}
100
101# ---------------------------------------------------------------------------
102# the root's ledger, keeping its `[workspace]` table
103
104# a ledger the command writes at the root, once it is read: as the root's
105# with the table it had, or as it is when it does not read
106def keep.parsed(rd: M.Read, +spec: W.Spec, root: W.Root, +text: String) -> String:
107 match rd:
108 case M.Good{mm}:
109 U.root.text(mm, spec, root)
110 case M.Bad{_why}:
111 text
112
113# once it is known whether the root's ledger, `old`, had a `[workspace]`
114# table
115def keep.read(rd: W.Read, +old: String, +text: String) -> String:
116 match rd:
117 case W.Root{+spec}:
118 keep.parsed(M.parse(text), spec, W.root.of(old), text)
119 case W.Lone{}:
120 text
121 case W.Bad{_why}:
122 text
123
124# the text a command writes as the root's ledger, which held `old`
125def keep(+old: String, +text: String) -> String:
126 keep.read(W.spec.of(old), old, text)
127
128# a file the command writes, once it is known whether it is the ledger
129def keep.write(hit: Bool, +old: String, +path: String, +text: String) -> LP.Effect:
130 match hit:
131 case True{}:
132 LP.Write{path, keep(old, text)}
133 case False{}:
134 LP.Write{path, text}
135
136# an effect of the command at the root, its ledger keeping its table
137def keep.effect(+old: String, effect: LP.Effect) -> LP.Effect:
138 match effect:
139 case LP.Write{+path, text}:
140 keep.write(String.eq(path, R.toml()), old, path, text)
141 case LP.Lay{place, hash, files}:
142 LP.Lay{place, hash, files}
143 case LP.Drop{hash}:
144 LP.Drop{hash}
145 case LP.Say{text}:
146 LP.Say{text}
147 case LP.Name{nv, hash}:
148 LP.Name{nv, hash}
149
150# every effect of the command at the root, so
151def keep.effects(+old: String, es: List<&2, LP.Effect>) -> List<&2, LP.Effect>:
152 match es:
153 case []:
154 []
155 case hd <> tl:
156 keep.effect(old, hd) <> keep.effects(old, tl)
157
158# the command's own plan, run at the path `rel` from the root, whose ledger
159# is `old`: at the root, with the root's ledger keeping its table, and in a
160# member as it is
161def edit.at(rel: List<&2, String>, +old: String, pl: LP.Plan) -> LP.Plan:
162 match rel:
163 case []:
164 LP.Plan{es, outcome} = pl
165 LP.Plan{keep.effects(old, es), outcome}
166 case _hd <> _tl:
167 pl
168
169# the command's own plan, in the workspace
170def edit(root: Rooted, pl: LP.Plan) -> LP.Plan:
171 Rooted{_at, rel, text, _found, _listing, _lock} = root
172 edit.at(rel, text, pl)
173
174# ---------------------------------------------------------------------------
175# the ledgers the command leaves
176
177# the root's ledger and every other ez.toml git lists under the root
178type Ledgers is Data:
179 Ledgers{text: String, found: List<&2, J.Found>}
180
181# a listed ez.toml, once it is known whether it is the one the command writes
182def put.pick(hit: Bool, +path: String, was: String, +text: String, rest: List<&2, J.Found>) -> List<&2, J.Found>:
183 match hit:
184 case True{}:
185 J.Found{path, text} <> rest
186 case False{}:
187 J.Found{path, was} <> rest
188
189# every listed ez.toml, the one at `path` holding `text`
190def put.found(fs: List<&2, J.Found>, +path: String, +text: String) -> List<&2, J.Found>:
191 match fs:
192 case []:
193 []
194 case J.Found{+fp, ft} <> tl:
195 rest = put.found(tl, path, text)
196 put.pick(String.eq(fp, path), fp, ft, text, rest)
197
198# the ledgers once the command at `rel` writes `new`: the root's, at the
199# root, or the member's ez.toml under it
200def ledgers.at(rel: List<&2, String>, +new: String, +text: String, +fs: List<&2, J.Found>) -> Ledgers:
201 match rel:
202 case []:
203 Ledgers{new, fs}
204 case hd <> tl:
205 Ledgers{text, put.found(fs, P.join(W.segs.text(hd <> tl), R.toml()), new)}
206
207# the ledger's text the command leaves, "" when it leaves none, which only a
208# command that refuses does
209def new.of(led: Maybe<&2, String>) -> String:
210 match led:
211 case None{}:
212 ""
213 case Some{text}:
214 text
215
216# the ledgers the command leaves, when its own plan, as the workspace runs it,
217# is `pl` and its ledger held `was` before
218def ledgers(root: Rooted, pl: LP.Plan, was: Maybe<&2, String>) -> Ledgers:
219 Rooted{_at, rel, +text, fs, _listing, _lock} = root
220 ledgers.at(rel, new.of(R.ledger(pl, was)), text, fs)
221
222# ---------------------------------------------------------------------------
223# the root's lock
224
225# the lock at the root, as a plain `ez lock` there plans it: what its upgrade
226# half needs and the World it reads, or why the ledgers cannot be joined
227type Lock is Data:
228 Lock{back: U.Back, world: LW.World}
229 Stop{why: String}
230
231# the World a plain `ez lock` reads at the root
232def world(+at: String, led: Maybe<&2, String>, listing: LW.Listing, rs: List<&2, LW.Reply>) -> LW.World:
233 LW.World{LW.Args{False{}, "", at}, led, listing, rs, []}
234
235# the lock, once the ledgers were joined: as `ez lock` at the root reads it
236# (ws/run.bend's `joint.at`), with the members' answers first
237def lock.joint(
238 jj: J.Joint,
239 +at: String,
240 +text: String,
241 +spec: W.Spec,
242 +fs: List<&2, J.Found>,
243 +listing: LW.Listing,
244 +lock: String,
245 rs: List<&2, LW.Reply>
246) -> Lock:
247 match jj:
248 case J.Lone{}:
249 Lock{U.Alone{}, world(at, Some{text}, listing, rs)}
250 case J.Clash{why}:
251 Stop{why}
252 case J.Joint{+mm}:
253 Lock{U.Back{text, spec, fs, lock},
254 world(at, Some{Rend.show(mm)}, listing,
255 List.append(&2, LW.Reply, J.seeds(J.Joint{mm}, J.files(listing)), rs))}
256
257# the lock, once the root's ledger is read
258def lock.read(
259 rd: W.Read,
260 +at: String,
261 +text: String,
262 +fs: List<&2, J.Found>,
263 +listing: LW.Listing,
264 +lock: String,
265 rs: List<&2, LW.Reply>
266) -> Lock:
267 match rd:
268 case W.Lone{}:
269 Lock{U.Alone{}, world(at, Some{text}, listing, rs)}
270 case W.Bad{why}:
271 Stop{why}
272 case W.Root{+spec}:
273 lock.joint(J.joint(text, fs, J.files(listing)), at, text, spec, fs, listing, lock, rs)
274
275# the lock at the root over the ledgers the command leaves
276def lock.of(lg: Ledgers, root: Rooted, rs: List<&2, LW.Reply>) -> Lock:
277 Ledgers{+text, +fs} = lg
278 Rooted{+at, _rel, _text, _found, +listing, +lock} = root
279 lock.read(W.spec.of(text), at, text, fs, listing, lock, rs)
280
281# a refusal, with no effect
282def stop(+why: String) -> LP.Plan:
283 LP.Plan{[], LP.Refused{why}}
284
285# the lock's plan
286def lock.plan(lk: Lock) -> LP.Plan:
287 match lk:
288 case Lock{back, +world}:
289 U.plan(back, world)
290 case Stop{why}:
291 stop(why)
292
293# the lock's step
294def lock.step(lk: Lock) -> LP.Step:
295 match lk:
296 case Lock{+back, +world}:
297 U.step(back, world)
298 case Stop{why}:
299 LP.Run{stop(why)}
300
301# the packages the lock records, none when it cannot be made
302def lock.packs(lk: Lock) -> List<&2, L.Pack>:
303 match lk:
304 case Lock{_back, +world}:
305 LP.packs(world)
306 case Stop{_why}:
307 []
308
309# ---------------------------------------------------------------------------
310# `ez add`
311
312# the add's own plan, as the workspace runs it
313def add.edit(+root: Rooted, +aw: A.World) -> LP.Plan:
314 edit(root, AP.command(aw))
315
316# the root's lock over what the add leaves, with the answers the add has for
317# it first
318def add.lock(+root: Rooted, world: AR.World) -> Lock:
319 AR.World{+aw, _listing, locks} = world
320 lock.of(ledgers(root, add.edit(root, aw), A.ledger(aw)), root,
321 List.append(&2, LW.Reply, AR.seeds(aw), locks))
322
323# the note (EZ-DOC-7), over the root's lock
324def add.notes(+lk: Lock, +aw: A.World) -> List<&2, LP.Effect>:
325 AR.notes.of(AR.held(lock.packs(lk), AR.added(aw)), AR.added(aw))
326
327# `ez add` in a workspace: the add, then the root's lock over what it leaves,
328# run from where the add runs
329def add.plan(+root: Rooted, world: AR.World) -> LP.Plan:
330 AR.World{+aw, +listing, +locks} = world
331 +lk = add.lock(root, AR.World{aw, listing, locks})
332 R.join(add.edit(root, aw), at.plan(rooted.at(root), lock.plan(lk)), add.notes(lk, aw), AR.tail(aw))
333
334# the lock's step, once the add is planned and succeeds
335def add.next.lock(st: LP.Step, +root: Rooted, +world: AR.World) -> AR.Next:
336 match st:
337 case LP.Asking{as, ups, ns}:
338 AR.AskLock{R.asks(LP.Asking{as, ups, ns})}
339 case LP.Run{_pl}:
340 AR.Planned{add.plan(root, world)}
341
342# the add's plan made: refused, it is the plan; otherwise the lock is next
343def add.next.made(refused: Bool, +root: Rooted, +world: AR.World) -> AR.Next:
344 match refused:
345 case True{}:
346 AR.Planned{add.plan(root, world)}
347 case False{}:
348 add.next.lock(lock.step(add.lock(root, world)), root, world)
349
350# the add's own step: its questions, or its plan made
351def add.next.add(nx: AP.Next, +root: Rooted, +world: AR.World) -> AR.Next:
352 match nx:
353 case AP.AskGit{asks}:
354 AR.AskGit{asks}
355 case AP.AskHub{asks}:
356 AR.AskHub{asks}
357 case AP.Planned{pl}:
358 add.next.made(R.refuses(pl), root, world)
359
360# what the interpreter does next for `ez add`: in a project of its own, what
361# add/relock.bend says; in a workspace, the add, then the root's lock
362def add.next(ws: Ws, +world: AR.World) -> AR.Next:
363 match ws:
364 case Own{}:
365 AR.next(world)
366 case Inside{+root}:
367 add.next.add(AP.next(AR.add(world)), root, world)
368
369# `ez add`'s plan, wherever it runs
370def add.planned(ws: Ws, world: AR.World) -> LP.Plan:
371 match ws:
372 case Own{}:
373 AR.plan(world)
374 case Inside{+root}:
375 add.plan(root, world)
376
377# ---------------------------------------------------------------------------
378# `ez remove`
379
380# the remove's own plan, as the workspace runs it
381def remove.edit(+root: Rooted, +rw: RP.World) -> LP.Plan:
382 edit(root, RP.plan(rw))
383
384# the root's lock over what the remove leaves
385def remove.lock(+root: Rooted, world: RR.World) -> Lock:
386 RR.World{+rw, _here, _listing, locks} = world
387 lock.of(ledgers(root, remove.edit(root, rw), RR.ledger.was(rw)), root, locks)
388
389# `ez remove` in a workspace: the remove, then the root's lock over what it
390# leaves, run from where the remove runs
391def remove.plan(+root: Rooted, world: RR.World) -> LP.Plan:
392 RR.World{+rw, +here, +listing, +locks} = world
393 +lk = remove.lock(root, RR.World{rw, here, listing, locks})
394 R.join(remove.edit(root, rw), at.plan(rooted.at(root), lock.plan(lk)), [], RR.tail())
395
396# the lock's step, once the remove succeeds
397def remove.next.lock(st: LP.Step, +root: Rooted, +world: RR.World) -> RR.Next:
398 match st:
399 case LP.Asking{as, ups, ns}:
400 RR.AskLock{R.asks(LP.Asking{as, ups, ns})}
401 case LP.Run{_pl}:
402 RR.Planned{remove.plan(root, world)}
403
404# the remove refused is the plan; otherwise the lock is next
405def remove.next.made(refused: Bool, +root: Rooted, +world: RR.World) -> RR.Next:
406 match refused:
407 case True{}:
408 RR.Planned{remove.plan(root, world)}
409 case False{}:
410 remove.next.lock(lock.step(remove.lock(root, world)), root, world)
411
412# what the interpreter does next for `ez remove`: in a project of its own,
413# what remove/relock.bend says; in a workspace, the remove, then the root's
414# lock
415def remove.next(ws: Ws, +world: RR.World) -> RR.Next:
416 match ws:
417 case Own{}:
418 RR.next(world)
419 case Inside{+root}:
420 remove.next.made(R.refuses(RP.plan(RR.remove(world))), root, world)
421
422# `ez remove`'s plan, wherever it runs
423def remove.planned(ws: Ws, world: RR.World) -> LP.Plan:
424 match ws:
425 case Own{}:
426 RR.plan(world)
427 case Inside{+root}:
428 remove.plan(root, world)