feat(gitea): add Gitea/Forgejo as a git provider
Full parity with the existing GitLab provider: - Server module packages/web/server/lib/gitea (auth/client/repo/routes + docs + tests) with Gitea REST v1 API, PAT + base URL auth, multi-account storage - Shared GiteaAPI types and web API client - Provider detection generalized with user-configurable custom domains per provider (github/gitlab/gitea), additive with built-in defaults (github.com, gitlab.com) and connected-account hosts; precedence github -> gitlab -> gitea - Gitea PR view, issues section, pickers, integration dialog, branch PR status helper, settings UI (PAT + base URL + custom domains) - Magic prompts (gitea.pr.review, gitea.issue.review) and full 11-locale i18n parity
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||
import type { GiteaPullRequestSummary } from '@/lib/api/types';
|
||||
|
||||
// Branch PR lookups are cheap to re-request but visible to the user on every
|
||||
// mount of the surfaces that display them (walkthrough header, git view, work
|
||||
// status). A shared TTL cache keeps those surfaces consistent with each other
|
||||
// and stops repeated Gitea calls while a branch is in view.
|
||||
const CACHE_TTL_MS = 90_000;
|
||||
const prCache = new Map<string, { at: number; pr: GiteaPullRequestSummary | null }>();
|
||||
|
||||
const cacheKeyFor = (directory: string, branch: string): string => `${directory}\n${branch}`;
|
||||
|
||||
const readCachedPr = (directory: string, branch: string): GiteaPullRequestSummary | null | undefined => {
|
||||
const entry = prCache.get(cacheKeyFor(directory, branch));
|
||||
return entry?.pr;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve the pull request targeting `branch` in `directory`, preferring the
|
||||
* open request and falling back to a merged one so a just-merged branch still
|
||||
* surfaces its request instead of nothing.
|
||||
*
|
||||
* Returns null when the runtime has no Gitea API, the request fails, or no PR
|
||||
* matches — callers only use the result to show or hide an additive chip, so a
|
||||
* null answer simply means "nothing to show". Results are cached per
|
||||
* directory+branch for CACHE_TTL_MS, including the null case.
|
||||
*/
|
||||
export const resolveGiteaPrForBranch = async (
|
||||
directory: string,
|
||||
branch: string,
|
||||
): Promise<GiteaPullRequestSummary | null> => {
|
||||
const gitea = getRegisteredRuntimeAPIs()?.gitea;
|
||||
if (!gitea?.prsList || !directory || !branch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const key = cacheKeyFor(directory, branch);
|
||||
const cached = prCache.get(key);
|
||||
if (cached && Date.now() - cached.at < CACHE_TTL_MS) {
|
||||
return cached.pr;
|
||||
}
|
||||
|
||||
let pr: GiteaPullRequestSummary | null = null;
|
||||
try {
|
||||
const result = await gitea.prsList(directory, { sourceBranch: branch });
|
||||
const candidates = result.prs ?? [];
|
||||
pr = candidates.find((item) => item.state === 'open')
|
||||
?? candidates.find((item) => item.state === 'merged')
|
||||
?? null;
|
||||
} catch {
|
||||
pr = null;
|
||||
}
|
||||
|
||||
prCache.set(key, { at: Date.now(), pr });
|
||||
return pr;
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribe to the branch's pull request. Reads the TTL cache synchronously
|
||||
* for the initial render so an already-resolved PR never flashes away while a
|
||||
* refresh runs; a cache miss shows a loading state instead of a stale result
|
||||
* from another branch.
|
||||
*/
|
||||
export const useGiteaPrForBranch = (
|
||||
directory: string | null | undefined,
|
||||
branch: string | null | undefined,
|
||||
): { pr: GiteaPullRequestSummary | null; isLoading: boolean } => {
|
||||
const [pr, setPr] = useState<GiteaPullRequestSummary | null>(() =>
|
||||
directory && branch ? (readCachedPr(directory, branch) ?? null) : null
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!directory || !branch) {
|
||||
setPr(null);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let mounted = true;
|
||||
const cached = readCachedPr(directory, branch);
|
||||
const cacheEntry = prCache.get(cacheKeyFor(directory, branch));
|
||||
const fresh = cacheEntry !== undefined && Date.now() - cacheEntry.at < CACHE_TTL_MS;
|
||||
|
||||
if (fresh) {
|
||||
setPr(cached ?? null);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// A stale entry stays on screen while it refreshes; a missing one shows
|
||||
// the loading state rather than a result from a previous branch.
|
||||
setPr(cached ?? null);
|
||||
setIsLoading(true);
|
||||
|
||||
void resolveGiteaPrForBranch(directory, branch).then((resolved) => {
|
||||
if (mounted) {
|
||||
setPr(resolved);
|
||||
setIsLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [directory, branch]);
|
||||
|
||||
return { pr, isLoading };
|
||||
};
|
||||
Reference in New Issue
Block a user