feat: phase 6 of git implementation
This commit is contained in:
@@ -69,6 +69,14 @@ export interface GitHistoryEntry {
|
|||||||
syncStatus?: GitHistorySyncStatus;
|
syncStatus?: GitHistorySyncStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GitRemoteStateDto {
|
||||||
|
localBranch: string | null;
|
||||||
|
upstreamBranch: string | null;
|
||||||
|
hasUpstream: boolean;
|
||||||
|
ahead: number;
|
||||||
|
behind: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type GitHistorySyncStatus = 'both' | 'local-only' | 'remote-only';
|
export type GitHistorySyncStatus = 'both' | 'local-only' | 'remote-only';
|
||||||
|
|
||||||
export type GitInitPhase =
|
export type GitInitPhase =
|
||||||
@@ -711,6 +719,26 @@ export class GitEngine {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getRemoteState(projectPath: string): Promise<GitRemoteStateDto> {
|
||||||
|
const git = simpleGit(projectPath);
|
||||||
|
const status = await git.status();
|
||||||
|
|
||||||
|
const localBranch = typeof status.current === 'string' && status.current.trim().length > 0
|
||||||
|
? status.current
|
||||||
|
: null;
|
||||||
|
const upstreamBranch = typeof status.tracking === 'string' && status.tracking.trim().length > 0
|
||||||
|
? status.tracking
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
localBranch,
|
||||||
|
upstreamBranch,
|
||||||
|
hasUpstream: Boolean(upstreamBranch),
|
||||||
|
ahead: typeof status.ahead === 'number' ? status.ahead : Number(status.ahead ?? 0),
|
||||||
|
behind: typeof status.behind === 'number' ? status.behind : Number(status.behind ?? 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async fetch(projectPath: string): Promise<GitActionResult> {
|
async fetch(projectPath: string): Promise<GitActionResult> {
|
||||||
const git = this.createNonInteractiveGit(projectPath);
|
const git = this.createNonInteractiveGit(projectPath);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ export function registerIpcHandlers(): void {
|
|||||||
return engine.getHistory(projectPath, limit);
|
return engine.getHistory(projectPath, limit);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
safeHandle('git:remoteState', async (_, projectPath: string) => {
|
||||||
|
const engine = getGitEngine();
|
||||||
|
return engine.getRemoteState(projectPath);
|
||||||
|
});
|
||||||
|
|
||||||
safeHandle('git:fetch', async (_, projectPath: string) => {
|
safeHandle('git:fetch', async (_, projectPath: string) => {
|
||||||
const engine = getGitEngine();
|
const engine = getGitEngine();
|
||||||
return engine.fetch(projectPath);
|
return engine.fetch(projectPath);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const electronAPI: ElectronAPI = {
|
|||||||
getDiffContent: (projectPath: string, filePath: string) => ipcRenderer.invoke('git:diffContent', projectPath, filePath),
|
getDiffContent: (projectPath: string, filePath: string) => ipcRenderer.invoke('git:diffContent', projectPath, filePath),
|
||||||
getCommitDiffContent: (projectPath: string, commitHash: string) => ipcRenderer.invoke('git:commitDiffContent', projectPath, commitHash),
|
getCommitDiffContent: (projectPath: string, commitHash: string) => ipcRenderer.invoke('git:commitDiffContent', projectPath, commitHash),
|
||||||
getHistory: (projectPath: string, limit?: number) => ipcRenderer.invoke('git:history', projectPath, limit),
|
getHistory: (projectPath: string, limit?: number) => ipcRenderer.invoke('git:history', projectPath, limit),
|
||||||
|
getRemoteState: (projectPath: string) => ipcRenderer.invoke('git:remoteState', projectPath),
|
||||||
fetch: (projectPath: string) => ipcRenderer.invoke('git:fetch', projectPath),
|
fetch: (projectPath: string) => ipcRenderer.invoke('git:fetch', projectPath),
|
||||||
pull: (projectPath: string) => ipcRenderer.invoke('git:pull', projectPath),
|
pull: (projectPath: string) => ipcRenderer.invoke('git:pull', projectPath),
|
||||||
push: (projectPath: string) => ipcRenderer.invoke('git:push', projectPath),
|
push: (projectPath: string) => ipcRenderer.invoke('git:push', projectPath),
|
||||||
|
|||||||
@@ -271,6 +271,14 @@ export interface GitHistoryEntry {
|
|||||||
syncStatus?: GitHistorySyncStatus;
|
syncStatus?: GitHistorySyncStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GitRemoteStateDto {
|
||||||
|
localBranch: string | null;
|
||||||
|
upstreamBranch: string | null;
|
||||||
|
hasUpstream: boolean;
|
||||||
|
ahead: number;
|
||||||
|
behind: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type GitInitPhase =
|
export type GitInitPhase =
|
||||||
| 'checking-git'
|
| 'checking-git'
|
||||||
| 'initializing-repo'
|
| 'initializing-repo'
|
||||||
@@ -395,6 +403,7 @@ export interface ElectronAPI {
|
|||||||
getDiffContent: (projectPath: string, filePath: string) => Promise<GitDiffContentDto>;
|
getDiffContent: (projectPath: string, filePath: string) => Promise<GitDiffContentDto>;
|
||||||
getCommitDiffContent: (projectPath: string, commitHash: string) => Promise<GitCommitDiffContentDto>;
|
getCommitDiffContent: (projectPath: string, commitHash: string) => Promise<GitCommitDiffContentDto>;
|
||||||
getHistory: (projectPath: string, limit?: number) => Promise<GitHistoryEntry[]>;
|
getHistory: (projectPath: string, limit?: number) => Promise<GitHistoryEntry[]>;
|
||||||
|
getRemoteState: (projectPath: string) => Promise<GitRemoteStateDto>;
|
||||||
fetch: (projectPath: string) => Promise<GitActionResult>;
|
fetch: (projectPath: string) => Promise<GitActionResult>;
|
||||||
pull: (projectPath: string) => Promise<GitActionResult>;
|
pull: (projectPath: string) => Promise<GitActionResult>;
|
||||||
push: (projectPath: string) => Promise<GitActionResult>;
|
push: (projectPath: string) => Promise<GitActionResult>;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useAppStore } from '../../store';
|
import { useAppStore } from '../../store';
|
||||||
import type { GitInitProgress, GitHistoryEntry } from '../../../main/shared/electronApi';
|
import type { GitInitProgress, GitHistoryEntry, GitRemoteStateDto } from '../../../main/shared/electronApi';
|
||||||
import './GitSidebar.css';
|
import './GitSidebar.css';
|
||||||
import '../Sidebar/Sidebar.css';
|
import '../Sidebar/Sidebar.css';
|
||||||
|
|
||||||
@@ -36,17 +36,21 @@ export const GitSidebar: React.FC = () => {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [errorGuidance, setErrorGuidance] = useState<string[]>([]);
|
const [errorGuidance, setErrorGuidance] = useState<string[]>([]);
|
||||||
const [isRepo, setIsRepo] = useState(false);
|
const [isRepo, setIsRepo] = useState(false);
|
||||||
|
const [hasRemote, setHasRemote] = useState(false);
|
||||||
const [currentBranch, setCurrentBranch] = useState<string | null>(null);
|
const [currentBranch, setCurrentBranch] = useState<string | null>(null);
|
||||||
const [statusFiles, setStatusFiles] = useState<Array<{ path: string; status: string }>>([]);
|
const [statusFiles, setStatusFiles] = useState<Array<{ path: string; status: string }>>([]);
|
||||||
const [commitMessage, setCommitMessage] = useState('');
|
const [commitMessage, setCommitMessage] = useState('');
|
||||||
const [historyLoading, setHistoryLoading] = useState(false);
|
const [historyLoading, setHistoryLoading] = useState(false);
|
||||||
const [historyEntries, setHistoryEntries] = useState<GitHistoryEntry[]>([]);
|
const [historyEntries, setHistoryEntries] = useState<GitHistoryEntry[]>([]);
|
||||||
|
const [remoteState, setRemoteState] = useState<GitRemoteStateDto | null>(null);
|
||||||
|
const [remoteStateError, setRemoteStateError] = useState<string | null>(null);
|
||||||
const [initProgress, setInitProgress] = useState<GitInitProgress | null>(null);
|
const [initProgress, setInitProgress] = useState<GitInitProgress | null>(null);
|
||||||
const [initTranscript, setInitTranscript] = useState<GitInitProgress[]>([]);
|
const [initTranscript, setInitTranscript] = useState<GitInitProgress[]>([]);
|
||||||
const [isTranscriptExpanded, setIsTranscriptExpanded] = useState(false);
|
const [isTranscriptExpanded, setIsTranscriptExpanded] = useState(false);
|
||||||
const remoteUrlInputRef = useRef<HTMLInputElement | null>(null);
|
const remoteUrlInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const commitMessageInputRef = useRef<HTMLInputElement | null>(null);
|
const commitMessageInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const statusRefreshInFlightRef = useRef(false);
|
const statusRefreshInFlightRef = useRef(false);
|
||||||
|
const remoteRefreshInFlightRef = useRef(false);
|
||||||
|
|
||||||
const refreshRepoDetails = useCallback(
|
const refreshRepoDetails = useCallback(
|
||||||
async (targetProjectPath: string, options?: { background?: boolean }) => {
|
async (targetProjectPath: string, options?: { background?: boolean }) => {
|
||||||
@@ -80,6 +84,45 @@ export const GitSidebar: React.FC = () => {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const refreshRemoteState = useCallback(
|
||||||
|
async (targetProjectPath: string, options?: { background?: boolean; fetchFirst?: boolean }) => {
|
||||||
|
if (remoteRefreshInFlightRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const background = options?.background ?? false;
|
||||||
|
const fetchFirst = options?.fetchFirst ?? false;
|
||||||
|
|
||||||
|
remoteRefreshInFlightRef.current = true;
|
||||||
|
try {
|
||||||
|
if (fetchFirst) {
|
||||||
|
const fetchResult = await window.electronAPI.git.fetch(targetProjectPath);
|
||||||
|
if (!fetchResult.success) {
|
||||||
|
const message = fetchResult.error || 'Failed to fetch remote updates.';
|
||||||
|
setRemoteStateError(message);
|
||||||
|
if (!background) {
|
||||||
|
setError(message);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextRemoteState = await window.electronAPI.git.getRemoteState(targetProjectPath);
|
||||||
|
setRemoteState(nextRemoteState);
|
||||||
|
setRemoteStateError(null);
|
||||||
|
} catch {
|
||||||
|
const message = 'Unable to refresh remote tracking state.';
|
||||||
|
setRemoteStateError(message);
|
||||||
|
if (!background) {
|
||||||
|
setError(message);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
remoteRefreshInFlightRef.current = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const getDiffTabId = (filePath: string): string => `git-diff:${filePath}`;
|
const getDiffTabId = (filePath: string): string => `git-diff:${filePath}`;
|
||||||
const getCommitDiffTabId = (commitHash: string): string => `git-diff:commit:${commitHash}`;
|
const getCommitDiffTabId = (commitHash: string): string => `git-diff:commit:${commitHash}`;
|
||||||
|
|
||||||
@@ -167,23 +210,35 @@ export const GitSidebar: React.FC = () => {
|
|||||||
|
|
||||||
const repoState = await window.electronAPI.git.getRepoState(resolvedProjectPath);
|
const repoState = await window.electronAPI.git.getRepoState(resolvedProjectPath);
|
||||||
setIsRepo(repoState.isRepo);
|
setIsRepo(repoState.isRepo);
|
||||||
|
setHasRemote(repoState.hasRemote);
|
||||||
setCurrentBranch(repoState.currentBranch || null);
|
setCurrentBranch(repoState.currentBranch || null);
|
||||||
|
|
||||||
if (repoState.isRepo) {
|
if (repoState.isRepo) {
|
||||||
await refreshRepoDetails(resolvedProjectPath);
|
await refreshRepoDetails(resolvedProjectPath);
|
||||||
|
if (repoState.hasRemote) {
|
||||||
|
await refreshRemoteState(resolvedProjectPath);
|
||||||
|
} else {
|
||||||
|
setRemoteState(null);
|
||||||
|
setRemoteStateError(null);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setStatusFiles([]);
|
setStatusFiles([]);
|
||||||
setHistoryEntries([]);
|
setHistoryEntries([]);
|
||||||
|
setRemoteState(null);
|
||||||
|
setRemoteStateError(null);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setError('Unable to load repository status.');
|
setError('Unable to load repository status.');
|
||||||
setIsRepo(false);
|
setIsRepo(false);
|
||||||
|
setHasRemote(false);
|
||||||
setStatusFiles([]);
|
setStatusFiles([]);
|
||||||
setHistoryEntries([]);
|
setHistoryEntries([]);
|
||||||
|
setRemoteState(null);
|
||||||
|
setRemoteStateError(null);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [refreshRepoDetails, resolveProjectPath]);
|
}, [refreshRemoteState, refreshRepoDetails, resolveProjectPath]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadRepoState();
|
void loadRepoState();
|
||||||
@@ -217,6 +272,20 @@ export const GitSidebar: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, [isRepo, projectPath, refreshRepoDetails]);
|
}, [isRepo, projectPath, refreshRepoDetails]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isRepo || !hasRemote || !projectPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const intervalId = globalThis.setInterval(() => {
|
||||||
|
void refreshRemoteState(projectPath, { background: true, fetchFirst: true });
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
globalThis.clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, [hasRemote, isRepo, projectPath, refreshRemoteState]);
|
||||||
|
|
||||||
const handleInitialize = async () => {
|
const handleInitialize = async () => {
|
||||||
if (!projectPath) {
|
if (!projectPath) {
|
||||||
return;
|
return;
|
||||||
@@ -508,6 +577,13 @@ export const GitSidebar: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{currentBranch && <div className="git-sidebar-empty-state">Branch: {currentBranch}</div>}
|
{currentBranch && <div className="git-sidebar-empty-state">Branch: {currentBranch}</div>}
|
||||||
|
{remoteState?.hasUpstream && remoteState.localBranch && remoteState.upstreamBranch && (
|
||||||
|
<div className="git-sidebar-empty-state">{remoteState.localBranch} → {remoteState.upstreamBranch}</div>
|
||||||
|
)}
|
||||||
|
{remoteState?.hasUpstream && (
|
||||||
|
<div className="git-sidebar-empty-state">ahead {remoteState.ahead} / behind {remoteState.behind}</div>
|
||||||
|
)}
|
||||||
|
{remoteStateError && <div className="git-sidebar-empty-state git-sidebar-error">{remoteStateError}</div>}
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<div className="git-sidebar-empty-state git-sidebar-error">
|
<div className="git-sidebar-empty-state git-sidebar-error">
|
||||||
|
|||||||
@@ -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', () => {
|
describe('ensureGitignore', () => {
|
||||||
it('should create .gitignore with default system metadata entries when missing', async () => {
|
it('should create .gitignore with default system metadata entries when missing', async () => {
|
||||||
mockReadFile.mockRejectedValue(new Error('ENOENT'));
|
mockReadFile.mockRejectedValue(new Error('ENOENT'));
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ const mockGitEngine = {
|
|||||||
getDiff: vi.fn(),
|
getDiff: vi.fn(),
|
||||||
getDiffContent: vi.fn(),
|
getDiffContent: vi.fn(),
|
||||||
getHistory: vi.fn(),
|
getHistory: vi.fn(),
|
||||||
|
getRemoteState: vi.fn(),
|
||||||
fetch: vi.fn(),
|
fetch: vi.fn(),
|
||||||
pull: vi.fn(),
|
pull: vi.fn(),
|
||||||
push: 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', () => {
|
describe('git:diffContent', () => {
|
||||||
it('should pass project path and file path to GitEngine.getDiffContent', async () => {
|
it('should pass project path and file path to GitEngine.getDiffContent', async () => {
|
||||||
mockGitEngine.getDiffContent.mockResolvedValue({
|
mockGitEngine.getDiffContent.mockResolvedValue({
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ describe('GitSidebar', () => {
|
|||||||
checkAvailability: vi.fn().mockResolvedValue({ gitFound: true, version: '2.49.0' }),
|
checkAvailability: vi.fn().mockResolvedValue({ gitFound: true, version: '2.49.0' }),
|
||||||
getRepoState: vi.fn().mockResolvedValue({ isRepo: false, hasRemote: false }),
|
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 } }),
|
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' }),
|
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: '' }),
|
getDiffContent: vi.fn().mockResolvedValue({ filePath: 'posts/a.md', original: '', modified: '' }),
|
||||||
getCommitDiffContent: vi.fn().mockResolvedValue({ commitHash: 'abc123', 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 () => {
|
it('polls repository status on an interval and prevents overlapping in-flight requests', async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user