Jelajahi Sumber

fix(repository): type expected reference failures (#28880)

Shoubhit Dash 2 bulan lalu
induk
melakukan
aee552c043

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

@@ -7,8 +7,11 @@ import {
   repositoryCachePath,
   sameRepositoryReference,
   parseRepositoryReference,
+  parseRemoteRepositoryReference,
   validateRepositoryBranch,
-  isRemoteRepositoryReference,
+  InvalidRepositoryBranchError,
+  InvalidRepositoryReferenceError,
+  UnsupportedLocalRepositoryError,
   type RemoteReference,
 } from "@/util/repository"
 
@@ -138,23 +141,26 @@ export function isError(error: unknown): error is Error {
 }
 
 export const parseRemoteReference = Effect.fn("RepositoryCache.parseRemoteReference")(function* (repository: string) {
-  const reference = parseRepositoryReference(repository)
-  if (!reference) {
+  try {
+    return parseRemoteRepositoryReference(repository)
+  } catch (error) {
+    if (error instanceof InvalidRepositoryReferenceError || error instanceof UnsupportedLocalRepositoryError) {
+      return yield* new InvalidRepositoryError({ repository: error.repository, message: error.message })
+    }
     return yield* new InvalidRepositoryError({
       repository,
-      message: "Repository must be a git URL, host/path reference, or GitHub owner/repo shorthand",
+      message: errorMessage(error),
     })
   }
-  if (!isRemoteRepositoryReference(reference)) {
-    return yield* new InvalidRepositoryError({ repository, message: "Local file repositories are not supported" })
-  }
-  return reference
 })
 
 export const validateBranch = Effect.fn("RepositoryCache.validateBranch")(function* (branch: string) {
   try {
     validateRepositoryBranch(branch)
   } catch (error) {
+    if (error instanceof InvalidRepositoryBranchError) {
+      return yield* new InvalidBranchError({ branch: error.branch, message: error.message })
+    }
     return yield* new InvalidBranchError({ branch, message: errorMessage(error) })
   }
 })

+ 54 - 5
packages/opencode/src/util/repository.ts

@@ -1,5 +1,6 @@
 import path from "path"
 import { fileURLToPath } from "url"
+import { Schema } from "effect"
 import { Global } from "@opencode-ai/core/global"
 
 type BaseReference = {
@@ -23,6 +24,43 @@ export type FileReference = BaseReference & {
 
 export type Reference = RemoteReference | FileReference
 
+export class InvalidRepositoryReferenceError extends Schema.TaggedErrorClass<InvalidRepositoryReferenceError>()(
+  "RepositoryInvalidReferenceError",
+  {
+    repository: Schema.String,
+    message: Schema.String,
+  },
+) {}
+
+export class UnsupportedLocalRepositoryError extends Schema.TaggedErrorClass<UnsupportedLocalRepositoryError>()(
+  "RepositoryUnsupportedLocalRepositoryError",
+  {
+    repository: Schema.String,
+    message: Schema.String,
+  },
+) {}
+
+export class InvalidRepositoryBranchError extends Schema.TaggedErrorClass<InvalidRepositoryBranchError>()(
+  "RepositoryInvalidBranchError",
+  {
+    branch: Schema.String,
+    message: Schema.String,
+  },
+) {}
+
+export type RepositoryError =
+  | InvalidRepositoryReferenceError
+  | UnsupportedLocalRepositoryError
+  | InvalidRepositoryBranchError
+
+export function isRepositoryError(error: unknown): error is RepositoryError {
+  return (
+    error instanceof InvalidRepositoryReferenceError ||
+    error instanceof UnsupportedLocalRepositoryError ||
+    error instanceof InvalidRepositoryBranchError
+  )
+}
+
 function normalizeRepositoryInput(input: string) {
   return input
     .trim()
@@ -147,16 +185,27 @@ export function isRemoteRepositoryReference(reference: Reference): reference is
 
 export function parseRemoteRepositoryReference(input: string) {
   const reference = parseRepositoryReference(input)
-  if (!reference) throw new Error("Repository must be a git URL, host/path reference, or GitHub owner/repo shorthand")
-  if (!isRemoteRepositoryReference(reference)) throw new Error("Local file repositories are not supported")
+  if (!reference) {
+    throw new InvalidRepositoryReferenceError({
+      repository: input,
+      message: "Repository must be a git URL, host/path reference, or GitHub owner/repo shorthand",
+    })
+  }
+  if (!isRemoteRepositoryReference(reference)) {
+    throw new UnsupportedLocalRepositoryError({
+      repository: input,
+      message: "Local file repositories are not supported",
+    })
+  }
   return reference
 }
 
 export function validateRepositoryBranch(branch: string) {
   if (!/^[A-Za-z0-9/_.-]+$/.test(branch) || branch.startsWith("-") || branch.includes("..")) {
-    throw new Error(
-      "Branch must contain only alphanumeric characters, /, _, ., and -, and cannot start with - or contain ..",
-    )
+    throw new InvalidRepositoryBranchError({
+      branch,
+      message: "Branch must contain only alphanumeric characters, /, _, ., and -, and cannot start with - or contain ..",
+    })
   }
 }
 

+ 12 - 0
packages/opencode/test/util/repository.test.ts

@@ -3,6 +3,9 @@ import path from "path"
 import { pathToFileURL } from "url"
 import { Global } from "@opencode-ai/core/global"
 import {
+  InvalidRepositoryBranchError,
+  InvalidRepositoryReferenceError,
+  UnsupportedLocalRepositoryError,
   isFileRepositoryReference,
   isRemoteRepositoryReference,
   parseRemoteRepositoryReference,
@@ -61,6 +64,14 @@ describe("util.repository", () => {
     expect(() => parseRemoteRepositoryReference(pathToFileURL(localPath).href)).toThrow(
       "Local file repositories are not supported",
     )
+    expect(() => parseRemoteRepositoryReference(pathToFileURL(localPath).href)).toThrow(UnsupportedLocalRepositoryError)
+  })
+
+  test("rejects invalid remote repository references with typed errors", () => {
+    expect(() => parseRemoteRepositoryReference("not-a-repo")).toThrow(InvalidRepositoryReferenceError)
+    expect(() => parseRemoteRepositoryReference("git@github.com:../../../etc/passwd")).toThrow(
+      InvalidRepositoryReferenceError,
+    )
   })
 
   test("compares cache identity independent of input spelling", () => {
@@ -77,5 +88,6 @@ describe("util.repository", () => {
     expect(() => validateRepositoryBranch("-bad")).toThrow("Branch must contain only alphanumeric characters")
     expect(() => validateRepositoryBranch("bad..branch")).toThrow("Branch must contain only alphanumeric characters")
     expect(() => validateRepositoryBranch("bad branch")).toThrow("Branch must contain only alphanumeric characters")
+    expect(() => validateRepositoryBranch("bad branch")).toThrow(InvalidRepositoryBranchError)
   })
 })