From c6e99b1efcd3cb795d62b000a2782139cd5573a3 Mon Sep 17 00:00:00 2001 From: chyoo Date: Fri, 3 Jul 2026 17:00:34 +0900 Subject: [PATCH] show recent commits in launcher source page --- src/pages/launcher/LauncherSourcePage.tsx | 125 ++++++++++++++++++++++ src/pages/launcher/launcher-api.ts | 38 +++++++ 2 files changed, 163 insertions(+) diff --git a/src/pages/launcher/LauncherSourcePage.tsx b/src/pages/launcher/LauncherSourcePage.tsx index e0cff82..e6fb45b 100644 --- a/src/pages/launcher/LauncherSourcePage.tsx +++ b/src/pages/launcher/LauncherSourcePage.tsx @@ -8,10 +8,12 @@ import type { GiteaRepo } from '../../types/application'; import { LoadingModal, useMinimumLoading } from '../operations/operation-utils'; import { getLauncherSource, + listGiteaCommits, listLauncherVersions, listPlatformRepos, listStudioRepos, updateLauncherSource, + type GiteaCommit, type LauncherVersion } from './launcher-api'; @@ -52,6 +54,13 @@ function normalizeRepository(repository?: string | null, fallbackOrg = giteaAppO return value.includes('/') ? value : `${fallbackOrg}/${value}`; } +function splitRepository(repository?: string | null, fallbackOrg = giteaAppOrg) { + const normalizedRepository = normalizeRepository(repository, fallbackOrg); + const [owner, ...repoParts] = normalizedRepository.split('/'); + const repo = repoParts.join('/'); + return owner && repo ? { owner, repo } : null; +} + function httpCloneUrl(repository?: string | null, fallbackOrg?: string) { const normalizedRepository = normalizeRepository(repository, fallbackOrg); return normalizedRepository ? `${giteaBaseUrl.replace(/\/$/, '')}/${normalizedRepository}.git` : '-'; @@ -72,6 +81,45 @@ function emptyForm(): SourceForm { }; } +function commitAuthor(commit: GiteaCommit) { + return commit.author?.login + || commit.author?.username + || commit.author?.full_name + || commit.author?.name + || commit.commit?.author?.name + || commit.committer?.login + || '-'; +} + +function commitMessage(commit: GiteaCommit) { + return (commit.commit?.message || '').split(/\r?\n/)[0]?.trim() || '-'; +} + +function commitDate(commit: GiteaCommit) { + return commit.commit?.author?.date + || commit.commit?.committer?.date + || commit.created + || ''; +} + +function relativeTime(value?: string | null) { + if (!value) return '-'; + const time = new Date(value).getTime(); + if (Number.isNaN(time)) return '-'; + + const seconds = Math.max(1, Math.floor((Date.now() - time) / 1000)); + if (seconds < 60) return '방금 전'; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}분 전`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}시간 전`; + const days = Math.floor(hours / 24); + if (days === 1) return '어제'; + if (days === 2) return '그저께'; + if (days < 7) return `${days}일 전`; + return new Intl.DateTimeFormat('ko-KR', { month: '2-digit', day: '2-digit' }).format(new Date(value)); +} + export function LauncherSourcePage() { const { toast } = useToast(); const [activeTarget, setActiveTarget] = useState('launcher-client'); @@ -81,6 +129,9 @@ export function LauncherSourcePage() { 'launcher-client': emptyForm(), 'launcher-backend': emptyForm() }); + const [commits, setCommits] = useState([]); + const [commitsLoading, setCommitsLoading] = useState(false); + const [commitsError, setCommitsError] = useState(''); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const loadingVisible = useMinimumLoading(loading); @@ -137,6 +188,36 @@ export function LauncherSourcePage() { refresh(); }, []); + useEffect(() => { + const repository = splitRepository(activeForm.repository, activeTargetMeta.defaultOrg); + if (!repository) { + setCommits([]); + setCommitsError(''); + return; + } + + let canceled = false; + setCommitsLoading(true); + setCommitsError(''); + listGiteaCommits(repository.owner, repository.repo, activeForm.branch || 'develop', 5) + .then((nextCommits) => { + if (!canceled) setCommits(nextCommits); + }) + .catch((error: Error) => { + if (!canceled) { + setCommits([]); + setCommitsError(error.message); + } + }) + .finally(() => { + if (!canceled) setCommitsLoading(false); + }); + + return () => { + canceled = true; + }; + }, [activeForm.repository, activeForm.branch, activeTargetMeta.defaultOrg]); + const save = () => { if (!activeForm.repository.trim()) return; setSaving(true); @@ -295,6 +376,50 @@ export function LauncherSourcePage() { + + +
+ + + + + + + + + + + {commitsLoading ? ( + + + + ) : null} + {!commitsLoading && commitsError ? ( + + + + ) : null} + {!commitsLoading && !commitsError && commits.length === 0 ? ( + + + + ) : null} + {!commitsLoading && !commitsError ? commits.map((commit) => ( + + + + + + + )) : null} + +
작성자SHA1메시지날짜
커밋을 불러오는 중입니다.
커밋 조회 실패: {commitsError}
표시할 커밋이 없습니다.
{commitAuthor(commit)}{commit.sha.slice(0, 10)}{commitMessage(commit)}{relativeTime(commitDate(commit))}
+
+
+ ); diff --git a/src/pages/launcher/launcher-api.ts b/src/pages/launcher/launcher-api.ts index a90972a..988293d 100644 --- a/src/pages/launcher/launcher-api.ts +++ b/src/pages/launcher/launcher-api.ts @@ -54,6 +54,34 @@ export type LauncherSource = { }; }; +export type GiteaCommit = { + sha: string; + commit?: { + message?: string; + author?: { + name?: string; + date?: string; + }; + committer?: { + name?: string; + date?: string; + }; + }; + author?: { + login?: string; + username?: string; + full_name?: string; + name?: string; + } | null; + committer?: { + login?: string; + username?: string; + full_name?: string; + name?: string; + } | null; + created?: string; +}; + export type LauncherAppLink = { id: string; appId: string; @@ -151,6 +179,16 @@ export function listStudioRepos(limit = 50) { .then((result) => result.repos); } +export function listGiteaCommits(owner: string, repo: string, branch = 'develop', limit = 5) { + const params = new URLSearchParams({ + sha: branch, + limit: String(limit) + }); + return apiRequest<{ commits: GiteaCommit[] }>( + `/gitea/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits?${params.toString()}` + ).then((result) => result.commits); +} + export function listLauncherBuildJobs() { return apiRequest('/launcher/builds'); }