From a3ccc4ea26aa2d5a01f08bfbb5a8b3a2b9e79015 Mon Sep 17 00:00:00 2001 From: ARUNAVO RAY Date: Tue, 22 Sep 2026 23:05:36 +0530 Subject: [PATCH] feat(export): download repositories and organizations as CSV (#445) An admin who runs this alone wants to show a team what is being mirrored without handing out a login (#428). GET /api/repositories/export and GET /api/organizations/export return the tracked rows as a CSV file, and an Export CSV button on both pages downloads them. The file is the table without the internal fields: the ids, the metadata sync state and the mirror option overrides. Rows are scoped to the signed in user and ordered by full name and by name. No filters in this pass, the export is always the whole list. toCsv in src/lib/utils/csv.ts does the RFC 4180 work: a header row, CRLF line endings, quoting for commas, quotes and newlines, ISO dates, true and false for booleans, and empty for null. --- docs/API.md | 18 ++ src/components/organizations/Organization.tsx | 32 +- src/components/repositories/Repository.tsx | 31 +- src/lib/utils/csv.test.ts | 176 +++++++++++ src/lib/utils/csv.ts | 76 +++++ src/pages/api/organizations/export.test.ts | 223 ++++++++++++++ src/pages/api/organizations/export.ts | 59 ++++ src/pages/api/repositories/export.test.ts | 274 ++++++++++++++++++ src/pages/api/repositories/export.ts | 77 +++++ 9 files changed, 960 insertions(+), 6 deletions(-) create mode 100644 src/lib/utils/csv.test.ts create mode 100644 src/lib/utils/csv.ts create mode 100644 src/pages/api/organizations/export.test.ts create mode 100644 src/pages/api/organizations/export.ts create mode 100644 src/pages/api/repositories/export.test.ts create mode 100644 src/pages/api/repositories/export.ts diff --git a/docs/API.md b/docs/API.md index 7d4e03f..9f121f7 100644 --- a/docs/API.md +++ b/docs/API.md @@ -125,6 +125,24 @@ When several sources are connected, an optional `sourceId` picks which one the o `PATCH /api/organizations/:id` with `{ "sourceId": "…" }` re-pins an existing organization to another source, and `{ "sourceId": null }` clears the pin so the organization follows every source's repositories of the same name again. A pin whose source was removed falls back the same way. +### Export as CSV + +`GET /api/repositories/export` and `GET /api/organizations/export` return everything the account tracks as a CSV file, so the list can be shared with people who do not have a login here (issue #428). + +Both answer `text/csv; charset=utf-8` with a download name such as `gitea-mirror-repositories-2026-09-22.csv`, start with a header row, use CRLF line endings and RFC 4180 quoting, and write dates as ISO strings and booleans as `true` or `false`. Repositories are ordered by full name, organizations by name. The export has no filters: it is every row of the account. Internal fields are left out, that is the ids, the metadata sync state and the mirror option overrides. + +Repository columns: `name`, `fullName`, `url`, `cloneUrl`, `owner`, `organization`, `sourceProvider`, `sourceUrl`, `destinationProvider`, `destinationUrl`, `destinationOrg`, `mirroredLocation`, `visibility`, `isPrivate`, `isForked`, `forkedFrom`, `isStarred`, `isArchived`, `hasLFS`, `hasSubmodules`, `hasIssues`, `language`, `description`, `defaultBranch`, `size`, `status`, `lastMirrored`, `errorMessage`, `importedAt`, `createdAt`, `updatedAt`. + +Organization columns: `name`, `membershipRole`, `isIncluded`, `destinationOrg`, `status`, `repositoryCount`, `publicRepositoryCount`, `privateRepositoryCount`, `forkRepositoryCount`, `lastMirrored`, `errorMessage`, `createdAt`, `updatedAt`. + +```bash +curl -sS "https://mirror.example.com/api/repositories/export" \ + -H "x-api-key: gm_..." \ + -o repositories.csv +``` + +The **Export CSV** button on the Repositories and Organizations pages downloads the same files. + ### Reconcile the destination `POST /api/cleanup/reconcile` diff --git a/src/components/organizations/Organization.tsx b/src/components/organizations/Organization.tsx index 705be34..94a4336 100644 --- a/src/components/organizations/Organization.tsx +++ b/src/components/organizations/Organization.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; -import { Search, RefreshCw, FlipHorizontal, Filter, LoaderCircle, Trash2 } from "lucide-react"; +import { Search, RefreshCw, FlipHorizontal, Filter, LoaderCircle, Trash2, Download } from "lucide-react"; import type { MirrorJob, Organization } from "@/lib/db/schema"; import { OrganizationList } from "./OrganizationsList"; import AddOrganizationDialog from "./AddOrganizationDialog"; @@ -29,6 +29,7 @@ import { toast } from "sonner"; import { invalidateConfigCache, useConfigStatus } from "@/hooks/useConfigStatus"; import { useNavigation } from "@/components/layout/MainLayout"; import { useLiveRefresh } from "@/hooks/useLiveRefresh"; +import { withBase } from "@/lib/base-path"; import { Drawer, DrawerClose, @@ -568,7 +569,7 @@ export function Organization() {
{/* Mobile: Search bar with filter button */}
-
+
- + + + + {/* CSV export of every tracked organization */} + + - + + + + {/* CSV export of every tracked repository */} + + {/* Mirror All action */}