feat(branches): add Merge Branch dialog and confirm flow
Introduce a modal dialog to configure and confirm branch merges. It replaces the old prompt-based workflow and lets users choose a merge strategy with localized labels. Confirming the dialog runs the merge, refreshes views, and clears the pending merge target. - Add a merge dialog UI with strategy options and keyboard support. - Replace prompt-based merge flow with a state-driven confirm dialog. - Include CSS for dialog layout, responsive sizing, and styling.
This commit is contained in:
+22
-4
@@ -27,6 +27,7 @@
|
|||||||
import ExplorerPanel from "./lib/components/ExplorerPanel.svelte";
|
import ExplorerPanel from "./lib/components/ExplorerPanel.svelte";
|
||||||
import HistoryPanel from "./lib/components/HistoryPanel.svelte";
|
import HistoryPanel from "./lib/components/HistoryPanel.svelte";
|
||||||
import InitRepositoryDialog from "./lib/components/InitRepositoryDialog.svelte";
|
import InitRepositoryDialog from "./lib/components/InitRepositoryDialog.svelte";
|
||||||
|
import MergeBranchDialog from "./lib/components/MergeBranchDialog.svelte";
|
||||||
import NewBranchDialog from "./lib/components/NewBranchDialog.svelte";
|
import NewBranchDialog from "./lib/components/NewBranchDialog.svelte";
|
||||||
import RenameBranchDialog from "./lib/components/RenameBranchDialog.svelte";
|
import RenameBranchDialog from "./lib/components/RenameBranchDialog.svelte";
|
||||||
import ReflogDialog from "./lib/components/ReflogDialog.svelte";
|
import ReflogDialog from "./lib/components/ReflogDialog.svelte";
|
||||||
@@ -169,6 +170,7 @@
|
|||||||
GitFileStatus,
|
GitFileStatus,
|
||||||
GitIgnoreKind,
|
GitIgnoreKind,
|
||||||
GitLfsStatus,
|
GitLfsStatus,
|
||||||
|
MergeStrategy,
|
||||||
GitRepositoryFile,
|
GitRepositoryFile,
|
||||||
GitRemote,
|
GitRemote,
|
||||||
PullStrategy,
|
PullStrategy,
|
||||||
@@ -391,6 +393,7 @@
|
|||||||
let comparisonFromLabel = "";
|
let comparisonFromLabel = "";
|
||||||
let comparisonToLabel = "";
|
let comparisonToLabel = "";
|
||||||
let newBranchCommit: GitCommit | null = null;
|
let newBranchCommit: GitCommit | null = null;
|
||||||
|
let mergeBranchTarget: GitBranchInfo | null = null;
|
||||||
let renameBranchTarget: GitBranchInfo | null = null;
|
let renameBranchTarget: GitBranchInfo | null = null;
|
||||||
let deleteBranchTarget: GitBranchInfo | null = null;
|
let deleteBranchTarget: GitBranchInfo | null = null;
|
||||||
let deleteBranchForce = false;
|
let deleteBranchForce = false;
|
||||||
@@ -3207,11 +3210,15 @@
|
|||||||
|
|
||||||
async function merge(branch: GitBranchInfo) {
|
async function merge(branch: GitBranchInfo) {
|
||||||
if (!activeRepoPath || branch.current) return;
|
if (!activeRepoPath || branch.current) return;
|
||||||
const strategy = (window.prompt("Merge strategy: default, squash, ff-only, or no-ff", "default") ?? "").trim();
|
mergeBranchTarget = branch;
|
||||||
if (!strategy) return;
|
}
|
||||||
if (!["default", "squash", "ff-only", "no-ff"].includes(strategy)) { errorMessage = "Unknown merge strategy."; return; }
|
|
||||||
|
async function confirmMerge(strategy: MergeStrategy) {
|
||||||
|
const branch = mergeBranchTarget;
|
||||||
|
if (!activeRepoPath || !branch || branch.current) return;
|
||||||
await runOperation(`Merging ${branch.name}`, async () => {
|
await runOperation(`Merging ${branch.name}`, async () => {
|
||||||
applyStatus(await mergeBranch(activeRepoPath, branch.name, strategy as import("./lib/types").MergeStrategy));
|
applyStatus(await mergeBranch(activeRepoPath, branch.name, strategy));
|
||||||
|
mergeBranchTarget = null;
|
||||||
await refreshRepositoryViews(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
trackEvent("branch_merged", {
|
trackEvent("branch_merged", {
|
||||||
remote: branch.remote ? 1 : 0,
|
remote: branch.remote ? 1 : 0,
|
||||||
@@ -5926,6 +5933,17 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if mergeBranchTarget}
|
||||||
|
<MergeBranchDialog
|
||||||
|
branch={mergeBranchTarget}
|
||||||
|
currentBranch={status?.current_branch ?? ""}
|
||||||
|
{isBusy}
|
||||||
|
language={appLanguage}
|
||||||
|
onMerge={confirmMerge}
|
||||||
|
onClose={() => { if (!isBusy) mergeBranchTarget = null; }}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if commitNoteTarget}
|
{#if commitNoteTarget}
|
||||||
<CommitNoteDialog
|
<CommitNoteDialog
|
||||||
commit={commitNoteTarget}
|
commit={commitNoteTarget}
|
||||||
|
|||||||
+42
@@ -3885,6 +3885,13 @@
|
|||||||
max-height: calc(100vh - 32px);
|
max-height: calc(100vh - 32px);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
.merge-branch-dialog {
|
||||||
|
display: block;
|
||||||
|
width: min(590px, calc(100vw - 32px));
|
||||||
|
height: auto;
|
||||||
|
max-height: calc(100vh - 32px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
.rename-branch-dialog {
|
.rename-branch-dialog {
|
||||||
display: block;
|
display: block;
|
||||||
width: min(520px, calc(100vw - 32px));
|
width: min(520px, calc(100vw - 32px));
|
||||||
@@ -4207,6 +4214,41 @@
|
|||||||
.init-repository-error { color: var(--color-danger, #ff6b78) !important; }
|
.init-repository-error { color: var(--color-danger, #ff6b78) !important; }
|
||||||
.init-repository-actions { padding-top: 2px; }
|
.init-repository-actions { padding-top: 2px; }
|
||||||
.init-repository-actions > button { font-size: 11.5px; }
|
.init-repository-actions > button { font-size: 11.5px; }
|
||||||
|
.merge-branch-header { padding: 15px 17px; }
|
||||||
|
.merge-branch-heading { display: flex; align-items: center; gap: 11px; }
|
||||||
|
.merge-branch-heading h2 { margin: 2px 0 0; color: var(--color-ink); font-size: 15px; line-height: 1.25; }
|
||||||
|
.merge-branch-icon {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--color-accent) 34%, var(--color-border));
|
||||||
|
border-radius: 9px;
|
||||||
|
color: var(--color-accent);
|
||||||
|
background: color-mix(in srgb, var(--color-accent) 10%, var(--color-surface-raised));
|
||||||
|
}
|
||||||
|
.merge-branch-form { display: flex; flex-direction: column; gap: 15px; padding: 17px; }
|
||||||
|
.merge-branch-route { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 12px; padding: 11px 12px; border: 1px solid var(--color-border-subtle); border-radius: 9px; background: var(--color-surface-raised); }
|
||||||
|
.merge-branch-route > div { display: grid; gap: 3px; min-width: 0; }
|
||||||
|
.merge-branch-route > div:last-child { text-align: right; }
|
||||||
|
.merge-branch-route span { color: var(--color-ink-faint); font-size: 9px; font-weight: 800; letter-spacing: .05em; text-transform: uppercase; }
|
||||||
|
.merge-branch-route strong { overflow: hidden; color: var(--color-ink); font-family: var(--font-mono); font-size: 11.5px; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.merge-branch-route > svg { color: var(--color-accent); }
|
||||||
|
.merge-strategy-fieldset { min-width: 0; margin: 0; padding: 0; border: 0; }
|
||||||
|
.merge-strategy-fieldset legend { margin-bottom: 7px; color: var(--color-ink-faint); font-size: 9.5px; font-weight: 800; letter-spacing: .05em; text-transform: uppercase; }
|
||||||
|
.merge-strategy-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
||||||
|
.merge-strategy-option { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: start; gap: 9px; min-width: 0; padding: 11px; border: 1px solid var(--color-border-subtle); border-radius: 8px; background: var(--color-surface-dim); cursor: pointer; }
|
||||||
|
.merge-strategy-option:hover { border-color: var(--color-border); background: var(--color-surface-hover); }
|
||||||
|
.merge-strategy-option.active { border-color: color-mix(in srgb, var(--color-accent) 52%, var(--color-border)); background: color-mix(in srgb, var(--color-accent) 8%, var(--color-surface-raised)); box-shadow: inset 2px 0 0 var(--color-accent); }
|
||||||
|
.merge-strategy-option > input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
|
||||||
|
.merge-strategy-check { display: grid; place-items: center; width: 17px; height: 17px; margin-top: 1px; border: 1px solid var(--color-border-input); border-radius: 50%; color: #fff; background: var(--color-surface-raised); }
|
||||||
|
.merge-strategy-option.active .merge-strategy-check { border-color: var(--color-accent); background: var(--color-accent); }
|
||||||
|
.merge-strategy-copy { display: grid; gap: 3px; min-width: 0; }
|
||||||
|
.merge-strategy-copy strong { color: var(--color-ink); font-size: 11.5px; }
|
||||||
|
.merge-strategy-copy small { color: var(--color-ink-faint); font-size: 9.5px; line-height: 1.4; }
|
||||||
|
.merge-branch-actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 1px; }
|
||||||
|
.merge-branch-actions > button { font-size: 11.5px; }
|
||||||
.rename-branch-form {
|
.rename-branch-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { Check, GitMerge, LoaderCircle, X } from "@lucide/svelte";
|
||||||
|
import type { AppLanguage, GitBranch, MergeStrategy } from "../types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
branch: GitBranch;
|
||||||
|
currentBranch: string;
|
||||||
|
isBusy: boolean;
|
||||||
|
language: AppLanguage;
|
||||||
|
onMerge: (strategy: MergeStrategy) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
branch,
|
||||||
|
currentBranch,
|
||||||
|
isBusy = false,
|
||||||
|
language = "en",
|
||||||
|
onMerge = () => {},
|
||||||
|
onClose = () => {},
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let strategy = $state<MergeStrategy>("default");
|
||||||
|
const isGerman = $derived(language === "de");
|
||||||
|
const options = $derived([
|
||||||
|
{
|
||||||
|
value: "default" as const,
|
||||||
|
label: isGerman ? "Standard" : "Default",
|
||||||
|
description: isGerman ? "Git wählt Fast-forward oder erstellt einen Merge-Commit." : "Git chooses fast-forward or creates a merge commit.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "squash" as const,
|
||||||
|
label: "Squash",
|
||||||
|
description: isGerman ? "Fasst alle Änderungen zu einem neuen Commit zusammen." : "Combines all changes into one new commit.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "ff-only" as const,
|
||||||
|
label: "Fast-forward only",
|
||||||
|
description: isGerman ? "Bricht ab, wenn ein Merge-Commit erforderlich wäre." : "Stops if a merge commit would be required.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "no-ff" as const,
|
||||||
|
label: "No fast-forward",
|
||||||
|
description: isGerman ? "Erstellt immer einen eigenen Merge-Commit." : "Always creates a dedicated merge commit.",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
function submit(event: SubmitEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!isBusy) onMerge(strategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === "Escape" && !isBusy) onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
window.addEventListener("keydown", handleKeydown);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeydown);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="dialog-backdrop app-chrome-backdrop" role="presentation">
|
||||||
|
<div class="dialog merge-branch-dialog" role="dialog" aria-modal="true" aria-labelledby="merge-branch-title" tabindex="-1">
|
||||||
|
<header class="dialog-header merge-branch-header">
|
||||||
|
<div class="merge-branch-heading">
|
||||||
|
<span class="merge-branch-icon" aria-hidden="true"><GitMerge size={18} /></span>
|
||||||
|
<div>
|
||||||
|
<span class="eyebrow">{isGerman ? "Branches zusammenführen" : "Combine branches"}</span>
|
||||||
|
<h2 id="merge-branch-title">{isGerman ? "Merge konfigurieren" : "Configure merge"}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="dialog-close" type="button" onclick={onClose} disabled={isBusy} title={isGerman ? "Schließen" : "Close"}>
|
||||||
|
<X size={18} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form class="merge-branch-form" onsubmit={submit}>
|
||||||
|
<div class="merge-branch-route" aria-label={isGerman ? "Merge-Richtung" : "Merge direction"}>
|
||||||
|
<div>
|
||||||
|
<span>{isGerman ? "Quell-Branch" : "Source branch"}</span>
|
||||||
|
<strong>{branch.name}</strong>
|
||||||
|
</div>
|
||||||
|
<GitMerge size={18} aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<span>{isGerman ? "In aktuellen Branch" : "Into current branch"}</span>
|
||||||
|
<strong>{currentBranch || "HEAD"}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset class="merge-strategy-fieldset" disabled={isBusy}>
|
||||||
|
<legend>{isGerman ? "Merge-Strategie" : "Merge strategy"}</legend>
|
||||||
|
<div class="merge-strategy-grid">
|
||||||
|
{#each options as option}
|
||||||
|
<label class:active={strategy === option.value} class="merge-strategy-option">
|
||||||
|
<input type="radio" name="merge-strategy" value={option.value} bind:group={strategy} />
|
||||||
|
<span class="merge-strategy-check" aria-hidden="true">
|
||||||
|
{#if strategy === option.value}<Check size={13} />{/if}
|
||||||
|
</span>
|
||||||
|
<span class="merge-strategy-copy">
|
||||||
|
<strong>{option.label}</strong>
|
||||||
|
<small>{option.description}</small>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="merge-branch-actions">
|
||||||
|
<button class="btn-secondary" type="button" onclick={onClose} disabled={isBusy}>{isGerman ? "Abbrechen" : "Cancel"}</button>
|
||||||
|
<button class="btn-primary" type="submit" disabled={isBusy}>
|
||||||
|
{#if isBusy}<LoaderCircle class="spin" size={16} aria-hidden="true" />{:else}<GitMerge size={16} aria-hidden="true" />{/if}
|
||||||
|
{isGerman ? "Branch mergen" : "Merge branch"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user