mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2025-12-06 02:30:47 -08:00
fix: git can't show and discard newly added or deleted files
This commit is contained in:
parent
1806fdcb15
commit
10a9d87733
13 changed files with 359 additions and 29 deletions
|
|
@ -251,18 +251,23 @@
|
|||
"CopyFilePath": "Copy file path",
|
||||
"CopyHash": "Copy submission hash",
|
||||
"CopyRelativeFilePath": "Copy relative file path",
|
||||
"CopySuccess": "Copied to clipboard",
|
||||
"CurrentVersion": "current version",
|
||||
"Date": "date",
|
||||
"Details": "Details",
|
||||
"DiffView": "Difference Comparison",
|
||||
"DiscardChanges": "Discard changes",
|
||||
"DiscardFailed": "Failed to discard changes",
|
||||
"DiscardSuccess": "Changes discarded",
|
||||
"FailedToLoadDiff": "Failed to load differences",
|
||||
"Files": "a file",
|
||||
"FilesChanged": "{{count}} file changed",
|
||||
"FilesChanged_other": "{{count}} files changed",
|
||||
"Hash": "hash value",
|
||||
"IgnoreExtension": "Ignore all .{{ext}} files",
|
||||
"IgnoreFailed": "Failed to add to .gitignore",
|
||||
"IgnoreFile": "Ignore file (add to .gitignore)",
|
||||
"IgnoreSuccess": "Added to .gitignore",
|
||||
"ImageInCommit": "Image at this commit",
|
||||
"ImageNotAvailable": "Image not available",
|
||||
"LoadingFull": "Loading...",
|
||||
|
|
|
|||
|
|
@ -251,18 +251,23 @@
|
|||
"CopyFilePath": "copier le chemin du fichier",
|
||||
"CopyHash": "copier soumettre le hachage",
|
||||
"CopyRelativeFilePath": "copier le chemin relatif",
|
||||
"CopySuccess": "copie réussie",
|
||||
"CurrentVersion": "version actuelle",
|
||||
"Date": "date",
|
||||
"Details": "détails",
|
||||
"DiffView": "comparaison des différences",
|
||||
"DiscardChanges": "abandonner les modifications",
|
||||
"DiscardFailed": "abandonner la modification échouée",
|
||||
"DiscardSuccess": "Modification abandonnée",
|
||||
"FailedToLoadDiff": "Échec du chargement des différences",
|
||||
"Files": "fichier",
|
||||
"FilesChanged": "fichiers modifiés",
|
||||
"FilesChanged_other": "{{count}} fichiers modifiés",
|
||||
"Hash": "valeur de hachage",
|
||||
"IgnoreExtension": "Ignorer tous les fichiers .{{ext}}",
|
||||
"IgnoreFailed": "Échec de l'ajout au .gitignore",
|
||||
"IgnoreFile": "Ignorer les fichiers (ajouter à .gitignore)",
|
||||
"IgnoreSuccess": "Ajouté à .gitignore",
|
||||
"ImageInCommit": "Image à ce commit",
|
||||
"ImageNotAvailable": "Image non disponible",
|
||||
"LoadingFull": "Chargement en cours...",
|
||||
|
|
|
|||
|
|
@ -251,18 +251,23 @@
|
|||
"CopyFilePath": "ファイルパスをコピー",
|
||||
"CopyHash": "コピーしてハッシュを提出",
|
||||
"CopyRelativeFilePath": "相対パスをコピー",
|
||||
"CopySuccess": "コピー成功",
|
||||
"CurrentVersion": "現在のバージョン",
|
||||
"Date": "日付",
|
||||
"Details": "詳細",
|
||||
"DiffView": "差異比較",
|
||||
"DiscardChanges": "修正を放棄する",
|
||||
"DiscardFailed": "修正の放棄に失敗しました",
|
||||
"DiscardSuccess": "修正を放棄しました",
|
||||
"FailedToLoadDiff": "差分の読み込みに失敗しました",
|
||||
"Files": "ファイル",
|
||||
"FilesChanged": "変更されたファイル",
|
||||
"FilesChanged_other": "{{count}} 個のファイルに変更があります",
|
||||
"Hash": "ハッシュ値",
|
||||
"IgnoreExtension": "すべての .{{ext}} ファイルを無視する",
|
||||
"IgnoreFailed": ".gitignore への追加に失敗しました",
|
||||
"IgnoreFile": "ファイルを無視(.gitignoreに追加)",
|
||||
"IgnoreSuccess": ".gitignoreに追加されました",
|
||||
"ImageInCommit": "このコミット時の画像",
|
||||
"ImageNotAvailable": "画像が利用できません",
|
||||
"LoadingFull": "読み込み中...",
|
||||
|
|
|
|||
|
|
@ -251,18 +251,23 @@
|
|||
"CopyFilePath": "скопировать путь к файлу",
|
||||
"CopyHash": "скопировать хэш отправки",
|
||||
"CopyRelativeFilePath": "скопировать относительный путь",
|
||||
"CopySuccess": "Копирование успешно",
|
||||
"CurrentVersion": "текущая версия",
|
||||
"Date": "дата",
|
||||
"Details": "подробности",
|
||||
"DiffView": "сравнение различий",
|
||||
"DiscardChanges": "отказаться от изменений",
|
||||
"DiscardFailed": "Отказаться от изменения неудачи",
|
||||
"DiscardSuccess": "Отказался от изменений",
|
||||
"FailedToLoadDiff": "Не удалось загрузить различия",
|
||||
"Files": "файл",
|
||||
"FilesChanged": "измененные файлы",
|
||||
"FilesChanged_other": "{{count}} файлов изменено",
|
||||
"Hash": "хэш-значение",
|
||||
"IgnoreExtension": "Игнорировать все файлы .{{ext}}",
|
||||
"IgnoreFailed": "Не удалось добавить в .gitignore",
|
||||
"IgnoreFile": "Игнорировать файлы (добавить в .gitignore)",
|
||||
"IgnoreSuccess": "добавлено в .gitignore",
|
||||
"ImageInCommit": "Изображение в этом коммите",
|
||||
"ImageNotAvailable": "Изображение недоступно",
|
||||
"LoadingFull": "Загрузка...",
|
||||
|
|
|
|||
|
|
@ -251,18 +251,23 @@
|
|||
"CopyFilePath": "复制文件路径",
|
||||
"CopyHash": "复制提交哈希",
|
||||
"CopyRelativeFilePath": "复制相对路径",
|
||||
"CopySuccess": "复制成功",
|
||||
"CurrentVersion": "当前版本",
|
||||
"Date": "日期",
|
||||
"Details": "详情",
|
||||
"DiffView": "差异对比",
|
||||
"DiscardChanges": "放弃修改",
|
||||
"DiscardFailed": "放弃修改失败",
|
||||
"DiscardSuccess": "已放弃修改",
|
||||
"FailedToLoadDiff": "加载差异失败",
|
||||
"Files": "文件",
|
||||
"FilesChanged": "变更了 {{count}} 个文件",
|
||||
"FilesChanged_other": "{{count}} 个文件有改动",
|
||||
"Hash": "哈希值",
|
||||
"IgnoreExtension": "忽略所有 .{{ext}} 文件",
|
||||
"IgnoreFailed": "添加到 .gitignore 失败",
|
||||
"IgnoreFile": "忽略文件(添加到 .gitignore)",
|
||||
"IgnoreSuccess": "已添加到 .gitignore",
|
||||
"ImageInCommit": "此提交时的图片",
|
||||
"ImageNotAvailable": "图片不可用",
|
||||
"LoadingFull": "加载中...",
|
||||
|
|
|
|||
|
|
@ -251,18 +251,23 @@
|
|||
"CopyFilePath": "複製檔案路徑",
|
||||
"CopyHash": "複製提交哈希",
|
||||
"CopyRelativeFilePath": "複製相對路徑",
|
||||
"CopySuccess": "複製成功",
|
||||
"CurrentVersion": "目前版本",
|
||||
"Date": "日期",
|
||||
"Details": "詳情",
|
||||
"DiffView": "差異對比",
|
||||
"DiscardChanges": "放棄修改",
|
||||
"DiscardFailed": "放棄修改失敗",
|
||||
"DiscardSuccess": "已放棄修改",
|
||||
"FailedToLoadDiff": "載入差異失敗",
|
||||
"Files": "檔案",
|
||||
"FilesChanged": "變更了 {{count}} 個文件",
|
||||
"FilesChanged_other": "{{count}} 個檔案有變更",
|
||||
"Hash": "雜湊值",
|
||||
"IgnoreExtension": "忽略所有 .{{ext}} 檔案",
|
||||
"IgnoreFailed": "添加到 .gitignore 失敗",
|
||||
"IgnoreFile": "忽略檔案(新增至 .gitignore)",
|
||||
"IgnoreSuccess": "已添加到 .gitignore",
|
||||
"ImageInCommit": "此提交時的圖片",
|
||||
"ImageNotAvailable": "圖片不可用",
|
||||
"LoadingFull": "載入中...",
|
||||
|
|
|
|||
|
|
@ -120,11 +120,29 @@ export async function getGitLog(repoPath: string, options: IGitLogOptions = {}):
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse git status code to file status
|
||||
*/
|
||||
function parseGitStatusCode(statusCode: string): import('./interface').GitFileStatus {
|
||||
const index = statusCode[0];
|
||||
const workTree = statusCode[1];
|
||||
|
||||
// Check for specific patterns
|
||||
if (statusCode === '??') return 'untracked';
|
||||
if (index === 'A' || workTree === 'A') return 'added';
|
||||
if (index === 'D' || workTree === 'D') return 'deleted';
|
||||
if (index === 'R' || workTree === 'R') return 'renamed';
|
||||
if (index === 'C' || workTree === 'C') return 'copied';
|
||||
if (index === 'M' || workTree === 'M') return 'modified';
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get files changed in a specific commit
|
||||
* If commitHash is empty, returns uncommitted changes
|
||||
*/
|
||||
export async function getCommitFiles(repoPath: string, commitHash: string): Promise<string[]> {
|
||||
export async function getCommitFiles(repoPath: string, commitHash: string): Promise<Array<import('./interface').IFileWithStatus>> {
|
||||
// Handle uncommitted changes
|
||||
if (!commitHash || commitHash === '') {
|
||||
const result = await gitExec(['-c', 'core.quotePath=false', 'status', '--porcelain'], repoPath);
|
||||
|
|
@ -139,22 +157,29 @@ export async function getCommitFiles(repoPath: string, commitHash: string): Prom
|
|||
.filter((line: string) => line.length > 0)
|
||||
.map((line: string) => {
|
||||
if (line.length <= 3) {
|
||||
return line.trim();
|
||||
return { path: line.trim(), status: 'unknown' as import('./interface').GitFileStatus };
|
||||
}
|
||||
|
||||
// Parse git status format: "XY filename"
|
||||
// XY is two-letter status code, filename starts at position 3
|
||||
const statusCode = line.slice(0, 2);
|
||||
const rawPath = line.slice(3);
|
||||
|
||||
// Handle rename format: "old -> new" – we want the new path
|
||||
const renameParts = rawPath.split(' -> ');
|
||||
return renameParts[renameParts.length - 1].trim();
|
||||
const filePath = renameParts[renameParts.length - 1].trim();
|
||||
|
||||
return {
|
||||
path: filePath,
|
||||
status: parseGitStatusCode(statusCode),
|
||||
};
|
||||
})
|
||||
.filter((line: string) => line.length > 0);
|
||||
.filter((item) => item.path.length > 0);
|
||||
}
|
||||
|
||||
// For committed changes, use diff-tree with --name-status to get file status
|
||||
const result = await gitExec(
|
||||
['-c', 'core.quotePath=false', 'diff-tree', '--no-commit-id', '--name-only', '-r', commitHash],
|
||||
['-c', 'core.quotePath=false', 'diff-tree', '--no-commit-id', '--name-status', '-r', commitHash],
|
||||
repoPath,
|
||||
);
|
||||
|
||||
|
|
@ -165,7 +190,22 @@ export async function getCommitFiles(repoPath: string, commitHash: string): Prom
|
|||
return result.stdout
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((line: string) => line.length > 0);
|
||||
.filter((line: string) => line.length > 0)
|
||||
.map((line: string) => {
|
||||
// Format: "STATUS\tFILENAME" or "STATUS\tOLDNAME\tNEWNAME" for renames
|
||||
const parts = line.split('\t');
|
||||
const statusChar = parts[0];
|
||||
const filePath = parts[parts.length - 1]; // Use last part for renames
|
||||
|
||||
let status: import('./interface').GitFileStatus = 'unknown';
|
||||
if (statusChar === 'A') status = 'added';
|
||||
else if (statusChar === 'M') status = 'modified';
|
||||
else if (statusChar === 'D') status = 'deleted';
|
||||
else if (statusChar.startsWith('R')) status = 'renamed';
|
||||
else if (statusChar.startsWith('C')) status = 'copied';
|
||||
|
||||
return { path: filePath, status };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -221,6 +261,40 @@ export async function getFileDiff(
|
|||
}
|
||||
}
|
||||
|
||||
// Check if file is deleted
|
||||
const isDeleted = statusCode.includes('D');
|
||||
|
||||
if (isDeleted) {
|
||||
// For deleted files, show the deletion diff
|
||||
const result = await gitExec(
|
||||
['diff', 'HEAD', '--', filePath],
|
||||
repoPath,
|
||||
);
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
// If diff fails, try to show the file content from HEAD
|
||||
const headContent = await gitExec(
|
||||
['show', `HEAD:${filePath}`],
|
||||
repoPath,
|
||||
);
|
||||
|
||||
if (headContent.exitCode === 0) {
|
||||
const diff = [
|
||||
`diff --git a/${filePath} b/${filePath}`,
|
||||
'deleted file mode 100644',
|
||||
`--- a/${filePath}`,
|
||||
'+++ /dev/null',
|
||||
...headContent.stdout.split(/\r?\n/).map(line => `-${line}`),
|
||||
].join('\n');
|
||||
return truncateDiff(diff, maxLines, maxChars);
|
||||
}
|
||||
|
||||
throw new Error(`Failed to get diff for deleted file: ${result.stderr}`);
|
||||
}
|
||||
|
||||
return truncateDiff(result.stdout, maxLines, maxChars);
|
||||
}
|
||||
|
||||
const result = await gitExec(
|
||||
['diff', 'HEAD', '--', filePath],
|
||||
repoPath,
|
||||
|
|
@ -271,6 +345,26 @@ export async function getFileContent(
|
|||
if (!commitHash) {
|
||||
const absolutePath = path.join(repoPath, filePath);
|
||||
|
||||
// Check if file exists in working tree
|
||||
try {
|
||||
await fs.access(absolutePath);
|
||||
} catch {
|
||||
// File doesn't exist (deleted), try to get from HEAD
|
||||
try {
|
||||
const result = await gitExec(
|
||||
['show', `HEAD:${filePath}`],
|
||||
repoPath,
|
||||
);
|
||||
|
||||
if (result.exitCode === 0) {
|
||||
return truncateContent(result.stdout, maxLines, maxChars);
|
||||
}
|
||||
} catch {
|
||||
// Silently fail and throw main error
|
||||
}
|
||||
throw new Error(`File not found: ${filePath} (deleted)`);
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(absolutePath, 'utf-8');
|
||||
return truncateContent(content, maxLines, maxChars);
|
||||
|
|
@ -484,13 +578,43 @@ export async function revertCommit(repoPath: string, commitHash: string, commitM
|
|||
}
|
||||
|
||||
/**
|
||||
* Discard changes for a specific file (restore from HEAD)
|
||||
* Discard changes for a specific file (restore from HEAD or delete if untracked)
|
||||
*/
|
||||
export async function discardFileChanges(repoPath: string, filePath: string): Promise<void> {
|
||||
const result = await gitExec(['checkout', 'HEAD', '--', filePath], repoPath);
|
||||
// First check the file status to determine if it's untracked (new file)
|
||||
const statusResult = await gitExec(['-c', 'core.quotePath=false', 'status', '--porcelain', '--', filePath], repoPath);
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Failed to discard changes: ${result.stderr}`);
|
||||
if (statusResult.exitCode !== 0) {
|
||||
throw new Error(`Failed to check file status: ${statusResult.stderr}`);
|
||||
}
|
||||
|
||||
const statusLine = statusResult.stdout.trim();
|
||||
|
||||
// If empty, file is not modified
|
||||
if (!statusLine) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse status code (first two characters)
|
||||
const statusCode = statusLine.slice(0, 2);
|
||||
|
||||
// Check if file is untracked (new file not yet added)
|
||||
// Status code "??" means untracked
|
||||
if (statusCode === '??') {
|
||||
// For untracked files, we need to delete them
|
||||
const fullPath = path.join(repoPath, filePath);
|
||||
try {
|
||||
await fs.unlink(fullPath);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to delete untracked file: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
} else {
|
||||
// For tracked files, use git checkout to restore from HEAD
|
||||
const result = await gitExec(['checkout', 'HEAD', '--', filePath], repoPath);
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Failed to discard changes: ${result.stderr}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -408,6 +408,8 @@ export class Git implements IGitService {
|
|||
|
||||
public async addToGitignore(wikiFolderPath: string, pattern: string): Promise<void> {
|
||||
await gitOperations.addToGitignore(wikiFolderPath, pattern);
|
||||
// Notify git state change to refresh git log
|
||||
this.notifyGitStateChange(wikiFolderPath, 'file-change');
|
||||
}
|
||||
|
||||
public async isAIGenerateBackupTitleEnabled(): Promise<boolean> {
|
||||
|
|
|
|||
|
|
@ -65,6 +65,13 @@ export interface IFileDiffResult {
|
|||
isTruncated: boolean;
|
||||
}
|
||||
|
||||
export type GitFileStatus = 'added' | 'modified' | 'deleted' | 'renamed' | 'copied' | 'untracked' | 'unknown';
|
||||
|
||||
export interface IFileWithStatus {
|
||||
path: string;
|
||||
status: GitFileStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Git state change event
|
||||
*/
|
||||
|
|
@ -117,7 +124,7 @@ export interface IGitService {
|
|||
/**
|
||||
* Get files changed in a specific commit
|
||||
*/
|
||||
getCommitFiles(wikiFolderPath: string, commitHash: string): Promise<string[]>;
|
||||
getCommitFiles(wikiFolderPath: string, commitHash: string): Promise<IFileWithStatus[]>;
|
||||
/**
|
||||
* Get the diff for a specific file in a commit
|
||||
* @param maxLines - Maximum number of lines to return (default: 500)
|
||||
|
|
|
|||
|
|
@ -53,6 +53,67 @@ const ActionsWrapper = styled(Box)`
|
|||
gap: 12px;
|
||||
`;
|
||||
|
||||
const FileStatusBadge = styled(Box)<{ $status?: string }>`
|
||||
display: inline-block;
|
||||
font-size: 0.6rem;
|
||||
padding: 1px 4px;
|
||||
margin-right: 4px;
|
||||
border-radius: 2px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
${({ $status, theme }) => {
|
||||
const isDark = theme.palette.mode === 'dark';
|
||||
switch ($status) {
|
||||
case 'added':
|
||||
case 'untracked':
|
||||
return isDark
|
||||
? `
|
||||
background-color: rgba(46, 160, 67, 0.3);
|
||||
color: #7ee787;
|
||||
`
|
||||
: `
|
||||
background-color: rgba(46, 160, 67, 0.2);
|
||||
color: #116329;
|
||||
`;
|
||||
case 'deleted':
|
||||
return isDark
|
||||
? `
|
||||
background-color: rgba(248, 81, 73, 0.3);
|
||||
color: #ffa198;
|
||||
`
|
||||
: `
|
||||
background-color: rgba(248, 81, 73, 0.2);
|
||||
color: #82071e;
|
||||
`;
|
||||
case 'modified':
|
||||
return isDark
|
||||
? `
|
||||
background-color: rgba(187, 128, 9, 0.3);
|
||||
color: #f0b83f;
|
||||
`
|
||||
: `
|
||||
background-color: rgba(187, 128, 9, 0.2);
|
||||
color: #7d4e00;
|
||||
`;
|
||||
case 'renamed':
|
||||
return isDark
|
||||
? `
|
||||
background-color: rgba(56, 139, 253, 0.3);
|
||||
color: #79c0ff;
|
||||
`
|
||||
: `
|
||||
background-color: rgba(56, 139, 253, 0.2);
|
||||
color: #0969da;
|
||||
`;
|
||||
default:
|
||||
return `
|
||||
background-color: ${theme.palette.action.hover};
|
||||
color: ${theme.palette.text.secondary};
|
||||
`;
|
||||
}
|
||||
}}
|
||||
`;
|
||||
|
||||
interface ICommitDetailsPanelProps {
|
||||
commit: GitLogEntry | null;
|
||||
isLatestCommit?: boolean;
|
||||
|
|
@ -241,13 +302,18 @@ export function CommitDetailsPanel(
|
|||
{fileChanges.map((file, index) => (
|
||||
<ListItem key={index} disablePadding>
|
||||
<ListItemButton
|
||||
selected={file === selectedFile}
|
||||
selected={file.path === selectedFile}
|
||||
onClick={() => {
|
||||
onFileSelect?.(file === selectedFile ? null : file);
|
||||
onFileSelect?.(file.path === selectedFile ? null : file.path);
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={file}
|
||||
primary={
|
||||
<>
|
||||
<FileStatusBadge $status={file.status}>{file.status.charAt(0)}</FileStatusBadge>
|
||||
{file.path}
|
||||
</>
|
||||
}
|
||||
slotProps={{
|
||||
primary: {
|
||||
variant: 'body2',
|
||||
|
|
|
|||
|
|
@ -136,9 +136,10 @@ interface IFileDiffPanelProps {
|
|||
commitHash: string;
|
||||
filePath: string | null;
|
||||
onDiscardSuccess?: () => void;
|
||||
showSnackbar?: (message: string, severity: 'success' | 'error' | 'info') => void;
|
||||
}
|
||||
|
||||
export function FileDiffPanel({ commitHash, filePath, onDiscardSuccess }: IFileDiffPanelProps): React.JSX.Element {
|
||||
export function FileDiffPanel({ commitHash, filePath, onDiscardSuccess, showSnackbar: showSnackbarFromParent }: IFileDiffPanelProps): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const [diff, setDiff] = useState<string>('');
|
||||
const [fileContent, setFileContent] = useState<string>('');
|
||||
|
|
@ -153,6 +154,9 @@ export function FileDiffPanel({ commitHash, filePath, onDiscardSuccess }: IFileD
|
|||
const [isLoadingFullDiff, setIsLoadingFullDiff] = useState(false);
|
||||
const [isLoadingFullContent, setIsLoadingFullContent] = useState(false);
|
||||
|
||||
// Use parent's showSnackbar if provided, otherwise create local one
|
||||
const showSnackbar = showSnackbarFromParent ?? (() => {});
|
||||
|
||||
const getWorkspace = async () => {
|
||||
const meta = window.meta();
|
||||
const workspaceID = (meta as { workspaceID?: string }).workspaceID;
|
||||
|
|
@ -192,11 +196,13 @@ export function FileDiffPanel({ commitHash, filePath, onDiscardSuccess }: IFileD
|
|||
|
||||
try {
|
||||
await window.service.git.discardFileChanges(workspace.wikiFolderLocation, filePath);
|
||||
showSnackbar(t('GitLog.DiscardSuccess'), 'success');
|
||||
// Clear selection and trigger refresh
|
||||
onDiscardSuccess?.();
|
||||
} catch (error) {
|
||||
console.error('Failed to discard changes:', error);
|
||||
// TODO: Show error message
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
showSnackbar(t('GitLog.DiscardFailed') + ': ' + errorMessage, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -207,10 +213,13 @@ export function FileDiffPanel({ commitHash, filePath, onDiscardSuccess }: IFileD
|
|||
|
||||
try {
|
||||
await window.service.git.addToGitignore(workspace.wikiFolderLocation, filePath);
|
||||
// TODO: Show success message
|
||||
showSnackbar(t('GitLog.IgnoreSuccess'), 'success');
|
||||
// Trigger refresh after adding to .gitignore
|
||||
onDiscardSuccess?.();
|
||||
} catch (error) {
|
||||
console.error('Failed to add to .gitignore:', error);
|
||||
// TODO: Show error message
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
showSnackbar(t('GitLog.IgnoreFailed') + ': ' + errorMessage, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -221,10 +230,13 @@ export function FileDiffPanel({ commitHash, filePath, onDiscardSuccess }: IFileD
|
|||
|
||||
try {
|
||||
await window.service.git.addToGitignore(workspace.wikiFolderLocation, `*.${fileExtension}`);
|
||||
// TODO: Show success message
|
||||
showSnackbar(t('GitLog.IgnoreSuccess'), 'success');
|
||||
// Trigger refresh after adding to .gitignore
|
||||
onDiscardSuccess?.();
|
||||
} catch (error) {
|
||||
console.error('Failed to add extension to .gitignore:', error);
|
||||
// TODO: Show error message
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
showSnackbar(t('GitLog.IgnoreFailed') + ': ' + errorMessage, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -235,13 +247,13 @@ export function FileDiffPanel({ commitHash, filePath, onDiscardSuccess }: IFileD
|
|||
|
||||
const fullPath = `${workspace.wikiFolderLocation}/${filePath}`;
|
||||
await navigator.clipboard.writeText(fullPath);
|
||||
// TODO: Show success message
|
||||
showSnackbar(t('GitLog.CopySuccess'), 'success');
|
||||
};
|
||||
|
||||
const handleCopyRelativePath = async () => {
|
||||
if (!filePath) return;
|
||||
await navigator.clipboard.writeText(filePath);
|
||||
// TODO: Show success message
|
||||
showSnackbar(t('GitLog.CopySuccess'), 'success');
|
||||
};
|
||||
|
||||
const handleShowInExplorer = async () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { Helmet } from '@dr.pogodin/react-helmet';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Box from '@mui/material/Box';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Container from '@mui/material/Container';
|
||||
import Snackbar from '@mui/material/Snackbar';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Table from '@mui/material/Table';
|
||||
|
|
@ -108,18 +110,67 @@ const LoadingContainer = styled(Box)`
|
|||
height: 100%;
|
||||
`;
|
||||
|
||||
const FileChip = styled(Box)`
|
||||
const FileChip = styled(Box)<{ $status?: string }>`
|
||||
display: inline-block;
|
||||
font-size: 0.7rem;
|
||||
font-family: monospace;
|
||||
padding: 2px 4px;
|
||||
margin: 2px;
|
||||
background-color: ${({ theme }) => theme.palette.action.hover};
|
||||
border-radius: 3px;
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
${({ $status, theme }) => {
|
||||
const isDark = theme.palette.mode === 'dark';
|
||||
switch ($status) {
|
||||
case 'added':
|
||||
case 'untracked':
|
||||
return isDark
|
||||
? `
|
||||
background-color: rgba(46, 160, 67, 0.3);
|
||||
color: #7ee787;
|
||||
`
|
||||
: `
|
||||
background-color: rgba(46, 160, 67, 0.2);
|
||||
color: #116329;
|
||||
`;
|
||||
case 'deleted':
|
||||
return isDark
|
||||
? `
|
||||
background-color: rgba(248, 81, 73, 0.3);
|
||||
color: #ffa198;
|
||||
`
|
||||
: `
|
||||
background-color: rgba(248, 81, 73, 0.2);
|
||||
color: #82071e;
|
||||
`;
|
||||
case 'modified':
|
||||
return isDark
|
||||
? `
|
||||
background-color: rgba(187, 128, 9, 0.3);
|
||||
color: #f0b83f;
|
||||
`
|
||||
: `
|
||||
background-color: rgba(187, 128, 9, 0.2);
|
||||
color: #7d4e00;
|
||||
`;
|
||||
case 'renamed':
|
||||
return isDark
|
||||
? `
|
||||
background-color: rgba(56, 139, 253, 0.3);
|
||||
color: #79c0ff;
|
||||
`
|
||||
: `
|
||||
background-color: rgba(56, 139, 253, 0.2);
|
||||
color: #0969da;
|
||||
`;
|
||||
default:
|
||||
return `
|
||||
background-color: ${theme.palette.action.hover};
|
||||
`;
|
||||
}
|
||||
}}
|
||||
`;
|
||||
|
||||
interface ICommitTableRowProps {
|
||||
|
|
@ -158,10 +209,10 @@ function CommitTableRow({ commit, selected, commitDate, onSelect }: ICommitTable
|
|||
<TableCell>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||
{displayFiles.map((file, index) => {
|
||||
const fileName = file.split('/').pop() || file;
|
||||
const fileName = file.path.split('/').pop() || file.path;
|
||||
return (
|
||||
<Tooltip key={index} title={file} placement='top'>
|
||||
<FileChip>{fileName}</FileChip>
|
||||
<Tooltip key={index} title={`${file.path} (${file.status})`} placement='top'>
|
||||
<FileChip $status={file.status}>{fileName}</FileChip>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
|
|
@ -195,6 +246,19 @@ export default function GitHistory(): React.JSX.Element {
|
|||
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||||
const [viewMode, setViewMode] = useState<'current' | 'all'>('current');
|
||||
const [shouldSelectFirst, setShouldSelectFirst] = useState(false);
|
||||
const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity: 'success' | 'error' | 'info' }>({
|
||||
open: false,
|
||||
message: '',
|
||||
severity: 'info',
|
||||
});
|
||||
|
||||
const showSnackbar = (message: string, severity: 'success' | 'error' | 'info' = 'info') => {
|
||||
setSnackbar({ open: true, message, severity });
|
||||
};
|
||||
|
||||
const handleCloseSnackbar = () => {
|
||||
setSnackbar(previous => ({ ...previous, open: false }));
|
||||
};
|
||||
|
||||
// Create a tooltip wrapper that passes the translation function
|
||||
// The props coming from react-git-log don't include 't', so we add it
|
||||
|
|
@ -387,9 +451,21 @@ export default function GitHistory(): React.JSX.Element {
|
|||
// Trigger git log refresh after discard
|
||||
setShouldSelectFirst(true);
|
||||
}}
|
||||
showSnackbar={showSnackbar}
|
||||
/>
|
||||
</DiffPanelWrapper>
|
||||
</ContentWrapper>
|
||||
|
||||
<Snackbar
|
||||
open={snackbar.open}
|
||||
autoHideDuration={4000}
|
||||
onClose={handleCloseSnackbar}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert onClose={handleCloseSnackbar} severity={snackbar.severity} sx={{ width: '100%' }}>
|
||||
{snackbar.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,19 @@ export interface CommitAuthor {
|
|||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* File status in git
|
||||
*/
|
||||
export type GitFileStatus = 'added' | 'modified' | 'deleted' | 'renamed' | 'copied' | 'untracked' | 'unknown';
|
||||
|
||||
/**
|
||||
* File with status information
|
||||
*/
|
||||
export interface FileWithStatus {
|
||||
path: string;
|
||||
status: GitFileStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a single entry in the git log.
|
||||
*/
|
||||
|
|
@ -45,7 +58,7 @@ export interface GitLogEntry {
|
|||
*/
|
||||
parents: string[];
|
||||
/**
|
||||
* Array of file paths changed in this commit.
|
||||
* Array of files with status changed in this commit.
|
||||
*/
|
||||
files?: string[];
|
||||
files?: FileWithStatus[];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue