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

This commit is contained in:
Shoubhit Dash 2026-05-18 20:42:41 +05:30 committed by GitHub
parent 54ff0a669b
commit eb389c58eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 85 additions and 23 deletions

View file

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

View file

@ -18,6 +18,52 @@ const Local = Schema.Struct({
})
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 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
}),
)
}

View file

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

View file

@ -5,6 +5,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Global } from "@opencode-ai/core/global"
import { Config } from "../../src/config/config"
import { ConfigReference } from "../../src/config/reference"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { Git } from "../../src/git"
import { Reference } from "../../src/reference/reference"
@ -86,25 +87,25 @@ describe("reference", () => {
const root = path.resolve("opencode-reference-root")
const local = Reference.resolve({
name: "docs",
reference: { path: "../docs" },
reference: ConfigReference.normalizeEntry({ path: "../docs" }),
directory: path.join(root, "packages", "app"),
worktree: root,
})
const repo = Reference.resolve({
name: "effect",
reference: { repository: "Effect-TS/effect", branch: "main" },
reference: ConfigReference.normalizeEntry({ repository: "Effect-TS/effect", branch: "main" }),
directory: path.join(root, "packages", "app"),
worktree: root,
})
const localString = Reference.resolve({
name: "notes",
reference: "./notes",
reference: ConfigReference.normalizeEntry("./notes"),
directory: path.join(root, "packages", "app"),
worktree: root,
})
const repoString = Reference.resolve({
name: "repo",
reference: "owner/repo",
reference: ConfigReference.normalizeEntry("owner/repo"),
directory: path.join(root, "packages", "app"),
worktree: root,
})
@ -159,11 +160,11 @@ describe("reference", () => {
const references = Reference.resolveAll({
directory: root,
worktree: root,
references: {
references: ConfigReference.normalize({
main: { repository: "owner/repo", branch: "main" },
dev: { repository: "github.com/owner/repo", branch: "dev" },
alsoMain: { repository: "https://github.com/owner/repo", branch: "main" },
},
}),
})
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", () =>
provideTmpdirInstance(
(_dir) =>