src/remove/plan.bend source
src/remove/plan.bend on the hub · documented module
# remove/plan: `ez remove` as a pure planner. It asks nothing: its World is# the name asked for, the ledger's text (None when there is no ez.toml) and# `.gitignore`'s text ("" when there is none), and its planner is one function# from that World to a Plan. The interpreter (remove/run.bend) reads the two# files, calls it, and runs what it returns.## The plan, when the ledger reads and names the dependency: ez.toml written# with the dependency dropped (`Rend.remove`); `.gitignore` written with the# allowlist `I.sync` derives from what is left, when that changes it; and# the committed tree `.ez/lib/<hash>` of a dependency that was vendored# removed, unless a dependency that is left still names that hash. Anything# else is a refusal, with no effect at all: no ledger, one that does not# parse, or a name the ledger does not have, which `cargo remove` refuses too.## The Effect, Plan and Outcome types are the lock's (lock/plan.bend), which# the add design moves to a shared plan/plan.bend later; see "Update (A1,# A4)" in docs/rfc/ez-add-planner.md.import Baseimport ../lock/plan.bend as Pimport ../ledger/manifest.bend as Mimport ../ledger/render.bend as Rendimport ../ledger/ignore.bend as I# what `ez remove` reads: the name it was given, the ledger's text or None,# and the ignore file's text, "" when there is nonetype World is Data: World{name: String, ledger: Maybe<&2, String>, ignore: String}# the ledger's path, which is fixeddef toml() -> String: "ez.toml"# the ignore file's path, which is fixeddef ignore.file() -> String: ".gitignore"# the ledger as read: what the file says, or, with no file, a read that did# not parse and says why. A missing ledger is not an empty one to write back.def read.of(got: Maybe<&2, String>) -> M.Read: match got: case None{}: M.Bad{"no " ++ toml() ++ " here; ez remove works on a project's " ++ "ledger, and ez init makes one"} case Some{text}: M.parse(text)# the dependencies of a ledger that was readdef deps.of(manifest: M.Manifest) -> List<&2, M.Dep>: M.Manifest{_n, _e, _b, _h, _pa, _pv, ds, _ts} = manifest ds# the rest of a scan only when this one was not itdef any.step(hit: Bool, rest: Unit -> Bool) -> Bool: match hit: case True{}: True{} case False{}: rest(Unit{})# whether a dependency has this namedef named(ds: List<&2, M.Dep>, +name: String) -> Bool: match ds: case []: False{} case +h <> t: any.step(String.eq(M.dep.name(h), name), _u => named(t, name))# whether a dependency names this hashdef holds(ds: List<&2, M.Dep>, +hash: String) -> Bool: match ds: case []: False{} case +h <> t: any.step(String.eq(M.dep.hash(h), hash), _u => holds(t, hash))# whether a char may stand in a hash that names a directory under `.ez/lib`def char.ok(+ch: Char) -> Bool: Bool.not(Bool.or(Char.is_eq(ch, '/'), Char.is_eq(ch, '\\')))# whether every char of a text maydef chars.ok(text: String) -> Bool: match text: case SNil{}: True{} case SCon{+h, t}: +rest = chars.ok(t) Bool.and(char.ok(h), rest)# a hash `rm -rf .ez/lib/<hash>` removes one tree for: a `0x` name with no# path separator in it. `M.parse` does not check a hash's shape, and a ledger# is text a person edits, so this is checked before a tree is removed.def safe(+hash: String) -> Bool: Bool.and(String.starts_with(hash, "0x"), chars.ok(hash))# a vendored hash, removed ahead of the rest when it is safe to remove and no# dependency that is left names itdef gone.hash(keep: Bool, +hash: String, rest: List<&2, String>) -> List<&2, String>: match keep: case True{}: rest case False{}: hash <> rest# a dependency's committed tree, when it is the one removed and is vendoreddef gone.put(hit: Bool, +hash: String, +left: List<&2, M.Dep>, rest: List<&2, String>) -> List<&2, String>: match hit: case True{}: gone.hash(Bool.or(holds(left, hash), Bool.not(safe(hash))), hash, rest) case False{}: rest# the committed trees removing `name` leaves no dependency to name: the hash# of each vendored dependency by that name that none in `left` namesdef gone(ds: List<&2, M.Dep>, +name: String, +left: List<&2, M.Dep>) -> List<&2, String>: match ds: case []: [] case +h <> t: gone.put(Bool.and(String.eq(M.dep.name(h), name), M.source.vendor(M.source.dep(h))), M.dep.hash(h), left, gone(t, name, left))# `.gitignore` written ahead of the rest when the allowlist changes itdef ignore.put(same: Bool, +next: String, rest: List<&2, P.Effect>) -> List<&2, P.Effect>: match same: case True{}: rest case False{}: P.Write{ignore.file(), next} <> rest# what `ez remove` does to a ledger that names the dependency: the ledger# written without it, the allowlist derived from what is left, and each# committed tree it leaves unnamed removeddef edits(+led: M.Manifest, +name: String, +ignore: String) -> List<&2, P.Effect>: +left = deps.of(Rend.remove(led, name)) +next = I.sync(left, ignore) P.Write{toml(), Rend.show(Rend.remove(led, name))} <> ignore.put(String.eq(next, ignore), next, P.drops.of(gone(deps.of(led), name, left), []))# the refusal for a name the ledger does not havedef absent(+name: String) -> String: "ez: " ++ toml() ++ " has no dependency named " ++ name# the refusal for a ledger that names the dependency but, with it dropped,# cannot be written so that it reads back (EZ-LED-4), or for a name it does# not havedef refusal(named: Bool, +name: String) -> String: Bool.pick(String, named, Rend.unrenderable(), absent(name))# the plan for a ledger that read: the edits when it names the dependency and# the ledger left can be written, a refusal when notdef plan.has(has: Bool, +led: M.Manifest, +name: String, +ignore: String) -> P.Plan: match has: case True{}: P.Plan{edits(led, name, ignore), P.Success{}} case False{}: P.Plan{[], P.Refused{refusal(named(deps.of(led), name), name)}}# the plan for the ledger as read. One that did not read, or is not there,# is refused before anything else, and its reason is the one every command# that edits a ledger gives.def plan.read(led: M.Read, +name: String, +ignore: String) -> P.Plan: match led: case M.Bad{why}: P.Plan{[], P.Refused{"ez: " ++ M.show(M.Bad{why})}} case M.Good{+m}: plan.has(Bool.and(named(deps.of(m), name), Rend.renderable(Rend.remove(m, name))), m, name, ignore)# `ez remove` over a Worlddef plan(world: World) -> P.Plan: World{+n, l, +ig} = world plan.read(read.of(l), n, ig)# whether a plan ends in a refusaldef refuses.plan(pl: P.Plan) -> Bool: P.Plan{_es, outcome} = pl match outcome: case P.Success{}: False{} case P.Refused{_why}: True{}# whether `ez remove` refuses on a Worlddef refuses(world: World) -> Bool: refuses.plan(plan(world))# what a path holds once a plan has run, when it held `was` beforedef after(wrote: P.Wrote, +was: String) -> String: match wrote: case P.Kept{}: was case P.Put{text}: text