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. 16 import Base 17 import ../lock/plan.bend as LP 18 import ../lock/world.bend as LW 19 import ../lock/lock.bend as L 20 import ../ledger/manifest.bend as M 21 import ../ledger/render.bend as Rend 22 import ../pkg/path.bend as P 23 import ../relock/relock.bend as R 24 import ../add/world.bend as A 25 import ../add/plan.bend as AP 26 import ../add/relock.bend as AR 27 import ../remove/plan.bend as RP 28 import ../remove/relock.bend as RR 29 import ./workspace.bend as W 30 import ./joint.bend as J 31 import ./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 37 type 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 48 type Ws is Data: 49 Own{} 50 Inside{root: Rooted} 51 52 # the root's directory 53 def 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 63 def 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. 75 def 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 89 def 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 97 def 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 106 def 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 115 def 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` 125 def 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 129 def 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 137 def 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 151 def 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 161 def 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 170 def 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 178 type 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 182 def 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` 190 def 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 200 def 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 209 def 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 218 def 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 227 type 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 232 def 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 237 def 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 258 def 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 276 def 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 282 def stop(+why: String) -> LP.Plan: 283 LP.Plan{[], LP.Refused{why}} 284 285 # the lock's plan 286 def 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 294 def 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 302 def 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 313 def 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 318 def 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 324 def 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 329 def 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 335 def 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 343 def 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 351 def 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 362 def 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 370 def 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 381 def 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 385 def 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 391 def 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 397 def 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 405 def 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 415 def 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 423 def 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)