Преглед изворни кода

refactor(reference): normalize config entries (#28178)

Shoubhit Dash пре 3 месеци
родитељ
комит
eb389c58eb

+ 3 - 3
packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx

@@ -19,7 +19,7 @@ import type { PromptInfo } from "./history"
 import { useFrecency } from "./frecency"
 import { useFrecency } from "./frecency"
 import { useBindings } from "../../keymap"
 import { useBindings } from "../../keymap"
 import { Reference } from "@/reference/reference"
 import { Reference } from "@/reference/reference"
-import type { Config } from "@/config/config"
+import { ConfigReference } from "@/config/reference"
 import { displayCharAt, mentionTriggerIndex } from "@/cli/cmd/prompt-display"
 import { displayCharAt, mentionTriggerIndex } from "@/cli/cmd/prompt-display"
 
 
 function removeLineRange(input: string) {
 function removeLineRange(input: string) {
@@ -310,7 +310,7 @@ export function Autocomplete(props: {
       `Referenced configured reference @${reference.name}.`,
       `Referenced configured reference @${reference.name}.`,
       ...(reference.kind === "local" ? ["Kind: local directory"] : []),
       ...(reference.kind === "local" ? ["Kind: local directory"] : []),
       ...(reference.kind === "git" ? ["Kind: git repository"] : []),
       ...(reference.kind === "git" ? ["Kind: git repository"] : []),
-      ...(reference.kind === "invalid" ? [`Repository: ${reference.repository}`] : []),
+      ...(reference.kind === "invalid" && reference.repository ? [`Repository: ${reference.repository}`] : []),
       ...(reference.kind === "git" ? [`Repository: ${reference.repository}`] : []),
       ...(reference.kind === "git" ? [`Repository: ${reference.repository}`] : []),
       ...(reference.kind === "git" && reference.branch ? [`Branch/ref: ${reference.branch}`] : []),
       ...(reference.kind === "git" && reference.branch ? [`Branch/ref: ${reference.branch}`] : []),
       ...(reference.kind === "invalid" ? [] : [`Reference root: ${reference.path}`]),
       ...(reference.kind === "invalid" ? [] : [`Reference root: ${reference.path}`]),
@@ -324,7 +324,7 @@ export function Autocomplete(props: {
 
 
   const references = createMemo(() =>
   const references = createMemo(() =>
     Reference.resolveAll({
     Reference.resolveAll({
-      references: (sync.data.config.reference ?? {}) as NonNullable<Config.Info["reference"]>,
+      references: ConfigReference.normalize(sync.data.config.reference ?? {}),
       directory: sync.path.directory || process.cwd(),
       directory: sync.path.directory || process.cwd(),
       worktree: sync.path.worktree || sync.path.directory || process.cwd(),
       worktree: sync.path.worktree || sync.path.directory || process.cwd(),
     }),
     }),

+ 46 - 0
packages/opencode/src/config/reference.ts

@@ -18,6 +18,52 @@ const Local = Schema.Struct({
 })
 })
 
 
 export const Entry = Schema.Union([Schema.String, Git, Local]).annotate({ identifier: "ReferenceConfigEntry" })
 export const Entry = Schema.Union([Schema.String, Git, Local]).annotate({ identifier: "ReferenceConfigEntry" })
+export type Entry = Schema.Schema.Type<typeof Entry>
 
 
 export const Info = Schema.Record(Schema.String, Entry).annotate({ identifier: "ReferenceConfig" })
 export const Info = Schema.Record(Schema.String, Entry).annotate({ identifier: "ReferenceConfig" })
 export type Info = Schema.Schema.Type<typeof Info>
 export type Info = Schema.Schema.Type<typeof Info>
+
+export type NormalizedEntry =
+  | {
+      kind: "local"
+      path: string
+    }
+  | {
+      kind: "git"
+      repository: string
+      branch?: string
+    }
+  | {
+      kind: "invalid"
+      message: string
+    }
+
+export type NormalizedInfo = Record<string, NormalizedEntry>
+
+export function validateAlias(name: string) {
+  if (name.length === 0) return "Reference alias must not be empty"
+  if (/[\/\s`,]/.test(name)) {
+    return "Reference alias must not contain /, whitespace, comma, or backtick"
+  }
+}
+
+export function normalizeEntry(entry: Entry): NormalizedEntry {
+  if (typeof entry === "string") {
+    if (entry.startsWith(".") || entry.startsWith("/") || entry.startsWith("~")) {
+      return { kind: "local", path: entry }
+    }
+    return { kind: "git", repository: entry }
+  }
+
+  if ("path" in entry) return { kind: "local", path: entry.path }
+  return { kind: "git", repository: entry.repository, branch: entry.branch }
+}
+
+export function normalize(info: Info): NormalizedInfo {
+  return Object.fromEntries(
+    Object.entries(info).map(([name, entry]) => {
+      const aliasError = validateAlias(name)
+      return [name, aliasError ? { kind: "invalid" as const, message: aliasError } : normalizeEntry(entry)] as const
+    }),
+  )
+}

+ 8 - 14
packages/opencode/src/reference/reference.ts

@@ -3,14 +3,13 @@ import { Effect, Context, Layer, Scope } from "effect"
 import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { Global } from "@opencode-ai/core/global"
 import { Global } from "@opencode-ai/core/global"
 import { Config } from "@/config/config"
 import { Config } from "@/config/config"
+import { ConfigReference } from "@/config/reference"
 import { InstanceState } from "@/effect/instance-state"
 import { InstanceState } from "@/effect/instance-state"
 import { RuntimeFlags } from "@/effect/runtime-flags"
 import { RuntimeFlags } from "@/effect/runtime-flags"
 import { Git } from "@/git"
 import { Git } from "@/git"
 import { parseRepositoryReference, repositoryCachePath, type Reference as RepositoryReference } from "@/util/repository"
 import { parseRepositoryReference, repositoryCachePath, type Reference as RepositoryReference } from "@/util/repository"
 import { RepositoryCache } from "./repository-cache"
 import { RepositoryCache } from "./repository-cache"
 
 
-type ReferenceEntry = NonNullable<Config.Info["reference"]>[string]
-
 export type Resolved =
 export type Resolved =
   | {
   | {
       name: string
       name: string
@@ -28,7 +27,7 @@ export type Resolved =
   | {
   | {
       name: string
       name: string
       kind: "invalid"
       kind: "invalid"
-      repository: string
+      repository?: string
       message: string
       message: string
     }
     }
 
 
@@ -92,26 +91,21 @@ function containsReferencePath(referencePath: string, target: string) {
 
 
 export function resolve(input: {
 export function resolve(input: {
   name: string
   name: string
-  reference: ReferenceEntry
+  reference: ConfigReference.NormalizedEntry
   directory: string
   directory: string
   worktree: string
   worktree: string
 }): Resolved {
 }): Resolved {
-  if (typeof input.reference === "string") {
-    if (input.reference.startsWith(".") || input.reference.startsWith("/") || input.reference.startsWith("~")) {
-      return { name: input.name, kind: "local", path: referencePath({ ...input, value: input.reference }) }
-    }
-    return resolveGit({ name: input.name, repository: input.reference })
+  if (input.reference.kind === "invalid") {
+    return { name: input.name, kind: "invalid", message: input.reference.message }
   }
   }
-
-  if ("path" in input.reference) {
+  if (input.reference.kind === "local") {
     return { name: input.name, kind: "local", path: referencePath({ ...input, value: input.reference.path }) }
     return { name: input.name, kind: "local", path: referencePath({ ...input, value: input.reference.path }) }
   }
   }
-
   return resolveGit({ name: input.name, repository: input.reference.repository, branch: input.reference.branch })
   return resolveGit({ name: input.name, repository: input.reference.repository, branch: input.reference.branch })
 }
 }
 
 
 export function resolveAll(input: {
 export function resolveAll(input: {
-  references: NonNullable<Config.Info["reference"]>
+  references: ConfigReference.NormalizedInfo
   directory: string
   directory: string
   worktree: string
   worktree: string
 }) {
 }) {
@@ -149,7 +143,7 @@ export const layer = Layer.effect(
       Effect.fn("Reference.state")(function* (ctx) {
       Effect.fn("Reference.state")(function* (ctx) {
         const cfg = yield* config.get()
         const cfg = yield* config.get()
         const references = resolveAll({
         const references = resolveAll({
-          references: cfg.reference ?? {},
+          references: ConfigReference.normalize(cfg.reference ?? {}),
           directory: ctx.directory,
           directory: ctx.directory,
           worktree: ctx.worktree,
           worktree: ctx.worktree,
         })
         })

+ 28 - 6
packages/opencode/test/reference/reference.test.ts

@@ -5,6 +5,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { Global } from "@opencode-ai/core/global"
 import { Global } from "@opencode-ai/core/global"
 import { Config } from "../../src/config/config"
 import { Config } from "../../src/config/config"
+import { ConfigReference } from "../../src/config/reference"
 import { RuntimeFlags } from "../../src/effect/runtime-flags"
 import { RuntimeFlags } from "../../src/effect/runtime-flags"
 import { Git } from "../../src/git"
 import { Git } from "../../src/git"
 import { Reference } from "../../src/reference/reference"
 import { Reference } from "../../src/reference/reference"
@@ -86,25 +87,25 @@ describe("reference", () => {
       const root = path.resolve("opencode-reference-root")
       const root = path.resolve("opencode-reference-root")
       const local = Reference.resolve({
       const local = Reference.resolve({
         name: "docs",
         name: "docs",
-        reference: { path: "../docs" },
+        reference: ConfigReference.normalizeEntry({ path: "../docs" }),
         directory: path.join(root, "packages", "app"),
         directory: path.join(root, "packages", "app"),
         worktree: root,
         worktree: root,
       })
       })
       const repo = Reference.resolve({
       const repo = Reference.resolve({
         name: "effect",
         name: "effect",
-        reference: { repository: "Effect-TS/effect", branch: "main" },
+        reference: ConfigReference.normalizeEntry({ repository: "Effect-TS/effect", branch: "main" }),
         directory: path.join(root, "packages", "app"),
         directory: path.join(root, "packages", "app"),
         worktree: root,
         worktree: root,
       })
       })
       const localString = Reference.resolve({
       const localString = Reference.resolve({
         name: "notes",
         name: "notes",
-        reference: "./notes",
+        reference: ConfigReference.normalizeEntry("./notes"),
         directory: path.join(root, "packages", "app"),
         directory: path.join(root, "packages", "app"),
         worktree: root,
         worktree: root,
       })
       })
       const repoString = Reference.resolve({
       const repoString = Reference.resolve({
         name: "repo",
         name: "repo",
-        reference: "owner/repo",
+        reference: ConfigReference.normalizeEntry("owner/repo"),
         directory: path.join(root, "packages", "app"),
         directory: path.join(root, "packages", "app"),
         worktree: root,
         worktree: root,
       })
       })
@@ -159,11 +160,11 @@ describe("reference", () => {
       const references = Reference.resolveAll({
       const references = Reference.resolveAll({
         directory: root,
         directory: root,
         worktree: root,
         worktree: root,
-        references: {
+        references: ConfigReference.normalize({
           main: { repository: "owner/repo", branch: "main" },
           main: { repository: "owner/repo", branch: "main" },
           dev: { repository: "github.com/owner/repo", branch: "dev" },
           dev: { repository: "github.com/owner/repo", branch: "dev" },
           alsoMain: { repository: "https://github.com/owner/repo", branch: "main" },
           alsoMain: { repository: "https://github.com/owner/repo", branch: "main" },
-        },
+        }),
       })
       })
 
 
       expect(references.map((reference) => reference.kind)).toEqual(["git", "invalid", "git"])
       expect(references.map((reference) => reference.kind)).toEqual(["git", "invalid", "git"])
@@ -175,6 +176,27 @@ describe("reference", () => {
     }),
     }),
   )
   )
 
 
+  it.live("represents invalid aliases as invalid references", () =>
+    Effect.gen(function* () {
+      const root = path.resolve("opencode-reference-root")
+      const references = Reference.resolveAll({
+        directory: root,
+        worktree: root,
+        references: ConfigReference.normalize({
+          "bad/name": "owner/repo",
+        }),
+      })
+
+      expect(references).toEqual([
+        {
+          name: "bad/name",
+          kind: "invalid",
+          message: "Reference alias must not contain /, whitespace, comma, or backtick",
+        },
+      ])
+    }),
+  )
+
   scout.live("materializes configured git references during init", () =>
   scout.live("materializes configured git references during init", () =>
     provideTmpdirInstance(
     provideTmpdirInstance(
       (_dir) =>
       (_dir) =>