src/remove/plan.bend
ezx@1.6.0.0 · 194 lines · 0 definitions
1# remove/plan: `ez remove` as a pure planner. It asks nothing: its World is
2# the name asked for, the ledger's text (None when there is no ez.toml) and
3# `.gitignore`'s text ("" when there is none), and its planner is one function
4# from that World to a Plan. The interpreter (remove/run.bend) reads the two
5# files, calls it, and runs what it returns.
6#
7# The plan, when the ledger reads and names the dependency: ez.toml written
8# with the dependency dropped (`Rend.remove`); `.gitignore` written with the
9# allowlist `I.sync` derives from what is left, when that changes it; and
10# the committed tree `.ez/lib/<hash>` of a dependency that was vendored
11# removed, unless a dependency that is left still names that hash. Anything
12# else is a refusal, with no effect at all: no ledger, one that does not
13# parse, or a name the ledger does not have, which `cargo remove` refuses too.
14#
15# The Effect, Plan and Outcome types are the lock's (lock/plan.bend), which
16# the add design moves to a shared plan/plan.bend later; see "Update (A1,
17# A4)" in docs/rfc/ez-add-planner.md.
18import Base
19import ezx@1.6.0.0/src/lock/plan.bend as P
20import ezx@1.6.0.0/src/ledger/manifest.bend as M
21import ezx@1.6.0.0/src/ledger/render.bend as Rend
22import ezx@1.6.0.0/src/ledger/ignore.bend as I
23
24# what `ez remove` reads: the name it was given, the ledger's text or None,
25# and the ignore file's text, "" when there is none
26type World is Data:
27 World{name: String, ledger: Maybe<&2, String>, ignore: String}
28
29# the ledger's path, which is fixed
30def toml() -> String:
31 "ez.toml"
32
33# the ignore file's path, which is fixed
34def ignore.file() -> String:
35 ".gitignore"
36
37# the ledger as read: what the file says, or, with no file, a read that did
38# not parse and says why. A missing ledger is not an empty one to write back.
39def read.of(got: Maybe<&2, String>) -> M.Read:
40 match got:
41 case None{}:
42 M.Bad{"no " ++ toml() ++ " here; ez remove works on a project's " ++
43 "ledger, and ez init makes one"}
44 case Some{text}:
45 M.parse(text)
46
47# the dependencies of a ledger that was read
48def deps.of(manifest: M.Manifest) -> List<&2, M.Dep>:
49 M.Manifest{_n, _e, _b, _h, _pa, _pv, ds, _ts} = manifest
50 ds
51
52# the rest of a scan only when this one was not it
53def any.step(hit: Bool, rest: Unit -> Bool) -> Bool:
54 match hit:
55 case True{}:
56 True{}
57 case False{}:
58 rest(Unit{})
59
60# whether a dependency has this name
61def named(ds: List<&2, M.Dep>, +name: String) -> Bool:
62 match ds:
63 case []:
64 False{}
65 case +h <> t:
66 any.step(String.eq(M.dep.name(h), name), _u => named(t, name))
67
68# whether a dependency names this hash
69def holds(ds: List<&2, M.Dep>, +hash: String) -> Bool:
70 match ds:
71 case []:
72 False{}
73 case +h <> t:
74 any.step(String.eq(M.dep.hash(h), hash), _u => holds(t, hash))
75
76# whether a char may stand in a hash that names a directory under `.ez/lib`
77def char.ok(+ch: Char) -> Bool:
78 Bool.not(Bool.or(Char.is_eq(ch, '/'), Char.is_eq(ch, '\\')))
79
80# whether every char of a text may
81def chars.ok(text: String) -> Bool:
82 match text:
83 case SNil{}:
84 True{}
85 case SCon{+h, t}:
86 +rest = chars.ok(t)
87 Bool.and(char.ok(h), rest)
88
89# a hash `rm -rf .ez/lib/<hash>` removes one tree for: a `0x` name with no
90# path separator in it. `M.parse` does not check a hash's shape, and a ledger
91# is text a person edits, so this is checked before a tree is removed.
92def safe(+hash: String) -> Bool:
93 Bool.and(String.starts_with(hash, "0x"), chars.ok(hash))
94
95# a vendored hash, removed ahead of the rest when it is safe to remove and no
96# dependency that is left names it
97def gone.hash(keep: Bool, +hash: String, rest: List<&2, String>) -> List<&2, String>:
98 match keep:
99 case True{}:
100 rest
101 case False{}:
102 hash <> rest
103
104# a dependency's committed tree, when it is the one removed and is vendored
105def gone.put(hit: Bool, +hash: String, +left: List<&2, M.Dep>, rest: List<&2, String>) -> List<&2, String>:
106 match hit:
107 case True{}:
108 gone.hash(Bool.or(holds(left, hash), Bool.not(safe(hash))), hash, rest)
109 case False{}:
110 rest
111
112# the committed trees removing `name` leaves no dependency to name: the hash
113# of each vendored dependency by that name that none in `left` names
114def gone(ds: List<&2, M.Dep>, +name: String, +left: List<&2, M.Dep>) -> List<&2, String>:
115 match ds:
116 case []:
117 []
118 case +h <> t:
119 gone.put(Bool.and(String.eq(M.dep.name(h), name),
120 M.source.vendor(M.source.dep(h))), M.dep.hash(h), left, gone(t, name, left))
121
122# `.gitignore` written ahead of the rest when the allowlist changes it
123def ignore.put(same: Bool, +next: String, rest: List<&2, P.Effect>) -> List<&2, P.Effect>:
124 match same:
125 case True{}:
126 rest
127 case False{}:
128 P.Write{ignore.file(), next} <> rest
129
130# what `ez remove` does to a ledger that names the dependency: the ledger
131# written without it, the allowlist derived from what is left, and each
132# committed tree it leaves unnamed removed
133def edits(+led: M.Manifest, +name: String, +ignore: String) -> List<&2, P.Effect>:
134 +left = deps.of(Rend.remove(led, name))
135 +next = I.sync(left, ignore)
136 P.Write{toml(), Rend.show(Rend.remove(led, name))} <>
137 ignore.put(String.eq(next, ignore), next,
138 P.drops.of(gone(deps.of(led), name, left), []))
139
140# the refusal for a name the ledger does not have
141def absent(+name: String) -> String:
142 "ez: " ++ toml() ++ " has no dependency named " ++ name
143
144# the refusal for a ledger that names the dependency but, with it dropped,
145# cannot be written so that it reads back (EZ-LED-4), or for a name it does
146# not have
147def refusal(named: Bool, +name: String) -> String:
148 Bool.pick(String, named, Rend.unrenderable(), absent(name))
149
150# the plan for a ledger that read: the edits when it names the dependency and
151# the ledger left can be written, a refusal when not
152def plan.has(has: Bool, +led: M.Manifest, +name: String, +ignore: String) -> P.Plan:
153 match has:
154 case True{}:
155 P.Plan{edits(led, name, ignore), P.Success{}}
156 case False{}:
157 P.Plan{[], P.Refused{refusal(named(deps.of(led), name), name)}}
158
159# the plan for the ledger as read. One that did not read, or is not there,
160# is refused before anything else, and its reason is the one every command
161# that edits a ledger gives.
162def plan.read(led: M.Read, +name: String, +ignore: String) -> P.Plan:
163 match led:
164 case M.Bad{why}:
165 P.Plan{[], P.Refused{"ez: " ++ M.show(M.Bad{why})}}
166 case M.Good{+m}:
167 plan.has(Bool.and(named(deps.of(m), name), Rend.renderable(Rend.remove(m, name))),
168 m, name, ignore)
169
170# `ez remove` over a World
171def plan(world: World) -> P.Plan:
172 World{+n, l, +ig} = world
173 plan.read(read.of(l), n, ig)
174
175# whether a plan ends in a refusal
176def refuses.plan(pl: P.Plan) -> Bool:
177 P.Plan{_es, outcome} = pl
178 match outcome:
179 case P.Success{}:
180 False{}
181 case P.Refused{_why}:
182 True{}
183
184# whether `ez remove` refuses on a World
185def refuses(world: World) -> Bool:
186 refuses.plan(plan(world))
187
188# what a path holds once a plan has run, when it held `was` before
189def after(wrote: P.Wrote, +was: String) -> String:
190 match wrote:
191 case P.Kept{}:
192 was
193 case P.Put{text}:
194 text