Refresh optimistic worktrees after base fetch

This commit is contained in:
Jinjing
2026-05-19 00:39:30 -07:00
parent cf614b5f4f
commit db5fb4c54b
5 changed files with 554 additions and 4 deletions
+106
View File
@@ -0,0 +1,106 @@
# Worktree Post-Create Base Refresh
## Problem
Before this change, only one local-create path was intentionally stale-on-create:
- `baseBranch` resolves to a remote-tracking ref.
- That ref exists locally.
- Fetch freshness is stale.
In that path, Orca starts `getOrStartRemoteFetch(...)` and does not await it before `git worktree add` ([src/main/ipc/worktree-remote.ts](/Users/jinjingliang/Documents/projects/orca/https-github.com-stablyai-orca-issues-2307/src/main/ipc/worktree-remote.ts:531), [src/main/ipc/worktree-remote.ts](/Users/jinjingliang/Documents/projects/orca/https-github.com-stablyai-orca-issues-2307/src/main/ipc/worktree-remote.ts:724)). The new worktree may be created from stale `refs/remotes/<remote>/<branch>`.
Later, `reconcileWorktreeBaseStatus` classifies the result (`current` / `drift` / `base_changed` / `unknown`) but did not repair the worktree ([src/main/runtime/orca-runtime.ts](/Users/jinjingliang/Documents/projects/orca/https-github.com-stablyai-orca-issues-2307/src/main/runtime/orca-runtime.ts:6068)).
Important current behavior constraints:
- If the remote-tracking ref does not exist locally, create blocks on fetch before `addWorktree`; this path is not stale-on-create.
- If fetch freshness is hit, create previously did not schedule reconcile: no `checking`, no post-create classification ([src/main/ipc/worktree-remote.ts](/Users/jinjingliang/Documents/projects/orca/https-github.com-stablyai-orca-issues-2307/src/main/ipc/worktree-remote.ts:551)).
- `refreshLocalBaseRefOnWorktreeCreate` is separate and default-off ([src/shared/constants.ts](/Users/jinjingliang/Documents/projects/orca/https-github.com-stablyai-orca-issues-2307/src/shared/constants.ts:145)). It may fast-forward a local branch pointer, but does not make the remote-tracking base fresh ([src/main/git/worktree.ts](/Users/jinjingliang/Documents/projects/orca/https-github.com-stablyai-orca-issues-2307/src/main/git/worktree.ts:164)).
## Root Cause
Latency optimization decouples create from fetch completion, but reconcile is read-only. There is no guarded post-fetch fast-forward of the just-created worktree.
## Non-Goals
- Do not block the optimistic local-create path on network fetch.
- Do not mutate worktrees after any user/setup/agent filesystem or commit activity.
- Do not change base-ref selection semantics.
- Do not add extra fetches beyond existing `getOrStartRemoteFetch` reuse.
- Do not broaden to SSH in this change.
## Design
1. Keep current non-blocking create behavior in the optimistic path.
2. Extend reconcile with guarded auto-refresh before emitting `drift`.
Trigger only when:
- `postFetchSha !== createdBaseSha`.
- `createdBaseSha` is an ancestor of `postFetchSha`.
Action:
- `git reset --hard <postFetchSha>` in the created worktree.
3. Required guards before reset.
- Reconcile token still current.
- Worktree `instanceId` still matches create-time value.
- Worktree path still exists and is still a git worktree.
- `HEAD` still equals `createdBaseSha`.
- Current branch still equals created branch (skip detached/switch).
- Worktree is clean via `git status --porcelain` (include untracked).
4. Status behavior.
- Reset succeeds: emit `current`.
- Any guard fails in fast-forward case: emit existing `drift` payload.
- Non-ancestor: emit `base_changed`.
- Keep existing publish-remote conflict behavior unchanged: only run on `current` / `drift` / `base_changed` (not on `unknown`).
5. Close the fresh-cache classification gap.
Freshness-hit creates now still schedule reconcile by passing `Promise.resolve({ ok: true })` as `fetchPromise`. This adds `checking` -> `current` consistency without starting another fetch.
## Required Plumbing Changes
- Thread `createdWorktreePath` and `createdInstanceId` from create into reconcile args.
- Reconcile currently only gets `worktreeId`; that is insufficient for safe mutation without extra lookups and path-reuse protection.
- Add a runtime helper for “validate untouched + reset hard to postFetchSha”.
- Re-validate token immediately before mutation and before emit.
## Consistency and Concurrency
- Token check handles stale async completions in one runtime instance.
- `instanceId` check is mandatory for same-path delete/recreate races.
- Fetch cache key canonicalizes by git common-dir + remote, so parallel creates across linked worktrees share one in-flight fetch/freshness window.
- External git mutations between create and reconcile are expected; guards must downgrade to non-mutating classification.
- Multi-window renderers are unaffected; event type remains `WorktreeBaseStatusEvent`.
## Edge Cases
- Fetch fails/offline: emit `unknown`, no mutation.
- Base ref missing after fetch (`rev-parse` fails): emit `unknown`.
- Force-push/rewrite (non-ancestor): emit `base_changed`, no mutation.
- User/setup/agent file writes (including untracked): skip reset, emit `drift`.
- User commit before reconcile: `HEAD` mismatch, skip reset.
- Branch switched/detached: skip reset.
- Worktree removed/recreated at same path: token + instance check prevent wrong-target mutation.
## Rollout
1. Add reconcile-time untouched-state/reset helper in runtime.
2. Pass `createdWorktreePath` + `createdInstanceId` into reconcile.
3. Invoke helper after ancestry check and before drift emit.
4. Always schedule reconcile in the optimistic-base path, including freshness-hit creates.
5. Add tests for:
- successful guarded auto-refresh to `current`;
- dirty/untracked skip;
- HEAD-changed skip;
- branch-switched/detached skip;
- non-ancestor `base_changed`;
- stale-token and instance-id mismatch skip;
- deleted/recreated path skip;
- freshness-hit path schedules reconcile (if adopted).
6. Keep non-blocking cold-fetch create test intent intact (`src/main/ipc/worktrees.test.ts`).
+7 -4
View File
@@ -551,9 +551,9 @@ export async function createLocalWorktree(
const hasLocalBaseRef = await runtime.hasRemoteTrackingRef(repo.path, optimisticBase)
if (hasLocalBaseRef) {
const isFresh = await runtime.isRemoteFetchFresh(repo.path, optimisticBase.remote)
if (!isFresh) {
optimisticFetchPromise = runtime.getOrStartRemoteFetch(repo.path, optimisticBase.remote)
}
optimisticFetchPromise = isFresh
? Promise.resolve({ ok: true })
: runtime.getOrStartRemoteFetch(repo.path, optimisticBase.remote)
} else {
emitCreateWorktreeProgress(mainWindow, 'fetching')
const result = await runtime.getOrStartRemoteFetch(repo.path, optimisticBase.remote)
@@ -770,11 +770,12 @@ export async function createLocalWorktree(
const worktreeId = `${repo.id}::${created.path}`
const now = Date.now()
const createdInstanceId = randomUUID()
const metaUpdates: Partial<WorktreeMeta> = {
// Why: path-derived worktree IDs can be reused after external deletion.
// Fresh creations must rotate instance identity so stale lineage cannot
// attach to the new occupant of the same path.
instanceId: randomUUID(),
instanceId: createdInstanceId,
// Stamp activity so the worktree sorts into its final position
// immediately — prevents scroll-to-reveal racing with a later
// bumpWorktreeActivity that would re-sort the list.
@@ -882,6 +883,8 @@ export async function createLocalWorktree(
repoId: repo.id,
repoPath: repo.path,
worktreeId,
createdWorktreePath: created.path,
createdInstanceId,
base: optimisticBase,
branchName,
createdBaseSha,
+54
View File
@@ -956,6 +956,8 @@ describe('registerWorktreeHandlers', () => {
})
expect(runtimeStub.reconcileWorktreeBaseStatus).toHaveBeenCalledWith(
expect.objectContaining({
createdWorktreePath: '/workspace/improve-dashboard',
createdInstanceId: expect.any(String),
createdBaseSha: 'created-sha',
fetchPromise: pendingFetch
})
@@ -968,6 +970,58 @@ describe('registerWorktreeHandlers', () => {
resolveFetch()
})
it('schedules optimistic reconcile when remote-tracking base freshness is cached', async () => {
const remoteBase = {
remote: 'origin',
branch: 'main',
ref: 'refs/remotes/origin/main',
base: 'origin/main'
}
runtimeStub.resolveRemoteTrackingBase.mockResolvedValue(remoteBase)
runtimeStub.hasRemoteTrackingRef.mockResolvedValue(true)
runtimeStub.isRemoteFetchFresh.mockResolvedValue(true)
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/improve-dashboard',
head: 'created-sha',
branch: 'improve-dashboard',
isBare: false,
isMainWorktree: false
}
])
gitExecFileAsyncMock.mockResolvedValue({ stdout: 'created-sha\n', stderr: '' })
const result = await handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'improve-dashboard'
})
expect(runtimeStub.getOrStartRemoteFetch).not.toHaveBeenCalled()
expect(runtimeStub.emitWorktreeBaseStatus).toHaveBeenCalledWith({
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/improve-dashboard',
status: 'checking',
base: 'origin/main',
remote: 'origin'
})
expect(runtimeStub.reconcileWorktreeBaseStatus).toHaveBeenCalledWith(
expect.objectContaining({
createdWorktreePath: '/workspace/improve-dashboard',
createdInstanceId: expect.any(String),
createdBaseSha: 'created-sha',
fetchPromise: expect.any(Promise)
})
)
await expect(
runtimeStub.reconcileWorktreeBaseStatus.mock.calls[0][0].fetchPromise
).resolves.toEqual({ ok: true })
expect(result).toEqual(
expect.objectContaining({
initialBaseStatus: expect.objectContaining({ status: 'checking', base: 'origin/main' })
})
)
})
it('throws a clear error when no default base ref can be resolved', async () => {
// Why: guard against regressing to a silent 'origin/main' fallback. When
// getDefaultBaseRef returns null (e.g. a fresh repo with no origin/HEAD,
+302
View File
@@ -5470,6 +5470,8 @@ describe('OrcaRuntimeService', () => {
repoId: TEST_REPO_ID,
repoPath: TEST_REPO_PATH,
worktreeId: TEST_WORKTREE_ID,
createdWorktreePath: TEST_WORKTREE_PATH,
createdInstanceId: 'instance-1',
base: {
remote: 'origin',
branch: 'main',
@@ -5485,6 +5487,306 @@ describe('OrcaRuntimeService', () => {
expect(worktreeBaseStatus).not.toHaveBeenCalled()
})
const remoteTrackingBase = {
remote: 'origin',
branch: 'main',
ref: 'refs/remotes/origin/main',
base: 'origin/main'
}
function createReconcileRuntime(instanceId: string | null = 'instance-1'): {
runtime: OrcaRuntimeService
worktreeBaseStatus: ReturnType<typeof vi.fn>
} {
const worktreeBaseStatus = vi.fn()
const runtimeStore = {
...store,
getWorktreeMeta: (worktreeId: string) =>
worktreeId === TEST_WORKTREE_ID && instanceId
? { ...store.getWorktreeMeta(worktreeId), instanceId }
: store.getWorktreeMeta(worktreeId)
}
const runtime = new OrcaRuntimeService(runtimeStore as never)
runtime.setNotifier({
worktreeBaseStatus,
worktreeRemoteBranchConflict: vi.fn()
} as never)
return { runtime, worktreeBaseStatus }
}
function mockReconcileGit(options: {
postFetchSha?: string
ancestor?: boolean
worktreeExists?: boolean
headSha?: string
branchName?: string
branchDetached?: boolean
status?: string
baseRefMissing?: boolean
}) {
const {
postFetchSha = 'new-base-sha',
ancestor = true,
worktreeExists = true,
headSha = 'created-base-sha',
branchName = 'feature',
branchDetached = false,
status = '',
baseRefMissing = false
} = options
return vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args, options) => {
const command = args as string[]
const cwd = (options as { cwd?: string } | undefined)?.cwd
if (
cwd === TEST_REPO_PATH &&
command[0] === 'rev-parse' &&
command[1] === '--verify' &&
command[2] === `${remoteTrackingBase.ref}^{commit}`
) {
if (baseRefMissing) {
throw new Error('missing base ref')
}
return { stdout: `${postFetchSha}\n`, stderr: '' }
}
if (cwd === TEST_REPO_PATH && command[0] === 'merge-base') {
if (!ancestor) {
throw new Error('not ancestor')
}
return { stdout: '', stderr: '' }
}
if (cwd === TEST_WORKTREE_PATH && command[0] === 'rev-parse') {
if (command[1] === '--is-inside-work-tree') {
if (!worktreeExists) {
throw new Error('not a worktree')
}
return { stdout: 'true\n', stderr: '' }
}
if (command[1] === '--verify' && command[2] === 'HEAD^{commit}') {
return { stdout: `${headSha}\n`, stderr: '' }
}
}
if (cwd === TEST_WORKTREE_PATH && command[0] === 'symbolic-ref') {
if (branchDetached) {
throw new Error('detached')
}
return { stdout: `${branchName}\n`, stderr: '' }
}
if (cwd === TEST_WORKTREE_PATH && command[0] === 'status') {
return { stdout: status, stderr: '' }
}
if (cwd === TEST_WORKTREE_PATH && command[0] === 'reset') {
return { stdout: `HEAD is now at ${postFetchSha}\n`, stderr: '' }
}
if (cwd === TEST_REPO_PATH && command[0] === 'rev-list') {
return { stdout: '3\n', stderr: '' }
}
if (cwd === TEST_REPO_PATH && command[0] === 'log') {
return { stdout: 'base commit 3\nbase commit 2\n', stderr: '' }
}
if (cwd === TEST_REPO_PATH && command[0] === 'config') {
throw new Error('config missing')
}
if (
cwd === TEST_REPO_PATH &&
command[0] === 'rev-parse' &&
command[1] === '--verify' &&
command[2] === 'refs/remotes/origin/feature^{commit}'
) {
throw new Error('no publish branch conflict')
}
throw new Error(`unexpected git command: ${command.join(' ')}`)
})
}
async function reconcileWithToken(runtime: OrcaRuntimeService, token: string): Promise<void> {
await runtime.reconcileWorktreeBaseStatus({
repoId: TEST_REPO_ID,
repoPath: TEST_REPO_PATH,
worktreeId: TEST_WORKTREE_ID,
createdWorktreePath: TEST_WORKTREE_PATH,
createdInstanceId: 'instance-1',
base: remoteTrackingBase,
branchName: 'feature',
createdBaseSha: 'created-base-sha',
token,
fetchPromise: Promise.resolve({ ok: true })
})
}
it('auto-refreshes an untouched created worktree to current after fetch fast-forwards base', async () => {
const { runtime, worktreeBaseStatus } = createReconcileRuntime()
const token = runtime.recordOptimisticReconcileToken(TEST_WORKTREE_ID)
const gitSpy = mockReconcileGit({})
try {
await reconcileWithToken(runtime, token)
expect(gitSpy).toHaveBeenCalledWith(['reset', '--hard', 'new-base-sha'], {
cwd: TEST_WORKTREE_PATH
})
expect(worktreeBaseStatus).toHaveBeenCalledWith({
repoId: TEST_REPO_ID,
worktreeId: TEST_WORKTREE_ID,
base: 'origin/main',
remote: 'origin',
status: 'current'
})
} finally {
gitSpy.mockRestore()
}
})
it('skips auto-refresh and emits drift when the created worktree has untracked files', async () => {
const { runtime, worktreeBaseStatus } = createReconcileRuntime()
const token = runtime.recordOptimisticReconcileToken(TEST_WORKTREE_ID)
const gitSpy = mockReconcileGit({ status: '?? scratch.txt\n' })
try {
await reconcileWithToken(runtime, token)
expect(gitSpy).not.toHaveBeenCalledWith(['reset', '--hard', 'new-base-sha'], {
cwd: TEST_WORKTREE_PATH
})
expect(worktreeBaseStatus).toHaveBeenCalledWith(
expect.objectContaining({
status: 'drift',
behind: 3,
recentSubjects: ['base commit 3', 'base commit 2']
})
)
} finally {
gitSpy.mockRestore()
}
})
it('skips auto-refresh and emits drift when HEAD changed before reconcile', async () => {
const { runtime, worktreeBaseStatus } = createReconcileRuntime()
const token = runtime.recordOptimisticReconcileToken(TEST_WORKTREE_ID)
const gitSpy = mockReconcileGit({ headSha: 'user-commit-sha' })
try {
await reconcileWithToken(runtime, token)
expect(gitSpy).not.toHaveBeenCalledWith(['reset', '--hard', 'new-base-sha'], {
cwd: TEST_WORKTREE_PATH
})
expect(worktreeBaseStatus).toHaveBeenCalledWith(expect.objectContaining({ status: 'drift' }))
} finally {
gitSpy.mockRestore()
}
})
it('skips auto-refresh and emits drift when the created branch was switched or detached', async () => {
const { runtime, worktreeBaseStatus } = createReconcileRuntime()
const token = runtime.recordOptimisticReconcileToken(TEST_WORKTREE_ID)
const gitSpy = mockReconcileGit({ branchDetached: true })
try {
await reconcileWithToken(runtime, token)
expect(gitSpy).not.toHaveBeenCalledWith(['reset', '--hard', 'new-base-sha'], {
cwd: TEST_WORKTREE_PATH
})
expect(worktreeBaseStatus).toHaveBeenCalledWith(expect.objectContaining({ status: 'drift' }))
} finally {
gitSpy.mockRestore()
}
})
it('emits base_changed without mutation when the fetched base rewrote history', async () => {
const { runtime, worktreeBaseStatus } = createReconcileRuntime()
const token = runtime.recordOptimisticReconcileToken(TEST_WORKTREE_ID)
const gitSpy = mockReconcileGit({ ancestor: false })
try {
await reconcileWithToken(runtime, token)
expect(gitSpy).not.toHaveBeenCalledWith(['reset', '--hard', 'new-base-sha'], {
cwd: TEST_WORKTREE_PATH
})
expect(worktreeBaseStatus).toHaveBeenCalledWith(
expect.objectContaining({ status: 'base_changed' })
)
} finally {
gitSpy.mockRestore()
}
})
it('skips stale-token and instance-mismatch reconciles without mutating or emitting stale status', async () => {
const stale = createReconcileRuntime()
const staleToken = stale.runtime.recordOptimisticReconcileToken(TEST_WORKTREE_ID)
stale.runtime.recordOptimisticReconcileToken(TEST_WORKTREE_ID)
const staleGitSpy = mockReconcileGit({})
try {
await reconcileWithToken(stale.runtime, staleToken)
expect(stale.worktreeBaseStatus).not.toHaveBeenCalled()
expect(staleGitSpy).not.toHaveBeenCalled()
} finally {
staleGitSpy.mockRestore()
}
const mismatch = createReconcileRuntime('new-instance')
const mismatchToken = mismatch.runtime.recordOptimisticReconcileToken(TEST_WORKTREE_ID)
const mismatchGitSpy = mockReconcileGit({})
try {
await reconcileWithToken(mismatch.runtime, mismatchToken)
expect(mismatchGitSpy).not.toHaveBeenCalledWith(['reset', '--hard', 'new-base-sha'], {
cwd: TEST_WORKTREE_PATH
})
expect(mismatch.worktreeBaseStatus).not.toHaveBeenCalled()
} finally {
mismatchGitSpy.mockRestore()
}
})
it('skips auto-refresh and emits drift when the created path is gone', async () => {
const { runtime, worktreeBaseStatus } = createReconcileRuntime()
const token = runtime.recordOptimisticReconcileToken(TEST_WORKTREE_ID)
const gitSpy = mockReconcileGit({ worktreeExists: false })
try {
await reconcileWithToken(runtime, token)
expect(gitSpy).not.toHaveBeenCalledWith(['reset', '--hard', 'new-base-sha'], {
cwd: TEST_WORKTREE_PATH
})
expect(worktreeBaseStatus).toHaveBeenCalledWith(expect.objectContaining({ status: 'drift' }))
} finally {
gitSpy.mockRestore()
}
})
it('emits unknown without mutation when fetch fails or the base ref is missing', async () => {
const fetchFailure = createReconcileRuntime()
const fetchFailureToken = fetchFailure.runtime.recordOptimisticReconcileToken(TEST_WORKTREE_ID)
await fetchFailure.runtime.reconcileWorktreeBaseStatus({
repoId: TEST_REPO_ID,
repoPath: TEST_REPO_PATH,
worktreeId: TEST_WORKTREE_ID,
createdWorktreePath: TEST_WORKTREE_PATH,
createdInstanceId: 'instance-1',
base: remoteTrackingBase,
branchName: 'feature',
createdBaseSha: 'created-base-sha',
token: fetchFailureToken,
fetchPromise: Promise.resolve({ ok: false, errorKind: 'git_error' })
})
expect(fetchFailure.worktreeBaseStatus).toHaveBeenCalledWith(
expect.objectContaining({ status: 'unknown' })
)
const missingBase = createReconcileRuntime()
const missingBaseToken = missingBase.runtime.recordOptimisticReconcileToken(TEST_WORKTREE_ID)
const gitSpy = mockReconcileGit({ baseRefMissing: true })
try {
await reconcileWithToken(missingBase.runtime, missingBaseToken)
expect(gitSpy).not.toHaveBeenCalledWith(['reset', '--hard', 'new-base-sha'], {
cwd: TEST_WORKTREE_PATH
})
expect(missingBase.worktreeBaseStatus).toHaveBeenCalledWith(
expect.objectContaining({ status: 'unknown' })
)
} finally {
gitSpy.mockRestore()
}
})
it('invalidates the filesystem-auth cache after CLI worktree creation', async () => {
// Reproduces: CLI-created worktrees fail with "Access denied: unknown
// repository or worktree path" because the filesystem-auth cache was
+85
View File
@@ -6065,10 +6065,77 @@ export class OrcaRuntimeService {
this.notifier?.worktreeBaseStatus?.(event)
}
private async refreshUntouchedCreatedWorktree(args: {
worktreeId: string
createdWorktreePath: string
createdInstanceId: string
branchName: string
createdBaseSha: string
postFetchSha: string
stillCurrent: () => boolean
}): Promise<'refreshed' | 'skipped' | 'stale'> {
const instanceMatches = (): boolean =>
this.store?.getWorktreeMeta(args.worktreeId)?.instanceId === args.createdInstanceId
if (!args.stillCurrent()) {
return 'stale'
}
if (!instanceMatches()) {
return 'stale'
}
try {
const { stdout: isWorkTreeStdout } = await gitExecFileAsync(
['rev-parse', '--is-inside-work-tree'],
{ cwd: args.createdWorktreePath }
)
if (isWorkTreeStdout.trim() !== 'true') {
return 'skipped'
}
const { stdout: headStdout } = await gitExecFileAsync(
['rev-parse', '--verify', 'HEAD^{commit}'],
{ cwd: args.createdWorktreePath }
)
if (headStdout.trim() !== args.createdBaseSha) {
return 'skipped'
}
const { stdout: branchStdout } = await gitExecFileAsync(
['symbolic-ref', '--quiet', '--short', 'HEAD'],
{ cwd: args.createdWorktreePath }
)
if (branchStdout.trim() !== args.branchName) {
return 'skipped'
}
const { stdout: statusStdout } = await gitExecFileAsync(['status', '--porcelain'], {
cwd: args.createdWorktreePath
})
if (statusStdout.trim()) {
return 'skipped'
}
if (!args.stillCurrent()) {
return 'stale'
}
if (!instanceMatches()) {
return 'stale'
}
await gitExecFileAsync(['reset', '--hard', args.postFetchSha], {
cwd: args.createdWorktreePath
})
return args.stillCurrent() && instanceMatches() ? 'refreshed' : 'stale'
} catch {
return args.stillCurrent() ? 'skipped' : 'stale'
}
}
async reconcileWorktreeBaseStatus(args: {
repoId: string
repoPath: string
worktreeId: string
createdWorktreePath: string
createdInstanceId: string
base: RemoteTrackingBase
branchName: string
createdBaseSha: string
@@ -6176,6 +6243,24 @@ export class OrcaRuntimeService {
return
}
const refreshResult = await this.refreshUntouchedCreatedWorktree({
worktreeId: args.worktreeId,
createdWorktreePath: args.createdWorktreePath,
createdInstanceId: args.createdInstanceId,
branchName: args.branchName,
createdBaseSha: args.createdBaseSha,
postFetchSha,
stillCurrent
})
if (refreshResult === 'stale') {
return
}
if (refreshResult === 'refreshed') {
emit({ status: 'current' })
await checkPublishRemoteConflict()
return
}
const { stdout: countStdout } = await gitExecFileAsync(
['rev-list', '--count', `${args.createdBaseSha}..${postFetchSha}`],
{ cwd: args.repoPath }