add auto-updater and git workflow enhancements
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:
Christoph Brandau
2026-06-30 13:23:30 +02:00
parent 4aa9a537f0
commit 58bd246221
19 changed files with 1283 additions and 37 deletions
+78 -7
View File
@@ -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>