feat(git-graph): VS Code-style git graph with commit actions in History modal (#1431)
* feat(types): add parents to GitLogEntry and new commit action types
* feat(git): add parent hashes and --all flag to getLog
* fix(git): move record separator to start of log format string
* feat(git): add checkoutCommit server function and route
* feat(git): add cherryPick server function and route
* feat(git): add revertCommit server function and route
* feat(git): add resetToCommit server function and route
* fix(tests): make git service tests branch-name portable, add error path tests
* feat(client): add checkoutCommit, cherryPick, revertCommit, resetToCommit API wrappers
* feat(git-graph): add lane assignment algorithm with tests
* feat(git-graph): add GitGraphSegment per-row SVG renderer
* feat(i18n): add locale strings for git graph action buttons
* fix(git-graph): handle lane convergence, fix SVG path coords, add connector tests
* feat(git-graph): add ref badges and action buttons to HistoryCommitRow
* fix(git-graph): add loading guards to reset actions, use theme tokens for ref badges
* fix(git-graph): conditional hooks, stale graph log, conflict handling, i18n
* fix(types): replace toBeDefined with toBeTruthy, fix toast API usage
* fix(lint): remove unused variables
* fix(git-graph): fix SVG height causing 150px row spacing
* fix(git-graph): smooth bezier curves, fill row height, round line caps
* fix(git-graph): non-scaling-stroke fixes bezier white spaces, sort curves on top
* fix(git-graph): remove viewBox scaling, match SVG height to actual row height
* fix(git-graph): ResizeObserver tracks actual row height, eliminates SVG height mismatch
* feat(git-graph): replace SVG with Canvas for graph rendering
* fix(git-graph): isolate canvas from flex layout to prevent replaced-element height leak
* feat(git-graph): align action buttons, add confirmation popups for all actions
* fix(git-graph): address code review findings CR-001 through CR-005
- CR-001: VS Code getGitLog now forwards 'all' option and parses %P parents
- CR-002: VS Code bridge/gitService implement checkoutCommit, cherryPick,
revertCommit, resetToCommit with conflict detection and hard-reset guard
- CR-003: server-side commit hash validated with /^[0-9a-fA-F]{7,40}$/
in both routes.js and service.js; 12 new rejection tests added
- CR-004: cherry-pick/revert conflict path now refreshes fetchStatus/
fetchBranches/fetchLog; conflict toast uses i18n keys in all 7 locales
- CR-005: corrected O(n) comment to O(n x lanes)
* fix(i18n): add zh-TW locale and common.language.traditionalChinese key to all locales
upstream/main added zh-TW.ts after branch diverged; CI type-check fails
when PR is merged because zh-TW.ts was missing all gitView.history.actions.*
keys and loadMore/loadingMore. Also adds common.language.traditionalChinese
to en.ts and all 6 non-English files to match upstream en.ts.
* fix: harden git history actions
* feat: split git history graph view
* chore: remove git graph planning docs
---------
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
cc3d1bd63c
commit
52ffe9daef
@@ -236,6 +236,37 @@ export interface GitMergeResult {
|
||||
conflictFiles?: string[];
|
||||
}
|
||||
|
||||
export interface CheckoutCommitResponse {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface CherryPickRequest {
|
||||
hash: string;
|
||||
}
|
||||
export interface CherryPickResponse {
|
||||
success: boolean;
|
||||
conflict?: boolean;
|
||||
conflictFiles?: string[];
|
||||
}
|
||||
|
||||
export interface RevertCommitRequest {
|
||||
hash: string;
|
||||
}
|
||||
export interface RevertCommitResponse {
|
||||
success: boolean;
|
||||
conflict?: boolean;
|
||||
conflictFiles?: string[];
|
||||
}
|
||||
|
||||
export interface ResetToCommitRequest {
|
||||
hash: string;
|
||||
mode: 'soft' | 'mixed' | 'hard';
|
||||
force?: boolean;
|
||||
}
|
||||
export interface ResetToCommitResponse {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface GitRebaseResult {
|
||||
success: boolean;
|
||||
conflict?: boolean;
|
||||
@@ -291,6 +322,7 @@ export interface GitLogEntry {
|
||||
filesChanged: number;
|
||||
insertions: number;
|
||||
deletions: number;
|
||||
parents: string[];
|
||||
}
|
||||
|
||||
export interface GitLogResponse {
|
||||
@@ -404,6 +436,7 @@ export interface GitLogOptions {
|
||||
from?: string;
|
||||
to?: string;
|
||||
file?: string;
|
||||
all?: boolean;
|
||||
}
|
||||
|
||||
export interface GeneratedCommitMessage {
|
||||
@@ -484,6 +517,10 @@ export interface GitAPI {
|
||||
merge(directory: string, options: { branch: string }): Promise<GitMergeResult>;
|
||||
abortMerge(directory: string): Promise<{ success: boolean }>;
|
||||
continueMerge(directory: string): Promise<{ success: boolean; conflict: boolean; conflictFiles?: string[] }>;
|
||||
checkoutCommit(directory: string, hash: string): Promise<CheckoutCommitResponse>;
|
||||
cherryPick(directory: string, hash: string): Promise<CherryPickResponse>;
|
||||
revertCommit(directory: string, hash: string): Promise<RevertCommitResponse>;
|
||||
resetToCommit(directory: string, hash: string, mode: 'soft' | 'mixed' | 'hard', force?: boolean): Promise<ResetToCommitResponse>;
|
||||
stash(directory: string, options?: { message?: string; includeUntracked?: boolean }): Promise<{ success: boolean }>;
|
||||
stashPop(directory: string): Promise<{ success: boolean }>;
|
||||
getConflictDetails(directory: string): Promise<MergeConflictDetails>;
|
||||
|
||||
@@ -784,6 +784,44 @@ export async function merge(
|
||||
return gitHttp.merge(directory, options);
|
||||
}
|
||||
|
||||
export async function checkoutCommit(
|
||||
directory: string,
|
||||
hash: string
|
||||
): Promise<import('./api/types').CheckoutCommitResponse> {
|
||||
const runtime = getRuntimeGit();
|
||||
if (runtime) return runtime.checkoutCommit(directory, hash);
|
||||
return gitHttp.checkoutCommit(directory, hash);
|
||||
}
|
||||
|
||||
export async function cherryPick(
|
||||
directory: string,
|
||||
hash: string
|
||||
): Promise<import('./api/types').CherryPickResponse> {
|
||||
const runtime = getRuntimeGit();
|
||||
if (runtime) return runtime.cherryPick(directory, hash);
|
||||
return gitHttp.cherryPick(directory, hash);
|
||||
}
|
||||
|
||||
export async function revertCommit(
|
||||
directory: string,
|
||||
hash: string
|
||||
): Promise<import('./api/types').RevertCommitResponse> {
|
||||
const runtime = getRuntimeGit();
|
||||
if (runtime) return runtime.revertCommit(directory, hash);
|
||||
return gitHttp.revertCommit(directory, hash);
|
||||
}
|
||||
|
||||
export async function resetToCommit(
|
||||
directory: string,
|
||||
hash: string,
|
||||
mode: 'soft' | 'mixed' | 'hard',
|
||||
force?: boolean
|
||||
): Promise<import('./api/types').ResetToCommitResponse> {
|
||||
const runtime = getRuntimeGit();
|
||||
if (runtime) return runtime.resetToCommit(directory, hash, mode, force);
|
||||
return gitHttp.resetToCommit(directory, hash, mode, force);
|
||||
}
|
||||
|
||||
export async function abortMerge(directory: string): Promise<{ success: boolean }> {
|
||||
const runtime = getRuntimeGit();
|
||||
if (runtime) return runtime.abortMerge(directory);
|
||||
|
||||
@@ -30,6 +30,10 @@ import type {
|
||||
GitIdentitySummary,
|
||||
DiscoveredGitCredential,
|
||||
MergeConflictDetails,
|
||||
CheckoutCommitResponse,
|
||||
CherryPickResponse,
|
||||
RevertCommitResponse,
|
||||
ResetToCommitResponse,
|
||||
} from './api/types';
|
||||
|
||||
declare global {
|
||||
@@ -711,6 +715,7 @@ export async function getGitLog(
|
||||
from: options.from,
|
||||
to: options.to,
|
||||
file: options.file,
|
||||
all: options.all ? 'true' : undefined,
|
||||
})
|
||||
);
|
||||
if (!response.ok) {
|
||||
@@ -930,6 +935,72 @@ export async function merge(
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function checkoutCommit(
|
||||
directory: string,
|
||||
hash: string
|
||||
): Promise<CheckoutCommitResponse> {
|
||||
const response = await fetch(buildUrl(`${API_BASE}/checkout-commit`, directory), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ hash }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: response.statusText }));
|
||||
throw new Error(error.error || 'Failed to checkout commit');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function cherryPick(
|
||||
directory: string,
|
||||
hash: string
|
||||
): Promise<CherryPickResponse> {
|
||||
const response = await fetch(buildUrl(`${API_BASE}/cherry-pick`, directory), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ hash }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: response.statusText }));
|
||||
throw new Error(error.error || 'Failed to cherry-pick');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function revertCommit(
|
||||
directory: string,
|
||||
hash: string
|
||||
): Promise<RevertCommitResponse> {
|
||||
const response = await fetch(buildUrl(`${API_BASE}/revert-commit`, directory), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ hash }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: response.statusText }));
|
||||
throw new Error(error.error || 'Failed to revert commit');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function resetToCommit(
|
||||
directory: string,
|
||||
hash: string,
|
||||
mode: 'soft' | 'mixed' | 'hard',
|
||||
force?: boolean
|
||||
): Promise<ResetToCommitResponse> {
|
||||
const response = await fetch(buildUrl(`${API_BASE}/reset-to-commit`, directory), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ hash, mode, force }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: response.statusText }));
|
||||
throw new Error(error.error || 'Failed to reset');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function abortMerge(directory: string): Promise<{ success: boolean }> {
|
||||
const response = await fetch(buildUrl(`${API_BASE}/merge/abort`, directory), {
|
||||
method: 'POST',
|
||||
|
||||
@@ -503,11 +503,39 @@ export const dict = {
|
||||
'gitView.header.identityTooltip': 'Git identity',
|
||||
'gitView.header.noIdentity': 'No identity',
|
||||
'gitView.header.noProfiles': 'No profiles available to apply.',
|
||||
'gitView.header.repositoryViews': 'Repository views',
|
||||
'gitView.header.removeRemoteAria': 'Remove Remote aria label',
|
||||
'gitView.header.removeRemoteTitle': 'Remove Remote Title',
|
||||
'gitView.header.upstreamSynced': 'synced',
|
||||
'gitView.header.upstreamTooltip': 'Compared with {target}.',
|
||||
'gitView.header.upstreamTooltipTracking': 'Compared with {target}. Primary sync badges still reflect {tracking}.',
|
||||
'gitView.history.actions.cancelButton': 'Cancel',
|
||||
'gitView.history.actions.checkout': 'Checkout',
|
||||
'gitView.history.actions.checkoutConfirm': 'Check out this commit as detached HEAD?',
|
||||
'gitView.history.actions.cherryPick': 'Cherry-pick',
|
||||
'gitView.history.actions.cherryPickConfirm': 'Cherry-pick this commit onto the current branch?',
|
||||
'gitView.history.actions.conflictToastDescription': 'Conflicts in: {files}. Resolve manually and commit, or abort with git cherry-pick/revert --abort.',
|
||||
'gitView.history.actions.conflictToastTitle': 'Conflict',
|
||||
'gitView.history.actions.confirmButton': 'Confirm',
|
||||
'gitView.history.actions.createBranch': 'Create branch here',
|
||||
'gitView.history.actions.createBranchConfirm': 'Create',
|
||||
'gitView.history.actions.createBranchPlaceholder': 'Branch name',
|
||||
'gitView.history.actions.detachedHead': 'Checked out (detached HEAD)',
|
||||
'gitView.history.actions.merge': 'Merge into current',
|
||||
'gitView.history.actions.mergeConfirm': 'Merge this commit into the current branch?',
|
||||
'gitView.history.actions.rebase': 'Rebase onto this',
|
||||
'gitView.history.actions.rebaseConfirm': 'Rebase current branch onto this commit?',
|
||||
'gitView.history.actions.reset': 'Reset...',
|
||||
'gitView.history.actions.resetHard': 'Hard — discard all changes',
|
||||
'gitView.history.actions.resetHardConfirm': 'Hard reset — HEAD moves, all uncommitted changes permanently discarded.',
|
||||
'gitView.history.actions.resetHardConfirmButton': 'Discard changes',
|
||||
'gitView.history.actions.resetMixed': 'Mixed — unstage changes',
|
||||
'gitView.history.actions.resetMixedConfirm': 'Mixed reset — HEAD moves, changes unstaged.',
|
||||
'gitView.history.actions.resetSoft': 'Soft — keep staged',
|
||||
'gitView.history.actions.resetSoftConfirm': 'Soft reset — HEAD moves, staged changes preserved.',
|
||||
'gitView.history.actions.revert': 'Revert',
|
||||
'gitView.history.actions.revertConfirm': 'Stage a revert of this commit?',
|
||||
|
||||
'gitView.history.binary': 'Binary',
|
||||
'gitView.history.binaryNoDiff': 'Binary file — no diff available',
|
||||
'gitView.history.commitsPlaceholder': 'Commits Placeholder',
|
||||
@@ -517,6 +545,8 @@ export const dict = {
|
||||
'gitView.history.largeDiffTitle': 'Large diff ({count} changed lines)',
|
||||
'gitView.history.loadingDiff': 'Loading diff...',
|
||||
'gitView.history.loadingFiles': 'Loading files...',
|
||||
'gitView.history.loadMore': 'Load more',
|
||||
'gitView.history.loadingMore': 'Loading...',
|
||||
'gitView.history.logSize100': 'Log Size100',
|
||||
'gitView.history.logSize25': 'Log Size25',
|
||||
'gitView.history.logSize50': 'Log Size50',
|
||||
@@ -525,6 +555,7 @@ export const dict = {
|
||||
'gitView.history.renamedNoDiff': 'Renamed file — diff not supported',
|
||||
'gitView.history.renderDiffAnyway': 'Render anyway',
|
||||
'gitView.history.title': 'History',
|
||||
'gitView.graph.title': 'Graph',
|
||||
'gitView.integrate.checking': 'Checking…',
|
||||
'gitView.integrate.cherryPickAbortedToast': 'Cherry Pick Aborted Toast',
|
||||
'gitView.integrate.cherryPickConflictDescription': 'Cherry Pick Conflict Description',
|
||||
|
||||
@@ -504,11 +504,39 @@ export const dict: Record<I18nKey, string> = {
|
||||
"gitView.header.identityTooltip": "Identidad de Git",
|
||||
"gitView.header.noIdentity": "Sin identidad",
|
||||
"gitView.header.noProfiles": "No hay perfiles disponibles para aplicar.",
|
||||
"gitView.header.repositoryViews": "Vistas del repositorio",
|
||||
"gitView.header.removeRemoteAria": "Eliminar remoto",
|
||||
"gitView.header.removeRemoteTitle": "Eliminar remoto",
|
||||
"gitView.header.upstreamSynced": "sincronizado",
|
||||
"gitView.header.upstreamTooltip": "Comparado con {target}.",
|
||||
"gitView.header.upstreamTooltipTracking": "Comparado con {target}. Los indicadores principales de sincronización aún reflejan {tracking}.",
|
||||
"gitView.history.actions.cancelButton": "Cancelar",
|
||||
"gitView.history.actions.checkoutConfirm": "¿Cambiar a este commit como HEAD separado?",
|
||||
"gitView.history.actions.cherryPickConfirm": "¿Aplicar cherry-pick de este commit sobre la rama actual?",
|
||||
"gitView.history.actions.conflictToastDescription": "Conflictos en: {files}. Resuélvelos manualmente y haz commit, o aborta con git cherry-pick/revert --abort.",
|
||||
"gitView.history.actions.conflictToastTitle": "Conflicto",
|
||||
"gitView.history.actions.confirmButton": "Confirmar",
|
||||
"gitView.history.actions.mergeConfirm": "¿Fusionar este commit en la rama actual?",
|
||||
"gitView.history.actions.rebaseConfirm": "¿Rebasear la rama actual sobre este commit?",
|
||||
"gitView.history.actions.resetHardConfirm": "Reset hard — HEAD se mueve y todos los cambios sin commit se descartan permanentemente.",
|
||||
"gitView.history.actions.resetMixedConfirm": "Reset mixed — HEAD se mueve y los cambios quedan sin preparar.",
|
||||
"gitView.history.actions.resetSoftConfirm": "Reset soft — HEAD se mueve y los cambios preparados se conservan.",
|
||||
"gitView.history.actions.revertConfirm": "¿Preparar una reversión de este commit?",
|
||||
"gitView.history.actions.checkout": "Cambiar a commit",
|
||||
"gitView.history.actions.cherryPick": "Cherry-pick",
|
||||
"gitView.history.actions.createBranch": "Crear rama aquí",
|
||||
"gitView.history.actions.createBranchConfirm": "Crear",
|
||||
"gitView.history.actions.createBranchPlaceholder": "Nombre de rama",
|
||||
"gitView.history.actions.detachedHead": "Checkout realizado (HEAD separado)",
|
||||
"gitView.history.actions.merge": "Fusionar en la actual",
|
||||
"gitView.history.actions.rebase": "Rebasear sobre este",
|
||||
"gitView.history.actions.reset": "Reset...",
|
||||
"gitView.history.actions.resetHard": "Hard — descartar todos los cambios",
|
||||
"gitView.history.actions.resetHardConfirmButton": "Descartar cambios",
|
||||
"gitView.history.actions.resetMixed": "Mixed — quitar del stage",
|
||||
"gitView.history.actions.resetSoft": "Soft — conservar staged",
|
||||
"gitView.history.actions.revert": "Revertir",
|
||||
|
||||
"gitView.history.binary": "Binario",
|
||||
"gitView.history.binaryNoDiff": "Archivo binario — no hay diff disponible",
|
||||
"gitView.history.commitsPlaceholder": "Buscar commits...",
|
||||
@@ -518,6 +546,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
"gitView.history.largeDiffTitle": "Diff grande ({count} líneas cambiadas)",
|
||||
"gitView.history.loadingDiff": "Cargando diff...",
|
||||
"gitView.history.loadingFiles": "Cargando archivos...",
|
||||
"gitView.history.loadMore": "Cargar más",
|
||||
"gitView.history.loadingMore": "Cargando...",
|
||||
"gitView.history.logSize100": "100 commits",
|
||||
"gitView.history.logSize25": "25 commits",
|
||||
"gitView.history.logSize50": "50 commits",
|
||||
@@ -526,6 +556,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"gitView.history.renamedNoDiff": "Archivo renombrado — diff no soportado",
|
||||
"gitView.history.renderDiffAnyway": "Renderizar igualmente",
|
||||
"gitView.history.title": "Historial",
|
||||
"gitView.graph.title": "Grafo",
|
||||
"gitView.integrate.checking": "Verificando…",
|
||||
"gitView.integrate.cherryPickAbortedToast": "Cherry-pick abortado",
|
||||
"gitView.integrate.cherryPickConflictDescription": "Resuelve los conflictos de cherry-pick para continuar.",
|
||||
|
||||
@@ -504,11 +504,39 @@ export const dict: Record<I18nKey, string> = {
|
||||
'gitView.header.identityTooltip': 'Git 인증 정보',
|
||||
'gitView.header.noIdentity': '인증 정보 없음',
|
||||
'gitView.header.noProfiles': '적용할 프로필 없음',
|
||||
'gitView.header.repositoryViews': '저장소 보기',
|
||||
'gitView.header.removeRemoteAria': '리모트 제거',
|
||||
'gitView.header.removeRemoteTitle': '리모트 제거',
|
||||
'gitView.header.upstreamSynced': '동기화됨',
|
||||
'gitView.header.upstreamTooltip': '{target}와 비교됨.',
|
||||
'gitView.header.upstreamTooltipTracking': '{target}와 비교됨. 기본 동기화 배지는 계속 {tracking}을 반영합니다.',
|
||||
'gitView.history.actions.cancelButton': '취소',
|
||||
'gitView.history.actions.checkoutConfirm': '이 커밋을 detached HEAD로 체크아웃할까요?',
|
||||
'gitView.history.actions.cherryPickConfirm': '이 커밋을 현재 브랜치에 cherry-pick할까요?',
|
||||
'gitView.history.actions.conflictToastDescription': '충돌 파일: {files}. 수동으로 해결한 뒤 커밋하거나 git cherry-pick/revert --abort로 중단하세요.',
|
||||
'gitView.history.actions.conflictToastTitle': '충돌',
|
||||
'gitView.history.actions.confirmButton': '확인',
|
||||
'gitView.history.actions.mergeConfirm': '이 커밋을 현재 브랜치에 병합할까요?',
|
||||
'gitView.history.actions.rebaseConfirm': '현재 브랜치를 이 커밋 위로 rebase할까요?',
|
||||
'gitView.history.actions.resetHardConfirm': 'Hard reset — HEAD가 이동하고 커밋하지 않은 모든 변경 사항이 영구적으로 삭제됩니다.',
|
||||
'gitView.history.actions.resetMixedConfirm': 'Mixed reset — HEAD가 이동하고 변경 사항은 stage에서 내려갑니다.',
|
||||
'gitView.history.actions.resetSoftConfirm': 'Soft reset — HEAD가 이동하고 staged 변경 사항은 유지됩니다.',
|
||||
'gitView.history.actions.revertConfirm': '이 커밋의 revert 변경 사항을 stage할까요?',
|
||||
'gitView.history.actions.checkout': '체크아웃',
|
||||
'gitView.history.actions.cherryPick': 'Cherry-pick',
|
||||
'gitView.history.actions.createBranch': '여기에 브랜치 만들기',
|
||||
'gitView.history.actions.createBranchConfirm': '만들기',
|
||||
'gitView.history.actions.createBranchPlaceholder': '브랜치 이름',
|
||||
'gitView.history.actions.detachedHead': '체크아웃됨(detached HEAD)',
|
||||
'gitView.history.actions.merge': '현재 브랜치에 병합',
|
||||
'gitView.history.actions.rebase': '여기로 rebase',
|
||||
'gitView.history.actions.reset': 'Reset...',
|
||||
'gitView.history.actions.resetHard': 'Hard — 모든 변경 사항 삭제',
|
||||
'gitView.history.actions.resetHardConfirmButton': '변경 사항 삭제',
|
||||
'gitView.history.actions.resetMixed': 'Mixed — stage에서 내리기',
|
||||
'gitView.history.actions.resetSoft': 'Soft — staged 유지',
|
||||
'gitView.history.actions.revert': 'Revert',
|
||||
|
||||
'gitView.history.binary': '바이너리',
|
||||
'gitView.history.binaryNoDiff': '바이너리 파일 — diff 없음',
|
||||
'gitView.history.commitsPlaceholder': '커밋 검색',
|
||||
@@ -518,6 +546,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
'gitView.history.largeDiffTitle': '큰 diff({count}개 변경된 줄)',
|
||||
'gitView.history.loadingDiff': 'diff 로드 중…',
|
||||
'gitView.history.loadingFiles': '파일 로드 중…',
|
||||
'gitView.history.loadMore': '더 불러오기',
|
||||
'gitView.history.loadingMore': '로드 중...',
|
||||
'gitView.history.logSize100': '최근 100개',
|
||||
'gitView.history.logSize25': '최근 25개',
|
||||
'gitView.history.logSize50': '최근 50개',
|
||||
@@ -526,6 +556,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'gitView.history.renamedNoDiff': '이름 변경된 파일 — diff 미지원',
|
||||
'gitView.history.renderDiffAnyway': '그래도 렌더링',
|
||||
'gitView.history.title': '히스토리',
|
||||
'gitView.graph.title': '그래프',
|
||||
'gitView.integrate.checking': '확인 중…',
|
||||
'gitView.integrate.cherryPickAbortedToast': 'Cherry-pick이 중단되었습니다',
|
||||
'gitView.integrate.cherryPickConflictDescription': '충돌을 해결한 뒤 계속 진행하세요.',
|
||||
|
||||
@@ -1491,11 +1491,39 @@ export const dict: Record<I18nKey, string> = {
|
||||
'gitView.header.identityTooltip': 'Git identity',
|
||||
'gitView.header.noIdentity': 'No identity',
|
||||
'gitView.header.noProfiles': 'No profiles available to apply.',
|
||||
'gitView.header.repositoryViews': 'Widoki repozytorium',
|
||||
'gitView.header.removeRemoteAria': 'Remove Remote aria label',
|
||||
'gitView.header.removeRemoteTitle': 'Remove Remote Title',
|
||||
'gitView.header.upstreamSynced': 'zsynchronizowano',
|
||||
'gitView.header.upstreamTooltip': 'Porównano z {target}.',
|
||||
'gitView.header.upstreamTooltipTracking': 'Porównano z {target}. Główne wskaźniki synchronizacji nadal odzwierciedlają {tracking}.',
|
||||
'gitView.history.actions.cancelButton': 'Anuluj',
|
||||
'gitView.history.actions.checkoutConfirm': 'Przełączyć na ten commit jako detached HEAD?',
|
||||
'gitView.history.actions.cherryPickConfirm': 'Wykonać cherry-pick tego commitu na bieżącą gałąź?',
|
||||
'gitView.history.actions.conflictToastDescription': 'Konflikty w: {files}. Rozwiąż je ręcznie i wykonaj commit albo przerwij przez git cherry-pick/revert --abort.',
|
||||
'gitView.history.actions.conflictToastTitle': 'Konflikt',
|
||||
'gitView.history.actions.confirmButton': 'Potwierdź',
|
||||
'gitView.history.actions.mergeConfirm': 'Scalić ten commit z bieżącą gałęzią?',
|
||||
'gitView.history.actions.rebaseConfirm': 'Przebazować bieżącą gałąź na ten commit?',
|
||||
'gitView.history.actions.resetHardConfirm': 'Hard reset — HEAD zostanie przesunięty, a wszystkie niezatwierdzone zmiany trwale usunięte.',
|
||||
'gitView.history.actions.resetMixedConfirm': 'Mixed reset — HEAD zostanie przesunięty, a zmiany zostaną usunięte ze stage.',
|
||||
'gitView.history.actions.resetSoftConfirm': 'Soft reset — HEAD zostanie przesunięty, a staged zmiany zostaną zachowane.',
|
||||
'gitView.history.actions.revertConfirm': 'Przygotować revert tego commitu?',
|
||||
'gitView.history.actions.checkout': 'Checkout',
|
||||
'gitView.history.actions.cherryPick': 'Cherry-pick',
|
||||
'gitView.history.actions.createBranch': 'Utwórz gałąź tutaj',
|
||||
'gitView.history.actions.createBranchConfirm': 'Utwórz',
|
||||
'gitView.history.actions.createBranchPlaceholder': 'Nazwa gałęzi',
|
||||
'gitView.history.actions.detachedHead': 'Checkout wykonany (detached HEAD)',
|
||||
'gitView.history.actions.merge': 'Scal z bieżącą',
|
||||
'gitView.history.actions.rebase': 'Rebase na ten commit',
|
||||
'gitView.history.actions.reset': 'Reset...',
|
||||
'gitView.history.actions.resetHard': 'Hard — usuń wszystkie zmiany',
|
||||
'gitView.history.actions.resetHardConfirmButton': 'Usuń zmiany',
|
||||
'gitView.history.actions.resetMixed': 'Mixed — usuń ze stage',
|
||||
'gitView.history.actions.resetSoft': 'Soft — zachowaj staged',
|
||||
'gitView.history.actions.revert': 'Revert',
|
||||
|
||||
'gitView.history.binary': 'Binary',
|
||||
'gitView.history.binaryNoDiff': 'Binary file — no diff available',
|
||||
'gitView.history.commitsPlaceholder': 'Commits Placeholder',
|
||||
@@ -1506,6 +1534,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
'gitView.history.largeDiffTitle': 'Large diff ({count} changed lines)',
|
||||
'gitView.history.loadingDiff': 'Loading diff...',
|
||||
'gitView.history.loadingFiles': 'Loading files...',
|
||||
'gitView.history.loadMore': 'Załaduj więcej',
|
||||
'gitView.history.loadingMore': 'Ładowanie...',
|
||||
'gitView.history.logSize100': 'Log Size100',
|
||||
'gitView.history.logSize25': 'Log Size25',
|
||||
'gitView.history.logSize50': 'Log Size50',
|
||||
@@ -1514,6 +1544,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'gitView.history.renamedNoDiff': 'Renamed file — diff not supported',
|
||||
'gitView.history.renderDiffAnyway': 'Render anyway',
|
||||
'gitView.history.title': 'History',
|
||||
'gitView.graph.title': 'Graf',
|
||||
'gitView.integrate.checking': 'Sprawdzanie…',
|
||||
'gitView.integrate.cherryPickAbortedToast': 'Cherry-pick został przerwany',
|
||||
'gitView.integrate.cherryPickConflictDescription': 'Wykryto konflikt podczas cherry-pick.',
|
||||
|
||||
@@ -504,11 +504,39 @@ export const dict: Record<I18nKey, string> = {
|
||||
"gitView.header.identityTooltip": "Identidad de Git",
|
||||
"gitView.header.noIdentity": "Sem identidade",
|
||||
"gitView.header.noProfiles": "Não há perfiles disponíveis para aplicar.",
|
||||
"gitView.header.repositoryViews": "Visualizações do repositório",
|
||||
"gitView.header.removeRemoteAria": "Excluir remoto",
|
||||
"gitView.header.removeRemoteTitle": "Excluir remoto",
|
||||
"gitView.header.upstreamSynced": "sincronizado",
|
||||
"gitView.header.upstreamTooltip": "Comparado com {target}.",
|
||||
"gitView.header.upstreamTooltipTracking": "Comparado com {target}. Os indicadores principais de sincronização ainda refletem {tracking}.",
|
||||
"gitView.history.actions.cancelButton": "Cancelar",
|
||||
"gitView.history.actions.checkoutConfirm": "Fazer checkout deste commit como HEAD destacado?",
|
||||
"gitView.history.actions.cherryPickConfirm": "Aplicar cherry-pick deste commit na branch atual?",
|
||||
"gitView.history.actions.conflictToastDescription": "Conflitos em: {files}. Resolva manualmente e faça commit, ou aborte com git cherry-pick/revert --abort.",
|
||||
"gitView.history.actions.conflictToastTitle": "Conflito",
|
||||
"gitView.history.actions.confirmButton": "Confirmar",
|
||||
"gitView.history.actions.mergeConfirm": "Fazer merge deste commit na branch atual?",
|
||||
"gitView.history.actions.rebaseConfirm": "Fazer rebase da branch atual neste commit?",
|
||||
"gitView.history.actions.resetHardConfirm": "Reset hard — HEAD muda e todas as alterações sem commit são descartadas permanentemente.",
|
||||
"gitView.history.actions.resetMixedConfirm": "Reset mixed — HEAD muda e as alterações ficam fora do stage.",
|
||||
"gitView.history.actions.resetSoftConfirm": "Reset soft — HEAD muda e as alterações em stage são preservadas.",
|
||||
"gitView.history.actions.revertConfirm": "Preparar uma reversão deste commit?",
|
||||
"gitView.history.actions.checkout": "Checkout",
|
||||
"gitView.history.actions.cherryPick": "Cherry-pick",
|
||||
"gitView.history.actions.createBranch": "Criar branch aqui",
|
||||
"gitView.history.actions.createBranchConfirm": "Criar",
|
||||
"gitView.history.actions.createBranchPlaceholder": "Nome da branch",
|
||||
"gitView.history.actions.detachedHead": "Checkout concluído (HEAD destacado)",
|
||||
"gitView.history.actions.merge": "Merge na atual",
|
||||
"gitView.history.actions.rebase": "Rebase neste commit",
|
||||
"gitView.history.actions.reset": "Reset...",
|
||||
"gitView.history.actions.resetHard": "Hard — descartar todas as alterações",
|
||||
"gitView.history.actions.resetHardConfirmButton": "Descartar alterações",
|
||||
"gitView.history.actions.resetMixed": "Mixed — remover do stage",
|
||||
"gitView.history.actions.resetSoft": "Soft — manter em stage",
|
||||
"gitView.history.actions.revert": "Reverter",
|
||||
|
||||
"gitView.history.binary": "Binario",
|
||||
"gitView.history.binaryNoDiff": "Arquivo binário — diff não disponível",
|
||||
"gitView.history.commitsPlaceholder": "Buscar commits...",
|
||||
@@ -518,6 +546,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
"gitView.history.largeDiffTitle": "Diff grande ({count} linhas alteradas)",
|
||||
"gitView.history.loadingDiff": "Carregando diff...",
|
||||
"gitView.history.loadingFiles": "Carregando arquivos...",
|
||||
"gitView.history.loadMore": "Carregar mais",
|
||||
"gitView.history.loadingMore": "Carregando...",
|
||||
"gitView.history.logSize100": "100 commits",
|
||||
"gitView.history.logSize25": "25 commits",
|
||||
"gitView.history.logSize50": "50 commits",
|
||||
@@ -526,6 +556,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"gitView.history.renamedNoDiff": "Arquivo renomeado — diff não suportado",
|
||||
"gitView.history.renderDiffAnyway": "Renderizar mesmo assim",
|
||||
"gitView.history.title": "Histórico",
|
||||
"gitView.graph.title": "Grafo",
|
||||
"gitView.integrate.checking": "Verificando…",
|
||||
"gitView.integrate.cherryPickAbortedToast": "Cherry-pick abortado",
|
||||
"gitView.integrate.cherryPickConflictDescription": "Resuelve os conflitos de cherry-pick para continuar.",
|
||||
|
||||
@@ -504,11 +504,39 @@ export const dict: Record<I18nKey, string> = {
|
||||
"gitView.header.identityTooltip": "Ідентичність Git",
|
||||
"gitView.header.noIdentity": "Ідентичність не вибрано",
|
||||
"gitView.header.noProfiles": "Немає доступних профілів для застосування.",
|
||||
"gitView.header.repositoryViews": "Перегляди репозиторію",
|
||||
"gitView.header.removeRemoteAria": "Видалити remote",
|
||||
"gitView.header.removeRemoteTitle": "Видалити remote",
|
||||
"gitView.header.upstreamSynced": "синхронізовано",
|
||||
"gitView.header.upstreamTooltip": "Порівняно з {target}.",
|
||||
"gitView.header.upstreamTooltipTracking": "Порівняно з {target}. Основні індикатори синхронізації все ще відображають {tracking}.",
|
||||
"gitView.history.actions.cancelButton": "Скасувати",
|
||||
"gitView.history.actions.checkoutConfirm": "Перейти на цей коміт як detached HEAD?",
|
||||
"gitView.history.actions.cherryPickConfirm": "Застосувати cherry-pick цього коміту до поточної гілки?",
|
||||
"gitView.history.actions.conflictToastDescription": "Конфлікти у: {files}. Розв'яжіть їх вручну й закомітьте або скасуйте через git cherry-pick/revert --abort.",
|
||||
"gitView.history.actions.conflictToastTitle": "Конфлікт",
|
||||
"gitView.history.actions.confirmButton": "Підтвердити",
|
||||
"gitView.history.actions.mergeConfirm": "Змерджити цей коміт у поточну гілку?",
|
||||
"gitView.history.actions.rebaseConfirm": "Перебазувати поточну гілку на цей коміт?",
|
||||
"gitView.history.actions.resetHardConfirm": "Hard reset — HEAD переміститься, усі незакомічені зміни буде остаточно втрачено.",
|
||||
"gitView.history.actions.resetMixedConfirm": "Mixed reset — HEAD переміститься, зміни буде прибрано зі stage.",
|
||||
"gitView.history.actions.resetSoftConfirm": "Soft reset — HEAD переміститься, staged-зміни збережуться.",
|
||||
"gitView.history.actions.revertConfirm": "Підготувати revert цього коміту?",
|
||||
"gitView.history.actions.checkout": "Checkout",
|
||||
"gitView.history.actions.cherryPick": "Cherry-pick",
|
||||
"gitView.history.actions.createBranch": "Створити гілку тут",
|
||||
"gitView.history.actions.createBranchConfirm": "Створити",
|
||||
"gitView.history.actions.createBranchPlaceholder": "Назва гілки",
|
||||
"gitView.history.actions.detachedHead": "Checkout виконано (detached HEAD)",
|
||||
"gitView.history.actions.merge": "Merge у поточну",
|
||||
"gitView.history.actions.rebase": "Rebase на цей",
|
||||
"gitView.history.actions.reset": "Reset...",
|
||||
"gitView.history.actions.resetHard": "Hard — втратити всі зміни",
|
||||
"gitView.history.actions.resetHardConfirmButton": "Втратити зміни",
|
||||
"gitView.history.actions.resetMixed": "Mixed — прибрати зі stage",
|
||||
"gitView.history.actions.resetSoft": "Soft — зберегти staged",
|
||||
"gitView.history.actions.revert": "Revert",
|
||||
|
||||
"gitView.history.binary": "Бінарний",
|
||||
"gitView.history.binaryNoDiff": "Бінарний файл — diff недоступний",
|
||||
"gitView.history.commitsPlaceholder": "Пошук комітів",
|
||||
@@ -518,6 +546,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
"gitView.history.largeDiffTitle": "Великий diff ({count} змінених рядків)",
|
||||
"gitView.history.loadingDiff": "Завантаження diff...",
|
||||
"gitView.history.loadingFiles": "Завантаження файлів...",
|
||||
"gitView.history.loadMore": "Завантажити ще",
|
||||
"gitView.history.loadingMore": "Завантаження...",
|
||||
"gitView.history.logSize100": "Розмір журналу 100",
|
||||
"gitView.history.logSize25": "Розмір журналу 25",
|
||||
"gitView.history.logSize50": "Розмір журналу 50",
|
||||
@@ -526,6 +556,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"gitView.history.renamedNoDiff": "Перейменований файл — diff не підтримується",
|
||||
"gitView.history.renderDiffAnyway": "Показати все одно",
|
||||
"gitView.history.title": "Історія",
|
||||
"gitView.graph.title": "Граф",
|
||||
"gitView.integrate.checking": "Перевірка…",
|
||||
"gitView.integrate.cherryPickAbortedToast": "Cherry-pick перервано",
|
||||
"gitView.integrate.cherryPickConflictDescription": "Вирішіть конфлікти cherry-pick, щоб продовжити.",
|
||||
|
||||
@@ -504,11 +504,39 @@ export const dict: Record<I18nKey, string> = {
|
||||
'gitView.header.identityTooltip': 'Git 身份',
|
||||
'gitView.header.noIdentity': '无身份',
|
||||
'gitView.header.noProfiles': '没有可应用的配置。',
|
||||
'gitView.header.repositoryViews': '仓库视图',
|
||||
'gitView.header.removeRemoteAria': '移除远程 {name}',
|
||||
'gitView.header.removeRemoteTitle': '移除 {name}',
|
||||
'gitView.header.upstreamSynced': '已同步',
|
||||
'gitView.header.upstreamTooltip': '与 {target} 对比。',
|
||||
'gitView.header.upstreamTooltipTracking': '与 {target} 对比。主要同步徽标仍然反映 {tracking}。',
|
||||
'gitView.history.actions.cancelButton': '取消',
|
||||
'gitView.history.actions.checkoutConfirm': '要将此提交检出为 detached HEAD 吗?',
|
||||
'gitView.history.actions.cherryPickConfirm': '要将此提交 cherry-pick 到当前分支吗?',
|
||||
'gitView.history.actions.conflictToastDescription': '冲突文件:{files}。请手动解决并提交,或使用 git cherry-pick/revert --abort 中止。',
|
||||
'gitView.history.actions.conflictToastTitle': '冲突',
|
||||
'gitView.history.actions.confirmButton': '确认',
|
||||
'gitView.history.actions.mergeConfirm': '要将此提交合并到当前分支吗?',
|
||||
'gitView.history.actions.rebaseConfirm': '要将当前分支变基到此提交吗?',
|
||||
'gitView.history.actions.resetHardConfirm': 'Hard reset — HEAD 会移动,所有未提交更改将被永久丢弃。',
|
||||
'gitView.history.actions.resetMixedConfirm': 'Mixed reset — HEAD 会移动,更改将取消暂存。',
|
||||
'gitView.history.actions.resetSoftConfirm': 'Soft reset — HEAD 会移动,已暂存更改会保留。',
|
||||
'gitView.history.actions.revertConfirm': '要暂存对此提交的 revert 吗?',
|
||||
'gitView.history.actions.checkout': '检出',
|
||||
'gitView.history.actions.cherryPick': 'Cherry-pick',
|
||||
'gitView.history.actions.createBranch': '在此创建分支',
|
||||
'gitView.history.actions.createBranchConfirm': '创建',
|
||||
'gitView.history.actions.createBranchPlaceholder': '分支名称',
|
||||
'gitView.history.actions.detachedHead': '已检出(detached HEAD)',
|
||||
'gitView.history.actions.merge': '合并到当前分支',
|
||||
'gitView.history.actions.rebase': '变基到此处',
|
||||
'gitView.history.actions.reset': 'Reset...',
|
||||
'gitView.history.actions.resetHard': 'Hard — 丢弃所有更改',
|
||||
'gitView.history.actions.resetHardConfirmButton': '丢弃更改',
|
||||
'gitView.history.actions.resetMixed': 'Mixed — 取消暂存更改',
|
||||
'gitView.history.actions.resetSoft': 'Soft — 保留暂存',
|
||||
'gitView.history.actions.revert': 'Revert',
|
||||
|
||||
'gitView.history.binary': '二进制',
|
||||
'gitView.history.binaryNoDiff': '二进制文件,无法显示差异',
|
||||
'gitView.history.commitsPlaceholder': '提交数',
|
||||
@@ -518,6 +546,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
'gitView.history.largeDiffTitle': '大型差异({count} 行变更)',
|
||||
'gitView.history.loadingDiff': '正在加载差异...',
|
||||
'gitView.history.loadingFiles': '正在加载文件...',
|
||||
'gitView.history.loadMore': '加载更多',
|
||||
'gitView.history.loadingMore': '加载中...',
|
||||
'gitView.history.logSize100': '100 个提交',
|
||||
'gitView.history.logSize25': '25 个提交',
|
||||
'gitView.history.logSize50': '50 个提交',
|
||||
@@ -526,6 +556,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'gitView.history.renamedNoDiff': '已重命名文件,不支持显示差异',
|
||||
'gitView.history.renderDiffAnyway': '仍然渲染',
|
||||
'gitView.history.title': '历史',
|
||||
'gitView.graph.title': '图谱',
|
||||
'gitView.integrate.checking': '检查中…',
|
||||
'gitView.integrate.cherryPickAbortedToast': '已中止 cherry-pick',
|
||||
'gitView.integrate.cherryPickConflictDescription': '请先解决冲突,然后继续。',
|
||||
|
||||
@@ -504,12 +504,41 @@ export const dict: Record<I18nKey, string> = {
|
||||
'gitView.header.identityTooltip': 'Git 身分',
|
||||
'gitView.header.noIdentity': '無身分',
|
||||
'gitView.header.noProfiles': '沒有可套用的設定。',
|
||||
'gitView.header.repositoryViews': '儲存庫檢視',
|
||||
'gitView.header.removeRemoteAria': '移除遠端 {name}',
|
||||
'gitView.header.removeRemoteTitle': '移除 {name}',
|
||||
'gitView.header.upstreamSynced': '已同步',
|
||||
'gitView.header.upstreamTooltip': '與 {target} 比較。',
|
||||
'gitView.header.upstreamTooltipTracking': '與 {target} 比較。主要同步徽章仍反映 {tracking}。',
|
||||
'gitView.history.binary': '二進位',
|
||||
'gitView.history.actions.cancelButton': '取消',
|
||||
'gitView.history.actions.checkout': '簽出',
|
||||
'gitView.history.actions.checkoutConfirm': '要將此提交簽出為 detached HEAD 嗎?',
|
||||
'gitView.history.actions.cherryPick': 'Cherry-pick',
|
||||
'gitView.history.actions.cherryPickConfirm': '要將此提交 cherry-pick 到目前分支嗎?',
|
||||
'gitView.history.actions.conflictToastDescription': '衝突檔案:{files}。請手動解決並提交,或使用 git cherry-pick/revert --abort 中止。',
|
||||
'gitView.history.actions.conflictToastTitle': '衝突',
|
||||
'gitView.history.actions.confirmButton': '確認',
|
||||
'gitView.history.actions.createBranch': '在此建立分支',
|
||||
'gitView.history.actions.createBranchConfirm': '建立',
|
||||
'gitView.history.actions.createBranchPlaceholder': '分支名稱',
|
||||
'gitView.history.actions.detachedHead': '已簽出(detached HEAD)',
|
||||
'gitView.history.actions.merge': '合併到目前分支',
|
||||
'gitView.history.actions.mergeConfirm': '要將此提交合併到目前分支嗎?',
|
||||
'gitView.history.actions.rebase': 'Rebase 到此處',
|
||||
'gitView.history.actions.rebaseConfirm': '要將目前分支 rebase 到此提交嗎?',
|
||||
'gitView.history.actions.reset': 'Reset...',
|
||||
'gitView.history.actions.resetHard': 'Hard — 捨棄所有變更',
|
||||
'gitView.history.actions.resetHardConfirm': 'Hard reset — HEAD 會移動,所有未提交變更將永久捨棄。',
|
||||
'gitView.history.actions.resetHardConfirmButton': '捨棄變更',
|
||||
'gitView.history.actions.resetMixed': 'Mixed — 取消暫存變更',
|
||||
'gitView.history.actions.resetMixedConfirm': 'Mixed reset — HEAD 會移動,變更將取消暫存。',
|
||||
'gitView.history.actions.resetSoft': 'Soft — 保留暫存',
|
||||
'gitView.history.actions.resetSoftConfirm': 'Soft reset — HEAD 會移動,已暫存變更會保留。',
|
||||
'gitView.history.actions.revert': 'Revert',
|
||||
'gitView.history.actions.revertConfirm': '要暫存對此提交的 revert 嗎?',
|
||||
'gitView.history.loadMore': '載入更多',
|
||||
'gitView.history.loadingMore': '載入中...',
|
||||
'gitView.history.binaryNoDiff': '二進位檔案 — 無可用 diff',
|
||||
'gitView.history.commitsPlaceholder': '提交數',
|
||||
'gitView.history.copySha': '複製 SHA',
|
||||
@@ -526,6 +555,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'gitView.history.renamedNoDiff': '已重新命名檔案 — 不支援 diff',
|
||||
'gitView.history.renderDiffAnyway': '仍然渲染',
|
||||
'gitView.history.title': '歷史紀錄',
|
||||
'gitView.graph.title': '圖譜',
|
||||
'gitView.integrate.checking': '檢查中…',
|
||||
'gitView.integrate.cherryPickAbortedToast': '已中止 cherry-pick',
|
||||
'gitView.integrate.cherryPickConflictDescription': '請先解決衝突,然後繼續。',
|
||||
|
||||
Reference in New Issue
Block a user