1 # ws/up: `ez lock --upgrade` at a workspace root (EZ-WS-4). The upgrade is 2 # planned, as the lock is, on the joint ledger (ws/joint.bend), so a source a 3 # name declares moves once for the whole workspace. What it writes back is 4 # not the joint ledger, which no file holds: the one write of ez.toml the 5 # upgrade plans is replaced by a write of every ledger that declares a moved 6 # name, the root's and each member's, each dependency and tool taking its new 7 # pin from the joint ledger the upgrade leaves while keeping the source as 8 # that ledger records it, relative to its own directory. Every other effect of 9 # the plan is kept as it was. 10 import Base 11 import ../ledger/manifest.bend as M 12 import ../ledger/render.bend as Rend 13 import ../toml/toml.bend as T 14 import ../lock/world.bend as LW 15 import ../lock/plan.bend as LP 16 import ../pkg/pkg.bend as K 17 import ./workspace.bend as W 18 import ./joint.bend as J 19 import ./moved.bend as Mv 20 21 # what the write-back needs: nothing, for a project of its own, or the root's 22 # ledger text, what its `[workspace]` table says, every other ez.toml git 23 # lists under the root, and the text of the lock being rewritten, "" for 24 # none, which records each member's hash before it moved (EZ-WS-7) 25 type Back is Data: 26 Alone{} 27 Back{text: String, spec: W.Spec, found: List<&2, J.Found>, lock: String} 28 29 # --------------------------------------------------------------------------- 30 # a pin moved back into a ledger 31 32 # a source with the pin the joint ledger now gives it: the joint's commit, 33 # tag, root and narHash, and this ledger's own url and vendor bit 34 def source.back(own: M.Source, joint: M.Source) -> M.Source: 35 match own: 36 case M.Hub{nv}: 37 M.Hub{nv} 38 case M.Git{url, _rev, _tag, _root, _nar, vend}: 39 match joint: 40 case M.Hub{_jn}: 41 M.Git{url, _rev, _tag, _root, _nar, vend} 42 case M.Git{_ju, jr, jt, jo, jn, _jv}: 43 M.Git{url, jr, jt, jo, jn, vend} 44 case M.Member{_jp, _jm}: 45 M.Git{url, _rev, _tag, _root, _nar, vend} 46 case M.Member{path, named}: 47 M.Member{path, named} 48 49 # a dependency with the pin the joint ledger gives its name, by its own 50 # source: a workspace member's section is kept as it is written, since the 51 # hash and entry the joint ledger gives it are no ledger's to record 52 def dep.back.src(own: M.Source, +nn: String, +hh: String, +ee: String, joint: M.Dep) -> M.Dep: 53 match own: 54 case M.Member{path, nv}: 55 M.Dep{nn, hh, ee, M.Member{path, nv}} 56 case M.Hub{nv}: 57 M.Dep{_jn, jh, je, js} = joint 58 M.Dep{nn, jh, je, source.back(M.Hub{nv}, js)} 59 case M.Git{url, rev, tag, root, nar, vend}: 60 M.Dep{_jn, jh, je, js} = joint 61 M.Dep{nn, jh, je, source.back(M.Git{url, rev, tag, root, nar, vend}, js)} 62 63 # a dependency with the pin the joint ledger gives its name 64 def dep.back.at(own: M.Dep, joint: M.Dep) -> M.Dep: 65 M.Dep{nn, hh, ee, os} = own 66 dep.back.src(os, nn, hh, ee, joint) 67 68 # a dependency of a ledger, after the upgrade 69 def dep.back(+own: M.Dep, +js: List<&2, M.Dep>) -> M.Dep: 70 dep.back.at(own, M.find(js, M.dep.name(own))) 71 72 # every dependency of a ledger, after the upgrade 73 def deps.back(ds: List<&2, M.Dep>, +js: List<&2, M.Dep>) -> List<&2, M.Dep>: 74 match ds: 75 case []: 76 [] 77 case +hd <> tl: 78 dep.back(hd, js) <> deps.back(tl, js) 79 80 # a tool with the pin the joint ledger gives its name 81 def tool.back.at(own: M.Tool, joint: M.Tool) -> M.Tool: 82 M.Tool{nn, ee, bb, os} = own 83 M.Tool{_jn, _je, _jb, js} = joint 84 M.Tool{nn, ee, bb, source.back(os, js)} 85 86 # a tool of a ledger, after the upgrade 87 def tool.back(+own: M.Tool, +ts: List<&2, M.Tool>) -> M.Tool: 88 tool.back.at(own, M.tool.find(ts, M.tool.name(own))) 89 90 # every tool of a ledger, after the upgrade 91 def tools.back(ts: List<&2, M.Tool>, +js: List<&2, M.Tool>) -> List<&2, M.Tool>: 92 match ts: 93 case []: 94 [] 95 case +hd <> tl: 96 tool.back(hd, js) <> tools.back(tl, js) 97 98 # a ledger after the upgrade, every pin taken from the joint ledger 99 def ledger.back(mm: M.Manifest, joint: M.Manifest) -> M.Manifest: 100 M.Manifest{nn, ee, bb, hh, pa, pv, ds, ts} = mm 101 M.Manifest{_jn, _je, _jb, _jh, _jpa, _jpv, +jd, +jt} = joint 102 M.Manifest{nn, ee, bb, hh, pa, pv, deps.back(ds, jd), tools.back(ts, jt)} 103 104 # --------------------------------------------------------------------------- 105 # the root's ledger, written back with its `[workspace]` table 106 107 # texts written as a TOML array of strings 108 def array.items(ts: List<&2, String>) -> String: 109 match ts: 110 case []: 111 "" 112 case +hd <> []: 113 T.quote(hd) 114 case +hd <> tl: 115 T.quote(hd) ++ ", " ++ array.items(tl) 116 117 # every entry as it was written 118 def entries.text(es: List<&2, W.Entry>) -> List<&2, String>: 119 match es: 120 case []: 121 [] 122 case hd <> tl: 123 W.entry.text(hd) <> entries.text(tl) 124 125 # every path as it was written 126 def paths.text(ps: List<&2, List<&2, String>>) -> List<&2, String>: 127 match ps: 128 case []: 129 [] 130 case hd <> tl: 131 W.segs.text(hd) <> paths.text(tl) 132 133 # the `[workspace]` table, as it says what it says 134 def spec.text(spec: W.Spec) -> String: 135 W.Spec{es, ex} = spec 136 "[workspace]\nmembers = [" ++ array.items(entries.text(es)) ++ "]\nexclude = [" ++ 137 array.items(paths.text(ex)) ++ "]\n" 138 139 # the root's ledger as a file: its package table when it has one, which a 140 # virtual root does not, its dependencies and tools, and its `[workspace]` 141 def root.text(+mm: M.Manifest, +spec: W.Spec, +root: W.Root) -> String: 142 +body = Rend.text(mm) 143 T.normal(Bool.pick(String, W.root.member(root), body, String.drop(body, 10n)) ++ spec.text(spec)) 144 145 # --------------------------------------------------------------------------- 146 # the writes 147 148 # a ledger written back when the upgrade changed what it holds, and left as 149 # it was otherwise 150 def write.if(+mm: M.Manifest, +joint: M.Manifest, +at: String, +text: String) -> List<&2, LP.Effect>: 151 Bool.pick(List<&2, LP.Effect>, String.eq(Rend.show(mm), Rend.show(ledger.back(mm, joint))), 152 [], [LP.Write{at, text}]) 153 154 # a member's ledger, written back at its directory's ez.toml 155 def member.write(rd: M.Read, +ds: List<&2, String>, +joint: M.Manifest) -> List<&2, LP.Effect>: 156 match rd: 157 case M.Bad{_why}: 158 [] 159 case M.Good{+mm}: 160 write.if(mm, joint, W.segs.text(ds) ++ "/ez.toml", Rend.show(ledger.back(mm, joint))) 161 162 # a member's ledger written back, or, for another's, nothing 163 def found.pick(hit: Bool, +text: String, +ds: List<&2, String>, +joint: M.Manifest) -> List<&2, LP.Effect>: 164 match hit: 165 case True{}: 166 member.write(M.parse(text), ds, joint) 167 case False{}: 168 [] 169 170 # one ez.toml git listed, written back when it is a member's 171 def found.write( 172 lg: Maybe<&2, W.Ledger>, 173 +text: String, 174 +dirs: List<&2, List<&2, String>>, 175 +joint: M.Manifest 176 ) -> List<&2, LP.Effect>: 177 match lg: 178 case None{}: 179 [] 180 case Some{ll}: 181 +ds = W.ledger.dir(ll) 182 found.pick(W.has(dirs, ds), text, ds, joint) 183 184 # every member's ledger the upgrade changed, written back 185 def members.write( 186 fs: List<&2, J.Found>, 187 +dirs: List<&2, List<&2, String>>, 188 +joint: M.Manifest 189 ) -> List<&2, LP.Effect>: 190 match fs: 191 case []: 192 [] 193 case J.Found{+pp, +tt} <> tl: 194 List.append(&2, LP.Effect, found.write(W.ledger.of(pp, tt), tt, dirs, joint), 195 members.write(tl, dirs, joint)) 196 197 # the root's ledger, written back when the upgrade changed it 198 def root.write(rd: M.Read, +spec: W.Spec, +root: W.Root, +joint: M.Manifest) -> List<&2, LP.Effect>: 199 match rd: 200 case M.Bad{_why}: 201 [] 202 case M.Good{+mm}: 203 write.if(mm, joint, "ez.toml", root.text(ledger.back(mm, joint), spec, root)) 204 205 # every ledger the upgrade changed, the root's first, once the member set is 206 # known 207 def writes.members( 208 ms: W.Members, 209 +text: String, 210 +spec: W.Spec, 211 +fs: List<&2, J.Found>, 212 +joint: M.Manifest 213 ) -> List<&2, LP.Effect>: 214 match ms: 215 case W.Refused{_why}: 216 [] 217 case W.Members{_root, +dirs}: 218 List.append(&2, LP.Effect, root.write(M.parse(text), spec, W.root.of(text), joint), 219 members.write(fs, dirs, joint)) 220 221 # every ledger the upgrade changed, once the joint ledger it leaves was read 222 def writes.joint(joint: M.Read, +text: String, +spec: W.Spec, +fs: List<&2, J.Found>) -> List<&2, LP.Effect>: 223 match joint: 224 case M.Bad{_why}: 225 [] 226 case M.Good{+jm}: 227 writes.members(W.members(spec, W.root.of(text), J.ledgers(fs)), text, spec, fs, jm) 228 229 # every ledger the upgrade changed, from the joint ledger it leaves 230 def writes(back: Back, joint: M.Read) -> List<&2, LP.Effect>: 231 match back: 232 case Alone{}: 233 [] 234 case Back{+text, +spec, +fs, _lock}: 235 writes.joint(joint, text, spec, fs) 236 237 # a write kept, or, when it is of ez.toml, the ledgers' in its place 238 def effect.write( 239 hit: Bool, 240 at: String, 241 text: String, 242 ws: List<&2, LP.Effect>, 243 rest: List<&2, LP.Effect> 244 ) -> List<&2, LP.Effect>: 245 match hit: 246 case True{}: 247 List.append(&2, LP.Effect, ws, rest) 248 case False{}: 249 LP.Write{at, text} <> rest 250 251 # one effect of the plan: the write of ez.toml replaced by the ledgers' 252 def effect.back(effect: LP.Effect, +ws: List<&2, LP.Effect>, rest: List<&2, LP.Effect>) -> List<&2, LP.Effect>: 253 match effect: 254 case LP.Write{+at, text}: 255 effect.write(String.eq(at, "ez.toml"), at, text, ws, rest) 256 case LP.Lay{pl, hash, files}: 257 LP.Lay{pl, hash, files} <> rest 258 case LP.Drop{hash}: 259 LP.Drop{hash} <> rest 260 case LP.Say{text}: 261 LP.Say{text} <> rest 262 case LP.Name{nv, hash}: 263 LP.Name{nv, hash} <> rest 264 265 # every effect, with the write of ez.toml replaced 266 def effects.back(es: List<&2, LP.Effect>, +ws: List<&2, LP.Effect>) -> List<&2, LP.Effect>: 267 match es: 268 case []: 269 [] 270 case hd <> tl: 271 rest = effects.back(tl, ws) 272 effect.back(hd, ws, rest) 273 274 # the plan with its write of ez.toml replaced by the ledgers' 275 def plan.back(pl: LP.Plan, +ws: List<&2, LP.Effect>) -> LP.Plan: 276 LP.Plan{es, outcome} = pl 277 LP.Plan{effects.back(es, ws), outcome} 278 279 # --------------------------------------------------------------------------- 280 # members imported by their hash (EZ-WS-7) 281 282 # the World an upgrade plans on, and the files it rewrites 283 type Up is Data: 284 Up{world: LW.World, edits: List<&2, LP.Effect>} 285 286 # the committed files a World lists 287 def files.of(world: LW.World) -> List<&2, K.Source>: 288 LW.World{_args, _ledger, listing, _replies, _ups} = world 289 J.files(listing) 290 291 # an upgrade at a workspace root, once its moves are known: the World with 292 # every committed file rewritten by them, and the writes of the files they 293 # change 294 def up.moves(+ms: List<&2, Mv.Shift>, +text: String, +fs: List<&2, J.Found>, +world: LW.World) -> Up: 295 +files = files.of(world) 296 +moved = Mv.rewrite(Mv.swaps(ms), files) 297 Up{Mv.world.moved(J.joint(text, fs, moved), moved, world), Mv.edits(Mv.swaps(ms), files)} 298 299 # an upgrade at a workspace root: every member it moves (ws/moved.bend), its 300 # import lines rewritten, until none moves further 301 def up.of(+text: String, +fs: List<&2, J.Found>, +lock: String, +world: LW.World) -> Up: 302 up.moves(Mv.cascade(Mv.ctx(text, fs, files.of(world), lock, LW.args.only(world))), text, fs, world) 303 304 # the World an upgrade plans on 305 def up.world(got: Up) -> LW.World: 306 Up{w2, _eds} = got 307 w2 308 309 # the World `ez lock` plans on at a workspace root: a plain lock's as it is, 310 # and an upgrade's with every moved member's import lines rewritten 311 def world.at(up: Bool, +text: String, +fs: List<&2, J.Found>, +lock: String, +world: LW.World) -> LW.World: 312 match up: 313 case True{}: 314 up.world(up.of(text, fs, lock, world)) 315 case False{}: 316 world 317 318 # the World `ez lock` plans on 319 def plans.on(back: Back, +world: LW.World) -> LW.World: 320 match back: 321 case Alone{}: 322 world 323 case Back{+text, _spec, +fs, +lock}: 324 world.at(LW.args.upgrade(world), text, fs, lock, world) 325 326 # why a plain lock at a workspace root refuses before it asks anything: a 327 # committed line names the hash the lock being rewritten records for a member 328 # that has moved since. "" when it does not, and always for an upgrade, which 329 # rewrites the line, and for a project of its own. 330 def stale.at(up: Bool, +lock: String, +world: LW.World) -> String: 331 match up: 332 case True{}: 333 "" 334 case False{}: 335 Mv.stale.world(lock, world) 336 337 # and at the root as the command found it 338 def stale(back: Back, +world: LW.World) -> String: 339 match back: 340 case Alone{}: 341 "" 342 case Back{_text, _spec, _fs, +lock}: 343 stale.at(LW.args.upgrade(world), lock, world) 344 345 # the plan that refuses, writing nothing 346 def refused(+why: String) -> LP.Plan: 347 LP.Plan{[], LP.Refused{why}} 348 349 # the files an upgrade rewrites, written ahead of the plan, unless it refuses 350 def edits.put(refuses: Bool, eds: List<&2, LP.Effect>, pl: LP.Plan) -> LP.Plan: 351 match refuses: 352 case True{}: 353 pl 354 case False{}: 355 LP.Plan{es, outcome} = pl 356 LP.Plan{List.append(&2, LP.Effect, eds, es), outcome} 357 358 # an upgrade's plan on the World it rewrote: the files it rewrote written 359 # first, so a source the lock planner rewrites too is left as the planner 360 # writes it, and then the planner's plan with the ledgers written back 361 def plan.up(got: Up, +back: Back) -> LP.Plan: 362 Up{+w2, eds} = got 363 edits.put(LP.refuses(w2), eds, plan.back(LP.plan(w2), writes(back, LP.ledger.next(w2)))) 364 365 # a plain lock's plan, once it is known whether a line is stale 366 def plan.plain(fine: Bool, +why: String, +world: LW.World) -> LP.Plan: 367 match fine: 368 case True{}: 369 LP.plan(world) 370 case False{}: 371 refused(why) 372 373 # what `ez lock` runs at a workspace root, under `--upgrade` or not 374 def plan.at(up: Bool, +back: Back, +world: LW.World) -> LP.Plan: 375 match up: 376 case True{}: 377 match back: 378 case Alone{}: 379 LP.plan(world) 380 case Back{+text, +spec, +fs, +lock}: 381 plan.up(up.of(text, fs, lock, world), Back{text, spec, fs, lock}) 382 case False{}: 383 match back: 384 case Alone{}: 385 LP.plan(world) 386 case Back{_text, _spec, _fs, +lock}: 387 +why = Mv.stale.world(lock, world) 388 plan.plain(String.is_empty(why), why, world) 389 390 # what `ez lock` runs: for a project of its own, the plan the lock planner 391 # makes; at a workspace root, the plan it makes on the joint ledger, with, 392 # under `--upgrade`, every moved member's import lines rewritten and every 393 # ledger the upgrade changed written back in place of the joint one. A plain 394 # lock writes no ledger, so its plan is as it was, unless a line names a 395 # moved member's old hash, which it refuses. 396 def plan(back: Back, +world: LW.World) -> LP.Plan: 397 match back: 398 case Alone{}: 399 LP.plan(world) 400 case Back{+text, +spec, +fs, +lock}: 401 plan.at(LW.args.upgrade(world), Back{text, spec, fs, lock}, world) 402 403 # the step a plain lock takes, once it is known whether a line is stale 404 def step.plain(fine: Bool, +why: String, +world: LW.World) -> LP.Step: 405 match fine: 406 case True{}: 407 LP.step(world) 408 case False{}: 409 LP.Run{refused(why)} 410 411 # the lock planner's step on the World `ez lock` plans on: its questions, or 412 # its plan. A plain lock with a stale line asks nothing and refuses. 413 def step(+back: Back, +world: LW.World) -> LP.Step: 414 +why = stale(back, world) 415 step.plain(String.is_empty(why), why, plans.on(back, world)) 416