1 # git/git: what ez asks of git, and how it reads the answers. The decisions are 2 # pure functions over what `git ls-remote` printed and what a checkout's 3 # ez.toml says: which tag `ez add` pins (`choose`), what a named ref names 4 # (`exact`), the default branch (`tip.*`), whether a ref is already a commit 5 # (`is_rev`), and a package's entry and name (`entry.*`, `called.*`). The 6 # planners (add/plan.bend, lock/up.bend) make them; the commands that run git 7 # are the interpreters', which clone, weigh and ask about ancestry, and they 8 # live in git/exec.bend, since they reach foreign code a law may not import. 9 # 10 # Nothing here reimplements git. Bend links no TLS and Base has no readdir, so 11 # the programs that already speak the protocol are run instead, through the one 12 # process effect the rest of ez uses: arguments execvp'd, no shell between. 13 import Base 14 import ../io/file.bend as F 15 import ../toml/toml.bend as T 16 import ../ledger/manifest.bend as M 17 import ../pkg/pkg.bend as K 18 import ../pkg/path.bend as P 19 20 # one `\t` row of what `git ls-remote` printed 21 type Row is Data: 22 Row{rev: String, name: String} 23 24 # how two versions compare. A release is greater than its prerelease, and a 25 # numeric identifier is less than a word, which is the semver order. 26 type Ord is Data: 27 Lt{} 28 Eq{} 29 Gt{} 30 31 # one prerelease identifier 32 type Id is Data: 33 IdNum{n: Nat} 34 IdWord{s: String} 35 36 # a semver-ish tag, or a name that is not one. Build metadata is not kept: 37 # it does not change which tag is newer. 38 type Ver is Data: 39 No{} 40 Yes{nums: List<&2, Nat>, pre: List<&2, Id>} 41 42 # a string split once, on the first time a char occurs 43 type Cut is Data: 44 Cut{left: String, right: String} 45 46 # one step of a scan looking for one hit, with the head's answer in hand. 47 # `Bool.or` is an ordinary function and reduces both of its sides, so the 48 # alphabet was read to its end for every char of every ref. The rest of the 49 # scan arrives as a thunk, which is a value, and only the arm that wants it 50 # applies it. 51 def among.step(here: Bool, rest: Unit -> Bool) -> Bool: 52 match here: 53 case True{}: 54 True{} 55 case False{}: 56 rest(Unit{}) 57 58 # whether a char is one of these. Base has `Char.is_digit` and `Char.is_alpha` 59 # and nothing between, and a commit name is written with neither alphabet but 60 # with sixteen characters of its own, so the sixteen are named. 61 def among(+ch: Char, cs: List<&2, Char>) -> Bool: 62 match cs: 63 case []: 64 False{} 65 case +h <> t: 66 among.step(Char.is_eq(h, ch), _u => among(ch, t)) 67 68 # a char a commit name may hold 69 def hex.at(ch: Char) -> Bool: 70 among(ch, String.to_list("0123456789abcdef")) 71 72 # one step of the walk, and `among.step` turned around: `Bool.and` reduces 73 # both of its sides too, so the first char that is not hex still cost a walk 74 # of the rest of the ref 75 def hex.all.step(here: Bool, rest: Unit -> Bool) -> Bool: 76 match here: 77 case True{}: 78 rest(Unit{}) 79 case False{}: 80 False{} 81 82 # whether every char of a ref is one. Base's `List.all` applies an erased 83 # function parameter, which falls outside the termination check; this walk is 84 # structural. 85 def hex.all(cs: List<&2, Char>) -> Bool: 86 match cs: 87 case []: 88 True{} 89 case +h <> t: 90 hex.all.step(hex.at(h), _u => hex.all(t)) 91 92 # a ref that is already the commit it names, which is what `git rev-parse` 93 # prints: forty hex characters and nothing else. Such a ref needs no remote 94 # asked about it, and leaves no tag behind to re-resolve later. 95 def is_rev(+ref: String) -> Bool: 96 Bool.and(Nat.is_eq(String.length(ref), 40n), hex.all(String.to_list(ref))) 97 98 # a row's commit 99 def row.rev(line: Row) -> String: 100 Row{rev, _name} = line 101 rev 102 103 # a row's ref name 104 def row.name(line: Row) -> String: 105 Row{_rev, name} = line 106 name 107 108 # one row taken apart. ls-remote separates the two fields with a tab. 109 def row_of(+text: String) -> Row: 110 Row{K.word(String.split(text, '\t'), 0n), K.word(String.split(text, '\t'), 1n)} 111 112 # a line that says nothing is no row 113 def row.put(+text: String) -> List<&2, Row>: 114 Bool.pick(List<&2, Row>, String.is_empty(String.trim(text)), [], [row_of(text)]) 115 116 # every row of what ls-remote printed 117 def rows(ls: List<&2, String>) -> List<&2, Row>: 118 match ls: 119 case []: 120 [] 121 case h <> t: 122 List.append(&2, Row, row.put(h), rows(t)) 123 124 # one step of the search, with the head's answer in hand. A recursive call in 125 # an arm of a `Bool.pick` is not an arm at all: it is built whatever the 126 # condition says, so the first row that answered still cost a walk of every 127 # row after it. 128 def named.step(here: Bool, rev: String, rest: Unit -> String) -> String: 129 match here: 130 case True{}: 131 rev 132 case False{}: 133 rest(Unit{}) 134 135 # the commit of the first row whose ref name is exactly `want`, or "" when no 136 # row is. The whole name is compared: `git ls-remote` matches its patterns 137 # against the tail of a ref, so it answers `refs/heads/main` with 138 # `refs/heads/feature/main` as well, and only this comparison tells them apart. 139 def named(rs: List<&2, Row>, +want: String) -> String: 140 match rs: 141 case []: 142 "" 143 case +h <> t: 144 named.step(String.eq(row.name(h), want), row.rev(h), _u => named(t, want)) 145 146 # the first of two answers that says anything 147 def or_else(+primary: String, fallback: String) -> String: 148 Bool.pick(String, String.is_empty(primary), fallback, primary) 149 150 # the commit a ref names, from the rows the remote answered with. A tag comes 151 # first, through its peeled `^{}` row when it is annotated, since that row is 152 # the commit and the other is the tag object; then a branch of that name. No 153 # other row is ever picked, so a tag and a branch that share a name resolve to 154 # the tag, and `main` never resolves to `feature/main`. 155 def exact(+rs: List<&2, Row>, +ref: String) -> String: 156 or_else(named(rs, "refs/tags/" ++ ref ++ "^{}"), 157 or_else(named(rs, "refs/tags/" ++ ref), named(rs, "refs/heads/" ++ ref))) 158 159 # that commit is the tip of 160 type Tip is Data: 161 Tip{rev: String, branch: String} 162 163 # the rest of the scan only when this row said nothing. A recursive call 164 # written as an argument of `Bool.pick` would run for a row that already 165 # answered. 166 def tip.step(empty: Bool, +got: String, rest: Unit -> String) -> String: 167 match empty: 168 case True{}: 169 rest(Unit{}) 170 case False{}: 171 got 172 173 # a HEAD row whose first field is already a commit 174 def tip.rev.hex(hex: Bool, rev: String) -> String: 175 match hex: 176 case True{}: 177 rev 178 case False{}: 179 "" 180 181 # a HEAD row's commit, or "" when the row is the symref or not HEAD 182 def tip.rev.pick(head: Bool, hex: Bool, rev: String) -> String: 183 match head: 184 case False{}: 185 "" 186 case True{}: 187 tip.rev.hex(hex, rev) 188 189 # this row's commit when it is HEAD's commit 190 def tip.rev.at(line: Row) -> String: 191 Row{+rev, name} = line 192 tip.rev.pick(String.eq(name, "HEAD"), is_rev(rev), rev) 193 194 # the commit HEAD names, from the rows `ls-remote --symref` wrote 195 def tip.rev(rs: List<&2, Row>) -> String: 196 match rs: 197 case []: 198 "" 199 case +h <> t: 200 tip.step(String.is_empty(tip.rev.at(h)), tip.rev.at(h), _u => tip.rev(t)) 201 202 # the prefix taken off when it is there, and "" when it is not, so a commit 203 # line is not mistaken for a branch name 204 def tip.strip(+text: String, +pre: String) -> String: 205 Bool.pick(String, String.starts_with(text, pre), 206 String.drop(text, String.length(pre)), "") 207 208 # `ref: refs/heads/main` is `main`. Anything else is no branch. 209 def tip.sym(+rev: String) -> String: 210 tip.strip(tip.strip(rev, "ref: "), "refs/heads/") 211 212 # the symref only, so the commit row that follows it contributes nothing 213 def tip.branch.pick(head: Bool, rev: String) -> String: 214 match head: 215 case False{}: 216 "" 217 case True{}: 218 tip.sym(rev) 219 220 # this row's branch when it is the symref for HEAD 221 def tip.branch.at(line: Row) -> String: 222 Row{rev, name} = line 223 tip.branch.pick(String.eq(name, "HEAD"), rev) 224 225 # the branch HEAD is, or "" when the remote did not say 226 def tip.branch(rs: List<&2, Row>) -> String: 227 match rs: 228 case []: 229 "" 230 case +h <> t: 231 tip.step(String.is_empty(tip.branch.at(h)), tip.branch.at(h), _u => tip.branch(t)) 232 233 # how many components a path has, which is how far down it sits 234 def depth(path: String) -> Nat: 235 List.length(&2, String, String.split(P.norm(path), '/')) 236 237 # the directory inside the repo a package's paths are written from. `pkg/pkg` 238 # resolves that against the checkout it read; the lock records it against the 239 # repo, so the same number of components is climbed back off the entry's own 240 # directory here. It is "." until a module reached through `..` re-roots the 241 # package above the entry. 242 def root.rel(+at: String, root: String, +entry: String) -> String: 243 P.climb(Nat.sub(depth(P.dir(at)), depth(root)), P.dir(entry)) 244 245 # the first char of a name in upper case, which is how bend writes an alias 246 def title.go(cs: List<&2, Char>) -> String: 247 match cs: 248 case []: 249 "" 250 case h <> t: 251 String.from_list(Char.to_upper(h) <> t) 252 253 # a name as an import alias, as `ez add` and `ez publish` print it 254 def title(text: String) -> String: 255 title.go(String.to_list(text)) 256 257 # the import line a consumer writes for a package: the hash, the entry's path 258 # inside the package, which is where the hub serves it, and the entry's name 259 # as the alias, the way bend prints it. `ez add` and `ez publish` both print 260 # this line. 261 def import.line(+hash: String, +at: String) -> String: 262 "import " ++ hash ++ "/" ++ at ++ " as " ++ title(P.unbend(P.base(at))) 263 264 # `main.bend` when neither the entry nor the bin was named. That is the file 265 # `ez init` writes when it is not told an entry. 266 def entry.main(no: Bool, +binary: String) -> String: 267 match no: 268 case False{}: 269 binary 270 case True{}: 271 "main.bend" 272 273 # `bin` only when the package named no entry. The bin is the file a tool 274 # builds; it is not the entry unless the entry is absent. 275 def entry.bin(no: Bool, +file: String, +binary: String) -> String: 276 match no: 277 case False{}: 278 file 279 case True{}: 280 entry.main(String.is_empty(binary), binary) 281 282 # the file a package is taken from: its entry, else its bin, else `main.bend` 283 def entry.pick(+file: String, +binary: String) -> String: 284 entry.bin(String.is_empty(file), file, binary) 285 286 # a document that did not parse has no entry to take 287 def entry.sects(ok: Bool, ss: List<&2, T.Sect>) -> String: 288 match ok: 289 case False{}: 290 "main.bend" 291 case True{}: 292 +ps = M.pairs_of(ss, "package") 293 entry.pick(M.value(ps, "entry"), M.value(ps, "bin")) 294 295 # the entry a parsed document names 296 def entry.doc(toml: T.Toml) -> String: 297 T.Toml{bad, _name, _pairs, sects} = toml 298 entry.sects(String.is_empty(bad), sects) 299 300 # the entry a parsed ez.toml names 301 def entry.text(+text: String) -> String: 302 entry.doc(T.parse(text)) 303 304 # a missing ez.toml names `main.bend`, the same stub `ez init` uses 305 def entry.maybe(got: Maybe<&2, String>) -> String: 306 match got: 307 case None{}: 308 "main.bend" 309 case Some{text}: 310 entry.text(text) 311 312 # the checkout's ez.toml, read for the entry it names 313 def entry.at(+work: String) -> IO(String): 314 do IO: 315 m : Maybe<&2, String> <- F.read(work ++ "/ez.toml") 316 return entry.maybe(m) 317 318 # the package name a parsed document gives, or "" when it did not parse 319 def called.sects(ok: Bool, ss: List<&2, T.Sect>) -> String: 320 match ok: 321 case False{}: 322 "" 323 case True{}: 324 M.value(M.pairs_of(ss, "package"), "name") 325 326 # the package name a parsed document gives 327 def called.doc(toml: T.Toml) -> String: 328 T.Toml{bad, _name, _pairs, sects} = toml 329 called.sects(String.is_empty(bad), sects) 330 331 # a checkout with no ez.toml gives no package name 332 def called.maybe(got: Maybe<&2, String>) -> String: 333 match got: 334 case None{}: 335 "" 336 case Some{text}: 337 called.doc(T.parse(text)) 338 339 # the entry a vendoring is taken from. One that was asked for is that one. 340 def entry.for(given: Bool, +entry: String, +work: String) -> IO(String): 341 match given: 342 case True{}: 343 IO.pure(String, entry) 344 case False{}: 345 entry.at(work) 346 347 # A tag `ez add` may pin when no ref was asked for. It is semver-ish when, 348 # after an optional `v`, it is dotted numbers, then an optional prerelease 349 # of dot-separated identifiers, then optional `+` build metadata. Identifiers 350 # are letters, digits and hyphens. Comparison is semver: numbers beat lexical 351 # order, a longer prerelease beats a shorter one that matches up to there, 352 # and a release beats its prerelease. Anything else is not a candidate. 353 354 def ord.lt(lt: Bool) -> Ord: 355 match lt: 356 case True{}: 357 Lt{} 358 case False{}: 359 Gt{} 360 361 def ord.nat(eq: Bool, lt: Bool) -> Ord: 362 match eq: 363 case True{}: 364 Eq{} 365 case False{}: 366 ord.lt(lt) 367 368 def ord.str(eq: Bool, le: Bool) -> Ord: 369 match eq: 370 case True{}: 371 Eq{} 372 case False{}: 373 ord.lt(le) 374 375 def ord.flip(order: Ord) -> Ord: 376 match order: 377 case Lt{}: 378 Gt{} 379 case Gt{}: 380 Lt{} 381 case Eq{}: 382 Eq{} 383 384 def ord.pad.at(zero: Bool, rest: Unit -> Ord) -> Ord: 385 match zero: 386 case False{}: 387 Lt{} 388 case True{}: 389 rest(Unit{}) 390 391 def ord.pad(xs: List<&2, Nat>) -> Ord: 392 match xs: 393 case Nil{}: 394 Eq{} 395 case Con{+h, t}: 396 ord.pad.at(Nat.is_eq(h, 0n), _u => ord.pad(t)) 397 398 def ord.nums.at(order: Ord, rest: Unit -> Ord) -> Ord: 399 match order: 400 case Eq{}: 401 rest(Unit{}) 402 case Lt{}: 403 Lt{} 404 case Gt{}: 405 Gt{} 406 407 def ord.nums(left: List<&2, Nat>, right: List<&2, Nat>) -> Ord: 408 match left right: 409 case Nil{} Nil{}: 410 Eq{} 411 case Nil{} Con{+h, t}: 412 ord.pad(h <> t) 413 case Con{+h, t} Nil{}: 414 ord.flip(ord.pad(h <> t)) 415 case Con{+ha, ta} Con{+hb, tb}: 416 ord.nums.at(ord.nat(Nat.is_eq(ha, hb), Nat.is_lt(ha, hb)), 417 _u => ord.nums(ta, tb)) 418 419 def ord.id(left: Id, right: Id) -> Ord: 420 match left right: 421 case IdNum{+n} IdNum{+m}: 422 ord.nat(Nat.is_eq(n, m), Nat.is_lt(n, m)) 423 case IdNum{_n} IdWord{_s}: 424 Lt{} 425 case IdWord{_s} IdNum{_n}: 426 Gt{} 427 case IdWord{+s} IdWord{+t}: 428 ord.str(String.eq(s, t), String.is_le(s, t)) 429 430 def ord.ids(left: List<&2, Id>, right: List<&2, Id>) -> Ord: 431 match left right: 432 case Nil{} Nil{}: 433 Eq{} 434 case Nil{} Con{_h, _t}: 435 Lt{} 436 case Con{_h, _t} Nil{}: 437 Gt{} 438 case Con{ha, ta} Con{hb, tb}: 439 ord.nums.at(ord.id(ha, hb), _u => ord.ids(ta, tb)) 440 441 def ord.rel.at(ae: Bool, be: Bool, left: List<&2, Id>, right: List<&2, Id>) -> Ord: 442 match ae be: 443 case True{} True{}: 444 Eq{} 445 case True{} False{}: 446 Gt{} 447 case False{} True{}: 448 Lt{} 449 case False{} False{}: 450 ord.ids(left, right) 451 452 def ord.rel(+left: List<&2, Id>, +right: List<&2, Id>) -> Ord: 453 ord.rel.at(List.is_empty(&2, Id, left), List.is_empty(&2, Id, right), left, right) 454 455 def ord.ver.at(order: Ord, pa: List<&2, Id>, pb: List<&2, Id>) -> Ord: 456 ord.nums.at(order, _u => ord.rel(pa, pb)) 457 458 def ord.ver(left: Ver, right: Ver) -> Ord: 459 match left right: 460 case Yes{na, pa} Yes{nb, pb}: 461 ord.ver.at(ord.nums(na, nb), pa, pb) 462 case No{} _: 463 Eq{} 464 case Yes{_na, _pa} No{}: 465 Eq{} 466 467 # a decimal digit's value 468 def dig(ch: Char) -> Nat: 469 U32.to_nat(U32.sub(Char.to_u32(ch), 48)) 470 471 def num.go(text: String, +acc: Nat) -> Nat: 472 match text: 473 case SNil{}: 474 acc 475 case SCon{+h, t}: 476 num.go(t, Nat.add(Nat.mul(acc, 10n), dig(h))) 477 478 def flag.step(ok: Bool, rest: Unit -> Bool) -> Bool: 479 match ok: 480 case False{}: 481 False{} 482 case True{}: 483 rest(Unit{}) 484 485 def digits.go(text: String) -> Bool: 486 match text: 487 case SNil{}: 488 True{} 489 case SCon{+h, t}: 490 flag.step(Char.is_digit(h), _u => digits.go(t)) 491 492 def digits.at(empty: Bool, +text: String) -> Bool: 493 match empty: 494 case True{}: 495 False{} 496 case False{}: 497 digits.go(text) 498 499 # a non-empty run of decimal digits 500 def digits(+text: String) -> Bool: 501 digits.at(String.is_empty(text), text) 502 503 def nums.cons(value: Nat, rest: Maybe<&2, List<&2, Nat>>) -> Maybe<&2, List<&2, Nat>>: 504 match rest: 505 case None{}: 506 None{} 507 case Some{ns}: 508 Some{value <> ns} 509 510 def nums.put(ok: Bool, +part: String, rest: Maybe<&2, List<&2, Nat>>) -> Maybe<&2, List<&2, Nat>>: 511 match ok: 512 case False{}: 513 None{} 514 case True{}: 515 nums.cons(num.go(part, 0n), rest) 516 517 def nums.of(ps: List<&2, String>) -> Maybe<&2, List<&2, Nat>>: 518 match ps: 519 case Nil{}: 520 Some{[]} 521 case Con{+h, t}: 522 nums.put(digits(h), h, nums.of(t)) 523 524 def id.char(+ch: Char) -> Bool: 525 Bool.or(Char.is_digit(ch), Bool.or(Char.is_alpha(ch), Char.is_eq(ch, '-'))) 526 527 def id.chars(text: String) -> Bool: 528 match text: 529 case SNil{}: 530 True{} 531 case SCon{+h, t}: 532 flag.step(id.char(h), _u => id.chars(t)) 533 534 def id.make(num: Bool, +text: String) -> Id: 535 match num: 536 case True{}: 537 IdNum{num.go(text, 0n)} 538 case False{}: 539 IdWord{text} 540 541 def ids.cons(ident: Id, rest: Maybe<&2, List<&2, Id>>) -> Maybe<&2, List<&2, Id>>: 542 match rest: 543 case None{}: 544 None{} 545 case Some{xs}: 546 Some{ident <> xs} 547 548 def ids.ok(ok: Bool, num: Bool, +part: String, rest: Maybe<&2, List<&2, Id>>) -> Maybe<&2, List<&2, Id>>: 549 match ok: 550 case False{}: 551 None{} 552 case True{}: 553 ids.cons(id.make(num, part), rest) 554 555 def ids.put(empty: Bool, ok: Bool, num: Bool, +part: String, rest: Maybe<&2, List<&2, Id>>) -> Maybe<&2, List<&2, Id>>: 556 match empty: 557 case True{}: 558 None{} 559 case False{}: 560 ids.ok(ok, num, part, rest) 561 562 def ids.of(ps: List<&2, String>) -> Maybe<&2, List<&2, Id>>: 563 match ps: 564 case Nil{}: 565 Some{[]} 566 case Con{+h, t}: 567 ids.put(String.is_empty(h), id.chars(h), digits(h), h, ids.of(t)) 568 569 def pre.parts(empty: Bool, +raw: String) -> Maybe<&2, List<&2, Id>>: 570 match empty: 571 case True{}: 572 None{} 573 case False{}: 574 ids.of(String.split(raw, '.')) 575 576 def pre.of(had: Bool, +raw: String) -> Maybe<&2, List<&2, Id>>: 577 match had: 578 case False{}: 579 Some{[]} 580 case True{}: 581 pre.parts(String.is_empty(raw), raw) 582 583 def split1.put(ch: Char, cut: Cut) -> Cut: 584 Cut{left, right} = cut 585 Cut{SCon{ch, left}, right} 586 587 def split1.step(hit: Bool, ch: Char, +tail: String, rest: Unit -> Cut) -> Cut: 588 match hit: 589 case True{}: 590 Cut{"", tail} 591 case False{}: 592 split1.put(ch, rest(Unit{})) 593 594 # a string cut at the first of a character, the character dropped; no hit leaves 595 # the whole string on the left 596 def split1(+sep: Char, text: String) -> Cut: 597 match text: 598 case SNil{}: 599 Cut{"", ""} 600 case SCon{+h, +t}: 601 split1.step(Char.is_eq(h, sep), h, t, _u => split1(sep, t)) 602 603 def unv.step(drop: Bool, ch: Char, tail: String) -> String: 604 match drop: 605 case True{}: 606 tail 607 case False{}: 608 SCon{ch, tail} 609 610 # a tag with one leading `v` or `V` dropped 611 def unv(text: String) -> String: 612 match text: 613 case SNil{}: 614 "" 615 case SCon{+h, t}: 616 unv.step(Bool.or(Char.is_eq(h, 'v'), Char.is_eq(h, 'V')), h, t) 617 618 def ver.yes(empty: Bool, xs: List<&2, Nat>, ys: List<&2, Id>) -> Ver: 619 match empty: 620 case True{}: 621 No{} 622 case False{}: 623 Yes{xs, ys} 624 625 def ver.pre(+xs: List<&2, Nat>, ps: Maybe<&2, List<&2, Id>>) -> Ver: 626 match ps: 627 case None{}: 628 No{} 629 case Some{ys}: 630 ver.yes(List.is_empty(&2, Nat, xs), xs, ys) 631 632 def ver.nums(had: Bool, ns: Maybe<&2, List<&2, Nat>>, +pre: String) -> Ver: 633 match ns: 634 case None{}: 635 No{} 636 case Some{xs}: 637 ver.pre(xs, pre.of(had, pre)) 638 639 def ver.from(empty: Bool, had: Bool, +core: String, +pre: String) -> Ver: 640 match empty: 641 case True{}: 642 No{} 643 case False{}: 644 ver.nums(had, nums.of(String.split(core, '.')), pre) 645 646 def ver.cut(had: Bool, cut: Cut) -> Ver: 647 Cut{+core, +pre} = cut 648 ver.from(String.is_empty(core), had, core, pre) 649 650 def ver.main(+main: String) -> Ver: 651 ver.cut(String.contains(main, "-"), split1('-', main)) 652 653 def ver.core(cut: Cut) -> Ver: 654 Cut{+main, _build} = cut 655 ver.main(main) 656 657 # a tag read as a version. A leading `v` is optional, and `+` metadata is dropped. 658 def ver.of(+tag: String) -> Ver: 659 ver.core(split1('+', unv(tag))) 660 661 # whether the first version is strictly newer 662 def newer(order: Ord) -> Bool: 663 match order: 664 case Gt{}: 665 True{} 666 case Lt{}: 667 False{} 668 case Eq{}: 669 False{} 670 671 def latest.cmp(take: Bool, +name: String, +rest: String) -> String: 672 match take: 673 case True{}: 674 name 675 case False{}: 676 rest 677 678 def latest.have(empty: Bool, +name: String, +rest: String, version: Ver) -> String: 679 match empty: 680 case True{}: 681 name 682 case False{}: 683 latest.cmp(newer(ord.ver(version, ver.of(rest))), name, rest) 684 685 def latest.step(version: Ver, +name: String, +rest: String) -> String: 686 match version: 687 case No{}: 688 rest 689 case Yes{ns, ps}: 690 latest.have(String.is_empty(rest), name, rest, Yes{ns, ps}) 691 692 # the newest semver-ish name, or "" when none of them is one 693 def latest(names: List<&2, String>) -> String: 694 match names: 695 case Nil{}: 696 "" 697 case Con{+h, t}: 698 latest.step(ver.of(h), h, latest(t)) 699 700 # a version with no pre-release, which is a release 701 def release.ver(version: Ver) -> Bool: 702 match version: 703 case No{}: 704 False{} 705 case Yes{_ns, ps}: 706 List.is_empty(&2, Id, ps) 707 708 # a semver-ish tag that is a release: `v1.9.0`, not `v2.0.0-rc1` 709 def release(+name: String) -> Bool: 710 release.ver(ver.of(name)) 711 712 # a name that is a release, weighed against the greatest release after it the 713 # way `latest` weighs any two versions; a name that is not is passed over 714 def latest.rel.step(keep: Bool, +name: String, +rest: String) -> String: 715 match keep: 716 case False{}: 717 rest 718 case True{}: 719 latest.have(String.is_empty(rest), name, rest, ver.of(name)) 720 721 # the greatest release, or "" when no name is one 722 def latest.rel(names: List<&2, String>) -> String: 723 match names: 724 case Nil{}: 725 "" 726 case Con{+h, t}: 727 latest.rel.step(release(h), h, latest.rel(t)) 728 729 # one name found to be a release, or the rest still to be asked about 730 def has.release.step(here: Bool, rest: Unit -> Bool) -> Bool: 731 match here: 732 case True{}: 733 True{} 734 case False{}: 735 rest(Unit{}) 736 737 # whether any name is a release 738 def has.release(names: List<&2, String>) -> Bool: 739 match names: 740 case Nil{}: 741 False{} 742 case Con{+h, t}: 743 has.release.step(release(h), _u => has.release(t)) 744 745 # the greatest release when there is one, and otherwise the greatest of what 746 # is left, every one of which is a pre-release 747 def choose.go(none: Bool, +rel: String, +any: String) -> String: 748 match none: 749 case True{}: 750 any 751 case False{}: 752 rel 753 754 # the tag `ez add` pins when no ref was asked for: a release beats every 755 # pre-release, however much greater the pre-release's numbers are, and 756 # within each class the greatest wins by `ord.ver`, the later of two equal 757 # ones winning as `latest` has it. "" when no name is semver-ish. 758 def choose(+names: List<&2, String>) -> String: 759 choose.go(String.is_empty(latest.rel(names)), latest.rel(names), latest(names)) 760 761 762 # one row's tag name, or nothing when the row is peeled or not a tag 763 def tag.put(hit: Bool, +name: String) -> List<&2, String>: 764 match hit: 765 case False{}: 766 [] 767 case True{}: 768 [String.drop(name, String.length("refs/tags/"))] 769 770 def tag.keep(peeled: Bool, +name: String) -> List<&2, String>: 771 match peeled: 772 case True{}: 773 [] 774 case False{}: 775 tag.put(String.starts_with(name, "refs/tags/"), name) 776 777 def tag.one(line: Row) -> List<&2, String>: 778 Row{_rev, +name} = line 779 tag.keep(String.ends_with(name, "^{}"), name) 780 781 # every tag name `ls-remote --tags` wrote, peeled rows left out 782 def tag.names(rs: List<&2, Row>) -> List<&2, String>: 783 match rs: 784 case Nil{}: 785 [] 786 case Con{+h, t}: 787 List.append(&2, String, tag.one(h), tag.names(t))