@@ -101,9 +99,9 @@ export default async function BrowsePage({
) : (
)}
@@ -114,21 +112,21 @@ interface CodePreviewWrapper {
path: string,
repoName: string,
revisionName: string,
- orgId: number,
+ domain: string,
}
const CodePreviewWrapper = async ({
path,
repoName,
revisionName,
- orgId,
+ domain,
}: CodePreviewWrapper) => {
// @todo: this will depend on `pathType`.
const fileSourceResponse = await getFileSource({
fileName: path,
repository: repoName,
branch: revisionName,
- }, orgId);
+ }, domain);
if (isServiceError(fileSourceResponse)) {
if (fileSourceResponse.errorCode === ErrorCode.FILE_NOT_FOUND) {
diff --git a/packages/web/src/app/[domain]/components/fileHeader.tsx b/packages/web/src/app/[domain]/components/fileHeader.tsx
index 0ff343f8..852e7c50 100644
--- a/packages/web/src/app/[domain]/components/fileHeader.tsx
+++ b/packages/web/src/app/[domain]/components/fileHeader.tsx
@@ -1,17 +1,22 @@
-import { Repository } from "@/features/search/types";
-import { getRepoCodeHostInfo } from "@/lib/utils";
+
+import { getCodeHostInfoForRepo } from "@/lib/utils";
import { LaptopIcon } from "@radix-ui/react-icons";
import clsx from "clsx";
import Image from "next/image";
import Link from "next/link";
interface FileHeaderProps {
- repo?: Repository;
fileName: string;
fileNameHighlightRange?: {
from: number;
to: number;
}
+ repo: {
+ name: string;
+ codeHostType: string;
+ displayName?: string;
+ webUrl?: string;
+ },
branchDisplayName?: string;
branchDisplayTitle?: string;
}
@@ -23,7 +28,12 @@ export const FileHeader = ({
branchDisplayName,
branchDisplayTitle,
}: FileHeaderProps) => {
- const info = getRepoCodeHostInfo(repo);
+ const info = getCodeHostInfoForRepo({
+ name: repo.name,
+ codeHostType: repo.codeHostType,
+ displayName: repo.displayName,
+ webUrl: repo.webUrl,
+ });
return (
diff --git a/packages/web/src/app/[domain]/components/repositoryCarousel.tsx b/packages/web/src/app/[domain]/components/repositoryCarousel.tsx
index 0ce235f2..2bbc9024 100644
--- a/packages/web/src/app/[domain]/components/repositoryCarousel.tsx
+++ b/packages/web/src/app/[domain]/components/repositoryCarousel.tsx
@@ -6,7 +6,7 @@ import {
CarouselItem,
} from "@/components/ui/carousel";
import Autoscroll from "embla-carousel-auto-scroll";
-import { getRepoQueryCodeHostInfo } from "@/lib/utils";
+import { getCodeHostInfoForRepo } from "@/lib/utils";
import Image from "next/image";
import { FileIcon } from "@radix-ui/react-icons";
import clsx from "clsx";
@@ -57,7 +57,12 @@ const RepositoryBadge = ({
repo
}: RepositoryBadgeProps) => {
const { repoIcon, displayName, repoLink } = (() => {
- const info = getRepoQueryCodeHostInfo(repo);
+ const info = getCodeHostInfoForRepo({
+ codeHostType: repo.codeHostType,
+ name: repo.repoName,
+ displayName: repo.repoDisplayName,
+ webUrl: repo.webUrl,
+ });
if (info) {
return {
diff --git a/packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx b/packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx
index 0d20b9a6..b3ea530b 100644
--- a/packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx
+++ b/packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx
@@ -46,7 +46,7 @@ export const CodePreviewPanel = ({
content: decodedSource,
filepath: fileMatch.fileName.text,
matches: fileMatch.chunks,
- link: fileMatch.url,
+ link: fileMatch.webUrl,
language: fileMatch.language,
revision: branch ?? "HEAD",
};
diff --git a/packages/web/src/app/[domain]/search/components/filterPanel/index.tsx b/packages/web/src/app/[domain]/search/components/filterPanel/index.tsx
index ab623fb6..bb799587 100644
--- a/packages/web/src/app/[domain]/search/components/filterPanel/index.tsx
+++ b/packages/web/src/app/[domain]/search/components/filterPanel/index.tsx
@@ -1,8 +1,8 @@
'use client';
import { FileIcon } from "@/components/ui/fileIcon";
-import { Repository, SearchResultFile } from "@/features/search/types";
-import { cn, getRepoCodeHostInfo } from "@/lib/utils";
+import { RepositoryInfo, SearchResultFile } from "@/features/search/types";
+import { cn, getCodeHostInfoForRepo } from "@/lib/utils";
import { LaptopIcon } from "@radix-ui/react-icons";
import Image from "next/image";
import { useRouter, useSearchParams } from "next/navigation";
@@ -13,7 +13,7 @@ import { Filter } from "./filter";
interface FilePanelProps {
matches: SearchResultFile[];
onFilterChanged: (filteredMatches: SearchResultFile[]) => void,
- repoMetadata: Record
;
+ repoInfo: Record;
}
const LANGUAGES_QUERY_PARAM = "langs";
@@ -22,7 +22,7 @@ const REPOS_QUERY_PARAM = "repos";
export const FilterPanel = ({
matches,
onFilterChanged,
- repoMetadata,
+ repoInfo,
}: FilePanelProps) => {
const router = useRouter();
const searchParams = useSearchParams();
@@ -38,9 +38,16 @@ export const FilterPanel = ({
return aggregateMatches(
"repository",
matches,
- (key) => {
- const repo: Repository | undefined = repoMetadata[key];
- const info = getRepoCodeHostInfo(repo);
+ ({ key, match }) => {
+ const repo: RepositoryInfo | undefined = repoInfo[match.repositoryId];
+
+ const info = repo ? getCodeHostInfoForRepo({
+ name: repo.name,
+ codeHostType: repo.codeHostType,
+ displayName: repo.displayName,
+ webUrl: repo.webUrl,
+ }) : undefined;
+
const Icon = info ? (
{
const selectedLanguages = getSelectedFromQuery(LANGUAGES_QUERY_PARAM);
return aggregateMatches(
"language",
matches,
- (key) => {
+ ({ key }) => {
const Icon = (
)
@@ -168,14 +175,14 @@ export const FilterPanel = ({
const aggregateMatches = (
propName: 'repository' | 'language',
matches: SearchResultFile[],
- createEntry: (key: string) => Entry
+ createEntry: (props: { key: string, match: SearchResultFile }) => Entry
) => {
return matches
- .map((match) => match[propName])
- .filter((key) => key.length > 0)
- .reduce((aggregation, key) => {
+ .map((match) => ({ key: match[propName], match }))
+ .filter(({ key }) => key.length > 0)
+ .reduce((aggregation, { key, match }) => {
if (!aggregation[key]) {
- aggregation[key] = createEntry(key);
+ aggregation[key] = createEntry({ key, match });
}
aggregation[key].count += 1;
return aggregation;
diff --git a/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatchContainer.tsx b/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatchContainer.tsx
index 2efddcc7..813fe10a 100644
--- a/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatchContainer.tsx
+++ b/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatchContainer.tsx
@@ -5,7 +5,7 @@ import { Separator } from "@/components/ui/separator";
import { DoubleArrowDownIcon, DoubleArrowUpIcon } from "@radix-ui/react-icons";
import { useCallback, useMemo } from "react";
import { FileMatch } from "./fileMatch";
-import { Repository, SearchResultFile } from "@/features/search/types";
+import { RepositoryInfo, SearchResultFile } from "@/features/search/types";
export const MAX_MATCHES_TO_PREVIEW = 3;
@@ -16,7 +16,7 @@ interface FileMatchContainerProps {
showAllMatches: boolean;
onShowAllMatchesButtonClicked: () => void;
isBranchFilteringEnabled: boolean;
- repoMetadata: Record;
+ repoInfo: Record;
yOffset: number;
}
@@ -27,7 +27,7 @@ export const FileMatchContainer = ({
showAllMatches,
onShowAllMatchesButtonClicked,
isBranchFilteringEnabled,
- repoMetadata,
+ repoInfo,
yOffset,
}: FileMatchContainerProps) => {
@@ -87,6 +87,10 @@ export const FileMatchContainer = ({
return `${branches[0]}${branches.length > 1 ? ` +${branches.length - 1}` : ''}`;
}, [isBranchFilteringEnabled, branches]);
+ const repo = useMemo(() => {
+ return repoInfo[file.repositoryId];
+ }, [repoInfo, file.repositoryId]);
+
return (
@@ -101,7 +105,12 @@ export const FileMatchContainer = ({
}}
>
void;
isBranchFilteringEnabled: boolean;
- repoMetadata: Record;
+ repoInfo: Record;
}
const ESTIMATED_LINE_HEIGHT_PX = 20;
@@ -26,7 +26,7 @@ export const SearchResultsPanel = ({
isLoadMoreButtonVisible,
onLoadMoreButtonClicked,
isBranchFilteringEnabled,
- repoMetadata,
+ repoInfo,
}: SearchResultsPanelProps) => {
const parentRef = useRef(null);
const [showAllMatchesStates, setShowAllMatchesStates] = useState(Array(fileMatches.length).fill(false));
@@ -151,7 +151,7 @@ export const SearchResultsPanel = ({
onShowAllMatchesButtonClicked(virtualRow.index);
}}
isBranchFilteringEnabled={isBranchFilteringEnabled}
- repoMetadata={repoMetadata}
+ repoInfo={repoInfo}
yOffset={virtualRow.start}
/>
diff --git a/packages/web/src/app/[domain]/search/page.tsx b/packages/web/src/app/[domain]/search/page.tsx
index f719d2b0..ac793ff8 100644
--- a/packages/web/src/app/[domain]/search/page.tsx
+++ b/packages/web/src/app/[domain]/search/page.tsx
@@ -16,14 +16,14 @@ import { useQuery } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ImperativePanelHandle } from "react-resizable-panels";
-import { getRepos, search } from "../../api/(client)/client";
+import { search } from "../../api/(client)/client";
import { TopBar } from "../components/topBar";
import { CodePreviewPanel } from "./components/codePreviewPanel";
import { FilterPanel } from "./components/filterPanel";
import { SearchResultsPanel } from "./components/searchResultsPanel";
import { useDomain } from "@/hooks/useDomain";
import { useToast } from "@/components/hooks/use-toast";
-import { Repository, SearchResultFile } from "@/features/search/types";
+import { RepositoryInfo, SearchResultFile } from "@/features/search/types";
const DEFAULT_MATCH_COUNT = 10000;
@@ -90,25 +90,6 @@ const SearchPageInternal = () => {
])
}, [searchQuery, setSearchHistory]);
- // Use the /api/repos endpoint to get a useful list of
- // repository metadata (like host type, repo name, etc.)
- // Convert this into a map of repo name to repo metadata
- // for easy lookup.
- const { data: repoMetadata, isLoading: isRepoMetadataLoading } = useQuery({
- queryKey: ["repos"],
- queryFn: () => getRepos(domain),
- select: (data): Record =>
- data.repos
- .reduce(
- (acc, repo) => ({
- ...acc,
- [repo.name]: repo,
- }),
- {},
- ),
- refetchOnWindowFocus: false,
- });
-
useEffect(() => {
if (!searchResponse) {
return;
@@ -141,13 +122,14 @@ const SearchPageInternal = () => {
});
}, [captureEvent, searchQuery, searchResponse]);
- const { fileMatches, searchDurationMs, totalMatchCount, isBranchFilteringEnabled } = useMemo(() => {
+ const { fileMatches, searchDurationMs, totalMatchCount, isBranchFilteringEnabled, repositoryInfo } = useMemo(() => {
if (!searchResponse) {
return {
fileMatches: [],
searchDurationMs: 0,
totalMatchCount: 0,
isBranchFilteringEnabled: false,
+ repositoryInfo: {},
};
}
@@ -156,6 +138,10 @@ const SearchPageInternal = () => {
searchDurationMs: Math.round(searchResponse.durationMs),
totalMatchCount: searchResponse.zoektStats.matchCount,
isBranchFilteringEnabled: searchResponse.isBranchFilteringEnabled,
+ repositoryInfo: searchResponse.repositoryInfo.reduce((acc, repo) => {
+ acc[repo.id] = repo;
+ return acc;
+ }, {} as Record),
}
}, [searchResponse]);
@@ -194,7 +180,7 @@ const SearchPageInternal = () => {
- {(isSearchLoading || isRepoMetadataLoading) ? (
+ {(isSearchLoading) ? (
Searching...
@@ -205,7 +191,7 @@ const SearchPageInternal = () => {
isMoreResultsButtonVisible={isMoreResultsButtonVisible}
onLoadMoreResults={onLoadMoreResults}
isBranchFilteringEnabled={isBranchFilteringEnabled}
- repoMetadata={repoMetadata ?? {}}
+ repoInfo={repositoryInfo}
searchDurationMs={searchDurationMs}
numMatches={numMatches}
/>
@@ -219,7 +205,7 @@ interface PanelGroupProps {
isMoreResultsButtonVisible?: boolean;
onLoadMoreResults: () => void;
isBranchFilteringEnabled: boolean;
- repoMetadata: Record
;
+ repoInfo: Record;
searchDurationMs: number;
numMatches: number;
}
@@ -229,7 +215,7 @@ const PanelGroup = ({
isMoreResultsButtonVisible,
onLoadMoreResults,
isBranchFilteringEnabled,
- repoMetadata,
+ repoInfo,
searchDurationMs,
numMatches,
}: PanelGroupProps) => {
@@ -267,7 +253,7 @@ const PanelGroup = ({
) : (
diff --git a/packages/web/src/app/api/(server)/repos/route.ts b/packages/web/src/app/api/(server)/repos/route.ts
index 893c3c49..20048ffa 100644
--- a/packages/web/src/app/api/(server)/repos/route.ts
+++ b/packages/web/src/app/api/(server)/repos/route.ts
@@ -2,25 +2,24 @@
import { listRepositories } from "@/features/search/listReposApi";
import { NextRequest } from "next/server";
-import { sew, withAuth, withOrgMembership } from "@/actions";
import { isServiceError } from "@/lib/utils";
import { serviceErrorResponse } from "@/lib/serviceError";
+import { StatusCodes } from "http-status-codes";
+import { ErrorCode } from "@/lib/errorCodes";
export const GET = async (request: NextRequest) => {
- const domain = request.headers.get("X-Org-Domain")!;
- const response = await getRepos(domain);
+ const domain = request.headers.get("X-Org-Domain");
+ if (!domain) {
+ return serviceErrorResponse({
+ statusCode: StatusCodes.BAD_REQUEST,
+ errorCode: ErrorCode.MISSING_ORG_DOMAIN_HEADER,
+ message: "Missing X-Org-Domain header",
+ });
+ }
+ const response = await listRepositories(domain);
if (isServiceError(response)) {
return serviceErrorResponse(response);
}
return Response.json(response);
}
-
-
-const getRepos = (domain: string) => sew(() =>
- withAuth((session) =>
- withOrgMembership(session, domain, async ({ orgId }) => {
- const response = await listRepositories(orgId);
- return response;
- }
- ), /* allowSingleTenantUnauthedAccess */ true));
\ No newline at end of file
diff --git a/packages/web/src/app/api/(server)/search/route.ts b/packages/web/src/app/api/(server)/search/route.ts
index d04279a3..2b80d3ec 100644
--- a/packages/web/src/app/api/(server)/search/route.ts
+++ b/packages/web/src/app/api/(server)/search/route.ts
@@ -3,13 +3,21 @@
import { search } from "@/features/search/searchApi";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";
-import { sew, withAuth, withOrgMembership } from "@/actions";
import { schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
import { searchRequestSchema } from "@/features/search/schemas";
-import { SearchRequest } from "@/features/search/types";
+import { ErrorCode } from "@/lib/errorCodes";
+import { StatusCodes } from "http-status-codes";
export const POST = async (request: NextRequest) => {
- const domain = request.headers.get("X-Org-Domain")!;
+ const domain = request.headers.get("X-Org-Domain");
+ if (!domain) {
+ return serviceErrorResponse({
+ statusCode: StatusCodes.BAD_REQUEST,
+ errorCode: ErrorCode.MISSING_ORG_DOMAIN_HEADER,
+ message: "Missing X-Org-Domain header",
+ });
+ }
+
const body = await request.json();
const parsed = await searchRequestSchema.safeParseAsync(body);
if (!parsed.success) {
@@ -18,17 +26,9 @@ export const POST = async (request: NextRequest) => {
);
}
- const response = await postSearch(parsed.data, domain);
+ const response = await search(parsed.data, domain);
if (isServiceError(response)) {
return serviceErrorResponse(response);
}
return Response.json(response);
-}
-
-const postSearch = (request: SearchRequest, domain: string) => sew(() =>
- withAuth((session) =>
- withOrgMembership(session, domain, async ({ orgId }) => {
- const response = await search(request, orgId);
- return response;
- }
- ), /* allowSingleTenantUnauthedAccess */ true));
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/packages/web/src/app/api/(server)/source/route.ts b/packages/web/src/app/api/(server)/source/route.ts
index dc361a02..c997ba72 100644
--- a/packages/web/src/app/api/(server)/source/route.ts
+++ b/packages/web/src/app/api/(server)/source/route.ts
@@ -4,11 +4,20 @@ import { getFileSource } from "@/features/search/fileSourceApi";
import { schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";
-import { sew, withAuth, withOrgMembership } from "@/actions";
import { fileSourceRequestSchema } from "@/features/search/schemas";
-import { FileSourceRequest } from "@/features/search/types";
+import { ErrorCode } from "@/lib/errorCodes";
+import { StatusCodes } from "http-status-codes";
export const POST = async (request: NextRequest) => {
+ const domain = request.headers.get("X-Org-Domain");
+ if (!domain) {
+ return serviceErrorResponse({
+ statusCode: StatusCodes.BAD_REQUEST,
+ errorCode: ErrorCode.MISSING_ORG_DOMAIN_HEADER,
+ message: "Missing X-Org-Domain header",
+ });
+ }
+
const body = await request.json();
const parsed = await fileSourceRequestSchema.safeParseAsync(body);
if (!parsed.success) {
@@ -18,19 +27,11 @@ export const POST = async (request: NextRequest) => {
}
- const response = await postSource(parsed.data, request.headers.get("X-Org-Domain")!);
+
+ const response = await getFileSource(parsed.data, domain);
if (isServiceError(response)) {
return serviceErrorResponse(response);
}
return Response.json(response);
}
-
-
-export const postSource = (request: FileSourceRequest, domain: string) => sew(() =>
- withAuth(async (session) =>
- withOrgMembership(session, domain, async ({ orgId }) => {
- const response = await getFileSource(request, orgId);
- return response;
- }
- ), /* allowSingleTenantUnauthedAccess */ true));
diff --git a/packages/web/src/features/agents/review-agent/nodes/fetchFileContent.ts b/packages/web/src/features/agents/review-agent/nodes/fetchFileContent.ts
index edd03274..e4f7649c 100644
--- a/packages/web/src/features/agents/review-agent/nodes/fetchFileContent.ts
+++ b/packages/web/src/features/agents/review-agent/nodes/fetchFileContent.ts
@@ -1,7 +1,7 @@
import { sourcebot_context, sourcebot_pr_payload } from "@/features/agents/review-agent/types";
+import { getFileSource } from "@/features/search/fileSourceApi";
import { fileSourceResponseSchema } from "@/features/search/schemas";
import { base64Decode } from "@/lib/utils";
-import { postSource } from "@/app/api/(server)/source/route";
import { isServiceError } from "@/lib/utils";
export const fetchFileContent = async (pr_payload: sourcebot_pr_payload, filename: string): Promise
=> {
@@ -14,7 +14,7 @@ export const fetchFileContent = async (pr_payload: sourcebot_pr_payload, filenam
}
console.log(JSON.stringify(fileSourceRequest, null, 2));
- const response = await postSource(fileSourceRequest, "~");
+ const response = await getFileSource(fileSourceRequest, "~");
if (isServiceError(response)) {
throw new Error(`Failed to fetch file content for ${filename} from ${repoPath}: ${response.message}`);
}
diff --git a/packages/web/src/features/search/fileSourceApi.ts b/packages/web/src/features/search/fileSourceApi.ts
index 87ef4b0a..131ff881 100644
--- a/packages/web/src/features/search/fileSourceApi.ts
+++ b/packages/web/src/features/search/fileSourceApi.ts
@@ -3,40 +3,44 @@ import { fileNotFound, ServiceError } from "../../lib/serviceError";
import { FileSourceRequest, FileSourceResponse } from "./types";
import { isServiceError } from "../../lib/utils";
import { search } from "./searchApi";
+import { sew, withAuth, withOrgMembership } from "@/actions";
// @todo (bkellam) : We should really be using `git show :` to fetch file contents here.
// This will allow us to support permalinks to files at a specific revision that may not be indexed
// by zoekt.
-export const getFileSource = async ({ fileName, repository, branch }: FileSourceRequest, orgId: number): Promise => {
- const escapedFileName = escapeStringRegexp(fileName);
- const escapedRepository = escapeStringRegexp(repository);
+export const getFileSource = async ({ fileName, repository, branch }: FileSourceRequest, domain: string): Promise => sew(() =>
+ withAuth((session) =>
+ withOrgMembership(session, domain, async () => {
+ const escapedFileName = escapeStringRegexp(fileName);
+ const escapedRepository = escapeStringRegexp(repository);
- let query = `file:${escapedFileName} repo:^${escapedRepository}$`;
- if (branch) {
- query = query.concat(` branch:${branch}`);
- }
+ let query = `file:${escapedFileName} repo:^${escapedRepository}$`;
+ if (branch) {
+ query = query.concat(` branch:${branch}`);
+ }
- const searchResponse = await search({
- query,
- matches: 1,
- whole: true,
- }, orgId);
+ const searchResponse = await search({
+ query,
+ matches: 1,
+ whole: true,
+ }, domain);
- if (isServiceError(searchResponse)) {
- return searchResponse;
- }
+ if (isServiceError(searchResponse)) {
+ return searchResponse;
+ }
- const files = searchResponse.files;
+ const files = searchResponse.files;
- if (!files || files.length === 0) {
- return fileNotFound(fileName, repository);
- }
+ if (!files || files.length === 0) {
+ return fileNotFound(fileName, repository);
+ }
- const file = files[0];
- const source = file.content ?? '';
- const language = file.language;
- return {
- source,
- language,
- } satisfies FileSourceResponse;
-}
\ No newline at end of file
+ const file = files[0];
+ const source = file.content ?? '';
+ const language = file.language;
+ return {
+ source,
+ language,
+ } satisfies FileSourceResponse;
+ }), /* allowSingleTenantUnauthedAccess = */ true)
+);
diff --git a/packages/web/src/features/search/listReposApi.ts b/packages/web/src/features/search/listReposApi.ts
index fb0a0318..9077d166 100644
--- a/packages/web/src/features/search/listReposApi.ts
+++ b/packages/web/src/features/search/listReposApi.ts
@@ -2,42 +2,45 @@ import { invalidZoektResponse, ServiceError } from "../../lib/serviceError";
import { ListRepositoriesResponse } from "./types";
import { zoektFetch } from "./zoektClient";
import { zoektListRepositoriesResponseSchema } from "./zoektSchema";
-
-
-export const listRepositories = async (orgId: number): Promise => {
- const body = JSON.stringify({
- opts: {
- Field: 0,
- }
- });
-
- let header: Record = {};
- header = {
- "X-Tenant-ID": orgId.toString()
- };
-
- const listResponse = await zoektFetch({
- path: "/api/list",
- body,
- header,
- method: "POST",
- cache: "no-store",
- });
-
- if (!listResponse.ok) {
- return invalidZoektResponse(listResponse);
- }
-
- const listBody = await listResponse.json();
-
- const parser = zoektListRepositoriesResponseSchema.transform(({ List }) => ({
- repos: List.Repos.map((repo) => ({
- name: repo.Repository.Name,
- url: repo.Repository.URL,
- branches: repo.Repository.Branches?.map((branch) => branch.Name) ?? [],
- rawConfig: repo.Repository.RawConfig ?? undefined,
- }))
- } satisfies ListRepositoriesResponse));
-
- return parser.parse(listBody);
-}
\ No newline at end of file
+import { sew, withAuth, withOrgMembership } from "@/actions";
+
+export const listRepositories = async (domain: string): Promise => sew(() =>
+ withAuth((session) =>
+ withOrgMembership(session, domain, async ({ orgId }) => {
+ const body = JSON.stringify({
+ opts: {
+ Field: 0,
+ }
+ });
+
+ let header: Record = {};
+ header = {
+ "X-Tenant-ID": orgId.toString()
+ };
+
+ const listResponse = await zoektFetch({
+ path: "/api/list",
+ body,
+ header,
+ method: "POST",
+ cache: "no-store",
+ });
+
+ if (!listResponse.ok) {
+ return invalidZoektResponse(listResponse);
+ }
+
+ const listBody = await listResponse.json();
+
+ const parser = zoektListRepositoriesResponseSchema.transform(({ List }) => ({
+ repos: List.Repos.map((repo) => ({
+ name: repo.Repository.Name,
+ webUrl: repo.Repository.URL.length > 0 ? repo.Repository.URL : undefined,
+ branches: repo.Repository.Branches?.map((branch) => branch.Name) ?? [],
+ rawConfig: repo.Repository.RawConfig ?? undefined,
+ }))
+ } satisfies ListRepositoriesResponse));
+
+ return parser.parse(listBody);
+ }), /* allowSingleTenantUnauthedAccess = */ true)
+);
diff --git a/packages/web/src/features/search/schemas.ts b/packages/web/src/features/search/schemas.ts
index a4305c00..9e86d991 100644
--- a/packages/web/src/features/search/schemas.ts
+++ b/packages/web/src/features/search/schemas.ts
@@ -31,6 +31,14 @@ export const searchRequestSchema = z.object({
whole: z.boolean().optional(),
});
+export const repositoryInfoSchema = z.object({
+ id: z.number(),
+ codeHostType: z.string(),
+ name: z.string(),
+ displayName: z.string().optional(),
+ webUrl: z.string().optional(),
+})
+
export const searchResponseSchema = z.object({
zoektStats: z.object({
// The duration (in nanoseconds) of the search.
@@ -62,8 +70,9 @@ export const searchResponseSchema = z.object({
// Any matching ranges
matchRanges: z.array(rangeSchema),
}),
- url: z.string(),
+ webUrl: z.string().optional(),
repository: z.string(),
+ repositoryId: z.number(),
language: z.string(),
chunks: z.array(z.object({
content: z.string(),
@@ -78,13 +87,14 @@ export const searchResponseSchema = z.object({
// Set if `whole` is true.
content: z.string().optional(),
})),
+ repositoryInfo: z.array(repositoryInfoSchema),
isBranchFilteringEnabled: z.boolean(),
});
export const repositorySchema = z.object({
name: z.string(),
- url: z.string(),
branches: z.array(z.string()),
+ webUrl: z.string().optional(),
rawConfig: z.record(z.string(), z.string()).optional(),
});
diff --git a/packages/web/src/features/search/searchApi.ts b/packages/web/src/features/search/searchApi.ts
index 6ba75091..4e83c24d 100644
--- a/packages/web/src/features/search/searchApi.ts
+++ b/packages/web/src/features/search/searchApi.ts
@@ -7,7 +7,9 @@ import { ErrorCode } from "../../lib/errorCodes";
import { StatusCodes } from "http-status-codes";
import { zoektSearchResponseSchema } from "./zoektSchema";
import { SearchRequest, SearchResponse, SearchResultRange } from "./types";
-import assert from "assert";
+import { Repo } from "@sourcebot/db";
+import * as Sentry from "@sentry/nextjs";
+import { sew, withAuth, withOrgMembership } from "@/actions";
// List of supported query prefixes in zoekt.
// @see : https://github.com/sourcebot-dev/zoekt/blob/main/query/parse.go#L417
@@ -92,178 +94,244 @@ const transformZoektQuery = async (query: string, orgId: number): Promise {
// This is a hacky parser for templates generated by
// the go text/template package. Example template:
// {{URLJoinPath "https://github.com/sourcebot-dev/sourcebot" "blob" .Version .Path}}
-
- // The template should always match this regex, so let's assert that.
- assert(template.match(/^{{URLJoinPath\s.*}}(\?.+)?$/), "Invalid template");
+
+ if (!template.match(/^{{URLJoinPath\s.*}}(\?.+)?$/)) {
+ return undefined;
+ }
const url =
template.substring("{{URLJoinPath ".length, template.indexOf("}}"))
- .replace(".Version", branch)
- .replace(".Path", fileName)
- .split(" ")
- .map((part) => {
- // remove wrapping quotes
- if (part.startsWith("\"")) part = part.substring(1);
- if (part.endsWith("\"")) part = part.substring(0, part.length - 1);
- return part;
- })
- .join("/");
+ .replace(".Version", branch)
+ .replace(".Path", fileName)
+ .split(" ")
+ .map((part) => {
+ // remove wrapping quotes
+ if (part.startsWith("\"")) part = part.substring(1);
+ if (part.endsWith("\"")) part = part.substring(0, part.length - 1);
+ return part;
+ })
+ .join("/");
const optionalQueryParams =
template.substring(template.indexOf("}}") + 2)
- .replace("{{.Version}}", branch)
- .replace("{{.Path}}", fileName);
+ .replace("{{.Version}}", branch)
+ .replace("{{.Path}}", fileName);
return encodeURI(url + optionalQueryParams);
}
-export const search = async ({ query, matches, contextLines, whole }: SearchRequest, orgId: number) => {
- const transformedQuery = await transformZoektQuery(query, orgId);
- if (isServiceError(transformedQuery)) {
- return transformedQuery;
- }
- query = transformedQuery;
+export const search = async ({ query, matches, contextLines, whole }: SearchRequest, domain: string) => sew(() =>
+ withAuth((session) =>
+ withOrgMembership(session, domain, async ({ orgId }) => {
+ const transformedQuery = await transformZoektQuery(query, orgId);
+ if (isServiceError(transformedQuery)) {
+ return transformedQuery;
+ }
+ query = transformedQuery;
- const isBranchFilteringEnabled = (
- query.includes(zoektPrefixes.branch) ||
- query.includes(zoektPrefixes.branchShort)
- );
+ const isBranchFilteringEnabled = (
+ query.includes(zoektPrefixes.branch) ||
+ query.includes(zoektPrefixes.branchShort)
+ );
- // We only want to show matches for the default branch when
- // the user isn't explicitly filtering by branch.
- if (!isBranchFilteringEnabled) {
- query = query.concat(` branch:HEAD`);
- }
+ // We only want to show matches for the default branch when
+ // the user isn't explicitly filtering by branch.
+ if (!isBranchFilteringEnabled) {
+ query = query.concat(` branch:HEAD`);
+ }
- const body = JSON.stringify({
- q: query,
- // @see: https://github.com/sourcebot-dev/zoekt/blob/main/api.go#L892
- opts: {
- ChunkMatches: true,
- MaxMatchDisplayCount: matches,
- NumContextLines: contextLines,
- Whole: !!whole,
- TotalMaxMatchCount: env.TOTAL_MAX_MATCH_COUNT,
- ShardMaxMatchCount: env.SHARD_MAX_MATCH_COUNT,
- MaxWallTime: env.ZOEKT_MAX_WALL_TIME_MS * 1000 * 1000, // zoekt expects a duration in nanoseconds
- }
- });
-
- let header: Record = {};
- header = {
- "X-Tenant-ID": orgId.toString()
- };
-
- const searchResponse = await zoektFetch({
- path: "/api/search",
- body,
- header,
- method: "POST",
- });
-
- if (!searchResponse.ok) {
- return invalidZoektResponse(searchResponse);
- }
+ const body = JSON.stringify({
+ q: query,
+ // @see: https://github.com/sourcebot-dev/zoekt/blob/main/api.go#L892
+ opts: {
+ ChunkMatches: true,
+ MaxMatchDisplayCount: matches,
+ NumContextLines: contextLines,
+ Whole: !!whole,
+ TotalMaxMatchCount: env.TOTAL_MAX_MATCH_COUNT,
+ ShardMaxMatchCount: env.SHARD_MAX_MATCH_COUNT,
+ MaxWallTime: env.ZOEKT_MAX_WALL_TIME_MS * 1000 * 1000, // zoekt expects a duration in nanoseconds
+ }
+ });
+
+ let header: Record = {};
+ header = {
+ "X-Tenant-ID": orgId.toString()
+ };
+
+ const searchResponse = await zoektFetch({
+ path: "/api/search",
+ body,
+ header,
+ method: "POST",
+ });
+
+ if (!searchResponse.ok) {
+ return invalidZoektResponse(searchResponse);
+ }
+
+ const searchBody = await searchResponse.json();
+
+ const parser = zoektSearchResponseSchema.transform(async ({ Result }) => {
+
+ // @note (2025-05-12): in zoekt, repositories are identified by the `RepositoryID` field
+ // which corresponds to the `id` in the Repo table. In order to efficiently fetch repository
+ // metadata when transforming (potentially thousands) of file matches, we aggregate a unique
+ // set of repository ids* and map them to their corresponding Repo record.
+ //
+ // *Q: Why is `RepositoryID` optional? And why are we falling back to `Repository`?
+ // A: Prior to this change, the repository id was not plumbed into zoekt, so RepositoryID was
+ // always undefined. To make this a non-breaking change, we fallback to using the repository's name
+ // (`Repository`) as the identifier in these cases. This is not guaranteed to be unique, but in
+ // practice it is since the repository name includes the host and path (e.g., 'github.com/org/repo',
+ // 'gitea.com/org/repo', etc.).
+ //
+ // Note: When a repository is re-indexed (every hour) this ID will be populated.
+ // @see: https://github.com/sourcebot-dev/zoekt/pull/6
+ const repoIdentifiers = new Set(Result.Files?.map((file) => file.RepositoryID ?? file.Repository) ?? []);
+ const repos = new Map();
- const searchBody = await searchResponse.json();
-
- const parser = zoektSearchResponseSchema.transform(({ Result }) => ({
- zoektStats: {
- duration: Result.Duration,
- fileCount: Result.FileCount,
- matchCount: Result.MatchCount,
- filesSkipped: Result.FilesSkipped,
- contentBytesLoaded: Result.ContentBytesLoaded,
- indexBytesLoaded: Result.IndexBytesLoaded,
- crashes: Result.Crashes,
- shardFilesConsidered: Result.ShardFilesConsidered,
- filesConsidered: Result.FilesConsidered,
- filesLoaded: Result.FilesLoaded,
- shardsScanned: Result.ShardsScanned,
- shardsSkipped: Result.ShardsSkipped,
- shardsSkippedFilter: Result.ShardsSkippedFilter,
- ngramMatches: Result.NgramMatches,
- ngramLookups: Result.NgramLookups,
- wait: Result.Wait,
- matchTreeConstruction: Result.MatchTreeConstruction,
- matchTreeSearch: Result.MatchTreeSearch,
- regexpsConsidered: Result.RegexpsConsidered,
- flushReason: Result.FlushReason,
- },
- files: Result.Files?.map((file) => {
- const fileNameChunks = file.ChunkMatches.filter((chunk) => chunk.FileName);
-
- const template = Result.RepoURLs[file.Repository];
- assert(template, `Template not found for repository ${file.Repository}`);
-
- // If there are multiple branches pointing to the same revision of this file, it doesn't
- // matter which branch we use here, so use the first one.
- const branch = file.Branches && file.Branches.length > 0 ? file.Branches[0] : "HEAD";
- const url = getRepositoryUrl(template, branch, file.FileName);
-
- return {
- fileName: {
- text: file.FileName,
- matchRanges: fileNameChunks.length === 1 ? fileNameChunks[0].Ranges.map((range) => ({
- start: {
- byteOffset: range.Start.ByteOffset,
- column: range.Start.Column,
- lineNumber: range.Start.LineNumber,
+ (await prisma.repo.findMany({
+ where: {
+ id: {
+ in: Array.from(repoIdentifiers).filter((id) => typeof id === "number"),
},
- end: {
- byteOffset: range.End.ByteOffset,
- column: range.End.Column,
- lineNumber: range.End.LineNumber,
+ orgId,
+ }
+ })).forEach(repo => repos.set(repo.id, repo));
+
+ (await prisma.repo.findMany({
+ where: {
+ name: {
+ in: Array.from(repoIdentifiers).filter((id) => typeof id === "string"),
+ },
+ orgId,
+ }
+ })).forEach(repo => repos.set(repo.name, repo));
+
+ return {
+ zoektStats: {
+ duration: Result.Duration,
+ fileCount: Result.FileCount,
+ matchCount: Result.MatchCount,
+ filesSkipped: Result.FilesSkipped,
+ contentBytesLoaded: Result.ContentBytesLoaded,
+ indexBytesLoaded: Result.IndexBytesLoaded,
+ crashes: Result.Crashes,
+ shardFilesConsidered: Result.ShardFilesConsidered,
+ filesConsidered: Result.FilesConsidered,
+ filesLoaded: Result.FilesLoaded,
+ shardsScanned: Result.ShardsScanned,
+ shardsSkipped: Result.ShardsSkipped,
+ shardsSkippedFilter: Result.ShardsSkippedFilter,
+ ngramMatches: Result.NgramMatches,
+ ngramLookups: Result.NgramLookups,
+ wait: Result.Wait,
+ matchTreeConstruction: Result.MatchTreeConstruction,
+ matchTreeSearch: Result.MatchTreeSearch,
+ regexpsConsidered: Result.RegexpsConsidered,
+ flushReason: Result.FlushReason,
+ },
+ files: Result.Files?.map((file) => {
+ const fileNameChunks = file.ChunkMatches.filter((chunk) => chunk.FileName);
+
+ const webUrl = (() => {
+ const template: string | undefined = Result.RepoURLs[file.Repository];
+ if (!template) {
+ return undefined;
+ }
+
+ // If there are multiple branches pointing to the same revision of this file, it doesn't
+ // matter which branch we use here, so use the first one.
+ const branch = file.Branches && file.Branches.length > 0 ? file.Branches[0] : "HEAD";
+ return getFileWebUrl(template, branch, file.FileName);
+ })();
+
+ const identifier = file.RepositoryID ?? file.Repository;
+ const repo = repos.get(identifier);
+
+ // This should never happen... but if it does, we skip the file.
+ if (!repo) {
+ Sentry.captureMessage(
+ `Repository not found for identifier: ${identifier}; skipping file "${file.FileName}"`,
+ 'warning'
+ );
+ return undefined;
}
- })) : [],
- },
- repository: file.Repository,
- url: url,
- language: file.Language,
- chunks: file.ChunkMatches
- .filter((chunk) => !chunk.FileName) // Filter out filename chunks.
- .map((chunk) => {
+
return {
- content: chunk.Content,
- matchRanges: chunk.Ranges.map((range) => ({
- start: {
- byteOffset: range.Start.ByteOffset,
- column: range.Start.Column,
- lineNumber: range.Start.LineNumber,
- },
- end: {
- byteOffset: range.End.ByteOffset,
- column: range.End.Column,
- lineNumber: range.End.LineNumber,
- }
- }) satisfies SearchResultRange),
- contentStart: {
- byteOffset: chunk.ContentStart.ByteOffset,
- column: chunk.ContentStart.Column,
- lineNumber: chunk.ContentStart.LineNumber,
+ fileName: {
+ text: file.FileName,
+ matchRanges: fileNameChunks.length === 1 ? fileNameChunks[0].Ranges.map((range) => ({
+ start: {
+ byteOffset: range.Start.ByteOffset,
+ column: range.Start.Column,
+ lineNumber: range.Start.LineNumber,
+ },
+ end: {
+ byteOffset: range.End.ByteOffset,
+ column: range.End.Column,
+ lineNumber: range.End.LineNumber,
+ }
+ })) : [],
},
- symbols: chunk.SymbolInfo?.map((symbol) => {
- return {
- symbol: symbol.Sym,
- kind: symbol.Kind,
- parent: symbol.Parent.length > 0 ? {
- symbol: symbol.Parent,
- kind: symbol.ParentKind,
- } : undefined,
- }
- }) ?? undefined,
+ repository: repo.name,
+ repositoryId: repo.id,
+ webUrl: webUrl,
+ language: file.Language,
+ chunks: file.ChunkMatches
+ .filter((chunk) => !chunk.FileName) // Filter out filename chunks.
+ .map((chunk) => {
+ return {
+ content: chunk.Content,
+ matchRanges: chunk.Ranges.map((range) => ({
+ start: {
+ byteOffset: range.Start.ByteOffset,
+ column: range.Start.Column,
+ lineNumber: range.Start.LineNumber,
+ },
+ end: {
+ byteOffset: range.End.ByteOffset,
+ column: range.End.Column,
+ lineNumber: range.End.LineNumber,
+ }
+ }) satisfies SearchResultRange),
+ contentStart: {
+ byteOffset: chunk.ContentStart.ByteOffset,
+ column: chunk.ContentStart.Column,
+ lineNumber: chunk.ContentStart.LineNumber,
+ },
+ symbols: chunk.SymbolInfo?.map((symbol) => {
+ return {
+ symbol: symbol.Sym,
+ kind: symbol.Kind,
+ parent: symbol.Parent.length > 0 ? {
+ symbol: symbol.Parent,
+ kind: symbol.ParentKind,
+ } : undefined,
+ }
+ }) ?? undefined,
+ }
+ }),
+ branches: file.Branches,
+ content: file.Content,
}
- }),
- branches: file.Branches,
- content: file.Content,
- }
- }) ?? [],
- isBranchFilteringEnabled: isBranchFilteringEnabled,
- } satisfies SearchResponse));
+ }).filter((file) => file !== undefined) ?? [],
+ repositoryInfo: Array.from(repos.values()).map((repo) => ({
+ id: repo.id,
+ codeHostType: repo.external_codeHostType,
+ name: repo.name,
+ displayName: repo.displayName ?? undefined,
+ webUrl: repo.webUrl ?? undefined,
+ })),
+ isBranchFilteringEnabled: isBranchFilteringEnabled,
+ } satisfies SearchResponse;
+ });
- return parser.parse(searchBody);
-}
+ return parser.parseAsync(searchBody);
+ }), /* allowSingleTenantUnauthedAccess = */ true)
+)
diff --git a/packages/web/src/features/search/types.ts b/packages/web/src/features/search/types.ts
index ccf2c32f..1e2d331d 100644
--- a/packages/web/src/features/search/types.ts
+++ b/packages/web/src/features/search/types.ts
@@ -8,6 +8,7 @@ import {
rangeSchema,
fileSourceRequestSchema,
symbolSchema,
+ repositoryInfoSchema,
} from "./schemas";
import { z } from "zod";
@@ -23,4 +24,6 @@ export type ListRepositoriesResponse = z.infer;
-export type FileSourceResponse = z.infer;
\ No newline at end of file
+export type FileSourceResponse = z.infer;
+
+export type RepositoryInfo = z.infer;
\ No newline at end of file
diff --git a/packages/web/src/features/search/zoektSchema.ts b/packages/web/src/features/search/zoektSchema.ts
index d4091fb8..752d360c 100644
--- a/packages/web/src/features/search/zoektSchema.ts
+++ b/packages/web/src/features/search/zoektSchema.ts
@@ -54,6 +54,7 @@ export const zoektSearchResponseSchema = z.object({
Files: z.array(z.object({
FileName: z.string(),
Repository: z.string(),
+ RepositoryID: z.number().optional(),
Version: z.string().optional(),
Language: z.string(),
Branches: z.array(z.string()).optional(),
diff --git a/packages/web/src/lib/errorCodes.ts b/packages/web/src/lib/errorCodes.ts
index 56702cd5..f0d503ba 100644
--- a/packages/web/src/lib/errorCodes.ts
+++ b/packages/web/src/lib/errorCodes.ts
@@ -23,4 +23,5 @@ export enum ErrorCode {
STRIPE_CLIENT_NOT_INITIALIZED = 'STRIPE_CLIENT_NOT_INITIALIZED',
ACTION_DISALLOWED_IN_TENANCY_MODE = 'ACTION_DISALLOWED_IN_TENANCY_MODE',
SEARCH_CONTEXT_NOT_FOUND = 'SEARCH_CONTEXT_NOT_FOUND',
+ MISSING_ORG_DOMAIN_HEADER = 'MISSING_ORG_DOMAIN_HEADER',
}
diff --git a/packages/web/src/lib/utils.ts b/packages/web/src/lib/utils.ts
index 3d211ccb..ddaac1c0 100644
--- a/packages/web/src/lib/utils.ts
+++ b/packages/web/src/lib/utils.ts
@@ -5,9 +5,8 @@ import gitlabLogo from "@/public/gitlab.svg";
import giteaLogo from "@/public/gitea.svg";
import gerritLogo from "@/public/gerrit.svg";
import bitbucketLogo from "@/public/bitbucket.svg";
+import gitLogo from "@/public/git.svg";
import { ServiceError } from "./serviceError";
-import { RepositoryQuery } from "./types";
-import { Repository } from "@/features/search/types";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
@@ -33,47 +32,40 @@ export const createPathWithQueryParams = (path: string, ...queryParams: [string,
return `${path}?${queryString}`;
}
-export type CodeHostType = "github" | "gitlab" | "gitea" | "gerrit" | "bitbucket-cloud" | "bitbucket-server";
+export type CodeHostType =
+ "github" |
+ "gitlab" |
+ "gitea" |
+ "gerrit" |
+ "bitbucket-cloud" |
+ "bitbucket-server" |
+ "generic-git-host";
type CodeHostInfo = {
type: CodeHostType;
displayName: string;
codeHostName: string;
- repoLink: string;
+ repoLink?: string;
icon: string;
iconClassName?: string;
}
-export const getRepoCodeHostInfo = (repo?: Repository): CodeHostInfo | undefined => {
- if (!repo) {
- return undefined;
- }
-
- if (!repo.rawConfig) {
- return undefined;
- }
-
- // @todo : use zod to validate config schema
- const webUrlType = repo.rawConfig['web-url-type']!;
- const displayName = repo.rawConfig['display-name'] ?? repo.rawConfig['name']!;
-
- return _getCodeHostInfoInternal(webUrlType, displayName, repo.url);
-}
-
-export const getRepoQueryCodeHostInfo = (repo: RepositoryQuery): CodeHostInfo | undefined => {
- const displayName = repo.repoDisplayName ?? repo.repoName;
- return _getCodeHostInfoInternal(repo.codeHostType, displayName, repo.webUrl ?? repo.repoCloneUrl);
-}
+export const getCodeHostInfoForRepo = (repo: {
+ codeHostType: string,
+ name: string,
+ displayName?: string,
+ webUrl?: string,
+}): CodeHostInfo | undefined => {
+ const { codeHostType, name, displayName, webUrl } = repo;
-const _getCodeHostInfoInternal = (type: string, displayName: string, cloneUrl: string): CodeHostInfo | undefined => {
- switch (type) {
+ switch (codeHostType) {
case 'github': {
const { src, className } = getCodeHostIcon('github')!;
return {
type: "github",
- displayName: displayName,
+ displayName: displayName ?? name,
codeHostName: "GitHub",
- repoLink: cloneUrl,
+ repoLink: webUrl,
icon: src,
iconClassName: className,
}
@@ -82,9 +74,9 @@ const _getCodeHostInfoInternal = (type: string, displayName: string, cloneUrl: s
const { src, className } = getCodeHostIcon('gitlab')!;
return {
type: "gitlab",
- displayName: displayName,
+ displayName: displayName ?? name,
codeHostName: "GitLab",
- repoLink: cloneUrl,
+ repoLink: webUrl,
icon: src,
iconClassName: className,
}
@@ -93,9 +85,9 @@ const _getCodeHostInfoInternal = (type: string, displayName: string, cloneUrl: s
const { src, className } = getCodeHostIcon('gitea')!;
return {
type: "gitea",
- displayName: displayName,
+ displayName: displayName ?? name,
codeHostName: "Gitea",
- repoLink: cloneUrl,
+ repoLink: webUrl,
icon: src,
iconClassName: className,
}
@@ -105,9 +97,9 @@ const _getCodeHostInfoInternal = (type: string, displayName: string, cloneUrl: s
const { src, className } = getCodeHostIcon('gerrit')!;
return {
type: "gerrit",
- displayName: displayName,
+ displayName: displayName ?? name,
codeHostName: "Gerrit",
- repoLink: cloneUrl,
+ repoLink: webUrl,
icon: src,
iconClassName: className,
}
@@ -116,9 +108,9 @@ const _getCodeHostInfoInternal = (type: string, displayName: string, cloneUrl: s
const { src, className } = getCodeHostIcon('bitbucket-server')!;
return {
type: "bitbucket-server",
- displayName: displayName,
+ displayName: displayName ?? name,
codeHostName: "Bitbucket",
- repoLink: cloneUrl,
+ repoLink: webUrl,
icon: src,
iconClassName: className,
}
@@ -127,9 +119,20 @@ const _getCodeHostInfoInternal = (type: string, displayName: string, cloneUrl: s
const { src, className } = getCodeHostIcon('bitbucket-cloud')!;
return {
type: "bitbucket-cloud",
- displayName: displayName,
+ displayName: displayName ?? name,
codeHostName: "Bitbucket",
- repoLink: cloneUrl,
+ repoLink: webUrl,
+ icon: src,
+ iconClassName: className,
+ }
+ }
+ case "generic-git-host": {
+ const { src, className } = getCodeHostIcon('generic-git-host')!;
+ return {
+ type: "generic-git-host",
+ displayName: displayName ?? name,
+ codeHostName: "Generic Git Host",
+ repoLink: webUrl,
icon: src,
iconClassName: className,
}
@@ -161,6 +164,10 @@ export const getCodeHostIcon = (codeHostType: CodeHostType): { src: string, clas
return {
src: bitbucketLogo,
}
+ case "generic-git-host":
+ return {
+ src: gitLogo,
+ }
default:
return null;
}
@@ -174,6 +181,7 @@ export const isAuthSupportedForCodeHost = (codeHostType: CodeHostType): boolean
case "bitbucket-cloud":
case "bitbucket-server":
return true;
+ case "generic-git-host":
case "gerrit":
return false;
}
diff --git a/schemas/v3/connection.json b/schemas/v3/connection.json
index d1cab143..c7c93c75 100644
--- a/schemas/v3/connection.json
+++ b/schemas/v3/connection.json
@@ -16,6 +16,9 @@
},
{
"$ref": "./bitbucket.json"
+ },
+ {
+ "$ref": "./genericGitHost.json"
}
]
}
\ No newline at end of file
diff --git a/schemas/v3/genericGitHost.json b/schemas/v3/genericGitHost.json
new file mode 100644
index 00000000..e2f2c7f3
--- /dev/null
+++ b/schemas/v3/genericGitHost.json
@@ -0,0 +1,30 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "title": "GenericGitHostConnectionConfig",
+ "properties": {
+ "type": {
+ "const": "git",
+ "description": "Generic Git host configuration"
+ },
+ "url": {
+ "type": "string",
+ "format": "url",
+ "description": "The URL to the git repository. This can either be a remote URL (prefixed with `http://` or `https://`) or a absolute path to a directory on the local machine (prefixed with `file://`). If a local directory is specified, it must point to the root of a git repository. Local directories are treated as read-only modified. Local directories support glob patterns.",
+ "pattern": "^(https?:\\/\\/[^\\s/$.?#].[^\\s]*|file:\\/\\/\\/[^\\s]+)$",
+ "examples": [
+ "https://github.com/sourcebot-dev/sourcebot",
+ "file:///path/to/repo",
+ "file:///repos/*"
+ ]
+ },
+ "revisions": {
+ "$ref": "./shared.json#/definitions/GitRevisions"
+ }
+ },
+ "required": [
+ "type",
+ "url"
+ ],
+ "additionalProperties": false
+}
\ No newline at end of file
diff --git a/schemas/v3/index.json b/schemas/v3/index.json
index 81026026..a4ba7b89 100644
--- a/schemas/v3/index.json
+++ b/schemas/v3/index.json
@@ -78,7 +78,7 @@
},
"contexts": {
"type": "object",
- "description": "[Sourcebot EE] Defines a collection of search contexts. This is only available in single-tenancy mode. See: https://docs.sourcebot.dev/self-hosting/more/search-contexts",
+ "description": "[Sourcebot EE] Defines a collection of search contexts. This is only available in single-tenancy mode. See: https://docs.sourcebot.dev/docs/search/search-contexts",
"patternProperties": {
"^[a-zA-Z0-9_-]+$": {
"$ref": "#/definitions/SearchContext"
diff --git a/vendor/zoekt b/vendor/zoekt
index 7d189621..12a2f4ad 160000
--- a/vendor/zoekt
+++ b/vendor/zoekt
@@ -1 +1 @@
-Subproject commit 7d1896215eea6f97af66c9549c9ec70436356b51
+Subproject commit 12a2f4ad075359a09bd8a91793acb002211217aa
diff --git a/yarn.lock b/yarn.lock
index 6a6acbdd..f3bf9fe6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5460,6 +5460,7 @@ __metadata:
cross-fetch: "npm:^4.0.0"
dotenv: "npm:^16.4.5"
express: "npm:^4.21.2"
+ git-url-parse: "npm:^16.1.0"
gitea-js: "npm:^1.22.0"
glob: "npm:^11.0.0"
ioredis: "npm:^5.4.2"
@@ -6084,6 +6085,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/parse-path@npm:^7.0.0":
+ version: 7.0.3
+ resolution: "@types/parse-path@npm:7.0.3"
+ checksum: 10c0/8344b6c7acba4e4e5a8d542f56f53c297685fa92f9b0c085d7532cc7e1b661432cecfc1c75c76cdb0d161c95679b6ecfe0573d9fef7c836962aacf604150a984
+ languageName: node
+ linkType: hard
+
"@types/pg-pool@npm:2.0.6":
version: 2.0.6
resolution: "@types/pg-pool@npm:2.0.6"
@@ -9830,6 +9838,25 @@ __metadata:
languageName: node
linkType: hard
+"git-up@npm:^8.1.0":
+ version: 8.1.1
+ resolution: "git-up@npm:8.1.1"
+ dependencies:
+ is-ssh: "npm:^1.4.0"
+ parse-url: "npm:^9.2.0"
+ checksum: 10c0/2cc4461d8565a3f7a1ecd3d262a58ddb8df0a67f7f7d4915df2913c460b2e88ae570a6ea810700a6d22fb3b9e4bea8dd10a8eb469900ddc12e35c62208608c03
+ languageName: node
+ linkType: hard
+
+"git-url-parse@npm:^16.1.0":
+ version: 16.1.0
+ resolution: "git-url-parse@npm:16.1.0"
+ dependencies:
+ git-up: "npm:^8.1.0"
+ checksum: 10c0/b8f5ebcbd5b2baf9f1bb77a217376f0247c47fe1d42811ccaac3015768eebb0759a59051f758e50e70adf5c67ae059d1975bf6b750164f36bfd39138d11b940b
+ languageName: node
+ linkType: hard
+
"gitea-js@npm:^1.22.0":
version: 1.23.0
resolution: "gitea-js@npm:1.23.0"
@@ -10633,6 +10660,15 @@ __metadata:
languageName: node
linkType: hard
+"is-ssh@npm:^1.4.0":
+ version: 1.4.1
+ resolution: "is-ssh@npm:1.4.1"
+ dependencies:
+ protocols: "npm:^2.0.1"
+ checksum: 10c0/021a7355cb032625d58db3cc8266ad9aa698cbabf460b71376a0307405577fd7d3aa0826c0bf1951d7809f134c0ee80403306f6d7633db94a5a3600a0106b398
+ languageName: node
+ linkType: hard
+
"is-stream@npm:^2.0.0":
version: 2.0.1
resolution: "is-stream@npm:2.0.1"
@@ -12400,6 +12436,25 @@ __metadata:
languageName: node
linkType: hard
+"parse-path@npm:^7.0.0":
+ version: 7.1.0
+ resolution: "parse-path@npm:7.1.0"
+ dependencies:
+ protocols: "npm:^2.0.0"
+ checksum: 10c0/8c8c8b3019323d686e7b1cd6fd9653bc233404403ad68827836fbfe59dfe26aaef64ed4e0396d0e20c4a7e1469312ec969a679618960e79d5e7c652dc0da5a0f
+ languageName: node
+ linkType: hard
+
+"parse-url@npm:^9.2.0":
+ version: 9.2.0
+ resolution: "parse-url@npm:9.2.0"
+ dependencies:
+ "@types/parse-path": "npm:^7.0.0"
+ parse-path: "npm:^7.0.0"
+ checksum: 10c0/b8f56cdb01e76616255dff82544f4b5ab4378f6f4bac8604ed6fde03a75b0f71c547d92688386d8f22f38fad3c928c075abf69458677c6185da76c841bfd7a93
+ languageName: node
+ linkType: hard
+
"parse5@npm:^7.1.2":
version: 7.2.1
resolution: "parse5@npm:7.2.1"
@@ -13010,6 +13065,13 @@ __metadata:
languageName: node
linkType: hard
+"protocols@npm:^2.0.0, protocols@npm:^2.0.1":
+ version: 2.0.2
+ resolution: "protocols@npm:2.0.2"
+ checksum: 10c0/b87d78c1fcf038d33691da28447ce94011d5c7f0c7fd25bcb5fb4d975991c99117873200c84f4b6a9d7f8b9092713a064356236960d1473a7d6fcd4228897b60
+ languageName: node
+ linkType: hard
+
"proxy-addr@npm:^2.0.7, proxy-addr@npm:~2.0.7":
version: 2.0.7
resolution: "proxy-addr@npm:2.0.7"