src/ws/root.bend
1# ws/root: which workspace a command run in a directory belongs to
2# (EZ-WS-2). ez reads ez.toml from the directory it runs in; in a workspace
3# that directory may be a member, so a command first finds its root: the
4# nearest directory at or above it, up to the top of the git checkout, whose
5# ledger is a workspace root that makes it a member. With none, the
6# directory is a project of its own, as it always was. The design is
7# docs/rfc/ez-workspaces.md.
8#
9# Both halves are pure. `asks` names the directories whose ez.toml the
10# interpreter reads, each with the path from it down to where ez runs, nearest
11# first; `root.go` decides from what was read, in that order. A path is its
12# segments, relative to the top of the checkout, as `git rev-parse
13# --show-prefix` prints it (`here.of`).
14import Base
15import ezx@1.11.0.0/src/ws/workspace.bend as W
16
17# a directory at or above where ez runs: its path from the top of the
18# checkout, and the path from it down to where ez runs
19type Ask is Data:
20 Ask{dir: List<&2, String>, rel: List<&2, String>}
21
22# one such directory, read: its ez.toml's text, or None when it has none
23type Up is Data:
24 Up{dir: List<&2, String>, rel: List<&2, String>, text: Maybe<&2, String>}
25
26# where a command run in a directory stands: in a project of its own, in a
27# workspace at `root` as the member `rel` below it (`[]` for the root itself),
28# or nowhere, because a workspace table above it could not be read
29type Found is Data:
30 Alone{}
31 Within{root: List<&2, String>, rel: List<&2, String>}
32 Broken{why: String}
33
34# where ez runs, from what `git rev-parse --show-prefix` printed: the
35# segments before the `/` it ends with, none at the top. Anything else is no
36# answer.
37def here.of(+prefix: String) -> Maybe<&2, List<&2, String>>:
38 W.cut(W.segs.of(prefix), "")
39
40# every directory asked about from one segment further down, now one segment
41# further from the top
42def asks.under(+hd: String, xs: List<&2, Ask>) -> List<&2, Ask>:
43 match xs:
44 case []:
45 []
46 case Ask{ds, rs} <> tl:
47 Ask{hd <> ds, rs} <> asks.under(hd, tl)
48
49# the directories whose ez.toml is read: where ez runs, then each directory
50# above it in turn, ending at the top of the checkout
51def asks(here: List<&2, String>) -> List<&2, Ask>:
52 match here:
53 case []:
54 [Ask{[], []}]
55 case +hd <> +tl:
56 List.append(&2, Ask, asks.under(hd, asks(tl)), [Ask{[], hd <> tl}])
57
58# whether a workspace root makes the directory this far below it a member:
59# the root itself always, and any other when an entry names it and no
60# exclusion does
61def claims(+spec: W.Spec, rel: List<&2, String>) -> Bool:
62 match rel:
63 case []:
64 True{}
65 case +hd <> +tl:
66 W.chosen(spec, hd <> tl)
67
68# a workspace root claims the directory, so the search ends there, or does
69# not, and goes on above it
70def root.claim(hit: Bool, +dir: List<&2, String>, +rel: List<&2, String>, rest: Found) -> Found:
71 match hit:
72 case True{}:
73 Within{dir, rel}
74 case False{}:
75 rest
76
77# what one directory's ledger says, against what the directories above it
78# decided
79def root.read(rd: W.Read, +dir: List<&2, String>, +rel: List<&2, String>, rest: Found) -> Found:
80 match rd:
81 case W.Lone{}:
82 rest
83 case W.Bad{why}:
84 Broken{why}
85 case W.Root{+spec}:
86 root.claim(claims(spec, rel), dir, rel, rest)
87
88# one directory, with or without a ledger
89def root.at(text: Maybe<&2, String>, +dir: List<&2, String>, +rel: List<&2, String>, rest: Found) -> Found:
90 match text:
91 case None{}:
92 rest
93 case Some{tt}:
94 root.read(W.spec.of(tt), dir, rel, rest)
95
96# the root, from the directories read, nearest first: the first whose ledger
97# is a workspace root that claims where ez runs, a table that could not be
98# read if one comes first, and a project of its own when there is neither
99def root.go(ups: List<&2, Up>) -> Found:
100 match ups:
101 case []:
102 Alone{}
103 case Up{ds, rs, tx} <> tl:
104 rest = root.go(tl)
105 root.at(tx, ds, rs, rest)