feat: version diffs work now
This commit is contained in:
@@ -47,6 +47,19 @@ export interface GitDiffContentDto {
|
|||||||
modified: string;
|
modified: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GitCommitDiffContentDto {
|
||||||
|
commitHash: string;
|
||||||
|
original: string;
|
||||||
|
modified: string;
|
||||||
|
files: GitCommitDiffFileDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GitCommitDiffFileDto {
|
||||||
|
filePath: string;
|
||||||
|
original: string;
|
||||||
|
modified: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GitHistoryEntry {
|
export interface GitHistoryEntry {
|
||||||
hash: string;
|
hash: string;
|
||||||
shortHash: string;
|
shortHash: string;
|
||||||
@@ -505,6 +518,129 @@ export class GitEngine {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCommitDiffContent(projectPath: string, commitHash: string): Promise<GitCommitDiffContentDto> {
|
||||||
|
const git = simpleGit(projectPath);
|
||||||
|
const patch = await git.show(['--format=', '--patch', commitHash]);
|
||||||
|
const files = this.parseUnifiedPatchFiles(patch);
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
return {
|
||||||
|
commitHash,
|
||||||
|
original: '',
|
||||||
|
modified: patch,
|
||||||
|
files: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstFile = files[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
commitHash,
|
||||||
|
original: firstFile.original,
|
||||||
|
modified: firstFile.modified,
|
||||||
|
files,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseUnifiedPatchFiles(patch: string): GitCommitDiffFileDto[] {
|
||||||
|
interface FileDiffBuffers {
|
||||||
|
path: string;
|
||||||
|
original: string[];
|
||||||
|
modified: string[];
|
||||||
|
inHunk: boolean;
|
||||||
|
touched: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = patch.split('\n');
|
||||||
|
const files: FileDiffBuffers[] = [];
|
||||||
|
let currentFile: FileDiffBuffers | null = null;
|
||||||
|
|
||||||
|
const flushCurrent = () => {
|
||||||
|
if (!currentFile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentFile.touched || currentFile.original.length > 0 || currentFile.modified.length > 0) {
|
||||||
|
files.push(currentFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentFile = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('diff --git ')) {
|
||||||
|
flushCurrent();
|
||||||
|
|
||||||
|
const match = line.match(/^diff --git a\/(.+) b\/(.+)$/);
|
||||||
|
const filePath = match ? match[2] : line;
|
||||||
|
currentFile = {
|
||||||
|
path: filePath,
|
||||||
|
original: [],
|
||||||
|
modified: [],
|
||||||
|
inHunk: false,
|
||||||
|
touched: false,
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentFile) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith('@@')) {
|
||||||
|
currentFile.inHunk = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith('Binary files ')) {
|
||||||
|
currentFile.original.push(line);
|
||||||
|
currentFile.modified.push(line);
|
||||||
|
currentFile.touched = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentFile.inHunk) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith('\\ No newline at end of file')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith('+')) {
|
||||||
|
currentFile.modified.push(line.slice(1));
|
||||||
|
currentFile.touched = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith('-')) {
|
||||||
|
currentFile.original.push(line.slice(1));
|
||||||
|
currentFile.touched = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith(' ')) {
|
||||||
|
const contextLine = line.slice(1);
|
||||||
|
currentFile.original.push(contextLine);
|
||||||
|
currentFile.modified.push(contextLine);
|
||||||
|
currentFile.touched = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentFile.original.push(line);
|
||||||
|
currentFile.modified.push(line);
|
||||||
|
currentFile.touched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
flushCurrent();
|
||||||
|
|
||||||
|
return files.map((file) => ({
|
||||||
|
filePath: file.path,
|
||||||
|
original: file.original.join('\n'),
|
||||||
|
modified: file.modified.join('\n'),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
async getHistory(projectPath: string, limit = 20): Promise<GitHistoryEntry[]> {
|
async getHistory(projectPath: string, limit = 20): Promise<GitHistoryEntry[]> {
|
||||||
const git = simpleGit(projectPath);
|
const git = simpleGit(projectPath);
|
||||||
const history = await git.log({ maxCount: limit });
|
const history = await git.log({ maxCount: limit });
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ export function registerIpcHandlers(): void {
|
|||||||
return engine.getDiffContent(projectPath, filePath);
|
return engine.getDiffContent(projectPath, filePath);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
safeHandle('git:commitDiffContent', async (_, projectPath: string, commitHash: string) => {
|
||||||
|
const engine = getGitEngine();
|
||||||
|
return engine.getCommitDiffContent(projectPath, commitHash);
|
||||||
|
});
|
||||||
|
|
||||||
safeHandle('git:history', async (_, projectPath: string, limit?: number) => {
|
safeHandle('git:history', async (_, projectPath: string, limit?: number) => {
|
||||||
const engine = getGitEngine();
|
const engine = getGitEngine();
|
||||||
return engine.getHistory(projectPath, limit);
|
return engine.getHistory(projectPath, limit);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const electronAPI: ElectronAPI = {
|
|||||||
getStatus: (projectPath: string) => ipcRenderer.invoke('git:status', projectPath),
|
getStatus: (projectPath: string) => ipcRenderer.invoke('git:status', projectPath),
|
||||||
getDiff: (projectPath: string, filePath: string) => ipcRenderer.invoke('git:diff', projectPath, filePath),
|
getDiff: (projectPath: string, filePath: string) => ipcRenderer.invoke('git:diff', projectPath, filePath),
|
||||||
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),
|
||||||
getHistory: (projectPath: string, limit?: number) => ipcRenderer.invoke('git:history', projectPath, limit),
|
getHistory: (projectPath: string, limit?: number) => ipcRenderer.invoke('git:history', projectPath, limit),
|
||||||
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),
|
||||||
|
|||||||
@@ -247,6 +247,19 @@ export interface GitDiffContentDto {
|
|||||||
modified: string;
|
modified: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GitCommitDiffContentDto {
|
||||||
|
commitHash: string;
|
||||||
|
original: string;
|
||||||
|
modified: string;
|
||||||
|
files: GitCommitDiffFileDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GitCommitDiffFileDto {
|
||||||
|
filePath: string;
|
||||||
|
original: string;
|
||||||
|
modified: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GitHistoryEntry {
|
export interface GitHistoryEntry {
|
||||||
hash: string;
|
hash: string;
|
||||||
shortHash: string;
|
shortHash: string;
|
||||||
@@ -376,6 +389,7 @@ export interface ElectronAPI {
|
|||||||
getStatus: (projectPath: string) => Promise<GitStatusDto>;
|
getStatus: (projectPath: string) => Promise<GitStatusDto>;
|
||||||
getDiff: (projectPath: string, filePath: string) => Promise<GitDiffDto>;
|
getDiff: (projectPath: string, filePath: string) => Promise<GitDiffDto>;
|
||||||
getDiffContent: (projectPath: string, filePath: string) => Promise<GitDiffContentDto>;
|
getDiffContent: (projectPath: string, filePath: string) => Promise<GitDiffContentDto>;
|
||||||
|
getCommitDiffContent: (projectPath: string, commitHash: string) => Promise<GitCommitDiffContentDto>;
|
||||||
getHistory: (projectPath: string, limit?: number) => Promise<GitHistoryEntry[]>;
|
getHistory: (projectPath: string, limit?: number) => Promise<GitHistoryEntry[]>;
|
||||||
fetch: (projectPath: string) => Promise<GitActionResult>;
|
fetch: (projectPath: string) => Promise<GitActionResult>;
|
||||||
pull: (projectPath: string) => Promise<GitActionResult>;
|
pull: (projectPath: string) => Promise<GitActionResult>;
|
||||||
|
|||||||
@@ -19,6 +19,43 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.git-diff-commit-nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid var(--vscode-editorWidget-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-diff-commit-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-diff-commit-select {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 4px 6px;
|
||||||
|
border: 1px solid var(--vscode-input-border);
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
color: var(--vscode-input-foreground);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-diff-commit-button {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid var(--vscode-button-border);
|
||||||
|
background: var(--vscode-button-secondaryBackground);
|
||||||
|
color: var(--vscode-button-secondaryForeground);
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-diff-commit-button:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.git-diff-message,
|
.git-diff-message,
|
||||||
.git-diff-error {
|
.git-diff-error {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ import { DiffEditor } from '@monaco-editor/react';
|
|||||||
import { useAppStore } from '../../store';
|
import { useAppStore } from '../../store';
|
||||||
import './GitDiffView.css';
|
import './GitDiffView.css';
|
||||||
|
|
||||||
|
interface CommitFileDiff {
|
||||||
|
filePath: string;
|
||||||
|
original: string;
|
||||||
|
modified: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface GitDiffViewProps {
|
interface GitDiffViewProps {
|
||||||
filePath: string;
|
filePath: string;
|
||||||
}
|
}
|
||||||
@@ -35,9 +41,9 @@ function detectLanguage(filePath: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toModelPath(filePath: string, side: 'original' | 'modified'): string {
|
function toModelPath(filePath: string, side: 'original' | 'modified', scope: string): string {
|
||||||
const normalized = filePath.replace(/^\/+/, '');
|
const normalized = filePath.replace(/^\/+/, '');
|
||||||
return `inmemory://model/git-diff/${side}/${normalized}`;
|
return `inmemory://model/git-diff/${scope}/${side}/${normalized}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GitDiffView: React.FC<GitDiffViewProps> = ({ filePath }) => {
|
export const GitDiffView: React.FC<GitDiffViewProps> = ({ filePath }) => {
|
||||||
@@ -46,6 +52,40 @@ export const GitDiffView: React.FC<GitDiffViewProps> = ({ filePath }) => {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [original, setOriginal] = useState('');
|
const [original, setOriginal] = useState('');
|
||||||
const [modified, setModified] = useState('');
|
const [modified, setModified] = useState('');
|
||||||
|
const [commitFiles, setCommitFiles] = useState<CommitFileDiff[]>([]);
|
||||||
|
const [selectedCommitFilePath, setSelectedCommitFilePath] = useState<string>('');
|
||||||
|
const isCommitDiff = filePath.startsWith('commit:');
|
||||||
|
const commitHash = isCommitDiff ? filePath.slice('commit:'.length) : '';
|
||||||
|
const selectedCommitFile = commitFiles.find((entry) => entry.filePath === selectedCommitFilePath) ?? null;
|
||||||
|
const selectedCommitFileIndex = selectedCommitFilePath
|
||||||
|
? commitFiles.findIndex((entry) => entry.filePath === selectedCommitFilePath)
|
||||||
|
: -1;
|
||||||
|
const canSelectPreviousFile = selectedCommitFileIndex > 0;
|
||||||
|
const canSelectNextFile = selectedCommitFileIndex >= 0 && selectedCommitFileIndex < commitFiles.length - 1;
|
||||||
|
const displayedOriginal = selectedCommitFile ? selectedCommitFile.original : original;
|
||||||
|
const displayedModified = selectedCommitFile ? selectedCommitFile.modified : modified;
|
||||||
|
const activeFilePath = selectedCommitFile ? selectedCommitFile.filePath : filePath;
|
||||||
|
const modelScope = isCommitDiff ? `commit-${commitHash}` : 'working-tree';
|
||||||
|
|
||||||
|
const selectPreviousCommitFile = () => {
|
||||||
|
if (!canSelectPreviousFile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const previousFile = commitFiles[selectedCommitFileIndex - 1];
|
||||||
|
if (previousFile) {
|
||||||
|
setSelectedCommitFilePath(previousFile.filePath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectNextCommitFile = () => {
|
||||||
|
if (!canSelectNextFile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextFile = commitFiles[selectedCommitFileIndex + 1];
|
||||||
|
if (nextFile) {
|
||||||
|
setSelectedCommitFilePath(nextFile.filePath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadDiff = async () => {
|
const loadDiff = async () => {
|
||||||
@@ -67,9 +107,27 @@ export const GitDiffView: React.FC<GitDiffViewProps> = ({ filePath }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const diff = await window.electronAPI.git.getDiffContent(projectPath, filePath);
|
if (isCommitDiff) {
|
||||||
setOriginal(diff.original || '');
|
const diff = await window.electronAPI.git.getCommitDiffContent(projectPath, commitHash);
|
||||||
setModified(diff.modified || '');
|
const files = diff.files || [];
|
||||||
|
setCommitFiles(files);
|
||||||
|
|
||||||
|
if (files.length > 0) {
|
||||||
|
setSelectedCommitFilePath(files[0].filePath);
|
||||||
|
setOriginal(files[0].original || '');
|
||||||
|
setModified(files[0].modified || '');
|
||||||
|
} else {
|
||||||
|
setSelectedCommitFilePath('');
|
||||||
|
setOriginal(diff.original || '');
|
||||||
|
setModified(diff.modified || '');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setCommitFiles([]);
|
||||||
|
setSelectedCommitFilePath('');
|
||||||
|
const diff = await window.electronAPI.git.getDiffContent(projectPath, filePath);
|
||||||
|
setOriginal(diff.original || '');
|
||||||
|
setModified(diff.modified || '');
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setError('Failed to load diff.');
|
setError('Failed to load diff.');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -78,12 +136,12 @@ export const GitDiffView: React.FC<GitDiffViewProps> = ({ filePath }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
void loadDiff();
|
void loadDiff();
|
||||||
}, [activeProject, filePath]);
|
}, [activeProject, filePath, isCommitDiff, commitHash]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="git-diff-view">
|
<div className="git-diff-view">
|
||||||
<div className="git-diff-header">Diff: {filePath}</div>
|
<div className="git-diff-header">Diff: {isCommitDiff ? `Commit ${commitHash}` : filePath}</div>
|
||||||
<div className="git-diff-message">Loading diff...</div>
|
<div className="git-diff-message">Loading diff...</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -92,7 +150,7 @@ export const GitDiffView: React.FC<GitDiffViewProps> = ({ filePath }) => {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="git-diff-view">
|
<div className="git-diff-view">
|
||||||
<div className="git-diff-header">Diff: {filePath}</div>
|
<div className="git-diff-header">Diff: {isCommitDiff ? `Commit ${commitHash}` : filePath}</div>
|
||||||
<div className="git-diff-error">{error}</div>
|
<div className="git-diff-error">{error}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -100,16 +158,54 @@ export const GitDiffView: React.FC<GitDiffViewProps> = ({ filePath }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="git-diff-view">
|
<div className="git-diff-view">
|
||||||
<div className="git-diff-header">Diff: {filePath}</div>
|
<div className="git-diff-header">Diff: {isCommitDiff ? `Commit ${commitHash}` : filePath}</div>
|
||||||
|
{isCommitDiff && commitFiles.length > 0 && (
|
||||||
|
<div className="git-diff-commit-nav">
|
||||||
|
<label htmlFor="git-diff-commit-files" className="git-diff-commit-label">
|
||||||
|
Changed files
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="git-diff-commit-button"
|
||||||
|
onClick={selectPreviousCommitFile}
|
||||||
|
disabled={!canSelectPreviousFile}
|
||||||
|
aria-label="Previous file"
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
<select
|
||||||
|
id="git-diff-commit-files"
|
||||||
|
className="git-diff-commit-select"
|
||||||
|
value={selectedCommitFilePath}
|
||||||
|
onChange={(event) => setSelectedCommitFilePath(event.target.value)}
|
||||||
|
aria-label="Changed files"
|
||||||
|
>
|
||||||
|
{commitFiles.map((entry) => (
|
||||||
|
<option key={entry.filePath} value={entry.filePath}>
|
||||||
|
{entry.filePath}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="git-diff-commit-button"
|
||||||
|
onClick={selectNextCommitFile}
|
||||||
|
disabled={!canSelectNextFile}
|
||||||
|
aria-label="Next file"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="git-diff-editor-wrap">
|
<div className="git-diff-editor-wrap">
|
||||||
<DiffEditor
|
<DiffEditor
|
||||||
original={original}
|
original={displayedOriginal}
|
||||||
modified={modified}
|
modified={displayedModified}
|
||||||
originalModelPath={toModelPath(filePath, 'original')}
|
originalModelPath={toModelPath(activeFilePath, 'original', modelScope)}
|
||||||
modifiedModelPath={toModelPath(filePath, 'modified')}
|
modifiedModelPath={toModelPath(activeFilePath, 'modified', modelScope)}
|
||||||
keepCurrentOriginalModel
|
keepCurrentOriginalModel
|
||||||
keepCurrentModifiedModel
|
keepCurrentModifiedModel
|
||||||
language={detectLanguage(filePath)}
|
language={detectLanguage(activeFilePath)}
|
||||||
theme="vs-dark"
|
theme="vs-dark"
|
||||||
height="100%"
|
height="100%"
|
||||||
options={{
|
options={{
|
||||||
|
|||||||
@@ -112,9 +112,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.git-sidebar-history-item {
|
.git-sidebar-history-item {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: var(--vscode-input-background);
|
background: var(--vscode-input-background);
|
||||||
|
color: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-sidebar-history-item:hover {
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.git-sidebar-history-subject {
|
.git-sidebar-history-subject {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const GitSidebar: React.FC = () => {
|
|||||||
const commitMessageInputRef = useRef<HTMLInputElement | null>(null);
|
const commitMessageInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
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 getActionProgressMessage = (action: 'fetch' | 'pull' | 'push' | 'commit'): string => {
|
const getActionProgressMessage = (action: 'fetch' | 'pull' | 'push' | 'commit'): string => {
|
||||||
if (action === 'push') {
|
if (action === 'push') {
|
||||||
@@ -51,6 +52,17 @@ export const GitSidebar: React.FC = () => {
|
|||||||
[openTab],
|
[openTab],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const openCommitDiffTab = useCallback(
|
||||||
|
(commitHash: string, isTransient: boolean) => {
|
||||||
|
openTab({
|
||||||
|
type: 'git-diff',
|
||||||
|
id: getCommitDiffTabId(commitHash),
|
||||||
|
isTransient,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[openTab],
|
||||||
|
);
|
||||||
|
|
||||||
const resolveProjectPath = useCallback(async (): Promise<string | null> => {
|
const resolveProjectPath = useCallback(async (): Promise<string | null> => {
|
||||||
if (!activeProject) {
|
if (!activeProject) {
|
||||||
return null;
|
return null;
|
||||||
@@ -367,14 +379,21 @@ export const GitSidebar: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="git-sidebar-history-list" role="list" aria-label="Version History">
|
<div className="git-sidebar-history-list" role="list" aria-label="Version History">
|
||||||
{historyEntries.map((entry) => (
|
{historyEntries.map((entry) => (
|
||||||
<div key={entry.hash} className="git-sidebar-history-item">
|
<button
|
||||||
|
key={entry.hash}
|
||||||
|
type="button"
|
||||||
|
className="git-sidebar-history-item"
|
||||||
|
onClick={() => openCommitDiffTab(entry.hash, true)}
|
||||||
|
onDoubleClick={() => openCommitDiffTab(entry.hash, false)}
|
||||||
|
title={`${entry.shortHash}: ${entry.subject}`}
|
||||||
|
>
|
||||||
<div className="git-sidebar-history-subject">{entry.subject}</div>
|
<div className="git-sidebar-history-subject">{entry.subject}</div>
|
||||||
<div className="git-sidebar-history-meta">
|
<div className="git-sidebar-history-meta">
|
||||||
<span>{entry.shortHash}</span>
|
<span>{entry.shortHash}</span>
|
||||||
<span>{entry.author}</span>
|
<span>{entry.author}</span>
|
||||||
<span>{new Date(entry.date).toLocaleDateString()}</span>
|
<span>{new Date(entry.date).toLocaleDateString()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,15 +4,36 @@ import './TabBar.css';
|
|||||||
|
|
||||||
const MAX_CHAT_TITLE_LENGTH = 18;
|
const MAX_CHAT_TITLE_LENGTH = 18;
|
||||||
|
|
||||||
|
function getGitDiffResource(tabId: string): string {
|
||||||
|
return tabId.startsWith('git-diff:') ? tabId.slice('git-diff:'.length) : tabId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCommitHashFromGitDiffTabId(tabId: string): string | null {
|
||||||
|
const resource = getGitDiffResource(tabId);
|
||||||
|
if (!resource.startsWith('commit:')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return resource.slice('commit:'.length);
|
||||||
|
}
|
||||||
|
|
||||||
const getTabTitle = (
|
const getTabTitle = (
|
||||||
tab: Tab,
|
tab: Tab,
|
||||||
postTitles: Map<string, string>,
|
postTitles: Map<string, string>,
|
||||||
media: { id: string; originalName: string }[],
|
media: { id: string; originalName: string }[],
|
||||||
chatTitles: Map<string, string>,
|
chatTitles: Map<string, string>,
|
||||||
importDefTitles: Map<string, string>
|
importDefTitles: Map<string, string>,
|
||||||
|
commitTitles: Map<string, string>
|
||||||
): string => {
|
): string => {
|
||||||
if (tab.type === 'git-diff') {
|
if (tab.type === 'git-diff') {
|
||||||
const filePath = tab.id.startsWith('git-diff:') ? tab.id.slice('git-diff:'.length) : tab.id;
|
const filePath = getGitDiffResource(tab.id);
|
||||||
|
const commitHash = getCommitHashFromGitDiffTabId(tab.id);
|
||||||
|
if (commitHash) {
|
||||||
|
const commitTitle = commitTitles.get(commitHash);
|
||||||
|
if (commitTitle) {
|
||||||
|
return commitTitle;
|
||||||
|
}
|
||||||
|
return `Commit ${commitHash.slice(0, 7)}`;
|
||||||
|
}
|
||||||
const filename = filePath.split('/').pop();
|
const filename = filePath.split('/').pop();
|
||||||
return filename || filePath;
|
return filename || filePath;
|
||||||
}
|
}
|
||||||
@@ -138,6 +159,7 @@ export const TabBar: React.FC = () => {
|
|||||||
tabs,
|
tabs,
|
||||||
activeTabId,
|
activeTabId,
|
||||||
media,
|
media,
|
||||||
|
activeProject,
|
||||||
dirtyPosts,
|
dirtyPosts,
|
||||||
sidebarVisible,
|
sidebarVisible,
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
@@ -152,6 +174,7 @@ export const TabBar: React.FC = () => {
|
|||||||
const [postTitles, setPostTitles] = useState<Map<string, string>>(new Map());
|
const [postTitles, setPostTitles] = useState<Map<string, string>>(new Map());
|
||||||
const [chatTitles, setChatTitles] = useState<Map<string, string>>(new Map());
|
const [chatTitles, setChatTitles] = useState<Map<string, string>>(new Map());
|
||||||
const [importDefTitles, setImportDefTitles] = useState<Map<string, string>>(new Map());
|
const [importDefTitles, setImportDefTitles] = useState<Map<string, string>>(new Map());
|
||||||
|
const [commitTitles, setCommitTitles] = useState<Map<string, string>>(new Map());
|
||||||
|
|
||||||
// Fetch post titles from database for post tabs
|
// Fetch post titles from database for post tabs
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -289,6 +312,65 @@ export const TabBar: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Fetch commit subjects for commit-based git-diff tabs
|
||||||
|
useEffect(() => {
|
||||||
|
const commitHashes = tabs
|
||||||
|
.filter((tab) => tab.type === 'git-diff')
|
||||||
|
.map((tab) => getCommitHashFromGitDiffTabId(tab.id))
|
||||||
|
.filter((hash): hash is string => Boolean(hash));
|
||||||
|
|
||||||
|
if (commitHashes.length === 0 || !activeProject) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingHashes = commitHashes.filter((hash) => !commitTitles.has(hash));
|
||||||
|
if (missingHashes.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const fetchCommitTitles = async () => {
|
||||||
|
try {
|
||||||
|
const projectPath = activeProject.dataPath
|
||||||
|
? activeProject.dataPath
|
||||||
|
: await window.electronAPI?.app.getDefaultProjectPath(activeProject.id);
|
||||||
|
|
||||||
|
if (!projectPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = await window.electronAPI?.git.getHistory(projectPath, 200);
|
||||||
|
if (!history || cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCommitTitles((previous) => {
|
||||||
|
const updated = new Map(previous);
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
for (const hash of missingHashes) {
|
||||||
|
const match = history.find((entry) => entry.hash === hash);
|
||||||
|
if (match) {
|
||||||
|
updated.set(hash, `${match.shortHash} ${match.subject}`);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed ? updated : previous;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch commit titles:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void fetchCommitTitles();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [tabs, activeProject]);
|
||||||
|
|
||||||
// Check if arrows are needed based on scroll position
|
// Check if arrows are needed based on scroll position
|
||||||
const updateArrowVisibility = useCallback(() => {
|
const updateArrowVisibility = useCallback(() => {
|
||||||
const container = tabsContainerRef.current;
|
const container = tabsContainerRef.current;
|
||||||
@@ -419,7 +501,7 @@ export const TabBar: React.FC = () => {
|
|||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
const isActive = tab.id === activeTabId;
|
const isActive = tab.id === activeTabId;
|
||||||
const isDirty = tab.type === 'post' && dirtyPosts.has(tab.id);
|
const isDirty = tab.type === 'post' && dirtyPosts.has(tab.id);
|
||||||
const title = getTabTitle(tab, postTitles, media, chatTitles, importDefTitles);
|
const title = getTabTitle(tab, postTitles, media, chatTitles, importDefTitles, commitTitles);
|
||||||
const icon = getTabIcon(tab);
|
const icon = getTabIcon(tab);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -203,6 +203,48 @@ describe('GitEngine', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getCommitDiffContent', () => {
|
||||||
|
it('should return commit patch text in diff content shape', async () => {
|
||||||
|
mockShow.mockResolvedValue([
|
||||||
|
'diff --git a/posts/first.md b/posts/first.md',
|
||||||
|
'index 1234567..89abcde 100644',
|
||||||
|
'--- a/posts/first.md',
|
||||||
|
'+++ b/posts/first.md',
|
||||||
|
'@@ -1 +1 @@',
|
||||||
|
'-old',
|
||||||
|
'+new',
|
||||||
|
'diff --git a/src/main.ts b/src/main.ts',
|
||||||
|
'index 1234567..89abcde 100644',
|
||||||
|
'--- a/src/main.ts',
|
||||||
|
'+++ b/src/main.ts',
|
||||||
|
'@@ -1 +1 @@',
|
||||||
|
'-const oldValue = 1;',
|
||||||
|
'+const newValue = 2;',
|
||||||
|
].join('\n'));
|
||||||
|
|
||||||
|
const result = await gitEngine.getCommitDiffContent('/tmp/project', 'abc123def456');
|
||||||
|
|
||||||
|
expect(mockShow).toHaveBeenCalledWith(['--format=', '--patch', 'abc123def456']);
|
||||||
|
expect(result).toEqual({
|
||||||
|
commitHash: 'abc123def456',
|
||||||
|
original: 'old',
|
||||||
|
modified: 'new',
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
filePath: 'posts/first.md',
|
||||||
|
original: 'old',
|
||||||
|
modified: 'new',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filePath: 'src/main.ts',
|
||||||
|
original: 'const oldValue = 1;',
|
||||||
|
modified: 'const newValue = 2;',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getHistory', () => {
|
describe('getHistory', () => {
|
||||||
it('should return latest commits from git log', async () => {
|
it('should return latest commits from git log', async () => {
|
||||||
mockLog.mockResolvedValue({
|
mockLog.mockResolvedValue({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
import { GitDiffView } from '../../../src/renderer/components/GitDiffView/GitDiffView';
|
import { GitDiffView } from '../../../src/renderer/components/GitDiffView/GitDiffView';
|
||||||
import { useAppStore } from '../../../src/renderer/store';
|
import { useAppStore } from '../../../src/renderer/store';
|
||||||
|
|
||||||
@@ -58,6 +58,23 @@ describe('GitDiffView', () => {
|
|||||||
original: '# old line',
|
original: '# old line',
|
||||||
modified: '# new line',
|
modified: '# new line',
|
||||||
}),
|
}),
|
||||||
|
getCommitDiffContent: vi.fn().mockResolvedValue({
|
||||||
|
commitHash: 'abc123def456',
|
||||||
|
original: '--- posts/first.md\nold',
|
||||||
|
modified: '--- posts/first.md\nnew',
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
filePath: 'posts/first.md',
|
||||||
|
original: 'old',
|
||||||
|
modified: 'new',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filePath: 'src/main.ts',
|
||||||
|
original: 'const oldValue = 1;',
|
||||||
|
modified: 'const newValue = 2;',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
...(window as any).electronAPI?.app,
|
...(window as any).electronAPI?.app,
|
||||||
@@ -79,4 +96,33 @@ describe('GitDiffView', () => {
|
|||||||
expect(screen.getByText('keepOriginal:true')).toBeInTheDocument();
|
expect(screen.getByText('keepOriginal:true')).toBeInTheDocument();
|
||||||
expect(screen.getByText('keepModified:true')).toBeInTheDocument();
|
expect(screen.getByText('keepModified:true')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('loads commit diff content when a commit tab identifier is used', async () => {
|
||||||
|
render(<GitDiffView filePath="commit:abc123def456" />);
|
||||||
|
|
||||||
|
const diffEditor = await screen.findByTestId('monaco-diff-editor');
|
||||||
|
expect(diffEditor).toBeInTheDocument();
|
||||||
|
expect(await screen.findByRole('combobox', { name: /changed files/i })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: /previous file/i })).toBeDisabled();
|
||||||
|
expect(screen.getByRole('button', { name: /next file/i })).toBeEnabled();
|
||||||
|
expect((window as any).electronAPI.git.getCommitDiffContent).toHaveBeenCalledWith('/repo/path', 'abc123def456');
|
||||||
|
expect(diffEditor).toHaveTextContent(/original:\s*old/);
|
||||||
|
expect(diffEditor).toHaveTextContent(/modified:\s*new/);
|
||||||
|
expect(screen.getByText('language:markdown')).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /next file/i }));
|
||||||
|
|
||||||
|
expect(diffEditor).toHaveTextContent(/original:\s*const oldValue = 1;/);
|
||||||
|
expect(diffEditor).toHaveTextContent(/modified:\s*const newValue = 2;/);
|
||||||
|
expect(screen.getByText('language:typescript')).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: /previous file/i })).toBeEnabled();
|
||||||
|
expect(screen.getByRole('button', { name: /next file/i })).toBeDisabled();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /previous file/i }));
|
||||||
|
|
||||||
|
expect(diffEditor).toHaveTextContent(/original:\s*old/);
|
||||||
|
expect(diffEditor).toHaveTextContent(/modified:\s*new/);
|
||||||
|
expect(screen.getByRole('button', { name: /previous file/i })).toBeDisabled();
|
||||||
|
expect(screen.getByRole('button', { name: /next file/i })).toBeEnabled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ describe('GitSidebar', () => {
|
|||||||
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 } }),
|
||||||
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: '' }),
|
||||||
getHistory: vi.fn().mockResolvedValue([]),
|
getHistory: vi.fn().mockResolvedValue([]),
|
||||||
fetch: vi.fn().mockResolvedValue({ success: true }),
|
fetch: vi.fn().mockResolvedValue({ success: true }),
|
||||||
pull: vi.fn().mockResolvedValue({ success: true }),
|
pull: vi.fn().mockResolvedValue({ success: true }),
|
||||||
@@ -171,6 +172,72 @@ describe('GitSidebar', () => {
|
|||||||
expect(getStore().tabs[0]).toMatchObject({ type: 'git-diff', id: 'git-diff:posts/first.md', isTransient: false });
|
expect(getStore().tabs[0]).toMatchObject({ type: 'git-diff', id: 'git-diff:posts/first.md', isTransient: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('single click on a commit opens a transient git-diff commit tab', async () => {
|
||||||
|
(window as any).electronAPI.git.getRepoState = vi.fn().mockResolvedValue({
|
||||||
|
isRepo: true,
|
||||||
|
rootPath: '/repo/path',
|
||||||
|
currentBranch: 'main',
|
||||||
|
hasRemote: true,
|
||||||
|
});
|
||||||
|
(window as any).electronAPI.git.getHistory = vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
hash: 'abc123def456',
|
||||||
|
shortHash: 'abc123d',
|
||||||
|
date: '2026-02-16T10:00:00.000Z',
|
||||||
|
subject: 'feat: add sidebar history click',
|
||||||
|
author: 'Dev One',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
render(<GitSidebar />);
|
||||||
|
|
||||||
|
const commitItem = await screen.findByRole('button', { name: /feat: add sidebar history click/i });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(commitItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getStore().tabs).toHaveLength(1);
|
||||||
|
expect(getStore().tabs[0]).toMatchObject({
|
||||||
|
type: 'git-diff',
|
||||||
|
id: 'git-diff:commit:abc123def456',
|
||||||
|
isTransient: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('double click on a commit opens a persistent git-diff commit tab', async () => {
|
||||||
|
(window as any).electronAPI.git.getRepoState = vi.fn().mockResolvedValue({
|
||||||
|
isRepo: true,
|
||||||
|
rootPath: '/repo/path',
|
||||||
|
currentBranch: 'main',
|
||||||
|
hasRemote: true,
|
||||||
|
});
|
||||||
|
(window as any).electronAPI.git.getHistory = vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
hash: 'abc123def456',
|
||||||
|
shortHash: 'abc123d',
|
||||||
|
date: '2026-02-16T10:00:00.000Z',
|
||||||
|
subject: 'feat: add sidebar history click',
|
||||||
|
author: 'Dev One',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
render(<GitSidebar />);
|
||||||
|
|
||||||
|
const commitItem = await screen.findByRole('button', { name: /feat: add sidebar history click/i });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.doubleClick(commitItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getStore().tabs).toHaveLength(1);
|
||||||
|
expect(getStore().tabs[0]).toMatchObject({
|
||||||
|
type: 'git-diff',
|
||||||
|
id: 'git-diff:commit:abc123def456',
|
||||||
|
isTransient: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('initializes repository and refreshes repo state after clicking Initialize Git', async () => {
|
it('initializes repository and refreshes repo state after clicking Initialize Git', async () => {
|
||||||
const getRepoStateMock = vi
|
const getRepoStateMock = vi
|
||||||
.fn()
|
.fn()
|
||||||
|
|||||||
81
tests/renderer/components/TabBar.test.tsx
Normal file
81
tests/renderer/components/TabBar.test.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { TabBar } from '../../../src/renderer/components/TabBar/TabBar';
|
||||||
|
import { useAppStore } from '../../../src/renderer/store';
|
||||||
|
|
||||||
|
describe('TabBar', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
(window as any).addEventListener = vi.fn();
|
||||||
|
(window as any).removeEventListener = vi.fn();
|
||||||
|
|
||||||
|
if (!(globalThis as any).ResizeObserver) {
|
||||||
|
(globalThis as any).ResizeObserver = class {
|
||||||
|
observe() {}
|
||||||
|
disconnect() {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
useAppStore.setState({
|
||||||
|
activeProject: {
|
||||||
|
id: 'project-1',
|
||||||
|
name: 'Test Project',
|
||||||
|
slug: 'test-project',
|
||||||
|
isActive: true,
|
||||||
|
dataPath: '/repo/path',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
tabs: [
|
||||||
|
{ type: 'git-diff', id: 'git-diff:commit:abc123def456', isTransient: false },
|
||||||
|
],
|
||||||
|
activeTabId: 'git-diff:commit:abc123def456',
|
||||||
|
media: [],
|
||||||
|
dirtyPosts: new Set<string>(),
|
||||||
|
sidebarVisible: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
(window as any).electronAPI = {
|
||||||
|
...(window as any).electronAPI,
|
||||||
|
git: {
|
||||||
|
...(window as any).electronAPI?.git,
|
||||||
|
getHistory: vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
hash: 'abc123def456',
|
||||||
|
shortHash: 'abc123d',
|
||||||
|
date: '2026-02-16T10:00:00.000Z',
|
||||||
|
subject: 'feat: improve commit diff tabs',
|
||||||
|
author: 'Dev One',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
...(window as any).electronAPI?.app,
|
||||||
|
getDefaultProjectPath: vi.fn().mockResolvedValue('/repo/path'),
|
||||||
|
},
|
||||||
|
posts: {
|
||||||
|
...(window as any).electronAPI?.posts,
|
||||||
|
get: vi.fn(),
|
||||||
|
},
|
||||||
|
chat: {
|
||||||
|
...(window as any).electronAPI?.chat,
|
||||||
|
getConversation: vi.fn(),
|
||||||
|
onTitleUpdated: vi.fn(() => () => {}),
|
||||||
|
},
|
||||||
|
importDefinitions: {
|
||||||
|
...(window as any).electronAPI?.importDefinitions,
|
||||||
|
get: vi.fn(),
|
||||||
|
onNameUpdated: vi.fn(() => () => {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders commit subject in git-diff commit tab titles when available', async () => {
|
||||||
|
render(<TabBar />);
|
||||||
|
|
||||||
|
expect(await screen.findByText('abc123d feat: improve commit diff tabs')).toBeInTheDocument();
|
||||||
|
expect((window as any).electronAPI.git.getHistory).toHaveBeenCalledWith('/repo/path', 200);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user