add auto-updater and git workflow enhancements
publish / publish-tauri (, windows-latest) (release) Failing after 2m52s
publish / publish-tauri (, windows-latest) (release) Failing after 2m52s
This change introduces a comprehensive auto-update mechanism for the application and significant improvements to the integrated Git client experience. Key features include: - **Automated Release Workflow:** A new Gitea Actions workflow (`app_builder.yaml`) and a Python `cicd_tool` are added to automatically build, sign, and upload release artifacts to S3-compatible storage (MinIO) upon a new release. This also generates the `latest.json` file required by the updater. - **Tauri Updater Integration:** The `tauri-plugin-updater` is integrated into the application, enabling it to check for and apply updates seamlessly. - **Robust Git Push Handling:** The application now intelligently handles non-fast-forward push failures by prompting the user to perform a pull/merge operation before re-attempting the push. - **Enhanced File Comparison:** A new `compare_file_to_parent` command is introduced, allowing detailed file diffs against a commit's direct parent, including the "empty tree" for initial commits. - **Explorer Panel Improvements:** "Expand All" and "Collapse All" functionality is added to the file explorer for better navigation.
This commit is contained in:
+78
-7
@@ -22,7 +22,7 @@
|
||||
compareCommits,
|
||||
cancelCodeSearch,
|
||||
diffFileAgainstWorkingTree,
|
||||
compareFileToHead,
|
||||
compareFileToParent,
|
||||
getStatus,
|
||||
listBranches,
|
||||
listCommits,
|
||||
@@ -181,6 +181,18 @@
|
||||
try { return JSON.stringify(error) ?? "Unknown error"; } catch { return "Unknown error"; }
|
||||
}
|
||||
|
||||
function isNonFastForwardPushError(message: string): boolean {
|
||||
const value = message.toLowerCase();
|
||||
return value.includes("non-fast-forward")
|
||||
|| value.includes("failed to push some refs")
|
||||
|| value.includes("tip of your current branch is behind")
|
||||
|| value.includes("fetch first");
|
||||
}
|
||||
|
||||
function statusHasConflicts(value: GitStatus | null): boolean {
|
||||
return (value?.files ?? []).some((file) => file.staged === "conflicted" || file.unstaged === "conflicted");
|
||||
}
|
||||
|
||||
async function runOperation(label: string, task: () => Promise<void>) {
|
||||
if (isBusy) return;
|
||||
operation = label;
|
||||
@@ -201,13 +213,17 @@
|
||||
});
|
||||
}
|
||||
|
||||
function defaultExpandedExplorerPaths(files: GitRepositoryFile[]): Set<string> {
|
||||
const expanded = new Set<string>();
|
||||
function allExplorerFolderPaths(files: GitRepositoryFile[]): Set<string> {
|
||||
const folders = new Set<string>();
|
||||
for (const file of files) {
|
||||
const parts = file.path.split(/[\\/]+/).filter(Boolean);
|
||||
if (parts.length > 1) expanded.add(parts[0]);
|
||||
let currentPath = "";
|
||||
for (let index = 0; index < parts.length - 1; index++) {
|
||||
currentPath = currentPath ? `${currentPath}/${parts[index]}` : parts[index];
|
||||
folders.add(currentPath);
|
||||
}
|
||||
}
|
||||
return expanded;
|
||||
return folders;
|
||||
}
|
||||
|
||||
// ── Refresh helpers ────────────────────────────────────────────────────────
|
||||
@@ -231,7 +247,8 @@
|
||||
|
||||
async function refreshExplorerFiles(path = activeRepoPath) {
|
||||
repoFiles = await listRepositoryFiles(path);
|
||||
if (expandedExplorerPaths.size === 0) expandedExplorerPaths = defaultExpandedExplorerPaths(repoFiles);
|
||||
const folderPaths = allExplorerFolderPaths(repoFiles);
|
||||
expandedExplorerPaths = new Set([...expandedExplorerPaths].filter((folder) => folderPaths.has(folder)));
|
||||
if (selectedExplorerPath && !explorerPathExists(repoFiles, selectedExplorerPath)) {
|
||||
selectedExplorerPath = "";
|
||||
selectedExplorerKind = "file";
|
||||
@@ -405,6 +422,50 @@
|
||||
await refreshCommitHistory(activeRepoPath);
|
||||
await refreshFileHistory(activeRepoPath);
|
||||
});
|
||||
|
||||
if (errorMessage && !isAuthError(errorMessage) && isNonFastForwardPushError(errorMessage)) {
|
||||
errorMessage = "";
|
||||
const shouldSync = window.confirm(
|
||||
"Der Remote hat neuere Commits, deshalb wurde der Push abgelehnt.\n\nJetzt Pull/Merge ausfuehren und danach den Push erneut versuchen?",
|
||||
);
|
||||
|
||||
if (!shouldSync) {
|
||||
const message = "Push abgelehnt: Der Remote hat neuere Commits. Pull zuerst ausfuehren, dann erneut pushen.";
|
||||
if (fromStore) errorMessage = message;
|
||||
else credDialogError = message;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fromStore) credDialogError = "";
|
||||
|
||||
await runOperation("Pulling before push", async () => {
|
||||
applyStatus(await pull(activeRepoPath, username, password));
|
||||
await refreshBranchList(activeRepoPath);
|
||||
await refreshCommitHistory(activeRepoPath);
|
||||
await refreshExplorerFiles(activeRepoPath);
|
||||
await refreshFileHistory(activeRepoPath);
|
||||
});
|
||||
|
||||
if (errorMessage) {
|
||||
handleRemoteResult("pull", key, fromStore);
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusHasConflicts(status)) {
|
||||
credDialogOpen = false;
|
||||
credDialogAction = null;
|
||||
errorMessage = "Pull hat Merge-Konflikte erzeugt. Loese die Konflikte, committe den Merge und pushe danach erneut.";
|
||||
return;
|
||||
}
|
||||
|
||||
await runOperation("Pushing after pull", async () => {
|
||||
applyStatus(await push(activeRepoPath, username, password));
|
||||
await refreshBranchList(activeRepoPath);
|
||||
await refreshCommitHistory(activeRepoPath);
|
||||
await refreshFileHistory(activeRepoPath);
|
||||
});
|
||||
}
|
||||
|
||||
handleRemoteResult("push", key, fromStore);
|
||||
}
|
||||
|
||||
@@ -541,7 +602,7 @@
|
||||
async function previewCommitFileFromHistory(target: GitCommit, file: GitCommitFile) {
|
||||
if (!activeRepoPath) return;
|
||||
await runOperation(`Diffing ${file.path}`, async () => {
|
||||
const result = await compareFileToHead(activeRepoPath, target.hash, file.path);
|
||||
const result = await compareFileToParent(activeRepoPath, target.hash, file.path, file.old_path);
|
||||
const matchingFile = result.files.find((diffFile) =>
|
||||
diffFile.path === file.path || diffFile.old_path === file.old_path || diffFile.old_path === file.path,
|
||||
);
|
||||
@@ -561,6 +622,14 @@
|
||||
expandedExplorerPaths = next;
|
||||
}
|
||||
|
||||
function expandAllExplorerFolders() {
|
||||
expandedExplorerPaths = allExplorerFolderPaths(repoFiles);
|
||||
}
|
||||
|
||||
function collapseAllExplorerFolders() {
|
||||
expandedExplorerPaths = new Set();
|
||||
}
|
||||
|
||||
async function selectExplorerNode(node: ExplorerNode) {
|
||||
if (!activeRepoPath || (selectedExplorerPath === node.path && selectedExplorerKind === node.kind)) return;
|
||||
selectedExplorerPath = node.path;
|
||||
@@ -845,6 +914,8 @@
|
||||
{hasRepository}
|
||||
{isBusy}
|
||||
onToggleFolder={toggleExplorerFolder}
|
||||
onExpandAllFolders={expandAllExplorerFolders}
|
||||
onCollapseAllFolders={collapseAllExplorerFolders}
|
||||
onSelectNode={selectExplorerNode}
|
||||
/>
|
||||
</aside>
|
||||
|
||||
+17
@@ -593,6 +593,23 @@
|
||||
|
||||
/* --- Explorer --- */
|
||||
|
||||
.explorer-head-actions { display: flex; align-items: center; gap: 5px; flex: 0 0 auto; }
|
||||
.explorer-bulk-button {
|
||||
width: 26px;
|
||||
min-width: 26px;
|
||||
min-height: 26px;
|
||||
padding: 0;
|
||||
border-color: rgba(65,209,255,0.2);
|
||||
border-radius: 7px;
|
||||
color: var(--color-ink-dim);
|
||||
background: rgba(65,209,255,0.06);
|
||||
}
|
||||
.explorer-bulk-button:hover:not(:disabled) {
|
||||
border-color: rgba(65,209,255,0.45);
|
||||
color: #ffffff;
|
||||
background: rgba(65,209,255,0.13);
|
||||
}
|
||||
|
||||
.explorer-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto minmax(0, 1fr) auto;
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
hasRepository: boolean;
|
||||
isBusy: boolean;
|
||||
onToggleFolder: (node: ExplorerNode) => void;
|
||||
onExpandAllFolders: () => void;
|
||||
onCollapseAllFolders: () => void;
|
||||
onSelectNode: (node: ExplorerNode) => void;
|
||||
}
|
||||
|
||||
@@ -42,6 +44,8 @@
|
||||
hasRepository = false,
|
||||
isBusy = false,
|
||||
onToggleFolder = () => {},
|
||||
onExpandAllFolders = () => {},
|
||||
onCollapseAllFolders = () => {},
|
||||
onSelectNode = () => {},
|
||||
}: Props = $props();
|
||||
|
||||
@@ -152,6 +156,7 @@
|
||||
|
||||
let explorerTree = $derived(buildExplorerTree(repoFiles));
|
||||
let visibleNodes = $derived(flattenExplorerTree(explorerTree, expandedExplorerPaths));
|
||||
let hasFolders = $derived(explorerTree.some((node) => node.kind === "folder"));
|
||||
</script>
|
||||
|
||||
<section class="panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label="File explorer">
|
||||
@@ -160,7 +165,29 @@
|
||||
<span class="eyebrow">Explorer</span>
|
||||
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Files</h2>
|
||||
</div>
|
||||
<span class="pill pill-count">{repoFiles.length}</span>
|
||||
<div class="explorer-head-actions">
|
||||
<button
|
||||
class="explorer-bulk-button"
|
||||
type="button"
|
||||
onclick={onExpandAllFolders}
|
||||
disabled={isBusy || !hasRepository || !hasFolders}
|
||||
title="Alle Ordner aufklappen"
|
||||
aria-label="Alle Ordner aufklappen"
|
||||
>
|
||||
<FolderOpen size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
class="explorer-bulk-button"
|
||||
type="button"
|
||||
onclick={onCollapseAllFolders}
|
||||
disabled={isBusy || !hasRepository || !hasFolders || expandedExplorerPaths.size === 0}
|
||||
title="Alle Ordner zuklappen"
|
||||
aria-label="Alle Ordner zuklappen"
|
||||
>
|
||||
<Folder size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<span class="pill pill-count">{repoFiles.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !hasRepository}
|
||||
|
||||
@@ -128,6 +128,15 @@ export function compareFileToHead(
|
||||
return invoke<GitCommitComparison>("compare_file_to_head", { path, commit, file });
|
||||
}
|
||||
|
||||
export function compareFileToParent(
|
||||
path: string,
|
||||
commit: string,
|
||||
file: string,
|
||||
oldFile: string | null = null,
|
||||
): Promise<GitCommitComparison> {
|
||||
return invoke<GitCommitComparison>("compare_file_to_parent", { path, commit, file, oldFile });
|
||||
}
|
||||
|
||||
export function searchCodeIntroductions(
|
||||
path: string,
|
||||
query: string,
|
||||
|
||||
Reference in New Issue
Block a user