Skip to content
Merged
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 @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added commit diff viewer to code browser. [#1154](https://github.com/sourcebot-dev/sourcebot/pull/1154)
- Added `/api/commits/authors` to the public API to allow fetching a list of authors for a given path and revision. [#1150](https://github.com/sourcebot-dev/sourcebot/pull/1150)
- Added optional `path` query parameter to the `/api/diff` endpoint and `get_diff` MCP tool to restrict diffs to changes touching a specific file. [#1154](https://github.com/sourcebot-dev/sourcebot/pull/1154)
- Added collapsible file diffs in the commit diff panel. [#1157](https://github.com/sourcebot-dev/sourcebot/pull/1157)

### Fixed
- Bumped `postcss` to `8.5.10`. [#1155](https://github.com/sourcebot-dev/sourcebot/pull/1155)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { FileDiff } from "@/features/git";
import { useVirtualizer } from "@tanstack/react-virtual";
import { useMemo, useRef } from "react";
import { useCallback, useRef, useState } from "react";
import { FileDiffRow } from "./fileDiffRow";

interface FileDiffListProps {
Expand All @@ -23,7 +23,15 @@ const LINE_HEIGHT_PX = 18;
const MIN_ROW_HEIGHT_PX = 200;
const CONTEXT_LINES_PER_HUNK = 6;

const estimateRowHeight = (file: FileDiff): number => {
const getRowKey = (file: FileDiff, index: number): string => {
return file.newPath ?? file.oldPath ?? `idx-${index}`;
};

const estimateRowHeight = (file: FileDiff, isCollapsed: boolean): number => {
if (isCollapsed) {
return ROW_HEADER_PX;
}

const visibleLines = file.hunks.reduce((sum, hunk) => {
return sum + Math.max(hunk.oldRange.lines, hunk.newRange.lines) + CONTEXT_LINES_PER_HUNK;
}, 0);
Expand All @@ -34,14 +42,39 @@ const estimateRowHeight = (file: FileDiff): number => {

export const FileDiffList = ({ files, repoName, commitSha, parentSha }: FileDiffListProps) => {
const parentRef = useRef<HTMLDivElement>(null);
const [collapsedKeys, setCollapsedKeys] = useState<Set<string>>(() => new Set());
const collapsedKeysRef = useRef(collapsedKeys);
collapsedKeysRef.current = collapsedKeys;

const virtualizer = useVirtualizer({
count: files.length,
getScrollElement: () => parentRef.current,
estimateSize: (index) => estimateRowHeight(files[index]),
estimateSize: (index) => {
const file = files[index];
return estimateRowHeight(file, collapsedKeys.has(getRowKey(file, index)));
},
overscan: 6,
});

const toggleCollapsed = useCallback((key: string, index: number, rowStart: number) => {
const willCollapse = !collapsedKeysRef.current.has(key);
setCollapsedKeys((prev) => {
const next = new Set(prev);
if (next.has(key)) {
next.delete(key);
} else {
next.add(key);
}
return next;
});
if (willCollapse) {
const scrollTop = parentRef.current?.scrollTop ?? 0;
if (scrollTop > rowStart) {
virtualizer.scrollToIndex(index, { align: 'start' });
}
}
}, [virtualizer]);

return (
<div
ref={parentRef}
Expand All @@ -61,7 +94,7 @@ export const FileDiffList = ({ files, repoName, commitSha, parentSha }: FileDiff
>
{virtualizer.getVirtualItems().map((virtualRow) => {
const file = files[virtualRow.index];
const rowKey = file.newPath ?? file.oldPath ?? `idx-${virtualRow.index}`;
const rowKey = getRowKey(file, virtualRow.index);
return (
<div
key={virtualRow.key}
Expand All @@ -82,6 +115,8 @@ export const FileDiffList = ({ files, repoName, commitSha, parentSha }: FileDiff
repoName={repoName}
commitSha={commitSha}
parentSha={parentSha}
isCollapsed={collapsedKeys.has(rowKey)}
onToggleCollapsed={() => toggleCollapsed(rowKey, virtualRow.index, virtualRow.start)}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ interface FileDiffRowProps {
commitSha: string;
// Null for the initial commit (no parent).
parentSha: string | null;
isCollapsed: boolean;
onToggleCollapsed: () => void;
}

export const FileDiffRow = ({ file, yOffset, repoName, commitSha, parentSha }: FileDiffRowProps) => {
export const FileDiffRow = ({ file, yOffset, repoName, commitSha, parentSha, isCollapsed, onToggleCollapsed }: FileDiffRowProps) => {
const status = getFileStatus(file);

// Deleted files don't exist at the commit, so the link points to the
Expand Down Expand Up @@ -61,7 +63,7 @@ export const FileDiffRow = ({ file, yOffset, repoName, commitSha, parentSha }: F
className="flex flex-row items-center gap-2 py-2 px-3 border-b bg-muted sticky z-10"
style={{ top: `-${yOffset}px` }}
>
<StatusBadge status={status} />
<StatusBadge status={status} onToggle={onToggleCollapsed} isCollapsed={isCollapsed} />
<div className="flex-1 min-w-0 flex flex-row items-center gap-1 overflow-hidden">
<code className="text-xs truncate">{getDisplayPath(file)}</code>
<CopyIconButton onCopy={onCopyPath} className="flex-shrink-0" />
Expand All @@ -75,16 +77,18 @@ export const FileDiffRow = ({ file, yOffset, repoName, commitSha, parentSha }: F
/>
)}
</div>
{file.hunks.length === 0 ? (
<div className="p-4 text-sm text-muted-foreground">
No textual diff (binary file or empty change).
</div>
) : (
<LightweightDiffViewer
hunks={file.hunks}
oldPath={file.oldPath}
newPath={file.newPath}
/>
{!isCollapsed && (
file.hunks.length === 0 ? (
<div className="p-4 text-sm text-muted-foreground">
No textual diff (binary file or empty change).
</div>
) : (
<LightweightDiffViewer
hunks={file.hunks}
oldPath={file.oldPath}
newPath={file.newPath}
/>
)
)}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { FileDiff } from "@/features/git";
import { ChevronDown, ChevronRight } from "lucide-react";

export type FileStatus = 'added' | 'modified' | 'deleted' | 'renamed';

Expand Down Expand Up @@ -29,10 +30,35 @@ const STATUS_BADGE_COLORS: Record<FileStatus, string> = {
renamed: 'bg-blue-500/20 text-blue-700 dark:text-blue-400',
};

export const StatusBadge = ({ status }: { status: FileStatus }) => (
<span
className={`inline-flex items-center justify-center w-5 h-5 rounded text-xs font-mono font-bold ${STATUS_BADGE_COLORS[status]}`}
>
{STATUS_BADGE_LABELS[status]}
</span>
);
interface StatusBadgeProps {
status: FileStatus;
onToggle?: () => void;
isCollapsed?: boolean;
}

export const StatusBadge = ({ status, onToggle, isCollapsed }: StatusBadgeProps) => {
const baseClassName = `inline-flex items-center justify-center w-5 h-5 rounded text-xs font-mono font-bold ${STATUS_BADGE_COLORS[status]}`;

if (!onToggle) {
return (
<span className={baseClassName}>
{STATUS_BADGE_LABELS[status]}
</span>
);
}

const Chevron = isCollapsed ? ChevronRight : ChevronDown;

return (
<button
type="button"
onClick={onToggle}
aria-label={isCollapsed ? "Expand diff" : "Collapse diff"}
aria-expanded={!isCollapsed}
className={`${baseClassName} group cursor-pointer`}
>
<span className="group-hover:hidden">{STATUS_BADGE_LABELS[status]}</span>
<Chevron className="hidden group-hover:block w-3 h-3" />
</button>
);
};
Loading