2024-09-11 04:55:00 +00:00
|
|
|
import escapeStringRegexp from "escape-string-regexp";
|
2024-09-10 06:16:41 +00:00
|
|
|
import { SHARD_MAX_MATCH_COUNT, TOTAL_MAX_MATCH_COUNT } from "../environment";
|
2024-11-07 02:28:10 +00:00
|
|
|
import { listRepositoriesResponseSchema, searchResponseSchema, zoektSearchResponseSchema } from "../schemas";
|
2024-09-26 03:12:20 +00:00
|
|
|
import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "../types";
|
2024-09-11 04:55:00 +00:00
|
|
|
import { fileNotFound, invalidZoektResponse, ServiceError, unexpectedError } from "../serviceError";
|
2024-09-10 06:16:41 +00:00
|
|
|
import { isServiceError } from "../utils";
|
|
|
|
|
import { zoektFetch } from "./zoektClient";
|
|
|
|
|
|
2024-11-07 02:28:10 +00:00
|
|
|
// List of supported query prefixes in zoekt.
|
|
|
|
|
// @see : https://github.com/sourcebot-dev/zoekt/blob/main/query/parse.go#L417
|
|
|
|
|
enum zoektPrefixes {
|
|
|
|
|
archived = "archived:",
|
|
|
|
|
branchShort = "b:",
|
|
|
|
|
branch = "branch:",
|
|
|
|
|
caseShort = "c:",
|
|
|
|
|
case = "case:",
|
|
|
|
|
content = "content:",
|
|
|
|
|
fileShort = "f:",
|
|
|
|
|
file = "file:",
|
|
|
|
|
fork = "fork:",
|
|
|
|
|
public = "public:",
|
|
|
|
|
repoShort = "r:",
|
|
|
|
|
repo = "repo:",
|
|
|
|
|
regex = "regex:",
|
|
|
|
|
lang = "lang:",
|
|
|
|
|
sym = "sym:",
|
|
|
|
|
typeShort = "t:",
|
|
|
|
|
type = "type:",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mapping of additional "alias" prefixes to zoekt prefixes.
|
|
|
|
|
const aliasPrefixMappings: Record<string, zoektPrefixes> = {
|
|
|
|
|
"rev:": zoektPrefixes.branch,
|
|
|
|
|
"revision:": zoektPrefixes.branch,
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-26 06:31:51 +00:00
|
|
|
export const search = async ({ query, maxMatchDisplayCount, whole }: SearchRequest): Promise<SearchResponse | ServiceError> => {
|
2024-11-07 02:28:10 +00:00
|
|
|
// Replace any alias prefixes with their corresponding zoekt prefixes.
|
|
|
|
|
for (const [prefix, zoektPrefix] of Object.entries(aliasPrefixMappings)) {
|
|
|
|
|
query = query.replaceAll(prefix, zoektPrefix);
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-10 06:16:41 +00:00
|
|
|
const body = JSON.stringify({
|
|
|
|
|
q: query,
|
2024-09-29 21:39:17 +00:00
|
|
|
// @see: https://github.com/sourcebot-dev/zoekt/blob/main/api.go#L892
|
2024-09-10 06:16:41 +00:00
|
|
|
opts: {
|
|
|
|
|
NumContextLines: 2,
|
|
|
|
|
ChunkMatches: true,
|
2024-09-26 06:31:51 +00:00
|
|
|
MaxMatchDisplayCount: maxMatchDisplayCount,
|
2024-09-10 06:16:41 +00:00
|
|
|
Whole: !!whole,
|
|
|
|
|
ShardMaxMatchCount: SHARD_MAX_MATCH_COUNT,
|
|
|
|
|
TotalMaxMatchCount: TOTAL_MAX_MATCH_COUNT,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const searchResponse = await zoektFetch({
|
|
|
|
|
path: "/api/search",
|
|
|
|
|
body,
|
|
|
|
|
method: "POST",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!searchResponse.ok) {
|
|
|
|
|
return invalidZoektResponse(searchResponse);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const searchBody = await searchResponse.json();
|
2024-11-07 02:28:10 +00:00
|
|
|
const parsedSearchResponse = zoektSearchResponseSchema.safeParse(searchBody);
|
2024-09-10 19:24:12 +00:00
|
|
|
if (!parsedSearchResponse.success) {
|
|
|
|
|
console.error(`Failed to parse zoekt response. Error: ${parsedSearchResponse.error}`);
|
|
|
|
|
return unexpectedError(`Something went wrong while parsing the response from zoekt`);
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-07 02:28:10 +00:00
|
|
|
const isBranchFilteringEnabled = (
|
|
|
|
|
query.includes(zoektPrefixes.branch) ||
|
|
|
|
|
query.includes(zoektPrefixes.branchShort)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...parsedSearchResponse.data,
|
|
|
|
|
isBranchFilteringEnabled,
|
|
|
|
|
}
|
2024-09-10 06:16:41 +00:00
|
|
|
}
|
|
|
|
|
|
2024-11-07 02:28:10 +00:00
|
|
|
export const getFileSource = async ({ fileName, repository, branch }: FileSourceRequest): Promise<FileSourceResponse | ServiceError> => {
|
2024-09-10 19:24:12 +00:00
|
|
|
const escapedFileName = escapeStringRegexp(fileName);
|
|
|
|
|
const escapedRepository = escapeStringRegexp(repository);
|
2024-11-07 02:28:10 +00:00
|
|
|
|
|
|
|
|
let query = `file:${escapedFileName} repo:^${escapedRepository}$`;
|
|
|
|
|
if (branch) {
|
|
|
|
|
query = query.concat(` branch:${branch}`);
|
|
|
|
|
}
|
2024-09-10 19:24:12 +00:00
|
|
|
|
2024-09-10 06:16:41 +00:00
|
|
|
const searchResponse = await search({
|
2024-11-07 02:28:10 +00:00
|
|
|
query,
|
2024-09-26 06:31:51 +00:00
|
|
|
maxMatchDisplayCount: 1,
|
2024-09-10 06:16:41 +00:00
|
|
|
whole: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (isServiceError(searchResponse)) {
|
|
|
|
|
return searchResponse;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const files = searchResponse.Result.Files;
|
|
|
|
|
|
2024-09-10 19:24:12 +00:00
|
|
|
if (!files || files.length === 0) {
|
2024-09-10 06:16:41 +00:00
|
|
|
return fileNotFound(fileName, repository);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const source = files[0].Content ?? '';
|
|
|
|
|
return {
|
|
|
|
|
source
|
|
|
|
|
}
|
2024-09-11 04:55:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const listRepositories = async (): Promise<ListRepositoriesResponse | ServiceError> => {
|
|
|
|
|
const body = JSON.stringify({
|
|
|
|
|
opts: {
|
|
|
|
|
Field: 0,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
const listResponse = await zoektFetch({
|
|
|
|
|
path: "/api/list",
|
|
|
|
|
body,
|
2024-09-11 19:15:12 +00:00
|
|
|
method: "POST",
|
|
|
|
|
cache: "no-store",
|
2024-09-11 04:55:00 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!listResponse.ok) {
|
|
|
|
|
return invalidZoektResponse(listResponse);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const listBody = await listResponse.json();
|
|
|
|
|
const parsedListResponse = listRepositoriesResponseSchema.safeParse(listBody);
|
|
|
|
|
if (!parsedListResponse.success) {
|
|
|
|
|
console.error(`Failed to parse zoekt response. Error: ${parsedListResponse.error}`);
|
|
|
|
|
return unexpectedError(`Something went wrong while parsing the response from zoekt`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return parsedListResponse.data;
|
2024-09-10 06:16:41 +00:00
|
|
|
}
|