Opt/app layout #18
@@ -78,7 +78,8 @@
|
||||
"Bash(rustfmt --edition 2024 --check src/badge.rs src/main.rs)",
|
||||
"Bash(rustfmt --edition 2024 --check src/git.rs)",
|
||||
"Bash(rustfmt --edition 2024 --check src/badge.rs)",
|
||||
"Bash(rustfmt --edition 2024 --check src/git.rs src/main.rs)"
|
||||
"Bash(rustfmt --edition 2024 --check src/git.rs src/main.rs)",
|
||||
"Bash(pkg-config --list-all)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+34
-3
@@ -3415,16 +3415,47 @@ fn is_auth_error(details: &str) -> bool {
|
||||
|| d.contains("authentication required")
|
||||
}
|
||||
|
||||
// Windows' CreateProcess rejects command lines longer than ~32K chars with
|
||||
// "os error 206" (filename or extension too long). Staging/restoring a large
|
||||
// number of files can easily exceed that, so split the paths across multiple
|
||||
// invocations and concatenate their output.
|
||||
const MAX_PATH_ARGS_CHARS: usize = 8_000;
|
||||
|
||||
fn run_git_with_paths(
|
||||
repo: &Path,
|
||||
base_args: &[&str],
|
||||
files: &[String],
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let mut args = Vec::with_capacity(base_args.len() + files.len() + 1);
|
||||
if files.is_empty() {
|
||||
let args: Vec<OsString> = base_args.iter().map(OsString::from).collect();
|
||||
return run_git(repo, args);
|
||||
}
|
||||
|
||||
let mut combined = Vec::new();
|
||||
let mut start = 0;
|
||||
while start < files.len() {
|
||||
let mut end = start;
|
||||
let mut chunk_chars = 0usize;
|
||||
while end < files.len() {
|
||||
let len = files[end].len() + 1;
|
||||
if end > start && chunk_chars + len > MAX_PATH_ARGS_CHARS {
|
||||
break;
|
||||
}
|
||||
chunk_chars += len;
|
||||
end += 1;
|
||||
}
|
||||
let chunk = &files[start..end];
|
||||
|
||||
let mut args = Vec::with_capacity(base_args.len() + chunk.len() + 1);
|
||||
args.extend(base_args.iter().map(OsString::from));
|
||||
args.push(OsString::from("--"));
|
||||
args.extend(files.iter().map(OsString::from));
|
||||
run_git(repo, args)
|
||||
args.extend(chunk.iter().map(OsString::from));
|
||||
combined.extend(run_git(repo, args)?);
|
||||
|
||||
start = end;
|
||||
}
|
||||
|
||||
Ok(combined)
|
||||
}
|
||||
|
||||
fn run_git_with_paths_cancellable(
|
||||
|
||||
+25
-2
@@ -1639,6 +1639,7 @@
|
||||
.branch-actions .btn-sm { min-height: 24px; padding: 0 6px; font-size: 11px; }
|
||||
|
||||
.branch-context-menu,
|
||||
.history-context-menu,
|
||||
.explorer-context-menu {
|
||||
z-index: 120;
|
||||
display: grid;
|
||||
@@ -1651,10 +1652,12 @@
|
||||
box-shadow: 0 18px 50px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.branch-context-menu { position: absolute; }
|
||||
.branch-context-menu,
|
||||
.history-context-menu { position: absolute; }
|
||||
.explorer-context-menu { position: fixed; }
|
||||
|
||||
.branch-context-menu button,
|
||||
.history-context-menu button,
|
||||
.explorer-context-menu button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1673,13 +1676,15 @@
|
||||
}
|
||||
|
||||
.branch-context-menu button:hover:not(:disabled),
|
||||
.history-context-menu button:hover:not(:disabled),
|
||||
.explorer-context-menu button:hover:not(:disabled) {
|
||||
border-color: var(--color-border-subtle);
|
||||
background: rgba(255,255,255,0.06);
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.branch-context-menu .menu-separator {
|
||||
.branch-context-menu .menu-separator,
|
||||
.history-context-menu .menu-separator {
|
||||
height: 1px;
|
||||
margin: 4px 3px;
|
||||
background: var(--color-border-subtle);
|
||||
@@ -1696,6 +1701,7 @@
|
||||
}
|
||||
|
||||
.branch-context-menu button:disabled,
|
||||
.history-context-menu button:disabled,
|
||||
.explorer-context-menu button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.48;
|
||||
@@ -1817,6 +1823,7 @@
|
||||
|
||||
/* --- Commit history --- */
|
||||
|
||||
.history-panel { position: relative; }
|
||||
.history-list { min-width: 0; padding: 6px; overflow: auto; }
|
||||
|
||||
.commit-row { display: grid; min-width: 0; gap: 8px; padding: 10px; border: 1px solid var(--color-border-subtle); border-radius: 8px; background: var(--color-surface-raised); transition: border-color 120ms; }
|
||||
@@ -2017,6 +2024,22 @@
|
||||
}
|
||||
.commit-action-buttons { display: flex; flex: 0 0 auto; align-items: center; gap: 5px; }
|
||||
.commit-action-buttons button { flex: 0 0 auto; white-space: nowrap; }
|
||||
.commit-menu-button {
|
||||
width: 28px;
|
||||
min-width: 28px;
|
||||
min-height: 26px;
|
||||
padding: 0;
|
||||
border-color: rgba(94,110,156,0.16);
|
||||
border-radius: 7px;
|
||||
background: rgba(255,255,255,0.035);
|
||||
color: var(--color-ink-dim);
|
||||
}
|
||||
.commit-menu-button:hover:not(:disabled),
|
||||
.commit-menu-button[aria-expanded="true"] {
|
||||
border-color: rgba(65,209,255,0.28);
|
||||
background: rgba(65,209,255,0.08);
|
||||
color: var(--color-ink);
|
||||
}
|
||||
.file-history-head { align-items: flex-start; }
|
||||
.file-history-heading { min-width: 0; flex: 1 1 auto; overflow: hidden; }
|
||||
.section-head .file-history-name {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { ChevronDown, ChevronRight, GitBranch, GitMerge, RotateCcw, X } from "@lucide/svelte";
|
||||
import { ChevronDown, ChevronRight, EllipsisVertical, GitBranch, GitMerge, RotateCcw, X } from "@lucide/svelte";
|
||||
import type { FileStatusKind, GitCommit, GitCommitFile } from "../types";
|
||||
|
||||
interface GraphSegment {
|
||||
@@ -60,6 +60,10 @@
|
||||
let branchDialogOpen = $state(false);
|
||||
let userAdjustedBranchFilter = $state(false);
|
||||
let lastDefaultFilterKey = $state("");
|
||||
let panelElement = $state<HTMLElement | null>(null);
|
||||
let contextCommit = $state<GitCommit | null>(null);
|
||||
let contextMenuX = $state(0);
|
||||
let contextMenuY = $state(0);
|
||||
|
||||
function laneColor(col: number): string {
|
||||
return GRAPH_COLORS[((col % GRAPH_COLORS.length) + GRAPH_COLORS.length) % GRAPH_COLORS.length];
|
||||
@@ -203,6 +207,24 @@
|
||||
return labels.filter(branchIsVisible);
|
||||
}
|
||||
|
||||
function commitHoverBranchLabels(commit: GitCommit, row: GraphRow | undefined): string[] {
|
||||
const directBranches = localBranchRefs(commit);
|
||||
if (directBranches.length > 0) return directBranches;
|
||||
|
||||
const containingBranches = row?.branchLabels ?? [];
|
||||
if (containingBranches.length <= 3) return containingBranches;
|
||||
return [...containingBranches.slice(0, 3), `+${containingBranches.length - 3} more`];
|
||||
}
|
||||
|
||||
function commitHoverTitle(commit: GitCommit, row: GraphRow | undefined): string {
|
||||
const directBranches = localBranchRefs(commit);
|
||||
if (directBranches.length > 0) return `Branches: ${directBranches.join(", ")}`;
|
||||
|
||||
const containingBranches = row?.branchLabels ?? [];
|
||||
if (containingBranches.length === 0) return commit.short_hash;
|
||||
return `Branches containing this commit: ${containingBranches.join(", ")}`;
|
||||
}
|
||||
|
||||
function segmentIsVisible(segment: GraphSegment): boolean {
|
||||
return segment.branches.length === 0 || segment.branches.some(branchIsVisible);
|
||||
}
|
||||
@@ -316,6 +338,54 @@
|
||||
}
|
||||
}
|
||||
|
||||
function openCommitActionMenu(event: MouseEvent, commit: GitCommit) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (isBusy) return;
|
||||
|
||||
if (contextCommit?.hash === commit.hash) {
|
||||
closeCommitContextMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
const panelRect = panelElement?.getBoundingClientRect();
|
||||
const buttonRect = event.currentTarget instanceof HTMLElement
|
||||
? event.currentTarget.getBoundingClientRect()
|
||||
: null;
|
||||
const rawX = panelRect && buttonRect ? buttonRect.right - panelRect.left - 184 : event.offsetX;
|
||||
const rawY = panelRect && buttonRect ? buttonRect.bottom - panelRect.top + 4 : event.offsetY;
|
||||
const maxX = Math.max(8, (panelRect?.width ?? window.innerWidth) - 192);
|
||||
const maxY = Math.max(8, (panelRect?.height ?? window.innerHeight) - 96);
|
||||
|
||||
contextCommit = commit;
|
||||
contextMenuX = Math.max(8, Math.min(rawX, maxX));
|
||||
contextMenuY = Math.max(8, Math.min(rawY, maxY));
|
||||
}
|
||||
|
||||
function closeCommitContextMenu() {
|
||||
contextCommit = null;
|
||||
}
|
||||
|
||||
async function createBranchFromContextCommit() {
|
||||
const commit = contextCommit;
|
||||
if (!commit || isBusy) return;
|
||||
closeCommitContextMenu();
|
||||
await onCreateBranchFromCommit(commit);
|
||||
}
|
||||
|
||||
async function restoreContextCommit() {
|
||||
const commit = contextCommit;
|
||||
if (!commit || isBusy) return;
|
||||
closeCommitContextMenu();
|
||||
await onRestoreCommit(commit);
|
||||
}
|
||||
|
||||
function handleWindowKeydown(event: KeyboardEvent) {
|
||||
if (event.key !== "Escape") return;
|
||||
closeCommitContextMenu();
|
||||
handleBranchDialogKeydown(event);
|
||||
}
|
||||
|
||||
function localBranchRefs(commit: GitCommit): string[] {
|
||||
const seen = new Set<string>();
|
||||
const labels: string[] = [];
|
||||
@@ -393,9 +463,9 @@
|
||||
let graphWidth = $derived(Math.max(Math.max(graph.columns, 1) * GRAPH_LANE + 18, 42));
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleBranchDialogKeydown} />
|
||||
<svelte:window onclick={closeCommitContextMenu} onkeydown={handleWindowKeydown} on:contextmenu|capture={closeCommitContextMenu} />
|
||||
|
||||
<section class="panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label="Commit history">
|
||||
<section bind:this={panelElement} class="panel history-panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label="Commit history">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<span class="eyebrow">History</span>
|
||||
@@ -431,9 +501,14 @@
|
||||
{#each visibleCommitEntries as entry, rowIndex (entry.commit.hash)}
|
||||
{@const item = entry.commit}
|
||||
{@const row = graphRows[rowIndex]}
|
||||
{@const hoverBranchRefs = visibleBranchLabels(row?.branchLabels ?? [])}
|
||||
{@const hoverBranchRefs = commitHoverBranchLabels(item, row)}
|
||||
{@const otherRefs = visibleRefs(item)}
|
||||
<article class="commit-row graph-row" class:merge-row={item.parents.length > 1} class:root-row={item.parents.length === 0} class:tip-row={item.refs.length > 0}>
|
||||
<article
|
||||
class="commit-row graph-row"
|
||||
class:merge-row={item.parents.length > 1}
|
||||
class:root-row={item.parents.length === 0}
|
||||
class:tip-row={item.refs.length > 0}
|
||||
>
|
||||
<div class="graph-gutter" style={`width:${graphWidth}px`} aria-hidden="true">
|
||||
{#if row}
|
||||
<svg class="graph-svg" viewBox={`0 0 ${graphWidth} 100`} preserveAspectRatio="none">
|
||||
@@ -461,7 +536,7 @@
|
||||
class:merge={item.parents.length > 1}
|
||||
class:tip={item.refs.length > 0}
|
||||
class:hidden-branch={!rowGraphIsVisible(row)}
|
||||
title={hoverBranchRefs.length > 0 ? `Contained in: ${hoverBranchRefs.join(", ")}` : item.short_hash}
|
||||
title={commitHoverTitle(item, row)}
|
||||
style={`left:${graphColX(row.dotCol)}px; --dot-color:${row.dotColor}`}
|
||||
></span>
|
||||
{#if hoverBranchRefs.length > 0}
|
||||
@@ -545,13 +620,17 @@
|
||||
<div class="commit-actions">
|
||||
<time class="commit-time" datetime={item.date}>{formatCommitDate(item.date)}</time>
|
||||
<div class="commit-action-buttons">
|
||||
<button class="btn-sm" type="button" onclick={() => onCreateBranchFromCommit(item)} disabled={isBusy} title="Create a new branch from this commit">
|
||||
<GitBranch size={15} aria-hidden="true" />
|
||||
Branch
|
||||
</button>
|
||||
<button class="btn-sm" type="button" onclick={() => onRestoreCommit(item)} disabled={isBusy} title="Bring this commit's files into your working tree as unstaged changes (no history is changed)">
|
||||
<RotateCcw size={15} aria-hidden="true" />
|
||||
Restore
|
||||
<button
|
||||
class="commit-menu-button"
|
||||
type="button"
|
||||
onclick={(event) => openCommitActionMenu(event, item)}
|
||||
disabled={isBusy}
|
||||
title="Commit actions"
|
||||
aria-label={`Actions for ${item.short_hash}`}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={contextCommit?.hash === item.hash}
|
||||
>
|
||||
<EllipsisVertical size={15} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -560,6 +639,25 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if contextCommit}
|
||||
<div
|
||||
class="history-context-menu"
|
||||
style={`left: ${contextMenuX}px; top: ${contextMenuY}px;`}
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
aria-label={`Actions for ${contextCommit.short_hash}`}
|
||||
>
|
||||
<button type="button" role="menuitem" onclick={createBranchFromContextCommit} disabled={isBusy}>
|
||||
<GitBranch size={14} aria-hidden="true" />
|
||||
Branch
|
||||
</button>
|
||||
<button type="button" role="menuitem" onclick={restoreContextCommit} disabled={isBusy}>
|
||||
<RotateCcw size={14} aria-hidden="true" />
|
||||
Restore
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{#if branchDialogOpen}
|
||||
|
||||
Reference in New Issue
Block a user