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/` 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/` line that names the old hash. 10 import Base 11 import ./manifest.bend as M 12 13 # what an upgrade will ask the remote, once the ledger has been read 14 type Aim is Data: 15 Hold{} 16 Follow{+ref: String} 17 Forward{} 18 19 # what that answer means for the pin 20 type 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 28 type Swap is Data: 29 Swap{old: String, new: String} 30 31 # an empty `--package` is every dependency; a name is that one 32 def 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 40 def 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 44 def 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 52 def 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 60 def 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 68 def 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 72 def 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 80 def 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. 91 def 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 99 def 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 107 def 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. 118 def 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 126 def 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 134 def 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 142 def 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. 147 def 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 152 def 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 170 def 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 /`. 175 # `import 0x/.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. 178 def reimport.head(+hash: String) -> String: 179 String.append("import ", String.append(hash, "/")) 180 181 # an empty hash names no package, so it rewrites nothing 182 def 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. 190 def 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 199 def 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 203 def 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 211 def 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. 222 def reimport.many(+ss: List<&2, Swap>, +text: String) -> String: 223 String.join(reimport.many.lines(ss, String.lines(text)), "\n")