~/bend-docscommunity

src/ledger/upgrade.bend source

src/ledger/upgrade.bend on the hub · documented module

# ledger/upgrade: what `ez lock --upgrade` may do to one dependency, before# it talks to a remote. The ledger pins exact commits. A tag is the only# floating name a person wrote down, so that is what gets re-resolved. A pin# with no tag fast-forwards to the default branch tip when the commit is an# ancestor of it, and stays a commit pin. A hub package is the hash itself.# `vend` says the tree under `.ez/lib/<hash>` is committed, so a move lays the# new tree there; the allowlist that names it is `ledger/ignore.bend`'s,# derived from the ledger the move writes. A move also rewrites an# `import 0x<hash>/` line that names the old hash.import Baseimport ./manifest.bend as M# what an upgrade will ask the remote, once the ledger has been readtype Aim is Data:  Hold{}  Follow{+ref: String}  Forward{}# what that answer means for the pintype Verdict is Data:  Keep{}  Drift{}  Off{}  Advance{rev: String, tag: String}# one hash that moved, and the hash that replaces it: an import line to# rewrite, or a committed tree to removetype Swap is Data:  Swap{old: String, new: String}# an empty `--package` is every dependency; a name is that onedef chosen.go(all: Bool, +only: String, +name: String) -> Bool:  match all:    case True{}:      True{}    case False{}:      String.eq(only, name)# an empty `--package` is every dependency; a name is that onedef chosen(+only: String, +name: String) -> Bool:  chosen.go(String.is_empty(only), only, name)# the rest of the scan only when this one was not itdef known.step(hit: Bool, rest: Unit -> Bool) -> Bool:  match hit:    case True{}:      True{}    case False{}:      rest(Unit{})# whether the ledger names this dependencydef known.in(ds: List<&2, M.Dep>, +name: String) -> Bool:  match ds:    case []:      False{}    case +h <> t:      known.step(String.eq(M.dep.name(h), name), _u => known.in(t, name))# `--package` with no such dependency is a mistake, and no filter is notdef known.go(all: Bool, ds: List<&2, M.Dep>, +name: String) -> Bool:  match all:    case True{}:      True{}    case False{}:      known.in(ds, name)# whether the ledger names the dependency `--package` asked fordef known(+only: String, ds: List<&2, M.Dep>) -> Bool:  known.go(String.is_empty(only), ds, only)# a selected git pin with no tag follows the default branchdef aim.tag(bare: Bool, +tag: String) -> Aim:  match bare:    case True{}:      Forward{}    case False{}:      Follow{tag}# a selected hub pin has nothing to followdef aim.src(source: M.Source) -> Aim:  match source:    case M.Hub{_named}:      Hold{}    case M.Git{_url, _rev, +tag, _root, _nar, _vend}:      aim.tag(String.is_empty(tag), tag)# what this dependency is asked. Unselected dependencies are left alone,# including a git one, so `--package` does not move its neighbours.def aim(sel: Bool, source: M.Source) -> Aim:  match sel:    case False{}:      Hold{}    case True{}:      aim.src(source)# the same commit either still hashes to the pin or it does notdef judge.same(agree: Bool) -> Verdict:  match agree:    case True{}:      Keep{}    case False{}:      Drift{}# a different commit is the upgrade only when it descends from the pindef judge.move(onward: Bool, +rev: String, +tag: String) -> Verdict:  match onward:    case True{}:      Advance{rev, tag}    case False{}:      Off{}# `same` is the resolved commit against the pin. `onward` is that commit# being a descendant of the pin. `agree` is the recomputed hash, NAR hash# and root against what the ledger recorded, and it only matters when the# commit did not move.def judge(same: Bool, onward: Bool, agree: Bool, rev: String, tag: String) -> Verdict:  match same:    case True{}:      judge.same(agree)    case False{}:      judge.move(onward, rev, tag)# the three fields a checkout can disagree on, each requireddef agree.nar(same_nar: Bool, same_root: Bool) -> Bool:  match same_nar:    case False{}:      False{}    case True{}:      same_root# whether a recomputed pin is the one the ledger already recordeddef agree.go(same_hash: Bool, same_nar: Bool, same_root: Bool) -> Bool:  match same_hash:    case False{}:      False{}    case True{}:      agree.nar(same_nar, same_root)# whether a recomputed pin is the one the ledger already recordeddef agree(+oh: String, +nh: String, +onar: String, +nnar: String, +oroot: String, +nroot: String) -> Bool:  agree.go(String.eq(oh, nh), String.eq(onar, nnar), String.eq(oroot, nroot))# a hub miss is a different digest only when the body arrived and hashed# wrong. A package the hub has never seen is not that.def hub.bad(+why: String) -> Bool:  String.contains(why, "does not hash")# the dependency with a new commit written in, and the name, the entry, the# tag and the vendor bit left as they weredef retarget.go(  +name: String,  +entry: String,  source: M.Source,  +rev: String,  +hash: String,  +root: String,  +nar: String) -> M.Dep:  match source:    case M.Hub{named}:      M.Dep{name, hash, entry, M.Hub{named}}    case M.Git{url, _rev, tag, _root, _nar, vend}:      M.Dep{name, hash, entry, M.Git{url, rev, tag, root, nar, vend}}# the dependency with a new commit written indef retarget(dep: M.Dep, +rev: String, +hash: String, +root: String, +nar: String) -> M.Dep:  M.Dep{n, _h, e, s} = dep  retarget.go(n, e, s, rev, hash, root, nar)# what an import line that names a package starts with: `import <hash>/`.# `import 0x<hash>/<file>.bend as P` is the only shape bend has, and the match# is exact at column 0, so an indented import is not rewritten, and neither is# a line that names the hash with no `/` after it.def reimport.head(+hash: String) -> String:  String.append("import ", String.append(hash, "/"))# an empty hash names no package, so it rewrites nothingdef reimport.hit(empty: Bool, +line: String, +old: String) -> Bool:  match empty:    case True{}:      False{}    case False{}:      String.starts_with(line, reimport.head(old))# one line, with the branch already chosen. A match cannot scrutinise a call.def reimport.line.go(hit: Bool, +line: String, +old: String, +new: String) -> String:  match hit:    case True{}:      String.append(reimport.head(new),        String.drop(line, String.length(reimport.head(old))))    case False{}:      line# one line, with its import hash rewritten when the line names the old onedef reimport.line(+line: String, +old: String, +new: String) -> String:  reimport.line.go(reimport.hit(String.is_empty(old), line, old), line, old, new)# one line through every replacement, in orderdef reimport.many.line(ss: List<&2, Swap>, +line: String) -> String:  match ss:    case []:      line    case Swap{+old, +new} <> t:      reimport.many.line(t, reimport.line(line, old, new))# every line, in orderdef reimport.many.lines(+ss: List<&2, Swap>, ls: List<&2, String>) -> List<&2, String>:  match ls:    case []:      []    case +h <> t:      reimport.many.line(ss, h) <> reimport.many.lines(ss, t)# a source file with every moved import hash replaced, in order. The text is# cut at its newlines once and joined back with them, so a line that names no# moved hash is left as it was, and so is the newline the file ends with, or# its lack of one.def reimport.many(+ss: List<&2, Swap>, +text: String) -> String:  String.join(reimport.many.lines(ss, String.lines(text)), "\n")