Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Send anonymous server-side PostHog events as personless so unauthenticated requests don't inflate person counts. [#1367](https://github.com/sourcebot-dev/sourcebot/pull/1367)
- [EE] Fixed Ask Sourcebot mermaid diagrams overflowing their container by contain-fitting them to both width and height, and made revealing a diagram from the answer jump it into view instantly to avoid over/undershooting. [#1373](https://github.com/sourcebot-dev/sourcebot/pull/1373)
- Passed Zoekt index parameters via argv to preserve revision names with punctuation. [#1376](https://github.com/sourcebot-dev/sourcebot/pull/1376)
- Show a clearer missing-file state in the code browser when a blob path no longer exists. [#1381](https://github.com/sourcebot-dev/sourcebot/pull/1381)

## [5.0.4] - 2026-06-18

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { cleanup, render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { TooltipProvider } from '@/components/ui/tooltip';
import { ErrorCode } from '@/lib/errorCodes';

const mocks = vi.hoisted(() => ({
getRepoInfoByName: vi.fn(),
getFileSource: vi.fn(),
getFileBlame: vi.fn(),
}));

vi.mock('@/actions', () => ({
getRepoInfoByName: mocks.getRepoInfoByName,
}));

vi.mock('@/features/git', () => ({
getFileSource: mocks.getFileSource,
getFileBlame: mocks.getFileBlame,
}));

vi.mock('@/app/(app)/components/pathHeader', () => ({
PathHeader: ({ path, revisionName }: { path: string; revisionName?: string }) => (
<div data-testid="path-header">Path: {path}; Revision: {revisionName ?? 'default'}</div>
),
}));

vi.mock('./pureCodePreviewPanel', () => ({
PureCodePreviewPanel: () => <div>Code preview</div>,
}));

vi.mock('@/ee/features/codeNav/components/symbolHoverPopup', () => ({
SymbolHoverPopup: () => null,
}));

import { CodePreviewPanel } from './codePreviewPanel';

const renderCodePreviewPanel = async (props: Parameters<typeof CodePreviewPanel>[0]) => {
return render(
<TooltipProvider>
{await CodePreviewPanel(props)}
</TooltipProvider>
);
};

afterEach(() => {
cleanup();
});

describe('CodePreviewPanel', () => {
beforeEach(() => {
vi.clearAllMocks();

mocks.getRepoInfoByName.mockResolvedValue({
name: 'github.com/sourcebot-dev/sourcebot',
displayName: 'sourcebot-dev/sourcebot',
codeHostType: 'github',
externalWebUrl: 'https://github.com/sourcebot-dev/sourcebot',
});
});

test('renders a browse 404 when the requested file does not exist', async () => {
mocks.getFileSource.mockResolvedValue({
statusCode: 404,
errorCode: ErrorCode.FILE_NOT_FOUND,
message: 'File "src/missing.ts" not found in repository "github.com/sourcebot-dev/sourcebot"',
});

await renderCodePreviewPanel({
path: 'src/missing.ts',
repoName: 'github.com/sourcebot-dev/sourcebot',
revisionName: 'feature-branch',
});

expect(screen.queryByText('File not found')).toBeTruthy();
expect(screen.queryAllByText(/src\/missing\.ts/).length).toBeGreaterThan(0);
expect(screen.queryByText(/Error loading file source/)).toBeNull();
expect(screen.getByRole('link', { name: 'Return to repository root' }).getAttribute('href')).toBe(
'/browse/github.com/sourcebot-dev/sourcebot@feature-branch/-/tree'
);
});

test('keeps preview 404 navigation anchored to the browse revision', async () => {
mocks.getFileSource.mockResolvedValue({
statusCode: 404,
errorCode: ErrorCode.FILE_NOT_FOUND,
message: 'File "src/missing.ts" not found in repository "github.com/sourcebot-dev/sourcebot"',
});

await renderCodePreviewPanel({
path: 'src/missing.ts',
repoName: 'github.com/sourcebot-dev/sourcebot',
revisionName: 'main',
previewRef: 'abc123def456',
});

expect(screen.queryByText('File not found')).toBeTruthy();
expect(screen.getAllByText('abc123def456').length).toBeGreaterThan(0);
expect(screen.getByText('src/missing.ts').className).toContain('break-all');
expect(screen.getAllByText('abc123def456').some((element) => element.className.includes('break-all'))).toBe(true);
expect(screen.getByRole('link', { name: 'Return to repository root' }).getAttribute('href')).toBe(
'/browse/github.com/sourcebot-dev/sourcebot@main/-/tree'
);
Comment thread
DivyamTalwar marked this conversation as resolved.
expect(screen.getByRole('link', { name: 'Close preview' }).getAttribute('href')).toBe(
'/browse/github.com/sourcebot-dev/sourcebot@main/-/blob/src%2Fmissing.ts'
);
});

test('keeps successful preview header anchored to the browse revision', async () => {
mocks.getFileSource.mockResolvedValue({
source: 'const value = 1;\n',
language: 'typescript',
});

await renderCodePreviewPanel({
path: 'src/index.ts',
repoName: 'github.com/sourcebot-dev/sourcebot',
revisionName: 'main',
previewRef: 'abc123def456',
});

expect(mocks.getFileSource).toHaveBeenCalledWith({
path: 'src/index.ts',
repo: 'github.com/sourcebot-dev/sourcebot',
ref: 'abc123def456',
}, { source: 'sourcebot-web-client' });
expect(screen.getByTestId('path-header').textContent).toBe('Path: src/index.ts; Revision: main');

const closePreviewLink = screen.getByRole('link', { name: 'Close preview' });
expect(closePreviewLink.getAttribute('href')).toBe(
'/browse/github.com/sourcebot-dev/sourcebot@main/-/blob/src%2Findex.ts'
);
expect(closePreviewLink.closest('button')).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { PathHeader } from "@/app/(app)/components/pathHeader";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { ErrorCode } from "@/lib/errorCodes";
import { cn, getCodeHostInfoForRepo, isServiceError, truncateSha } from "@/lib/utils";
import { X } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { getBrowsePath } from "../../../hooks/utils";
import { BlameAgeLegend } from "./blameAgeLegend";
import { BlameViewToggle } from "./blameViewToggle";
import { FileNotFoundPanel } from "./fileNotFoundPanel";
import { PureCodePreviewPanel } from "./pureCodePreviewPanel";
import { getFileBlame, getFileSource } from '@/features/git';

Expand Down Expand Up @@ -54,14 +56,31 @@ export const CodePreviewPanel = async ({ path, repoName, revisionName, previewRe
: Promise.resolve(undefined),
]);

if (isServiceError(fileSourceResponse)) {
return <div>Error loading file source: {fileSourceResponse.message}</div>
}

if (isServiceError(repoInfoResponse)) {
return <div>Error loading repo info: {repoInfoResponse.message}</div>
}

if (isServiceError(fileSourceResponse)) {
if (fileSourceResponse.errorCode === ErrorCode.FILE_NOT_FOUND) {
return (
<FileNotFoundPanel
path={path}
repoName={repoName}
browseRevisionName={revisionName}
missingRevisionName={contentRef}
previewRef={previewRef}
repo={{
codeHostType: repoInfoResponse.codeHostType,
displayName: repoInfoResponse.displayName,
externalWebUrl: repoInfoResponse.externalWebUrl,
}}
/>
);
}

return <div>Error loading file source: {fileSourceResponse.message}</div>
}

if (blameResponse !== undefined && isServiceError(blameResponse)) {
return <div>Error loading blame: {blameResponse.message}</div>
}
Expand Down Expand Up @@ -96,7 +115,7 @@ export const CodePreviewPanel = async ({ path, repoName, revisionName, previewRe
displayName: repoInfoResponse.displayName,
externalWebUrl: repoInfoResponse.externalWebUrl,
}}
revisionName={contentRef}
revisionName={revisionName}
/>

{fileWebUrl && (
Expand Down Expand Up @@ -154,7 +173,7 @@ export const CodePreviewPanel = async ({ path, repoName, revisionName, previewRe
</Link>
</span>
<Tooltip key={previewRef}>
<TooltipTrigger>
<TooltipTrigger asChild>
<Button
asChild
variant="ghost"
Expand Down Expand Up @@ -188,4 +207,4 @@ export const CodePreviewPanel = async ({ path, repoName, revisionName, previewRe
/>
</>
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { PathHeader } from "@/app/(app)/components/pathHeader";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { truncateSha } from "@/lib/utils";
import { FileQuestion, X } from "lucide-react";
import Link from "next/link";
import { getBrowsePath } from "../../../hooks/utils";
import type { CodeHostType } from "@sourcebot/db";

interface FileNotFoundPanelProps {
path: string;
repoName: string;
browseRevisionName?: string;
missingRevisionName?: string;
previewRef?: string;
repo: {
codeHostType: CodeHostType;
displayName?: string;
externalWebUrl?: string;
};
}

export const FileNotFoundPanel = ({
path,
repoName,
browseRevisionName,
missingRevisionName,
previewRef,
repo,
}: FileNotFoundPanelProps) => {
return (
<>
<div className="flex flex-row py-1 px-2 items-center justify-between">
<PathHeader
path={path}
repo={{
name: repoName,
codeHostType: repo.codeHostType,
displayName: repo.displayName,
externalWebUrl: repo.externalWebUrl,
}}
revisionName={browseRevisionName}
/>
</div>
<Separator />
{previewRef && (
<div className="flex flex-row items-center justify-between gap-2 px-4 py-2 border-b shrink-0">
<span className="text-sm">
Previewing file at revision{" "}
<Link
href={getBrowsePath({
repoName,
revisionName: browseRevisionName,
path: '',
pathType: 'commit',
commitSha: previewRef,
})}
className="font-mono text-link hover:underline"
>
{truncateSha(previewRef)}
</Link>
</span>
<Tooltip key={previewRef}>
<TooltipTrigger asChild>
<Button
asChild
variant="ghost"
size="icon"
className="h-6 w-6 text-muted-foreground"
>
<Link
href={getBrowsePath({
repoName,
revisionName: browseRevisionName,
path,
pathType: 'blob',
})}
aria-label="Close preview"
>
<X className="h-4 w-4" />
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>Close preview</TooltipContent>
</Tooltip>
</div>
)}
<div className="flex min-h-72 flex-col items-center justify-center gap-4 px-6 py-12 text-center">
<div className="flex h-12 w-12 items-center justify-center rounded-md border bg-muted text-muted-foreground">
<FileQuestion className="h-6 w-6" />
</div>
<div className="space-y-1">
<h2 className="text-lg font-semibold">File not found</h2>
<p className="max-w-xl break-words text-sm text-muted-foreground">
The path <span className="break-all font-mono text-foreground">{path}</span> does not exist
{missingRevisionName ? <> at <span className="break-all font-mono text-foreground">{missingRevisionName}</span></> : null}.
</p>
</div>
<Button asChild variant="outline">
<Link
href={getBrowsePath({
repoName,
revisionName: browseRevisionName,
path: '',
pathType: 'tree',
})}
>
Return to repository root
</Link>
</Button>
</div>
</>
);
}