diff --git a/client/src/components/lanes/FileExplorer.tsx b/client/src/components/lanes/FileExplorer.tsx index 153171d1..c85a82e4 100644 --- a/client/src/components/lanes/FileExplorer.tsx +++ b/client/src/components/lanes/FileExplorer.tsx @@ -229,6 +229,8 @@ async function walkEntry( out.push({ file, rel: `${prefix}${entry.name}` }); return; } + // The server takes nothing into .git: never read one off the disk at all. + if (entry.name === ".git") return; const reader = (entry as FileSystemDirectoryEntry).createReader(); // readEntries answers in pages until it answers []. for (;;) { @@ -614,8 +616,14 @@ export function FileBrowser({ }; /** Upload `files` (each with its path inside the pick/drop) under `dir`. */ - const uploadAll = async (files: { file: File; rel: string }[], dir = path) => { + const uploadAll = async (picked: { file: File; rel: string }[], dir = path) => { setMenu(null); + // A dropped/picked repo carries its .git, which the server refuses file by + // file — skip it here with ONE notice instead of a toast per object. + const files = picked.filter((f) => !f.rel.split("/").includes(".git")); + if (files.length < picked.length) { + toast.info(t("fx.gitSkipped", { count: picked.length - files.length })); + } if (!files.length) return; const bytes = files.reduce((n, f) => n + f.file.size, 0); if (bytes > FX_BATCH_MAX_BYTES) { diff --git a/client/src/components/lanes/__tests__/FileExplorer.test.tsx b/client/src/components/lanes/__tests__/FileExplorer.test.tsx index 1315311f..e653e5e0 100644 --- a/client/src/components/lanes/__tests__/FileExplorer.test.tsx +++ b/client/src/components/lanes/__tests__/FileExplorer.test.tsx @@ -3,7 +3,8 @@ * opens sub-folders, makes a folder inline, deletes into the Trash without a * confirm, copies in one workspace and pastes into another (`fromLane`), * asks Replace / Keep both / Skip when an upload's name is taken, refuses a - * batch over 500 MB before sending anything, restores from the user's own + * batch over 500 MB before sending anything, leaves a repo's .git out of a + * folder upload with one notice, restores from the user's own * Trash, opens an admin's Trash on one folder per user (counted, an unreadable * one saying why), opens each user's Trash from its folder (asking before a * purge there, sending the item's store) and @@ -269,6 +270,23 @@ describe("FileExplorer", () => { expect(api.files.uploadBatch).not.toHaveBeenCalled(); }); + it("leaves a picked repo's .git out of a folder upload, with one notice", async () => { + vi.mocked(api.files.upload).mockResolvedValue({ path: "x" }); + open(); + await screen.findByText("a.txt"); + const mk = (rel: string) => { + const f = new File(["hi"], rel.split("/").pop()!); + Object.defineProperty(f, "webkitRelativePath", { value: rel }); + return f; + }; + const files = ["repo/.git/HEAD", "repo/.git/objects/d4/ab", "repo/src/a.js"].map(mk); + fireEvent.change(screen.getByTestId("fx-upload-folder"), { target: { files } }); + await waitFor(() => expect(api.files.upload).toHaveBeenCalledTimes(1)); + expect(vi.mocked(api.files.upload).mock.calls[0]![1]).toBe("repo/src/a.js"); + expect(toast.info).toHaveBeenCalledTimes(1); + expect(toast.error).not.toHaveBeenCalled(); + }); + it("refuses a batch over 500 MB before sending anything", async () => { open(); await screen.findByText("a.txt"); diff --git a/client/src/i18n/locales/en/lanes.json b/client/src/i18n/locales/en/lanes.json index 2a4ec824..963f12ee 100644 --- a/client/src/i18n/locales/en/lanes.json +++ b/client/src/i18n/locales/en/lanes.json @@ -590,6 +590,7 @@ "fx.dropHint": "Drag files or folders here to upload · drag onto a folder to move (Ctrl: copy)", "fx.uploading": "Uploading {{done}}/{{total}}…", "fx.uploaded": "Uploaded {{count}} file(s).", + "fx.gitSkipped": "Skipped {{count}} file(s) inside .git — nothing is uploaded into .git.", "fx.uploadFailed": "Could not upload {{name}}: {{message}}", "fx.tooBig": "{{size}} is more than one upload may carry ({{max}}). Upload fewer files at a time.", "fx.trashed": "Moved {{count}} item(s) to the Trash.", diff --git a/client/src/i18n/locales/vi/lanes.json b/client/src/i18n/locales/vi/lanes.json index 778cb74f..b17dfdf2 100644 --- a/client/src/i18n/locales/vi/lanes.json +++ b/client/src/i18n/locales/vi/lanes.json @@ -590,6 +590,7 @@ "fx.dropHint": "Kéo file hoặc thư mục vào đây để tải lên · kéo vào một thư mục để di chuyển (Ctrl: sao chép)", "fx.uploading": "Đang tải lên {{done}}/{{total}}…", "fx.uploaded": "Đã tải lên {{count}} file.", + "fx.gitSkipped": "Đã bỏ qua {{count}} file trong .git — không tải gì vào .git.", "fx.uploadFailed": "Không tải lên được {{name}}: {{message}}", "fx.tooBig": "{{size}} vượt quá giới hạn một lần tải lên ({{max}}). Hãy tải ít file hơn mỗi lần.", "fx.trashed": "Đã chuyển {{count}} mục vào Thùng rác.",