Improved handling for deleting remote branches across the application, enhancing both user experience and backend reliability. This includes adding structured logging to all Git remote operations in Rust, refining UI components to handle remote-specific deletion flows, and providing clear status/error feedback in sync settings. - Standardized styling for action toggles (Stash, Branch, Explorer) using consistent dimensions. - Implemented detailed console logging for all Git remote operations on the backend. - Refined dialogs and sync settings to provide explicit status and error messages during remote management.
703 lines
23 KiB
Svelte
703 lines
23 KiB
Svelte
<script lang="ts">
|
|
import { Check, ChevronDown, ChevronRight, Folder, FolderOpen, GitBranch, GitMerge, Pencil, Plus, Tag as TagIcon, Trash2, Upload, X } from "@lucide/svelte";
|
|
import type { GitBranch as GitBranchInfo, GitTag } from "../types";
|
|
|
|
type BranchTreeNode = BranchFolderNode | BranchLeafNode;
|
|
|
|
interface BranchFolderNode {
|
|
kind: "folder";
|
|
id: string;
|
|
name: string;
|
|
children: BranchTreeNode[];
|
|
branchCount: number;
|
|
current: boolean;
|
|
folders: Map<string, BranchFolderNode>;
|
|
}
|
|
|
|
interface BranchLeafNode {
|
|
kind: "branch";
|
|
id: string;
|
|
branch: GitBranchInfo;
|
|
displayName: string;
|
|
}
|
|
|
|
type BranchRow = BranchFolderRow | BranchLeafRow;
|
|
|
|
interface BranchFolderRow {
|
|
kind: "folder";
|
|
id: string;
|
|
name: string;
|
|
depth: number;
|
|
branchCount: number;
|
|
current: boolean;
|
|
}
|
|
|
|
interface BranchLeafRow {
|
|
kind: "branch";
|
|
id: string;
|
|
branch: GitBranchInfo;
|
|
displayName: string;
|
|
depth: number;
|
|
scopeLabel: string;
|
|
}
|
|
|
|
interface Props {
|
|
branches: GitBranchInfo[];
|
|
localBranches: GitBranchInfo[];
|
|
remoteBranches: GitBranchInfo[];
|
|
tags: GitTag[];
|
|
hasRepository: boolean;
|
|
isBusy: boolean;
|
|
onCheckout: (branch: GitBranchInfo) => void;
|
|
onMerge: (branch: GitBranchInfo) => void;
|
|
onRebase: (branch: GitBranchInfo) => void;
|
|
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>;
|
|
collapsed?: boolean;
|
|
onToggleCollapsed?: () => void;
|
|
}
|
|
|
|
let {
|
|
branches = [],
|
|
localBranches = [],
|
|
remoteBranches = [],
|
|
tags = [],
|
|
hasRepository = false,
|
|
isBusy = false,
|
|
onCheckout = () => {},
|
|
onMerge = () => {},
|
|
onRebase = () => {},
|
|
onCreateBranch = () => {},
|
|
onRenameBranch = () => {},
|
|
onDeleteBranch = () => {},
|
|
onDeleteRemoteBranch = () => {},
|
|
onCreateTag = () => {},
|
|
onDeleteTag = () => {},
|
|
onPushTag = () => {},
|
|
collapsed = false,
|
|
onToggleCollapsed = () => {},
|
|
}: Props = $props();
|
|
|
|
let localOpen = $state(true);
|
|
let remoteOpen = $state(false);
|
|
let tagsOpen = $state(false);
|
|
let createOpen = $state(false);
|
|
let newBranchName = $state("");
|
|
let createInput = $state<HTMLInputElement | null>(null);
|
|
let tagCreateOpen = $state(false);
|
|
let newTagName = $state("");
|
|
let newTagMessage = $state("");
|
|
let tagCreateInput = $state<HTMLInputElement | null>(null);
|
|
let panelElement = $state<HTMLElement | null>(null);
|
|
let contextBranch = $state<GitBranchInfo | null>(null);
|
|
let contextMenuX = $state(0);
|
|
let contextMenuY = $state(0);
|
|
let contextTag = $state<GitTag | null>(null);
|
|
let tagContextMenuX = $state(0);
|
|
let tagContextMenuY = $state(0);
|
|
let collapsedBranchFolders = $state<Set<string>>(new Set());
|
|
|
|
let localBranchRows = $derived(buildBranchRows("local", localBranches, "local"));
|
|
let remoteBranchRows = $derived(buildBranchRows("remote", remoteBranches, "remote"));
|
|
|
|
function createFolder(id: string, name: string): BranchFolderNode {
|
|
return {
|
|
kind: "folder",
|
|
id,
|
|
name,
|
|
children: [],
|
|
branchCount: 0,
|
|
current: false,
|
|
folders: new Map(),
|
|
};
|
|
}
|
|
|
|
function buildBranchRows(scope: string, branchList: GitBranchInfo[], scopeLabel: string): BranchRow[] {
|
|
const root = createFolder(`${scope}:root`, "");
|
|
|
|
for (const branch of branchList) {
|
|
const parts = branch.name.split("/").filter(Boolean);
|
|
const displayName = parts.length > 0 ? parts[parts.length - 1] : branch.name;
|
|
const folderParts = parts.slice(0, -1);
|
|
let parent = root;
|
|
|
|
for (let index = 0; index < folderParts.length; index += 1) {
|
|
const folderName = folderParts[index];
|
|
const folderPath = folderParts.slice(0, index + 1).join("/");
|
|
let folder = parent.folders.get(folderName);
|
|
|
|
if (!folder) {
|
|
folder = createFolder(`${scope}:folder:${folderPath}`, folderName);
|
|
parent.folders.set(folderName, folder);
|
|
parent.children.push(folder);
|
|
}
|
|
|
|
folder.branchCount += 1;
|
|
folder.current ||= branch.current;
|
|
parent = folder;
|
|
}
|
|
|
|
parent.children.push({
|
|
kind: "branch",
|
|
id: `${scope}:branch:${branch.name}`,
|
|
branch,
|
|
displayName,
|
|
});
|
|
}
|
|
|
|
sortBranchNodes(root.children);
|
|
|
|
const rows: BranchRow[] = [];
|
|
flattenBranchNodes(root.children, rows, 0, scopeLabel);
|
|
return rows;
|
|
}
|
|
|
|
function sortBranchNodes(nodes: BranchTreeNode[]) {
|
|
nodes.sort((left, right) => {
|
|
if (left.kind !== right.kind) return left.kind === "folder" ? -1 : 1;
|
|
const leftName = left.kind === "folder" ? left.name : left.displayName;
|
|
const rightName = right.kind === "folder" ? right.name : right.displayName;
|
|
return leftName.localeCompare(rightName, undefined, { sensitivity: "base" });
|
|
});
|
|
|
|
for (const node of nodes) {
|
|
if (node.kind === "folder") sortBranchNodes(node.children);
|
|
}
|
|
}
|
|
|
|
function flattenBranchNodes(nodes: BranchTreeNode[], rows: BranchRow[], depth: number, scopeLabel: string) {
|
|
for (const node of nodes) {
|
|
if (node.kind === "folder") {
|
|
rows.push({
|
|
kind: "folder",
|
|
id: node.id,
|
|
name: node.name,
|
|
depth,
|
|
branchCount: node.branchCount,
|
|
current: node.current,
|
|
});
|
|
|
|
if (isBranchFolderOpen(node.id)) {
|
|
flattenBranchNodes(node.children, rows, depth + 1, scopeLabel);
|
|
}
|
|
} else {
|
|
rows.push({
|
|
kind: "branch",
|
|
id: node.id,
|
|
branch: node.branch,
|
|
displayName: node.displayName,
|
|
depth,
|
|
scopeLabel,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function isBranchFolderOpen(id: string) {
|
|
return !collapsedBranchFolders.has(id);
|
|
}
|
|
|
|
function toggleBranchFolder(id: string) {
|
|
const next = new Set(collapsedBranchFolders);
|
|
if (next.has(id)) next.delete(id);
|
|
else next.add(id);
|
|
collapsedBranchFolders = next;
|
|
}
|
|
|
|
function openCreateForm() {
|
|
if (!hasRepository || isBusy) return;
|
|
createOpen = true;
|
|
queueMicrotask(() => createInput?.focus());
|
|
}
|
|
|
|
function closeCreateForm() {
|
|
createOpen = false;
|
|
newBranchName = "";
|
|
}
|
|
|
|
async function submitCreate(event: SubmitEvent) {
|
|
event.preventDefault();
|
|
const value = newBranchName.trim();
|
|
if (!value || !hasRepository || isBusy) return;
|
|
await onCreateBranch(value);
|
|
newBranchName = "";
|
|
createOpen = false;
|
|
localOpen = true;
|
|
}
|
|
|
|
function checkoutOnDoubleClick(event: MouseEvent, branch: GitBranchInfo) {
|
|
if (branch.current || isBusy) return;
|
|
const target = event.target instanceof HTMLElement ? event.target : null;
|
|
if (target?.closest("button")) return;
|
|
onCheckout(branch);
|
|
}
|
|
|
|
function openBranchContextMenu(event: MouseEvent, branch: GitBranchInfo) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
if (isBusy) return;
|
|
|
|
const rect = panelElement?.getBoundingClientRect();
|
|
const rawX = rect ? event.clientX - rect.left : event.offsetX;
|
|
const rawY = rect ? event.clientY - rect.top : event.offsetY;
|
|
const maxX = Math.max(8, (rect?.width ?? window.innerWidth) - 192);
|
|
const maxY = Math.max(8, (rect?.height ?? window.innerHeight) - 190);
|
|
|
|
contextBranch = branch;
|
|
contextMenuX = Math.max(8, Math.min(rawX, maxX));
|
|
contextMenuY = Math.max(8, Math.min(rawY, maxY));
|
|
}
|
|
|
|
function closeBranchContextMenu() {
|
|
contextBranch = null;
|
|
}
|
|
|
|
async function renameContextBranch() {
|
|
const branch = contextBranch;
|
|
if (!branch || isBusy) return;
|
|
closeBranchContextMenu();
|
|
await onRenameBranch(branch);
|
|
}
|
|
|
|
async function deleteContextBranch() {
|
|
const branch = contextBranch;
|
|
if (!branch || branch.current || isBusy) return;
|
|
closeBranchContextMenu();
|
|
if (branch.remote) await onDeleteRemoteBranch(branch); else await onDeleteBranch(branch);
|
|
}
|
|
|
|
async function checkoutContextBranch() {
|
|
const branch = contextBranch;
|
|
if (!branch || branch.current || isBusy) return;
|
|
closeBranchContextMenu();
|
|
await onCheckout(branch);
|
|
}
|
|
|
|
async function mergeContextBranch() {
|
|
const branch = contextBranch;
|
|
if (!branch || branch.current || isBusy) return;
|
|
closeBranchContextMenu();
|
|
await onMerge(branch);
|
|
}
|
|
|
|
async function rebaseContextBranch() {
|
|
const branch = contextBranch;
|
|
if (!branch || branch.current || isBusy) return;
|
|
closeBranchContextMenu();
|
|
await onRebase(branch);
|
|
}
|
|
|
|
function openTagCreateForm() {
|
|
if (!hasRepository || isBusy) return;
|
|
tagCreateOpen = true;
|
|
queueMicrotask(() => tagCreateInput?.focus());
|
|
}
|
|
|
|
function closeTagCreateForm() {
|
|
tagCreateOpen = false;
|
|
newTagName = "";
|
|
newTagMessage = "";
|
|
}
|
|
|
|
async function submitCreateTag(event: SubmitEvent) {
|
|
event.preventDefault();
|
|
const name = newTagName.trim();
|
|
if (!name || !hasRepository || isBusy) return;
|
|
await onCreateTag(name, newTagMessage.trim());
|
|
newTagName = "";
|
|
newTagMessage = "";
|
|
tagCreateOpen = false;
|
|
tagsOpen = true;
|
|
}
|
|
|
|
function openTagContextMenu(event: MouseEvent, tag: GitTag) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
if (isBusy) return;
|
|
|
|
const rect = panelElement?.getBoundingClientRect();
|
|
const rawX = rect ? event.clientX - rect.left : event.offsetX;
|
|
const rawY = rect ? event.clientY - rect.top : event.offsetY;
|
|
const maxX = Math.max(8, (rect?.width ?? window.innerWidth) - 192);
|
|
const maxY = Math.max(8, (rect?.height ?? window.innerHeight) - 130);
|
|
|
|
contextTag = tag;
|
|
tagContextMenuX = Math.max(8, Math.min(rawX, maxX));
|
|
tagContextMenuY = Math.max(8, Math.min(rawY, maxY));
|
|
}
|
|
|
|
function closeTagContextMenu() {
|
|
contextTag = null;
|
|
}
|
|
|
|
async function pushContextTag() {
|
|
const tag = contextTag;
|
|
if (!tag || isBusy) return;
|
|
closeTagContextMenu();
|
|
await onPushTag(tag);
|
|
}
|
|
|
|
async function deleteContextTag() {
|
|
const tag = contextTag;
|
|
if (!tag || isBusy) return;
|
|
closeTagContextMenu();
|
|
await onDeleteTag(tag);
|
|
}
|
|
|
|
function closeAllContextMenus() {
|
|
closeBranchContextMenu();
|
|
closeTagContextMenu();
|
|
}
|
|
|
|
function handleWindowKeydown(event: KeyboardEvent) {
|
|
if (event.key === "Escape") closeAllContextMenus();
|
|
}
|
|
</script>
|
|
|
|
<svelte:window on:click={closeAllContextMenus} on:keydown={handleWindowKeydown} on:contextmenu|capture={closeAllContextMenus} />
|
|
|
|
<section bind:this={panelElement} class="panel branch-panel grid grid-rows-[auto_1fr] overflow-hidden" class:collapsed aria-label="Branches">
|
|
<div class="section-head">
|
|
<div>
|
|
<span class="eyebrow">Branches</span>
|
|
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Refs</h2>
|
|
</div>
|
|
<div class="branch-head-actions">
|
|
<button
|
|
class="branch-create-toggle"
|
|
type="button"
|
|
onclick={openCreateForm}
|
|
disabled={!hasRepository || isBusy}
|
|
title="Create new branch"
|
|
aria-label="Create new branch"
|
|
>
|
|
<Plus size={14} aria-hidden="true" />
|
|
</button>
|
|
<span class="pill pill-count">{branches.length}</span>
|
|
<button
|
|
class="branch-create-toggle panel-collapse-toggle"
|
|
type="button"
|
|
onclick={onToggleCollapsed}
|
|
aria-expanded={!collapsed}
|
|
title={collapsed ? "Expand branches" : "Collapse branches"}
|
|
aria-label={collapsed ? "Expand branches panel" : "Collapse branches panel"}
|
|
>
|
|
{#if collapsed}
|
|
<ChevronRight size={14} aria-hidden="true" />
|
|
{:else}
|
|
<ChevronDown size={14} aria-hidden="true" />
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{#if collapsed}
|
|
<!-- collapsed -->
|
|
{:else if !hasRepository}
|
|
<p class="blank-state">Open a repository to list branches.</p>
|
|
{:else if branches.length === 0 && tags.length === 0}
|
|
<p class="blank-state">No branches returned.</p>
|
|
{:else}
|
|
<div class="branch-list overflow-auto p-2 flex flex-col gap-0">
|
|
{#if createOpen}
|
|
<form class="branch-create-form" onsubmit={submitCreate}>
|
|
<GitBranch size={15} aria-hidden="true" />
|
|
<input
|
|
bind:this={createInput}
|
|
bind:value={newBranchName}
|
|
disabled={isBusy}
|
|
autocomplete="off"
|
|
spellcheck="false"
|
|
placeholder="new-branch-name"
|
|
aria-label="New branch name"
|
|
/>
|
|
<button class="branch-create-action confirm" type="submit" disabled={isBusy || newBranchName.trim().length === 0} title="Create branch">
|
|
<Check size={14} aria-hidden="true" />
|
|
</button>
|
|
<button class="branch-create-action" type="button" onclick={closeCreateForm} disabled={isBusy} title="Cancel">
|
|
<X size={14} aria-hidden="true" />
|
|
</button>
|
|
</form>
|
|
{/if}
|
|
|
|
<div class="branch-group">
|
|
<button
|
|
class="branch-group-toggle"
|
|
type="button"
|
|
onclick={() => { localOpen = !localOpen; }}
|
|
aria-expanded={localOpen}
|
|
>
|
|
{#if localOpen}
|
|
<ChevronDown size={14} aria-hidden="true" />
|
|
{:else}
|
|
<ChevronRight size={14} aria-hidden="true" />
|
|
{/if}
|
|
<span>Local</span>
|
|
<span class="branch-group-count">{localBranches.length}</span>
|
|
</button>
|
|
|
|
{#if localOpen}
|
|
{#if localBranches.length === 0}
|
|
<div class="branch-empty">No local branches.</div>
|
|
{:else}
|
|
{#each localBranchRows as row (row.id)}
|
|
{#if row.kind === "folder"}
|
|
<button
|
|
class="branch-folder-row"
|
|
class:current={row.current}
|
|
style={`--branch-indent: ${row.depth * 16}px;`}
|
|
type="button"
|
|
onclick={() => toggleBranchFolder(row.id)}
|
|
aria-expanded={isBranchFolderOpen(row.id)}
|
|
title={`${row.name} (${row.branchCount})`}
|
|
>
|
|
{#if isBranchFolderOpen(row.id)}
|
|
<ChevronDown size={14} aria-hidden="true" />
|
|
<FolderOpen size={15} aria-hidden="true" />
|
|
{:else}
|
|
<ChevronRight size={14} aria-hidden="true" />
|
|
<Folder size={15} aria-hidden="true" />
|
|
{/if}
|
|
<span class="branch-folder-name">{row.name}</span>
|
|
<span class="branch-folder-count">{row.branchCount}</span>
|
|
</button>
|
|
{:else}
|
|
<article
|
|
class="branch-row"
|
|
class:current={row.branch.current}
|
|
style={`--branch-indent: ${row.depth * 16}px;`}
|
|
ondblclick={(event) => checkoutOnDoubleClick(event, row.branch)}
|
|
oncontextmenu={(event) => openBranchContextMenu(event, row.branch)}
|
|
title={row.branch.current ? "Current branch" : row.branch.name}
|
|
>
|
|
<div class="branch-info">
|
|
<GitBranch size={16} aria-hidden="true" />
|
|
<div>
|
|
<strong>{row.displayName}</strong>
|
|
<span>{row.scopeLabel}</span>
|
|
</div>
|
|
</div>
|
|
{#if row.branch.current}
|
|
<span class="pill pill-active">Current</span>
|
|
{/if}
|
|
</article>
|
|
{/if}
|
|
{/each}
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="branch-group">
|
|
<button
|
|
class="branch-group-toggle"
|
|
type="button"
|
|
onclick={() => { remoteOpen = !remoteOpen; }}
|
|
aria-expanded={remoteOpen}
|
|
>
|
|
{#if remoteOpen}
|
|
<ChevronDown size={14} aria-hidden="true" />
|
|
{:else}
|
|
<ChevronRight size={14} aria-hidden="true" />
|
|
{/if}
|
|
<span>Remote</span>
|
|
<span class="branch-group-count">{remoteBranches.length}</span>
|
|
</button>
|
|
|
|
{#if remoteOpen}
|
|
{#if remoteBranches.length === 0}
|
|
<div class="branch-empty">No remote branches.</div>
|
|
{:else}
|
|
{#each remoteBranchRows as row (row.id)}
|
|
{#if row.kind === "folder"}
|
|
<button
|
|
class="branch-folder-row"
|
|
class:current={row.current}
|
|
style={`--branch-indent: ${row.depth * 16}px;`}
|
|
type="button"
|
|
onclick={() => toggleBranchFolder(row.id)}
|
|
aria-expanded={isBranchFolderOpen(row.id)}
|
|
title={`${row.name} (${row.branchCount})`}
|
|
>
|
|
{#if isBranchFolderOpen(row.id)}
|
|
<ChevronDown size={14} aria-hidden="true" />
|
|
<FolderOpen size={15} aria-hidden="true" />
|
|
{:else}
|
|
<ChevronRight size={14} aria-hidden="true" />
|
|
<Folder size={15} aria-hidden="true" />
|
|
{/if}
|
|
<span class="branch-folder-name">{row.name}</span>
|
|
<span class="branch-folder-count">{row.branchCount}</span>
|
|
</button>
|
|
{:else}
|
|
<article
|
|
class="branch-row"
|
|
class:current={row.branch.current}
|
|
style={`--branch-indent: ${row.depth * 16}px;`}
|
|
ondblclick={(event) => checkoutOnDoubleClick(event, row.branch)}
|
|
oncontextmenu={(event) => openBranchContextMenu(event, row.branch)}
|
|
title={row.branch.current ? "Current branch" : row.branch.name}
|
|
>
|
|
<div class="branch-info">
|
|
<GitBranch size={16} aria-hidden="true" />
|
|
<div>
|
|
<strong>{row.displayName}</strong>
|
|
<span>{row.scopeLabel}</span>
|
|
</div>
|
|
</div>
|
|
{#if row.branch.current}
|
|
<span class="pill pill-active">Current</span>
|
|
{/if}
|
|
</article>
|
|
{/if}
|
|
{/each}
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="branch-group">
|
|
<div class="tag-group-head">
|
|
<button
|
|
class="branch-group-toggle"
|
|
type="button"
|
|
onclick={() => { tagsOpen = !tagsOpen; }}
|
|
aria-expanded={tagsOpen}
|
|
>
|
|
{#if tagsOpen}
|
|
<ChevronDown size={14} aria-hidden="true" />
|
|
{:else}
|
|
<ChevronRight size={14} aria-hidden="true" />
|
|
{/if}
|
|
<span>Tags</span>
|
|
<span class="branch-group-count">{tags.length}</span>
|
|
</button>
|
|
<button
|
|
class="branch-create-toggle"
|
|
type="button"
|
|
onclick={openTagCreateForm}
|
|
disabled={!hasRepository || isBusy}
|
|
title="Create new tag"
|
|
aria-label="Create new tag"
|
|
>
|
|
<Plus size={13} aria-hidden="true" />
|
|
</button>
|
|
</div>
|
|
|
|
{#if tagCreateOpen}
|
|
<form class="branch-create-form tag-create-form" onsubmit={submitCreateTag}>
|
|
<TagIcon size={15} aria-hidden="true" />
|
|
<input
|
|
bind:this={tagCreateInput}
|
|
bind:value={newTagName}
|
|
disabled={isBusy}
|
|
autocomplete="off"
|
|
spellcheck="false"
|
|
placeholder="v1.0.0"
|
|
aria-label="New tag name"
|
|
/>
|
|
<input
|
|
bind:value={newTagMessage}
|
|
disabled={isBusy}
|
|
autocomplete="off"
|
|
spellcheck="false"
|
|
placeholder="Message (optional)"
|
|
aria-label="Tag message"
|
|
/>
|
|
<button class="branch-create-action confirm" type="submit" disabled={isBusy || newTagName.trim().length === 0} title="Create tag">
|
|
<Check size={14} aria-hidden="true" />
|
|
</button>
|
|
<button class="branch-create-action" type="button" onclick={closeTagCreateForm} disabled={isBusy} title="Cancel">
|
|
<X size={14} aria-hidden="true" />
|
|
</button>
|
|
</form>
|
|
{/if}
|
|
|
|
{#if tagsOpen}
|
|
{#if tags.length === 0}
|
|
<div class="branch-empty">No tags.</div>
|
|
{:else}
|
|
{#each tags as tag (tag.name)}
|
|
<article
|
|
class="branch-row"
|
|
oncontextmenu={(event) => openTagContextMenu(event, tag)}
|
|
title={tag.message ?? tag.name}
|
|
>
|
|
<div class="branch-info">
|
|
<TagIcon size={16} aria-hidden="true" />
|
|
<div>
|
|
<strong>{tag.name}</strong>
|
|
<span>{tag.short_hash}</span>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
{/each}
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if contextBranch}
|
|
<div
|
|
class="branch-context-menu"
|
|
style={`left: ${contextMenuX}px; top: ${contextMenuY}px;`}
|
|
role="menu"
|
|
tabindex="-1"
|
|
aria-label={`Actions for ${contextBranch.name}`}
|
|
>
|
|
<button type="button" role="menuitem" onclick={checkoutContextBranch} disabled={isBusy || contextBranch.current}>
|
|
<GitBranch size={14} aria-hidden="true" />
|
|
Checkout
|
|
</button>
|
|
<button type="button" role="menuitem" onclick={mergeContextBranch} disabled={isBusy || contextBranch.current}>
|
|
<GitMerge size={14} aria-hidden="true" />
|
|
Merge into current
|
|
</button>
|
|
<button type="button" role="menuitem" onclick={rebaseContextBranch} disabled={isBusy || contextBranch.current}>
|
|
<GitBranch size={14} aria-hidden="true" />
|
|
Rebase current onto this
|
|
</button>
|
|
<div class="menu-separator" role="separator"></div>
|
|
<button type="button" role="menuitem" onclick={renameContextBranch} disabled={isBusy || contextBranch.remote}>
|
|
<Pencil size={14} aria-hidden="true" />
|
|
Rename
|
|
</button>
|
|
<button
|
|
class="danger"
|
|
type="button"
|
|
role="menuitem"
|
|
onclick={deleteContextBranch}
|
|
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" />
|
|
{contextBranch.remote ? "Delete remote" : "Delete"}
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if contextTag}
|
|
<div
|
|
class="branch-context-menu"
|
|
style={`left: ${tagContextMenuX}px; top: ${tagContextMenuY}px;`}
|
|
role="menu"
|
|
tabindex="-1"
|
|
aria-label={`Actions for ${contextTag.name}`}
|
|
>
|
|
<button type="button" role="menuitem" onclick={pushContextTag} disabled={isBusy}>
|
|
<Upload size={14} aria-hidden="true" />
|
|
Push to remote
|
|
</button>
|
|
<div class="menu-separator" role="separator"></div>
|
|
<button class="danger" type="button" role="menuitem" onclick={deleteContextTag} disabled={isBusy} title="Delete local tag">
|
|
<Trash2 size={14} aria-hidden="true" />
|
|
Delete
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</section>
|