src/lock/run.bend

ezx@1.11.0.0 · 754 lines · 74 definitions

1# lock/run: the interpreter for `ez lock`. It reads the ledger and the
2# committed sources into a World, then loops: it asks the planner
3# (lock/plan.bend) what is still open, answers each question by IO, and adds
4# the answers to the World, until nothing is left to ask. Then it runs the
5# planner's plan. It decides nothing: which packages to read, whether their
6# bytes are the ones the ledger names, where an upgrade moves a pin, and what
7# is written are the planner's. That it reads and executes faithfully is
8# EZ-TRUST-2. It answers a name's question by asking the hub once, and the
9# lock's by reading ez.lock.toml. Under `--upgrade` it also answers the
10# upgrade's questions (lock/up.bend): what git prints for refs, the default
11# branch and tags, ancestry, checkouts at a rev, the hub's manifest for a
12# moved hash, `.gitignore`, and the project's sources. A new checkout's package is walked
13# from its entry here, by `K.pkg_of`, which is interpreter code under
14# EZ-TRUST-2 until `ez add` is converted.
15import Base
16import snap@1.4.0.0/main.bend as R
17import ezx@1.11.0.0/src/io/file.bend as F
18import ezx@1.11.0.0/src/hub/hub.bend as Web
19import ezx@1.11.0.0/src/hub/get.bend as Net
20import ezx@1.11.0.0/src/git/git.bend as Git
21import ezx@1.11.0.0/src/git/exec.bend as GX
22import ezx@1.11.0.0/src/pkg/pkg.bend as K
23import ezx@1.11.0.0/src/pkg/path.bend as P
24import ezx@1.11.0.0/src/share/say.bend as Say
25import ezx@1.11.0.0/src/share/spin.bend as Spin
26import ezx@1.11.0.0/src/lock/lock.bend as L
27import ezx@1.11.0.0/src/lock/world.bend as W
28import ezx@1.11.0.0/src/lock/up.bend as Up
29import ezx@1.11.0.0/src/lock/plan.bend as Plan
30import ezx@1.11.0.0/src/ws/run.bend as WsRun
31import ezx@1.11.0.0/src/ws/up.bend as WsUp
32import ezx@1.11.0.0/src/ws/joint.bend as J
33
34# ---------------------------------------------------------------------------
35# the World's first fields
36
37# an environment variable's text; unset, ""
38def env.text(res: Result<&1, &1, U32 & String, String>) -> String:
39  match res:
40    case Fail{_e}:
41      ""
42    case Done{s}:
43      s
44
45# where packages live, which is with the project rather than in ~/.bend/lib
46def lib.or(+dir: String) -> String:
47  Bool.pick(String, String.is_empty(dir), ".ez/lib", dir)
48
49# the BEND_LIB this project pins
50def bend.lib() -> IO(String):
51  do IO<String>:
52    r : Result<&1, &1, U32 & String, String> <- IO.get_env("BEND_LIB")
53    return lib.or(String.trim(env.text(r)))
54
55# the non-empty lines
56def nonempty(ss: List<&2, String>) -> List<&2, String>:
57  match ss:
58    case []:
59      []
60    case +h <> t:
61      +more = nonempty(t)
62      Bool.pick(List<&2, String>, String.is_empty(h), more, h <> more)
63
64# the ledger's text, or None when there is no ez.toml
65def read.ledger() -> IO(Maybe<&2, String>):
66  F.read("ez.toml")
67
68# every listed path, read
69def read.all(ps: List<&2, String>) -> IO(List<&2, W.Source>):
70  match ps:
71    case []:
72      IO.pure(List<&2, W.Source>, [])
73    case +h <> t:
74      do IO<List<&2, W.Source>>:
75        m : Maybe<&2, String> <- F.read(h)
76        rest : List<&2, W.Source> <- read.all(t)
77        return W.Source{h, F.text_of(m)} <> rest
78
79# what git listed, or why it could not
80def listed(ok: Bool, +text: String) -> IO(W.Listing):
81  match ok:
82    case True{}:
83      do IO<W.Listing>:
84        fs : List<&2, W.Source> <- read.all(nonempty(String.lines(text)))
85        return W.Listed{fs}
86    case False{}:
87      IO.pure(W.Listing, W.Unlisted{
88        "ez: ez lock reads the .bend files git tracks, and git could not list them: " ++
89          String.trim(text)})
90
91# every `.bend` file git tracks under the current directory, with its text.
92# Untracked files are not a clone's, so they are not the lock's.
93def read.listing() -> IO(W.Listing):
94  do IO<W.Listing>:
95    +out : String <- R.exec(["git", "-c", "core.quotepath=off", "ls-files",
96      "--", "*.bend"])
97    listed(R.ok(out), R.text(out))
98
99# ---------------------------------------------------------------------------
100# answering one question
101
102# every text a fetch answered, or the first miss
103def gots(+how: W.How, +manifest: String, gs: List<&2, Web.Got>, acc: List<&2, String>) -> W.Answer:
104  match gs:
105    case []:
106      W.Got{how, manifest, List.reverse(&2, String, acc)}
107    case g <> t:
108      match g:
109        case Web.Miss{why}:
110          W.Miss{"ez: " ++ why}
111        case Web.Have{body}:
112          gots(how, manifest, t, body <> acc)
113
114# every file of a hub package, fetched and checked against its sum
115def hub.all(+hub: String, +hash: String, fs: List<&2, K.Item>) -> IO(List<&2, Web.Got>):
116  match fs:
117    case []:
118      IO.pure(List<&2, Web.Got>, [])
119    case +h <> t:
120      do IO<List<&2, Web.Got>>:
121        g : Web.Got <- Net.fetch(L.url_of(hub, hash, K.file.at(h)), K.file.sum(h))
122        rest : List<&2, Web.Got> <- hub.all(hub, hash, t)
123        return g <> rest
124
125# a hub package's files, once its manifest arrived
126def hub.got(got: Web.Got, +hub: String, +hash: String) -> IO(W.Answer):
127  match got:
128    case Web.Miss{why}:
129      IO.pure(W.Answer, W.Miss{why})
130    case Web.Have{+body}:
131      do IO<W.Answer>:
132        gs : List<&2, Web.Got> <- hub.all(hub, hash, L.manifest.files(String.lines(body)))
133        return gots(W.Served{}, body, gs, [])
134
135# a hub package: its manifest, then every file it names
136def answer.hub(+hub: String, +hash: String) -> IO(W.Answer):
137  do IO<W.Answer>:
138    +s : Say.Spin <- Spin.hold(Say.dep.hub(hash))
139    g : Web.Got <- Net.fetch(L.url_of(hub, hash, "manifest"), L.want(hash))
140    a : W.Answer <- hub.got(g, hub, hash)
141    Spin.done(s)
142    return a
143
144# every file a tree's manifest names, read from beside it. A file that is not
145# there reads as "", which does not hash to its sum.
146def disk.all(+dir: String, fs: List<&2, K.Item>) -> IO(List<&2, String>):
147  match fs:
148    case []:
149      IO.pure(List<&2, String>, [])
150    case +h <> t:
151      do IO<List<&2, String>>:
152        m : Maybe<&2, String> <- F.read(P.join(dir, K.file.at(h)))
153        rest : List<&2, String> <- disk.all(dir, t)
154        return F.text_of(m) <> rest
155
156# a tree under BEND_LIB, as it is
157def answer.lib(+dir: String, +manifest: String) -> IO(W.Answer):
158  do IO<W.Answer>:
159    ss : List<&2, String> <- disk.all(dir, L.manifest.files(String.lines(manifest)))
160    return W.Got{W.Lib{}, manifest, ss}
161
162# a checkout that could not be made or read is removed, and the question is
163# answered with why
164def clone.miss(+work: String, +why: String) -> IO(W.Answer):
165  do IO<W.Answer>:
166    _rm : String <- R.exec(["rm", "-rf", work])
167    return W.Miss{why}
168
169# a command of a clone that worked goes on with the clone; one that failed
170# ends it
171def clone.ran(ok: Bool, +work: String, +why: String, go: Unit -> IO(W.Answer)) -> IO(W.Answer):
172  match ok:
173    case True{}:
174      go(Unit{})
175    case False{}:
176      clone.miss(work, why)
177
178# one git command of a clone, and the rest of the clone when it worked
179def clone.run(+work: String, what: String, args: List<&2, String>, go: Unit -> IO(W.Answer)) -> IO(W.Answer):
180  do IO<W.Answer>:
181    +out : String <- R.exec(args)
182    clone.ran(R.ok(out), work, "ez: git " ++ what ++ ": " ++ R.text(out), go)
183
184# the checkout's package, walked from its entry the way `ez add` walked it
185def clone.walk(+work: String, +entry: String, +nar: String) -> IO(W.Answer):
186  do IO<W.Answer>:
187    +p : K.Pkg <- K.pkg_in(work, entry)
188    +fs : List<&2, K.Item> <- IO.pure(List<&2, K.Item>, K.pkg.files(p))
189    ss : List<&2, String> <- disk.all(K.pkg.root(p), fs)
190    _rm : String <- R.exec(["rm", "-rf", work])
191    return W.Got{W.Clone{nar}, K.manifest_of(fs), ss}
192
193# the entry has to be in the checkout for there to be a package to walk
194def clone.entry(ok: Bool, +work: String, +entry: String, +url: String, +rev: String, +nar: String) -> IO(W.Answer):
195  match ok:
196    case True{}:
197      clone.walk(work, entry, nar)
198    case False{}:
199      clone.miss(work, "ez: " ++ entry ++ " is not in " ++ url ++ " at " ++ rev)
200
201# the checkout, weighed and walked
202def clone.weigh(+work: String, +url: String, +rev: String, +entry: String) -> IO(W.Answer):
203  do IO<W.Answer>:
204    _dg : String <- R.exec(["rm", "-rf", work ++ "/.git"])
205    +s : Say.Spin <- Spin.hold(Say.hashing(rev))
206    +nar : String <- GX.nar(work)
207    Spin.done(s)
208    +ck : String <- R.exec(["test", "-f", P.join(work, entry)])
209    clone.entry(R.ok(ck), work, entry, url, rev, nar)
210
211# a git package whose tree is not under BEND_LIB: a shallow clone at the rev
212# the ledger pins, the way `ez add` and `ez fetch` clone, from the source the
213# question names, which the planner anchored at the project root. Nothing is
214# laid here; the plan lays the tree once it has checked it.
215def answer.clone(+lib: String, +hash: String, +url: String, +rev: String, +entry: String) -> IO(W.Answer):
216  +work = lib ++ "/.work-" ++ hash
217  do IO<W.Answer>:
218    _rm : String <- R.exec(["rm", "-rf", work])
219    _mk : String <- R.exec(["mkdir", "-p", work])
220    +s : Say.Spin <- Spin.hold(Say.dep.git(Say.label(hash, entry), url))
221    a : W.Answer <- clone.run(work, "init", ["git", "init", "-q", work], _u =>
222      clone.run(work, "fetch", ["git", "-C", work, "fetch", "-q", "--depth", "1", url, rev], _v =>
223        clone.run(work, "checkout", ["git", "-C", work, "checkout", "-q", rev], _w =>
224          clone.weigh(work, url, rev, entry))))
225    Spin.done(s)
226    return a
227
228# a tree already under BEND_LIB is read; a missing one is cloned
229def git.seen(
230  got: Maybe<&2, String>,
231  +lib: String,
232  +hash: String,
233  +url: String,
234  +rev: String,
235  +entry: String
236) -> IO(W.Answer):
237  match got:
238    case Some{text}:
239      answer.lib(lib ++ "/" ++ hash, text)
240    case None{}:
241      answer.clone(lib, hash, url, rev, entry)
242
243# a git package
244def answer.git(+lib: String, +hash: String, +url: String, +rev: String, +entry: String) -> IO(W.Answer):
245  do IO<W.Answer>:
246    m : Maybe<&2, String> <- F.read(lib ++ "/" ++ hash ++ "/manifest")
247    git.seen(m, lib, hash, url, rev, entry)
248
249# a package, from where its source says
250def answer.pkg(+lib: String, +hash: String, src: L.Src, +hub: String) -> IO(W.Answer):
251  match src:
252    case L.Hub{}:
253      answer.hub(hub, hash)
254    case L.Git{url, rev, entry, _root, _nar, _tag}:
255      answer.git(lib, hash, url, rev, entry)
256    case L.Member{path, _entry}:
257      IO.pure(W.Answer, W.Miss{"ez: the workspace member at " ++ path ++ " was not walked"})
258
259# what the hub says a name names, as it said it; the planner reads the hash
260# out of it
261def said(got: Web.Got) -> W.Answer:
262  match got:
263    case Web.Miss{why}:
264      W.Miss{"ez: " ++ why}
265    case Web.Have{body}:
266      W.Said{body}
267
268# a name, asked of the hub once: `GET <hub>/name/<nv>`
269def answer.name(+hub: String, +nv: String) -> IO(W.Answer):
270  do IO<W.Answer>:
271    +s : Say.Spin <- Spin.hold(Say.dep.hub(nv))
272    g : Web.Got <- Net.fetch(hub ++ "/name/" ++ nv, "")
273    Spin.done(s)
274    return said(g)
275
276# the lock being rewritten, "" when there is none
277def answer.lock() -> IO(W.Answer):
278  do IO<W.Answer>:
279    m : Maybe<&2, String> <- F.read(W.lockfile())
280    return W.Locked{F.text_of(m)}
281
282# one question, answered
283def answer(+lib: String, ask: W.Ask) -> IO(W.Answer):
284  match ask:
285    case W.Pkg{+hash, src, +hub}:
286      answer.pkg(lib, hash, src, hub)
287    case W.Name{+nv, +hub}:
288      answer.name(hub, nv)
289    case W.Lock{}:
290      answer.lock()
291
292# every question, answered
293def answer.all(+lib: String, asks: List<&2, W.Ask>) -> IO(List<&2, W.Reply>):
294  match asks:
295    case []:
296      IO.pure(List<&2, W.Reply>, [])
297    case +h <> t:
298      do IO<List<&2, W.Reply>>:
299        a : W.Answer <- answer(lib, h)
300        rest : List<&2, W.Reply> <- answer.all(lib, t)
301        return W.Reply{h, a} <> rest
302
303# ---------------------------------------------------------------------------
304# answering the upgrade's questions
305
306# what git printed, and whether it succeeded, kept as it was
307def git.said(+what: String, args: List<&2, String>) -> IO(Up.Answer):
308  do IO<Up.Answer>:
309    +s : Say.Spin <- Spin.hold(what)
310    +out : String <- R.exec(args)
311    Spin.done(s)
312    return Up.Printed{R.ok(out), R.text(out)}
313
314# The upgrade's questions, and `ez add`'s and `ez fetch`'s, name the source
315# as git is to read it, anchored at the project by their planners
316# (`Up.anchor`, `P.Query`), so each is handed to git as it is.
317
318# the refs a ref could name: a tag, its peeled form, and a branch, all at
319# once, because which of them exists is the question
320def answer.refs(+url: String, +ref: String) -> IO(Up.Answer):
321  git.said(Say.resolving.at(url), ["git", "ls-remote", url, "refs/tags/" ++ ref,
322    "refs/tags/" ++ ref ++ "^{}", "refs/heads/" ++ ref])
323
324# HEAD on the remote, with the branch it is a symref to
325def answer.head(+url: String) -> IO(Up.Answer):
326  git.said(Say.resolving.at(url), ["git", "ls-remote", "--symref", url, "HEAD"])
327
328# every tag on the remote
329def answer.tags(+url: String) -> IO(Up.Answer):
330  git.said(Say.resolving.at(url), ["git", "ls-remote", "--tags", url])
331
332# whether `tip` descends from `pin`, asked of a blobless bare clone
333def answer.above(+lib: String, +url: String, +pin: String, +tip: String) -> IO(Up.Answer):
334  do IO<Up.Answer>:
335    yes : Bool <- GX.descendant.at(lib ++ "/.work-above", url, pin, tip)
336    return Up.Onward{yes}
337
338# the checkout's package, walked from its entry the way `ez add` walks it, with
339# the directory its paths are written from, against the repo
340def tree.walk(+work: String, +at: String, +nar: String) -> IO(Up.Answer):
341  do IO<Up.Answer>:
342    +p : K.Pkg <- K.pkg_in(work, at)
343    +fs : List<&2, K.Item> <- IO.pure(List<&2, K.Item>, K.pkg.files(p))
344    ss : List<&2, String> <- disk.all(K.pkg.root(p), fs)
345    +rt : String <- IO.pure(String, Git.root.rel(P.join(work, at), K.pkg.root(p), at))
346    _rm : String <- R.exec(["rm", "-rf", work])
347    return Up.Checkout{nar, rt, K.manifest_of(fs), ss}
348
349# the entry has to be in the checkout for there to be a package to walk
350def tree.entry(ok: Bool, +work: String, +at: String, +url: String, +rev: String, +nar: String) -> IO(Up.Answer):
351  match ok:
352    case True{}:
353      tree.walk(work, at, nar)
354    case False{}:
355      do IO<Up.Answer>:
356        _rm : String <- R.exec(["rm", "-rf", work])
357        return Up.Miss{"ez: " ++ at ++ " is not in " ++ url ++ " at " ++ rev}
358
359# a checkout at a rev: cloned, its `.git` removed, weighed, and walked. An
360# entry that was not recorded is read from the checkout. Nothing is laid and
361# nothing is noted: a tree the upgrade moves to is laid by the plan, once the
362# upgrade and the lock both succeed.
363def answer.tree(+lib: String, +url: String, +rev: String, +entry: String) -> IO(Up.Answer):
364  +work = lib ++ "/.work-" ++ rev
365  do IO<Up.Answer>:
366    _rm : String <- R.exec(["rm", "-rf", work])
367    _mk : String <- R.exec(["mkdir", "-p", work])
368    GX.clone(work, url, rev)
369    +at : String <- Git.entry.for(Bool.not(String.is_empty(entry)), entry, work)
370    _dg : String <- R.exec(["rm", "-rf", work ++ "/.git"])
371    +s : Say.Spin <- Spin.hold(Say.hashing(rev))
372    +nar : String <- GX.nar(work)
373    Spin.done(s)
374    +ck : String <- R.exec(["test", "-f", P.join(work, at)])
375    tree.entry(R.ok(ck), work, at, url, rev, nar)
376
377# a commit's NAR hash, for a tool
378def answer.weigh(+url: String, +rev: String) -> IO(Up.Answer):
379  do IO<Up.Answer>:
380    +nar : String <- GX.weigh(url, rev)
381    return Up.Weighed{nar}
382
383# the hub's manifest for a hash a dependency moved to
384def answer.hubhas(+hub: String, +hash: String) -> IO(Up.Answer):
385  do IO<Up.Answer>:
386    g : Web.Got <- Net.fetch(L.url_of(hub, hash, "manifest"), L.want(hash))
387    return Up.HubSaid{g}
388
389# the ignore file, "" when it is missing
390def answer.ignore() -> IO(Up.Answer):
391  do IO<Up.Answer>:
392    m : Maybe<&2, String> <- F.read(".gitignore")
393    return Up.Plain{F.text_of(m)}
394
395# one source in front of the rest, or the first that could not be read
396def found.put(got: Maybe<&2, String>, +at: String, rest: Up.Answer) -> Up.Answer:
397  match got:
398    case None{}:
399      Up.Miss{"ez: " ++ at ++ " could not be read"}
400    case Some{text}:
401      match rest:
402        case Up.Files{fs}:
403          Up.Files{Up.Found{at, text} <> fs}
404        case _:
405          rest
406
407# every listed source, read
408def found.all(ps: List<&2, String>) -> IO(Up.Answer):
409  match ps:
410    case []:
411      IO.pure(Up.Answer, Up.Files{[]})
412    case +h <> t:
413      do IO<Up.Answer>:
414        m : Maybe<&2, String> <- F.read(h)
415        rest : Up.Answer <- found.all(t)
416        return found.put(m, h, rest)
417
418def found.listed(ok: Bool, +text: String) -> IO(Up.Answer):
419  match ok:
420    case True{}:
421      found.all(nonempty(String.lines(text)))
422    case False{}:
423      IO.pure(Up.Answer, Up.Miss{"ez: could not list sources: " ++ String.trim(text)})
424
425# every Bend source of the project. Vendored trees and the gate's shadow live
426# under .ez, and a checkout's .git is not source, so neither is rewritten.
427def answer.sources() -> IO(Up.Answer):
428  do IO<Up.Answer>:
429    +out : String <- R.exec(["find", ".", "-name", "*.bend",
430      "-not", "-path", "*/.ez/*", "-not", "-path", "*/.git/*"])
431    found.listed(R.ok(out), R.text(out))
432
433# a checkout `ez add` walks, which could not be made or read: removed, and
434# the question answered with why
435def whole.miss(+work: String, +why: String) -> IO(Up.Answer):
436  do IO<Up.Answer>:
437    _rm : String <- R.exec(["rm", "-rf", work])
438    return Up.Miss{why}
439
440# a command of the checkout that worked goes on with it; one that failed ends
441# it
442def whole.ran(ok: Bool, +work: String, +why: String, go: Unit -> IO(Up.Answer)) -> IO(Up.Answer):
443  match ok:
444    case True{}:
445      go(Unit{})
446    case False{}:
447      whole.miss(work, why)
448
449# one git command of the checkout, and the rest of it when it worked
450def whole.run(+work: String, what: String, args: List<&2, String>, go: Unit -> IO(Up.Answer)) -> IO(Up.Answer):
451  do IO<Up.Answer>:
452    +out : String <- R.exec(args)
453    whole.ran(R.ok(out), work, "ez: git " ++ what ++ ": " ++ String.trim(R.text(out)), go)
454
455# a file read, in front of the rest
456def whole.put(got: Maybe<&2, String>, +at: String, rest: List<&2, K.Source>) -> List<&2, K.Source>:
457  match got:
458    case None{}:
459      rest
460    case Some{text}:
461      K.Source{at, text} <> rest
462
463# every listed file of the checkout, read, with its path from the checkout's
464# top. One that cannot be read as text, such as a link to a directory, is
465# left out, as the walk would find it missing.
466def whole.read(+work: String, ps: List<&2, String>) -> IO(List<&2, K.Source>):
467  match ps:
468    case []:
469      IO.pure(List<&2, K.Source>, [])
470    case +h <> t:
471      do IO<List<&2, K.Source>>:
472        m : Maybe<&2, String> <- F.read(h)
473        rest : List<&2, K.Source> <- whole.read(work, t)
474        return whole.put(m, String.drop(h, (1n + String.length(work) : Nat)), rest)
475
476# the checkout, its `.git` removed, weighed, and every file of it read; then
477# removed. Nothing is laid: `ez add` lays the tree from its plan.
478# the files of a checkout, read, once `find` has listed them. A listing that
479# failed is refused rather than read: `find` writes what it could not reach
480# to stderr, which snap answers beside the paths (SNAP-ANS-4), and such a
481# line is no path of the package.
482def whole.listed(ok: Bool, +work: String, +nar: String, +text: String) -> IO(Up.Answer):
483  match ok:
484    case True{}:
485      do IO<Up.Answer>:
486        fs : List<&2, K.Source> <- whole.read(work, nonempty(String.lines(text)))
487        _rm : String <- R.exec(["rm", "-rf", work])
488        return Up.Cloned{nar, fs}
489    case False{}:
490      do IO<Up.Answer>:
491        _rm : String <- R.exec(["rm", "-rf", work])
492        return Up.Miss{"ez: could not list the checkout: " ++ String.trim(text)}
493
494def whole.weigh(+work: String, +rev: String) -> IO(Up.Answer):
495  do IO<Up.Answer>:
496    _dg : String <- R.exec(["rm", "-rf", work ++ "/.git"])
497    +s : Say.Spin <- Spin.hold(Say.hashing(rev))
498    +nar : String <- GX.nar(work)
499    Spin.done(s)
500    +out : String <- R.exec(["find", work, "(", "-type", "f", "-o", "-type", "l", ")"])
501    whole.listed(R.ok(out), work, nar, R.text(out))
502
503# a scratch directory made, or why it could not be
504def whole.dir(ok: Bool, +work: String, +url: String, +rev: String) -> IO(Up.Answer):
505  match ok:
506    case False{}:
507      IO.pure(Up.Answer, Up.Miss{"ez: no scratch directory to clone into: " ++ work})
508    case True{}:
509      do IO<Up.Answer>:
510        +s : Say.Spin <- Spin.hold(Say.git.fetch(url))
511        a : Up.Answer <- whole.run(work, "init", ["git", "init", "-q", work], _u =>
512          whole.run(work, "fetch", ["git", "-C", work, "fetch", "-q", "--depth", "1", url, rev], _v =>
513            whole.run(work, "checkout", ["git", "-C", work, "checkout", "-q", rev], _w =>
514              whole.weigh(work, rev))))
515        Spin.done(s)
516        return a
517
518# a checkout at a rev for `ez add`: a shallow clone into a scratch directory
519# outside the project and BEND_LIB, weighed, and every file's text read, so
520# the planner walks the package itself. The scratch directory is removed
521# whether or not the clone worked, so a refused add leaves no trace.
522def answer.whole(+url: String, +rev: String) -> IO(Up.Answer):
523  do IO<Up.Answer>:
524    +tmp : String <- R.exec(["mktemp", "-d"])
525    whole.dir(R.ok(tmp), String.trim(R.text(tmp)), url, rev)
526
527# one of the upgrade's questions, answered
528def answer.up(+lib: String, ask: Up.Ask) -> IO(Up.Answer):
529  match ask:
530    case Up.Refs{url, ref}:
531      answer.refs(url, ref)
532    case Up.Head{url}:
533      answer.head(url)
534    case Up.Tags{url}:
535      answer.tags(url)
536    case Up.Above{url, pin, tip}:
537      answer.above(lib, url, pin, tip)
538    case Up.Tree{url, rev, entry}:
539      answer.tree(lib, url, rev, entry)
540    case Up.Weigh{url, rev}:
541      answer.weigh(url, rev)
542    case Up.HubHas{hub, hash}:
543      answer.hubhas(hub, hash)
544    case Up.Ignore{}:
545      answer.ignore()
546    case Up.Sources{}:
547      answer.sources()
548    case Up.Clone{url, rev}:
549      answer.whole(url, rev)
550
551# every one of them, answered
552def answer.ups(+lib: String, asks: List<&2, Up.Ask>) -> IO(List<&2, Up.Reply>):
553  match asks:
554    case []:
555      IO.pure(List<&2, Up.Reply>, [])
556    case +h <> t:
557      do IO<List<&2, Up.Reply>>:
558        a : Up.Answer <- answer.up(lib, h)
559        rest : List<&2, Up.Reply> <- answer.ups(lib, t)
560        return Up.Reply{h, a} <> rest
561
562# every query of the lock's upgrade: git is asked the question as the planner
563# anchored it, and the answer is recorded under the question the upgrade
564# looks it up by
565def answer.queries(+lib: String, qs: List<&2, Plan.Query>) -> IO(List<&2, Up.Reply>):
566  match qs:
567    case []:
568      IO.pure(List<&2, Up.Reply>, [])
569    case Plan.Query{ask, git} <> t:
570      do IO<List<&2, Up.Reply>>:
571        a : Up.Answer <- answer.up(lib, git)
572        rest : List<&2, Up.Reply> <- answer.queries(lib, t)
573        return Up.Reply{ask, a} <> rest
574
575# the World with more answers in it
576def more(world: W.World, rs: List<&2, W.Reply>, us: List<&2, Up.Reply>) -> W.World:
577  W.World{args, ledger, listing, replies, ups} = world
578  W.World{args, ledger, listing, List.append(&2, W.Reply, replies, rs),
579    List.append(&2, Up.Reply, ups, us)}
580
581# ---------------------------------------------------------------------------
582# executing a plan
583
584# a write that did not happen stops the command
585def wrote(ok: Bool, +at: String) -> IO(Unit):
586  match ok:
587    case True{}:
588      IO.pure(Unit, Unit{})
589    case False{}:
590      IO.die(Unit, 1, "ez: " ++ at ++ " could not be written")
591
592# one file written, with the directory it goes in made first
593def write(+at: String, text: String) -> IO(Unit):
594  do IO<Unit>:
595    _mk : String <- R.exec(["mkdir", "-p", P.dir(at)])
596    ok : Bool <- F.write(at, text)
597    wrote(ok, at)
598
599# every file of a tree, in order
600def lay.all(+dir: String, fs: List<&2, W.Source>) -> IO(Unit):
601  match fs:
602    case []:
603      IO.pure(Unit, Unit{})
604    case W.Source{at, text} <> t:
605      do IO<Unit>:
606        write(dir ++ "/" ++ at, text)
607        lay.all(dir, t)
608
609# where a place is
610def place(+lib: String, pl: Plan.Place) -> String:
611  match pl:
612    case Plan.Committed{}:
613      ".ez/lib"
614    case Plan.Cache{}:
615      lib
616    case Plan.Under{dir}:
617      dir ++ "/.ez/lib"
618
619# one effect
620def exec(+lib: String, effect: Plan.Effect) -> IO(Unit):
621  match effect:
622    case Plan.Write{at, text}:
623      write(at, text)
624    case Plan.Lay{pl, hash, fs}:
625      +dir = place(lib, pl) ++ "/" ++ hash
626      do IO<Unit>:
627        _rm : String <- R.exec(["rm", "-rf", dir])
628        lay.all(dir, fs)
629    case Plan.Drop{hash}:
630      do IO<Unit>:
631        _rm : String <- R.exec(["rm", "-rf", ".ez/lib/" ++ hash])
632        return Unit{}
633    case Plan.Say{text}:
634      IO.print(text)
635    case Plan.Name{nv, hash}:
636      write(lib ++ "/names/" ++ nv, L.name.text(hash))
637
638# every effect, in order
639def exec.all(+lib: String, es: List<&2, Plan.Effect>) -> IO(Unit):
640  match es:
641    case []:
642      IO.pure(Unit, Unit{})
643    case h <> t:
644      do IO<Unit>:
645        exec(lib, h)
646        exec.all(lib, t)
647
648# a status that is 0 ends nothing, so the program runs out and exits 0; any
649# other exits with that status and the reason
650def end.at(zero: Bool, +code: U32, +why: String) -> IO(Unit):
651  match zero:
652    case True{}:
653      IO.pure(Unit, Unit{})
654    case False{}:
655      IO.die(Unit, code, why)
656
657# how the command ends: with the status the planner gave its outcome
658# (`Plan.status`, EZ-OUT-1), and the reason when it refused
659def end(+outcome: Plan.Outcome) -> IO(Unit):
660  +code = Plan.status(outcome)
661  end.at(U32.is_eq(code, 0), code, Plan.why(outcome))
662
663# a plan, executed
664def exec.plan(+lib: String, pl: Plan.Plan) -> IO(Unit):
665  Plan.Plan{es, outcome} = pl
666  do IO<Unit>:
667    exec.all(lib, es)
668    end(outcome)
669
670# ---------------------------------------------------------------------------
671# the loop
672
673# the planner's step: its questions answered and the loop gone round again,
674# or its plan run
675def loop.step(st: Plan.Step, +lib: String, back: WsUp.Back, world: W.World, go: W.World -> IO(Unit)) -> IO(Unit):
676  match st:
677    case Plan.Asking{asks, ups, names}:
678      do IO<Unit>:
679        rs : List<&2, W.Reply> <- answer.all(lib, asks)
680        us : List<&2, Up.Reply> <- answer.queries(lib, ups)
681        ns : List<&2, W.Reply> <- answer.all(lib, names)
682        go(more(world, List.append(&2, W.Reply, rs, ns), us))
683    case Plan.Run{_pl}:
684      exec.plan(lib, WsUp.plan(back, world))
685
686# the rounds the loop may take. Each round's questions come from the last
687# round's answers (a package's imports are known once git has answered for
688# it), so how many there will be is not known up front. This is a guard
689# against a planner that keeps asking, not a limit on a lock: running out of
690# it dies loudly. The walk inside each round takes its fuel from its inputs
691# (`Plan.walk.fuel`).
692def rounds() -> Nat:
693  U32.to_nat(100000)
694
695# the loop, under fuel. Every round adds at least one answer, since the
696# planner never asks what the World answers already.
697def loop(fuel: Nat, +lib: String, +back: WsUp.Back, +world: W.World) -> IO(Unit):
698  match fuel:
699    case 0n:
700      IO.die(Unit, 1, "ez: the lock kept asking questions")
701    case 1n+f:
702      loop.step(WsUp.step(back, world), lib, back, world, w => loop(f, lib, back, w))
703
704# a member's package as the lock reads it: from BEND_LIB when a tree is laid
705# there under its hash, as a git package's is (`git.seen`), and fresh from
706# the committed tree, as the joint ledger walked it, otherwise
707def settle.seen(got: Maybe<&2, String>, ask: W.Ask, answer: W.Answer, +lib: String, +hash: String) -> IO(W.Reply):
708  match got:
709    case None{}:
710      IO.pure(W.Reply, W.Reply{ask, answer})
711    case Some{text}:
712      do IO<W.Reply>:
713        a : W.Answer <- answer.lib(lib ++ "/" ++ hash, text)
714        return W.Reply{ask, a}
715
716# one member's package, so
717def settle.one(rp: W.Reply, +lib: String) -> IO(W.Reply):
718  W.Reply{+ask, answer} = rp
719  +hash = W.ask.hash(ask)
720  do IO<W.Reply>:
721    got : Maybe<&2, String> <- F.read(lib ++ "/" ++ hash ++ "/manifest")
722    settle.seen(got, ask, answer, lib, hash)
723
724# every member's package, so
725def settle(rs: List<&2, W.Reply>, +lib: String) -> IO(List<&2, W.Reply>):
726  match rs:
727    case []:
728      IO.pure(List<&2, W.Reply>, [])
729    case hd <> tl:
730      do IO<List<&2, W.Reply>>:
731        rp : W.Reply <- settle.one(hd, lib)
732        rest : List<&2, W.Reply> <- settle(tl, lib)
733        return rp <> rest
734
735# the loop, on the ledger the lock is planned from, with the workspace's
736# members already answered
737def run.planned(pd: WsRun.Planned, +lib: String, up: Bool, only: String, here: String, ls: W.Listing) -> IO(Unit):
738  WsRun.Planned{led, back, seeds} = pd
739  do IO<Unit>:
740    rs : List<&2, W.Reply> <- settle(seeds, lib)
741    loop(rounds(), lib, back, W.World{W.Args{up, only, here}, led, ls, rs, []})
742
743# `ez lock` and `ez lock --upgrade [--package only]`: the directory it runs
744# in, the ledger and the listing read, every question answered, and the plan
745# run
746def run(up: Bool, +only: String) -> IO(Unit):
747  do IO<Unit>:
748    IO.print_err(Say.resolving())
749    lib : String <- bend.lib()
750    +pwd : String <- R.exec(["pwd"])
751    own : Maybe<&2, String> <- read.ledger()
752    +ls : W.Listing <- read.listing()
753    pd : WsRun.Planned <- WsRun.ledger(own, J.files(ls))
754    run.planned(pd, lib, up, only, String.trim(R.text(pwd)), ls)