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

This commit is contained in:
Shoubhit Dash 2026-05-22 23:13:14 +05:30 committed by GitHub
parent 4f6eaf859b
commit aee552c043
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 80 additions and 13 deletions

View file

@ -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) })
}
})

View file

@ -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 ..",
})
}
}

View file

@ -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)
})
})