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)