Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 31 additions & 13 deletions apps/staged/src/lib/features/branches/BranchCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,30 @@
let requestedTimelineKey: string | null = null;
let timelineLoadVersion = 0;
let revalidationVersion = 0;
let gitRefreshVersion = 0;

/**
* Kick off a background git-state refresh (TTL-gated fetch). Fresh state
* arrives via the `git-state-updated` event listener below.
*
* Clear the flag on settle, not just on the event: branches with no workdir
* (or a deleted worktree) resolve Ok without emitting. Version-gated so an
* older refresh settling can't end the spinner while a newer one — e.g. a
* `loadTimeline` re-run overlapping the mount-time hydration refresh — is
* still in flight.
*/
function startGitStateRefresh() {
const version = ++gitRefreshVersion;
refreshingGitState = true;
commands
.refreshBranchGitState(branch.id)
.catch(() => {})
.finally(() => {
if (version === gitRefreshVersion) {
refreshingGitState = false;
}
});
}

function isCurrentTimelineLoad(loadVersion: number, timelineKey: string): boolean {
return loadVersion === timelineLoadVersion && branchTimelineReadyKey(branch) === timelineKey;
Expand Down Expand Up @@ -565,11 +589,7 @@
void loadTimelineReviewDetails(cached.reviews);
}

// Kick off a background git-state refresh (TTL-gated fetch).
refreshingGitState = true;
commands.refreshBranchGitState(branch.id).catch(() => {
refreshingGitState = false;
});
startGitStateRefresh();
}

// Synchronously hydrate timeline from cache so isSettingUp is never true
Expand Down Expand Up @@ -697,7 +717,10 @@
if (timeline) {
timeline = { ...timeline, gitState: payload.gitState };
}
refreshingGitState = false;
// `refreshingGitState` is cleared by startGitStateRefresh's settle
// handler, not here: an event from an external refresh (e.g. the bulk
// refresh on project open) must not end the spinner while this card's
// own refresh is still in flight.
});

return () => {
Expand Down Expand Up @@ -799,12 +822,7 @@
}
}

// Kick off a background git-state refresh (TTL-gated fetch).
// The result arrives via the `git-state-updated` event listener above.
refreshingGitState = true;
commands.refreshBranchGitState(branch.id).catch(() => {
refreshingGitState = false;
});
startGitStateRefresh();
}

function getTimelineReviewDetails(fullReview: TimelineFullReview): TimelineReviewDetails {
Expand Down Expand Up @@ -1702,7 +1720,7 @@
baseBranch={isRemote
? (branch.workspaceName ?? formatBaseBranch(branch.baseBranch))
: formatBaseBranch(branch.baseBranch)}
parentAheadCount={refreshingGitState ? 0 : (timeline?.gitState?.base.commitsSinceFork ?? 0)}
parentAheadCount={timeline?.gitState?.base.commitsSinceFork ?? 0}
onRebase={branchCommandDisabledReason
? undefined
: () => startBranchCommandPipeline('rebase')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,21 @@
refreshingGitState = false,
fetchError = null,
}: Props = $props();

const capsuleTitle = $derived.by(() => {
if (!baseBranch || parentAheadCount <= 0) return baseBranch ?? undefined;
const behind = `${parentAheadCount} commit${parentAheadCount === 1 ? '' : 's'} behind`;
return refreshingGitState
? `${baseBranch} · ${behind} (checking for updates…)`
: `${baseBranch} · ${behind}`;
});
</script>

{#snippet capsule()}
<span class="branch-capsule" title={baseBranch ?? undefined}>
<span class="branch-capsule" title={capsuleTitle}>
{baseBranch}{#if parentAheadCount > 0}<span
class="ahead-count"
class:provisional={refreshingGitState}
transition:fade={{ duration: 150 }}
>
+{parentAheadCount}</span
Expand Down Expand Up @@ -175,6 +184,10 @@
color: var(--ui-accent);
}

.ahead-count.provisional {
opacity: 0.6;
}

.branch-warning {
display: inline-flex;
align-items: center;
Expand Down