1 # add/relock: `ez add` as it runs, the add and the lock in one plan 2 # (EZ-DOC-6). The add is planned as before (`AP.command`, add/plan.bend); 3 # once it is known to succeed, the plain `ez lock` planner is run over the 4 # World it leaves (`R.world`, relock/relock.bend): the ledger the add writes, 5 # the `.bend` files git lists, and the lock's answers. The package the add 6 # walked is handed to the lock as the answer for its hash, read as a tree 7 # under BEND_LIB, which is where the add lays it, so it is not read twice; a 8 # hub package's answers are the ones the add was given. Every other package 9 # the lock asks about is answered by the lock's interpreter as `ez lock` 10 # answers it. 11 # 12 # The plan, when both succeed: the add's effects, a note when the lock does 13 # not hold the package added (EZ-DOC-7), then the lock's effects, the lock 14 # last. When either refuses, a refusal with no effect at all. 15 import Base 16 import ./world.bend as A 17 import ./plan.bend as AP 18 import ./hub.bend as H 19 import ../lock/plan.bend as P 20 import ../lock/world.bend as W 21 import ../lock/up.bend as Up 22 import ../lock/lock.bend as L 23 import ../ledger/manifest.bend as M 24 import ../pkg/pkg.bend as K 25 import ../relock/relock.bend as R 26 27 # what `ez add` reads: the add's World, the `.bend` files git lists, and the 28 # answers to the lock's questions 29 type World is Data: 30 World{add: A.World, listing: W.Listing, locks: List<&2, W.Reply>} 31 32 # the add's World 33 def add(world: World) -> A.World: 34 World{a, _listing, _locks} = world 35 a 36 37 # --------------------------------------------------------------------------- 38 # the lock's World 39 40 # this file's text when it is the one at the path, else the rest's. The rest 41 # arrives as a thunk, so the first hit ends the scan. 42 def found.pick(hit: Bool, body: String, rest: Unit -> String) -> String: 43 match hit: 44 case True{}: 45 body 46 case False{}: 47 rest(Unit{}) 48 49 # the text of the file at a path among a walk's files, "" when none is there 50 def text.at(fs: List<&2, K.Source>, +at: String) -> String: 51 match fs: 52 case []: 53 "" 54 case K.Source{+p, body} <> t: 55 found.pick(String.eq(p, at), body, _u => text.at(t, at)) 56 57 # the text of each file a manifest names, in its order 58 def texts(+fs: List<&2, K.Source>, items: List<&2, K.Item>) -> List<&2, String>: 59 match items: 60 case []: 61 [] 62 case K.Item{at, _sum} <> t: 63 text.at(fs, at) <> texts(fs, t) 64 65 # the package a walk made, as the answer to the lock's question about its 66 # hash: its manifest and the text of each file it names, read as a tree under 67 # BEND_LIB, which is where the add lays it. The lock checks it as it checks 68 # any tree there. A walk that made no package answers nothing. 69 def seed.of(walk: K.Walked) -> List<&2, W.Reply>: 70 match walk: 71 case K.Walked{h, _root, +fs, ss}: 72 +manifest = K.manifest_of(ss) 73 [W.Reply{W.Pkg{h, L.Hub{}, ""}, 74 W.Got{W.Lib{}, manifest, texts(fs, L.manifest.files(String.lines(manifest)))}}] 75 case K.Asks{_at}: 76 [] 77 case K.Refused{_why}: 78 [] 79 80 # the answers the add already has for the lock: a hub package's are those the 81 # hub gave the add, and a git package's is the package its walk made 82 def seeds.of(named: Bool, +aw: A.World) -> List<&2, W.Reply>: 83 match named: 84 case True{}: 85 A.hubs(aw) 86 case False{}: 87 seed.of(AP.walked(aw)) 88 89 # the answers the add already has for the lock, on the add's World 90 def seeds(+aw: A.World) -> List<&2, W.Reply>: 91 seeds.of(A.named(aw), aw) 92 93 # the World a plain `ez lock` reads once the add has run: the ledger it 94 # writes, the `.bend` files git lists, and the answers it was given, the add's 95 # first 96 def lock.world(world: World) -> W.World: 97 World{+a, listing, locks} = world 98 R.world(A.here(a), R.ledger(AP.command(a), A.ledger(a)), listing, 99 List.append(&2, W.Reply, seeds(a), locks)) 100 101 # the plan of that lock 102 def lock(world: World) -> P.Plan: 103 P.plan(lock.world(world)) 104 105 # --------------------------------------------------------------------------- 106 # the note (EZ-DOC-7) 107 108 # the hash of the package the add records 109 def added.of(named: Bool, +aw: A.World) -> String: 110 match named: 111 case True{}: 112 H.hash(aw) 113 case False{}: 114 AP.hash(aw) 115 116 # the hash of the package the add records, on the add's World 117 def added(+aw: A.World) -> String: 118 added.of(A.named(aw), aw) 119 120 # whether the lock holds a package under the hash 121 def held(ps: List<&2, L.Pack>, +hash: String) -> Bool: 122 match ps: 123 case []: 124 False{} 125 case h <> t: 126 L.any.step(String.eq(L.pack.hash(h), hash), _u => held(t, hash)) 127 128 # the line that says the lock does not hold it 129 def unlocked(+hash: String) -> String: 130 "ez: no .bend file git lists imports " ++ hash ++ " yet, so ez.lock.toml does " ++ 131 "not hold it. Import it, `git add` the file, and run `ez lock`." 132 133 # the note: the line when the lock does not hold the package, none when it does 134 def notes.of(held: Bool, +hash: String) -> List<&2, P.Effect>: 135 match held: 136 case True{}: 137 [] 138 case False{}: 139 [P.Say{unlocked(hash)}] 140 141 # the note on a World 142 def notes(+world: World) -> List<&2, P.Effect>: 143 notes.of(held(P.packs(lock.world(world)), added(add(world))), added(add(world))) 144 145 # --------------------------------------------------------------------------- 146 # the plan 147 148 # what is added after the lock's reason when the lock refuses: that nothing 149 # was written, and, when the add moves a dependency's hash, where the old one 150 # may still be imported from 151 def moved.of(moved: Bool, +name: String, +old: String, +new: String) -> String: 152 match moved: 153 case True{}: 154 "\nez: ez add moves " ++ name ++ " from " ++ old ++ " to " ++ new ++ 155 "; a .bend file that imports " ++ old ++ " has to name " ++ new ++ 156 " first, or `ez lock --upgrade --package " ++ name ++ "` moves it" 157 case False{}: 158 "" 159 160 def tail.dep(+dep: M.Dep, +old: M.Dep) -> String: 161 "\nez: ez add wrote nothing, since it writes ez.toml and ez.lock.toml together" ++ 162 moved.of(Bool.and(Bool.not(String.is_empty(M.dep.hash(old))), 163 Bool.not(String.eq(M.dep.hash(old), M.dep.hash(dep)))), 164 M.dep.name(dep), M.dep.hash(old), M.dep.hash(dep)) 165 166 # the dependency the add records 167 def dep.of(named: Bool, +aw: A.World) -> M.Dep: 168 match named: 169 case True{}: 170 H.added(aw) 171 case False{}: 172 AP.added(aw) 173 174 # what is added after the lock's reason, on the add's World 175 def tail(+aw: A.World) -> String: 176 +dep = dep.of(A.named(aw), aw) 177 tail.dep(dep, M.find(A.deps(aw), M.dep.name(dep))) 178 179 # `ez add` over a World: the add, then the lock over what it leaves 180 def plan(world: World) -> P.Plan: 181 World{+a, +listing, +locks} = world 182 R.join(AP.command(a), lock(World{a, listing, locks}), notes(World{a, listing, locks}), 183 tail(a)) 184 185 # whether `ez add` refuses on a World 186 def refuses(world: World) -> Bool: 187 R.refuses(plan(world)) 188 189 # --------------------------------------------------------------------------- 190 # the step 191 192 # what the interpreter does next: answer git's questions or the hub's for the 193 # add, answer the lock's, or run the plan 194 type Next is Data: 195 AskGit{asks: List<&2, Up.Ask>} 196 AskHub{asks: List<&2, W.Ask>} 197 AskLock{asks: List<&2, W.Ask>} 198 Planned{plan: P.Plan} 199 200 # the lock's step, once the add is planned and succeeds 201 def next.lock(st: P.Step, +world: World) -> Next: 202 match st: 203 case P.Asking{as, ups, ns}: 204 AskLock{R.asks(P.Asking{as, ups, ns})} 205 case P.Run{_pl}: 206 Planned{plan(world)} 207 208 # the add's plan made: refused, it is the plan; otherwise the lock is next 209 def next.made(refused: Bool, +world: World) -> Next: 210 match refused: 211 case True{}: 212 Planned{plan(world)} 213 case False{}: 214 next.lock(P.step(lock.world(world)), world) 215 216 def next.add(nx: AP.Next, +world: World) -> Next: 217 match nx: 218 case AP.AskGit{asks}: 219 AskGit{asks} 220 case AP.AskHub{asks}: 221 AskHub{asks} 222 case AP.Planned{pl}: 223 next.made(R.refuses(pl), world) 224 225 # what the interpreter does next on a World 226 def next(+world: World) -> Next: 227 next.add(AP.next(add(world)), world)