feat: better diff. integration
This commit is contained in:
@@ -41,6 +41,20 @@ export interface GitDiffDto {
|
||||
patch: string;
|
||||
}
|
||||
|
||||
export interface GitDiffContentDto {
|
||||
filePath: string;
|
||||
original: string;
|
||||
modified: string;
|
||||
}
|
||||
|
||||
export interface GitHistoryEntry {
|
||||
hash: string;
|
||||
shortHash: string;
|
||||
date: string;
|
||||
subject: string;
|
||||
author: string;
|
||||
}
|
||||
|
||||
export type GitInitPhase =
|
||||
| 'checking-git'
|
||||
| 'initializing-repo'
|
||||
@@ -232,6 +246,34 @@ export class GitEngine {
|
||||
};
|
||||
}
|
||||
|
||||
async getDiffContent(projectPath: string, filePath: string): Promise<GitDiffContentDto> {
|
||||
const git = simpleGit(projectPath);
|
||||
|
||||
const [original, modified] = await Promise.all([
|
||||
git.show([`HEAD:${filePath}`]).catch(() => ''),
|
||||
fsPromises.readFile(path.join(projectPath, filePath), 'utf8').catch(() => ''),
|
||||
]);
|
||||
|
||||
return {
|
||||
filePath,
|
||||
original,
|
||||
modified,
|
||||
};
|
||||
}
|
||||
|
||||
async getHistory(projectPath: string, limit = 20): Promise<GitHistoryEntry[]> {
|
||||
const git = simpleGit(projectPath);
|
||||
const history = await git.log({ maxCount: limit });
|
||||
|
||||
return history.all.map((entry) => ({
|
||||
hash: entry.hash,
|
||||
shortHash: entry.hash.slice(0, 7),
|
||||
date: entry.date,
|
||||
subject: entry.message,
|
||||
author: entry.author_name,
|
||||
}));
|
||||
}
|
||||
|
||||
async ensureGitignore(projectPath: string): Promise<GitIgnoreEnsureResult> {
|
||||
const gitignorePath = path.join(projectPath, '.gitignore');
|
||||
|
||||
|
||||
@@ -80,6 +80,8 @@ export {
|
||||
type RepoState,
|
||||
type GitStatusDto,
|
||||
type GitDiffDto,
|
||||
type GitDiffContentDto,
|
||||
type GitHistoryEntry,
|
||||
type GitStatusFile,
|
||||
type GitStatusCounts,
|
||||
type GitInitResult,
|
||||
|
||||
@@ -53,6 +53,16 @@ export function registerIpcHandlers(): void {
|
||||
return engine.getDiff(projectPath, filePath);
|
||||
});
|
||||
|
||||
safeHandle('git:diffContent', async (_, projectPath: string, filePath: string) => {
|
||||
const engine = getGitEngine();
|
||||
return engine.getDiffContent(projectPath, filePath);
|
||||
});
|
||||
|
||||
safeHandle('git:history', async (_, projectPath: string, limit?: number) => {
|
||||
const engine = getGitEngine();
|
||||
return engine.getHistory(projectPath, limit);
|
||||
});
|
||||
|
||||
safeHandle('git:init', async (event, projectPath: string, remoteUrl?: string) => {
|
||||
const engine = getGitEngine();
|
||||
return engine.initializeRepo(projectPath, remoteUrl, (progress) => {
|
||||
|
||||
@@ -11,6 +11,8 @@ export const electronAPI: ElectronAPI = {
|
||||
getRepoState: (projectPath: string) => ipcRenderer.invoke('git:getRepoState', projectPath),
|
||||
getStatus: (projectPath: string) => ipcRenderer.invoke('git:status', projectPath),
|
||||
getDiff: (projectPath: string, filePath: string) => ipcRenderer.invoke('git:diff', projectPath, filePath),
|
||||
getDiffContent: (projectPath: string, filePath: string) => ipcRenderer.invoke('git:diffContent', projectPath, filePath),
|
||||
getHistory: (projectPath: string, limit?: number) => ipcRenderer.invoke('git:history', projectPath, limit),
|
||||
ensureGitignore: (projectPath: string) => ipcRenderer.invoke('git:ensureGitignore', projectPath),
|
||||
pruneLfs: (projectPath: string, options?: { dryRun?: boolean; verifyRemote?: boolean }) => ipcRenderer.invoke('git:pruneLfs', projectPath, options),
|
||||
init: (projectPath: string, remoteUrl?: string) => {
|
||||
|
||||
@@ -241,6 +241,20 @@ export interface GitDiffDto {
|
||||
patch: string;
|
||||
}
|
||||
|
||||
export interface GitDiffContentDto {
|
||||
filePath: string;
|
||||
original: string;
|
||||
modified: string;
|
||||
}
|
||||
|
||||
export interface GitHistoryEntry {
|
||||
hash: string;
|
||||
shortHash: string;
|
||||
date: string;
|
||||
subject: string;
|
||||
author: string;
|
||||
}
|
||||
|
||||
export type GitInitPhase =
|
||||
| 'checking-git'
|
||||
| 'initializing-repo'
|
||||
@@ -354,6 +368,8 @@ export interface ElectronAPI {
|
||||
getRepoState: (projectPath: string) => Promise<GitRepoState>;
|
||||
getStatus: (projectPath: string) => Promise<GitStatusDto>;
|
||||
getDiff: (projectPath: string, filePath: string) => Promise<GitDiffDto>;
|
||||
getDiffContent: (projectPath: string, filePath: string) => Promise<GitDiffContentDto>;
|
||||
getHistory: (projectPath: string, limit?: number) => Promise<GitHistoryEntry[]>;
|
||||
ensureGitignore: (projectPath: string) => Promise<GitIgnoreEnsureResult>;
|
||||
pruneLfs: (projectPath: string, options?: { dryRun?: boolean; verifyRemote?: boolean }) => Promise<GitLfsPruneResult>;
|
||||
init: (projectPath: string, remoteUrl?: string) => Promise<GitInitResult>;
|
||||
|
||||
@@ -14,14 +14,9 @@
|
||||
color: var(--vscode-sideBar-foreground);
|
||||
}
|
||||
|
||||
.git-diff-patch {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
white-space: pre;
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
.git-diff-editor-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.git-diff-message,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { DiffEditor } from '@monaco-editor/react';
|
||||
import { useAppStore } from '../../store';
|
||||
import './GitDiffView.css';
|
||||
|
||||
@@ -6,11 +7,40 @@ interface GitDiffViewProps {
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
function detectLanguage(filePath: string): string {
|
||||
const extension = filePath.split('.').pop()?.toLowerCase();
|
||||
switch (extension) {
|
||||
case 'md':
|
||||
case 'markdown':
|
||||
return 'markdown';
|
||||
case 'ts':
|
||||
return 'typescript';
|
||||
case 'tsx':
|
||||
return 'typescript';
|
||||
case 'js':
|
||||
return 'javascript';
|
||||
case 'jsx':
|
||||
return 'javascript';
|
||||
case 'json':
|
||||
return 'json';
|
||||
case 'css':
|
||||
return 'css';
|
||||
case 'html':
|
||||
return 'html';
|
||||
case 'yml':
|
||||
case 'yaml':
|
||||
return 'yaml';
|
||||
default:
|
||||
return 'plaintext';
|
||||
}
|
||||
}
|
||||
|
||||
export const GitDiffView: React.FC<GitDiffViewProps> = ({ filePath }) => {
|
||||
const { activeProject } = useAppStore();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [patch, setPatch] = useState('');
|
||||
const [original, setOriginal] = useState('');
|
||||
const [modified, setModified] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const loadDiff = async () => {
|
||||
@@ -32,8 +62,9 @@ export const GitDiffView: React.FC<GitDiffViewProps> = ({ filePath }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const diff = await window.electronAPI.git.getDiff(projectPath, filePath);
|
||||
setPatch(diff.patch || '');
|
||||
const diff = await window.electronAPI.git.getDiffContent(projectPath, filePath);
|
||||
setOriginal(diff.original || '');
|
||||
setModified(diff.modified || '');
|
||||
} catch {
|
||||
setError('Failed to load diff.');
|
||||
} finally {
|
||||
@@ -65,7 +96,27 @@ export const GitDiffView: React.FC<GitDiffViewProps> = ({ filePath }) => {
|
||||
return (
|
||||
<div className="git-diff-view">
|
||||
<div className="git-diff-header">Diff: {filePath}</div>
|
||||
{patch ? <pre className="git-diff-patch">{patch}</pre> : <div className="git-diff-message">No diff available.</div>}
|
||||
<div className="git-diff-editor-wrap">
|
||||
<DiffEditor
|
||||
original={original}
|
||||
modified={modified}
|
||||
language={detectLanguage(filePath)}
|
||||
theme="vs-dark"
|
||||
height="100%"
|
||||
options={{
|
||||
readOnly: true,
|
||||
renderSideBySide: false,
|
||||
minimap: { enabled: false },
|
||||
lineNumbers: 'on',
|
||||
scrollBeyondLastLine: false,
|
||||
renderOverviewRuler: true,
|
||||
originalEditable: false,
|
||||
diffCodeLens: false,
|
||||
wordWrap: 'off',
|
||||
ignoreTrimWhitespace: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -42,15 +42,6 @@
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.git-sidebar-section-header {
|
||||
padding: 0 12px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--vscode-sideBar-foreground);
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.git-sidebar-empty-state {
|
||||
padding: 0 12px 8px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
@@ -96,6 +87,34 @@
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.git-sidebar-history-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 0 12px 8px;
|
||||
}
|
||||
|
||||
.git-sidebar-history-item {
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--vscode-input-background);
|
||||
}
|
||||
|
||||
.git-sidebar-history-subject {
|
||||
font-size: 12px;
|
||||
color: var(--vscode-sideBar-foreground);
|
||||
margin-bottom: 2px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.git-sidebar-history-meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 10px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.git-sidebar-main {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useAppStore } from '../../store';
|
||||
import type { GitInitProgress } from '../../../main/shared/electronApi';
|
||||
import type { GitInitProgress, GitHistoryEntry } from '../../../main/shared/electronApi';
|
||||
import './GitSidebar.css';
|
||||
import '../Sidebar/Sidebar.css';
|
||||
|
||||
export const GitSidebar: React.FC = () => {
|
||||
const { activeProject, openTab } = useAppStore();
|
||||
@@ -13,6 +14,8 @@ export const GitSidebar: React.FC = () => {
|
||||
const [isRepo, setIsRepo] = useState(false);
|
||||
const [currentBranch, setCurrentBranch] = useState<string | null>(null);
|
||||
const [statusFiles, setStatusFiles] = useState<Array<{ path: string; status: string }>>([]);
|
||||
const [historyLoading, setHistoryLoading] = useState(false);
|
||||
const [historyEntries, setHistoryEntries] = useState<GitHistoryEntry[]>([]);
|
||||
const [initProgress, setInitProgress] = useState<GitInitProgress | null>(null);
|
||||
const [initTranscript, setInitTranscript] = useState<GitInitProgress[]>([]);
|
||||
const [isTranscriptExpanded, setIsTranscriptExpanded] = useState(false);
|
||||
@@ -70,19 +73,27 @@ export const GitSidebar: React.FC = () => {
|
||||
|
||||
if (repoState.isRepo) {
|
||||
setStatusLoading(true);
|
||||
setHistoryLoading(true);
|
||||
try {
|
||||
const status = await window.electronAPI.git.getStatus(resolvedProjectPath);
|
||||
const [status, history] = await Promise.all([
|
||||
window.electronAPI.git.getStatus(resolvedProjectPath),
|
||||
window.electronAPI.git.getHistory(resolvedProjectPath, 20),
|
||||
]);
|
||||
setStatusFiles(status.files);
|
||||
setHistoryEntries(history);
|
||||
} finally {
|
||||
setStatusLoading(false);
|
||||
setHistoryLoading(false);
|
||||
}
|
||||
} else {
|
||||
setStatusFiles([]);
|
||||
setHistoryEntries([]);
|
||||
}
|
||||
} catch {
|
||||
setError('Unable to load repository status.');
|
||||
setIsRepo(false);
|
||||
setStatusFiles([]);
|
||||
setHistoryEntries([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -176,7 +187,7 @@ export const GitSidebar: React.FC = () => {
|
||||
<div className="git-sidebar-header">SOURCE CONTROL</div>
|
||||
<div className="git-sidebar-content">
|
||||
<div className="git-sidebar-section">
|
||||
<div className="git-sidebar-section-header">OPEN CHANGES</div>
|
||||
<div className="sidebar-section-title">Open Changes ({statusFiles.length})</div>
|
||||
{statusLoading ? (
|
||||
<div className="git-sidebar-empty-state">Loading changes...</div>
|
||||
) : statusFiles.length === 0 ? (
|
||||
@@ -201,10 +212,26 @@ export const GitSidebar: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="git-sidebar-section git-sidebar-history">
|
||||
<div className="git-sidebar-section-header">VERSION HISTORY</div>
|
||||
<div className="git-sidebar-empty-state">
|
||||
{currentBranch ? `Branch: ${currentBranch}` : 'No branch information'}
|
||||
</div>
|
||||
<div className="sidebar-section-title">Version History ({historyEntries.length})</div>
|
||||
{historyLoading ? (
|
||||
<div className="git-sidebar-empty-state">Loading history...</div>
|
||||
) : historyEntries.length === 0 ? (
|
||||
<div className="git-sidebar-empty-state">No commits yet</div>
|
||||
) : (
|
||||
<div className="git-sidebar-history-list" role="list" aria-label="Version History">
|
||||
{historyEntries.map((entry) => (
|
||||
<div key={entry.hash} className="git-sidebar-history-item">
|
||||
<div className="git-sidebar-history-subject">{entry.subject}</div>
|
||||
<div className="git-sidebar-history-meta">
|
||||
<span>{entry.shortHash}</span>
|
||||
<span>{entry.author}</span>
|
||||
<span>{new Date(entry.date).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{currentBranch && <div className="git-sidebar-empty-state">Branch: {currentBranch}</div>}
|
||||
</div>
|
||||
{transcriptSection}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user