mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-11 20:05:25 +00:00
chore(api): Changed the search api to return raw source (instead of base64 encoding) (#356)
This PR alters the behaviour of the search api (and all apis that depend on it) to return raw source code instead of a base64 encoding. Reasoning: we are decoding it on the client in multiple different places, so it would be beneficial to decode it in a single spot. **Note**: This is a **breaking change** to the API surface. However, since the API surface is still unofficial/unsupported, I will roll this as a patch version change. See #101
This commit is contained in:
parent
22d548e171
commit
1d95e82b95
12 changed files with 25 additions and 47 deletions
|
|
@ -7,10 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
<!-- @NOTE: this release includes a API change that affects the MCP package (@sourcebot/mcp). On release, bump the MCP package's version and delete this message. -->
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Delete account join request when redeeming an invite. [#352](https://github.com/sourcebot-dev/sourcebot/pull/352)
|
- Delete account join request when redeeming an invite. [#352](https://github.com/sourcebot-dev/sourcebot/pull/352)
|
||||||
- Fix issue where a repository would not be included in a search context if the context was created before the repository. [#354](https://github.com/sourcebot-dev/sourcebot/pull/354)
|
- Fix issue where a repository would not be included in a search context if the context was created before the repository. [#354](https://github.com/sourcebot-dev/sourcebot/pull/354)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Changed search api (and all apis that depend on it) to return raw source code instead of base64 encoded string. ([356](https://github.com/sourcebot-dev/sourcebot/pull/356)).
|
||||||
|
|
||||||
|
|
||||||
## [4.3.0] - 2025-06-11
|
## [4.3.0] - 2025-06-11
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated API client to match the latest Sourcebot release. [#356](https://github.com/sourcebot-dev/sourcebot/pull/356)
|
||||||
|
|
||||||
## [1.0.2] - 2025-05-28
|
## [1.0.2] - 2025-05-28
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { z } from 'zod';
|
||||||
import { listRepos, search, getFileSource } from './client.js';
|
import { listRepos, search, getFileSource } from './client.js';
|
||||||
import { env, numberSchema } from './env.js';
|
import { env, numberSchema } from './env.js';
|
||||||
import { TextContent } from './types.js';
|
import { TextContent } from './types.js';
|
||||||
import { base64Decode, isServiceError } from './utils.js';
|
import { isServiceError } from './utils.js';
|
||||||
|
|
||||||
// Create MCP server
|
// Create MCP server
|
||||||
const server = new McpServer({
|
const server = new McpServer({
|
||||||
|
|
@ -114,8 +114,7 @@ server.tool(
|
||||||
|
|
||||||
if (includeCodeSnippets) {
|
if (includeCodeSnippets) {
|
||||||
const snippets = file.chunks.map(chunk => {
|
const snippets = file.chunks.map(chunk => {
|
||||||
const content = base64Decode(chunk.content);
|
return `\`\`\`\n${chunk.content}\n\`\`\``
|
||||||
return `\`\`\`\n${content}\n\`\`\``
|
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
text += `\n\n${snippets}`;
|
text += `\n\n${snippets}`;
|
||||||
}
|
}
|
||||||
|
|
@ -201,7 +200,7 @@ server.tool(
|
||||||
|
|
||||||
const content: TextContent[] = [{
|
const content: TextContent[] = [{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `file: ${fileName}\nrepository: ${repoId}\nlanguage: ${response.language}\nsource:\n${base64Decode(response.source)}`,
|
text: `file: ${fileName}\nrepository: ${repoId}\nlanguage: ${response.language}\nsource:\n${response.source}`,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
import { ServiceError } from "./types.js";
|
import { ServiceError } from "./types.js";
|
||||||
|
|
||||||
// From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
|
|
||||||
export const base64Decode = (base64: string): string => {
|
|
||||||
const binString = atob(base64);
|
|
||||||
return Buffer.from(Uint8Array.from(binString, (m) => m.codePointAt(0)!).buffer).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isServiceError = (data: unknown): data is ServiceError => {
|
export const isServiceError = (data: unknown): data is ServiceError => {
|
||||||
return typeof data === 'object' &&
|
return typeof data === 'object' &&
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { base64Decode, getCodeHostInfoForRepo, unwrapServiceError } from "@/lib/utils";
|
import { getCodeHostInfoForRepo, unwrapServiceError } from "@/lib/utils";
|
||||||
import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams";
|
import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { getFileSource } from "@/features/search/fileSourceApi";
|
import { getFileSource } from "@/features/search/fileSourceApi";
|
||||||
|
|
@ -88,7 +88,7 @@ export const CodePreviewPanel = () => {
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<PureCodePreviewPanel
|
<PureCodePreviewPanel
|
||||||
source={base64Decode(fileSourceResponse.source)}
|
source={fileSourceResponse.source}
|
||||||
language={fileSourceResponse.language}
|
language={fileSourceResponse.language}
|
||||||
repoName={repoName}
|
repoName={repoName}
|
||||||
path={path}
|
path={path}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import { useDomain } from "@/hooks/useDomain";
|
||||||
import { SymbolIcon } from "@radix-ui/react-icons";
|
import { SymbolIcon } from "@radix-ui/react-icons";
|
||||||
import { SetStateAction, Dispatch, useMemo } from "react";
|
import { SetStateAction, Dispatch, useMemo } from "react";
|
||||||
import { getFileSource } from "@/features/search/fileSourceApi";
|
import { getFileSource } from "@/features/search/fileSourceApi";
|
||||||
import { base64Decode } from "@/lib/utils";
|
|
||||||
import { unwrapServiceError } from "@/lib/utils";
|
import { unwrapServiceError } from "@/lib/utils";
|
||||||
|
|
||||||
interface CodePreviewPanelProps {
|
interface CodePreviewPanelProps {
|
||||||
|
|
@ -41,10 +40,8 @@ export const CodePreviewPanel = ({
|
||||||
}, domain)
|
}, domain)
|
||||||
),
|
),
|
||||||
select: (data) => {
|
select: (data) => {
|
||||||
const decodedSource = base64Decode(data.source);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: decodedSource,
|
content: data.source,
|
||||||
filepath: previewedFile.fileName.text,
|
filepath: previewedFile.fileName.text,
|
||||||
matches: previewedFile.chunks,
|
matches: previewedFile.chunks,
|
||||||
link: previewedFile.webUrl,
|
link: previewedFile.webUrl,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback } from "react";
|
||||||
import { SearchResultFile, SearchResultChunk } from "@/features/search/types";
|
import { SearchResultFile, SearchResultChunk } from "@/features/search/types";
|
||||||
import { base64Decode } from "@/lib/utils";
|
|
||||||
import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter";
|
import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter";
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -17,17 +16,12 @@ export const FileMatch = ({
|
||||||
file,
|
file,
|
||||||
onOpen: _onOpen,
|
onOpen: _onOpen,
|
||||||
}: FileMatchProps) => {
|
}: FileMatchProps) => {
|
||||||
|
|
||||||
const content = useMemo(() => {
|
|
||||||
return base64Decode(match.content);
|
|
||||||
}, [match.content]);
|
|
||||||
|
|
||||||
const onOpen = useCallback((isCtrlKeyPressed: boolean) => {
|
const onOpen = useCallback((isCtrlKeyPressed: boolean) => {
|
||||||
const startLineNumber = match.contentStart.lineNumber;
|
const startLineNumber = match.contentStart.lineNumber;
|
||||||
const endLineNumber = content.trimEnd().split('\n').length + startLineNumber - 1;
|
const endLineNumber = match.content.trimEnd().split('\n').length + startLineNumber - 1;
|
||||||
|
|
||||||
_onOpen(startLineNumber, endLineNumber, isCtrlKeyPressed);
|
_onOpen(startLineNumber, endLineNumber, isCtrlKeyPressed);
|
||||||
}, [content, match.contentStart.lineNumber, _onOpen]);
|
}, [match.content, match.contentStart.lineNumber, _onOpen]);
|
||||||
|
|
||||||
// If it's just the title, don't show a code preview
|
// If it's just the title, don't show a code preview
|
||||||
if (match.matchRanges.length === 0) {
|
if (match.matchRanges.length === 0) {
|
||||||
|
|
@ -57,7 +51,7 @@ export const FileMatch = ({
|
||||||
lineNumbersOffset={match.contentStart.lineNumber}
|
lineNumbersOffset={match.contentStart.lineNumber}
|
||||||
renderWhitespace={true}
|
renderWhitespace={true}
|
||||||
>
|
>
|
||||||
{content}
|
{match.content}
|
||||||
</LightweightCodeHighlighter>
|
</LightweightCodeHighlighter>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import { PathHeader } from "@/app/[domain]/components/pathHeader";
|
||||||
import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter";
|
import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter";
|
||||||
import { FindRelatedSymbolsResponse } from "@/features/codeNav/types";
|
import { FindRelatedSymbolsResponse } from "@/features/codeNav/types";
|
||||||
import { RepositoryInfo, SourceRange } from "@/features/search/types";
|
import { RepositoryInfo, SourceRange } from "@/features/search/types";
|
||||||
import { base64Decode } from "@/lib/utils";
|
|
||||||
import { useMemo, useRef } from "react";
|
import { useMemo, useRef } from "react";
|
||||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
|
|
@ -155,10 +154,6 @@ const ReferenceListItem = ({
|
||||||
onClick,
|
onClick,
|
||||||
onMouseEnter,
|
onMouseEnter,
|
||||||
}: ReferenceListItemProps) => {
|
}: ReferenceListItemProps) => {
|
||||||
const decodedLineContent = useMemo(() => {
|
|
||||||
return base64Decode(lineContent);
|
|
||||||
}, [lineContent]);
|
|
||||||
|
|
||||||
const highlightRanges = useMemo(() => [range], [range]);
|
const highlightRanges = useMemo(() => [range], [range]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -174,7 +169,7 @@ const ReferenceListItem = ({
|
||||||
lineNumbersOffset={range.start.lineNumber}
|
lineNumbersOffset={range.start.lineNumber}
|
||||||
renderWhitespace={false}
|
renderWhitespace={false}
|
||||||
>
|
>
|
||||||
{decodedLineContent}
|
{lineContent}
|
||||||
</LightweightCodeHighlighter>
|
</LightweightCodeHighlighter>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
|
||||||
import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter";
|
import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { SourceRange } from "@/features/search/types";
|
import { SourceRange } from "@/features/search/types";
|
||||||
import { base64Decode } from "@/lib/utils";
|
|
||||||
|
|
||||||
interface SymbolDefinitionPreviewProps {
|
interface SymbolDefinitionPreviewProps {
|
||||||
symbolDefinition: {
|
symbolDefinition: {
|
||||||
|
|
@ -21,10 +20,6 @@ export const SymbolDefinitionPreview = ({
|
||||||
const { lineContent, language, range } = symbolDefinition;
|
const { lineContent, language, range } = symbolDefinition;
|
||||||
const highlightRanges = useMemo(() => [range], [range]);
|
const highlightRanges = useMemo(() => [range], [range]);
|
||||||
|
|
||||||
const decodedLineContent = useMemo(() => {
|
|
||||||
return base64Decode(lineContent);
|
|
||||||
}, [lineContent]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 mb-2">
|
<div className="flex flex-col gap-2 mb-2">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|
@ -55,7 +50,7 @@ export const SymbolDefinitionPreview = ({
|
||||||
lineNumbersOffset={range.start.lineNumber}
|
lineNumbersOffset={range.start.lineNumber}
|
||||||
renderWhitespace={false}
|
renderWhitespace={false}
|
||||||
>
|
>
|
||||||
{decodedLineContent}
|
{lineContent}
|
||||||
</LightweightCodeHighlighter>
|
</LightweightCodeHighlighter>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { sourcebot_context, sourcebot_pr_payload } from "@/features/agents/review-agent/types";
|
import { sourcebot_context, sourcebot_pr_payload } from "@/features/agents/review-agent/types";
|
||||||
import { getFileSource } from "@/features/search/fileSourceApi";
|
import { getFileSource } from "@/features/search/fileSourceApi";
|
||||||
import { fileSourceResponseSchema } from "@/features/search/schemas";
|
import { fileSourceResponseSchema } from "@/features/search/schemas";
|
||||||
import { base64Decode } from "@/lib/utils";
|
|
||||||
import { isServiceError } from "@/lib/utils";
|
import { isServiceError } from "@/lib/utils";
|
||||||
import { env } from "@/env.mjs";
|
import { env } from "@/env.mjs";
|
||||||
import { createLogger } from "@sourcebot/logger";
|
import { createLogger } from "@sourcebot/logger";
|
||||||
|
|
@ -24,7 +23,7 @@ export const fetchFileContent = async (pr_payload: sourcebot_pr_payload, filenam
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileSourceResponse = fileSourceResponseSchema.parse(response);
|
const fileSourceResponse = fileSourceResponseSchema.parse(response);
|
||||||
const fileContent = base64Decode(fileSourceResponse.source);
|
const fileContent = fileSourceResponse.source;
|
||||||
|
|
||||||
const fileContentContext: sourcebot_context = {
|
const fileContentContext: sourcebot_context = {
|
||||||
type: "file_content",
|
type: "file_content",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { SearchRequest, SearchResponse, SourceRange } from "./types";
|
||||||
import { OrgRole, Repo } from "@sourcebot/db";
|
import { OrgRole, Repo } from "@sourcebot/db";
|
||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from "@sentry/nextjs";
|
||||||
import { sew, withAuth, withOrgMembership } from "@/actions";
|
import { sew, withAuth, withOrgMembership } from "@/actions";
|
||||||
|
import { base64Decode } from "@sourcebot/shared";
|
||||||
|
|
||||||
// List of supported query prefixes in zoekt.
|
// List of supported query prefixes in zoekt.
|
||||||
// @see : https://github.com/sourcebot-dev/zoekt/blob/main/query/parse.go#L417
|
// @see : https://github.com/sourcebot-dev/zoekt/blob/main/query/parse.go#L417
|
||||||
|
|
@ -264,7 +265,7 @@ export const search = async ({ query, matches, contextLines, whole }: SearchRequ
|
||||||
.filter((chunk) => !chunk.FileName) // Filter out filename chunks.
|
.filter((chunk) => !chunk.FileName) // Filter out filename chunks.
|
||||||
.map((chunk) => {
|
.map((chunk) => {
|
||||||
return {
|
return {
|
||||||
content: chunk.Content,
|
content: base64Decode(chunk.Content),
|
||||||
matchRanges: chunk.Ranges.map((range) => ({
|
matchRanges: chunk.Ranges.map((range) => ({
|
||||||
start: {
|
start: {
|
||||||
byteOffset: range.Start.ByteOffset,
|
byteOffset: range.Start.ByteOffset,
|
||||||
|
|
@ -295,7 +296,7 @@ export const search = async ({ query, matches, contextLines, whole }: SearchRequ
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
branches: file.Branches,
|
branches: file.Branches,
|
||||||
content: file.Content,
|
content: file.Content ? base64Decode(file.Content) : undefined,
|
||||||
}
|
}
|
||||||
}).filter((file) => file !== undefined) ?? [];
|
}).filter((file) => file !== undefined) ?? [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -301,12 +301,6 @@ export const isServiceError = (data: unknown): data is ServiceError => {
|
||||||
'message' in data;
|
'message' in data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
|
|
||||||
export const base64Decode = (base64: string): string => {
|
|
||||||
const binString = atob(base64);
|
|
||||||
return Buffer.from(Uint8Array.from(binString, (m) => m.codePointAt(0)!).buffer).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// @see: https://stackoverflow.com/a/65959350/23221295
|
// @see: https://stackoverflow.com/a/65959350/23221295
|
||||||
export const isDefined = <T>(arg: T | null | undefined): arg is T extends null | undefined ? never : T => {
|
export const isDefined = <T>(arg: T | null | undefined): arg is T extends null | undefined ? never : T => {
|
||||||
return arg !== null && arg !== undefined;
|
return arg !== null && arg !== undefined;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue