src/git/exec.bend
ezx@1.8.0.0 · 153 lines · 0 definitions
1# git/exec: the git commands the interpreters run: a shallow clone of one
2# commit, a checkout weighed for its NAR hash, HEAD on a remote, and whether
3# one commit descends from another. What they print is read by the pure
4# functions in git/git.bend. They live apart from those because git runs
5# through snap's foreign effect, and bend 2.0.32 fails a proof whose imports
6# reach foreign code, so a law may import git/git but never this.
7import Base
8import snap@1.3.0.0/main.bend as R
9import ezx@1.8.0.0/src/pkg/path.bend as P
10import ezx@1.8.0.0/src/sha/dump.bend as Dump
11import ezx@1.8.0.0/src/share/say.bend as Say
12import ezx@1.8.0.0/src/share/spin.bend as Spin
13import ezx@1.8.0.0/src/git/git.bend as G
14
15# a git command that failed stops the command, with whatever git said
16def ran(ok: Bool, what: String, text: String) -> IO(Unit):
17 match ok:
18 case True{}:
19 IO.pure(Unit, Unit{})
20 case False{}:
21 IO.die(Unit, 1, "ez: git " ++ what ++ ": " ++ text)
22
23# one git command
24def run(what: String, args: List<&2, String>) -> IO(Unit):
25 do IO<Unit>:
26 +out : String <- R.exec(args)
27 ran(R.ok(out), what, R.text(out))
28
29# a source as git is to read it. A ledger records a local path as it was
30# given, relative to the project, which is the directory ez runs in, so the
31# ledger reads the same on every clone. git reads such a path from somewhere
32# else: a fetch from inside its work directory, and `ls-remote` from the top
33# of any repository around it. Every git command here that is handed a source
34# is handed this instead.
35def anchored(+url: String) -> IO(String):
36 do IO<String>:
37 +here : String <- R.exec(["pwd"])
38 return P.anchor(String.trim(R.text(here)), url)
39
40# the checkout a package is taken from: the pinned commit alone, at the depth
41# of one, so a repo with a long history costs nothing to vendor. The fetch is
42# quiet; the line above it is what a person sees while it runs.
43def clone.say(+work: String, +url: String, +rev: String, +msg: String) -> IO(Unit):
44 do IO<Unit>:
45 +s : Say.Spin <- Spin.hold(msg)
46 +from : String <- anchored(url)
47 run("init", ["git", "init", "-q", work])
48 run("fetch", ["git", "-C", work, "fetch", "-q", "--depth", "1", from, rev])
49 run("checkout", ["git", "-C", work, "checkout", "-q", rev])
50 Spin.done(s)
51
52# a shallow clone of one commit into `work`, with a line saying what it fetches
53def clone(+work: String, +url: String, +rev: String) -> IO(Unit):
54 clone.say(work, url, rev, Say.git.fetch(url))
55
56# the NAR hash nix rebuilds the checkout by. `fetchgit` hashes the working
57# tree with its `.git` taken off, so that is what is weighed here and the
58# caller strips `.git` before asking. Computed in Bend; nix is not required.
59def nar(+work: String) -> IO(String):
60 Dump.path(work)
61
62# the NAR hash of one commit. The checkout is weighed the way `fetchgit`
63# weighs it, then removed. Nothing is laid under BEND_LIB.
64def weigh(+url: String, +rev: String) -> IO(String):
65 +work = ".ez/weigh-" ++ rev
66 do IO<String>:
67 _rm : String <- R.exec(["rm", "-rf", work])
68 _mk : String <- R.exec(["mkdir", "-p", work])
69 clone(work, url, rev)
70 _dg : String <- R.exec(["rm", "-rf", work ++ "/.git"])
71 +s : Say.Spin <- Spin.hold(Say.hashing(rev))
72 +nh : String <- nar(work)
73 Spin.done(s)
74 _cl : String <- R.exec(["rm", "-rf", work])
75 return nh
76
77# a remote with no HEAD is not a repo an upgrade can move
78def remote.got(empty: Bool, +rev: String, +branch: String, +url: String) -> IO(G.Tip):
79 match empty:
80 case True{}:
81 IO.die(G.Tip, 1, "ez: " ++ url ++ " has no HEAD")
82 case False{}:
83 IO.pure(G.Tip, G.Tip{rev, branch})
84
85# what ls-remote said, or why it said nothing
86def remote.said(ok: Bool, +url: String, +text: String) -> IO(G.Tip):
87 match ok:
88 case False{}:
89 IO.die(G.Tip, 1, "ez: git ls-remote " ++ url ++ ": " ++ text)
90 case True{}:
91 +rs = G.rows(String.lines(text))
92 remote.got(String.is_empty(G.tip.rev(rs)), G.tip.rev(rs), G.tip.branch(rs), url)
93
94# HEAD on the remote: the commit the default branch tips, and that branch's
95# name. `--symref` is what puts the branch on the line in front of the commit.
96def remote(+url: String) -> IO(G.Tip):
97 do IO<G.Tip>:
98 +from : String <- anchored(url)
99 +out : String <- R.exec(["git", "ls-remote", "--symref", from, "HEAD"])
100 remote.said(R.ok(out), url, R.text(out))
101
102# a bare clone, blobs left behind. Ancestry only needs the commits. The clone
103# is quiet; the line is what shows while it runs.
104def clone.bare(+work: String, +url: String) -> IO(Unit):
105 do IO<Unit>:
106 +s : Say.Spin <- Spin.hold(Say.git.fetch(url))
107 +from : String <- anchored(url)
108 run("clone", ["git", "clone", "--bare", "--filter=blob:none", "-q", from, work])
109 Spin.done(s)
110
111# merge-base answers 0 for a descendant, 1 for anything else, and some other
112# status when the question itself could not be asked
113def descendant.no(no: Bool, +text: String) -> IO(Bool):
114 match no:
115 case True{}:
116 IO.pure(Bool, False{})
117 case False{}:
118 IO.die(Bool, 1, "ez: git merge-base: " ++ text)
119
120# the status, read
121def descendant.pick(yes: Bool, no: Bool, text: String) -> IO(Bool):
122 match yes:
123 case True{}:
124 IO.pure(Bool, True{})
125 case False{}:
126 descendant.no(no, text)
127
128# the status a merge-base answered with
129def descendant.code(+code: String, +text: String) -> IO(Bool):
130 descendant.pick(String.eq(code, "0"), String.eq(code, "1"), text)
131
132# the clone, the question, the clone gone. The same commit is a descendant
133# of itself and needs no clone to say so.
134def descendant.clone(+work: String, +url: String, +pin: String, +tip: String) -> IO(Bool):
135 do IO<Bool>:
136 _rm : String <- R.exec(["rm", "-rf", work])
137 clone.bare(work, url)
138 +out : String <- R.exec(["git", "-C", work, "merge-base", "--is-ancestor",
139 pin, tip])
140 _cl : String <- R.exec(["rm", "-rf", work])
141 descendant.code(R.code(out), R.text(out))
142
143# whether `tip` descends from `pin`, the same commit included
144def descendant.go(same: Bool, work: String, url: String, pin: String, tip: String) -> IO(Bool):
145 match same:
146 case True{}:
147 IO.pure(Bool, True{})
148 case False{}:
149 descendant.clone(work, url, pin, tip)
150
151# whether `tip` descends from `pin`. `work` is a directory the clone may use.
152def descendant.at(+work: String, +url: String, +pin: String, +tip: String) -> IO(Bool):
153 descendant.go(String.eq(pin, tip), work, url, pin, tip)