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 87 # what this dependency is asked. Unselected dependencies are left alone, 88 # including a git one, so `--package` does not move its neighbours. 89 def aim(sel: Bool, source: M.Source) -> Aim: 90 match sel: 91 case False{}: 92 Hold{} 93 case True{}: 94 aim.src(source) 95 96 # the same commit either still hashes to the pin or it does not 97 def judge.same(agree: Bool) -> Verdict: 98 match agree: 99 case True{}: 100 Keep{} 101 case False{}: 102 Drift{} 103 104 # a different commit is the upgrade only when it descends from the pin 105 def judge.move(onward: Bool, +rev: String, +tag: String) -> Verdict: 106 match onward: 107 case True{}: 108 Advance{rev, tag} 109 case False{}: 110 Off{} 111 112 # `same` is the resolved commit against the pin. `onward` is that commit 113 # being a descendant of the pin. `agree` is the recomputed hash, NAR hash 114 # and root against what the ledger recorded, and it only matters when the 115 # commit did not move. 116 def judge(same: Bool, onward: Bool, agree: Bool, rev: String, tag: String) -> Verdict: 117 match same: 118 case True{}: 119 judge.same(agree) 120 case False{}: 121 judge.move(onward, rev, tag) 122 123 # the three fields a checkout can disagree on, each required 124 def agree.nar(same_nar: Bool, same_root: Bool) -> Bool: 125 match same_nar: 126 case False{}: 127 False{} 128 case True{}: 129 same_root 130 131 # whether a recomputed pin is the one the ledger already recorded 132 def agree.go(same_hash: Bool, same_nar: Bool, same_root: Bool) -> Bool: 133 match same_hash: 134 case False{}: 135 False{} 136 case True{}: 137 agree.nar(same_nar, same_root) 138 139 # whether a recomputed pin is the one the ledger already recorded 140 def agree(+oh: String, +nh: String, +onar: String, +nnar: String, +oroot: String, +nroot: String) -> Bool: 141 agree.go(String.eq(oh, nh), String.eq(onar, nnar), String.eq(oroot, nroot)) 142 143 # a hub miss is a different digest only when the body arrived and hashed 144 # wrong. A package the hub has never seen is not that. 145 def hub.bad(+why: String) -> Bool: 146 String.contains(why, "does not hash") 147 148 # the dependency with a new commit written in, and the name, the entry, the 149 # tag and the vendor bit left as they were 150 def retarget.go( 151 +name: String, 152 +entry: String, 153 source: M.Source, 154 +rev: String, 155 +hash: String, 156 +root: String, 157 +nar: String 158 ) -> M.Dep: 159 match source: 160 case M.Hub{named}: 161 M.Dep{name, hash, entry, M.Hub{named}} 162 case M.Git{url, _rev, tag, _root, _nar, vend}: 163 M.Dep{name, hash, entry, M.Git{url, rev, tag, root, nar, vend}} 164 165 # the dependency with a new commit written in 166 def retarget(dep: M.Dep, +rev: String, +hash: String, +root: String, +nar: String) -> M.Dep: 167 M.Dep{n, _h, e, s} = dep 168 retarget.go(n, e, s, rev, hash, root, nar) 169 170 # what an import line that names a package starts with: `import /`. 171 # `import 0x/.bend as P` is the only shape bend has, and the match 172 # is exact at column 0, so an indented import is not rewritten, and neither is 173 # a line that names the hash with no `/` after it. 174 def reimport.head(+hash: String) -> String: 175 String.append("import ", String.append(hash, "/")) 176 177 # an empty hash names no package, so it rewrites nothing 178 def reimport.hit(empty: Bool, +line: String, +old: String) -> Bool: 179 match empty: 180 case True{}: 181 False{} 182 case False{}: 183 String.starts_with(line, reimport.head(old)) 184 185 # one line, with the branch already chosen. A match cannot scrutinise a call. 186 def reimport.line.go(hit: Bool, +line: String, +old: String, +new: String) -> String: 187 match hit: 188 case True{}: 189 String.append(reimport.head(new), 190 String.drop(line, String.length(reimport.head(old)))) 191 case False{}: 192 line 193 194 # one line, with its import hash rewritten when the line names the old one 195 def reimport.line(+line: String, +old: String, +new: String) -> String: 196 reimport.line.go(reimport.hit(String.is_empty(old), line, old), line, old, new) 197 198 # one line through every replacement, in order 199 def reimport.many.line(ss: List<&2, Swap>, +line: String) -> String: 200 match ss: 201 case []: 202 line 203 case Swap{+old, +new} <> t: 204 reimport.many.line(t, reimport.line(line, old, new)) 205 206 # every line, in order 207 def reimport.many.lines(+ss: List<&2, Swap>, ls: List<&2, String>) -> List<&2, String>: 208 match ls: 209 case []: 210 [] 211 case +h <> t: 212 reimport.many.line(ss, h) <> reimport.many.lines(ss, t) 213 214 # a source file with every moved import hash replaced, in order. The text is 215 # cut at its newlines once and joined back with them, so a line that names no 216 # moved hash is left as it was, and so is the newline the file ends with, or 217 # its lack of one. 218 def reimport.many(+ss: List<&2, Swap>, +text: String) -> String: 219 String.join(reimport.many.lines(ss, String.lines(text)), "\n")