Skip to content

Commit 0b975b0

Browse files
authored
feat: unwrap ugit namespace to flat exports + barrel (anomalyco#22704)
1 parent bb90aa6 commit 0b975b0

2 files changed

Lines changed: 259 additions & 260 deletions

File tree

packages/opencode/src/git/git.ts

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
2+
import { Effect, Layer, Context, Stream } from "effect"
3+
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
4+
5+
const cfg = [
6+
"--no-optional-locks",
7+
"-c",
8+
"core.autocrlf=false",
9+
"-c",
10+
"core.fsmonitor=false",
11+
"-c",
12+
"core.longpaths=true",
13+
"-c",
14+
"core.symlinks=true",
15+
"-c",
16+
"core.quotepath=false",
17+
] as const
18+
19+
const out = (result: { text(): string }) => result.text().trim()
20+
const nuls = (text: string) => text.split("\0").filter(Boolean)
21+
const fail = (err: unknown) =>
22+
({
23+
exitCode: 1,
24+
text: () => "",
25+
stdout: Buffer.alloc(0),
26+
stderr: Buffer.from(err instanceof Error ? err.message : String(err)),
27+
}) satisfies Result
28+
29+
export type Kind = "added" | "deleted" | "modified"
30+
31+
export type Base = {
32+
readonly name: string
33+
readonly ref: string
34+
}
35+
36+
export type Item = {
37+
readonly file: string
38+
readonly code: string
39+
readonly status: Kind
40+
}
41+
42+
export type Stat = {
43+
readonly file: string
44+
readonly additions: number
45+
readonly deletions: number
46+
}
47+
48+
export interface Result {
49+
readonly exitCode: number
50+
readonly text: () => string
51+
readonly stdout: Buffer
52+
readonly stderr: Buffer
53+
}
54+
55+
export interface Options {
56+
readonly cwd: string
57+
readonly env?: Record<string, string>
58+
}
59+
60+
export interface Interface {
61+
readonly run: (args: string[], opts: Options) => Effect.Effect<Result>
62+
readonly branch: (cwd: string) => Effect.Effect<string | undefined>
63+
readonly prefix: (cwd: string) => Effect.Effect<string>
64+
readonly defaultBranch: (cwd: string) => Effect.Effect<Base | undefined>
65+
readonly hasHead: (cwd: string) => Effect.Effect<boolean>
66+
readonly mergeBase: (cwd: string, base: string, head?: string) => Effect.Effect<string | undefined>
67+
readonly show: (cwd: string, ref: string, file: string, prefix?: string) => Effect.Effect<string>
68+
readonly status: (cwd: string) => Effect.Effect<Item[]>
69+
readonly diff: (cwd: string, ref: string) => Effect.Effect<Item[]>
70+
readonly stats: (cwd: string, ref: string) => Effect.Effect<Stat[]>
71+
}
72+
73+
const kind = (code: string): Kind => {
74+
if (code === "??") return "added"
75+
if (code.includes("U")) return "modified"
76+
if (code.includes("A") && !code.includes("D")) return "added"
77+
if (code.includes("D") && !code.includes("A")) return "deleted"
78+
return "modified"
79+
}
80+
81+
export class Service extends Context.Service<Service, Interface>()("@opencode/Git") {}
82+
83+
export const layer = Layer.effect(
84+
Service,
85+
Effect.gen(function* () {
86+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
87+
88+
const run = Effect.fn("Git.run")(
89+
function* (args: string[], opts: Options) {
90+
const proc = ChildProcess.make("git", [...cfg, ...args], {
91+
cwd: opts.cwd,
92+
env: opts.env,
93+
extendEnv: true,
94+
stdin: "ignore",
95+
stdout: "pipe",
96+
stderr: "pipe",
97+
})
98+
const handle = yield* spawner.spawn(proc)
99+
const [stdout, stderr] = yield* Effect.all(
100+
[Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))],
101+
{ concurrency: 2 },
102+
)
103+
return {
104+
exitCode: yield* handle.exitCode,
105+
text: () => stdout,
106+
stdout: Buffer.from(stdout),
107+
stderr: Buffer.from(stderr),
108+
} satisfies Result
109+
},
110+
Effect.scoped,
111+
Effect.catch((err) => Effect.succeed(fail(err))),
112+
)
113+
114+
const text = Effect.fn("Git.text")(function* (args: string[], opts: Options) {
115+
return (yield* run(args, opts)).text()
116+
})
117+
118+
const lines = Effect.fn("Git.lines")(function* (args: string[], opts: Options) {
119+
return (yield* text(args, opts))
120+
.split(/\r?\n/)
121+
.map((item) => item.trim())
122+
.filter(Boolean)
123+
})
124+
125+
const refs = Effect.fnUntraced(function* (cwd: string) {
126+
return yield* lines(["for-each-ref", "--format=%(refname:short)", "refs/heads"], { cwd })
127+
})
128+
129+
const configured = Effect.fnUntraced(function* (cwd: string, list: string[]) {
130+
const result = yield* run(["config", "init.defaultBranch"], { cwd })
131+
const name = out(result)
132+
if (!name || !list.includes(name)) return
133+
return { name, ref: name } satisfies Base
134+
})
135+
136+
const primary = Effect.fnUntraced(function* (cwd: string) {
137+
const list = yield* lines(["remote"], { cwd })
138+
if (list.includes("origin")) return "origin"
139+
if (list.length === 1) return list[0]
140+
if (list.includes("upstream")) return "upstream"
141+
return list[0]
142+
})
143+
144+
const branch = Effect.fn("Git.branch")(function* (cwd: string) {
145+
const result = yield* run(["symbolic-ref", "--quiet", "--short", "HEAD"], { cwd })
146+
if (result.exitCode !== 0) return
147+
const text = out(result)
148+
return text || undefined
149+
})
150+
151+
const prefix = Effect.fn("Git.prefix")(function* (cwd: string) {
152+
const result = yield* run(["rev-parse", "--show-prefix"], { cwd })
153+
if (result.exitCode !== 0) return ""
154+
return out(result)
155+
})
156+
157+
const defaultBranch = Effect.fn("Git.defaultBranch")(function* (cwd: string) {
158+
const remote = yield* primary(cwd)
159+
if (remote) {
160+
const head = yield* run(["symbolic-ref", `refs/remotes/${remote}/HEAD`], { cwd })
161+
if (head.exitCode === 0) {
162+
const ref = out(head).replace(/^refs\/remotes\//, "")
163+
const name = ref.startsWith(`${remote}/`) ? ref.slice(`${remote}/`.length) : ""
164+
if (name) return { name, ref } satisfies Base
165+
}
166+
}
167+
168+
const list = yield* refs(cwd)
169+
const next = yield* configured(cwd, list)
170+
if (next) return next
171+
if (list.includes("main")) return { name: "main", ref: "main" } satisfies Base
172+
if (list.includes("master")) return { name: "master", ref: "master" } satisfies Base
173+
})
174+
175+
const hasHead = Effect.fn("Git.hasHead")(function* (cwd: string) {
176+
const result = yield* run(["rev-parse", "--verify", "HEAD"], { cwd })
177+
return result.exitCode === 0
178+
})
179+
180+
const mergeBase = Effect.fn("Git.mergeBase")(function* (cwd: string, base: string, head = "HEAD") {
181+
const result = yield* run(["merge-base", base, head], { cwd })
182+
if (result.exitCode !== 0) return
183+
const text = out(result)
184+
return text || undefined
185+
})
186+
187+
const show = Effect.fn("Git.show")(function* (cwd: string, ref: string, file: string, prefix = "") {
188+
const target = prefix ? `${prefix}${file}` : file
189+
const result = yield* run(["show", `${ref}:${target}`], { cwd })
190+
if (result.exitCode !== 0) return ""
191+
if (result.stdout.includes(0)) return ""
192+
return result.text()
193+
})
194+
195+
const status = Effect.fn("Git.status")(function* (cwd: string) {
196+
return nuls(
197+
yield* text(["status", "--porcelain=v1", "--untracked-files=all", "--no-renames", "-z", "--", "."], {
198+
cwd,
199+
}),
200+
).flatMap((item) => {
201+
const file = item.slice(3)
202+
if (!file) return []
203+
const code = item.slice(0, 2)
204+
return [{ file, code, status: kind(code) } satisfies Item]
205+
})
206+
})
207+
208+
const diff = Effect.fn("Git.diff")(function* (cwd: string, ref: string) {
209+
const list = nuls(
210+
yield* text(["diff", "--no-ext-diff", "--no-renames", "--name-status", "-z", ref, "--", "."], { cwd }),
211+
)
212+
return list.flatMap((code, idx) => {
213+
if (idx % 2 !== 0) return []
214+
const file = list[idx + 1]
215+
if (!code || !file) return []
216+
return [{ file, code, status: kind(code) } satisfies Item]
217+
})
218+
})
219+
220+
const stats = Effect.fn("Git.stats")(function* (cwd: string, ref: string) {
221+
return nuls(
222+
yield* text(["diff", "--no-ext-diff", "--no-renames", "--numstat", "-z", ref, "--", "."], { cwd }),
223+
).flatMap((item) => {
224+
const a = item.indexOf("\t")
225+
const b = item.indexOf("\t", a + 1)
226+
if (a === -1 || b === -1) return []
227+
const file = item.slice(b + 1)
228+
if (!file) return []
229+
const adds = item.slice(0, a)
230+
const dels = item.slice(a + 1, b)
231+
const additions = adds === "-" ? 0 : Number.parseInt(adds || "0", 10)
232+
const deletions = dels === "-" ? 0 : Number.parseInt(dels || "0", 10)
233+
return [
234+
{
235+
file,
236+
additions: Number.isFinite(additions) ? additions : 0,
237+
deletions: Number.isFinite(deletions) ? deletions : 0,
238+
} satisfies Stat,
239+
]
240+
})
241+
})
242+
243+
return Service.of({
244+
run,
245+
branch,
246+
prefix,
247+
defaultBranch,
248+
hasHead,
249+
mergeBase,
250+
show,
251+
status,
252+
diff,
253+
stats,
254+
})
255+
}),
256+
)
257+
258+
export const defaultLayer = layer.pipe(Layer.provide(CrossSpawnSpawner.defaultLayer))

0 commit comments

Comments
 (0)