mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-08-18 15:29:53 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ea62a9f3d | |||
| 1f60b2cf39 | |||
| a02865a1aa | |||
| ad549dad9b | |||
| 20103220d9 | |||
| fe2c825244 | |||
| 4b858a0251 | |||
| 585d2ceb84 | |||
| 7c1f24dc2f | |||
| 680b374c84 |
@@ -51,13 +51,13 @@ jobs:
|
||||
ref: ${{ env.SHA }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
with:
|
||||
driver-opts: network=host
|
||||
|
||||
- name: Log into registry
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
# Login to Docker Hub for Docker Scout (optional - provides better vulnerability data)
|
||||
# Add DOCKERHUB_USERNAME and DOCKERHUB_TOKEN secrets to enable this
|
||||
- name: Log into Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
# Extract metadata for Docker
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE }}
|
||||
labels: |
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
# Build and push Docker image
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
# Load image locally for security scanning (PRs only)
|
||||
- name: Load image for scanning
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
@@ -212,7 +212,7 @@ jobs:
|
||||
|
||||
# Docker Scout comprehensive security analysis
|
||||
- name: Docker Scout - Vulnerability Analysis & Recommendations
|
||||
uses: docker/scout-action@v1
|
||||
uses: docker/scout-action@bacf462e8d090c09660de30a6ccc718035f961e3 # v1.20.4
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
command: cves,recommendations
|
||||
@@ -226,7 +226,7 @@ jobs:
|
||||
|
||||
# Docker Scout for Pull Requests (using local image)
|
||||
- name: Docker Scout - Vulnerability Analysis (PR)
|
||||
uses: docker/scout-action@v1
|
||||
uses: docker/scout-action@bacf462e8d090c09660de30a6ccc718035f961e3 # v1.20.4
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
command: cves,recommendations
|
||||
@@ -240,7 +240,7 @@ jobs:
|
||||
|
||||
# Compare to latest for PRs and pushes
|
||||
- name: Docker Scout - Compare to Latest
|
||||
uses: docker/scout-action@v1
|
||||
uses: docker/scout-action@bacf462e8d090c09660de30a6ccc718035f961e3 # v1.20.4
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
command: compare
|
||||
|
||||
@@ -38,10 +38,10 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@main
|
||||
uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22
|
||||
|
||||
- name: Setup Nix Cache
|
||||
uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
uses: DeterminateSystems/magic-nix-cache-action@565684385bcd71bad329742eefe8d12f2e765b39 # v13
|
||||
|
||||
- name: Regenerate bun.nix from bun.lock
|
||||
run: nix run --accept-flake-config github:nix-community/bun2nix -- -o bun.nix
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1.4
|
||||
|
||||
FROM oven/bun:1.3.13-debian AS base
|
||||
FROM oven/bun:1.3.14-debian@sha256:9dba1a1b43ce28c9d7931bfc4eb00feb63b0114720a0277a8f939ae4dfc9db6f AS base
|
||||
WORKDIR /app
|
||||
RUN apt-get update && apt-get -y upgrade && apt-get install -y --no-install-recommends \
|
||||
python3 make g++ gcc wget sqlite3 openssl ca-certificates \
|
||||
@@ -49,7 +49,7 @@ RUN git clone --branch "v${GIT_LFS_VERSION}" --depth 1 https://github.com/git-lf
|
||||
&& install -m 755 /tmp/git-lfs/bin/git-lfs /usr/local/bin/git-lfs
|
||||
|
||||
# ----------------------------
|
||||
FROM oven/bun:1.3.13-debian AS runner
|
||||
FROM oven/bun:1.3.14-debian@sha256:9dba1a1b43ce28c9d7931bfc4eb00feb63b0114720a0277a8f939ae4dfc9db6f AS runner
|
||||
WORKDIR /app
|
||||
RUN apt-get update && apt-get -y upgrade && apt-get install -y --no-install-recommends \
|
||||
git wget sqlite3 openssl ca-certificates \
|
||||
|
||||
@@ -82,16 +82,13 @@
|
||||
},
|
||||
"overrides": {
|
||||
"@esbuild-kit/esm-loader": "npm:tsx@^4.21.0",
|
||||
"@xmldom/xmldom": "^0.8.12",
|
||||
"defu": "^6.1.7",
|
||||
"devalue": "^5.6.4",
|
||||
"fast-xml-parser": "^5.5.6",
|
||||
"kysely": "^0.28.16",
|
||||
"@xmldom/xmldom": "^0.8.13",
|
||||
"devalue": "^5.8.1",
|
||||
"fast-uri": "^3.1.2",
|
||||
"fast-xml-builder": "^1.1.7",
|
||||
"kysely": "^0.28.17",
|
||||
"lodash": "^4.18.1",
|
||||
"node-forge": "^1.3.3",
|
||||
"picomatch": "^4.0.4",
|
||||
"rollup": ">=4.59.0",
|
||||
"svgo": "^4.0.1",
|
||||
},
|
||||
"packages": {
|
||||
"@acemir/cssom": ["@acemir/cssom@0.9.31", "", {}, "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA=="],
|
||||
@@ -726,7 +723,7 @@
|
||||
|
||||
"@xmldom/is-dom-node": ["@xmldom/is-dom-node@1.0.1", "", {}, "sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q=="],
|
||||
|
||||
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.12", "", {}, "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg=="],
|
||||
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
@@ -876,7 +873,7 @@
|
||||
|
||||
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||
|
||||
"devalue": ["devalue@5.6.4", "", {}, "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA=="],
|
||||
"devalue": ["devalue@5.8.1", "", {}, "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw=="],
|
||||
|
||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||
|
||||
@@ -960,9 +957,9 @@
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
"fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
|
||||
|
||||
"fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="],
|
||||
"fast-xml-builder": ["fast-xml-builder@1.2.0", "", { "dependencies": { "path-expression-matcher": "^1.5.0", "xml-naming": "^0.1.0" } }, "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q=="],
|
||||
|
||||
"fast-xml-parser": ["fast-xml-parser@5.5.6", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.1.3", "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-3+fdZyBRVg29n4rXP0joHthhcHdPUHaIC16cuyyd1iLsuaO6Vea36MPrxgAzbZna8lhvZeRL8Bc9GP56/J9xEw=="],
|
||||
|
||||
@@ -1090,7 +1087,7 @@
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"kysely": ["kysely@0.28.16", "", {}, "sha512-3i5pmOiZvMDj00qhrIVbH0AnioVTx22DMP7Vn5At4yJO46iy+FM8Y/g61ltenLVSo3fiO8h8Q3QOFgf/gQ72ww=="],
|
||||
"kysely": ["kysely@0.28.17", "", {}, "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
@@ -1662,6 +1659,8 @@
|
||||
|
||||
"xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
|
||||
|
||||
"xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="],
|
||||
|
||||
"xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
|
||||
|
||||
"xpath": ["xpath@0.0.34", "", {}, "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA=="],
|
||||
@@ -1760,6 +1759,8 @@
|
||||
|
||||
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"fast-xml-builder/path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="],
|
||||
|
||||
"hast-util-from-html/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||
|
||||
"hast-util-raw/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||
|
||||
+7
-10
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "gitea-mirror",
|
||||
"type": "module",
|
||||
"version": "3.15.9",
|
||||
"version": "3.15.12",
|
||||
"engines": {
|
||||
"bun": ">=1.2.9"
|
||||
},
|
||||
@@ -45,16 +45,13 @@
|
||||
},
|
||||
"overrides": {
|
||||
"@esbuild-kit/esm-loader": "npm:tsx@^4.21.0",
|
||||
"@xmldom/xmldom": "^0.8.12",
|
||||
"defu": "^6.1.7",
|
||||
"devalue": "^5.6.4",
|
||||
"fast-xml-parser": "^5.5.6",
|
||||
"kysely": "^0.28.16",
|
||||
"@xmldom/xmldom": "^0.8.13",
|
||||
"devalue": "^5.8.1",
|
||||
"fast-uri": "^3.1.2",
|
||||
"fast-xml-builder": "^1.1.7",
|
||||
"kysely": "^0.28.17",
|
||||
"lodash": "^4.18.1",
|
||||
"node-forge": "^1.3.3",
|
||||
"picomatch": "^4.0.4",
|
||||
"rollup": ">=4.59.0",
|
||||
"svgo": "^4.0.1"
|
||||
"picomatch": "^4.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.7",
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { KeyRound, LogOut, Mail } from "lucide-react";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { withBase } from "@/lib/base-path";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { ChangePasswordDialog } from "./ChangePasswordDialog";
|
||||
import { ChangeEmailDialog } from "./ChangeEmailDialog";
|
||||
|
||||
export function AccountMenu() {
|
||||
const { user, logout, refreshUser } = useAuth();
|
||||
const [hasPassword, setHasPassword] = useState<boolean | null>(null);
|
||||
const [passwordOpen, setPasswordOpen] = useState(false);
|
||||
const [emailOpen, setEmailOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
setHasPassword(null);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const accounts = await authClient.listAccounts();
|
||||
if (cancelled) return;
|
||||
const list = Array.isArray(accounts) ? accounts : accounts?.data;
|
||||
setHasPassword(
|
||||
Array.isArray(list) && list.some((a) => a.providerId === "credential")
|
||||
);
|
||||
} catch {
|
||||
// Fail open: if we can't check, show the option rather than locking the
|
||||
// user out of changing their password.
|
||||
if (!cancelled) setHasPassword(true);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [user?.id]);
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<a href={withBase("/login")}>Login</a>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
toast.success("Logged out successfully");
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
logout();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="relative h-10 w-10 rounded-full p-0"
|
||||
>
|
||||
<Avatar className="h-full w-full">
|
||||
<AvatarImage src={user.image || ""} alt={user.name || user.email} />
|
||||
<AvatarFallback>
|
||||
{(user.name || user.email || "U").charAt(0).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-60">
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{user.name && (
|
||||
<span className="text-sm font-medium leading-none">
|
||||
{user.name}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs leading-none text-muted-foreground truncate">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{hasPassword && (
|
||||
<DropdownMenuItem
|
||||
onSelect={() => setPasswordOpen(true)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<KeyRound className="h-4 w-4 mr-2" />
|
||||
Change password
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onSelect={() => setEmailOpen(true)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Mail className="h-4 w-4 mr-2" />
|
||||
Change email
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onSelect={handleLogout} className="cursor-pointer">
|
||||
<LogOut className="h-4 w-4 mr-2" />
|
||||
Logout
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{hasPassword && (
|
||||
<ChangePasswordDialog
|
||||
open={passwordOpen}
|
||||
onOpenChange={setPasswordOpen}
|
||||
/>
|
||||
)}
|
||||
<ChangeEmailDialog
|
||||
open={emailOpen}
|
||||
onOpenChange={setEmailOpen}
|
||||
currentEmail={user.email}
|
||||
onUpdated={refreshUser}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
interface ChangeEmailDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
currentEmail: string;
|
||||
onUpdated?: () => void;
|
||||
}
|
||||
|
||||
export function ChangeEmailDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
currentEmail,
|
||||
onUpdated,
|
||||
}: ChangeEmailDialogProps) {
|
||||
const [newEmail, setNewEmail] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const reset = () => {
|
||||
setNewEmail("");
|
||||
setIsSubmitting(false);
|
||||
};
|
||||
|
||||
const handleOpenChange = (next: boolean) => {
|
||||
if (!next) reset();
|
||||
onOpenChange(next);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const trimmed = newEmail.trim();
|
||||
if (!trimmed) {
|
||||
toast.error("Please enter a new email");
|
||||
return;
|
||||
}
|
||||
if (trimmed.toLowerCase() === currentEmail.toLowerCase()) {
|
||||
toast.error("New email must differ from current email");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const { error } = await authClient.changeEmail({ newEmail: trimmed });
|
||||
if (error) {
|
||||
toast.error(error.message || "Failed to change email");
|
||||
return;
|
||||
}
|
||||
toast.success("Email updated.");
|
||||
onUpdated?.();
|
||||
handleOpenChange(false);
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "Failed to change email");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Change email</DialogTitle>
|
||||
<DialogDescription>
|
||||
Current: <span className="font-medium">{currentEmail}</span>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="new-email">New email</Label>
|
||||
<Input
|
||||
id="new-email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
value={newEmail}
|
||||
onChange={(e) => setNewEmail(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => handleOpenChange(false)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? "Updating..." : "Update email"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
interface ChangePasswordDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function ChangePasswordDialog({ open, onOpenChange }: ChangePasswordDialogProps) {
|
||||
const [currentPassword, setCurrentPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [revokeOtherSessions, setRevokeOtherSessions] = useState(true);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const reset = () => {
|
||||
setCurrentPassword("");
|
||||
setNewPassword("");
|
||||
setConfirmPassword("");
|
||||
setRevokeOtherSessions(true);
|
||||
setIsSubmitting(false);
|
||||
};
|
||||
|
||||
const handleOpenChange = (next: boolean) => {
|
||||
if (!next) reset();
|
||||
onOpenChange(next);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!currentPassword || !newPassword) {
|
||||
toast.error("Please fill in all fields");
|
||||
return;
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
toast.error("New passwords do not match");
|
||||
return;
|
||||
}
|
||||
if (newPassword === currentPassword) {
|
||||
toast.error("New password must differ from current password");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const { error } = await authClient.changePassword({
|
||||
currentPassword,
|
||||
newPassword,
|
||||
revokeOtherSessions,
|
||||
});
|
||||
if (error) {
|
||||
toast.error(error.message || "Failed to change password");
|
||||
return;
|
||||
}
|
||||
toast.success(
|
||||
revokeOtherSessions
|
||||
? "Password updated. Other sessions signed out."
|
||||
: "Password updated."
|
||||
);
|
||||
handleOpenChange(false);
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "Failed to change password");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Change password</DialogTitle>
|
||||
<DialogDescription>
|
||||
Enter your current password and a new one. You'll stay signed in on this device.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="current-password">Current password</Label>
|
||||
<Input
|
||||
id="current-password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="new-password">New password</Label>
|
||||
<Input
|
||||
id="new-password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
required
|
||||
minLength={8}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirm-password">Confirm new password</Label>
|
||||
<Input
|
||||
id="confirm-password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
required
|
||||
minLength={8}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="revoke-sessions"
|
||||
checked={revokeOtherSessions}
|
||||
onCheckedChange={(checked) => setRevokeOtherSessions(checked === true)}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<Label htmlFor="revoke-sessions" className="text-sm font-normal cursor-pointer">
|
||||
Sign out other devices
|
||||
</Label>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => handleOpenChange(false)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? "Updating..." : "Update password"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,11 @@ import { useAuth } from "@/hooks/useAuth";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import { ModeToggle } from "@/components/theme/ModeToggle";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||
import { toast } from "sonner";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useLiveRefresh } from "@/hooks/useLiveRefresh";
|
||||
import { useConfigStatus } from "@/hooks/useConfigStatus";
|
||||
import { Menu, LogOut, PanelRightOpen, PanelRightClose } from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { PanelRightOpen, PanelRightClose } from "lucide-react";
|
||||
import { AccountMenu } from "@/components/auth/AccountMenu";
|
||||
import { withBase } from "@/lib/base-path";
|
||||
|
||||
interface HeaderProps {
|
||||
@@ -26,7 +19,7 @@ interface HeaderProps {
|
||||
}
|
||||
|
||||
export function Header({ currentPage, onNavigate, onMenuClick, onToggleCollapse, isSidebarCollapsed, isSidebarOpen }: HeaderProps) {
|
||||
const { user, logout, isLoading } = useAuth();
|
||||
const { isLoading } = useAuth();
|
||||
const { isLiveEnabled, toggleLive } = useLiveRefresh();
|
||||
const { isFullyConfigured, isLoading: configLoading } = useConfigStatus();
|
||||
|
||||
@@ -47,13 +40,6 @@ export function Header({ currentPage, onNavigate, onMenuClick, onToggleCollapse,
|
||||
return isLiveEnabled ? 'Disable live refresh' : 'Enable live refresh';
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
toast.success("Logged out successfully");
|
||||
// Small delay to show the toast before redirecting
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
logout();
|
||||
};
|
||||
|
||||
// Auth buttons skeleton loader
|
||||
function AuthButtonsSkeleton() {
|
||||
return (
|
||||
@@ -141,32 +127,7 @@ export function Header({ currentPage, onNavigate, onMenuClick, onToggleCollapse,
|
||||
|
||||
<ModeToggle />
|
||||
|
||||
{isLoading ? (
|
||||
<AuthButtonsSkeleton />
|
||||
) : user ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="lg" className="relative h-10 w-10 rounded-full p-0">
|
||||
<Avatar className="h-full w-full">
|
||||
<AvatarImage src={user.image || ""} alt={user.name || user.email} />
|
||||
<AvatarFallback>
|
||||
{(user.name || user.email || "U").charAt(0).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuItem onClick={handleLogout} className="cursor-pointer">
|
||||
<LogOut className="h-4 w-4 mr-2" />
|
||||
Logout
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<a href={withBase('/login')}>Login</a>
|
||||
</Button>
|
||||
)}
|
||||
{isLoading ? <AuthButtonsSkeleton /> : <AccountMenu />}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -145,6 +145,12 @@ export const auth = betterAuth({
|
||||
input: false, // Don't show in signup form - we'll derive from email
|
||||
}
|
||||
},
|
||||
changeEmail: {
|
||||
enabled: true,
|
||||
// Email verification isn't wired up (sendResetPassword is a TODO),
|
||||
// so allow direct updates. Safe here because emails stay unverified.
|
||||
updateEmailWithoutVerification: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Plugins configuration
|
||||
|
||||
@@ -848,12 +848,15 @@ describe("Enhanced Gitea Operations", () => {
|
||||
}
|
||||
);
|
||||
|
||||
// All metadata components were previously synced, so none should be called again
|
||||
// Metadata reconciliation now runs on every sync (mirror* functions
|
||||
// are idempotent and PATCH existing entries by marker/name/title).
|
||||
// Releases are still skipped here because the flag is off in this config.
|
||||
// Labels are still skipped because the issues path also handles labels.
|
||||
expect(mockMirrorGitHubReleasesToGitea).not.toHaveBeenCalled();
|
||||
expect(mockMirrorGitRepoIssuesToGitea).not.toHaveBeenCalled();
|
||||
expect(mockMirrorGitRepoPullRequestsToGitea).not.toHaveBeenCalled();
|
||||
expect(mockMirrorGitRepoIssuesToGitea).toHaveBeenCalledTimes(1);
|
||||
expect(mockMirrorGitRepoPullRequestsToGitea).toHaveBeenCalledTimes(1);
|
||||
expect(mockMirrorGitRepoLabelsToGitea).not.toHaveBeenCalled();
|
||||
expect(mockMirrorGitRepoMilestonesToGitea).not.toHaveBeenCalled();
|
||||
expect(mockMirrorGitRepoMilestonesToGitea).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -601,25 +601,23 @@ export async function syncGiteaRepoEnhanced({
|
||||
return metadataOctokit;
|
||||
};
|
||||
|
||||
// Reconcile metadata on every sync (matches the release path).
|
||||
// The underlying mirror* functions are idempotent: issues/PRs are
|
||||
// matched via [GH-ISSUE #N] / [GH-PR #N] markers and PATCHed in
|
||||
// place, labels are deduped by name, milestones by title.
|
||||
const shouldMirrorReleases =
|
||||
!!config.giteaConfig?.mirrorReleases && !skipMetadataForStarred;
|
||||
const shouldMirrorIssuesThisRun =
|
||||
!!config.giteaConfig?.mirrorIssues &&
|
||||
!skipMetadataForStarred &&
|
||||
!metadataState.components.issues;
|
||||
!!config.giteaConfig?.mirrorIssues && !skipMetadataForStarred;
|
||||
const shouldMirrorPullRequests =
|
||||
!!config.giteaConfig?.mirrorPullRequests &&
|
||||
!skipMetadataForStarred &&
|
||||
!metadataState.components.pullRequests;
|
||||
!!config.giteaConfig?.mirrorPullRequests && !skipMetadataForStarred;
|
||||
// Labels-only path; issues run already creates/reconciles labels.
|
||||
const shouldMirrorLabels =
|
||||
!!config.giteaConfig?.mirrorLabels &&
|
||||
!skipMetadataForStarred &&
|
||||
!shouldMirrorIssuesThisRun &&
|
||||
!metadataState.components.labels;
|
||||
!shouldMirrorIssuesThisRun;
|
||||
const shouldMirrorMilestones =
|
||||
!!config.giteaConfig?.mirrorMilestones &&
|
||||
!skipMetadataForStarred &&
|
||||
!metadataState.components.milestones;
|
||||
!!config.giteaConfig?.mirrorMilestones && !skipMetadataForStarred;
|
||||
|
||||
if (shouldMirrorReleases) {
|
||||
const octokit = ensureOctokit();
|
||||
@@ -684,13 +682,6 @@ export async function syncGiteaRepoEnhanced({
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
config.giteaConfig?.mirrorIssues &&
|
||||
metadataState.components.issues
|
||||
) {
|
||||
console.log(
|
||||
`[Sync] Issues already mirrored for ${repository.name}; skipping to avoid duplicates`
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldMirrorPullRequests) {
|
||||
@@ -721,13 +712,6 @@ export async function syncGiteaRepoEnhanced({
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
config.giteaConfig?.mirrorPullRequests &&
|
||||
metadataState.components.pullRequests
|
||||
) {
|
||||
console.log(
|
||||
`[Sync] Pull requests already mirrored for ${repository.name}; skipping`
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldMirrorLabels) {
|
||||
@@ -760,13 +744,6 @@ export async function syncGiteaRepoEnhanced({
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
config.giteaConfig?.mirrorLabels &&
|
||||
metadataState.components.labels
|
||||
) {
|
||||
console.log(
|
||||
`[Sync] Labels already mirrored for ${repository.name}; skipping`
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldMirrorMilestones) {
|
||||
@@ -799,13 +776,6 @@ export async function syncGiteaRepoEnhanced({
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
config.giteaConfig?.mirrorMilestones &&
|
||||
metadataState.components.milestones
|
||||
) {
|
||||
console.log(
|
||||
`[Sync] Milestones already mirrored for ${repository.name}; skipping`
|
||||
);
|
||||
}
|
||||
|
||||
if (metadataUpdated) {
|
||||
|
||||
+24
-81
@@ -898,14 +898,15 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
}
|
||||
}
|
||||
|
||||
// Determine metadata operations to avoid duplicates
|
||||
// Reconcile metadata on every sync (matches the release path above).
|
||||
// The underlying mirror* functions are idempotent: issues/PRs are
|
||||
// matched via [GH-ISSUE #N] / [GH-PR #N] markers and PATCHed in place,
|
||||
// labels are deduped by name, milestones by title.
|
||||
const shouldMirrorIssuesThisRun =
|
||||
!!config.giteaConfig?.mirrorIssues &&
|
||||
!skipMetadataForStarred &&
|
||||
!metadataState.components.issues;
|
||||
!!config.giteaConfig?.mirrorIssues && !skipMetadataForStarred;
|
||||
|
||||
console.log(
|
||||
`[Metadata] Issue mirroring check: mirrorIssues=${config.giteaConfig?.mirrorIssues}, alreadyMirrored=${metadataState.components.issues}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorIssues=${shouldMirrorIssuesThisRun}`
|
||||
`[Metadata] Issue mirroring check: mirrorIssues=${config.giteaConfig?.mirrorIssues}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorIssues=${shouldMirrorIssuesThisRun}`
|
||||
);
|
||||
|
||||
if (shouldMirrorIssuesThisRun) {
|
||||
@@ -931,19 +932,13 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
);
|
||||
// Continue with other metadata operations even if issues fail
|
||||
}
|
||||
} else if (config.giteaConfig?.mirrorIssues && metadataState.components.issues) {
|
||||
console.log(
|
||||
`[Metadata] Issues already mirrored for ${repository.name}; skipping to avoid duplicates`
|
||||
);
|
||||
}
|
||||
|
||||
const shouldMirrorPullRequests =
|
||||
!!config.giteaConfig?.mirrorPullRequests &&
|
||||
!skipMetadataForStarred &&
|
||||
!metadataState.components.pullRequests;
|
||||
!!config.giteaConfig?.mirrorPullRequests && !skipMetadataForStarred;
|
||||
|
||||
console.log(
|
||||
`[Metadata] Pull request mirroring check: mirrorPullRequests=${config.giteaConfig?.mirrorPullRequests}, alreadyMirrored=${metadataState.components.pullRequests}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorPullRequests=${shouldMirrorPullRequests}`
|
||||
`[Metadata] Pull request mirroring check: mirrorPullRequests=${config.giteaConfig?.mirrorPullRequests}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorPullRequests=${shouldMirrorPullRequests}`
|
||||
);
|
||||
|
||||
if (shouldMirrorPullRequests) {
|
||||
@@ -968,23 +963,16 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
);
|
||||
// Continue with other metadata operations even if PRs fail
|
||||
}
|
||||
} else if (
|
||||
config.giteaConfig?.mirrorPullRequests &&
|
||||
metadataState.components.pullRequests
|
||||
) {
|
||||
console.log(
|
||||
`[Metadata] Pull requests already mirrored for ${repository.name}; skipping`
|
||||
);
|
||||
}
|
||||
|
||||
// Labels-only path; issues run above already creates/reconciles labels.
|
||||
const shouldMirrorLabels =
|
||||
!!config.giteaConfig?.mirrorLabels &&
|
||||
!skipMetadataForStarred &&
|
||||
!shouldMirrorIssuesThisRun &&
|
||||
!metadataState.components.labels;
|
||||
!shouldMirrorIssuesThisRun;
|
||||
|
||||
console.log(
|
||||
`[Metadata] Label mirroring check: mirrorLabels=${config.giteaConfig?.mirrorLabels}, alreadyMirrored=${metadataState.components.labels}, issuesRunning=${shouldMirrorIssuesThisRun}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorLabels=${shouldMirrorLabels}`
|
||||
`[Metadata] Label mirroring check: mirrorLabels=${config.giteaConfig?.mirrorLabels}, issuesRunning=${shouldMirrorIssuesThisRun}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorLabels=${shouldMirrorLabels}`
|
||||
);
|
||||
|
||||
if (shouldMirrorLabels) {
|
||||
@@ -1009,19 +997,13 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
);
|
||||
// Continue with other metadata operations even if labels fail
|
||||
}
|
||||
} else if (config.giteaConfig?.mirrorLabels && metadataState.components.labels) {
|
||||
console.log(
|
||||
`[Metadata] Labels already mirrored for ${repository.name}; skipping`
|
||||
);
|
||||
}
|
||||
|
||||
const shouldMirrorMilestones =
|
||||
!!config.giteaConfig?.mirrorMilestones &&
|
||||
!skipMetadataForStarred &&
|
||||
!metadataState.components.milestones;
|
||||
!!config.giteaConfig?.mirrorMilestones && !skipMetadataForStarred;
|
||||
|
||||
console.log(
|
||||
`[Metadata] Milestone mirroring check: mirrorMilestones=${config.giteaConfig?.mirrorMilestones}, alreadyMirrored=${metadataState.components.milestones}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorMilestones=${shouldMirrorMilestones}`
|
||||
`[Metadata] Milestone mirroring check: mirrorMilestones=${config.giteaConfig?.mirrorMilestones}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorMilestones=${shouldMirrorMilestones}`
|
||||
);
|
||||
|
||||
if (shouldMirrorMilestones) {
|
||||
@@ -1046,13 +1028,6 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
);
|
||||
// Continue with other metadata operations even if milestones fail
|
||||
}
|
||||
} else if (
|
||||
config.giteaConfig?.mirrorMilestones &&
|
||||
metadataState.components.milestones
|
||||
) {
|
||||
console.log(
|
||||
`[Metadata] Milestones already mirrored for ${repository.name}; skipping`
|
||||
);
|
||||
}
|
||||
|
||||
if (metadataUpdated) {
|
||||
@@ -1587,13 +1562,13 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
}
|
||||
}
|
||||
|
||||
// Reconcile metadata on every sync. See note in mirrorGithubRepoToGitea
|
||||
// above. The underlying mirror* functions are idempotent.
|
||||
const shouldMirrorIssuesThisRun =
|
||||
!!config.giteaConfig?.mirrorIssues &&
|
||||
!skipMetadataForStarred &&
|
||||
!metadataState.components.issues;
|
||||
!!config.giteaConfig?.mirrorIssues && !skipMetadataForStarred;
|
||||
|
||||
console.log(
|
||||
`[Metadata] Issue mirroring check: mirrorIssues=${config.giteaConfig?.mirrorIssues}, alreadyMirrored=${metadataState.components.issues}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorIssues=${shouldMirrorIssuesThisRun}`
|
||||
`[Metadata] Issue mirroring check: mirrorIssues=${config.giteaConfig?.mirrorIssues}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorIssues=${shouldMirrorIssuesThisRun}`
|
||||
);
|
||||
|
||||
if (shouldMirrorIssuesThisRun) {
|
||||
@@ -1619,22 +1594,13 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
);
|
||||
// Continue with other metadata operations even if issues fail
|
||||
}
|
||||
} else if (
|
||||
config.giteaConfig?.mirrorIssues &&
|
||||
metadataState.components.issues
|
||||
) {
|
||||
console.log(
|
||||
`[Metadata] Issues already mirrored for ${repository.name}; skipping`
|
||||
);
|
||||
}
|
||||
|
||||
const shouldMirrorPullRequests =
|
||||
!!config.giteaConfig?.mirrorPullRequests &&
|
||||
!skipMetadataForStarred &&
|
||||
!metadataState.components.pullRequests;
|
||||
!!config.giteaConfig?.mirrorPullRequests && !skipMetadataForStarred;
|
||||
|
||||
console.log(
|
||||
`[Metadata] Pull request mirroring check: mirrorPullRequests=${config.giteaConfig?.mirrorPullRequests}, alreadyMirrored=${metadataState.components.pullRequests}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorPullRequests=${shouldMirrorPullRequests}`
|
||||
`[Metadata] Pull request mirroring check: mirrorPullRequests=${config.giteaConfig?.mirrorPullRequests}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorPullRequests=${shouldMirrorPullRequests}`
|
||||
);
|
||||
|
||||
if (shouldMirrorPullRequests) {
|
||||
@@ -1659,23 +1625,16 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
);
|
||||
// Continue with other metadata operations even if PRs fail
|
||||
}
|
||||
} else if (
|
||||
config.giteaConfig?.mirrorPullRequests &&
|
||||
metadataState.components.pullRequests
|
||||
) {
|
||||
console.log(
|
||||
`[Metadata] Pull requests already mirrored for ${repository.name}; skipping`
|
||||
);
|
||||
}
|
||||
|
||||
// Labels-only path; issues run above already creates/reconciles labels.
|
||||
const shouldMirrorLabels =
|
||||
!!config.giteaConfig?.mirrorLabels &&
|
||||
!skipMetadataForStarred &&
|
||||
!shouldMirrorIssuesThisRun &&
|
||||
!metadataState.components.labels;
|
||||
!shouldMirrorIssuesThisRun;
|
||||
|
||||
console.log(
|
||||
`[Metadata] Label mirroring check: mirrorLabels=${config.giteaConfig?.mirrorLabels}, alreadyMirrored=${metadataState.components.labels}, issuesRunning=${shouldMirrorIssuesThisRun}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorLabels=${shouldMirrorLabels}`
|
||||
`[Metadata] Label mirroring check: mirrorLabels=${config.giteaConfig?.mirrorLabels}, issuesRunning=${shouldMirrorIssuesThisRun}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorLabels=${shouldMirrorLabels}`
|
||||
);
|
||||
|
||||
if (shouldMirrorLabels) {
|
||||
@@ -1700,22 +1659,13 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
);
|
||||
// Continue with other metadata operations even if labels fail
|
||||
}
|
||||
} else if (
|
||||
config.giteaConfig?.mirrorLabels &&
|
||||
metadataState.components.labels
|
||||
) {
|
||||
console.log(
|
||||
`[Metadata] Labels already mirrored for ${repository.name}; skipping`
|
||||
);
|
||||
}
|
||||
|
||||
const shouldMirrorMilestones =
|
||||
!!config.giteaConfig?.mirrorMilestones &&
|
||||
!skipMetadataForStarred &&
|
||||
!metadataState.components.milestones;
|
||||
!!config.giteaConfig?.mirrorMilestones && !skipMetadataForStarred;
|
||||
|
||||
console.log(
|
||||
`[Metadata] Milestone mirroring check: mirrorMilestones=${config.giteaConfig?.mirrorMilestones}, alreadyMirrored=${metadataState.components.milestones}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorMilestones=${shouldMirrorMilestones}`
|
||||
`[Metadata] Milestone mirroring check: mirrorMilestones=${config.giteaConfig?.mirrorMilestones}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorMilestones=${shouldMirrorMilestones}`
|
||||
);
|
||||
|
||||
if (shouldMirrorMilestones) {
|
||||
@@ -1740,13 +1690,6 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
);
|
||||
// Continue with other metadata operations even if milestones fail
|
||||
}
|
||||
} else if (
|
||||
config.giteaConfig?.mirrorMilestones &&
|
||||
metadataState.components.milestones
|
||||
) {
|
||||
console.log(
|
||||
`[Metadata] Milestones already mirrored for ${repository.name}; skipping`
|
||||
);
|
||||
}
|
||||
|
||||
if (metadataUpdated) {
|
||||
|
||||
@@ -39,37 +39,62 @@ function makeOctokit() {
|
||||
}
|
||||
|
||||
describe("getGithubRepositories - affiliation", () => {
|
||||
test("defaults to owner+collaborator when field is unset (backward compat)", async () => {
|
||||
test("defaults to owner+collaborator+organization_member when field is unset (backward compat)", async () => {
|
||||
const { octokit, getCaptured } = makeOctokit();
|
||||
await getGithubRepositories({ octokit, config: { githubConfig: { owner: "octo" } as any } });
|
||||
expect(getCaptured()?.affiliation).toBe("owner,collaborator");
|
||||
expect(getCaptured()?.affiliation).toBe("owner,collaborator,organization_member");
|
||||
});
|
||||
|
||||
test("uses owner only when includeCollaboratorRepos is false", async () => {
|
||||
test("uses owner+organization_member when includeCollaboratorRepos is false", async () => {
|
||||
const { octokit, getCaptured } = makeOctokit();
|
||||
await getGithubRepositories({
|
||||
octokit,
|
||||
config: { githubConfig: { owner: "octo", includeCollaboratorRepos: false } as any },
|
||||
});
|
||||
expect(getCaptured()?.affiliation).toBe("owner");
|
||||
expect(getCaptured()?.affiliation).toBe("owner,organization_member");
|
||||
});
|
||||
|
||||
test("uses owner+collaborator when includeCollaboratorRepos is true", async () => {
|
||||
test("uses owner+collaborator+organization_member when includeCollaboratorRepos is true", async () => {
|
||||
const { octokit, getCaptured } = makeOctokit();
|
||||
await getGithubRepositories({
|
||||
octokit,
|
||||
config: { githubConfig: { owner: "octo", includeCollaboratorRepos: true } as any },
|
||||
});
|
||||
expect(getCaptured()?.affiliation).toBe("owner,collaborator");
|
||||
expect(getCaptured()?.affiliation).toBe("owner,collaborator,organization_member");
|
||||
});
|
||||
|
||||
test("override forces owner+collaborator regardless of config (used by cleanup)", async () => {
|
||||
test("override forces owner+collaborator+organization_member regardless of config (used by cleanup)", async () => {
|
||||
const { octokit, getCaptured } = makeOctokit();
|
||||
await getGithubRepositories({
|
||||
octokit,
|
||||
config: { githubConfig: { owner: "octo", includeCollaboratorRepos: false } as any },
|
||||
includeCollaboratorReposOverride: true,
|
||||
});
|
||||
expect(getCaptured()?.affiliation).toBe("owner,collaborator");
|
||||
expect(getCaptured()?.affiliation).toBe("owner,collaborator,organization_member");
|
||||
});
|
||||
|
||||
test("always includes organization_member (regression guard for org-repo invisibility)", async () => {
|
||||
const cases: Array<{ includeCollab?: boolean; override?: boolean }> = [
|
||||
{},
|
||||
{ includeCollab: true },
|
||||
{ includeCollab: false },
|
||||
{ override: true },
|
||||
{ includeCollab: false, override: true },
|
||||
];
|
||||
for (const c of cases) {
|
||||
const { octokit, getCaptured } = makeOctokit();
|
||||
await getGithubRepositories({
|
||||
octokit,
|
||||
config: {
|
||||
githubConfig: {
|
||||
owner: "octo",
|
||||
...(c.includeCollab !== undefined && { includeCollaboratorRepos: c.includeCollab }),
|
||||
} as any,
|
||||
},
|
||||
...(c.override !== undefined && { includeCollaboratorReposOverride: c.override }),
|
||||
});
|
||||
const aff = String(getCaptured()?.affiliation ?? "");
|
||||
expect(aff.split(",")).toContain("organization_member");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+7
-1
@@ -249,7 +249,13 @@ export async function getGithubRepositories({
|
||||
includeCollaboratorReposOverride ??
|
||||
config.githubConfig?.includeCollaboratorRepos ??
|
||||
true;
|
||||
const affiliation = includeCollab ? "owner,collaborator" : "owner";
|
||||
// Always include organization_member so repos owned by orgs the user
|
||||
// belongs to are returned. Omitting it caused org repos to be invisible
|
||||
// to the main sync, the scheduler, and the cleanup service (which then
|
||||
// archived them on restart as if they had been deleted on GitHub).
|
||||
const affiliation = includeCollab
|
||||
? "owner,collaborator,organization_member"
|
||||
: "owner,organization_member";
|
||||
|
||||
const repos = await octokit.paginate(
|
||||
octokit.repos.listForAuthenticatedUser,
|
||||
|
||||
Reference in New Issue
Block a user