feat: git test connection and pull
This commit is contained in:
parent
7e66742269
commit
ee6cfe9531
|
|
@ -16,6 +16,8 @@ from app.schemas.common import OkResponse
|
||||||
from app.schemas.forgejo_connections import (
|
from app.schemas.forgejo_connections import (
|
||||||
ForgejoConnectionCreate,
|
ForgejoConnectionCreate,
|
||||||
ForgejoConnectionRead,
|
ForgejoConnectionRead,
|
||||||
|
ForgejoConnectionTestRequest,
|
||||||
|
ForgejoConnectionTestResponse,
|
||||||
ForgejoConnectionUpdate,
|
ForgejoConnectionUpdate,
|
||||||
)
|
)
|
||||||
from app.schemas.forgejo_validation import ForgejoConnectionValidationResponse
|
from app.schemas.forgejo_validation import ForgejoConnectionValidationResponse
|
||||||
|
|
@ -172,6 +174,62 @@ async def delete_connection(
|
||||||
return OkResponse()
|
return OkResponse()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/test",
|
||||||
|
response_model=ForgejoConnectionTestResponse,
|
||||||
|
summary="Test Connection Before Saving",
|
||||||
|
description="Validate a base URL and token against a Forgejo instance without saving a connection record.",
|
||||||
|
)
|
||||||
|
async def test_connection(
|
||||||
|
payload: ForgejoConnectionTestRequest,
|
||||||
|
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
||||||
|
) -> ForgejoConnectionTestResponse:
|
||||||
|
"""Test a Forgejo connection without saving it."""
|
||||||
|
import time
|
||||||
|
from app.services.forgejo_client import ForgejoAPIClient
|
||||||
|
|
||||||
|
base_url = payload.base_url.strip().rstrip("/")
|
||||||
|
if "/api/v1" in base_url:
|
||||||
|
import re
|
||||||
|
match = re.match(r"(https?://[^/]+)", base_url)
|
||||||
|
if match:
|
||||||
|
base_url = match.group(1).rstrip("/")
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
try:
|
||||||
|
async with ForgejoAPIClient(base_url=base_url, token=payload.token.strip()) as client:
|
||||||
|
user_data = await client.get_user()
|
||||||
|
repos_raw = await client.list_user_repos(limit=50)
|
||||||
|
except Exception as exc:
|
||||||
|
return ForgejoConnectionTestResponse(
|
||||||
|
valid=False,
|
||||||
|
error=str(exc),
|
||||||
|
response_time_ms=(time.time() - start) * 1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
repos = [
|
||||||
|
{
|
||||||
|
"full_name": r.get("full_name", ""),
|
||||||
|
"name": r.get("name", ""),
|
||||||
|
"owner": (r.get("owner") or {}).get("login", ""),
|
||||||
|
"default_branch": r.get("default_branch", "main") or "main",
|
||||||
|
"private": bool(r.get("private", False)),
|
||||||
|
"description": r.get("description") or None,
|
||||||
|
}
|
||||||
|
for r in repos_raw
|
||||||
|
if r.get("full_name") and r.get("name")
|
||||||
|
]
|
||||||
|
|
||||||
|
return ForgejoConnectionTestResponse(
|
||||||
|
valid=True,
|
||||||
|
user_login=str(user_data.get("login", "")),
|
||||||
|
user_full_name=str(user_data.get("full_name", "")) or None,
|
||||||
|
repo_count=len(repos),
|
||||||
|
repos=repos,
|
||||||
|
response_time_ms=(time.time() - start) * 1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/{connection_id}/repos",
|
"/{connection_id}/repos",
|
||||||
summary="List Available Repositories",
|
summary="List Available Repositories",
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,36 @@ class ForgejoConnectionUpdate(SQLModel):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class ForgejoConnectionTestRequest(SQLModel):
|
||||||
|
"""Payload for testing a connection before saving it."""
|
||||||
|
|
||||||
|
base_url: str
|
||||||
|
token: str
|
||||||
|
|
||||||
|
|
||||||
|
class ForgejoConnectionTestRepo(SQLModel):
|
||||||
|
"""Minimal repo info returned in a connection test."""
|
||||||
|
|
||||||
|
full_name: str
|
||||||
|
name: str
|
||||||
|
owner: str
|
||||||
|
default_branch: str
|
||||||
|
private: bool
|
||||||
|
description: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ForgejoConnectionTestResponse(SQLModel):
|
||||||
|
"""Result of a pre-save connection test."""
|
||||||
|
|
||||||
|
valid: bool
|
||||||
|
user_login: str | None = None
|
||||||
|
user_full_name: str | None = None
|
||||||
|
repo_count: int = 0
|
||||||
|
repos: list[ForgejoConnectionTestRepo] = []
|
||||||
|
error: str | None = None
|
||||||
|
response_time_ms: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
class ForgejoConnectionRead(ForgejoConnectionBase):
|
class ForgejoConnectionRead(ForgejoConnectionBase):
|
||||||
"""Connection payload returned from read endpoints."""
|
"""Connection payload returned from read endpoints."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,24 @@
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AlertCircle,
|
||||||
|
CheckCircle2,
|
||||||
|
FlaskConical,
|
||||||
|
GitBranch,
|
||||||
|
Loader2,
|
||||||
|
Lock,
|
||||||
|
Unlock,
|
||||||
|
User,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Loader2 } from "lucide-react";
|
import {
|
||||||
|
testForgejoConnection,
|
||||||
import type { ForgejoConnectionCreate } from "@/lib/api-forgejo";
|
type ForgejoConnectionCreate,
|
||||||
|
type ForgejoConnectionTestResult,
|
||||||
|
} from "@/lib/api-forgejo";
|
||||||
|
|
||||||
interface ForgejoConnectionFormProps {
|
interface ForgejoConnectionFormProps {
|
||||||
defaultValues?: Partial<ForgejoConnectionFormValues>;
|
defaultValues?: Partial<ForgejoConnectionFormValues>;
|
||||||
|
|
@ -37,140 +50,270 @@ export function ForgejoConnectionForm({
|
||||||
}: ForgejoConnectionFormProps) {
|
}: ForgejoConnectionFormProps) {
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
|
const [testResult, setTestResult] = useState<ForgejoConnectionTestResult | null>(null);
|
||||||
const [name, setName] = useState(defaultValues.name || "");
|
const [name, setName] = useState(defaultValues.name || "");
|
||||||
const [baseUrl, setBaseUrl] = useState(defaultValues.base_url || "");
|
const [baseUrl, setBaseUrl] = useState(defaultValues.base_url || "");
|
||||||
const [token, setToken] = useState(defaultValues.token || "");
|
const [token, setToken] = useState(defaultValues.token || "");
|
||||||
const isBusy = isSubmitting || isSaving;
|
const isBusy = isSubmitting || isSaving;
|
||||||
|
|
||||||
const tokenHelpText = isTokenRequired
|
const tokenHelpText = isTokenRequired
|
||||||
? "Paste your Git provider personal access token. Use read:issue; add write:issue if Pipeline should close issues. The token is stored server-side and never displayed."
|
? "Paste your Forgejo personal access token. Use read:issue scope; add write:issue if Pipeline should close issues."
|
||||||
: existingTokenLastEight
|
: existingTokenLastEight
|
||||||
? `Leave blank to keep the saved token ending in ${existingTokenLastEight}. Paste a new token to replace it.`
|
? `Leave blank to keep the saved token ending in ${existingTokenLastEight}. Paste a new token to replace it.`
|
||||||
: "Paste a new Git provider personal access token to replace the saved token.";
|
: "Paste a new personal access token to replace the saved one.";
|
||||||
|
|
||||||
|
const canTest = baseUrl.trim().length > 0 && token.trim().length > 0;
|
||||||
|
|
||||||
|
async function handleTest() {
|
||||||
|
setTestResult(null);
|
||||||
|
setError(null);
|
||||||
|
setIsTesting(true);
|
||||||
|
try {
|
||||||
|
const result = await testForgejoConnection({
|
||||||
|
base_url: baseUrl.trim(),
|
||||||
|
token: token.trim(),
|
||||||
|
});
|
||||||
|
setTestResult(result);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Test request failed.");
|
||||||
|
} finally {
|
||||||
|
setIsTesting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
await onSubmit({
|
await onSubmit({ name, base_url: baseUrl, token });
|
||||||
name,
|
|
||||||
base_url: baseUrl,
|
|
||||||
token,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "An error occurred");
|
const msg = err instanceof Error ? err.message : "An error occurred";
|
||||||
|
setError(msg);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<div className="w-full max-w-2xl space-y-4">
|
||||||
onSubmit={handleSubmit}
|
<form
|
||||||
className="surface-panel w-full max-w-2xl space-y-6 rounded-2xl p-4 sm:p-6"
|
onSubmit={handleSubmit}
|
||||||
>
|
className="surface-panel space-y-6 rounded-2xl p-4 sm:p-6"
|
||||||
<div className="space-y-4">
|
>
|
||||||
<div>
|
<div className="space-y-4">
|
||||||
<h3 className="text-lg font-semibold text-strong">{title}</h3>
|
<div>
|
||||||
{description && (
|
<h3 className="text-lg font-semibold text-strong">{title}</h3>
|
||||||
<p className="mt-1 text-sm text-muted">{description}</p>
|
{description && (
|
||||||
)}
|
<p className="mt-1 text-sm text-muted">{description}</p>
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="rounded-xl border border-[color:rgba(248,113,113,0.35)] bg-[color:rgba(248,113,113,0.08)] p-4 text-[color:var(--danger)]">
|
<div className="rounded-xl border border-[color:rgba(248,113,113,0.35)] bg-[color:rgba(248,113,113,0.08)] p-4 text-[color:var(--danger)]">
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{error === "Failed to fetch"
|
{error === "Failed to fetch"
|
||||||
? "Could not reach Pipeline backend"
|
? "Could not reach Pipeline backend"
|
||||||
: "Error"}
|
: "Error"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
{error === "Failed to fetch"
|
{error === "Failed to fetch"
|
||||||
? "Pipeline could not connect to its own API. Check that NEXT_PUBLIC_API_URL points to your backend and that your frontend origin is listed in CORS_ORIGINS."
|
? "Pipeline could not connect to its own API. Check that NEXT_PUBLIC_API_URL points to your backend and that your frontend origin is listed in CORS_ORIGINS."
|
||||||
: error}
|
: error}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="name" className="text-sm font-medium">
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="Team Git"
|
||||||
|
disabled={isBusy}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted">
|
||||||
|
A memorable name for this Git Projects connection.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label htmlFor="name" className="text-sm font-medium">
|
<label htmlFor="base_url" className="text-sm font-medium">
|
||||||
Name
|
Base URL
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
id="name"
|
id="base_url"
|
||||||
value={name}
|
value={baseUrl}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => {
|
||||||
placeholder="Team Git"
|
setBaseUrl(e.target.value);
|
||||||
disabled={isBusy}
|
setTestResult(null);
|
||||||
required
|
}}
|
||||||
/>
|
placeholder="https://git.example.com"
|
||||||
<p className="text-xs text-muted">
|
disabled={isBusy}
|
||||||
A memorable name for this Git Projects connection.
|
required
|
||||||
</p>
|
/>
|
||||||
|
<p className="text-xs text-muted">
|
||||||
|
The base URL of your Forgejo instance, without a trailing slash.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="token" className="text-sm font-medium">
|
||||||
|
Token
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="token"
|
||||||
|
type="password"
|
||||||
|
value={token}
|
||||||
|
onChange={(e) => {
|
||||||
|
setToken(e.target.value);
|
||||||
|
setTestResult(null);
|
||||||
|
}}
|
||||||
|
placeholder={
|
||||||
|
isTokenRequired
|
||||||
|
? "Paste token"
|
||||||
|
: existingTokenLastEight
|
||||||
|
? `Current token ends in ${existingTokenLastEight}`
|
||||||
|
: "Paste token"
|
||||||
|
}
|
||||||
|
disabled={isBusy}
|
||||||
|
required={isTokenRequired}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted">{tokenHelpText}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="flex flex-col-reverse gap-3 border-t border-[color:var(--border)] pt-4 sm:flex-row sm:justify-end">
|
||||||
<label htmlFor="base_url" className="text-sm font-medium">
|
<Button
|
||||||
Base URL
|
type="button"
|
||||||
</label>
|
variant="outline"
|
||||||
<Input
|
onClick={() => window.history.back()}
|
||||||
id="base_url"
|
|
||||||
value={baseUrl}
|
|
||||||
onChange={(e) => setBaseUrl(e.target.value)}
|
|
||||||
placeholder="https://git.example.com"
|
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
required
|
>
|
||||||
/>
|
Cancel
|
||||||
<p className="text-xs text-muted">
|
</Button>
|
||||||
The base URL of your Git provider instance, without a trailing
|
<Button
|
||||||
slash.
|
type="button"
|
||||||
</p>
|
variant="outline"
|
||||||
|
onClick={handleTest}
|
||||||
|
disabled={isBusy || isTesting || !canTest}
|
||||||
|
>
|
||||||
|
{isTesting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
Testing…
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FlaskConical className="h-4 w-4" />
|
||||||
|
Test Connection
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isBusy}>
|
||||||
|
{isBusy ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Saving…
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
submitLabel
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<div className="space-y-2">
|
{/* Test results panel */}
|
||||||
<label htmlFor="token" className="text-sm font-medium">
|
{testResult && (
|
||||||
Token
|
<div
|
||||||
</label>
|
className={`rounded-2xl border p-4 sm:p-6 ${
|
||||||
<Input
|
testResult.valid
|
||||||
id="token"
|
? "border-[color:rgba(52,211,153,0.35)] bg-[color:rgba(52,211,153,0.06)]"
|
||||||
type="password"
|
: "border-[color:rgba(248,113,113,0.35)] bg-[color:rgba(248,113,113,0.06)]"
|
||||||
value={token}
|
}`}
|
||||||
onChange={(e) => setToken(e.target.value)}
|
|
||||||
placeholder={
|
|
||||||
isTokenRequired
|
|
||||||
? "Paste token"
|
|
||||||
: existingTokenLastEight
|
|
||||||
? `Current token ends in ${existingTokenLastEight}`
|
|
||||||
: "Paste token"
|
|
||||||
}
|
|
||||||
disabled={isBusy}
|
|
||||||
required={isTokenRequired}
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted">{tokenHelpText}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col-reverse gap-3 border-t border-[color:var(--border)] pt-4 sm:flex-row sm:justify-end">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => window.history.back()}
|
|
||||||
disabled={isBusy}
|
|
||||||
>
|
>
|
||||||
Cancel
|
{/* Header */}
|
||||||
</Button>
|
<div className="flex items-start gap-3 mb-4">
|
||||||
<Button type="submit" disabled={isBusy}>
|
{testResult.valid ? (
|
||||||
{isBusy ? (
|
<CheckCircle2 className="h-5 w-5 shrink-0 text-[color:var(--success)] mt-0.5" />
|
||||||
<>
|
) : (
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<AlertCircle className="h-5 w-5 shrink-0 text-[color:var(--danger)] mt-0.5" />
|
||||||
Saving…
|
)}
|
||||||
</>
|
<div className="min-w-0">
|
||||||
) : (
|
<p className={`font-semibold ${testResult.valid ? "text-[color:var(--success)]" : "text-[color:var(--danger)]"}`}>
|
||||||
submitLabel
|
{testResult.valid ? "Connection successful" : "Connection failed"}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted mt-0.5">
|
||||||
|
{Math.round(testResult.response_time_ms)}ms
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{testResult.error && (
|
||||||
|
<p className="text-sm text-[color:var(--danger)] mb-4">{testResult.error}</p>
|
||||||
)}
|
)}
|
||||||
</Button>
|
|
||||||
</div>
|
{testResult.valid && (
|
||||||
</form>
|
<div className="space-y-4">
|
||||||
|
{/* Identity */}
|
||||||
|
<div className="flex items-center gap-2 text-sm text-strong">
|
||||||
|
<User className="h-4 w-4 shrink-0 text-muted" />
|
||||||
|
<span>
|
||||||
|
Connected as{" "}
|
||||||
|
<strong>@{testResult.user_login}</strong>
|
||||||
|
{testResult.user_full_name && testResult.user_full_name !== testResult.user_login
|
||||||
|
? ` (${testResult.user_full_name})`
|
||||||
|
: ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Repo list */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 text-sm font-medium text-strong mb-2">
|
||||||
|
<GitBranch className="h-4 w-4 shrink-0 text-muted" />
|
||||||
|
<span>
|
||||||
|
{testResult.repo_count}{" "}
|
||||||
|
{testResult.repo_count === 1 ? "repository" : "repositories"} accessible
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{testResult.repos.length > 0 && (
|
||||||
|
<div className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] divide-y divide-[color:var(--border)] overflow-hidden">
|
||||||
|
{testResult.repos.map((repo) => (
|
||||||
|
<div
|
||||||
|
key={repo.full_name}
|
||||||
|
className="flex items-start gap-3 px-4 py-2.5"
|
||||||
|
>
|
||||||
|
{repo.private ? (
|
||||||
|
<Lock className="h-3.5 w-3.5 shrink-0 text-muted mt-0.5" />
|
||||||
|
) : (
|
||||||
|
<Unlock className="h-3.5 w-3.5 shrink-0 text-muted mt-0.5" />
|
||||||
|
)}
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-sm font-medium text-strong truncate">
|
||||||
|
{repo.full_name}
|
||||||
|
</p>
|
||||||
|
{repo.description && (
|
||||||
|
<p className="text-xs text-muted truncate">{repo.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="ml-auto shrink-0 font-mono text-xs text-muted">
|
||||||
|
{repo.default_branch}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,39 @@ export async function deleteForgejoRepository(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-save connection test
|
||||||
|
export interface ForgejoConnectionTestRepo {
|
||||||
|
full_name: string;
|
||||||
|
name: string;
|
||||||
|
owner: string;
|
||||||
|
default_branch: string;
|
||||||
|
private: boolean;
|
||||||
|
description: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ForgejoConnectionTestResult {
|
||||||
|
valid: boolean;
|
||||||
|
user_login: string | null;
|
||||||
|
user_full_name: string | null;
|
||||||
|
repo_count: number;
|
||||||
|
repos: ForgejoConnectionTestRepo[];
|
||||||
|
error: string | null;
|
||||||
|
response_time_ms: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function testForgejoConnection(data: {
|
||||||
|
base_url: string;
|
||||||
|
token: string;
|
||||||
|
}): Promise<ForgejoConnectionTestResult> {
|
||||||
|
return fetchJson<ForgejoConnectionTestResult>(
|
||||||
|
"/api/v1/forgejo/connections/test",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Remote repo discovery
|
// Remote repo discovery
|
||||||
export interface ForgejoRemoteRepo {
|
export interface ForgejoRemoteRepo {
|
||||||
full_name: string;
|
full_name: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue