mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 16:03:26 +00:00
Fix push target upstream cache recovery
This commit is contained in:
@@ -2,6 +2,7 @@ import { getRuntimeGitStatus, getRuntimeGitUpstreamStatus } from '@/runtime/runt
|
||||
import {
|
||||
clearAutomaticPushTargetUpstreamStatusCache,
|
||||
getCachedAutomaticPushTargetUpstreamStatus,
|
||||
invalidateAutomaticPushTargetUpstreamStatusCache,
|
||||
storeCachedAutomaticPushTargetUpstreamStatus
|
||||
} from './push-target-upstream-refresh-cache'
|
||||
import type {
|
||||
@@ -95,7 +96,21 @@ async function fetchAndApplyAutomaticUpstreamStatus({
|
||||
applyUpstreamStatus: false
|
||||
}
|
||||
)
|
||||
if (!upstreamStatus || !shouldApplyAutomaticUpstreamRefresh(worktreeId, startGeneration)) {
|
||||
if (!upstreamStatus) {
|
||||
if (pushTarget) {
|
||||
// Why: failed publish-target refreshes must not let an older automatic
|
||||
// cache entry suppress the next recovery poll for the same target.
|
||||
invalidateAutomaticPushTargetUpstreamStatusCache({
|
||||
settings,
|
||||
worktreeId,
|
||||
worktreePath,
|
||||
connectionId,
|
||||
pushTarget
|
||||
})
|
||||
}
|
||||
return null
|
||||
}
|
||||
if (!shouldApplyAutomaticUpstreamRefresh(worktreeId, startGeneration)) {
|
||||
return null
|
||||
}
|
||||
deps.setUpstreamStatus(worktreeId, upstreamStatus)
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from './git-status-refresh'
|
||||
import {
|
||||
getCachedAutomaticPushTargetUpstreamStatus,
|
||||
invalidateAutomaticPushTargetUpstreamStatusCache,
|
||||
storeCachedAutomaticPushTargetUpstreamStatus
|
||||
} from './push-target-upstream-refresh-cache'
|
||||
import type { GitPushTarget, GitStatusResult, GitUpstreamStatus } from '../../../../shared/types'
|
||||
@@ -107,6 +108,42 @@ describe('push-target upstream refresh cache', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('retries after a failed push-target refresh invalidates an unchanged automatic poll cache hit', async () => {
|
||||
stubGitStatus([unchangedStatus])
|
||||
const deps = makeDeps()
|
||||
vi.mocked(deps.fetchUpstreamStatus)
|
||||
.mockResolvedValueOnce({
|
||||
hasUpstream: true,
|
||||
upstreamName: 'fork/feature/pr-head',
|
||||
ahead: 1,
|
||||
behind: 0
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
hasUpstream: true,
|
||||
upstreamName: 'fork/feature/pr-head',
|
||||
ahead: 0,
|
||||
behind: 0
|
||||
})
|
||||
|
||||
await refreshAutomatically({ deps })
|
||||
invalidateAutomaticPushTargetUpstreamStatusCache({
|
||||
settings: { activeRuntimeEnvironmentId: null },
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/repo',
|
||||
pushTarget
|
||||
})
|
||||
await refreshAutomatically({ deps })
|
||||
await refreshAutomatically({ deps })
|
||||
|
||||
expect(deps.fetchUpstreamStatus).toHaveBeenCalledTimes(2)
|
||||
expect(deps.setUpstreamStatus).toHaveBeenLastCalledWith('wt-1', {
|
||||
hasUpstream: true,
|
||||
upstreamName: 'fork/feature/pr-head',
|
||||
ahead: 0,
|
||||
behind: 0
|
||||
})
|
||||
})
|
||||
|
||||
it('refreshes again when HEAD changes', async () => {
|
||||
stubGitStatus([
|
||||
{ ...unchangedStatus, head: 'abc123' },
|
||||
|
||||
@@ -9,6 +9,7 @@ const AUTOMATIC_PUSH_TARGET_UPSTREAM_REFRESH_TTL_MS = 60_000
|
||||
const MAX_AUTOMATIC_PUSH_TARGET_UPSTREAM_CACHE_ENTRIES = 1024
|
||||
|
||||
type PushTargetUpstreamRefreshCacheEntry = {
|
||||
scopeKey: string
|
||||
status: GitUpstreamStatus
|
||||
refreshedAt: number
|
||||
}
|
||||
@@ -37,6 +38,28 @@ function getStatusIdentityKey(status: GitStatusResult): readonly unknown[] {
|
||||
return [status.head ?? null, status.branch ?? null]
|
||||
}
|
||||
|
||||
function getCacheScopeKey({
|
||||
settings,
|
||||
worktreeId,
|
||||
worktreePath,
|
||||
connectionId,
|
||||
pushTarget
|
||||
}: {
|
||||
settings?: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null
|
||||
worktreeId: string
|
||||
worktreePath: string
|
||||
connectionId?: string
|
||||
pushTarget: GitPushTarget
|
||||
}): string {
|
||||
return JSON.stringify([
|
||||
worktreeId,
|
||||
worktreePath,
|
||||
connectionId ?? null,
|
||||
getRuntimeEnvironmentKey(settings),
|
||||
getPushTargetKey(pushTarget)
|
||||
])
|
||||
}
|
||||
|
||||
function getCacheKey({
|
||||
settings,
|
||||
worktreeId,
|
||||
@@ -53,11 +76,7 @@ function getCacheKey({
|
||||
status: GitStatusResult
|
||||
}): string {
|
||||
return JSON.stringify([
|
||||
worktreeId,
|
||||
worktreePath,
|
||||
connectionId ?? null,
|
||||
getRuntimeEnvironmentKey(settings),
|
||||
getPushTargetKey(pushTarget),
|
||||
getCacheScopeKey({ settings, worktreeId, worktreePath, connectionId, pushTarget }),
|
||||
getStatusIdentityKey(status)
|
||||
])
|
||||
}
|
||||
@@ -106,12 +125,28 @@ export function storeCachedAutomaticPushTargetUpstreamStatus(
|
||||
upstreamStatus: GitUpstreamStatus
|
||||
): void {
|
||||
automaticPushTargetUpstreamRefreshCache.set(getCacheKey(input), {
|
||||
scopeKey: getCacheScopeKey(input),
|
||||
status: upstreamStatus,
|
||||
refreshedAt: Date.now()
|
||||
})
|
||||
trimAutomaticPushTargetUpstreamRefreshCache()
|
||||
}
|
||||
|
||||
export function invalidateAutomaticPushTargetUpstreamStatusCache(input: {
|
||||
settings?: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null
|
||||
worktreeId: string
|
||||
worktreePath: string
|
||||
connectionId?: string
|
||||
pushTarget: GitPushTarget
|
||||
}): void {
|
||||
const scopeKey = getCacheScopeKey(input)
|
||||
for (const [key, entry] of automaticPushTargetUpstreamRefreshCache) {
|
||||
if (entry.scopeKey === scopeKey) {
|
||||
automaticPushTargetUpstreamRefreshCache.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function clearAutomaticPushTargetUpstreamStatusCache(): void {
|
||||
automaticPushTargetUpstreamRefreshCache.clear()
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants'
|
||||
import { clampMarkdownTocPanelWidth } from '../../../../shared/markdown-toc-panel-width'
|
||||
import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import type { RemoteOpKind } from '@/components/right-sidebar/source-control-primary-action'
|
||||
import { invalidateAutomaticPushTargetUpstreamStatusCache } from '@/components/right-sidebar/push-target-upstream-refresh-cache'
|
||||
import {
|
||||
isNonFastForwardRemoteError,
|
||||
resolveRemoteOperationErrorMessage
|
||||
@@ -3545,8 +3546,8 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
||||
}
|
||||
}),
|
||||
fetchUpstreamStatus: async (worktreeId, worktreePath, connectionId, pushTarget, options) => {
|
||||
const runtimeSettings = options?.runtimeTargetSettings ?? get().settings
|
||||
try {
|
||||
const runtimeSettings = options?.runtimeTargetSettings ?? get().settings
|
||||
const status = await getRuntimeGitUpstreamStatus(
|
||||
{
|
||||
settings: runtimeSettings,
|
||||
@@ -3567,6 +3568,17 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
||||
// re-publish, clobbering the upstream relationship. If the branch is
|
||||
// genuinely newly unpublished, the polling effect will eventually correct
|
||||
// the status on success.
|
||||
if (pushTarget) {
|
||||
// Why: an old automatic poll cache entry must not suppress the next
|
||||
// retry after a post-push/fetch refresh fails transiently.
|
||||
invalidateAutomaticPushTargetUpstreamStatusCache({
|
||||
settings: runtimeSettings,
|
||||
worktreeId,
|
||||
worktreePath,
|
||||
connectionId,
|
||||
pushTarget
|
||||
})
|
||||
}
|
||||
console.error('fetchUpstreamStatus failed', error)
|
||||
return null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user