Implement the ability to create new local branches directly from the application's UI. This includes a new backend command to handle branch creation with validation and a frontend form in the branch panel. Additionally, extend the global search dialog to include a "Files" tab. Users can now search for files by name or path within the repository. Selecting a file in the search results displays its full commit history, with options to diff the file at a specific commit or restore it to that version.
201 lines
6.7 KiB
Svelte
201 lines
6.7 KiB
Svelte
<script lang="ts">
|
|
import { Check, ChevronDown, ChevronRight, GitBranch, GitMerge, Plus, X } from "@lucide/svelte";
|
|
import type { GitBranch as GitBranchInfo } from "../types";
|
|
|
|
interface Props {
|
|
branches: GitBranchInfo[];
|
|
localBranches: GitBranchInfo[];
|
|
remoteBranches: GitBranchInfo[];
|
|
hasRepository: boolean;
|
|
isBusy: boolean;
|
|
onCheckout: (branch: GitBranchInfo) => void;
|
|
onMerge: (branch: GitBranchInfo) => void;
|
|
onCreateBranch: (branchName: string) => void | Promise<void>;
|
|
}
|
|
|
|
let {
|
|
branches = [],
|
|
localBranches = [],
|
|
remoteBranches = [],
|
|
hasRepository = false,
|
|
isBusy = false,
|
|
onCheckout = () => {},
|
|
onMerge = () => {},
|
|
onCreateBranch = () => {},
|
|
}: Props = $props();
|
|
|
|
let localOpen = $state(true);
|
|
let remoteOpen = $state(false);
|
|
let createOpen = $state(false);
|
|
let newBranchName = $state("");
|
|
let createInput = $state<HTMLInputElement | null>(null);
|
|
|
|
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;
|
|
}
|
|
</script>
|
|
|
|
<section class="panel grid grid-rows-[auto_1fr] overflow-hidden" 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>
|
|
</div>
|
|
</div>
|
|
|
|
{#if !hasRepository}
|
|
<p class="blank-state">Open a repository to list branches.</p>
|
|
{:else if branches.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 localBranches as branch (branch.name)}
|
|
<article class="branch-row" class:current={branch.current}>
|
|
<div class="branch-info">
|
|
<GitBranch size={16} aria-hidden="true" />
|
|
<div>
|
|
<strong>{branch.name}</strong>
|
|
<span>local</span>
|
|
</div>
|
|
</div>
|
|
{#if branch.current}
|
|
<span class="pill pill-active">Current</span>
|
|
{:else}
|
|
<div class="branch-actions">
|
|
<button class="btn-sm" type="button" onclick={() => onCheckout(branch)} disabled={isBusy}>
|
|
Checkout
|
|
</button>
|
|
<button class="btn-sm" type="button" onclick={() => onMerge(branch)} disabled={isBusy} title="Merge into current">
|
|
<GitMerge size={15} aria-hidden="true" />
|
|
Merge
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</article>
|
|
{/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 remoteBranches as branch (branch.name)}
|
|
<article class="branch-row" class:current={branch.current}>
|
|
<div class="branch-info">
|
|
<GitBranch size={16} aria-hidden="true" />
|
|
<div>
|
|
<strong>{branch.name}</strong>
|
|
<span>remote</span>
|
|
</div>
|
|
</div>
|
|
{#if branch.current}
|
|
<span class="pill pill-active">Current</span>
|
|
{:else}
|
|
<div class="branch-actions">
|
|
<button class="btn-sm" type="button" onclick={() => onCheckout(branch)} disabled={isBusy}>
|
|
Checkout
|
|
</button>
|
|
<button class="btn-sm" type="button" onclick={() => onMerge(branch)} disabled={isBusy} title="Merge into current">
|
|
<GitMerge size={15} aria-hidden="true" />
|
|
Merge
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</article>
|
|
{/each}
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</section>
|