refactor(effect): use Git service in file and storage (#21803)
This commit is contained in:
@@ -11,7 +11,6 @@ import path from "path"
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { Filesystem } from "../util/filesystem"
|
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { Protected } from "./protected"
|
import { Protected } from "./protected"
|
||||||
import { Ripgrep } from "./ripgrep"
|
import { Ripgrep } from "./ripgrep"
|
||||||
@@ -344,6 +343,7 @@ export namespace File {
|
|||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const appFs = yield* AppFileSystem.Service
|
const appFs = yield* AppFileSystem.Service
|
||||||
|
const git = yield* Git.Service
|
||||||
|
|
||||||
const state = yield* InstanceState.make<State>(
|
const state = yield* InstanceState.make<State>(
|
||||||
Effect.fn("File.state")(() =>
|
Effect.fn("File.state")(() =>
|
||||||
@@ -410,6 +410,10 @@ export namespace File {
|
|||||||
cachedScan = yield* Effect.cached(scan().pipe(Effect.catchCause(() => Effect.void)))
|
cachedScan = yield* Effect.cached(scan().pipe(Effect.catchCause(() => Effect.void)))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const gitText = Effect.fnUntraced(function* (args: string[]) {
|
||||||
|
return (yield* git.run(args, { cwd: Instance.directory })).text()
|
||||||
|
})
|
||||||
|
|
||||||
const init = Effect.fn("File.init")(function* () {
|
const init = Effect.fn("File.init")(function* () {
|
||||||
yield* ensure()
|
yield* ensure()
|
||||||
})
|
})
|
||||||
@@ -417,100 +421,87 @@ export namespace File {
|
|||||||
const status = Effect.fn("File.status")(function* () {
|
const status = Effect.fn("File.status")(function* () {
|
||||||
if (Instance.project.vcs !== "git") return []
|
if (Instance.project.vcs !== "git") return []
|
||||||
|
|
||||||
return yield* Effect.promise(async () => {
|
const diffOutput = yield* gitText([
|
||||||
const diffOutput = (
|
"-c",
|
||||||
await Git.run(["-c", "core.fsmonitor=false", "-c", "core.quotepath=false", "diff", "--numstat", "HEAD"], {
|
"core.fsmonitor=false",
|
||||||
cwd: Instance.directory,
|
"-c",
|
||||||
|
"core.quotepath=false",
|
||||||
|
"diff",
|
||||||
|
"--numstat",
|
||||||
|
"HEAD",
|
||||||
|
])
|
||||||
|
|
||||||
|
const changed: File.Info[] = []
|
||||||
|
|
||||||
|
if (diffOutput.trim()) {
|
||||||
|
for (const line of diffOutput.trim().split("\n")) {
|
||||||
|
const [added, removed, file] = line.split("\t")
|
||||||
|
changed.push({
|
||||||
|
path: file,
|
||||||
|
added: added === "-" ? 0 : parseInt(added, 10),
|
||||||
|
removed: removed === "-" ? 0 : parseInt(removed, 10),
|
||||||
|
status: "modified",
|
||||||
})
|
})
|
||||||
).text()
|
|
||||||
|
|
||||||
const changed: File.Info[] = []
|
|
||||||
|
|
||||||
if (diffOutput.trim()) {
|
|
||||||
for (const line of diffOutput.trim().split("\n")) {
|
|
||||||
const [added, removed, file] = line.split("\t")
|
|
||||||
changed.push({
|
|
||||||
path: file,
|
|
||||||
added: added === "-" ? 0 : parseInt(added, 10),
|
|
||||||
removed: removed === "-" ? 0 : parseInt(removed, 10),
|
|
||||||
status: "modified",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const untrackedOutput = (
|
const untrackedOutput = yield* gitText([
|
||||||
await Git.run(
|
"-c",
|
||||||
[
|
"core.fsmonitor=false",
|
||||||
"-c",
|
"-c",
|
||||||
"core.fsmonitor=false",
|
"core.quotepath=false",
|
||||||
"-c",
|
"ls-files",
|
||||||
"core.quotepath=false",
|
"--others",
|
||||||
"ls-files",
|
"--exclude-standard",
|
||||||
"--others",
|
])
|
||||||
"--exclude-standard",
|
|
||||||
],
|
|
||||||
{
|
|
||||||
cwd: Instance.directory,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
).text()
|
|
||||||
|
|
||||||
if (untrackedOutput.trim()) {
|
if (untrackedOutput.trim()) {
|
||||||
for (const file of untrackedOutput.trim().split("\n")) {
|
for (const file of untrackedOutput.trim().split("\n")) {
|
||||||
try {
|
const content = yield* appFs
|
||||||
const content = await Filesystem.readText(path.join(Instance.directory, file))
|
.readFileString(path.join(Instance.directory, file))
|
||||||
changed.push({
|
.pipe(Effect.catch(() => Effect.succeed<string | undefined>(undefined)))
|
||||||
path: file,
|
if (content === undefined) continue
|
||||||
added: content.split("\n").length,
|
changed.push({
|
||||||
removed: 0,
|
path: file,
|
||||||
status: "added",
|
added: content.split("\n").length,
|
||||||
})
|
removed: 0,
|
||||||
} catch {
|
status: "added",
|
||||||
continue
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const deletedOutput = (
|
const deletedOutput = yield* gitText([
|
||||||
await Git.run(
|
"-c",
|
||||||
[
|
"core.fsmonitor=false",
|
||||||
"-c",
|
"-c",
|
||||||
"core.fsmonitor=false",
|
"core.quotepath=false",
|
||||||
"-c",
|
"diff",
|
||||||
"core.quotepath=false",
|
"--name-only",
|
||||||
"diff",
|
"--diff-filter=D",
|
||||||
"--name-only",
|
"HEAD",
|
||||||
"--diff-filter=D",
|
])
|
||||||
"HEAD",
|
|
||||||
],
|
|
||||||
{
|
|
||||||
cwd: Instance.directory,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
).text()
|
|
||||||
|
|
||||||
if (deletedOutput.trim()) {
|
if (deletedOutput.trim()) {
|
||||||
for (const file of deletedOutput.trim().split("\n")) {
|
for (const file of deletedOutput.trim().split("\n")) {
|
||||||
changed.push({
|
changed.push({
|
||||||
path: file,
|
path: file,
|
||||||
added: 0,
|
added: 0,
|
||||||
removed: 0,
|
removed: 0,
|
||||||
status: "deleted",
|
status: "deleted",
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return changed.map((item) => {
|
return changed.map((item) => {
|
||||||
const full = path.isAbsolute(item.path) ? item.path : path.join(Instance.directory, item.path)
|
const full = path.isAbsolute(item.path) ? item.path : path.join(Instance.directory, item.path)
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
path: path.relative(Instance.directory, full),
|
path: path.relative(Instance.directory, full),
|
||||||
}
|
}
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const read = Effect.fn("File.read")(function* (file: string) {
|
const read: Interface["read"] = Effect.fn("File.read")(function* (file: string) {
|
||||||
using _ = log.time("read", { file })
|
using _ = log.time("read", { file })
|
||||||
const full = path.join(Instance.directory, file)
|
const full = path.join(Instance.directory, file)
|
||||||
|
|
||||||
@@ -558,27 +549,19 @@ export namespace File {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (Instance.project.vcs === "git") {
|
if (Instance.project.vcs === "git") {
|
||||||
return yield* Effect.promise(async (): Promise<File.Content> => {
|
let diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--", file])
|
||||||
let diff = (
|
if (!diff.trim()) {
|
||||||
await Git.run(["-c", "core.fsmonitor=false", "diff", "--", file], { cwd: Instance.directory })
|
diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file])
|
||||||
).text()
|
}
|
||||||
if (!diff.trim()) {
|
if (diff.trim()) {
|
||||||
diff = (
|
const original = yield* git.show(Instance.directory, "HEAD", file)
|
||||||
await Git.run(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file], {
|
const patch = structuredPatch(file, file, original, content, "old", "new", {
|
||||||
cwd: Instance.directory,
|
context: Infinity,
|
||||||
})
|
ignoreWhitespace: true,
|
||||||
).text()
|
})
|
||||||
}
|
return { type: "text" as const, content, patch, diff: formatPatch(patch) }
|
||||||
if (diff.trim()) {
|
}
|
||||||
const original = (await Git.run(["show", `HEAD:${file}`], { cwd: Instance.directory })).text()
|
return { type: "text" as const, content }
|
||||||
const patch = structuredPatch(file, file, original, content, "old", "new", {
|
|
||||||
context: Infinity,
|
|
||||||
ignoreWhitespace: true,
|
|
||||||
})
|
|
||||||
return { type: "text", content, patch, diff: formatPatch(patch) }
|
|
||||||
}
|
|
||||||
return { type: "text", content }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type: "text" as const, content }
|
return { type: "text" as const, content }
|
||||||
@@ -660,7 +643,7 @@ export namespace File {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Git.defaultLayer))
|
||||||
|
|
||||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export namespace FileWatcher {
|
|||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
|
const git = yield* Git.Service
|
||||||
|
|
||||||
const state = yield* InstanceState.make(
|
const state = yield* InstanceState.make(
|
||||||
Effect.fn("FileWatcher.state")(
|
Effect.fn("FileWatcher.state")(
|
||||||
@@ -131,11 +132,9 @@ export namespace FileWatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Instance.project.vcs === "git") {
|
if (Instance.project.vcs === "git") {
|
||||||
const result = yield* Effect.promise(() =>
|
const result = yield* git.run(["rev-parse", "--git-dir"], {
|
||||||
Git.run(["rev-parse", "--git-dir"], {
|
cwd: Instance.project.worktree,
|
||||||
cwd: Instance.project.worktree,
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
const vcsDir =
|
const vcsDir =
|
||||||
result.exitCode === 0 ? path.resolve(Instance.project.worktree, result.text().trim()) : undefined
|
result.exitCode === 0 ? path.resolve(Instance.project.worktree, result.text().trim()) : undefined
|
||||||
if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
|
if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
|
||||||
@@ -161,7 +160,7 @@ export namespace FileWatcher {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Git.defaultLayer))
|
||||||
|
|
||||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ import { Git } from "@/git"
|
|||||||
export namespace Storage {
|
export namespace Storage {
|
||||||
const log = Log.create({ service: "storage" })
|
const log = Log.create({ service: "storage" })
|
||||||
|
|
||||||
type Migration = (dir: string, fs: AppFileSystem.Interface) => Effect.Effect<void, AppFileSystem.Error>
|
type Migration = (
|
||||||
|
dir: string,
|
||||||
|
fs: AppFileSystem.Interface,
|
||||||
|
git: Git.Interface,
|
||||||
|
) => Effect.Effect<void, AppFileSystem.Error>
|
||||||
|
|
||||||
export const NotFoundError = NamedError.create(
|
export const NotFoundError = NamedError.create(
|
||||||
"NotFoundError",
|
"NotFoundError",
|
||||||
@@ -83,7 +87,7 @@ export namespace Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MIGRATIONS: Migration[] = [
|
const MIGRATIONS: Migration[] = [
|
||||||
Effect.fn("Storage.migration.1")(function* (dir: string, fs: AppFileSystem.Interface) {
|
Effect.fn("Storage.migration.1")(function* (dir: string, fs: AppFileSystem.Interface, git: Git.Interface) {
|
||||||
const project = path.resolve(dir, "../project")
|
const project = path.resolve(dir, "../project")
|
||||||
if (!(yield* fs.isDir(project))) return
|
if (!(yield* fs.isDir(project))) return
|
||||||
const projectDirs = yield* fs.glob("*", {
|
const projectDirs = yield* fs.glob("*", {
|
||||||
@@ -110,11 +114,9 @@ export namespace Storage {
|
|||||||
}
|
}
|
||||||
if (!worktree) continue
|
if (!worktree) continue
|
||||||
if (!(yield* fs.isDir(worktree))) continue
|
if (!(yield* fs.isDir(worktree))) continue
|
||||||
const result = yield* Effect.promise(() =>
|
const result = yield* git.run(["rev-list", "--max-parents=0", "--all"], {
|
||||||
Git.run(["rev-list", "--max-parents=0", "--all"], {
|
cwd: worktree,
|
||||||
cwd: worktree,
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
const [id] = result
|
const [id] = result
|
||||||
.text()
|
.text()
|
||||||
.split("\n")
|
.split("\n")
|
||||||
@@ -220,6 +222,7 @@ export namespace Storage {
|
|||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const fs = yield* AppFileSystem.Service
|
const fs = yield* AppFileSystem.Service
|
||||||
|
const git = yield* Git.Service
|
||||||
const locks = yield* RcMap.make({
|
const locks = yield* RcMap.make({
|
||||||
lookup: () => TxReentrantLock.make(),
|
lookup: () => TxReentrantLock.make(),
|
||||||
idleTimeToLive: 0,
|
idleTimeToLive: 0,
|
||||||
@@ -236,7 +239,7 @@ export namespace Storage {
|
|||||||
for (let i = migration; i < MIGRATIONS.length; i++) {
|
for (let i = migration; i < MIGRATIONS.length; i++) {
|
||||||
log.info("running migration", { index: i })
|
log.info("running migration", { index: i })
|
||||||
const step = MIGRATIONS[i]!
|
const step = MIGRATIONS[i]!
|
||||||
const exit = yield* Effect.exit(step(dir, fs))
|
const exit = yield* Effect.exit(step(dir, fs, git))
|
||||||
if (Exit.isFailure(exit)) {
|
if (Exit.isFailure(exit)) {
|
||||||
log.error("failed to run migration", { index: i, cause: exit.cause })
|
log.error("failed to run migration", { index: i, cause: exit.cause })
|
||||||
break
|
break
|
||||||
@@ -327,7 +330,7 @@ export namespace Storage {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Git.defaultLayer))
|
||||||
|
|
||||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { tmpdir } from "../fixture/fixture"
|
|||||||
import { Bus } from "../../src/bus"
|
import { Bus } from "../../src/bus"
|
||||||
import { Config } from "../../src/config/config"
|
import { Config } from "../../src/config/config"
|
||||||
import { FileWatcher } from "../../src/file/watcher"
|
import { FileWatcher } from "../../src/file/watcher"
|
||||||
|
import { Git } from "../../src/git"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
|
|
||||||
// Native @parcel/watcher bindings aren't reliably available in CI (missing on Linux, flaky on Windows)
|
// Native @parcel/watcher bindings aren't reliably available in CI (missing on Linux, flaky on Windows)
|
||||||
@@ -32,6 +33,7 @@ function withWatcher<E>(directory: string, body: Effect.Effect<void, E>) {
|
|||||||
fn: async () => {
|
fn: async () => {
|
||||||
const layer: Layer.Layer<FileWatcher.Service, never, never> = FileWatcher.layer.pipe(
|
const layer: Layer.Layer<FileWatcher.Service, never, never> = FileWatcher.layer.pipe(
|
||||||
Layer.provide(Config.defaultLayer),
|
Layer.provide(Config.defaultLayer),
|
||||||
|
Layer.provide(Git.defaultLayer),
|
||||||
Layer.provide(watcherConfigLayer),
|
Layer.provide(watcherConfigLayer),
|
||||||
)
|
)
|
||||||
const rt = ManagedRuntime.make(layer)
|
const rt = ManagedRuntime.make(layer)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import fs from "fs/promises"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Layer, ManagedRuntime } from "effect"
|
import { Effect, Layer, ManagedRuntime } from "effect"
|
||||||
import { AppFileSystem } from "../../src/filesystem"
|
import { AppFileSystem } from "../../src/filesystem"
|
||||||
|
import { Git } from "../../src/git"
|
||||||
import { Global } from "../../src/global"
|
import { Global } from "../../src/global"
|
||||||
import { Storage } from "../../src/storage/storage"
|
import { Storage } from "../../src/storage/storage"
|
||||||
import { tmpdir } from "../fixture/fixture"
|
import { tmpdir } from "../fixture/fixture"
|
||||||
@@ -47,7 +48,7 @@ async function withStorage<T>(
|
|||||||
root: string,
|
root: string,
|
||||||
fn: (run: <A, E>(body: Effect.Effect<A, E, Storage.Service>) => Promise<A>) => Promise<T>,
|
fn: (run: <A, E>(body: Effect.Effect<A, E, Storage.Service>) => Promise<A>) => Promise<T>,
|
||||||
) {
|
) {
|
||||||
const rt = ManagedRuntime.make(Storage.layer.pipe(Layer.provide(layer(root))))
|
const rt = ManagedRuntime.make(Storage.layer.pipe(Layer.provide(layer(root)), Layer.provide(Git.defaultLayer)))
|
||||||
try {
|
try {
|
||||||
return await fn((body) => rt.runPromise(body))
|
return await fn((body) => rt.runPromise(body))
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user