1 # lock/up: what `ez lock --upgrade` decides, as a pure function of what the 2 # remote said. It is the upgrade half of the planner (lock/plan.bend): it reads 3 # the ledger and the answers to its own questions, and returns either the 4 # questions it still has, a refusal, or the ledger the lock is made from and 5 # everything the upgrade writes around it. The interpreter (lock/run.bend) 6 # answers each question by IO, and the planner turns the result into effects. 7 # 8 # A git dependency with a tag is re-resolved to that tag; one with only a 9 # commit fast-forwards to the default branch tip when the pin is an ancestor 10 # of it. A hub dependency is left, because the hash is the pin. `--package` 11 # selects one dependency or tool, and no other entry is asked about. A tool 12 # pin with no commit or no NAR hash is filled here and nowhere else. A moved 13 # dependency marked `vendor = true` has its new tree committed under 14 # `.ez/lib` and its old one removed, the gitignore allowlist is derived again 15 # from the ledger the upgrade leaves (`I.sync`), and an import that names a 16 # moved hash is rewritten. Nothing is written when the upgrade refuses: that 17 # is the planner's, since a refusal is a plan with no effects. 18 # 19 # A same commit is checked out and weighed, and `U.judge` is given whether 20 # that checkout agrees with the pin, so a pinned commit whose tree no longer 21 # hashes to the pin is `Drift` and refused. 22 import Base 23 import ./lock.bend as L 24 import ../ledger/manifest.bend as M 25 import ../ledger/render.bend as Rend 26 import ../ledger/upgrade.bend as U 27 import ../ledger/ignore.bend as I 28 import ../git/git.bend as Git 29 import ../pkg/pkg.bend as K 30 import ../pkg/path.bend as Path 31 import ../hub/hub.bend as Web 32 import ../share/sha.bend as Sha 33 import ../share/say.bend as Say 34 35 # --------------------------------------------------------------------------- 36 # questions and answers 37 38 # a question the upgrade asks, answered by one IO action. `ez add` 39 # (add/plan.bend) asks four of them too: `Tags`, `Head` and `Refs` to resolve 40 # a ref, and `Clone` for the checkout it walks. The upgrade asks each with 41 # the source its ledger records, and looks the answer up by that; git is asked 42 # the same question with that source anchored at the project (`anchor`), 43 # which the planner computes (lock/plan.bend, `P.Query`). `ez add` and 44 # `ez fetch` ask with the source anchored already. 45 type Ask is Data: 46 Refs{url: String, ref: String} # `git ls-remote url refs/tags/ ... refs/heads/` 47 Head{url: String} # `git ls-remote --symref url HEAD` 48 Tags{url: String} # `git ls-remote --tags url`, for a tool with no tag 49 Above{url: String, pin: String, tip: String} # whether `tip` descends from `pin` 50 Tree{url: String, rev: String, entry: String} # a checkout at rev: narHash, root, the package its entry reaches 51 Weigh{url: String, rev: String} # a checkout at rev, narHash only, for a tool 52 HubHas{hub: String, hash: String} # the hub's manifest for a hash a dependency moved to 53 Ignore{} # `.gitignore`, "" when missing 54 Sources{} # every `.bend` file outside `.ez` and `.git` 55 Clone{url: String, rev: String} # a checkout at rev: narHash and every file's text, for `ez add` 56 57 # the source a question names, "" for one that names none 58 def ask.url(ask: Ask) -> String: 59 match ask: 60 case Refs{url, _ref}: 61 url 62 case Head{url}: 63 url 64 case Tags{url}: 65 url 66 case Above{url, _pin, _tip}: 67 url 68 case Tree{url, _rev, _entry}: 69 url 70 case Weigh{url, _rev}: 71 url 72 case HubHas{_hub, _hash}: 73 "" 74 case Ignore{}: 75 "" 76 case Sources{}: 77 "" 78 case Clone{url, _rev}: 79 url 80 81 # a question as git is to be asked it: the source it names anchored at the 82 # directory `here` (`Path.anchor`), so a relative path, which the ledger keeps 83 # relative to the project, is read from the project wherever git runs. A URL 84 # and an absolute path are as they are, and a question that names no source 85 # is as it is. 86 def anchor(+here: String, ask: Ask) -> Ask: 87 match ask: 88 case Refs{url, ref}: 89 Refs{Path.anchor(here, url), ref} 90 case Head{url}: 91 Head{Path.anchor(here, url)} 92 case Tags{url}: 93 Tags{Path.anchor(here, url)} 94 case Above{url, pin, tip}: 95 Above{Path.anchor(here, url), pin, tip} 96 case Tree{url, rev, entry}: 97 Tree{Path.anchor(here, url), rev, entry} 98 case Weigh{url, rev}: 99 Weigh{Path.anchor(here, url), rev} 100 case HubHas{hub, hash}: 101 HubHas{hub, hash} 102 case Ignore{}: 103 Ignore{} 104 case Sources{}: 105 Sources{} 106 case Clone{url, rev}: 107 Clone{Path.anchor(here, url), rev} 108 109 # one source file of the project, with its text 110 type Found is Data: 111 Found{at: String, text: String} 112 113 # the answer to one question. Git's output is kept as it was printed, with 114 # whether the command succeeded, so reading it is the planner's decision. 115 type Answer is Data: 116 Printed{ok: Bool, text: String} # Refs, Head, Tags 117 Onward{yes: Bool} # Above 118 Checkout{nar: String, root: String, manifest: String, srcs: List<&2, String>} # Tree 119 Weighed{nar: String} # Weigh 120 HubSaid{got: Web.Got} # HubHas 121 Text{text: String} # Ignore 122 Files{files: List<&2, Found>} # Sources 123 Cloned{nar: String, files: List<&2, K.Source>} # Clone 124 Miss{why: String} # any question IO could not answer 125 126 # one question, answered 127 type Reply is Data: 128 Reply{ask: Ask, answer: Answer} 129 130 # a tree checked out at a rev a dependency moved to: the package the lock 131 # reads for its new hash, so it is not cloned a second time 132 type Laid is Data: 133 Laid{hash: String, nar: String, manifest: String, srcs: List<&2, String>} 134 135 # a file the upgrade writes: ez.toml, `.gitignore`, or a source 136 type Edit is Data: 137 Edit{at: String, text: String} 138 139 # what the upgrade decided. `model` is the ledger model the upgrade renders 140 # into ez.toml, and ez.toml as it is when the upgrade refuses, still asks, or 141 # moves nothing; the upgrade's decisions are stated over it. `read` is the 142 # ledger the lock is made from: the one the upgrade writes, read back, or 143 # ez.toml as it is when nothing moved. 144 # `stop` is why the upgrade refuses, "" when it does not; a World with 145 # questions still open refuses too, and names them in `asks`. The rest is what 146 # the upgrade writes around the lock: the trees it checked out, the hashes 147 # committed under `.ez/lib`, the lines a person is told, ez.toml and 148 # `.gitignore` when they change, the project's sources, which it writes where 149 # a moved hash is rewritten (`edit.sources`), the committed trees it removes, 150 # and the hashes import lines stop naming, each with the hash that replaces 151 # it, which the lock reads the committed sources through. The sources are 152 # rewritten once, by the plan, and not on every round of questions. 153 type Next is Data: 154 Next{model: M.Read, read: M.Read, stop: String, asks: List<&2, Ask>, trees: List<&2, Laid>, 155 vends: List<&2, String>, says: List<&2, String>, edits: List<&2, Edit>, 156 found: List<&2, Found>, drops: List<&2, String>, swaps: List<&2, U.Swap>} 157 158 def next.model(nx: Next) -> M.Read: 159 Next{model, _read, _stop, _asks, _trees, _vends, _says, _edits, _found, _drops, _swaps} = nx 160 model 161 162 def next.read(nx: Next) -> M.Read: 163 Next{_model, read, _stop, _asks, _trees, _vends, _says, _edits, _found, _drops, _swaps} = nx 164 read 165 166 def next.stop(nx: Next) -> String: 167 Next{_model, _read, stop, _asks, _trees, _vends, _says, _edits, _found, _drops, _swaps} = nx 168 stop 169 170 def next.asks(nx: Next) -> List<&2, Ask>: 171 Next{_model, _read, _stop, asks, _trees, _vends, _says, _edits, _found, _drops, _swaps} = nx 172 asks 173 174 def next.trees(nx: Next) -> List<&2, Laid>: 175 Next{_model, _read, _stop, _asks, trees, _vends, _says, _edits, _found, _drops, _swaps} = nx 176 trees 177 178 def next.vends(nx: Next) -> List<&2, String>: 179 Next{_model, _read, _stop, _asks, _trees, vends, _says, _edits, _found, _drops, _swaps} = nx 180 vends 181 182 def next.says(nx: Next) -> List<&2, String>: 183 Next{_model, _read, _stop, _asks, _trees, _vends, says, _edits, _found, _drops, _swaps} = nx 184 says 185 186 def next.edits(nx: Next) -> List<&2, Edit>: 187 Next{_model, _read, _stop, _asks, _trees, _vends, _says, edits, _found, _drops, _swaps} = nx 188 edits 189 190 def next.found(nx: Next) -> List<&2, Found>: 191 Next{_model, _read, _stop, _asks, _trees, _vends, _says, _edits, found, _drops, _swaps} = nx 192 found 193 194 def next.drops(nx: Next) -> List<&2, String>: 195 Next{_model, _read, _stop, _asks, _trees, _vends, _says, _edits, _found, drops, _swaps} = nx 196 drops 197 198 def next.swaps(nx: Next) -> List<&2, U.Swap>: 199 Next{_model, _read, _stop, _asks, _trees, _vends, _says, _edits, _found, _drops, swaps} = nx 200 swaps 201 202 # a plain lock: ez.toml as it is, and nothing around it 203 def plain(+read: M.Read) -> Next: 204 Next{read, read, "", [], [], [], [], [], [], [], []} 205 206 # a refusal's reason, never empty. An empty reason is no refusal at all, so 207 # an upgrade that stopped with one would otherwise go on to lock the ledger 208 # as it was, and a pin that halted with one would drop out of the ledger the 209 # upgrade writes. The interpreter never answers with an empty reason. 210 def halt.why(+why: String) -> String: 211 Bool.pick(String, String.is_empty(why), "ez: the upgrade stopped without a reason", why) 212 213 # an upgrade that refuses, or still asks: the lock is not made, and nothing is 214 # written 215 def stopped(+read: M.Read, +why: String, asks: List<&2, Ask>) -> Next: 216 Next{read, read, halt.why(why), asks, [], [], [], [], [], [], []} 217 218 # --------------------------------------------------------------------------- 219 # looking an answer up 220 221 # a question as a key. A ledger value never holds a newline, so the fields 222 # are kept apart by one. 223 def key(ask: Ask) -> String: 224 match ask: 225 case Refs{url, ref}: 226 "refs\n" ++ url ++ "\n" ++ ref 227 case Head{url}: 228 "head\n" ++ url 229 case Tags{url}: 230 "tags\n" ++ url 231 case Above{url, pin, tip}: 232 "above\n" ++ url ++ "\n" ++ pin ++ "\n" ++ tip 233 case Tree{url, rev, entry}: 234 "tree\n" ++ url ++ "\n" ++ rev ++ "\n" ++ entry 235 case Weigh{url, rev}: 236 "weigh\n" ++ url ++ "\n" ++ rev 237 case HubHas{hub, hash}: 238 "hub\n" ++ hub ++ "\n" ++ hash 239 case Ignore{}: 240 "ignore" 241 case Sources{}: 242 "sources" 243 case Clone{url, rev}: 244 "clone\n" ++ url ++ "\n" ++ rev 245 246 # an answer looked up, or none yet 247 type Heard is Data: 248 Heard{answer: Answer} 249 Open{} 250 251 # this answer when it is the question's, otherwise the rest's. The rest 252 # arrives as a thunk, so the first hit ends the scan. 253 def look.step(hit: Bool, answer: Answer, rest: Unit -> Heard) -> Heard: 254 match hit: 255 case True{}: 256 Heard{answer} 257 case False{}: 258 rest(Unit{}) 259 260 # the first answer given to a question 261 def look(rs: List<&2, Reply>, +want: String) -> Heard: 262 match rs: 263 case []: 264 Open{} 265 case Reply{ask, answer} <> t: 266 look.step(String.eq(key(ask), want), answer, _u => look(t, want)) 267 268 # the answer to a question, or none yet 269 def heard(rs: List<&2, Reply>, ask: Ask) -> Heard: 270 look(rs, key(ask)) 271 272 # what git printed, once it is known: the text, or why there is none, or the 273 # question still open 274 type Said is Data: 275 Said{text: String} 276 Unsaid{why: String} 277 Unasked{ask: Ask} 278 279 def printed.ok(ok: Bool, +what: String, text: String) -> Said: 280 match ok: 281 case True{}: 282 Said{text} 283 case False{}: 284 Unsaid{"ez: git " ++ what ++ ": " ++ String.trim(text)} 285 286 def printed.of(answer: Answer, +what: String) -> Said: 287 match answer: 288 case Printed{ok, text}: 289 printed.ok(ok, what, text) 290 case Miss{why}: 291 Unsaid{why} 292 case _: 293 Unsaid{"ez: git " ++ what ++ " was not answered with what git printed"} 294 295 # what git printed for `what` 296 def printed.heard(got: Heard, ask: Ask, +what: String) -> Said: 297 match got: 298 case Open{}: 299 Unasked{ask} 300 case Heard{answer}: 301 printed.of(answer, what) 302 303 # the same, looked up among the answers 304 def printed(rs: List<&2, Reply>, +ask: Ask, +what: String) -> Said: 305 printed.heard(heard(rs, ask), ask, what) 306 307 # a file's text, "" when it is missing 308 def text.of(answer: Answer) -> Said: 309 match answer: 310 case Text{text}: 311 Said{text} 312 case Miss{why}: 313 Unsaid{why} 314 case _: 315 Unsaid{"ez: a file was not answered with its text"} 316 317 def text.said.heard(got: Heard, ask: Ask) -> Said: 318 match got: 319 case Open{}: 320 Unasked{ask} 321 case Heard{answer}: 322 text.of(answer) 323 324 # the same, looked up among the answers 325 def text.said(rs: List<&2, Reply>, +ask: Ask) -> Said: 326 text.said.heard(heard(rs, ask), ask) 327 328 # a checkout, once it is known 329 type Co is Data: 330 Co{nar: String, root: String, manifest: String, srcs: List<&2, String>} 331 CoWait{ask: Ask} 332 CoNo{why: String} 333 334 def checkout.of(answer: Answer) -> Co: 335 match answer: 336 case Checkout{nar, root, manifest, srcs}: 337 Co{nar, root, manifest, srcs} 338 case Miss{why}: 339 CoNo{why} 340 case _: 341 CoNo{"ez: a checkout was not answered with one"} 342 343 # the checkout a question was answered with 344 def checkout.heard(got: Heard, ask: Ask) -> Co: 345 match got: 346 case Open{}: 347 CoWait{ask} 348 case Heard{answer}: 349 checkout.of(answer) 350 351 # the same, looked up among the answers 352 def checkout(rs: List<&2, Reply>, +ask: Ask) -> Co: 353 checkout.heard(heard(rs, ask), ask) 354 355 # ancestry, once it is known 356 type Asc is Data: 357 Asc{yes: Bool} 358 AscWait{ask: Ask} 359 AscNo{why: String} 360 361 def onward.of(answer: Answer) -> Asc: 362 match answer: 363 case Onward{yes}: 364 Asc{yes} 365 case Miss{why}: 366 AscNo{why} 367 case _: 368 AscNo{"ez: ancestry was not answered with whether it holds"} 369 370 # the ancestry a question was answered with 371 def onward.heard(got: Heard, ask: Ask) -> Asc: 372 match got: 373 case Open{}: 374 AscWait{ask} 375 case Heard{answer}: 376 onward.of(answer) 377 378 # the same, looked up among the answers 379 def onward(rs: List<&2, Reply>, +ask: Ask) -> Asc: 380 onward.heard(heard(rs, ask), ask) 381 382 # a NAR hash, once it is known 383 type Wd is Data: 384 Wd{nar: String} 385 WdWait{ask: Ask} 386 WdNo{why: String} 387 388 def weighed.of(answer: Answer) -> Wd: 389 match answer: 390 case Weighed{nar}: 391 Wd{nar} 392 case Miss{why}: 393 WdNo{why} 394 case _: 395 WdNo{"ez: a weighing was not answered with a NAR hash"} 396 397 # the NAR hash a question was answered with 398 def weighed.heard(got: Heard, ask: Ask) -> Wd: 399 match got: 400 case Open{}: 401 WdWait{ask} 402 case Heard{answer}: 403 weighed.of(answer) 404 405 # the same, looked up among the answers 406 def weighed(rs: List<&2, Reply>, +ask: Ask) -> Wd: 407 weighed.heard(heard(rs, ask), ask) 408 409 # the hub's word on a hash, once it is known. It stops the upgrade only when a 410 # manifest arrived under the hash and did not hash to it. 411 type Hubbed is Data: 412 Clear{} 413 Clash{} 414 HubWait{ask: Ask} 415 HubNo{why: String} 416 417 def hub.got(got: Web.Got) -> Hubbed: 418 match got: 419 case Web.Have{_body}: 420 Clear{} 421 case Web.Miss{why}: 422 Bool.pick(Hubbed, U.hub.bad(why), Clash{}, Clear{}) 423 424 def hub.of(answer: Answer) -> Hubbed: 425 match answer: 426 case HubSaid{got}: 427 hub.got(got) 428 case Miss{why}: 429 HubNo{why} 430 case _: 431 HubNo{"ez: the hub was not answered with what it served"} 432 433 # the hub's word a question was answered with 434 def hubbed.heard(got: Heard, ask: Ask) -> Hubbed: 435 match got: 436 case Open{}: 437 HubWait{ask} 438 case Heard{answer}: 439 hub.of(answer) 440 441 # the same, looked up among the answers 442 def hubbed(rs: List<&2, Reply>, +ask: Ask) -> Hubbed: 443 hubbed.heard(heard(rs, ask), ask) 444 445 # the project's sources, once they are known 446 type Srcs is Data: 447 Srcs{files: List<&2, Found>} 448 SrcsWait{ask: Ask} 449 SrcsNo{why: String} 450 451 def sources.of(answer: Answer) -> Srcs: 452 match answer: 453 case Files{fs}: 454 Srcs{fs} 455 case Miss{why}: 456 SrcsNo{why} 457 case _: 458 SrcsNo{"ez: the sources were not answered with their texts"} 459 460 # the sources a question was answered with 461 def sources.heard(got: Heard, ask: Ask) -> Srcs: 462 match got: 463 case Open{}: 464 SrcsWait{ask} 465 case Heard{answer}: 466 sources.of(answer) 467 468 # the same, looked up among the answers 469 def sources(rs: List<&2, Reply>, +ask: Ask) -> Srcs: 470 sources.heard(heard(rs, ask), ask) 471 472 # --------------------------------------------------------------------------- 473 # what a ref names 474 475 # the commit a pin is asked to move to, the tag it came through ("" for 476 # none), and the default branch it tips ("" for a tag); or the question still 477 # open, or why there is no answer 478 type Tip is Data: 479 At{rev: String, tag: String, branch: String} 480 TipWait{ask: Ask} 481 TipNo{why: String} 482 483 # the commit the rows name for a ref. A ref the remote does not have is a 484 # mistake worth naming. 485 def follow.rev(+rev: String, +url: String, +ref: String) -> Tip: 486 Bool.pick(Tip, String.is_empty(rev), TipNo{"ez: " ++ url ++ " has no ref named " ++ ref}, 487 At{rev, ref, ""}) 488 489 def follow.said(said: Said, +url: String, +ref: String) -> Tip: 490 match said: 491 case Said{text}: 492 follow.rev(Git.exact(Git.rows(String.lines(text)), ref), url, ref) 493 case Unsaid{why}: 494 TipNo{why} 495 case Unasked{ask}: 496 TipWait{ask} 497 498 def follow.ask(pinned: Bool, +rs: List<&2, Reply>, +url: String, +ref: String) -> Tip: 499 match pinned: 500 case True{}: 501 At{ref, "", ""} 502 case False{}: 503 follow.said(printed(rs, Refs{url, ref}, "ls-remote " ++ url), url, ref) 504 505 # the commit a ref names, as `Git.resolve` reads it: a 40-hex ref is a commit 506 # already, and asks the remote nothing 507 def follow(+rs: List<&2, Reply>, +url: String, +ref: String) -> Tip: 508 follow.ask(Git.is_rev(ref), rs, url, ref) 509 510 # a remote with no HEAD is not one an upgrade can move 511 def forward.rev(+rev: String, branch: String, +url: String) -> Tip: 512 Bool.pick(Tip, String.is_empty(rev), TipNo{"ez: " ++ url ++ " has no HEAD"}, 513 At{rev, "", branch}) 514 515 def forward.said(said: Said, +url: String) -> Tip: 516 match said: 517 case Said{text}: 518 +rows = Git.rows(String.lines(text)) 519 forward.rev(Git.tip.rev(rows), Git.tip.branch(rows), url) 520 case Unsaid{why}: 521 TipNo{why} 522 case Unasked{ask}: 523 TipWait{ask} 524 525 # the commit the default branch tips, and its name 526 def forward(+rs: List<&2, Reply>, +url: String) -> Tip: 527 forward.said(printed(rs, Head{url}, "ls-remote " ++ url), url) 528 529 # the word Off quotes: a branch name when there is one, otherwise the commit 530 def tip.of(+branch: String, +tip: String) -> String: 531 Bool.pick(String, String.is_empty(branch), tip, branch) 532 533 # a tag that moved off the pin's history, or a branch tip that does not 534 # descend from a rev-only pin 535 def off.why(+name: String, +tag: String, +pin: String, +tip: String) -> String: 536 Bool.pick(String, String.is_empty(tag), 537 "ez: " ++ name ++ ": " ++ pin ++ " is not an ancestor of " ++ tip, 538 "ez: " ++ name ++ ": tag " ++ tag ++ " moved off " ++ pin) 539 540 # the branch a rev-only pin followed, said when there is one 541 def branch.of(+label: String) -> String: 542 Bool.pick(String, String.is_empty(label), "", " (" ++ label ++ ")") 543 544 # a line saying a pin moved 545 def say.move(+name: String, +old: String, +new: String, +branch: String) -> String: 546 name ++ ": " ++ old ++ " -> " ++ new ++ branch.of(branch) 547 548 # the `0x` name a manifest is the digest of 549 def hash.of(+manifest: String) -> String: 550 "0x" ++ String.take(Sha.hex(manifest), 32n) 551 552 # the bare checkout's hash when it is the pin's, and the walk's otherwise 553 def hash.kept.or(same: Bool, +bare: String, +full: String) -> String: 554 match same: 555 case True{}: 556 bare 557 case False{}: 558 full 559 560 def hash.kept.pick(same: Bool, +pin: String, +full: String, +manifest: String) -> String: 561 match same: 562 case True{}: 563 full 564 case False{}: 565 +bare = hash.of(K.manifest.lines(K.bare(L.manifest.files(String.lines(manifest))))) 566 hash.kept.or(String.eq(bare, pin), bare, full) 567 568 # the hash a pinned commit's checkout has, by the rule the pin was taken by: 569 # the walk's, or, for a pin written before bend 2.0.27 took the LICENSE files 570 # along, the same checkout's without them. A pin that is neither is a drift, 571 # and names the walk's hash. 572 def hash.kept(+pin: String, +manifest: String) -> String: 573 +full = hash.of(manifest) 574 hash.kept.pick(String.eq(full, pin), pin, full, manifest) 575 576 # --------------------------------------------------------------------------- 577 # one dependency 578 579 # what one dependency became: its entry in the ledger the upgrade leaves, the 580 # committed tree to remove ("" for none), its new hash, the hash an import 581 # must stop naming ("" for none), whether it moved, what a person is told, and 582 # the tree checked out at its new rev. Or the questions still open, or why the 583 # upgrade refuses. 584 type Fate is Data: 585 Became{dep: M.Dep, old: String, new: String, prev: String, moved: Bool, 586 says: List<&2, String>, trees: List<&2, Laid>} 587 Waits{asks: List<&2, Ask>} 588 Halts{why: String} 589 590 # a dependency left as it was 591 def stay(dep: M.Dep, says: List<&2, String>) -> Fate: 592 Became{dep, "", "", "", False{}, says, []} 593 594 # the old hash, or "" when it did not change 595 def swap.hash(same: Bool, +old: String) -> String: 596 match same: 597 case True{}: 598 "" 599 case False{}: 600 old 601 602 # a committed tree whose hash changed is a tree to remove 603 def swap.of(vend: Bool, +old: String, +new: String) -> String: 604 match vend: 605 case False{}: 606 "" 607 case True{}: 608 swap.hash(String.eq(old, new), old) 609 610 # a line, only when there is something to say 611 def line.if(say: Bool, +text: String) -> List<&2, String>: 612 Bool.pick(List<&2, String>, say, [text], []) 613 614 # a moved pin, once the hub has not objected to its new hash: the ledger's 615 # entry retargeted, the committed tree it replaces, the hash imports stop 616 # naming, and what a person is told 617 def dep.moved( 618 +dep: M.Dep, 619 +hash: String, 620 +rev: String, 621 +root: String, 622 +nar: String, 623 manifest: String, 624 srcs: List<&2, String>, 625 +branch: String 626 ) -> Fate: 627 +src = M.source.dep(dep) 628 +name = M.dep.name(dep) 629 +old = M.dep.hash(dep) 630 +drop = swap.of(M.source.vendor(src), old, hash) 631 +prev = swap.hash(String.eq(old, hash), old) 632 Became{U.retarget(dep, rev, hash, root, nar), drop, hash, prev, True{}, 633 say.move(name, M.source.rev(src), rev, branch) <> 634 List.append(&2, String, 635 line.if(Bool.not(String.is_empty(prev)), name ++ ": imports " ++ old ++ " -> " ++ hash), 636 line.if(Bool.not(String.is_empty(drop)), name ++ ": vendored .ez/lib/" ++ hash)), 637 [Laid{hash, nar, manifest, srcs}]} 638 639 # the hub's word on the new hash 640 def dep.hub( 641 hubbed: Hubbed, 642 +dep: M.Dep, 643 +hash: String, 644 +rev: String, 645 +root: String, 646 +nar: String, 647 manifest: String, 648 srcs: List<&2, String>, 649 +branch: String 650 ) -> Fate: 651 match hubbed: 652 case Clear{}: 653 dep.moved(dep, hash, rev, root, nar, manifest, srcs, branch) 654 case Clash{}: 655 Halts{"ez: " ++ M.dep.name(dep) ++ ": hub digest does not match " ++ hash} 656 case HubWait{ask}: 657 Waits{[ask]} 658 case HubNo{why}: 659 Halts{why} 660 661 # the checkout at the rev the pin advances to, then the hub asked about the 662 # hash it has. The hub a moved hash is checked against is the ledger's. 663 def dep.advance(co: Co, +rs: List<&2, Reply>, +hub: String, +dep: M.Dep, +rev: String, +branch: String) -> Fate: 664 match co: 665 case Co{nar, root, +manifest, srcs}: 666 +hash = hash.of(manifest) 667 dep.hub(hubbed(rs, HubHas{hub, hash}), dep, hash, rev, root, nar, manifest, srcs, 668 branch) 669 case CoWait{ask}: 670 Waits{[ask]} 671 case CoNo{why}: 672 Halts{why} 673 674 # the verdict. `seen` is the hash the pinned commit's checkout has, which a 675 # drift names; "" when the commit moved and the pin was not checked out. 676 def dep.verdict( 677 verdict: U.Verdict, 678 +rs: List<&2, Reply>, 679 +hub: String, 680 +dep: M.Dep, 681 +tip: String, 682 +branch: String, 683 +seen: String 684 ) -> Fate: 685 match verdict: 686 case U.Keep{}: 687 stay(dep, [M.dep.name(dep) ++ ": unchanged " ++ M.source.rev(M.source.dep(dep))]) 688 case U.Drift{}: 689 +src = M.source.dep(dep) 690 Halts{Say.drift(M.dep.name(dep), M.source.tag(src), M.source.rev(src), 691 M.dep.hash(dep), seen)} 692 case U.Off{}: 693 +src = M.source.dep(dep) 694 Halts{off.why(M.dep.name(dep), M.source.tag(src), M.source.rev(src), 695 tip.of(branch, tip))} 696 case U.Advance{+rev, _tag}: 697 dep.advance(checkout(rs, Tree{M.source.url(M.source.dep(dep)), rev, M.dep.entry(dep)}), rs, hub, dep, rev, branch) 698 699 # the pinned commit's checkout, judged with whether it agrees with the pin: 700 # the same hash, NAR hash and root. One that does not is a drift. 701 def dep.kept(co: Co, +rs: List<&2, Reply>, +hub: String, +dep: M.Dep, +tip: String, +tag: String) -> Fate: 702 match co: 703 case Co{nar, root, +manifest, _srcs}: 704 +src = M.source.dep(dep) 705 +hash = hash.kept(M.dep.hash(dep), manifest) 706 dep.verdict(U.judge(True{}, True{}, 707 U.agree(M.dep.hash(dep), hash, M.source.nar(src), nar, M.source.root(src), root), 708 tip, tag), rs, hub, dep, tip, "", hash) 709 case CoWait{ask}: 710 Waits{[ask]} 711 case CoNo{why}: 712 Halts{why} 713 714 # a different commit, once ancestry is known. The commit moved, so no 715 # checkout at the pin was weighed and none agrees with it. 716 def dep.onward( 717 asc: Asc, 718 +rs: List<&2, Reply>, 719 +hub: String, 720 +dep: M.Dep, 721 +tip: String, 722 +tag: String, 723 +branch: String 724 ) -> Fate: 725 match asc: 726 case Asc{yes}: 727 dep.verdict(U.judge(False{}, yes, False{}, tip, tag), rs, hub, dep, tip, 728 branch, "") 729 case AscWait{ask}: 730 Waits{[ask]} 731 case AscNo{why}: 732 Halts{why} 733 734 # the same commit is checked out to be weighed; a different one is asked 735 # whether it descends from the pin 736 def dep.same( 737 same: Bool, 738 +rs: List<&2, Reply>, 739 +hub: String, 740 +dep: M.Dep, 741 +tip: String, 742 +tag: String, 743 +branch: String 744 ) -> Fate: 745 match same: 746 case True{}: 747 +src = M.source.dep(dep) 748 dep.kept(checkout(rs, Tree{M.source.url(src), M.source.rev(src), M.dep.entry(dep)}), rs, hub, dep, tip, tag) 749 case False{}: 750 +src = M.source.dep(dep) 751 dep.onward(onward(rs, Above{M.source.url(src), M.source.rev(src), tip}), rs, hub, dep, tip, tag, branch) 752 753 # the commit the remote names, against the pin 754 def dep.tip(tip: Tip, +rs: List<&2, Reply>, +hub: String, +dep: M.Dep) -> Fate: 755 match tip: 756 case At{+rev, tag, branch}: 757 dep.same(String.eq(M.source.rev(M.source.dep(dep)), rev), rs, hub, dep, rev, 758 tag, branch) 759 case TipWait{ask}: 760 Waits{[ask]} 761 case TipNo{why}: 762 Halts{why} 763 764 # a hub pin named with `--package` is reported and left. An unselected 765 # dependency is left quietly. 766 def dep.hold(sel: Bool, +dep: M.Dep) -> Fate: 767 stay(dep, line.if(sel, M.dep.name(dep) ++ ": pinned by its hash")) 768 769 # one dependency, by what it is asked 770 def dep.one(aim: U.Aim, +rs: List<&2, Reply>, +only: String, +hub: String, +dep: M.Dep) -> Fate: 771 match aim: 772 case U.Hold{}: 773 dep.hold(U.chosen(only, M.dep.name(dep)), dep) 774 case U.Follow{ref}: 775 dep.tip(follow(rs, M.source.url(M.source.dep(dep)), ref), rs, hub, dep) 776 case U.Forward{}: 777 dep.tip(forward(rs, M.source.url(M.source.dep(dep))), rs, hub, dep) 778 779 # every dependency after the upgrade: the ledger's entries in its order, the 780 # committed trees that moved (`swaps`), the hashes import lines stop naming 781 # (`edits`), whether any pin moved, what a person is told, the trees checked 782 # out, the questions still open, and the first refusal, "" for none 783 type Deps is Data: 784 Deps{deps: List<&2, M.Dep>, swaps: List<&2, U.Swap>, edits: List<&2, U.Swap>, 785 moved: Bool, says: List<&2, String>, trees: List<&2, Laid>, 786 asks: List<&2, Ask>, bad: String} 787 788 # a moved hash, kept only when there is one 789 def swaps.put(empty: Bool, +old: String, +new: String, rest: List<&2, U.Swap>) -> List<&2, U.Swap>: 790 match empty: 791 case True{}: 792 rest 793 case False{}: 794 U.Swap{old, new} <> rest 795 796 # one dependency in front of the rest. A refusal ahead of the rest wins, so 797 # the first in ledger order is the one given. 798 def deps.join(fate: Fate, rest: Deps) -> Deps: 799 match fate: 800 case Became{d, +old, +new, +prev, m, says, trees}: 801 Deps{ds, ss, es, any, sy, tr, asks, bad} = rest 802 Deps{d <> ds, swaps.put(String.is_empty(old), old, new, ss), 803 swaps.put(String.is_empty(prev), prev, new, es), Bool.or(m, any), 804 List.append(&2, String, says, sy), List.append(&2, Laid, trees, tr), asks, bad} 805 case Waits{more}: 806 Deps{ds, ss, es, any, sy, tr, asks, bad} = rest 807 Deps{ds, ss, es, any, sy, tr, List.append(&2, Ask, more, asks), bad} 808 case Halts{why}: 809 Deps{ds, ss, es, any, sy, tr, asks, _bad} = rest 810 Deps{ds, ss, es, any, sy, tr, asks, halt.why(why)} 811 812 # every dependency, in the order the ledger wrote them 813 def deps.walk(+rs: List<&2, Reply>, +only: String, +hub: String, ds: List<&2, M.Dep>) -> Deps: 814 match ds: 815 case []: 816 Deps{[], [], [], False{}, [], [], [], ""} 817 case +h <> t: 818 deps.join(dep.one(U.aim(U.chosen(only, M.dep.name(h)), M.source.dep(h)), rs, 819 only, hub, h), deps.walk(rs, only, hub, t)) 820 821 # --------------------------------------------------------------------------- 822 # one tool 823 824 # one tool after it was asked about, or the questions still open, or why the 825 # upgrade refuses 826 type Pinned is Data: 827 Pinned{tool: M.Tool, moved: Bool, says: List<&2, String>} 828 PinWait{asks: List<&2, Ask>} 829 PinHalt{why: String} 830 831 # the pin with a commit and a NAR hash written in. An empty tag leaves the one 832 # the ledger already had. The vendor bit is off: a tool is not committed under 833 # `.ez/lib`. 834 def set.go( 835 +name: String, 836 +entry: String, 837 +bin: String, 838 source: M.Source, 839 +rev: String, 840 +tag: String, 841 +nar: String 842 ) -> M.Tool: 843 match source: 844 case M.Hub{named}: 845 M.Tool{name, entry, bin, M.Hub{named}} 846 case M.Git{url, _rev, old, root, _nar, _vend}: 847 M.Tool{name, entry, bin, M.Git{url, rev, 848 Bool.pick(String, String.is_empty(tag), old, tag), M.root.or(root), 849 nar, False{}}} 850 851 # a tool's pin with a commit, a tag and a NAR hash written in 852 def set(tool: M.Tool, +rev: String, +tag: String, +nar: String) -> M.Tool: 853 M.Tool{n, e, b, s} = tool 854 set.go(n, e, b, s, rev, tag, nar) 855 856 # a commit and its NAR hash written into the pin. A pin with either one empty 857 # is short, and an upgrade that wrote one would fill it again on the next run, 858 # so a weighing that names no commit or no NAR hash pins nothing. 859 def tool.pin(short: Bool, +tool: M.Tool, +rev: String, +tag: String, +nar: String, says: List<&2, String>) -> Pinned: 860 match short: 861 case True{}: 862 PinHalt{"ez: tools." ++ M.tool.name(tool) ++ ": weighing " ++ rev ++ " gave no narHash"} 863 case False{}: 864 Pinned{set(tool, rev, tag, nar), True{}, says} 865 866 # a commit weighed, then written into the pin 867 def tool.weighed(wd: Wd, +tool: M.Tool, +rev: String, +tag: String, says: List<&2, String>) -> Pinned: 868 match wd: 869 case Wd{+nar}: 870 tool.pin(Bool.or(String.is_empty(rev), String.is_empty(nar)), tool, rev, tag, nar, says) 871 case WdWait{ask}: 872 PinWait{[ask]} 873 case WdNo{why}: 874 PinHalt{why} 875 876 # the commit a ref named, weighed. The pin records the ref as its tag, even 877 # one that names a commit, so the next upgrade asks about the same ref. 878 def tool.resolved(tip: Tip, +rs: List<&2, Reply>, +tool: M.Tool, +ref: String) -> Pinned: 879 match tip: 880 case At{+rev, _tag, _branch}: 881 tool.weighed(weighed(rs, Weigh{M.source.url(M.tool.source(tool)), rev}), tool, rev, ref, []) 882 case TipWait{ask}: 883 PinWait{[ask]} 884 case TipNo{why}: 885 PinHalt{why} 886 887 # the ref a pin with no commit is pinned from, once it is known 888 type Named is Data: 889 Named{ref: String} 890 NamedWait{ask: Ask} 891 NamedNo{why: String} 892 893 # a remote whose HEAD names no branch leaves nothing to pin 894 def heads.branch(+name: String, +url: String) -> Named: 895 Bool.pick(Named, String.is_empty(name), 896 NamedNo{"ez: " ++ url ++ 897 " has no semver-ish tag and its HEAD names no branch; name a ref to pin"}, 898 Named{name}) 899 900 def heads.said(said: Said, +url: String) -> Named: 901 match said: 902 case Said{text}: 903 heads.branch(Git.tip.branch(Git.rows(String.lines(text))), url) 904 case Unsaid{why}: 905 NamedNo{why} 906 case Unasked{ask}: 907 NamedWait{ask} 908 909 # the greatest semver-ish tag, else the default branch 910 def tags.best(none: Bool, +best: String, +rs: List<&2, Reply>, +url: String) -> Named: 911 match none: 912 case False{}: 913 Named{best} 914 case True{}: 915 heads.said(printed(rs, Head{url}, "ls-remote " ++ url), url) 916 917 def tags.said(said: Said, +rs: List<&2, Reply>, +url: String) -> Named: 918 match said: 919 case Said{text}: 920 +best = Git.choose(Git.tag.names(Git.rows(String.lines(text)))) 921 tags.best(String.is_empty(best), best, rs, url) 922 case Unsaid{why}: 923 NamedNo{why} 924 case Unasked{ask}: 925 NamedWait{ask} 926 927 # the ref `ez add` pins when none was asked for: the greatest semver-ish 928 # release tag, else the greatest pre-release, else the default branch 929 def tool.default(+rs: List<&2, Reply>, +url: String) -> Named: 930 tags.said(printed(rs, Tags{url}, "ls-remote " ++ url), rs, url) 931 932 # the ref to resolve: the tag the ledger named, or the default ref 933 def tool.ref(no_tag: Bool, +rs: List<&2, Reply>, +url: String, +tag: String) -> Named: 934 match no_tag: 935 case False{}: 936 Named{tag} 937 case True{}: 938 tool.default(rs, url) 939 940 def tool.named(named: Named, +rs: List<&2, Reply>, +tool: M.Tool, +url: String) -> Pinned: 941 match named: 942 case Named{+ref}: 943 tool.resolved(follow(rs, url, ref), rs, tool, ref) 944 case NamedWait{ask}: 945 PinWait{[ask]} 946 case NamedNo{why}: 947 PinHalt{why} 948 949 # the verdict on a full pin. A pin that moves keeps its own tag, as a 950 # dependency does: the tip was named by that tag, or by the default branch 951 # for a pin with none. 952 def tool.verdict(verdict: U.Verdict, +rs: List<&2, Reply>, +tool: M.Tool, +tip: String, +branch: String) -> Pinned: 953 match verdict: 954 case U.Keep{}: 955 Pinned{tool, False{}, [M.tool.name(tool) ++ ": unchanged " ++ 956 M.source.rev(M.tool.source(tool))]} 957 case U.Drift{}: 958 PinHalt{"ez: " ++ M.tool.name(tool) ++ ": narHash drifted at " ++ 959 M.source.rev(M.tool.source(tool))} 960 case U.Off{}: 961 +src = M.tool.source(tool) 962 PinHalt{off.why(M.tool.name(tool), M.source.tag(src), M.source.rev(src), 963 tip.of(branch, tip))} 964 case U.Advance{+rev, _tag}: 965 +src = M.tool.source(tool) 966 tool.weighed(weighed(rs, Weigh{M.source.url(src), rev}), tool, rev, "", 967 [say.move(M.tool.name(tool), M.source.rev(src), rev, branch)]) 968 969 # the pinned commit weighed: its NAR hash written in when the pin has none, 970 # else judged with whether it is the one the pin records 971 def tool.kept.nar(fill: Bool, +rs: List<&2, Reply>, +tool: M.Tool, +tip: String, +tag: String, +nar: String) -> Pinned: 972 match fill: 973 case True{}: 974 tool.pin(Bool.or(String.is_empty(tip), String.is_empty(nar)), tool, tip, "", nar, []) 975 case False{}: 976 tool.verdict(U.judge(True{}, True{}, 977 String.eq(M.source.nar(M.tool.source(tool)), nar), tip, tag), rs, tool, tip, "") 978 979 # the pinned commit weighed again 980 def tool.kept(wd: Wd, +rs: List<&2, Reply>, +tool: M.Tool, +tip: String, +tag: String) -> Pinned: 981 match wd: 982 case Wd{+nar}: 983 tool.kept.nar(String.is_empty(M.source.nar(M.tool.source(tool))), rs, tool, tip, tag, nar) 984 case WdWait{ask}: 985 PinWait{[ask]} 986 case WdNo{why}: 987 PinHalt{why} 988 989 def tool.onward(asc: Asc, +rs: List<&2, Reply>, +tool: M.Tool, +tip: String, +tag: String, +branch: String) -> Pinned: 990 match asc: 991 case Asc{yes}: 992 tool.verdict(U.judge(False{}, yes, False{}, tip, tag), rs, tool, tip, branch) 993 case AscWait{ask}: 994 PinWait{[ask]} 995 case AscNo{why}: 996 PinHalt{why} 997 998 def tool.same(same: Bool, +rs: List<&2, Reply>, +tool: M.Tool, +tip: String, +tag: String, +branch: String) -> Pinned: 999 match same: 1000 case True{}: 1001 +src = M.tool.source(tool) 1002 tool.kept(weighed(rs, Weigh{M.source.url(src), M.source.rev(src)}), rs, tool, tip, tag) 1003 case False{}: 1004 +src = M.tool.source(tool) 1005 tool.onward(onward(rs, Above{M.source.url(src), M.source.rev(src), tip}), rs, tool, tip, tag, branch) 1006 1007 def tool.tip(tip: Tip, +rs: List<&2, Reply>, +tool: M.Tool) -> Pinned: 1008 match tip: 1009 case At{+rev, tag, branch}: 1010 tool.same(String.eq(M.source.rev(M.tool.source(tool)), rev), rs, tool, rev, 1011 tag, branch) 1012 case TipWait{ask}: 1013 PinWait{[ask]} 1014 case TipNo{why}: 1015 PinHalt{why} 1016 1017 # a full pin, re-asked: its tag re-resolved, or the default branch followed 1018 def tool.aim(aim: U.Aim, +rs: List<&2, Reply>, +tool: M.Tool) -> Pinned: 1019 match aim: 1020 case U.Hold{}: 1021 Pinned{tool, False{}, []} 1022 case U.Follow{ref}: 1023 tool.tip(follow(rs, M.source.url(M.tool.source(tool)), ref), rs, tool) 1024 case U.Forward{}: 1025 tool.tip(forward(rs, M.source.url(M.tool.source(tool))), rs, tool) 1026 1027 # a pin with no commit is resolved from its tag, or the default ref, then 1028 # weighed. One with a commit and no NAR hash is upgraded as a full pin is, 1029 # and its NAR hash is written in wherever it lands: filled at its own commit 1030 # only, it would move on the next upgrade, which follows its tag or the 1031 # default branch. 1032 def tool.fill(no_rev: Bool, +rs: List<&2, Reply>, +tool: M.Tool) -> Pinned: 1033 match no_rev: 1034 case True{}: 1035 +src = M.tool.source(tool) 1036 +url = M.source.url(src) 1037 tool.named(tool.ref(String.is_empty(M.source.tag(src)), rs, url, 1038 M.source.tag(src)), rs, tool, url) 1039 case False{}: 1040 tool.aim(U.aim(True{}, M.tool.source(tool)), rs, tool) 1041 1042 # a selected pin with no commit or no NAR hash is filled; a full one is asked 1043 # whether it moved 1044 def tool.short(short: Bool, +rs: List<&2, Reply>, +tool: M.Tool) -> Pinned: 1045 match short: 1046 case True{}: 1047 tool.fill(String.is_empty(M.source.rev(M.tool.source(tool))), rs, tool) 1048 case False{}: 1049 tool.aim(U.aim(True{}, M.tool.source(tool)), rs, tool) 1050 1051 # a selected tool names a repo, or it cannot be pinned 1052 def tool.git(git: Bool, +rs: List<&2, Reply>, +tool: M.Tool) -> Pinned: 1053 match git: 1054 case False{}: 1055 PinHalt{"ez: tools." ++ M.tool.name(tool) ++ " has no git"} 1056 case True{}: 1057 +src = M.tool.source(tool) 1058 tool.short(Bool.or(String.is_empty(M.source.rev(src)), 1059 String.is_empty(M.source.nar(src))), rs, tool) 1060 1061 # a selected tool is asked; the others are left 1062 def tool.sel(sel: Bool, +rs: List<&2, Reply>, +tool: M.Tool) -> Pinned: 1063 match sel: 1064 case False{}: 1065 Pinned{tool, False{}, []} 1066 case True{}: 1067 tool.git(M.source.is_git(M.tool.source(tool)), rs, tool) 1068 1069 # every tool after the upgrade, as `Deps` is for dependencies 1070 type Tools is Data: 1071 Tools{tools: List<&2, M.Tool>, moved: Bool, says: List<&2, String>, 1072 asks: List<&2, Ask>, bad: String} 1073 1074 def tools.join(pinned: Pinned, rest: Tools) -> Tools: 1075 match pinned: 1076 case Pinned{t, m, says}: 1077 Tools{ts, any, sy, asks, bad} = rest 1078 Tools{t <> ts, Bool.or(m, any), List.append(&2, String, says, sy), asks, bad} 1079 case PinWait{more}: 1080 Tools{ts, any, sy, asks, bad} = rest 1081 Tools{ts, any, sy, List.append(&2, Ask, more, asks), bad} 1082 case PinHalt{why}: 1083 Tools{ts, any, sy, asks, _bad} = rest 1084 Tools{ts, any, sy, asks, halt.why(why)} 1085 1086 # every tool, in the order the ledger wrote them 1087 def tools.walk(+rs: List<&2, Reply>, +only: String, ts: List<&2, M.Tool>) -> Tools: 1088 match ts: 1089 case []: 1090 Tools{[], False{}, [], [], ""} 1091 case +h <> t: 1092 tools.join(tool.sel(U.chosen(only, M.tool.name(h)), rs, h), 1093 tools.walk(rs, only, t)) 1094 1095 # --------------------------------------------------------------------------- 1096 # what the upgrade writes around the lock 1097 1098 # ez.toml, only when a pin moved, so an upgrade that changed nothing does not 1099 # reformat the file 1100 def edit.toml(moved: Bool, +text: String, rest: List<&2, Edit>) -> List<&2, Edit>: 1101 match moved: 1102 case True{}: 1103 Edit{"ez.toml", text} <> rest 1104 case False{}: 1105 rest 1106 1107 # a file, only when its text changes 1108 def edit.if(same: Bool, +at: String, +text: String, rest: List<&2, Edit>) -> List<&2, Edit>: 1109 match same: 1110 case True{}: 1111 rest 1112 case False{}: 1113 Edit{at, text} <> rest 1114 1115 # every source with each moved hash rewritten in its imports, written only 1116 # when that changes it 1117 def edit.sources(fs: List<&2, Found>, +es: List<&2, U.Swap>) -> List<&2, Edit>: 1118 match fs: 1119 case []: 1120 [] 1121 case Found{+at, +text} <> t: 1122 +next = U.reimport.many(es, text) 1123 edit.if(String.eq(next, text), at, next, edit.sources(t, es)) 1124 1125 # every committed tree that moved, removed 1126 def drops.of(ss: List<&2, U.Swap>) -> List<&2, String>: 1127 match ss: 1128 case []: 1129 [] 1130 case U.Swap{old, _new} <> t: 1131 old <> drops.of(t) 1132 1133 # the ledger the lock is made from: the one the upgrade writes, read back, or 1134 # ez.toml as it was when nothing moved 1135 def read.next(moved: Bool, +text: String, old: M.Manifest) -> M.Read: 1136 match moved: 1137 case True{}: 1138 M.parse(text) 1139 case False{}: 1140 M.Good{old} 1141 1142 # the ledger model the upgrade renders, or ez.toml's when nothing moved 1143 def model.next(moved: Bool, next: M.Manifest, old: M.Manifest) -> M.Read: 1144 match moved: 1145 case True{}: 1146 M.Good{next} 1147 case False{}: 1148 M.Good{old} 1149 1150 # why an upgrade that moved a pin stops, "" when it does not: the ledger it 1151 # would write cannot be written so that it reads back as the model it renders 1152 # (EZ-LED-4), so it is refused, with no effect, rather than written as 1153 # something else 1154 def fit.why(moved: Bool, next: M.Manifest) -> String: 1155 match moved: 1156 case True{}: 1157 Bool.pick(String, Rend.renderable(next), "", Rend.unrenderable()) 1158 case False{}: 1159 "" 1160 1161 # every answer in: the ledger, the allowlist and the imports, and the trees 1162 def done( 1163 +old: M.Manifest, 1164 dd: Deps, 1165 tt: Tools, 1166 +ignore: String, 1167 fs: List<&2, Found> 1168 ) -> Next: 1169 M.Manifest{+name, +entry, +bin, +hub, +pubas, +pubver, _ds, _ts} = old 1170 Deps{+ds, swaps, +edits, dmoved, dsays, trees, _dasks, _dbad} = dd 1171 Tools{+ts, tmoved, tsays, _tasks, _tbad} = tt 1172 +moved = Bool.or(dmoved, tmoved) 1173 +text = Rend.show(M.Manifest{name, entry, bin, hub, pubas, pubver, ds, ts}) 1174 +allow = I.sync(ds, ignore) 1175 Next{model.next(moved, M.Manifest{name, entry, bin, hub, pubas, pubver, ds, ts}, old), 1176 read.next(moved, text, old), fit.why(moved, M.Manifest{name, entry, bin, hub, pubas, pubver, ds, ts}), [], 1177 trees, I.vended(ds), 1178 List.append(&2, String, dsays, tsays), 1179 edit.toml(moved, text, edit.if(String.eq(allow, ignore), ".gitignore", allow, [])), 1180 fs, drops.of(swaps), edits} 1181 1182 # the question still open, when one is 1183 def open.srcs(srcs: Srcs) -> List<&2, Ask>: 1184 match srcs: 1185 case SrcsWait{ask}: 1186 [ask] 1187 case _: 1188 [] 1189 1190 # the sources, once the ignore file is known 1191 def got.srcs(srcs: Srcs, +old: M.Manifest, dd: Deps, tt: Tools, +ignore: String) -> Next: 1192 match srcs: 1193 case Srcs{fs}: 1194 done(old, dd, tt, ignore, fs) 1195 case SrcsWait{ask}: 1196 stopped(M.Good{old}, "ez: the upgrade was not given the project's sources", [ask]) 1197 case SrcsNo{why}: 1198 stopped(M.Good{old}, why, []) 1199 1200 # the ignore file, then the sources 1201 def got.ignore(said: Said, +srcs: Srcs, +old: M.Manifest, dd: Deps, tt: Tools) -> Next: 1202 match said: 1203 case Said{ignore}: 1204 got.srcs(srcs, old, dd, tt, ignore) 1205 case Unasked{ask}: 1206 stopped(M.Good{old}, "ez: the upgrade was not given .gitignore", 1207 ask <> open.srcs(srcs)) 1208 case Unsaid{why}: 1209 stopped(M.Good{old}, why, []) 1210 1211 # the sources are read only when an import has a hash to stop naming 1212 def ask.srcs(none: Bool, +rs: List<&2, Reply>) -> Srcs: 1213 match none: 1214 case True{}: 1215 Srcs{[]} 1216 case False{}: 1217 sources(rs, Sources{}) 1218 1219 def deps.edits(dd: Deps) -> List<&2, U.Swap>: 1220 Deps{_ds, _ss, es, _m, _sy, _tr, _asks, _bad} = dd 1221 es 1222 1223 # every pin answered: what is written around the lock 1224 def settled(+rs: List<&2, Reply>, +old: M.Manifest, +dd: Deps, tt: Tools) -> Next: 1225 got.ignore(text.said(rs, Ignore{}), 1226 ask.srcs(List.is_empty(&2, U.Swap, deps.edits(dd)), rs), old, dd, tt) 1227 1228 # the pins' questions still open: the lock waits for them 1229 def pins.asks(asks: List<&2, Ask>, +rs: List<&2, Reply>, +old: M.Manifest, dd: Deps, tt: Tools) -> Next: 1230 match asks: 1231 case []: 1232 settled(rs, old, dd, tt) 1233 case h <> t: 1234 stopped(M.Good{old}, "ez: the upgrade still has questions for the remote", h <> t) 1235 1236 # a refusal wins over every question still open 1237 def pins.bad( 1238 clean: Bool, 1239 +bad: String, 1240 asks: List<&2, Ask>, 1241 +rs: List<&2, Reply>, 1242 +old: M.Manifest, 1243 dd: Deps, 1244 tt: Tools 1245 ) -> Next: 1246 match clean: 1247 case True{}: 1248 pins.asks(asks, rs, old, dd, tt) 1249 case False{}: 1250 stopped(M.Good{old}, bad, []) 1251 1252 # the dependencies' and the tools' answers together 1253 def pins(dd: Deps, tt: Tools, +rs: List<&2, Reply>, +old: M.Manifest) -> Next: 1254 Deps{ds, ss, es, dm, dsy, dtr, dasks, +dbad} = dd 1255 Tools{ts, tm, tsy, tasks, +tbad} = tt 1256 pins.bad(Bool.and(String.is_empty(dbad), String.is_empty(tbad)), 1257 Bool.pick(String, String.is_empty(dbad), tbad, dbad), 1258 List.append(&2, Ask, dasks, tasks), rs, old, 1259 Deps{ds, ss, es, dm, dsy, dtr, [], ""}, Tools{ts, tm, tsy, [], ""}) 1260 1261 # `--package` has to name a dependency or a pinned CLI 1262 def known(ok: Bool, +only: String, +rs: List<&2, Reply>, +old: M.Manifest) -> Next: 1263 match ok: 1264 case False{}: 1265 stopped(M.Good{old}, "ez: no dependency or tool named " ++ only, []) 1266 case True{}: 1267 M.Manifest{_name, _entry, _bin, +hub, +pubas, +pubver, +ds, +ts} = old 1268 pins(deps.walk(rs, only, M.hub.or(hub), ds), tools.walk(rs, only, ts), rs, old) 1269 1270 # `ez lock --upgrade [--package only]` over the ledger and the answers so far. 1271 # A ledger that does not parse is the plain lock's refusal. 1272 def next(+only: String, read: M.Read, +rs: List<&2, Reply>) -> Next: 1273 match read: 1274 case M.Bad{why}: 1275 plain(M.Bad{why}) 1276 case M.Good{+m}: 1277 M.Manifest{_name, _entry, _bin, _hub, _pubas, _pubver, +ds, +ts} = m 1278 known(Bool.or(U.known(only, ds), M.tool.known(only, ts)), only, rs, m)