feat: git pull now updates db

This commit is contained in:
2026-02-22 13:49:17 +01:00
parent ce6422a021
commit a29143d6dd
5 changed files with 560 additions and 2 deletions

View File

@@ -2957,4 +2957,87 @@ Content with [link](/posts/other-post)`);
expect(ftsInserts.length).toBeGreaterThanOrEqual(2);
});
});
describe('reconcilePublishedPostsFromGitChanges', () => {
it('should process added and modified markdown files as published posts', async () => {
postEngine.setProjectContext('default', '/repo');
const existingPublishedPath = '/repo/posts/2026/02/existing-post.md';
mockPosts.set('published-existing', {
id: 'published-existing',
projectId: 'default',
title: 'Existing Post',
slug: 'existing-post',
excerpt: null,
content: null,
status: 'published',
author: null,
createdAt: new Date('2026-02-01T10:00:00.000Z'),
updatedAt: new Date('2026-02-01T10:00:00.000Z'),
publishedAt: new Date('2026-02-01T10:00:00.000Z'),
filePath: existingPublishedPath,
checksum: 'old-checksum',
tags: '[]',
categories: '[]',
});
mockFiles.set(existingPublishedPath, `---\nid: published-existing\ntitle: Existing Post Updated\nslug: existing-post\nstatus: published\ncreatedAt: 2026-02-01T10:00:00.000Z\nupdatedAt: 2026-02-22T10:00:00.000Z\ntags:\n - synced\ncategories:\n - updates\n---\nUpdated content`);
mockFiles.set('/repo/posts/2026/02/new-from-pull.md', `---\nid: new-from-pull-id\ntitle: New From Pull\nslug: new-from-pull\nstatus: published\ncreatedAt: 2026-02-22T09:00:00.000Z\nupdatedAt: 2026-02-22T09:00:00.000Z\ntags:\n - new\ncategories:\n - updates\n---\nBrand new post content`);
const emitSpy = vi.spyOn(postEngine, 'emit');
const result = await postEngine.reconcilePublishedPostsFromGitChanges('/repo', [
{ status: 'modified', path: 'posts/2026/02/existing-post.md' },
{ status: 'added', path: 'posts/2026/02/new-from-pull.md' },
]);
expect(mockLocalDb.update).toHaveBeenCalled();
expect(mockLocalDb.insert).toHaveBeenCalled();
expect(emitSpy).toHaveBeenCalledWith('postUpdated', expect.objectContaining({ id: 'published-existing' }));
expect(emitSpy).toHaveBeenCalledWith('postCreated', expect.objectContaining({ slug: 'new-from-pull', status: 'published' }));
expect(result.created).toBe(1);
expect(result.updated).toBe(1);
expect(result.deleted).toBe(0);
expect(result.processedFiles).toBe(2);
});
it('should ignore draft posts when matching file paths appear in git changes', async () => {
postEngine.setProjectContext('default', '/repo');
const draftPath = '/repo/posts/2026/02/draft-post.md';
mockPosts.set('draft-post', {
id: 'draft-post',
projectId: 'default',
title: 'Draft Post',
slug: 'draft-post',
excerpt: null,
content: 'Draft content',
status: 'draft',
author: null,
createdAt: new Date('2026-02-01T10:00:00.000Z'),
updatedAt: new Date('2026-02-01T10:00:00.000Z'),
publishedAt: null,
filePath: draftPath,
checksum: 'draft-checksum',
tags: '[]',
categories: '[]',
});
mockFiles.set(draftPath, `---\nid: draft-post\ntitle: Draft Post From File\nslug: draft-post\nstatus: published\ncreatedAt: 2026-02-01T10:00:00.000Z\nupdatedAt: 2026-02-22T10:00:00.000Z\n---\nShould be ignored`);
const emitSpy = vi.spyOn(postEngine, 'emit');
const result = await postEngine.reconcilePublishedPostsFromGitChanges('/repo', [
{ status: 'modified', path: 'posts/2026/02/draft-post.md' },
]);
expect(mockLocalDb.update).not.toHaveBeenCalled();
expect(mockLocalDb.insert).not.toHaveBeenCalled();
expect(emitSpy).not.toHaveBeenCalledWith('postUpdated', expect.objectContaining({ id: 'draft-post' }));
expect(result.created).toBe(0);
expect(result.updated).toBe(0);
expect(result.deleted).toBe(0);
expect(result.processedFiles).toBe(0);
});
});
});

View File

@@ -44,6 +44,7 @@ const mockPostEngine = {
on: vi.fn(),
setProjectContext: vi.fn(),
setSearchLanguage: vi.fn(),
reconcilePublishedPostsFromGitChanges: vi.fn(),
createPost: vi.fn(),
updatePost: vi.fn(),
deletePost: vi.fn(),
@@ -159,6 +160,8 @@ const mockPostMediaEngine = {
const mockGitEngine = {
checkAvailability: vi.fn(),
getHeadCommit: vi.fn(),
getChangedPostFilesBetween: vi.fn(),
getRepoState: vi.fn(),
getStatus: vi.fn(),
getDiff: vi.fn(),
@@ -549,12 +552,58 @@ describe('IPC Handlers', () => {
});
describe('git:pull', () => {
it('should pass project path to GitEngine.pull', async () => {
it('should reconcile published posts from pulled post file changes when pull succeeds', async () => {
mockGitEngine.getHeadCommit
.mockResolvedValueOnce('before-head')
.mockResolvedValueOnce('after-head');
mockGitEngine.pull.mockResolvedValue({ success: true });
mockGitEngine.getChangedPostFilesBetween.mockResolvedValue([
{ status: 'modified', path: 'posts/2026/02/existing.md' },
{ status: 'added', path: 'posts/2026/02/new-post.md' },
]);
mockPostEngine.reconcilePublishedPostsFromGitChanges.mockResolvedValue({
created: 1,
updated: 1,
deleted: 0,
processedFiles: 2,
});
const result = await invokeHandler('git:pull', '/repo');
expect(mockGitEngine.getHeadCommit).toHaveBeenNthCalledWith(1, '/repo');
expect(mockGitEngine.pull).toHaveBeenCalledWith('/repo');
expect(mockGitEngine.getHeadCommit).toHaveBeenNthCalledWith(2, '/repo');
expect(mockGitEngine.getChangedPostFilesBetween).toHaveBeenCalledWith('/repo', 'before-head', 'after-head');
expect(mockPostEngine.reconcilePublishedPostsFromGitChanges).toHaveBeenCalledWith('/repo', [
{ status: 'modified', path: 'posts/2026/02/existing.md' },
{ status: 'added', path: 'posts/2026/02/new-post.md' },
]);
expect(result).toEqual({ success: true });
});
it('should skip reconciliation when pull fails', async () => {
mockGitEngine.getHeadCommit.mockResolvedValue('before-head');
mockGitEngine.pull.mockResolvedValue({ success: false, code: 'conflict' });
const result = await invokeHandler('git:pull', '/repo');
expect(mockGitEngine.pull).toHaveBeenCalledWith('/repo');
expect(mockGitEngine.getChangedPostFilesBetween).not.toHaveBeenCalled();
expect(mockPostEngine.reconcilePublishedPostsFromGitChanges).not.toHaveBeenCalled();
expect(result).toEqual({ success: false, code: 'conflict' });
});
it('should skip reconciliation when pull does not change HEAD', async () => {
mockGitEngine.getHeadCommit
.mockResolvedValueOnce('same-head')
.mockResolvedValueOnce('same-head');
mockGitEngine.pull.mockResolvedValue({ success: true });
const result = await invokeHandler('git:pull', '/repo');
expect(mockGitEngine.pull).toHaveBeenCalledWith('/repo');
expect(mockGitEngine.getChangedPostFilesBetween).not.toHaveBeenCalled();
expect(mockPostEngine.reconcilePublishedPostsFromGitChanges).not.toHaveBeenCalled();
expect(result).toEqual({ success: true });
});
});