src/add/plan.bend
ezx@1.11.0.0 · 830 lines · 102 definitions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41import Base
42import ezx@1.11.0.0/src/add/world.bend as A
43import ezx@1.11.0.0/src/add/hub.bend as H
44import ezx@1.11.0.0/src/lock/plan.bend as P
45import ezx@1.11.0.0/src/lock/world.bend as W
46import ezx@1.11.0.0/src/lock/up.bend as Up
47import ezx@1.11.0.0/src/ledger/manifest.bend as M
48import ezx@1.11.0.0/src/ledger/render.bend as Rend
49import ezx@1.11.0.0/src/ledger/ignore.bend as I
50import ezx@1.11.0.0/src/pkg/pkg.bend as K
51import ezx@1.11.0.0/src/pkg/path.bend as Path
52import ezx@1.11.0.0/src/git/git.bend as Git
53import ezx@1.11.0.0/src/ez/named.bend as Named
54import ezx@1.11.0.0/src/ez/target.bend as Tgt
55
56
57
58
59
60
61type Q is Data:
62 QTags{}
63 QHead{}
64 QRefs{ref: String}
65 QClone{rev: String}
66
67
68def ask.of(question: Q, +from: String) -> Up.Ask:
69 match question:
70 case QTags{}:
71 Up.Tags{from}
72 case QHead{}:
73 Up.Head{from}
74 case QRefs{ref}:
75 Up.Refs{from, ref}
76 case QClone{rev}:
77 Up.Clone{from, rev}
78
79
80def asks.of(qs: List<&2, Q>, +from: String) -> List<&2, Up.Ask>:
81 match qs:
82 case []:
83 []
84 case h <> t:
85 ask.of(h, from) <> asks.of(t, from)
86
87
88def said(+world: A.World, question: Q) -> Up.Said:
89 Up.printed(A.replies(world), ask.of(question, A.from(world)), "ls-remote " ++ A.url(world))
90
91
92def said.text(printed: Up.Said) -> String:
93 match printed:
94 case Up.Said{text}:
95 text
96 case Up.Unsaid{_why}:
97 ""
98 case Up.Unasked{_ask}:
99 ""
100
101
102def rows(+world: A.World, question: Q) -> List<&2, Git.Row>:
103 Git.rows(String.lines(said.text(said(world, question))))
104
105
106
107
108
109
110
111def default.ref(+world: A.World) -> String:
112 +chosen = Git.choose(Git.tag.names(rows(world, QTags{})))
113 Bool.pick(String, String.is_empty(chosen), Git.tip.branch(rows(world, QHead{})), chosen)
114
115
116
117def tag.of(rev: Bool, empty: Bool, +world: A.World) -> String:
118 match rev:
119 case True{}:
120 ""
121 case False{}:
122 Bool.pick(String, empty, default.ref(world), A.ref(world))
123
124
125def tag(+world: A.World) -> String:
126 tag.of(Git.is_rev(A.ref(world)), String.is_empty(A.ref(world)), world)
127
128
129type Tip is Data:
130 At{rev: String}
131 TipAsk{q: Q}
132 TipNo{why: String}
133
134
135def tip.found(+rev: String, why: String) -> Tip:
136 Bool.pick(Tip, String.is_empty(rev), TipNo{why}, At{rev})
137
138
139
140def tip.refs(printed: Up.Said, +world: A.World) -> Tip:
141 match printed:
142 case Up.Said{text}:
143 tip.found(Git.exact(Git.rows(String.lines(text)), A.ref(world)),
144 "ez: " ++ A.url(world) ++ " has no ref named " ++ A.ref(world))
145 case Up.Unsaid{why}:
146 TipNo{why}
147 case Up.Unasked{_ask}:
148 TipAsk{QRefs{A.ref(world)}}
149
150
151
152def tip.head.rows(+rows: List<&2, Git.Row>, +world: A.World) -> Tip:
153 Bool.pick(Tip, String.is_empty(Git.tip.branch(rows)),
154 TipNo{"ez: " ++ A.url(world) ++
155 " has no semver-ish tag and its HEAD names no branch; name a ref to pin"},
156 tip.found(Git.tip.rev(rows), "ez: " ++ A.url(world) ++ " has no HEAD"))
157
158def tip.head(printed: Up.Said, +world: A.World) -> Tip:
159 match printed:
160 case Up.Said{text}:
161 tip.head.rows(Git.rows(String.lines(text)), world)
162 case Up.Unsaid{why}:
163 TipNo{why}
164 case Up.Unasked{_ask}:
165 TipAsk{QHead{}}
166
167
168def tip.chosen(none: Bool, +chosen: String, +rows: List<&2, Git.Row>, +world: A.World) -> Tip:
169 match none:
170 case False{}:
171 tip.found(Git.exact(rows, chosen),
172 "ez: " ++ A.url(world) ++ " has no tag named " ++ chosen)
173 case True{}:
174 tip.head(said(world, QHead{}), world)
175
176def tip.tags.rows(+rows: List<&2, Git.Row>, +world: A.World) -> Tip:
177 +chosen = Git.choose(Git.tag.names(rows))
178 tip.chosen(String.is_empty(chosen), chosen, rows, world)
179
180def tip.tags(printed: Up.Said, +world: A.World) -> Tip:
181 match printed:
182 case Up.Said{text}:
183 tip.tags.rows(Git.rows(String.lines(text)), world)
184 case Up.Unsaid{why}:
185 TipNo{why}
186 case Up.Unasked{_ask}:
187 TipAsk{QTags{}}
188
189
190def tip.named(empty: Bool, +world: A.World) -> Tip:
191 match empty:
192 case True{}:
193 tip.tags(said(world, QTags{}), world)
194 case False{}:
195 tip.refs(said(world, QRefs{A.ref(world)}), world)
196
197
198def tip.of(rev: Bool, empty: Bool, +world: A.World) -> Tip:
199 match rev:
200 case True{}:
201 At{A.ref(world)}
202 case False{}:
203 tip.named(empty, world)
204
205
206def tip(+world: A.World) -> Tip:
207 tip.of(Git.is_rev(A.ref(world)), String.is_empty(A.ref(world)), world)
208
209def tip.rev(tip: Tip) -> String:
210 match tip:
211 case At{rev}:
212 rev
213 case TipAsk{_q}:
214 ""
215 case TipNo{_why}:
216 ""
217
218
219def rev(+world: A.World) -> String:
220 tip.rev(tip(world))
221
222
223
224
225
226
227type Co is Data:
228 Co{nar: String, files: List<&2, K.Source>}
229 CoWait{}
230 CoNo{why: String}
231
232def co.of(answer: Up.Answer) -> Co:
233 match answer:
234 case Up.Cloned{nar, fs}:
235 Co{nar, fs}
236 case Up.Miss{why}:
237 CoNo{why}
238 case _:
239 CoNo{"ez: a checkout was not answered with one"}
240
241def co.heard(got: Up.Heard) -> Co:
242 match got:
243 case Up.Open{}:
244 CoWait{}
245 case Up.Heard{answer}:
246 co.of(answer)
247
248
249def co.at(+world: A.World, +rev: String) -> Co:
250 co.heard(Up.heard(A.replies(world), ask.of(QClone{rev}, A.from(world))))
251
252
253def co(+world: A.World) -> Co:
254 co.at(world, rev(world))
255
256def co.files(checkout: Co) -> List<&2, K.Source>:
257 match checkout:
258 case Co{_nar, fs}:
259 fs
260 case CoWait{}:
261 []
262 case CoNo{_why}:
263 []
264
265def co.nar(checkout: Co) -> String:
266 match checkout:
267 case Co{nar, _fs}:
268 nar
269 case CoWait{}:
270 ""
271 case CoNo{_why}:
272 ""
273
274
275def files(+world: A.World) -> List<&2, K.Source>:
276 co.files(co(world))
277
278
279def nar(+world: A.World) -> String:
280 co.nar(co(world))
281
282
283def tree.toml(+world: A.World) -> Maybe<&2, String>:
284 K.look(files(world), "ez.toml")
285
286
287def pkg(+world: A.World) -> String:
288 Git.called.maybe(tree.toml(world))
289
290
291
292def entry(+world: A.World) -> String:
293 Bool.pick(String, String.is_empty(A.entry.arg(world)),
294 Git.entry.maybe(tree.toml(world)), A.entry.arg(world))
295
296
297
298
299
300
301def fuel(fs: List<&2, K.Source>) -> Nat:
302 match fs:
303 case []:
304 1n
305 case K.Source{_at, text} <> t:
306 (List.length(&2, String, String.lines(text)) + fuel(t) : Nat)
307
308
309def walked(+world: A.World) -> K.Walked:
310 K.of.fuel(fuel(files(world)), Path.norm(entry(world)), files(world))
311
312
313def name(+world: A.World) -> String:
314 Named.as(A.deps(world), A.url(world), A.rename(world), pkg(world), A.fresh(world))
315
316
317def why(+world: A.World) -> String:
318 Named.why.as(A.deps(world), name(world), A.url(world), A.rename(world))
319
320
321
322
323
324
325def decided(+world: A.World) -> Bool:
326 Bool.or(Bool.not(String.is_empty(A.rename(world))),
327 Bool.not(String.is_empty(Named.had(A.deps(world), A.url(world), ""))))
328
329
330
331
332
333def early(+world: A.World) -> String:
334 Bool.pick(String, decided(world),
335 Named.why.as(A.deps(world),
336 Named.as(A.deps(world), A.url(world), A.rename(world), "", A.fresh(world)),
337 A.url(world), A.rename(world)),
338 "")
339
340
341
342type Verdict is Data:
343 Go{}
344 Pending{qs: List<&2, Q>}
345 Stop{why: String}
346
347
348def decide.co(checkout: Co, +rev: String) -> Verdict:
349 match checkout:
350 case Co{_nar, _fs}:
351 Go{}
352 case CoWait{}:
353 Pending{[QClone{rev}]}
354 case CoNo{why}:
355 Stop{why}
356
357
358def decide.tip(tip: Tip, +world: A.World) -> Verdict:
359 match tip:
360 case At{+rev}:
361 decide.co(co.at(world, rev), rev)
362 case TipAsk{q}:
363 Pending{[q]}
364 case TipNo{why}:
365 Stop{why}
366
367
368def decide.early(ok: Bool, +why: String, +world: A.World) -> Verdict:
369 match ok:
370 case True{}:
371 decide.tip(tip(world), world)
372 case False{}:
373 Stop{why}
374
375
376def decide.place(place: Tgt.Place, +world: A.World) -> Verdict:
377 match place:
378 case Tgt.Bad{why}:
379 Stop{"ez: " ++ why}
380 case Tgt.Local{_path}:
381 +e = early(world)
382 decide.early(String.is_empty(e), e, world)
383 case Tgt.Remote{_url, _slug}:
384 +e = early(world)
385 decide.early(String.is_empty(e), e, world)
386
387
388
389def decide.read(read: M.Read, +world: A.World) -> Verdict:
390 match read:
391 case M.Bad{why}:
392 Stop{"ez: " ++ M.show(M.Bad{why})}
393 case M.Good{_m}:
394 decide.place(A.place(world), world)
395
396
397def decide(+world: A.World) -> Verdict:
398 decide.read(A.read(world), world)
399
400
401
402
403
404def dep.of(+hash: String, +root: String, +world: A.World) -> M.Dep:
405 M.Dep{name(world), hash, entry(world),
406 M.Git{A.url(world), rev(world), tag(world), root, nar(world), False{}}}
407
408
409
410
411def outcome.fit(fit: Bool) -> P.Outcome:
412 match fit:
413 case True{}:
414 P.Success{}
415 case False{}:
416 P.Refused{Rend.unrenderable()}
417
418
419def outcome.name(ok: Bool, fit: Bool, +world: A.World) -> P.Outcome:
420 match ok:
421 case True{}:
422 outcome.fit(fit)
423 case False{}:
424 P.Refused{why(world)}
425
426
427def outcome.walk(walk: K.Walked, +world: A.World) -> P.Outcome:
428 match walk:
429 case K.Walked{+h, +root, _fs, _ss}:
430 outcome.name(String.is_empty(why(world)),
431 Rend.renderable(Rend.add.keep(A.manifest.of(A.read(world)), dep.of(h, root, world))), world)
432 case K.Asks{at}:
433 P.Refused{"ez: " ++ at ++ " was never read"}
434 case K.Refused{why}:
435 P.Refused{why}
436
437
438def outcome.entry(got: Maybe<&2, String>, walk: K.Walked, +world: A.World) -> P.Outcome:
439 match got:
440 case None{}:
441 P.Refused{"ez: " ++ entry(world) ++ " is not in " ++ A.url(world) ++ " at " ++
442 rev(world)}
443 case Some{_text}:
444 outcome.walk(walk, world)
445
446
447def outcome.k(walk: K.Walked, +world: A.World) -> P.Outcome:
448 outcome.entry(K.look(files(world), Path.norm(entry(world))), walk, world)
449
450
451
452
453
454
455def added.k(walk: K.Walked, +world: A.World) -> M.Dep:
456 match walk:
457 case K.Walked{h, root, _fs, _ss}:
458 dep.of(h, root, world)
459 case K.Asks{_at}:
460 M.Dep{"", "", "", M.Hub{""}}
461 case K.Refused{_why}:
462 M.Dep{"", "", "", M.Hub{""}}
463
464
465def added(+world: A.World) -> M.Dep:
466 added.k(walked(world), world)
467
468
469def ledger.next(+world: A.World) -> M.Manifest:
470 Rend.add.keep(A.manifest.of(A.read(world)), added(world))
471
472
473def src.of(item: K.Source) -> W.Source:
474 K.Source{at, text} = item
475 W.Source{at, text}
476
477
478def srcs(fs: List<&2, K.Source>) -> List<&2, W.Source>:
479 match fs:
480 case []:
481 []
482 case h <> t:
483 src.of(h) <> srcs(t)
484
485
486
487
488def place.of(vend: Bool) -> P.Place:
489 match vend:
490 case True{}:
491 P.Committed{}
492 case False{}:
493 P.Cache{}
494
495
496def any.step(hit: Bool, rest: Unit -> Bool) -> Bool:
497 match hit:
498 case True{}:
499 True{}
500 case False{}:
501 rest(Unit{})
502
503
504def holds(ds: List<&2, M.Dep>, +hash: String) -> Bool:
505 match ds:
506 case []:
507 False{}
508 case +h <> t:
509 any.step(String.eq(M.dep.hash(h), hash), _u => holds(t, hash))
510
511
512def char.ok(+ch: Char) -> Bool:
513 Bool.not(Bool.or(Char.is_eq(ch, '/'), Char.is_eq(ch, '\\')))
514
515def chars.ok(text: String) -> Bool:
516 match text:
517 case SNil{}:
518 True{}
519 case SCon{+h, t}:
520 +rest = chars.ok(t)
521 Bool.and(char.ok(h), rest)
522
523
524
525def safe(+hash: String) -> Bool:
526 Bool.and(String.starts_with(hash, "0x"), chars.ok(hash))
527
528
529
530def gone(+old: M.Dep, +hash: String, +left: List<&2, M.Dep>) -> List<&2, String>:
531 Bool.pick(List<&2, String>,
532 Bool.and(M.source.vendor(M.source.dep(old)),
533 Bool.and(Bool.not(String.eq(M.dep.hash(old), hash)),
534 Bool.and(safe(M.dep.hash(old)), Bool.not(holds(left, M.dep.hash(old)))))),
535 [M.dep.hash(old)], [])
536
537
538def ignore.put(same: Bool, +next: String, rest: List<&2, P.Effect>) -> List<&2, P.Effect>:
539 match same:
540 case True{}:
541 rest
542 case False{}:
543 P.Write{A.ignore.file(), next} <> rest
544
545
546
547
548def at.of(+root: String, +world: A.World) -> String:
549 K.inside(root, Path.norm(entry(world)))
550
551
552
553
554def says(+hash: String, +at: String, +world: A.World) -> List<&2, P.Effect>:
555 +line = Git.import.line(hash, at)
556 Bool.pick(List<&2, P.Effect>, String.is_empty(tag(world)), [P.Say{line}],
557 [P.Say{tag(world) ++ " is " ++ rev(world)}, P.Say{line}])
558
559
560
561
562def effects.of(
563 +dep: M.Dep,
564 +hash: String,
565 +at: String,
566 fs: List<&2, K.Source>,
567 ss: List<&2, K.Item>,
568 +manifest: M.Manifest,
569 +world: A.World
570) -> List<&2, P.Effect>:
571 +old = M.find(A.deps.of(manifest), M.dep.name(dep))
572 +left = A.deps.of(Rend.add.keep(manifest, dep))
573 +next = I.sync(left, A.ignore(world))
574 P.Lay{place.of(M.source.vendor(M.source.dep(old))), hash,
575 List.append(&2, W.Source, srcs(fs), [W.Source{"manifest", K.manifest_of(ss)}])} <>
576 (P.Write{A.toml(), Rend.show(Rend.add.keep(manifest, dep))} <>
577 ignore.put(String.eq(next, A.ignore(world)), next,
578 P.drops.of(gone(old, hash, left), says(hash, at, world))))
579
580
581def effects.k(walk: K.Walked, +world: A.World) -> List<&2, P.Effect>:
582 match walk:
583 case K.Walked{+h, +root, fs, ss}:
584 effects.of(dep.of(h, root, world), h, at.of(root, world), fs, ss,
585 A.manifest.of(A.read(world)), world)
586 case K.Asks{_at}:
587 []
588 case K.Refused{_why}:
589 []
590
591
592
593
594
595def seal(es: List<&2, P.Effect>, outcome: P.Outcome) -> P.Plan:
596 match outcome:
597 case P.Success{}:
598 P.Plan{es, P.Success{}}
599 case P.Refused{why}:
600 P.Plan{[], P.Refused{why}}
601
602
603
604def made(+walk: K.Walked, +world: A.World) -> P.Plan:
605 seal(effects.k(walk, world), outcome.k(walk, world))
606
607
608
609def plan.v(verdict: Verdict, +world: A.World) -> P.Plan:
610 match verdict:
611 case Go{}:
612 made(walked(world), world)
613 case Pending{_qs}:
614 P.Plan{[], P.Refused{"ez: the remote was never asked what " ++ A.url(world) ++
615 " holds"}}
616 case Stop{why}:
617 P.Plan{[], P.Refused{why}}
618
619
620def plan(+world: A.World) -> P.Plan:
621 plan.v(decide(world), world)
622
623
624def refuses.plan(pl: P.Plan) -> Bool:
625 P.Plan{_es, outcome} = pl
626 match outcome:
627 case P.Success{}:
628 False{}
629 case P.Refused{_why}:
630 True{}
631
632
633def refuses(+world: A.World) -> Bool:
634 refuses.plan(plan(world))
635
636
637def qs.v(verdict: Verdict) -> List<&2, Q>:
638 match verdict:
639 case Go{}:
640 []
641 case Pending{qs}:
642 qs
643 case Stop{_why}:
644 []
645
646
647def wants(+world: A.World) -> List<&2, Up.Ask>:
648 asks.of(qs.v(decide(world)), A.from(world))
649
650
651def answered(+world: A.World) -> Bool:
652 List.is_empty(&2, Up.Ask, wants(world))
653
654
655type Step is Data:
656 Asking{asks: List<&2, Up.Ask>}
657 Run{plan: P.Plan}
658
659def step.v(verdict: Verdict, +world: A.World) -> Step:
660 match verdict:
661 case Go{}:
662 Run{plan.v(Go{}, world)}
663 case Pending{qs}:
664 Asking{asks.of(qs, A.from(world))}
665 case Stop{why}:
666 Run{plan.v(Stop{why}, world)}
667
668
669def step(+world: A.World) -> Step:
670 step.v(decide(world), world)
671
672
673
674
675
676
677def resolves.one(ask: Up.Ask) -> Bool:
678 match ask:
679 case Up.Tags{_url}:
680 True{}
681 case Up.Head{_url}:
682 True{}
683 case Up.Refs{_url, _ref}:
684 True{}
685 case _:
686 False{}
687
688
689def resolves(as: List<&2, Up.Ask>) -> Bool:
690 match as:
691 case []:
692 False{}
693 case h <> t:
694 +rest = resolves(t)
695 Bool.or(resolves.one(h), rest)
696
697
698def ask.url(ask: Up.Ask) -> String:
699 match ask:
700 case Up.Tags{url}:
701 url
702 case Up.Head{url}:
703 url
704 case Up.Refs{url, _ref}:
705 url
706 case Up.Clone{url, _rev}:
707 url
708 case _:
709 ""
710
711
712def asks.from(as: List<&2, Up.Ask>, +from: String) -> Bool:
713 match as:
714 case []:
715 True{}
716 case h <> t:
717 +rest = asks.from(t, from)
718 Bool.and(String.eq(ask.url(h), from), rest)
719
720
721def after(wrote: P.Wrote, +was: String) -> String:
722 match wrote:
723 case P.Kept{}:
724 was
725 case P.Put{text}:
726 text
727
728
729def hash.of(walk: K.Walked) -> String:
730 match walk:
731 case K.Walked{h, _root, _fs, _ss}:
732 h
733 case K.Asks{_at}:
734 ""
735 case K.Refused{_why}:
736 ""
737
738
739def root.of(walk: K.Walked) -> String:
740 match walk:
741 case K.Walked{_h, root, _fs, _ss}:
742 root
743 case K.Asks{_at}:
744 "."
745 case K.Refused{_why}:
746 "."
747
748
749def hash(+world: A.World) -> String:
750 hash.of(walked(world))
751
752
753
754def at(+world: A.World) -> String:
755 at.of(root.of(walked(world)), world)
756
757
758def last.go(ls: List<&2, String>, +prev: String) -> String:
759 match ls:
760 case []:
761 prev
762 case h <> t:
763 last.go(t, h)
764
765
766def last(pl: P.Plan) -> String:
767 last.go(P.said(pl), "")
768
769
770
771
772
773def command.of(named: Bool, +world: A.World) -> P.Plan:
774 match named:
775 case True{}:
776 H.plan(world)
777 case False{}:
778 plan(world)
779
780
781def command(+world: A.World) -> P.Plan:
782 command.of(A.named(world), world)
783
784
785def refuses.command(+world: A.World) -> Bool:
786 refuses.plan(command(world))
787
788
789
790type Next is Data:
791 AskGit{asks: List<&2, Up.Ask>}
792 AskHub{asks: List<&2, W.Ask>}
793 Planned{plan: P.Plan}
794
795
796def next.git(st: Step) -> Next:
797 match st:
798 case Asking{asks}:
799 AskGit{asks}
800 case Run{pl}:
801 Planned{pl}
802
803
804def next.hub(asks: List<&2, W.Ask>, +world: A.World) -> Next:
805 match asks:
806 case []:
807 Planned{H.plan(world)}
808 case h <> t:
809 AskHub{h <> t}
810
811def next.of(named: Bool, +world: A.World) -> Next:
812 match named:
813 case True{}:
814 next.hub(H.wants(world), world)
815 case False{}:
816 next.git(step(world))
817
818
819def next(+world: A.World) -> Next:
820 next.of(A.named(world), world)
821
822
823def next.asks(nx: Next) -> Bool:
824 match nx:
825 case AskGit{_asks}:
826 True{}
827 case AskHub{_asks}:
828 True{}
829 case Planned{_pl}:
830 False{}