src/ledger/upgrade.bend

ezx@1.11.0.0 · 223 lines · 28 definitions

1# ledger/upgrade: what `ez lock --upgrade` may do to one dependency, before
2# it talks to a remote. The ledger pins exact commits. A tag is the only
3# floating name a person wrote down, so that is what gets re-resolved. A pin
4# with no tag fast-forwards to the default branch tip when the commit is an
5# ancestor of it, and stays a commit pin. A hub package is the hash itself.
6# `vend` says the tree under `.ez/lib/<hash>` is committed, so a move lays the
7# new tree there; the allowlist that names it is `ledger/ignore.bend`'s,
8# derived from the ledger the move writes. A move also rewrites an
9# `import 0x<hash>/` line that names the old hash.
10import Base
11import ezx@1.11.0.0/src/ledger/manifest.bend as M
12
13# what an upgrade will ask the remote, once the ledger has been read
14type Aim is Data:
15  Hold{}
16  Follow{+ref: String}
17  Forward{}
18
19# what that answer means for the pin
20type Verdict is Data:
21  Keep{}
22  Drift{}
23  Off{}
24  Advance{rev: String, tag: String}
25
26# one hash that moved, and the hash that replaces it: an import line to
27# rewrite, or a committed tree to remove
28type Swap is Data:
29  Swap{old: String, new: String}
30
31# an empty `--package` is every dependency; a name is that one
32def chosen.go(all: Bool, +only: String, +name: String) -> Bool:
33  match all:
34    case True{}:
35      True{}
36    case False{}:
37      String.eq(only, name)
38
39# an empty `--package` is every dependency; a name is that one
40def chosen(+only: String, +name: String) -> Bool:
41  chosen.go(String.is_empty(only), only, name)
42
43# the rest of the scan only when this one was not it
44def known.step(hit: Bool, rest: Unit -> Bool) -> Bool:
45  match hit:
46    case True{}:
47      True{}
48    case False{}:
49      rest(Unit{})
50
51# whether the ledger names this dependency
52def known.in(ds: List<&2, M.Dep>, +name: String) -> Bool:
53  match ds:
54    case []:
55      False{}
56    case +h <> t:
57      known.step(String.eq(M.dep.name(h), name), _u => known.in(t, name))
58
59# `--package` with no such dependency is a mistake, and no filter is not
60def known.go(all: Bool, ds: List<&2, M.Dep>, +name: String) -> Bool:
61  match all:
62    case True{}:
63      True{}
64    case False{}:
65      known.in(ds, name)
66
67# whether the ledger names the dependency `--package` asked for
68def known(+only: String, ds: List<&2, M.Dep>) -> Bool:
69  known.go(String.is_empty(only), ds, only)
70
71# a selected git pin with no tag follows the default branch
72def aim.tag(bare: Bool, +tag: String) -> Aim:
73  match bare:
74    case True{}:
75      Forward{}
76    case False{}:
77      Follow{tag}
78
79# a selected hub pin has nothing to follow
80def aim.src(source: M.Source) -> Aim:
81  match source:
82    case M.Hub{_named}:
83      Hold{}
84    case M.Git{_url, _rev, +tag, _root, _nar, _vend}:
85      aim.tag(String.is_empty(tag), tag)
86    case M.Member{_path, _named}:
87      Hold{}
88
89# what this dependency is asked. Unselected dependencies are left alone,
90# including a git one, so `--package` does not move its neighbours.
91def aim(sel: Bool, source: M.Source) -> Aim:
92  match sel:
93    case False{}:
94      Hold{}
95    case True{}:
96      aim.src(source)
97
98# the same commit either still hashes to the pin or it does not
99def judge.same(agree: Bool) -> Verdict:
100  match agree:
101    case True{}:
102      Keep{}
103    case False{}:
104      Drift{}
105
106# a different commit is the upgrade only when it descends from the pin
107def judge.move(onward: Bool, +rev: String, +tag: String) -> Verdict:
108  match onward:
109    case True{}:
110      Advance{rev, tag}
111    case False{}:
112      Off{}
113
114# `same` is the resolved commit against the pin. `onward` is that commit
115# being a descendant of the pin. `agree` is the recomputed hash, NAR hash
116# and root against what the ledger recorded, and it only matters when the
117# commit did not move.
118def judge(same: Bool, onward: Bool, agree: Bool, rev: String, tag: String) -> Verdict:
119  match same:
120    case True{}:
121      judge.same(agree)
122    case False{}:
123      judge.move(onward, rev, tag)
124
125# the three fields a checkout can disagree on, each required
126def agree.nar(same_nar: Bool, same_root: Bool) -> Bool:
127  match same_nar:
128    case False{}:
129      False{}
130    case True{}:
131      same_root
132
133# whether a recomputed pin is the one the ledger already recorded
134def agree.go(same_hash: Bool, same_nar: Bool, same_root: Bool) -> Bool:
135  match same_hash:
136    case False{}:
137      False{}
138    case True{}:
139      agree.nar(same_nar, same_root)
140
141# whether a recomputed pin is the one the ledger already recorded
142def agree(+oh: String, +nh: String, +onar: String, +nnar: String, +oroot: String, +nroot: String) -> Bool:
143  agree.go(String.eq(oh, nh), String.eq(onar, nnar), String.eq(oroot, nroot))
144
145# a hub miss is a different digest only when the body arrived and hashed
146# wrong. A package the hub has never seen is not that.
147def hub.bad(+why: String) -> Bool:
148  String.contains(why, "does not hash")
149
150# the dependency with a new commit written in, and the name, the entry, the
151# tag and the vendor bit left as they were
152def retarget.go(
153  +name: String,
154  +entry: String,
155  source: M.Source,
156  +rev: String,
157  +hash: String,
158  +root: String,
159  +nar: String
160) -> M.Dep:
161  match source:
162    case M.Hub{named}:
163      M.Dep{name, hash, entry, M.Hub{named}}
164    case M.Git{url, _rev, tag, _root, _nar, vend}:
165      M.Dep{name, hash, entry, M.Git{url, rev, tag, root, nar, vend}}
166    case M.Member{path, named}:
167      M.Dep{name, hash, entry, M.Member{path, named}}
168
169# the dependency with a new commit written in
170def retarget(dep: M.Dep, +rev: String, +hash: String, +root: String, +nar: String) -> M.Dep:
171  M.Dep{n, _h, e, s} = dep
172  retarget.go(n, e, s, rev, hash, root, nar)
173
174# what an import line that names a package starts with: `import <hash>/`.
175# `import 0x<hash>/<file>.bend as P` is the only shape bend has, and the match
176# is exact at column 0, so an indented import is not rewritten, and neither is
177# a line that names the hash with no `/` after it.
178def reimport.head(+hash: String) -> String:
179  String.append("import ", String.append(hash, "/"))
180
181# an empty hash names no package, so it rewrites nothing
182def reimport.hit(empty: Bool, +line: String, +old: String) -> Bool:
183  match empty:
184    case True{}:
185      False{}
186    case False{}:
187      String.starts_with(line, reimport.head(old))
188
189# one line, with the branch already chosen. A match cannot scrutinise a call.
190def reimport.line.go(hit: Bool, +line: String, +old: String, +new: String) -> String:
191  match hit:
192    case True{}:
193      String.append(reimport.head(new),
194        String.drop(line, String.length(reimport.head(old))))
195    case False{}:
196      line
197
198# one line, with its import hash rewritten when the line names the old one
199def reimport.line(+line: String, +old: String, +new: String) -> String:
200  reimport.line.go(reimport.hit(String.is_empty(old), line, old), line, old, new)
201
202# one line through every replacement, in order
203def reimport.many.line(ss: List<&2, Swap>, +line: String) -> String:
204  match ss:
205    case []:
206      line
207    case Swap{+old, +new} <> t:
208      reimport.many.line(t, reimport.line(line, old, new))
209
210# every line, in order
211def reimport.many.lines(+ss: List<&2, Swap>, ls: List<&2, String>) -> List<&2, String>:
212  match ls:
213    case []:
214      []
215    case +h <> t:
216      reimport.many.line(ss, h) <> reimport.many.lines(ss, t)
217
218# a source file with every moved import hash replaced, in order. The text is
219# cut at its newlines once and joined back with them, so a line that names no
220# moved hash is left as it was, and so is the newline the file ends with, or
221# its lack of one.
222def reimport.many(+ss: List<&2, Swap>, +text: String) -> String:
223  String.join(reimport.many.lines(ss, String.lines(text)), "\n")