feat: git log as panel in the panel

This commit is contained in:
2026-02-17 13:13:55 +01:00
parent 5c0dbaff71
commit b13eba025a
9 changed files with 487 additions and 39 deletions

View File

@@ -348,6 +348,47 @@ describe('GitEngine', () => {
});
});
describe('getFileHistory', () => {
it('should return commits for a specific file path', async () => {
mockLog.mockResolvedValue({
all: [
{
hash: 'abc123def456',
date: '2026-02-16T10:00:00.000Z',
message: 'docs: update first post',
author_name: 'Dev One',
},
{
hash: '789fed654321',
date: '2026-02-15T09:00:00.000Z',
message: 'feat: add frontmatter field',
author_name: 'Dev Two',
},
],
});
const result = await gitEngine.getFileHistory('/tmp/project', 'posts/2026/02/first-post.md', 50);
expect(mockLog).toHaveBeenCalledWith(['--max-count', '50', '--', 'posts/2026/02/first-post.md']);
expect(result).toEqual([
{
hash: 'abc123def456',
shortHash: 'abc123d',
date: '2026-02-16T10:00:00.000Z',
subject: 'docs: update first post',
author: 'Dev One',
},
{
hash: '789fed654321',
shortHash: '789fed6',
date: '2026-02-15T09:00:00.000Z',
subject: 'feat: add frontmatter field',
author: 'Dev Two',
},
]);
});
});
describe('getRemoteState', () => {
it('should return upstream tracking and ahead/behind counts when tracking branch exists', async () => {
mockStatus.mockResolvedValue({
@@ -602,6 +643,15 @@ describe('GitEngine', () => {
});
describe('pruneLfsCache', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-02-16T12:00:00.000Z'));
});
afterEach(() => {
vi.useRealTimers();
});
it('should run git lfs prune with verify-remote and aggressive recency defaults', async () => {
mockLog.mockResolvedValue({
all: [

View File

@@ -0,0 +1,136 @@
import React from 'react';
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { act, render, screen } from '@testing-library/react';
import { Panel } from '../../../src/renderer/components/Panel/Panel';
import { useAppStore } from '../../../src/renderer/store';
import type { PostData, MediaData } from '../../../src/main/shared/electronApi';
const createPost = (overrides: Partial<PostData> = {}): PostData => ({
id: 'post-1',
projectId: 'project-1',
title: 'First Post',
slug: 'first-post',
content: 'Hello',
status: 'draft',
createdAt: '2026-02-01T08:00:00.000Z',
updatedAt: '2026-02-01T08:00:00.000Z',
tags: [],
categories: ['article'],
...overrides,
});
const createMedia = (overrides: Partial<MediaData> = {}): MediaData => ({
id: 'media-1',
projectId: 'project-1',
filename: 'image-1.jpg',
originalName: 'image-1.jpg',
mimeType: 'image/jpeg',
size: 123,
createdAt: '2026-02-01T08:00:00.000Z',
updatedAt: '2026-02-01T08:00:00.000Z',
tags: [],
...overrides,
});
describe('Panel', () => {
beforeEach(() => {
vi.clearAllMocks();
(window as any).electronAPI = {
...(window as any).electronAPI,
git: {
...(window as any).electronAPI?.git,
getFileHistory: vi.fn().mockResolvedValue([]),
},
media: {
...(window as any).electronAPI?.media,
getFilePath: vi.fn().mockResolvedValue('/repo/path/media/2026/02/image-1.jpg'),
},
posts: {
...(window as any).electronAPI?.posts,
get: vi.fn().mockResolvedValue(null),
},
};
useAppStore.setState({
panelVisible: true,
tasks: [],
activeProject: {
id: 'project-1',
name: 'Test Project',
slug: 'test-project',
isActive: true,
dataPath: '/repo/path',
createdAt: '2026-02-01T08:00:00.000Z',
updatedAt: '2026-02-01T08:00:00.000Z',
},
posts: [createPost()],
media: [createMedia()],
tabs: [{ type: 'post', id: 'post-1', isTransient: false }],
activeTabId: 'post-1',
});
});
afterEach(() => {
useAppStore.setState({ panelVisible: false });
});
it('renders a Git Log tab label instead of Sync Log', () => {
render(<Panel />);
expect(screen.getByRole('tab', { name: 'Git Log' })).toBeInTheDocument();
expect(screen.queryByText('Sync Log')).not.toBeInTheDocument();
});
it('loads git history for the focused item and updates when active editor changes', async () => {
const getFileHistory = vi.fn()
.mockResolvedValueOnce([
{
hash: 'abc123def456',
shortHash: 'abc123d',
date: '2026-02-16T10:00:00.000Z',
subject: 'docs: update first post',
author: 'Dev One',
},
])
.mockResolvedValueOnce([
{
hash: 'def456abc123',
shortHash: 'def456a',
date: '2026-02-17T09:00:00.000Z',
subject: 'chore: replace media file',
author: 'Dev Two',
},
]);
(window as any).electronAPI.git.getFileHistory = getFileHistory;
render(<Panel />);
await vi.waitFor(() => {
expect(getFileHistory).toHaveBeenCalledWith('/repo/path', 'posts/2026/02/first-post.md', 50);
});
act(() => {
useAppStore.setState({
tabs: [{ type: 'media', id: 'media-1', isTransient: false }],
activeTabId: 'media-1',
});
});
await vi.waitFor(() => {
expect(getFileHistory).toHaveBeenCalledWith('/repo/path', 'media/2026/02/image-1.jpg', 50);
});
});
it('disables Git Log tab when focused tab is not a post or media editor', () => {
useAppStore.setState({
tabs: [{ type: 'settings', id: 'settings', isTransient: false }],
activeTabId: 'settings',
});
render(<Panel />);
expect(screen.getByRole('tab', { name: 'Git Log' })).toHaveAttribute('aria-disabled', 'true');
});
});

View File

@@ -51,6 +51,7 @@ Object.defineProperty(globalThis, 'window', {
getDiff: vi.fn(),
getDiffContent: vi.fn(),
getHistory: vi.fn(),
getFileHistory: vi.fn(),
init: vi.fn(),
},
posts: {
@@ -83,6 +84,7 @@ Object.defineProperty(globalThis, 'window', {
regenerateThumbnails: vi.fn(),
search: vi.fn(),
getUrl: vi.fn(),
getFilePath: vi.fn(),
},
sync: {
configure: vi.fn(),