fix: reject oversized checkout diffs (#2488)
* fix: reject oversized checkout diffs * fix(diff): keep large previews from disconnecting clients The physical socket limit is an OOM backstop, not a frame-size contract. Bound structured diff construction incrementally and report oversized previews without closing the client's socket. * fix(app): preserve oversized diff state * fix(diff): stay within relay frame limit --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
co-authored by
Mohamed Boudra
parent
16bbead5a4
commit
9e5accee3c
@@ -204,25 +204,34 @@ describe("server data push router", () => {
|
||||
|
||||
fake.emit({
|
||||
type: "subscribe_checkout_diff_response",
|
||||
payload: { subscriptionId, cwd, files: [], error: null, requestId: "diff-1" },
|
||||
payload: {
|
||||
subscriptionId,
|
||||
cwd,
|
||||
files: [],
|
||||
error: null,
|
||||
diffTooLarge: true,
|
||||
requestId: "diff-1",
|
||||
},
|
||||
});
|
||||
|
||||
expect(queryClient.getQueryData(queryKey)).toEqual({
|
||||
cwd,
|
||||
files: [],
|
||||
error: null,
|
||||
diffTooLarge: true,
|
||||
requestId: "diff-1",
|
||||
});
|
||||
|
||||
fake.emit({
|
||||
type: "checkout_diff_update",
|
||||
payload: { subscriptionId, cwd, files: [], error: null },
|
||||
payload: { subscriptionId, cwd, files: [], error: null, diffTooLarge: true },
|
||||
});
|
||||
|
||||
expect(queryClient.getQueryData(queryKey)).toEqual({
|
||||
cwd,
|
||||
files: [],
|
||||
error: null,
|
||||
diffTooLarge: true,
|
||||
requestId: `subscription:${subscriptionId}`,
|
||||
});
|
||||
|
||||
|
||||
@@ -420,6 +420,9 @@ function applyCheckoutDiffUpdate(input: {
|
||||
cwd: input.message.payload.cwd,
|
||||
files: orderCheckoutDiffFiles(input.message.payload.files),
|
||||
error: input.message.payload.error,
|
||||
...(input.message.payload.diffTooLarge !== undefined
|
||||
? { diffTooLarge: input.message.payload.diffTooLarge }
|
||||
: {}),
|
||||
requestId: `subscription:${input.message.payload.subscriptionId}`,
|
||||
},
|
||||
});
|
||||
@@ -440,6 +443,9 @@ function applyCheckoutDiffSubscribeResponse(input: {
|
||||
cwd: input.message.payload.cwd,
|
||||
files: orderCheckoutDiffFiles(input.message.payload.files),
|
||||
error: input.message.payload.error,
|
||||
...(input.message.payload.diffTooLarge !== undefined
|
||||
? { diffTooLarge: input.message.payload.diffTooLarge }
|
||||
: {}),
|
||||
requestId: input.message.payload.requestId,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -119,6 +119,7 @@ import {
|
||||
type InlineReviewActions,
|
||||
} from "@/review";
|
||||
import { usePublishWorkingDiffAttachment, useWorkingDiff } from "@/git/use-working-diff";
|
||||
import { DiffTooLargeState } from "@/git/diff-too-large-state";
|
||||
|
||||
export type { GitActionId, GitAction, GitActions } from "@/git/policy";
|
||||
|
||||
@@ -1755,6 +1756,7 @@ interface DiffBodyContentProps {
|
||||
notGit: boolean;
|
||||
isDiffLoading: boolean;
|
||||
diffErrorMessage: string | null;
|
||||
diffTooLarge: boolean;
|
||||
hasChanges: boolean;
|
||||
emptyMessage: string;
|
||||
children: ReactElement;
|
||||
@@ -1768,6 +1770,7 @@ function DiffBodyContent({
|
||||
notGit,
|
||||
isDiffLoading,
|
||||
diffErrorMessage,
|
||||
diffTooLarge,
|
||||
hasChanges,
|
||||
emptyMessage,
|
||||
children,
|
||||
@@ -1803,6 +1806,9 @@ function DiffBodyContent({
|
||||
</View>
|
||||
);
|
||||
}
|
||||
if (diffTooLarge) {
|
||||
return <DiffTooLargeState />;
|
||||
}
|
||||
if (diffErrorMessage) {
|
||||
return (
|
||||
<View style={styles.errorContainer}>
|
||||
@@ -2693,6 +2699,7 @@ export function GitDiffPane({
|
||||
selectBase: handleSelectBase,
|
||||
files,
|
||||
diffPayloadError,
|
||||
diffTooLarge,
|
||||
isDiffLoading,
|
||||
reviewActions,
|
||||
reviewAttachment,
|
||||
@@ -2855,6 +2862,7 @@ export function GitDiffPane({
|
||||
notGit={notGit}
|
||||
isDiffLoading={isDiffLoading}
|
||||
diffErrorMessage={diffErrorMessage}
|
||||
diffTooLarge={diffTooLarge}
|
||||
hasChanges={hasChanges}
|
||||
emptyMessage={emptyMessage}
|
||||
checkingRepositoryLabel={t("workspace.git.diff.checkingRepository")}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Text, View } from "react-native";
|
||||
import { FileWarning } from "lucide-react-native";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { ICON_SIZE, type Theme } from "@/styles/theme";
|
||||
|
||||
const ThemedFileWarning = withUnistyles(FileWarning);
|
||||
const foregroundMutedIconColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||
|
||||
export function DiffTooLargeState() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View style={styles.container} testID="diff-too-large">
|
||||
<ThemedFileWarning size={ICON_SIZE.lg} uniProps={foregroundMutedIconColorMapping} />
|
||||
<View style={styles.copy}>
|
||||
<Text style={styles.title}>{t("workspace.git.diff.previewTooLargeTitle")}</Text>
|
||||
<Text style={styles.description}>{t("workspace.git.diff.previewTooLargeDescription")}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: theme.spacing[3],
|
||||
paddingHorizontal: theme.spacing[6],
|
||||
paddingTop: theme.spacing[16],
|
||||
},
|
||||
copy: {
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[1],
|
||||
maxWidth: 360,
|
||||
},
|
||||
title: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
textAlign: "center",
|
||||
},
|
||||
description: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.xs,
|
||||
textAlign: "center",
|
||||
},
|
||||
}));
|
||||
@@ -92,6 +92,7 @@ export function useCheckoutDiffQuery({
|
||||
return {
|
||||
files: payload?.files ?? [],
|
||||
payloadError,
|
||||
diffTooLarge: payload?.diffTooLarge === true,
|
||||
isLoading: payload === null && enabled && isConnected,
|
||||
isFetching: false,
|
||||
isError: Boolean(payloadError),
|
||||
|
||||
@@ -79,6 +79,7 @@ export function useWorkingDiff({
|
||||
const {
|
||||
files,
|
||||
payloadError: diffPayloadError,
|
||||
diffTooLarge,
|
||||
isLoading: isDiffLoading,
|
||||
} = useCheckoutDiffQuery({
|
||||
serverId,
|
||||
@@ -123,6 +124,7 @@ export function useWorkingDiff({
|
||||
selectBase,
|
||||
files,
|
||||
diffPayloadError,
|
||||
diffTooLarge,
|
||||
isDiffLoading,
|
||||
reviewActions,
|
||||
reviewAttachment,
|
||||
|
||||
@@ -764,6 +764,8 @@ export const ar: TranslationResources = {
|
||||
closeChangesTab: "إغلاق علامة تبويب التغييرات",
|
||||
binaryFile: "ملف ثنائي",
|
||||
tooLarge: "الفرق كبير جدًا بحيث لا يمكن عرضه",
|
||||
previewTooLargeTitle: "هذا الفرق كبير جدًا بحيث لا يمكن معاينته",
|
||||
previewTooLargeDescription: "ضيّق نطاق المقارنة لمعاينتها هنا",
|
||||
unified: "الفرق الموحدة",
|
||||
split: "فرق جنبًا إلى جنب",
|
||||
switchToUnified: "التبديل إلى الفرق الموحد",
|
||||
|
||||
@@ -774,6 +774,8 @@ export const en = {
|
||||
closeChangesTab: "Close Changes tab",
|
||||
binaryFile: "Binary file",
|
||||
tooLarge: "Diff too large to display",
|
||||
previewTooLargeTitle: "This diff is too large to preview",
|
||||
previewTooLargeDescription: "Narrow the comparison to preview it here",
|
||||
unified: "Unified diff",
|
||||
split: "Side-by-side diff",
|
||||
switchToUnified: "Switch to unified diff",
|
||||
|
||||
@@ -795,6 +795,8 @@ export const es: TranslationResources = {
|
||||
closeChangesTab: "Cerrar la pestaña Cambios",
|
||||
binaryFile: "archivo binario",
|
||||
tooLarge: "La diferencia es demasiado grande para mostrarse",
|
||||
previewTooLargeTitle: "Este diff es demasiado grande para previsualizarlo",
|
||||
previewTooLargeDescription: "Reduce la comparación para previsualizarla aquí",
|
||||
unified: "Diferencia unificada",
|
||||
split: "Diferencia de lado a lado",
|
||||
switchToUnified: "Cambiar a diferencia unificada",
|
||||
|
||||
@@ -794,6 +794,8 @@ export const fr: TranslationResources = {
|
||||
closeChangesTab: "Fermer l'onglet Modifications",
|
||||
binaryFile: "Fichier binaire",
|
||||
tooLarge: "Diff trop grand pour être affiché",
|
||||
previewTooLargeTitle: "Ce diff est trop volumineux pour être prévisualisé",
|
||||
previewTooLargeDescription: "Réduisez la comparaison pour la prévisualiser ici",
|
||||
unified: "Différentiel unifié",
|
||||
split: "Différent côte à côte",
|
||||
switchToUnified: "Passer au diff unifié",
|
||||
|
||||
@@ -775,6 +775,8 @@ export const ja: TranslationResources = {
|
||||
closeChangesTab: "変更タブを閉じる",
|
||||
binaryFile: "バイナリファイル",
|
||||
tooLarge: "差分が大きすぎて表示できません",
|
||||
previewTooLargeTitle: "この差分は大きすぎるためプレビューできません",
|
||||
previewTooLargeDescription: "比較範囲を狭めると、ここでプレビューできます",
|
||||
unified: "ユニファイド差分",
|
||||
split: "左右比較",
|
||||
switchToUnified: "ユニファイド差分に切り替え",
|
||||
|
||||
@@ -786,6 +786,8 @@ export const ptBR: TranslationResources = {
|
||||
closeChangesTab: "Fechar a aba Alterações",
|
||||
binaryFile: "Arquivo binário",
|
||||
tooLarge: "Diff grande demais para exibir",
|
||||
previewTooLargeTitle: "Este diff é grande demais para visualizar",
|
||||
previewTooLargeDescription: "Reduza a comparação para visualizá-la aqui",
|
||||
unified: "Diff unificado",
|
||||
split: "Diff lado a lado",
|
||||
switchToUnified: "Alternar para diff unificado",
|
||||
|
||||
@@ -786,6 +786,8 @@ export const ru: TranslationResources = {
|
||||
closeChangesTab: "Закрыть вкладку «Изменения»",
|
||||
binaryFile: "Бинарный файл",
|
||||
tooLarge: "Разница слишком велика для отображения",
|
||||
previewTooLargeTitle: "Этот diff слишком велик для предпросмотра",
|
||||
previewTooLargeDescription: "Сузьте сравнение, чтобы просмотреть его здесь",
|
||||
unified: "Единый дифференциал",
|
||||
split: "Параллельная разница",
|
||||
switchToUnified: "Переключить на объединенный diff",
|
||||
|
||||
@@ -756,6 +756,8 @@ export const zhCN: TranslationResources = {
|
||||
closeChangesTab: "关闭“更改”标签页",
|
||||
binaryFile: "二进制文件",
|
||||
tooLarge: "Diff 过大,无法显示",
|
||||
previewTooLargeTitle: "此 diff 过大,无法预览",
|
||||
previewTooLargeDescription: "缩小比较范围即可在此预览",
|
||||
unified: "Unified diff",
|
||||
split: "Side-by-side diff",
|
||||
switchToUnified: "切换到统一差异视图",
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
resolveDiffLayout,
|
||||
SharedDiffView,
|
||||
} from "@/git/diff-pane";
|
||||
import { DiffTooLargeState } from "@/git/diff-too-large-state";
|
||||
import { useCommitDiffFiles } from "@/git/use-diff-files";
|
||||
import { usePublishWorkingDiffAttachment, useWorkingDiff } from "@/git/use-working-diff";
|
||||
import { useChangesPreferences } from "@/hooks/use-changes-preferences";
|
||||
@@ -120,6 +121,9 @@ function WorkingDiffBody({
|
||||
if (workingDiff.notGit) {
|
||||
return <PanelState message={t("workspace.git.diff.notRepository")} />;
|
||||
}
|
||||
if (workingDiff.diffTooLarge) {
|
||||
return <DiffTooLargeState />;
|
||||
}
|
||||
if (workingDiff.diffPayloadError) {
|
||||
return (
|
||||
<PanelState message={t("panels.diff.loadError")} tone="error" testID="working-diff-error" />
|
||||
|
||||
Reference in New Issue
Block a user