Features/extend git features #20
+38
-26
@@ -395,6 +395,22 @@
|
|||||||
void backgroundRepoStatusTick(true);
|
void backgroundRepoStatusTick(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function backgroundFetchRepo(path: string) {
|
||||||
|
if (!autoRefreshEnabled || !path || backgroundFetchInFlight) return;
|
||||||
|
|
||||||
|
backgroundFetchInFlight = true;
|
||||||
|
try {
|
||||||
|
await fetchRemote(path);
|
||||||
|
const nextStatus = await getStatus(path);
|
||||||
|
if (sameRepoPath(path, activeRepoPath)) applyStatus(nextStatus);
|
||||||
|
else updateRepoManagementStatus(path, nextStatus);
|
||||||
|
} catch {
|
||||||
|
// ignore; manual Fetch/Pull surfaces auth or network problems
|
||||||
|
} finally {
|
||||||
|
backgroundFetchInFlight = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Keeps the repo list's branch/ahead/behind up to date for every *other* open
|
// Keeps the repo list's branch/ahead/behind up to date for every *other* open
|
||||||
// tab, not just the active one, so switching to the management view (or just
|
// tab, not just the active one, so switching to the management view (or just
|
||||||
// glancing at the tab bar) shows current data without an explicit fetch.
|
// glancing at the tab bar) shows current data without an explicit fetch.
|
||||||
@@ -408,6 +424,25 @@
|
|||||||
.filter((path) => !(activeView === "repository" && sameRepoPath(path, activeRepoPath)));
|
.filter((path) => !(activeView === "repository" && sameRepoPath(path, activeRepoPath)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateRepoManagementStatus(path: string, nextStatus: GitStatus) {
|
||||||
|
const openTab = repoTabs.find((tab) => sameRepoPath(tab.path, path));
|
||||||
|
const cached = repoStatusCache[repoKey(path)];
|
||||||
|
const row: RepoTab = {
|
||||||
|
path,
|
||||||
|
name: repoNameFromPath(path),
|
||||||
|
branch: nextStatus.current_branch,
|
||||||
|
ahead: nextStatus.ahead,
|
||||||
|
behind: nextStatus.behind,
|
||||||
|
changed: nextStatus.files.length,
|
||||||
|
lastOpened: openTab?.lastOpened ?? cached?.lastOpened ?? 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (openTab) {
|
||||||
|
repoTabs = repoTabs.map((tab) => sameRepoPath(tab.path, path) ? row : tab);
|
||||||
|
}
|
||||||
|
cacheRepoStatus(row);
|
||||||
|
}
|
||||||
|
|
||||||
async function backgroundRepoStatusTick(fetchFirst: boolean) {
|
async function backgroundRepoStatusTick(fetchFirst: boolean) {
|
||||||
if (!autoRefreshEnabled || backgroundRepoStatusInFlight) return;
|
if (!autoRefreshEnabled || backgroundRepoStatusInFlight) return;
|
||||||
const others = knownRepoPathsForBackground();
|
const others = knownRepoPathsForBackground();
|
||||||
@@ -427,22 +462,7 @@
|
|||||||
try {
|
try {
|
||||||
if (fetchFirst) await fetchRemote(path);
|
if (fetchFirst) await fetchRemote(path);
|
||||||
const nextStatus = await getStatus(path);
|
const nextStatus = await getStatus(path);
|
||||||
const openTab = repoTabs.find((tab) => sameRepoPath(tab.path, path));
|
updateRepoManagementStatus(path, nextStatus);
|
||||||
const cached = repoStatusCache[repoKey(path)];
|
|
||||||
const row: RepoTab = {
|
|
||||||
path,
|
|
||||||
name: repoNameFromPath(path),
|
|
||||||
branch: nextStatus.current_branch,
|
|
||||||
ahead: nextStatus.ahead,
|
|
||||||
behind: nextStatus.behind,
|
|
||||||
changed: nextStatus.files.length,
|
|
||||||
lastOpened: openTab?.lastOpened ?? cached?.lastOpened ?? 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (openTab) {
|
|
||||||
repoTabs = repoTabs.map((tab) => sameRepoPath(tab.path, path) ? row : tab);
|
|
||||||
}
|
|
||||||
cacheRepoStatus(row);
|
|
||||||
} catch {
|
} catch {
|
||||||
// ignore this repo — same rationale as the active-repo background fetch above
|
// ignore this repo — same rationale as the active-repo background fetch above
|
||||||
}
|
}
|
||||||
@@ -1169,16 +1189,6 @@
|
|||||||
// and the overlay appears to "come late".
|
// and the overlay appears to "come late".
|
||||||
await tick();
|
await tick();
|
||||||
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
||||||
// Fetch first so the bundle's ahead/behind reflects the remote's current
|
|
||||||
// state instead of whatever the local remote-tracking ref last saw. Best
|
|
||||||
// effort: no credentials are passed and failures are swallowed, same as
|
|
||||||
// the periodic background fetch — auth/network issues surface via the
|
|
||||||
// manual Fetch/Pull buttons instead of blocking (or erroring) repo open.
|
|
||||||
try {
|
|
||||||
await fetchRemote(path);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
// Single backend round-trip: resolves the repo and reads status, branches,
|
// Single backend round-trip: resolves the repo and reads status, branches,
|
||||||
// commits and files in one pass instead of four sequential git calls.
|
// commits and files in one pass instead of four sequential git calls.
|
||||||
const bundle = await openRepositoryBundle(path, 100);
|
const bundle = await openRepositoryBundle(path, 100);
|
||||||
@@ -1192,6 +1202,7 @@
|
|||||||
await refreshCommitHistory(activeRepoPath, bundle.commits);
|
await refreshCommitHistory(activeRepoPath, bundle.commits);
|
||||||
await refreshExplorerFiles(activeRepoPath, bundle.files);
|
await refreshExplorerFiles(activeRepoPath, bundle.files);
|
||||||
lastRepoSwitchAt = Date.now();
|
lastRepoSwitchAt = Date.now();
|
||||||
|
void backgroundFetchRepo(activeRepoPath);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2817,6 +2828,7 @@
|
|||||||
{commits}
|
{commits}
|
||||||
{localBranchNames}
|
{localBranchNames}
|
||||||
activeBranch={status?.current_branch ?? ""}
|
activeBranch={status?.current_branch ?? ""}
|
||||||
|
activeUpstream={status?.upstream ?? ""}
|
||||||
repositoryKey={activeRepoPath}
|
repositoryKey={activeRepoPath}
|
||||||
{hasRepository}
|
{hasRepository}
|
||||||
{isBusy}
|
{isBusy}
|
||||||
|
|||||||
+39
@@ -2241,6 +2241,15 @@
|
|||||||
.graph-svg path.hidden-branch {
|
.graph-svg path.hidden-branch {
|
||||||
opacity: 0.08;
|
opacity: 0.08;
|
||||||
}
|
}
|
||||||
|
.graph-svg path.graph-segment-ahead {
|
||||||
|
stroke: #e0a040;
|
||||||
|
filter: drop-shadow(0 0 3px rgba(224,160,64,0.24));
|
||||||
|
}
|
||||||
|
.graph-svg path.graph-segment-behind {
|
||||||
|
stroke: #7aacff;
|
||||||
|
stroke-dasharray: 4 4;
|
||||||
|
filter: drop-shadow(0 0 3px rgba(122,172,255,0.24));
|
||||||
|
}
|
||||||
.graph-dot {
|
.graph-dot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
@@ -2258,6 +2267,14 @@
|
|||||||
opacity: 0.16;
|
opacity: 0.16;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
.graph-dot.ahead {
|
||||||
|
background: #e0a040;
|
||||||
|
box-shadow: 0 0 0 1px rgba(224,160,64,0.42), 0 0 10px rgba(224,160,64,0.16);
|
||||||
|
}
|
||||||
|
.graph-dot.behind {
|
||||||
|
background: #7aacff;
|
||||||
|
box-shadow: 0 0 0 1px rgba(122,172,255,0.46), 0 0 10px rgba(122,172,255,0.18);
|
||||||
|
}
|
||||||
.graph-dot.tip { width: 14px; height: 14px; box-shadow: 0 0 0 1px var(--dot-color, #5a8cf8); }
|
.graph-dot.tip { width: 14px; height: 14px; box-shadow: 0 0 0 1px var(--dot-color, #5a8cf8); }
|
||||||
.graph-dot.merge {
|
.graph-dot.merge {
|
||||||
width: 15px;
|
width: 15px;
|
||||||
@@ -2303,6 +2320,22 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.graph-hover-branches span.remote,
|
||||||
|
.branch-filter-option.remote {
|
||||||
|
border-color: rgba(122,172,255,0.32);
|
||||||
|
color: #bcd2ff;
|
||||||
|
background: rgba(31,43,72,0.88);
|
||||||
|
}
|
||||||
|
.graph-hover-branches span.ahead {
|
||||||
|
border-color: rgba(224,160,64,0.42);
|
||||||
|
color: #ffd99a;
|
||||||
|
background: rgba(58,42,20,0.94);
|
||||||
|
}
|
||||||
|
.graph-hover-branches span.behind {
|
||||||
|
border-color: rgba(122,172,255,0.46);
|
||||||
|
color: #c7dbff;
|
||||||
|
background: rgba(25,39,70,0.94);
|
||||||
|
}
|
||||||
.graph-hover-branches svg {
|
.graph-hover-branches svg {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
color: #76d995;
|
color: #76d995;
|
||||||
@@ -2316,6 +2349,12 @@
|
|||||||
transition: background 120ms ease;
|
transition: background 120ms ease;
|
||||||
}
|
}
|
||||||
.graph-row + .graph-row .commit-body { border-top: 1px solid rgba(94,110,156,0.1); }
|
.graph-row + .graph-row .commit-body { border-top: 1px solid rgba(94,110,156,0.1); }
|
||||||
|
.graph-row.graph-ahead-row .commit-body {
|
||||||
|
box-shadow: inset 3px 0 0 rgba(224,160,64,0.72);
|
||||||
|
}
|
||||||
|
.graph-row.graph-behind-row .commit-body {
|
||||||
|
box-shadow: inset 3px 0 0 rgba(122,172,255,0.72);
|
||||||
|
}
|
||||||
.graph-row:hover .commit-body { background: rgba(37,40,62,0.54); }
|
.graph-row:hover .commit-body { background: rgba(37,40,62,0.54); }
|
||||||
.graph-row:hover .graph-svg path { opacity: 1; stroke-width: 2.65; }
|
.graph-row:hover .graph-svg path { opacity: 1; stroke-width: 2.65; }
|
||||||
.graph-row:hover .graph-svg path.hidden-branch { opacity: 0.12; stroke-width: 2.2; }
|
.graph-row:hover .graph-svg path.hidden-branch { opacity: 0.12; stroke-width: 2.2; }
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
commits: GitCommit[];
|
commits: GitCommit[];
|
||||||
localBranchNames: string[];
|
localBranchNames: string[];
|
||||||
activeBranch: string;
|
activeBranch: string;
|
||||||
|
activeUpstream: string;
|
||||||
repositoryKey: string;
|
repositoryKey: string;
|
||||||
hasRepository: boolean;
|
hasRepository: boolean;
|
||||||
isBusy: boolean;
|
isBusy: boolean;
|
||||||
@@ -47,6 +48,7 @@
|
|||||||
commits = [],
|
commits = [],
|
||||||
localBranchNames = [],
|
localBranchNames = [],
|
||||||
activeBranch = "",
|
activeBranch = "",
|
||||||
|
activeUpstream = "",
|
||||||
repositoryKey = "",
|
repositoryKey = "",
|
||||||
hasRepository = false,
|
hasRepository = false,
|
||||||
isBusy = false,
|
isBusy = false,
|
||||||
@@ -201,7 +203,13 @@
|
|||||||
return [...new Set(values.filter(Boolean))];
|
return [...new Set(values.filter(Boolean))];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let graphBranchNames = $derived(uniqueStrings([...localBranchNames, activeUpstream].filter(Boolean)));
|
||||||
|
let graphBranchNameSet = $derived(new Set(graphBranchNames));
|
||||||
|
|
||||||
function branchIsVisible(branch: string): boolean {
|
function branchIsVisible(branch: string): boolean {
|
||||||
|
if (branch === activeUpstream && activeUpstream) {
|
||||||
|
return !activeBranch || !hiddenGraphBranches.has(activeBranch);
|
||||||
|
}
|
||||||
return !hiddenGraphBranches.has(branch);
|
return !hiddenGraphBranches.has(branch);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +218,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function commitHoverBranchLabels(commit: GitCommit, row: GraphRow | undefined): string[] {
|
function commitHoverBranchLabels(commit: GitCommit, row: GraphRow | undefined): string[] {
|
||||||
const directBranches = localBranchRefs(commit);
|
const directBranches = graphBranchRefs(commit);
|
||||||
if (directBranches.length > 0) return directBranches;
|
if (directBranches.length > 0) return directBranches;
|
||||||
|
|
||||||
const containingBranches = row?.branchLabels ?? [];
|
const containingBranches = row?.branchLabels ?? [];
|
||||||
@@ -219,7 +227,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function commitHoverTitle(commit: GitCommit, row: GraphRow | undefined): string {
|
function commitHoverTitle(commit: GitCommit, row: GraphRow | undefined): string {
|
||||||
const directBranches = localBranchRefs(commit);
|
const directBranches = graphBranchRefs(commit);
|
||||||
if (directBranches.length > 0) return `Branches: ${directBranches.join(", ")}`;
|
if (directBranches.length > 0) return `Branches: ${directBranches.join(", ")}`;
|
||||||
|
|
||||||
const containingBranches = row?.branchLabels ?? [];
|
const containingBranches = row?.branchLabels ?? [];
|
||||||
@@ -231,8 +239,17 @@
|
|||||||
return segment.branches.length === 0 || segment.branches.some(branchIsVisible);
|
return segment.branches.length === 0 || segment.branches.some(branchIsVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncClassForBranches(labels: string[]): "" | "ahead" | "behind" {
|
||||||
|
if (!activeBranch || !activeUpstream) return "";
|
||||||
|
const hasLocal = labels.includes(activeBranch);
|
||||||
|
const hasRemote = labels.includes(activeUpstream);
|
||||||
|
if (hasLocal && !hasRemote) return "ahead";
|
||||||
|
if (hasRemote && !hasLocal) return "behind";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
function branchesAreVisible(branches: string[]): boolean {
|
function branchesAreVisible(branches: string[]): boolean {
|
||||||
if (localBranchNames.length === 0) return true;
|
if (graphBranchNames.length === 0) return true;
|
||||||
return branches.some(branchIsVisible);
|
return branches.some(branchIsVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +281,7 @@
|
|||||||
const membership = new Map<string, Set<string>>();
|
const membership = new Map<string, Set<string>>();
|
||||||
|
|
||||||
for (const commit of items) {
|
for (const commit of items) {
|
||||||
for (const branch of localBranchRefs(commit)) {
|
for (const branch of graphBranchRefs(commit)) {
|
||||||
const stack = [commit.hash];
|
const stack = [commit.hash];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
|
|
||||||
@@ -289,7 +306,7 @@
|
|||||||
return new Map(
|
return new Map(
|
||||||
items.map((commit) => [
|
items.map((commit) => [
|
||||||
commit.hash,
|
commit.hash,
|
||||||
localBranchNames.filter((branch) => membership.get(commit.hash)?.has(branch)),
|
graphBranchNames.filter((branch) => membership.get(commit.hash)?.has(branch)),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -407,6 +424,18 @@
|
|||||||
return labels;
|
return labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function graphBranchRefs(commit: GitCommit): string[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const labels: string[] = [];
|
||||||
|
for (const ref of commit.refs) {
|
||||||
|
const label = refLabel(ref);
|
||||||
|
if (!graphBranchNameSet.has(label) || seen.has(label)) continue;
|
||||||
|
seen.add(label);
|
||||||
|
labels.push(label);
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
function visibleRefs(commit: GitCommit): string[] {
|
function visibleRefs(commit: GitCommit): string[] {
|
||||||
return commit.refs.filter((ref) => !localBranchNameSet.has(refLabel(ref)));
|
return commit.refs.filter((ref) => !localBranchNameSet.has(refLabel(ref)));
|
||||||
}
|
}
|
||||||
@@ -440,7 +469,7 @@
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
const defaultBranch = activeBranch && localBranchNames.includes(activeBranch)
|
const defaultBranch = activeBranch && localBranchNames.includes(activeBranch)
|
||||||
? activeBranch
|
? activeBranch
|
||||||
: (localBranchNames[0] ?? "");
|
: (graphBranchNames[0] ?? "");
|
||||||
const defaultFilterKey = `${repositoryKey}::${defaultBranch}`;
|
const defaultFilterKey = `${repositoryKey}::${defaultBranch}`;
|
||||||
|
|
||||||
if (!defaultBranch) {
|
if (!defaultBranch) {
|
||||||
@@ -512,8 +541,11 @@
|
|||||||
{@const row = graphRows[rowIndex]}
|
{@const row = graphRows[rowIndex]}
|
||||||
{@const hoverBranchRefs = commitHoverBranchLabels(item, row)}
|
{@const hoverBranchRefs = commitHoverBranchLabels(item, row)}
|
||||||
{@const otherRefs = visibleRefs(item)}
|
{@const otherRefs = visibleRefs(item)}
|
||||||
|
{@const rowSyncClass = syncClassForBranches(row?.branchLabels ?? [])}
|
||||||
<article
|
<article
|
||||||
class="commit-row graph-row"
|
class="commit-row graph-row"
|
||||||
|
class:graph-ahead-row={rowSyncClass === "ahead"}
|
||||||
|
class:graph-behind-row={rowSyncClass === "behind"}
|
||||||
class:merge-row={item.parents.length > 1}
|
class:merge-row={item.parents.length > 1}
|
||||||
class:root-row={item.parents.length === 0}
|
class:root-row={item.parents.length === 0}
|
||||||
class:tip-row={item.refs.length > 0}
|
class:tip-row={item.refs.length > 0}
|
||||||
@@ -524,6 +556,8 @@
|
|||||||
{#each row.top as seg}
|
{#each row.top as seg}
|
||||||
<path
|
<path
|
||||||
class:hidden-branch={!segmentIsVisible(seg)}
|
class:hidden-branch={!segmentIsVisible(seg)}
|
||||||
|
class:graph-segment-ahead={syncClassForBranches(seg.branches) === "ahead"}
|
||||||
|
class:graph-segment-behind={syncClassForBranches(seg.branches) === "behind"}
|
||||||
d={graphPath(seg, 0, 50)}
|
d={graphPath(seg, 0, 50)}
|
||||||
stroke={seg.color}
|
stroke={seg.color}
|
||||||
stroke-width="2.2"
|
stroke-width="2.2"
|
||||||
@@ -533,6 +567,8 @@
|
|||||||
{#each row.bottom as seg}
|
{#each row.bottom as seg}
|
||||||
<path
|
<path
|
||||||
class:hidden-branch={!segmentIsVisible(seg)}
|
class:hidden-branch={!segmentIsVisible(seg)}
|
||||||
|
class:graph-segment-ahead={syncClassForBranches(seg.branches) === "ahead"}
|
||||||
|
class:graph-segment-behind={syncClassForBranches(seg.branches) === "behind"}
|
||||||
d={graphPath(seg, 50, 100)}
|
d={graphPath(seg, 50, 100)}
|
||||||
stroke={seg.color}
|
stroke={seg.color}
|
||||||
stroke-width="2.2"
|
stroke-width="2.2"
|
||||||
@@ -545,13 +581,20 @@
|
|||||||
class:merge={item.parents.length > 1}
|
class:merge={item.parents.length > 1}
|
||||||
class:tip={item.refs.length > 0}
|
class:tip={item.refs.length > 0}
|
||||||
class:hidden-branch={!rowGraphIsVisible(row)}
|
class:hidden-branch={!rowGraphIsVisible(row)}
|
||||||
|
class:ahead={rowSyncClass === "ahead"}
|
||||||
|
class:behind={rowSyncClass === "behind"}
|
||||||
title={commitHoverTitle(item, row)}
|
title={commitHoverTitle(item, row)}
|
||||||
style={`left:${graphColX(row.dotCol)}px; --dot-color:${row.dotColor}`}
|
style={`left:${graphColX(row.dotCol)}px; --dot-color:${row.dotColor}`}
|
||||||
></span>
|
></span>
|
||||||
{#if hoverBranchRefs.length > 0}
|
{#if hoverBranchRefs.length > 0}
|
||||||
<div class="graph-hover-branches" style={`left:${graphColX(row.dotCol) + 13}px`}>
|
<div class="graph-hover-branches" style={`left:${graphColX(row.dotCol) + 13}px`}>
|
||||||
{#each hoverBranchRefs as branch}
|
{#each hoverBranchRefs as branch}
|
||||||
<span title={branch}>
|
<span
|
||||||
|
class:remote={branch === activeUpstream}
|
||||||
|
class:ahead={activeBranch === branch && rowSyncClass === "ahead"}
|
||||||
|
class:behind={activeUpstream === branch && rowSyncClass === "behind"}
|
||||||
|
title={branch}
|
||||||
|
>
|
||||||
<GitBranch size={10} aria-hidden="true" />
|
<GitBranch size={10} aria-hidden="true" />
|
||||||
{branch}
|
{branch}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user