feat: phase 6 of git implementation

This commit is contained in:
2026-02-16 15:34:48 +01:00
parent 339e513a2d
commit e9743cb70f
8 changed files with 249 additions and 2 deletions

View File

@@ -348,6 +348,46 @@ describe('GitEngine', () => {
});
});
describe('getRemoteState', () => {
it('should return upstream tracking and ahead/behind counts when tracking branch exists', async () => {
mockStatus.mockResolvedValue({
current: 'main',
tracking: 'origin/main',
ahead: 2,
behind: 1,
});
const result = await gitEngine.getRemoteState('/tmp/project');
expect(result).toEqual({
localBranch: 'main',
upstreamBranch: 'origin/main',
hasUpstream: true,
ahead: 2,
behind: 1,
});
});
it('should return no-upstream state when tracking branch is missing', async () => {
mockStatus.mockResolvedValue({
current: 'main',
tracking: undefined,
ahead: 0,
behind: 0,
});
const result = await gitEngine.getRemoteState('/tmp/project');
expect(result).toEqual({
localBranch: 'main',
upstreamBranch: null,
hasUpstream: false,
ahead: 0,
behind: 0,
});
});
});
describe('ensureGitignore', () => {
it('should create .gitignore with default system metadata entries when missing', async () => {
mockReadFile.mockRejectedValue(new Error('ENOENT'));

View File

@@ -147,6 +147,7 @@ const mockGitEngine = {
getDiff: vi.fn(),
getDiffContent: vi.fn(),
getHistory: vi.fn(),
getRemoteState: vi.fn(),
fetch: vi.fn(),
pull: vi.fn(),
push: vi.fn(),
@@ -361,6 +362,29 @@ describe('IPC Handlers', () => {
});
});
describe('git:remoteState', () => {
it('should pass project path to GitEngine.getRemoteState', async () => {
mockGitEngine.getRemoteState.mockResolvedValue({
localBranch: 'main',
upstreamBranch: 'origin/main',
hasUpstream: true,
ahead: 2,
behind: 1,
});
const result = await invokeHandler('git:remoteState', '/repo');
expect(mockGitEngine.getRemoteState).toHaveBeenCalledWith('/repo');
expect(result).toEqual({
localBranch: 'main',
upstreamBranch: 'origin/main',
hasUpstream: true,
ahead: 2,
behind: 1,
});
});
});
describe('git:diffContent', () => {
it('should pass project path and file path to GitEngine.getDiffContent', async () => {
mockGitEngine.getDiffContent.mockResolvedValue({

View File

@@ -33,6 +33,7 @@ describe('GitSidebar', () => {
checkAvailability: vi.fn().mockResolvedValue({ gitFound: true, version: '2.49.0' }),
getRepoState: vi.fn().mockResolvedValue({ isRepo: false, hasRemote: false }),
getStatus: vi.fn().mockResolvedValue({ files: [], counts: { untracked: 0, modified: 0, deleted: 0, renamed: 0, staged: 0, total: 0 } }),
getRemoteState: vi.fn().mockResolvedValue({ localBranch: null, upstreamBranch: null, hasUpstream: false, ahead: 0, behind: 0 }),
getDiff: vi.fn().mockResolvedValue({ filePath: 'posts/a.md', patch: 'diff --git a/posts/a.md b/posts/a.md' }),
getDiffContent: vi.fn().mockResolvedValue({ filePath: 'posts/a.md', original: '', modified: '' }),
getCommitDiffContent: vi.fn().mockResolvedValue({ commitHash: 'abc123', original: '', modified: '' }),
@@ -637,6 +638,69 @@ describe('GitSidebar', () => {
});
});
it('renders upstream branch relation with ahead/behind indicators', async () => {
(window as any).electronAPI.git.getRepoState = vi.fn().mockResolvedValue({
isRepo: true,
rootPath: '/repo/path',
currentBranch: 'main',
hasRemote: true,
});
(window as any).electronAPI.git.getRemoteState = vi.fn().mockResolvedValue({
localBranch: 'main',
upstreamBranch: 'origin/main',
hasUpstream: true,
ahead: 2,
behind: 1,
});
render(<GitSidebar />);
expect(await screen.findByText('main → origin/main')).toBeInTheDocument();
expect(screen.getByText('ahead 2 / behind 1')).toBeInTheDocument();
});
it('polls remote fetch/state periodically when repository has a remote', async () => {
vi.useFakeTimers();
(window as any).electronAPI.git.getRepoState = vi.fn().mockResolvedValue({
isRepo: true,
rootPath: '/repo/path',
currentBranch: 'main',
hasRemote: true,
});
(window as any).electronAPI.git.getRemoteState = vi.fn().mockResolvedValue({
localBranch: 'main',
upstreamBranch: 'origin/main',
hasUpstream: true,
ahead: 0,
behind: 0,
});
(window as any).electronAPI.git.fetch = vi.fn().mockResolvedValue({ success: true });
try {
render(<GitSidebar />);
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
expect((window as any).electronAPI.git.getRemoteState).toHaveBeenCalledTimes(1);
expect((window as any).electronAPI.git.fetch).toHaveBeenCalledTimes(0);
await act(async () => {
vi.advanceTimersByTime(30000);
await Promise.resolve();
await Promise.resolve();
});
expect((window as any).electronAPI.git.fetch).toHaveBeenCalledTimes(1);
expect((window as any).electronAPI.git.getRemoteState).toHaveBeenCalledTimes(2);
} finally {
vi.useRealTimers();
}
});
it('polls repository status on an interval and prevents overlapping in-flight requests', async () => {
vi.useFakeTimers();