* 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>
113 lines
4.1 KiB
JavaScript
113 lines
4.1 KiB
JavaScript
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
|
|
|
const gitService = {
|
|
stageGitFiles: mock(),
|
|
unstageGitFiles: mock(),
|
|
checkoutCommit: mock(),
|
|
cherryPick: mock(),
|
|
revertCommit: mock(),
|
|
resetToCommit: mock(),
|
|
};
|
|
|
|
mock.module('./gitService', () => gitService);
|
|
|
|
const { handleStandardGitBridgeMessage } = await import('./bridge-git-runtime');
|
|
|
|
describe('bridge git runtime index mutations', () => {
|
|
beforeEach(() => {
|
|
gitService.stageGitFiles.mockReset();
|
|
gitService.unstageGitFiles.mockReset();
|
|
gitService.checkoutCommit.mockReset();
|
|
gitService.cherryPick.mockReset();
|
|
gitService.revertCommit.mockReset();
|
|
gitService.resetToCommit.mockReset();
|
|
});
|
|
|
|
it('accepts legacy stage path payloads', async () => {
|
|
const response = await handleStandardGitBridgeMessage({
|
|
id: '1',
|
|
type: 'api:git/stage',
|
|
payload: { directory: '/repo', path: 'a.ts' },
|
|
});
|
|
|
|
expect(response).toEqual({ id: '1', type: 'api:git/stage', success: true, data: { success: true } });
|
|
expect(gitService.stageGitFiles).toHaveBeenCalledWith('/repo', ['a.ts']);
|
|
});
|
|
|
|
it('accepts bulk stage paths payloads', async () => {
|
|
const response = await handleStandardGitBridgeMessage({
|
|
id: '1',
|
|
type: 'api:git/stage',
|
|
payload: { directory: '/repo', paths: ['a.ts', 'b.ts'] },
|
|
});
|
|
|
|
expect(response?.success).toBe(true);
|
|
expect(gitService.stageGitFiles).toHaveBeenCalledWith('/repo', ['a.ts', 'b.ts']);
|
|
});
|
|
|
|
it('accepts legacy unstage path payloads', async () => {
|
|
const response = await handleStandardGitBridgeMessage({
|
|
id: '1',
|
|
type: 'api:git/unstage',
|
|
payload: { directory: '/repo', path: 'a.ts' },
|
|
});
|
|
|
|
expect(response).toEqual({ id: '1', type: 'api:git/unstage', success: true, data: { success: true } });
|
|
expect(gitService.unstageGitFiles).toHaveBeenCalledWith('/repo', ['a.ts']);
|
|
});
|
|
|
|
it('accepts bulk unstage paths payloads', async () => {
|
|
const response = await handleStandardGitBridgeMessage({
|
|
id: '1',
|
|
type: 'api:git/unstage',
|
|
payload: { directory: '/repo', paths: ['a.ts', 'b.ts'] },
|
|
});
|
|
|
|
expect(response?.success).toBe(true);
|
|
expect(gitService.unstageGitFiles).toHaveBeenCalledWith('/repo', ['a.ts', 'b.ts']);
|
|
});
|
|
|
|
it('rejects invalid path payloads', async () => {
|
|
const response = await handleStandardGitBridgeMessage({
|
|
id: '1',
|
|
type: 'api:git/stage',
|
|
payload: { directory: '/repo', paths: [' ', null] },
|
|
});
|
|
|
|
expect(response?.success).toBe(false);
|
|
expect(gitService.stageGitFiles).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects invalid commit hashes before commit actions reach git service', async () => {
|
|
const checkoutResponse = await handleStandardGitBridgeMessage({
|
|
id: '1',
|
|
type: 'api:git/checkout-commit',
|
|
payload: { directory: '/repo', hash: 'HEAD' },
|
|
});
|
|
const cherryPickResponse = await handleStandardGitBridgeMessage({
|
|
id: '2',
|
|
type: 'api:git/cherry-pick',
|
|
payload: { directory: '/repo', hash: '--abort' },
|
|
});
|
|
const revertResponse = await handleStandardGitBridgeMessage({
|
|
id: '3',
|
|
type: 'api:git/revert-commit',
|
|
payload: { directory: '/repo', hash: '--continue' },
|
|
});
|
|
const resetResponse = await handleStandardGitBridgeMessage({
|
|
id: '4',
|
|
type: 'api:git/reset-to-commit',
|
|
payload: { directory: '/repo', hash: '--hard', mode: 'mixed' },
|
|
});
|
|
|
|
expect(checkoutResponse).toEqual({ id: '1', type: 'api:git/checkout-commit', success: false, error: 'Invalid commit hash' });
|
|
expect(cherryPickResponse).toEqual({ id: '2', type: 'api:git/cherry-pick', success: false, error: 'Invalid commit hash' });
|
|
expect(revertResponse).toEqual({ id: '3', type: 'api:git/revert-commit', success: false, error: 'Invalid commit hash' });
|
|
expect(resetResponse).toEqual({ id: '4', type: 'api:git/reset-to-commit', success: false, error: 'Invalid commit hash' });
|
|
expect(gitService.checkoutCommit).not.toHaveBeenCalled();
|
|
expect(gitService.cherryPick).not.toHaveBeenCalled();
|
|
expect(gitService.revertCommit).not.toHaveBeenCalled();
|
|
expect(gitService.resetToCommit).not.toHaveBeenCalled();
|
|
});
|
|
});
|