Merge branch 'new_featrues'

This commit is contained in:
Christoph Brandau
2026-07-20 23:05:00 +02:00
14 changed files with 882 additions and 80 deletions
+18 -3
View File
@@ -11,6 +11,7 @@
RefreshCw,
Search,
Upload,
Settings2,
} from "@lucide/svelte";
export let hasRepository: boolean = false;
@@ -28,8 +29,12 @@
export let onInteractiveRebase: () => void = () => {};
export let onReflog: () => void = () => {};
export let onOpenInExplorer: () => void = () => {};
export let onFetchPrune: () => void = () => {};
export let onForcePush: () => void = () => {};
export let onSyncOptions: () => void = () => {};
let historyOpen = false;
let syncOpen = false;
let toolbarElement: HTMLDivElement;
$: isGerman = language === "de";
@@ -40,9 +45,7 @@
}
function handleWindowClick(event: MouseEvent) {
if (historyOpen && toolbarElement && !toolbarElement.contains(event.target as Node)) {
historyOpen = false;
}
if (toolbarElement && !toolbarElement.contains(event.target as Node)) { historyOpen = false; syncOpen = false; }
}
function handleWindowKeydown(event: KeyboardEvent) {
@@ -108,6 +111,18 @@
<span class="repo-action-label">Push</span>
{#if ahead > 0}<span class="repo-action-count ahead">{ahead}</span>{/if}
</button>
<div class="repo-history-wrap">
<button class="repo-action" type="button" onclick={() => { syncOpen = !syncOpen; historyOpen = false; }} disabled={!hasRepository || isBusy} aria-label={isGerman ? "Sync-Optionen" : "Sync options"} aria-haspopup="menu">
<ChevronDown size={14} aria-hidden="true" />
</button>
{#if syncOpen}
<div class="repo-history-menu" role="menu">
<button type="button" role="menuitem" onclick={() => { syncOpen = false; onFetchPrune(); }}><CloudDownload size={15} /><span><strong>Fetch + Prune</strong><small>{isGerman ? "Veraltete Remote-Branches entfernen" : "Remove stale remote branches"}</small></span></button>
<button type="button" role="menuitem" onclick={() => { syncOpen = false; onForcePush(); }}><Upload size={15} /><span><strong>Force with lease</strong><small>{isGerman ? "Sicheres Pushen nach Rebase" : "Safe push after rebase"}</small></span></button>
<button type="button" role="menuitem" onclick={() => { syncOpen = false; onSyncOptions(); }}><Settings2 size={15} /><span><strong>{isGerman ? "Remotes & Strategien" : "Remotes & strategies"}</strong><small>{isGerman ? "Upstream, Pull und Remote verwalten" : "Manage upstream, pull and remotes"}</small></span></button>
</div>
{/if}
</div>
</div>
</div>
@@ -18,7 +18,8 @@
onClose = () => {},
}: Props = $props();
let title = $derived(force ? "Force delete branch?" : "Delete branch?");
let title = $derived(branch.remote ? "Delete remote branch?" : force ? "Force delete branch?" : "Delete branch?");
let remoteParts = $derived(branch.remote ? branch.name.split(/\/(.+)/) : []);
</script>
@@ -26,7 +27,7 @@
<div class="dialog branch-delete-dialog" role="dialog" aria-modal="true" aria-label={title}>
<header class="dialog-header">
<div>
<span class="eyebrow">{force ? "Force delete" : "Delete branch"}</span>
<span class="eyebrow">{branch.remote ? "Remote branch" : force ? "Force delete" : "Delete branch"}</span>
<p class="dialog-title">{title}</p>
</div>
<button class="btn-sm dialog-close" type="button" onclick={onClose} disabled={isBusy} aria-label="Close">
@@ -41,7 +42,9 @@
<div class="discard-confirm-copy">
<p>
{#if force}
{#if branch.remote}
Delete this branch from the remote server?
{:else if force}
This branch is not fully merged. Force deleting removes the branch pointer even if some commits are only reachable from this branch.
{:else}
Delete this local branch from the repository?
@@ -52,7 +55,9 @@
{branch.name}
</code>
<p class="discard-warning-text">
{#if force}
{#if branch.remote}
This affects everyone using <strong>{remoteParts[0] || "the remote"}</strong>. Your local commits and local branches are kept.
{:else if force}
Make sure you no longer need the unique commits on this branch.
{:else}
Git will refuse if the branch is not fully merged.
@@ -69,7 +74,7 @@
{:else}
<Trash2 size={15} aria-hidden="true" />
{/if}
{force ? "Force delete" : "Delete"}
{branch.remote ? "Delete from remote" : force ? "Force delete" : "Delete"}
</button>
</footer>
</div>
+7 -5
View File
@@ -54,6 +54,7 @@
onCreateBranch: (branchName: string) => void | Promise<void>;
onRenameBranch: (branch: GitBranchInfo) => void | Promise<void>;
onDeleteBranch: (branch: GitBranchInfo) => void | Promise<void>;
onDeleteRemoteBranch: (branch: GitBranchInfo) => void | Promise<void>;
onCreateTag: (name: string, message: string) => void | Promise<void>;
onDeleteTag: (tag: GitTag) => void | Promise<void>;
onPushTag: (tag: GitTag) => void | Promise<void>;
@@ -74,6 +75,7 @@
onCreateBranch = () => {},
onRenameBranch = () => {},
onDeleteBranch = () => {},
onDeleteRemoteBranch = () => {},
onCreateTag = () => {},
onDeleteTag = () => {},
onPushTag = () => {},
@@ -264,9 +266,9 @@
async function deleteContextBranch() {
const branch = contextBranch;
if (!branch || branch.current || branch.remote || isBusy) return;
if (!branch || branch.current || isBusy) return;
closeBranchContextMenu();
await onDeleteBranch(branch);
if (branch.remote) await onDeleteRemoteBranch(branch); else await onDeleteBranch(branch);
}
async function checkoutContextBranch() {
@@ -669,11 +671,11 @@
type="button"
role="menuitem"
onclick={deleteContextBranch}
disabled={isBusy || contextBranch.current || contextBranch.remote}
title={contextBranch.current ? "Current branch cannot be deleted" : contextBranch.remote ? "Remote branch cannot be deleted here" : "Delete local branch"}
disabled={isBusy || contextBranch.current}
title={contextBranch.current ? "Current branch cannot be deleted" : contextBranch.remote ? "Delete remote branch" : "Delete local branch"}
>
<Trash2 size={14} aria-hidden="true" />
Delete
{contextBranch.remote ? "Delete remote" : "Delete"}
</button>
</div>
{/if}
+12
View File
@@ -42,6 +42,7 @@
onToggleCommitFiles: (hash: string) => void;
onCreateBranchFromCommit: (commit: GitCommit) => void;
onCherryPickCommit: (commit: GitCommit) => void;
onRevertCommit: (commit: GitCommit) => void;
}
let {
@@ -58,6 +59,7 @@
onToggleCommitFiles = () => {},
onCreateBranchFromCommit = () => {},
onCherryPickCommit = () => {},
onRevertCommit = () => {},
}: Props = $props();
let hiddenGraphBranches = $state<Set<string>>(new Set());
@@ -406,6 +408,13 @@
await onCherryPickCommit(commit);
}
async function revertContextCommit() {
const commit = contextCommit;
if (!commit || isBusy) return;
closeCommitContextMenu();
await onRevertCommit(commit);
}
function handleWindowKeydown(event: KeyboardEvent) {
if (event.key !== "Escape") return;
closeCommitContextMenu();
@@ -718,6 +727,9 @@
<Cherry size={14} aria-hidden="true" />
Cherry-pick
</button>
<button type="button" role="menuitem" onclick={revertContextCommit} disabled={isBusy} title="Create a new commit that reverses this commit">
<RotateCcw size={14} aria-hidden="true" /> Revert
</button>
</div>
{/if}
</section>
@@ -0,0 +1,109 @@
<script lang="ts">
import { Cloud, GitBranch, Plus, Save, Trash2, X } from "@lucide/svelte";
import type { GitRemote, PullStrategy } from "../types";
export let remotes: GitRemote[] = [];
export let remoteBranches: string[] = [];
export let currentBranch = "";
export let currentUpstream = "";
export let strategy: PullStrategy = "merge";
export let selectedRemote = "";
export let isBusy = false;
export let language: "en" | "de" = "en";
export let onSaveSync: (strategy: PullStrategy, remote: string, upstream: string) => void | Promise<void> = () => {};
export let onAddRemote: (name: string, url: string) => void | Promise<void> = () => {};
export let onUpdateRemote: (name: string, url: string) => void | Promise<void> = () => {};
export let onRemoveRemote: (name: string) => void | Promise<void> = () => {};
export let onClose: () => void = () => {};
let draftStrategy = strategy;
let draftRemote = selectedRemote;
let draftUpstream = currentUpstream;
let newName = "origin";
let newUrl = "";
let editingName = "";
let editingUrl = "";
let actionError = "";
let actionStatus = "";
$: de = language === "de";
function beginEdit(remote: GitRemote) { actionError = ""; editingName = remote.name; editingUrl = remote.fetch_url; }
async function requestDelete(event: MouseEvent, name: string) {
console.log(name)
event.preventDefault();
event.stopPropagation();
editingName = "";
actionError = "";
actionStatus = de ? `Remote „${name}“ wird entfernt …` : `Removing remote “${name}” …`;
console.info("[Gitty remote] remove button activated", { name });
await remove(name);
}
function cancelEdit() { editingName = ""; editingUrl = ""; }
async function add() { if (!newName.trim() || !newUrl.trim()) return; await onAddRemote(newName.trim(), newUrl.trim()); newName = "origin"; newUrl = ""; }
async function update() { if (!editingName || !editingUrl.trim()) return; await onUpdateRemote(editingName, editingUrl.trim()); cancelEdit(); }
async function remove(name: string) {
actionError = "";
console.log("remove")
try {
await onRemoveRemote(name);
if (draftRemote === name) draftRemote = "";
if (draftUpstream.startsWith(`${name}/`)) draftUpstream = "";
} catch (error) {
actionError = error instanceof Error ? error.message : String(error);
} finally {
actionStatus = "";
}
}
</script>
<div class="dialog-backdrop" role="presentation">
<div class="sync-settings-dialog" role="dialog" aria-modal="true" aria-labelledby="sync-settings-title">
<header class="sync-settings-head">
<div class="sync-settings-title">
<span class="sync-settings-icon"><Cloud size={18} aria-hidden="true" /></span>
<div><span class="eyebrow">Git sync</span><h2 id="sync-settings-title">{de ? "Synchronisierung & Remotes" : "Sync & remotes"}</h2></div>
</div>
<button class="dialog-icon-button" type="button" onclick={onClose} disabled={isBusy} aria-label={de ? "Schließen" : "Close"}><X size={17} /></button>
</header>
<div class="sync-settings-body">
<section class="sync-settings-card">
<div class="sync-card-heading"><div><h3>{de ? "Pull-Verhalten" : "Pull behavior"}</h3><p>{de ? "Legt fest, wie entfernte Änderungen in deinen aktuellen Branch übernommen werden." : "Controls how remote changes are integrated into your current branch."}</p></div></div>
<div class="strategy-options">
<label class:active={draftStrategy === "merge"}><input type="radio" bind:group={draftStrategy} value="merge" /><span><strong>Merge</strong><small>{de ? "Erstellt bei getrennten Verläufen einen Merge-Commit. Sicher und leicht nachvollziehbar." : "Creates a merge commit for diverged history. Safe and easy to follow."}</small></span></label>
<label class:active={draftStrategy === "rebase"}><input type="radio" bind:group={draftStrategy} value="rebase" /><span><strong>Rebase</strong><small>{de ? "Setzt deine lokalen Commits auf die Remote-Änderungen. Ergibt eine lineare Historie." : "Replays your local commits on remote changes for a linear history."}</small></span></label>
<label class:active={draftStrategy === "ff-only"}><input type="radio" bind:group={draftStrategy} value="ff-only" /><span><strong>Fast-forward only</strong><small>{de ? "Pull stoppt, sobald ein Merge nötig wäre. Verändert die Historie nie automatisch." : "Stops when a merge would be required. Never combines diverged history automatically."}</small></span></label>
</div>
<div class="sync-fields">
<label><span>{de ? "Remote für Sync" : "Remote used for sync"}</span><select bind:value={draftRemote}><option value="">{de ? "Automatisch wählen" : "Choose automatically"}</option>{#each remotes as remote}<option value={remote.name}>{remote.name}</option>{/each}</select><small>{de ? "Ein Remote ist die gespeicherte Verbindung zu einem Server-Repository." : "A remote is a saved connection to a repository on a server."}</small></label>
<label><span>{de ? `Upstream für ${currentBranch || "aktuellen Branch"}` : `Upstream for ${currentBranch || "current branch"}`}</span><select bind:value={draftUpstream}><option value="">{de ? "Kein Upstream" : "No upstream"}</option>{#each remoteBranches as branch}<option value={branch}>{branch}</option>{/each}</select><small>{de ? "Der Upstream ist der Remote-Branch, mit dem Pull, Push und Ahead/Behind verglichen werden." : "The upstream is the remote branch used by Pull, Push, and Ahead/Behind."}</small></label>
</div>
</section>
<section class="sync-settings-card">
<div class="sync-card-heading"><div><h3>Remotes</h3><p>{de ? "Server-Verbindungen dieses Repositorys verwalten." : "Manage this repository's server connections."}</p></div><span class="count-pill">{remotes.length}</span></div>
<div class="remote-list">
{#if actionError}<div class="sync-action-error" role="alert"><strong>{de ? "Remote konnte nicht entfernt werden" : "Remote could not be removed"}</strong><span>{actionError}</span></div>{/if}
{#if actionStatus}<div class="sync-action-status" role="status">{actionStatus}</div>{/if}
{#each remotes as remote (remote.name)}
<div class="remote-row">
<span class="remote-mark"><GitBranch size={15} /></span>
{#if editingName === remote.name}
<div class="remote-edit"><strong>{remote.name}</strong><input bind:value={editingUrl} aria-label={`URL for ${remote.name}`} /></div>
<button class="btn-sm" type="button" onclick={update} disabled={isBusy || !editingUrl.trim()}><Save size={13} /> {de ? "Speichern" : "Save"}</button>
<button class="btn-sm" type="button" onclick={cancelEdit} disabled={isBusy}>{de ? "Abbrechen" : "Cancel"}</button>
{:else}
<button class="remote-main" type="button" onclick={() => beginEdit(remote)} disabled={isBusy}><strong>{remote.name}</strong><span>{remote.fetch_url}</span></button>
<button class="remote-delete" type="button" onclick={(event) => requestDelete(event, remote.name)} data-remote-name={remote.name} title={de ? "Remote-Verbindung sofort entfernen; lokale Daten bleiben erhalten" : "Remove remote connection now; local data is kept"}><Trash2 size={13} /><span>{de ? "Entfernen" : "Remove"}</span></button>
{/if}
</div>
{:else}<p class="remote-empty">{de ? "Noch kein Remote eingerichtet." : "No remote configured yet."}</p>{/each}
</div>
<div class="remote-add"><input bind:value={newName} placeholder={de ? "Name, z. B. origin" : "Name, e.g. origin"} aria-label="Remote name" /><input bind:value={newUrl} placeholder="https://… or git@…" aria-label="Remote URL" /><button class="btn-secondary" type="button" onclick={add} disabled={isBusy || !newName.trim() || !newUrl.trim()}><Plus size={14} /> {de ? "Hinzufügen" : "Add remote"}</button></div>
</section>
</div>
<footer class="sync-settings-footer"><p>{de ? "Fetch + Prune entfernt veraltete Remote-Verweise. Force with lease ist ein geschütztes Force-Push nach einem Rebase." : "Fetch + Prune removes stale remote references. Force with lease is a protected force-push after a rebase."}</p><div><button class="btn-secondary" type="button" onclick={onClose} disabled={isBusy}>{de ? "Abbrechen" : "Cancel"}</button><button class="btn-primary" type="button" onclick={() => onSaveSync(draftStrategy, draftRemote, draftUpstream)} disabled={isBusy}><Save size={14} /> {de ? "Einstellungen speichern" : "Save settings"}</button></div></footer>
</div>
</div>
+27 -8
View File
@@ -11,6 +11,9 @@ import type {
GitCommit,
GitCommitComparison,
GitRepositoryFile,
GitRemote,
MergeStrategy,
PullStrategy,
RebaseCommit,
RebasePlanItem,
ReflogEntry,
@@ -28,6 +31,10 @@ export function openRepository(path: string): Promise<GitStatus> {
return invoke<GitStatus>("open_repository", { path });
}
export function initRepository(path: string, initialBranch = "main"): Promise<GitStatus> {
return invoke<GitStatus>("init_repository", { path, initialBranch });
}
export function openRepoInExplorer(path: string): Promise<void> {
return invoke<void>("open_repo_in_explorer", { path });
}
@@ -72,6 +79,15 @@ export function listBranches(path: string): Promise<GitBranch[]> {
return invoke<GitBranch[]>("list_branches", { path });
}
export function listRemotes(path: string): Promise<GitRemote[]> { return invoke("list_remotes", { path }); }
export function addRemote(path: string, name: string, url: string): Promise<GitRemote[]> { return invoke("add_remote", { path, name, url }); }
export function updateRemote(path: string, name: string, url: string): Promise<GitRemote[]> { return invoke("update_remote", { path, name, url }); }
export function removeRemote(path: string, name: string): Promise<GitRemote[]> {
console.log("remove_remote")
return invoke("remove_remote", { path, name }); }
export function setBranchUpstream(path: string, branch: string, upstream?: string): Promise<GitStatus> { return invoke("set_branch_upstream", { path, branch, upstream: upstream || null }); }
export function deleteRemoteBranch(path: string, remote: string, branch: string): Promise<GitStatus> { return invoke("delete_remote_branch", { path, remote, branch }); }
export function listStashes(path: string): Promise<GitStash[]> {
return invoke<GitStash[]>("list_stashes", { path });
}
@@ -255,16 +271,16 @@ export function commitAiReview(path: string, options: CommitAiGenerateOptions):
});
}
export function pull(path: string, username?: string, password?: string): Promise<GitStatus> {
return invoke<GitStatus>("pull", { path, username: username ?? null, password: password ?? null });
export function pull(path: string, username?: string, password?: string, strategy: PullStrategy = "merge", remote?: string, branch?: string): Promise<GitStatus> {
return invoke<GitStatus>("pull", { path, username: username ?? null, password: password ?? null, strategy, remote: remote || null, branch: branch || null });
}
export function fetchRemote(path: string, username?: string, password?: string): Promise<GitStatus> {
return invoke<GitStatus>("fetch", { path, username: username ?? null, password: password ?? null });
export function fetchRemote(path: string, username?: string, password?: string, prune = false, remote?: string): Promise<GitStatus> {
return invoke<GitStatus>("fetch", { path, username: username ?? null, password: password ?? null, prune, remote: remote || null });
}
export function push(path: string, username?: string, password?: string): Promise<GitStatus> {
return invoke<GitStatus>("push", { path, username: username ?? null, password: password ?? null });
export function push(path: string, username?: string, password?: string, forceWithLease = false, remote?: string): Promise<GitStatus> {
return invoke<GitStatus>("push", { path, username: username ?? null, password: password ?? null, forceWithLease, remote: remote || null });
}
export function getRemoteUrl(path: string): Promise<string | null> {
@@ -304,9 +320,12 @@ export function restoreFileFromCommit(
return invoke<GitStatus>("restore_file_from_commit", { path, commit, file });
}
export function mergeBranch(path: string, branch: string): Promise<GitStatus> {
return invoke<GitStatus>("merge_branch", { path, branch });
export function mergeBranch(path: string, branch: string, strategy: MergeStrategy = "default"): Promise<GitStatus> {
return invoke<GitStatus>("merge_branch", { path, branch, strategy });
}
export function mergeContinue(path: string): Promise<GitStatus> { return invoke("merge_continue", { path }); }
export function mergeAbort(path: string): Promise<GitStatus> { return invoke("merge_abort", { path }); }
export function revertCommit(path: string, commit: string): Promise<GitStatus> { return invoke("revert_commit", { path, commit }); }
export function rebaseBranch(path: string, branch: string): Promise<GitStatus> {
return invoke<GitStatus>("rebase_branch", { path, branch });
+5
View File
@@ -68,8 +68,13 @@ export interface GitStatus {
clean: boolean;
rebase_in_progress: boolean;
cherry_pick_in_progress: boolean;
merge_in_progress: boolean;
}
export interface GitRemote { name: string; fetch_url: string; push_url: string; }
export type PullStrategy = "merge" | "rebase" | "ff-only";
export type MergeStrategy = "default" | "squash" | "ff-only" | "no-ff";
export interface GitFileStatus {
path: string;
old_path: string | null;