mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-07 15:07:38 +00:00
refactor(git-panel): Make GitPanel a feature based component and update imports
This commit is contained in:
645
src/components/git-panel/hooks/useGitPanelController.ts
Normal file
645
src/components/git-panel/hooks/useGitPanelController.ts
Normal file
@@ -0,0 +1,645 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { authenticatedFetch } from '../../../utils/api';
|
||||
import { DEFAULT_BRANCH, RECENT_COMMITS_LIMIT } from '../constants/constants';
|
||||
import type {
|
||||
GitApiErrorResponse,
|
||||
GitBranchesResponse,
|
||||
GitCommitSummary,
|
||||
GitCommitsResponse,
|
||||
GitDiffMap,
|
||||
GitDiffResponse,
|
||||
GitFileWithDiffResponse,
|
||||
GitGenerateMessageResponse,
|
||||
GitOperationResponse,
|
||||
GitPanelController,
|
||||
GitRemoteStatus,
|
||||
GitStatusResponse,
|
||||
UseGitPanelControllerOptions,
|
||||
} from '../types/types';
|
||||
import { getAllChangedFiles } from '../utils/gitPanelUtils';
|
||||
import { useSelectedProvider } from './useSelectedProvider';
|
||||
|
||||
// ! use authenticatedFetch directly. fetchWithAuth is redundant
|
||||
const fetchWithAuth = authenticatedFetch as (url: string, options?: RequestInit) => Promise<Response>;
|
||||
|
||||
async function readJson<T>(response: Response): Promise<T> {
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export function useGitPanelController({
|
||||
selectedProject,
|
||||
activeView,
|
||||
onFileOpen,
|
||||
}: UseGitPanelControllerOptions): GitPanelController {
|
||||
const [gitStatus, setGitStatus] = useState<GitStatusResponse | null>(null);
|
||||
const [gitDiff, setGitDiff] = useState<GitDiffMap>({});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentBranch, setCurrentBranch] = useState('');
|
||||
const [branches, setBranches] = useState<string[]>([]);
|
||||
const [recentCommits, setRecentCommits] = useState<GitCommitSummary[]>([]);
|
||||
const [commitDiffs, setCommitDiffs] = useState<GitDiffMap>({});
|
||||
const [remoteStatus, setRemoteStatus] = useState<GitRemoteStatus | null>(null);
|
||||
const [isCreatingBranch, setIsCreatingBranch] = useState(false);
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
const [isPulling, setIsPulling] = useState(false);
|
||||
const [isPushing, setIsPushing] = useState(false);
|
||||
const [isPublishing, setIsPublishing] = useState(false);
|
||||
const [isCreatingInitialCommit, setIsCreatingInitialCommit] = useState(false);
|
||||
|
||||
const provider = useSelectedProvider();
|
||||
|
||||
const fetchFileDiff = useCallback(
|
||||
async (filePath: string) => {
|
||||
if (!selectedProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth(
|
||||
`/api/git/diff?project=${encodeURIComponent(selectedProject.name)}&file=${encodeURIComponent(filePath)}`,
|
||||
);
|
||||
const data = await readJson<GitDiffResponse>(response);
|
||||
|
||||
if (!data.error && data.diff) {
|
||||
setGitDiff((previous) => ({
|
||||
...previous,
|
||||
[filePath]: data.diff as string,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching file diff:', error);
|
||||
}
|
||||
},
|
||||
[selectedProject],
|
||||
);
|
||||
|
||||
const fetchGitStatus = useCallback(async () => {
|
||||
if (!selectedProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetchWithAuth(`/api/git/status?project=${encodeURIComponent(selectedProject.name)}`);
|
||||
const data = await readJson<GitStatusResponse>(response);
|
||||
|
||||
if (data.error) {
|
||||
console.error('Git status error:', data.error);
|
||||
setGitStatus({ error: data.error, details: data.details });
|
||||
setCurrentBranch('');
|
||||
return;
|
||||
}
|
||||
|
||||
setGitStatus(data);
|
||||
setCurrentBranch(data.branch || DEFAULT_BRANCH);
|
||||
|
||||
const changedFiles = getAllChangedFiles(data);
|
||||
changedFiles.forEach((filePath) => {
|
||||
void fetchFileDiff(filePath);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching git status:', error);
|
||||
setGitStatus({ error: 'Git operation failed', details: String(error) });
|
||||
setCurrentBranch('');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [fetchFileDiff, selectedProject]);
|
||||
|
||||
const fetchBranches = useCallback(async () => {
|
||||
if (!selectedProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth(`/api/git/branches?project=${encodeURIComponent(selectedProject.name)}`);
|
||||
const data = await readJson<GitBranchesResponse>(response);
|
||||
|
||||
if (!data.error && data.branches) {
|
||||
setBranches(data.branches);
|
||||
return;
|
||||
}
|
||||
|
||||
setBranches([]);
|
||||
} catch (error) {
|
||||
console.error('Error fetching branches:', error);
|
||||
setBranches([]);
|
||||
}
|
||||
}, [selectedProject]);
|
||||
|
||||
const fetchRemoteStatus = useCallback(async () => {
|
||||
if (!selectedProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth(`/api/git/remote-status?project=${encodeURIComponent(selectedProject.name)}`);
|
||||
const data = await readJson<GitRemoteStatus | GitApiErrorResponse>(response);
|
||||
|
||||
if (!data.error) {
|
||||
setRemoteStatus(data as GitRemoteStatus);
|
||||
return;
|
||||
}
|
||||
|
||||
setRemoteStatus(null);
|
||||
} catch (error) {
|
||||
console.error('Error fetching remote status:', error);
|
||||
setRemoteStatus(null);
|
||||
}
|
||||
}, [selectedProject]);
|
||||
|
||||
const switchBranch = useCallback(
|
||||
async (branchName: string) => {
|
||||
if (!selectedProject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth('/api/git/checkout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
project: selectedProject.name,
|
||||
branch: branchName,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await readJson<GitOperationResponse>(response);
|
||||
if (!data.success) {
|
||||
console.error('Failed to switch branch:', data.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
setCurrentBranch(branchName);
|
||||
void fetchGitStatus();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error switching branch:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[fetchGitStatus, selectedProject],
|
||||
);
|
||||
|
||||
const createBranch = useCallback(
|
||||
async (branchName: string) => {
|
||||
const trimmedBranchName = branchName.trim();
|
||||
if (!selectedProject || !trimmedBranchName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setIsCreatingBranch(true);
|
||||
try {
|
||||
const response = await fetchWithAuth('/api/git/create-branch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
project: selectedProject.name,
|
||||
branch: trimmedBranchName,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await readJson<GitOperationResponse>(response);
|
||||
if (!data.success) {
|
||||
console.error('Failed to create branch:', data.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
setCurrentBranch(trimmedBranchName);
|
||||
void fetchBranches();
|
||||
void fetchGitStatus();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error creating branch:', error);
|
||||
return false;
|
||||
} finally {
|
||||
setIsCreatingBranch(false);
|
||||
}
|
||||
},
|
||||
[fetchBranches, fetchGitStatus, selectedProject],
|
||||
);
|
||||
|
||||
const handleFetch = useCallback(async () => {
|
||||
if (!selectedProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsFetching(true);
|
||||
try {
|
||||
const response = await fetchWithAuth('/api/git/fetch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
project: selectedProject.name,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await readJson<GitOperationResponse>(response);
|
||||
if (data.success) {
|
||||
void fetchGitStatus();
|
||||
void fetchRemoteStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('Fetch failed:', data.error);
|
||||
} catch (error) {
|
||||
console.error('Error fetching from remote:', error);
|
||||
} finally {
|
||||
setIsFetching(false);
|
||||
}
|
||||
}, [fetchGitStatus, fetchRemoteStatus, selectedProject]);
|
||||
|
||||
const handlePull = useCallback(async () => {
|
||||
if (!selectedProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsPulling(true);
|
||||
try {
|
||||
const response = await fetchWithAuth('/api/git/pull', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
project: selectedProject.name,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await readJson<GitOperationResponse>(response);
|
||||
if (data.success) {
|
||||
void fetchGitStatus();
|
||||
void fetchRemoteStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('Pull failed:', data.error);
|
||||
} catch (error) {
|
||||
console.error('Error pulling from remote:', error);
|
||||
} finally {
|
||||
setIsPulling(false);
|
||||
}
|
||||
}, [fetchGitStatus, fetchRemoteStatus, selectedProject]);
|
||||
|
||||
const handlePush = useCallback(async () => {
|
||||
if (!selectedProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsPushing(true);
|
||||
try {
|
||||
const response = await fetchWithAuth('/api/git/push', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
project: selectedProject.name,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await readJson<GitOperationResponse>(response);
|
||||
if (data.success) {
|
||||
void fetchGitStatus();
|
||||
void fetchRemoteStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('Push failed:', data.error);
|
||||
} catch (error) {
|
||||
console.error('Error pushing to remote:', error);
|
||||
} finally {
|
||||
setIsPushing(false);
|
||||
}
|
||||
}, [fetchGitStatus, fetchRemoteStatus, selectedProject]);
|
||||
|
||||
const handlePublish = useCallback(async () => {
|
||||
if (!selectedProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsPublishing(true);
|
||||
try {
|
||||
const response = await fetchWithAuth('/api/git/publish', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
project: selectedProject.name,
|
||||
branch: currentBranch,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await readJson<GitOperationResponse>(response);
|
||||
if (data.success) {
|
||||
void fetchGitStatus();
|
||||
void fetchRemoteStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('Publish failed:', data.error);
|
||||
} catch (error) {
|
||||
console.error('Error publishing branch:', error);
|
||||
} finally {
|
||||
setIsPublishing(false);
|
||||
}
|
||||
}, [currentBranch, fetchGitStatus, fetchRemoteStatus, selectedProject]);
|
||||
|
||||
const discardChanges = useCallback(
|
||||
async (filePath: string) => {
|
||||
if (!selectedProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth('/api/git/discard', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
project: selectedProject.name,
|
||||
file: filePath,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await readJson<GitOperationResponse>(response);
|
||||
if (data.success) {
|
||||
void fetchGitStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('Discard failed:', data.error);
|
||||
} catch (error) {
|
||||
console.error('Error discarding changes:', error);
|
||||
}
|
||||
},
|
||||
[fetchGitStatus, selectedProject],
|
||||
);
|
||||
|
||||
const deleteUntrackedFile = useCallback(
|
||||
async (filePath: string) => {
|
||||
if (!selectedProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth('/api/git/delete-untracked', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
project: selectedProject.name,
|
||||
file: filePath,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await readJson<GitOperationResponse>(response);
|
||||
if (data.success) {
|
||||
void fetchGitStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('Delete failed:', data.error);
|
||||
} catch (error) {
|
||||
console.error('Error deleting untracked file:', error);
|
||||
}
|
||||
},
|
||||
[fetchGitStatus, selectedProject],
|
||||
);
|
||||
|
||||
const fetchRecentCommits = useCallback(async () => {
|
||||
if (!selectedProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth(
|
||||
`/api/git/commits?project=${encodeURIComponent(selectedProject.name)}&limit=${RECENT_COMMITS_LIMIT}`,
|
||||
);
|
||||
const data = await readJson<GitCommitsResponse>(response);
|
||||
|
||||
if (!data.error && data.commits) {
|
||||
setRecentCommits(data.commits);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching commits:', error);
|
||||
}
|
||||
}, [selectedProject]);
|
||||
|
||||
const fetchCommitDiff = useCallback(
|
||||
async (commitHash: string) => {
|
||||
if (!selectedProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth(
|
||||
`/api/git/commit-diff?project=${encodeURIComponent(selectedProject.name)}&commit=${commitHash}`,
|
||||
);
|
||||
const data = await readJson<GitDiffResponse>(response);
|
||||
|
||||
if (!data.error && data.diff) {
|
||||
setCommitDiffs((previous) => ({
|
||||
...previous,
|
||||
[commitHash]: data.diff as string,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching commit diff:', error);
|
||||
}
|
||||
},
|
||||
[selectedProject],
|
||||
);
|
||||
|
||||
const generateCommitMessage = useCallback(
|
||||
async (files: string[]) => {
|
||||
if (!selectedProject || files.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch('/api/git/generate-commit-message', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
project: selectedProject.name,
|
||||
files,
|
||||
provider,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await readJson<GitGenerateMessageResponse>(response);
|
||||
if (data.message) {
|
||||
return data.message;
|
||||
}
|
||||
|
||||
console.error('Failed to generate commit message:', data.error);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error generating commit message:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[provider, selectedProject],
|
||||
);
|
||||
|
||||
const commitChanges = useCallback(
|
||||
async (message: string, files: string[]) => {
|
||||
if (!selectedProject || !message.trim() || files.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth('/api/git/commit', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
project: selectedProject.name,
|
||||
message,
|
||||
files,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await readJson<GitOperationResponse>(response);
|
||||
if (data.success) {
|
||||
void fetchGitStatus();
|
||||
void fetchRemoteStatus();
|
||||
return true;
|
||||
}
|
||||
|
||||
console.error('Commit failed:', data.error);
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Error committing changes:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[fetchGitStatus, fetchRemoteStatus, selectedProject],
|
||||
);
|
||||
|
||||
const createInitialCommit = useCallback(async () => {
|
||||
if (!selectedProject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setIsCreatingInitialCommit(true);
|
||||
try {
|
||||
const response = await fetchWithAuth('/api/git/initial-commit', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
project: selectedProject.name,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await readJson<GitOperationResponse>(response);
|
||||
if (data.success) {
|
||||
void fetchGitStatus();
|
||||
void fetchRemoteStatus();
|
||||
return true;
|
||||
}
|
||||
|
||||
console.error('Initial commit failed:', data.error);
|
||||
alert(data.error || 'Failed to create initial commit');
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Error creating initial commit:', error);
|
||||
alert('Failed to create initial commit');
|
||||
return false;
|
||||
} finally {
|
||||
setIsCreatingInitialCommit(false);
|
||||
}
|
||||
}, [fetchGitStatus, fetchRemoteStatus, selectedProject]);
|
||||
|
||||
const openFile = useCallback(
|
||||
async (filePath: string) => {
|
||||
if (!onFileOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedProject) {
|
||||
onFileOpen(filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth(
|
||||
`/api/git/file-with-diff?project=${encodeURIComponent(selectedProject.name)}&file=${encodeURIComponent(filePath)}`,
|
||||
);
|
||||
const data = await readJson<GitFileWithDiffResponse>(response);
|
||||
|
||||
if (data.error) {
|
||||
console.error('Error fetching file with diff:', data.error);
|
||||
onFileOpen(filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
onFileOpen(filePath, {
|
||||
old_string: data.oldContent || '',
|
||||
new_string: data.currentContent || '',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error opening file:', error);
|
||||
onFileOpen(filePath);
|
||||
}
|
||||
},
|
||||
[onFileOpen, selectedProject],
|
||||
);
|
||||
|
||||
const refreshAll = useCallback(() => {
|
||||
void fetchGitStatus();
|
||||
void fetchBranches();
|
||||
void fetchRemoteStatus();
|
||||
}, [fetchBranches, fetchGitStatus, fetchRemoteStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset repository-scoped state when project changes to avoid stale UI.
|
||||
setCurrentBranch('');
|
||||
setBranches([]);
|
||||
setGitStatus(null);
|
||||
setRemoteStatus(null);
|
||||
setGitDiff({});
|
||||
setRecentCommits([]);
|
||||
setCommitDiffs({});
|
||||
|
||||
if (!selectedProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
void fetchGitStatus();
|
||||
void fetchBranches();
|
||||
void fetchRemoteStatus();
|
||||
}, [fetchBranches, fetchGitStatus, fetchRemoteStatus, selectedProject]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedProject || activeView !== 'history') {
|
||||
return;
|
||||
}
|
||||
|
||||
void fetchRecentCommits();
|
||||
}, [activeView, fetchRecentCommits, selectedProject]);
|
||||
|
||||
return {
|
||||
gitStatus,
|
||||
gitDiff,
|
||||
isLoading,
|
||||
currentBranch,
|
||||
branches,
|
||||
recentCommits,
|
||||
commitDiffs,
|
||||
remoteStatus,
|
||||
isCreatingBranch,
|
||||
isFetching,
|
||||
isPulling,
|
||||
isPushing,
|
||||
isPublishing,
|
||||
isCreatingInitialCommit,
|
||||
refreshAll,
|
||||
switchBranch,
|
||||
createBranch,
|
||||
handleFetch,
|
||||
handlePull,
|
||||
handlePush,
|
||||
handlePublish,
|
||||
discardChanges,
|
||||
deleteUntrackedFile,
|
||||
fetchCommitDiff,
|
||||
generateCommitMessage,
|
||||
commitChanges,
|
||||
createInitialCommit,
|
||||
openFile,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user