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:
Erman HAVUÇ
2026-05-27 00:13:25 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent cc3d1bd63c
commit 52ffe9daef
26 changed files with 2373 additions and 111 deletions
+31
View File
@@ -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': '충돌을 해결한 뒤 계속 진행하세요.',