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
3 changed files with 80 additions and 13 deletions

View File

@@ -7,8 +7,11 @@ import {
repositoryCachePath, repositoryCachePath,
sameRepositoryReference, sameRepositoryReference,
parseRepositoryReference, parseRepositoryReference,
parseRemoteRepositoryReference,
validateRepositoryBranch, validateRepositoryBranch,
isRemoteRepositoryReference, InvalidRepositoryBranchError,
InvalidRepositoryReferenceError,
UnsupportedLocalRepositoryError,
type RemoteReference, type RemoteReference,
} from "@/util/repository" } 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) { export const parseRemoteReference = Effect.fn("RepositoryCache.parseRemoteReference")(function* (repository: string) {
const reference = parseRepositoryReference(repository) try {
if (!reference) { 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({ return yield* new InvalidRepositoryError({
repository, 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) { export const validateBranch = Effect.fn("RepositoryCache.validateBranch")(function* (branch: string) {
try { try {
validateRepositoryBranch(branch) validateRepositoryBranch(branch)
} catch (error) { } catch (error) {
if (error instanceof InvalidRepositoryBranchError) {
return yield* new InvalidBranchError({ branch: error.branch, message: error.message })
}
return yield* new InvalidBranchError({ branch, message: errorMessage(error) }) return yield* new InvalidBranchError({ branch, message: errorMessage(error) })
} }
}) })

View File

@@ -1,5 +1,6 @@
import path from "path" import path from "path"
import { fileURLToPath } from "url" import { fileURLToPath } from "url"
import { Schema } from "effect"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
type BaseReference = { type BaseReference = {
@@ -23,6 +24,43 @@ export type FileReference = BaseReference & {
export type Reference = RemoteReference | FileReference 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) { function normalizeRepositoryInput(input: string) {
return input return input
.trim() .trim()
@@ -147,16 +185,27 @@ export function isRemoteRepositoryReference(reference: Reference): reference is
export function parseRemoteRepositoryReference(input: string) { export function parseRemoteRepositoryReference(input: string) {
const reference = parseRepositoryReference(input) const reference = parseRepositoryReference(input)
if (!reference) throw new Error("Repository must be a git URL, host/path reference, or GitHub owner/repo shorthand") if (!reference) {
if (!isRemoteRepositoryReference(reference)) throw new Error("Local file repositories are not supported") 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 return reference
} }
export function validateRepositoryBranch(branch: string) { export function validateRepositoryBranch(branch: string) {
if (!/^[A-Za-z0-9/_.-]+$/.test(branch) || branch.startsWith("-") || branch.includes("..")) { if (!/^[A-Za-z0-9/_.-]+$/.test(branch) || branch.startsWith("-") || branch.includes("..")) {
throw new Error( throw new InvalidRepositoryBranchError({
"Branch must contain only alphanumeric characters, /, _, ., and -, and cannot start with - or contain ..", 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 { pathToFileURL } from "url"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { import {
InvalidRepositoryBranchError,
InvalidRepositoryReferenceError,
UnsupportedLocalRepositoryError,
isFileRepositoryReference, isFileRepositoryReference,
isRemoteRepositoryReference, isRemoteRepositoryReference,
parseRemoteRepositoryReference, parseRemoteRepositoryReference,
@@ -61,6 +64,14 @@ describe("util.repository", () => {
expect(() => parseRemoteRepositoryReference(pathToFileURL(localPath).href)).toThrow( expect(() => parseRemoteRepositoryReference(pathToFileURL(localPath).href)).toThrow(
"Local file repositories are not supported", "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", () => { 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")).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("Branch must contain only alphanumeric characters") expect(() => validateRepositoryBranch("bad branch")).toThrow("Branch must contain only alphanumeric characters")
expect(() => validateRepositoryBranch("bad branch")).toThrow(InvalidRepositoryBranchError)
}) })
}) })