mirror of
https://github.com/stablyai/orca.git
synced 2026-10-28 08:02:54 +00:00
feat: add source control action to abort merge (#2092)
Co-authored-by: Orca <help@stably.ai> Co-authored-by: brennanb2025 <brennankbenson@gmail.com>
This commit is contained in:
co-authored by
Orca
brennanb2025
parent
a65f7b4216
commit
659fe79226
@@ -333,4 +333,30 @@ describe('ConflictSummaryCard', () => {
|
||||
|
||||
expect(markup.indexOf('Resolve with AI')).toBeLessThan(markup.indexOf('Review conflicts'))
|
||||
})
|
||||
|
||||
it('shows Abort merge only for merge conflicts', () => {
|
||||
const mergeMarkup = renderToStaticMarkup(
|
||||
<ConflictSummaryCard
|
||||
conflictOperation="merge"
|
||||
unresolvedCount={1}
|
||||
isResolvingWithAI={false}
|
||||
onAbortMerge={vi.fn()}
|
||||
onResolveWithAI={vi.fn()}
|
||||
onReview={vi.fn()}
|
||||
/>
|
||||
)
|
||||
const rebaseMarkup = renderToStaticMarkup(
|
||||
<ConflictSummaryCard
|
||||
conflictOperation="rebase"
|
||||
unresolvedCount={1}
|
||||
isResolvingWithAI={false}
|
||||
onAbortMerge={vi.fn()}
|
||||
onResolveWithAI={vi.fn()}
|
||||
onReview={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(mergeMarkup).toContain('Abort merge')
|
||||
expect(rebaseMarkup).not.toContain('Abort merge')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -101,6 +101,7 @@ import {
|
||||
DialogTitle
|
||||
} from '@/components/ui/dialog'
|
||||
import { BaseRefPicker } from '@/components/settings/BaseRefPicker'
|
||||
import { useConfirmationDialog } from '@/components/confirmation-dialog'
|
||||
import { formatDiffComment, formatDiffComments } from '@/lib/diff-comments-format'
|
||||
import { getDiffCommentLineLabel, getDiffCommentSource } from '@/lib/diff-comment-compat'
|
||||
import { focusTerminalTabSurface } from '@/lib/focus-terminal-tab-surface'
|
||||
@@ -115,6 +116,7 @@ import {
|
||||
} from '@/components/editor/editor-autosave'
|
||||
import { getConnectionId } from '@/lib/connection-context'
|
||||
import {
|
||||
abortRuntimeGitMerge,
|
||||
bulkDiscardRuntimeGitPaths,
|
||||
bulkStageRuntimeGitPaths,
|
||||
bulkUnstageRuntimeGitPaths,
|
||||
@@ -160,7 +162,7 @@ import {
|
||||
import { hasExpandedCommitFailureDetails, summarizeCommitFailure } from './commit-failure-summary'
|
||||
|
||||
export type SourceControlScope = 'all' | 'uncommitted'
|
||||
type RemoteActionError = { kind: RemoteOpKind; message: string }
|
||||
type SourceControlActionError = { kind: RemoteOpKind | 'abort_merge'; message: string }
|
||||
|
||||
const EMPTY_GIT_STATUS_ENTRIES: GitStatusEntry[] = []
|
||||
const EMPTY_BRANCH_CHANGE_ENTRIES: GitBranchChangeEntry[] = []
|
||||
@@ -1119,7 +1121,7 @@ function SourceControlInner(): React.JSX.Element {
|
||||
const [commitDrafts, setCommitDrafts] = useState<CommitDraftsByWorktree>({})
|
||||
const [commitErrors, setCommitErrors] = useState<Record<string, string | null>>({})
|
||||
const [remoteActionErrors, setRemoteActionErrors] = useState<
|
||||
Record<string, RemoteActionError | null>
|
||||
Record<string, SourceControlActionError | null>
|
||||
>({})
|
||||
// Why: keep commit-in-flight state per-worktree. A single boolean would be
|
||||
// cleared when the user switched worktrees, letting them double-click Commit
|
||||
@@ -1128,6 +1130,11 @@ function SourceControlInner(): React.JSX.Element {
|
||||
const [commitInFlightByWorktree, setCommitInFlightByWorktree] = useState<Record<string, boolean>>(
|
||||
{}
|
||||
)
|
||||
const [abortMergeInFlightByWorktree, setAbortMergeInFlightByWorktree] = useState<
|
||||
Record<string, boolean>
|
||||
>({})
|
||||
const isAbortingMerge = abortMergeInFlightByWorktree[activeWorktreeId ?? ''] ?? false
|
||||
const confirmAction = useConfirmationDialog()
|
||||
const isCommitting = commitInFlightByWorktree[activeWorktreeId ?? ''] ?? false
|
||||
// Why: parallel state to commit. Same per-worktree shape so navigating between
|
||||
// worktrees mid-generation never silently cancels the in-flight request.
|
||||
@@ -1677,6 +1684,7 @@ function SourceControlInner(): React.JSX.Element {
|
||||
setCommitErrors((prev) => pruneRecord(prev))
|
||||
setRemoteActionErrors((prev) => pruneRecord(prev))
|
||||
setCommitInFlightByWorktree((prev) => pruneRecord(prev))
|
||||
setAbortMergeInFlightByWorktree((prev) => pruneRecord(prev))
|
||||
setGenerateInFlightByWorktree((prev) => pruneRecord(prev))
|
||||
setGenerateErrors((prev) => pruneRecord(prev))
|
||||
setGitHistoryByWorktree((prev) => pruneRecord(prev))
|
||||
@@ -2013,6 +2021,53 @@ function SourceControlInner(): React.JSX.Element {
|
||||
]
|
||||
)
|
||||
|
||||
const handleAbortMerge = useCallback(async (): Promise<void> => {
|
||||
if (!activeWorktreeId || !worktreePath || conflictOperation !== 'merge' || isAbortingMerge) {
|
||||
return
|
||||
}
|
||||
|
||||
const confirmed = await confirmAction({
|
||||
title: 'Abort merge?',
|
||||
description:
|
||||
'This cancels the merge in progress and can discard conflict resolutions made during this merge.',
|
||||
confirmLabel: 'Abort merge',
|
||||
confirmVariant: 'destructive'
|
||||
})
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
const connectionId = getConnectionId(activeWorktreeId) ?? undefined
|
||||
setAbortMergeInFlightByWorktree((prev) => ({ ...prev, [activeWorktreeId]: true }))
|
||||
setRemoteActionErrors((prev) => ({ ...prev, [activeWorktreeId]: null }))
|
||||
try {
|
||||
await abortRuntimeGitMerge({
|
||||
settings: useAppStore.getState().settings,
|
||||
worktreeId: activeWorktreeId,
|
||||
worktreePath,
|
||||
connectionId
|
||||
})
|
||||
await refreshActiveGitStatusAfterMutation()
|
||||
void refreshGitHistoryRef.current()
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to abort merge'
|
||||
toast.error('Abort merge failed', { description: message })
|
||||
setRemoteActionErrors((prev) => ({
|
||||
...prev,
|
||||
[activeWorktreeId]: { kind: 'abort_merge', message }
|
||||
}))
|
||||
} finally {
|
||||
setAbortMergeInFlightByWorktree((prev) => ({ ...prev, [activeWorktreeId]: false }))
|
||||
}
|
||||
}, [
|
||||
activeWorktreeId,
|
||||
confirmAction,
|
||||
conflictOperation,
|
||||
isAbortingMerge,
|
||||
refreshActiveGitStatusAfterMutation,
|
||||
worktreePath
|
||||
])
|
||||
|
||||
// Why: compound actions must commit first and only run the follow-up remote
|
||||
// op when the commit succeeds. handleCommit's return value carries that
|
||||
// signal — a failure leaves commitError populated and short-circuits here
|
||||
@@ -2557,7 +2612,7 @@ function SourceControlInner(): React.JSX.Element {
|
||||
hasMessage: commitMessage.trim().length > 0,
|
||||
hasUnresolvedConflicts: unresolvedConflicts.length > 0,
|
||||
isCommitting,
|
||||
isRemoteOperationActive,
|
||||
isRemoteOperationActive: isRemoteOperationActive || isAbortingMerge,
|
||||
upstreamStatus: remoteStatus,
|
||||
prState: hostedReview?.state ?? null,
|
||||
isPRStateLoading: isHostedReviewStateLoading,
|
||||
@@ -2575,6 +2630,7 @@ function SourceControlInner(): React.JSX.Element {
|
||||
hasUnstagedChanges,
|
||||
hasPartiallyStagedChanges,
|
||||
isCommitting,
|
||||
isAbortingMerge,
|
||||
isRemoteOperationActive,
|
||||
inFlightRemoteOpKind,
|
||||
hostedReviewCreation,
|
||||
@@ -2596,7 +2652,8 @@ function SourceControlInner(): React.JSX.Element {
|
||||
hasMessage: commitMessage.trim().length > 0,
|
||||
hasUnresolvedConflicts: unresolvedConflicts.length > 0,
|
||||
isCommitting,
|
||||
isRemoteOperationActive,
|
||||
isRemoteOperationActive: isRemoteOperationActive || isAbortingMerge,
|
||||
conflictOperation,
|
||||
upstreamStatus: remoteStatus,
|
||||
prState: hostedReview?.state ?? null,
|
||||
isPRStateLoading: isHostedReviewStateLoading,
|
||||
@@ -2613,6 +2670,8 @@ function SourceControlInner(): React.JSX.Element {
|
||||
hasUnstagedChanges,
|
||||
hasPartiallyStagedChanges,
|
||||
isCommitting,
|
||||
conflictOperation,
|
||||
isAbortingMerge,
|
||||
isRemoteOperationActive,
|
||||
inFlightRemoteOpKind,
|
||||
hostedReviewCreation,
|
||||
@@ -2647,6 +2706,9 @@ function SourceControlInner(): React.JSX.Element {
|
||||
case 'commit_sync':
|
||||
void runCompoundCommitAction('sync')
|
||||
return
|
||||
case 'abort_merge':
|
||||
void handleAbortMerge()
|
||||
return
|
||||
case 'create_pr':
|
||||
void handleCreatePullRequest()
|
||||
return
|
||||
@@ -2673,6 +2735,7 @@ function SourceControlInner(): React.JSX.Element {
|
||||
[
|
||||
handleCommit,
|
||||
handleCreatePullRequest,
|
||||
handleAbortMerge,
|
||||
isCreatingPr,
|
||||
prGenerating,
|
||||
runCompoundCommitAction,
|
||||
@@ -3926,6 +3989,8 @@ function SourceControlInner(): React.JSX.Element {
|
||||
conflictOperation={conflictOperation}
|
||||
unresolvedCount={unresolvedConflictReviewEntries.length}
|
||||
isResolvingWithAI={isLaunchingConflictAgent}
|
||||
isAbortingMerge={isAbortingMerge}
|
||||
onAbortMerge={() => void handleAbortMerge()}
|
||||
onResolveWithAI={() => {
|
||||
void handleResolveConflictsWithAI()
|
||||
}}
|
||||
@@ -3949,7 +4014,11 @@ function SourceControlInner(): React.JSX.Element {
|
||||
ConflictSummaryCard handles the "has conflicts" case above. */}
|
||||
{unresolvedConflictReviewEntries.length === 0 && conflictOperation !== 'unknown' && (
|
||||
<div className="px-3 pb-2">
|
||||
<OperationBanner conflictOperation={conflictOperation} />
|
||||
<OperationBanner
|
||||
conflictOperation={conflictOperation}
|
||||
isAbortingMerge={isAbortingMerge}
|
||||
onAbortMerge={() => void handleAbortMerge()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4042,7 +4111,7 @@ function SourceControlInner(): React.JSX.Element {
|
||||
generateError={generateError}
|
||||
stagedCount={grouped.staged.length}
|
||||
hasUnresolvedConflicts={unresolvedConflicts.length > 0}
|
||||
isRemoteOperationActive={isRemoteOperationActive}
|
||||
isRemoteOperationActive={isRemoteOperationActive || isAbortingMerge}
|
||||
inFlightRemoteOpKind={inFlightRemoteOpKind}
|
||||
primaryAction={primaryAction}
|
||||
dropdownItems={dropdownItems}
|
||||
@@ -4763,6 +4832,7 @@ function PullRequestComposer({
|
||||
key={entry.kind}
|
||||
disabled={entry.disabled}
|
||||
title={entry.title}
|
||||
variant={entry.variant}
|
||||
onSelect={(event) => {
|
||||
if (entry.disabled) {
|
||||
event.preventDefault()
|
||||
@@ -5205,6 +5275,7 @@ export function CommitArea({
|
||||
<DropdownMenuItem
|
||||
disabled={entry.disabled}
|
||||
title={entry.title}
|
||||
variant={entry.variant}
|
||||
className="w-full"
|
||||
onSelect={(event) => {
|
||||
if (entry.disabled) {
|
||||
@@ -5692,12 +5763,16 @@ export function ConflictSummaryCard({
|
||||
conflictOperation,
|
||||
unresolvedCount,
|
||||
isResolvingWithAI,
|
||||
isAbortingMerge = false,
|
||||
onAbortMerge,
|
||||
onResolveWithAI,
|
||||
onReview
|
||||
}: {
|
||||
conflictOperation: GitConflictOperation
|
||||
unresolvedCount: number
|
||||
isResolvingWithAI: boolean
|
||||
isAbortingMerge?: boolean
|
||||
onAbortMerge?: () => void
|
||||
onResolveWithAI: () => void
|
||||
onReview: () => void
|
||||
}): React.JSX.Element {
|
||||
@@ -5750,6 +5825,19 @@ export function ConflictSummaryCard({
|
||||
<GitMerge className="size-3.5" />
|
||||
Review conflicts
|
||||
</Button>
|
||||
{conflictOperation === 'merge' && onAbortMerge ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="mt-1.5 h-7 w-full text-xs"
|
||||
disabled={isResolvingWithAI || isAbortingMerge}
|
||||
onClick={onAbortMerge}
|
||||
>
|
||||
{isAbortingMerge ? <RefreshCw className="size-3.5 animate-spin" /> : null}
|
||||
Abort merge
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -5761,9 +5849,13 @@ export function ConflictSummaryCard({
|
||||
// user needs to see the operation state so they know the worktree is mid-rebase
|
||||
// and that they should run `git rebase --continue` or `--abort`.
|
||||
function OperationBanner({
|
||||
conflictOperation
|
||||
conflictOperation,
|
||||
isAbortingMerge = false,
|
||||
onAbortMerge
|
||||
}: {
|
||||
conflictOperation: GitConflictOperation
|
||||
isAbortingMerge?: boolean
|
||||
onAbortMerge?: () => void
|
||||
}): React.JSX.Element {
|
||||
const label =
|
||||
conflictOperation === 'merge'
|
||||
@@ -5782,6 +5874,19 @@ function OperationBanner({
|
||||
<Icon className="size-4 shrink-0 text-amber-600 dark:text-amber-400" />
|
||||
<span className="text-xs font-medium text-foreground">{label}</span>
|
||||
</div>
|
||||
{conflictOperation === 'merge' && onAbortMerge ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="mt-2 h-7 w-full text-xs"
|
||||
disabled={isAbortingMerge}
|
||||
onClick={onAbortMerge}
|
||||
>
|
||||
{isAbortingMerge ? <RefreshCw className="size-3.5 animate-spin" /> : null}
|
||||
Abort merge
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -214,6 +214,46 @@ describe('resolveDropdownItems', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('shows a destructive Abort merge item only while a merge is in progress', () => {
|
||||
const mergeItems = resolveDropdownItems(
|
||||
inputs({
|
||||
conflictOperation: 'merge',
|
||||
upstreamStatus: { hasUpstream: true, ahead: 0, behind: 0 }
|
||||
})
|
||||
)
|
||||
const mergeByKind = Object.fromEntries(
|
||||
mergeItems.filter((e) => e.kind !== 'separator').map((e) => [e.kind, e])
|
||||
)
|
||||
|
||||
expect(mergeByKind.abort_merge).toMatchObject({
|
||||
label: 'Abort merge',
|
||||
title: 'Abort the merge in progress',
|
||||
disabled: false,
|
||||
variant: 'destructive'
|
||||
})
|
||||
|
||||
for (const conflictOperation of ['unknown', 'rebase', 'cherry-pick'] as const) {
|
||||
const items = resolveDropdownItems(inputs({ conflictOperation }))
|
||||
expect(items.some((entry) => entry.kind === 'abort_merge')).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it('disables Abort merge while another action is busy', () => {
|
||||
const items = resolveDropdownItems(
|
||||
inputs({
|
||||
conflictOperation: 'merge',
|
||||
isRemoteOperationActive: true,
|
||||
upstreamStatus: { hasUpstream: true, ahead: 0, behind: 0 }
|
||||
})
|
||||
)
|
||||
const abortMerge = items.find((entry) => entry.kind === 'abort_merge')
|
||||
|
||||
expect(abortMerge).toMatchObject({
|
||||
disabled: true,
|
||||
title: 'Operation in progress…'
|
||||
})
|
||||
})
|
||||
|
||||
it('locks every item while a pull request operation is running', () => {
|
||||
const items = resolveDropdownItems(
|
||||
inputs({
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
// Why: split from source-control-primary-action because the primary and dropdown are independent derivations with different priority ladders; together they exceed the max-lines budget and tangle unrelated concerns.
|
||||
|
||||
import type { PrimaryActionInputs } from './source-control-primary-action'
|
||||
import type { GitConflictOperation } from '../../../../shared/types'
|
||||
import { shouldForcePushWithLeaseForUpstream } from '../../../../shared/git-upstream-status'
|
||||
|
||||
export type DropdownActionInputs = PrimaryActionInputs & {
|
||||
conflictOperation?: GitConflictOperation
|
||||
isPullRequestOperationActive?: boolean
|
||||
rebaseBaseRef?: string | null
|
||||
}
|
||||
@@ -13,6 +15,7 @@ export type DropdownActionKind =
|
||||
| 'commit'
|
||||
| 'commit_push'
|
||||
| 'commit_sync'
|
||||
| 'abort_merge'
|
||||
| 'create_pr'
|
||||
| 'push_create_pr'
|
||||
| 'push'
|
||||
@@ -28,6 +31,7 @@ export type DropdownItem = {
|
||||
title: string
|
||||
disabled: boolean
|
||||
hint?: string
|
||||
variant?: 'default' | 'destructive'
|
||||
}
|
||||
|
||||
export type DropdownSeparator = { kind: 'separator' }
|
||||
@@ -86,6 +90,7 @@ export function resolveDropdownItems(inputs: DropdownActionInputs): DropdownEntr
|
||||
prState,
|
||||
isPRStateLoading,
|
||||
hostedReviewCreation,
|
||||
conflictOperation = 'unknown',
|
||||
branchCommitsAhead,
|
||||
rebaseBaseRef,
|
||||
isPullRequestOperationActive = false
|
||||
@@ -414,6 +419,18 @@ export function resolveDropdownItems(inputs: DropdownActionInputs): DropdownEntr
|
||||
fetchItem,
|
||||
publishItem
|
||||
]
|
||||
if (conflictOperation === 'merge') {
|
||||
entries.push(
|
||||
{ kind: 'separator' },
|
||||
{
|
||||
kind: 'abort_merge',
|
||||
label: 'Abort merge',
|
||||
title: globalBusy ? 'Operation in progress…' : 'Abort the merge in progress',
|
||||
disabled: globalBusy,
|
||||
variant: 'destructive'
|
||||
}
|
||||
)
|
||||
}
|
||||
if (!isPullRequestOperationActive) {
|
||||
return entries
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
createCompatibleRuntimeStatusResponseIfNeeded,
|
||||
type RuntimeEnvironmentCallRequest
|
||||
} from './runtime-compatibility-test-fixture'
|
||||
import { abortRuntimeGitMerge } from './runtime-git-client'
|
||||
import { clearRuntimeCompatibilityCacheForTests } from './runtime-rpc-client'
|
||||
|
||||
const gitAbortMerge = vi.fn()
|
||||
const runtimeEnvironmentCall = vi.fn()
|
||||
const runtimeEnvironmentTransportCall = vi.fn()
|
||||
const runtimeCall = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
clearRuntimeCompatibilityCacheForTests()
|
||||
gitAbortMerge.mockReset()
|
||||
runtimeEnvironmentCall.mockReset()
|
||||
runtimeEnvironmentTransportCall.mockReset()
|
||||
runtimeCall.mockReset()
|
||||
runtimeEnvironmentTransportCall.mockImplementation((args: RuntimeEnvironmentCallRequest) => {
|
||||
return createCompatibleRuntimeStatusResponseIfNeeded(args) ?? runtimeEnvironmentCall(args)
|
||||
})
|
||||
vi.stubGlobal('window', {
|
||||
api: {
|
||||
git: { abortMerge: gitAbortMerge },
|
||||
runtime: { call: runtimeCall },
|
||||
runtimeEnvironments: { call: runtimeEnvironmentTransportCall }
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('runtime git client merge operations', () => {
|
||||
it('uses local git IPC when no remote runtime is active', async () => {
|
||||
gitAbortMerge.mockResolvedValue(undefined)
|
||||
|
||||
await abortRuntimeGitMerge({
|
||||
settings: { activeRuntimeEnvironmentId: null },
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/repo'
|
||||
})
|
||||
|
||||
expect(gitAbortMerge).toHaveBeenCalledWith({ connectionId: undefined, worktreePath: '/repo' })
|
||||
expect(runtimeEnvironmentCall).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('routes abort merge through the active runtime', async () => {
|
||||
runtimeEnvironmentCall.mockResolvedValue({
|
||||
id: 'rpc-1',
|
||||
ok: true,
|
||||
result: { success: true },
|
||||
_meta: { runtimeId: 'remote-runtime' }
|
||||
})
|
||||
|
||||
await abortRuntimeGitMerge({
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' },
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/repo'
|
||||
})
|
||||
|
||||
expect(runtimeEnvironmentCall).toHaveBeenCalledWith({
|
||||
selector: 'env-1',
|
||||
method: 'git.abortMerge',
|
||||
params: { worktree: 'wt-1' },
|
||||
timeoutMs: 30_000
|
||||
})
|
||||
expect(gitAbortMerge).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -167,6 +167,23 @@ export async function getRuntimeGitConflictOperation(
|
||||
)
|
||||
}
|
||||
|
||||
export async function abortRuntimeGitMerge(context: RuntimeGitContext): Promise<void> {
|
||||
const target = getActiveRuntimeTarget(context.settings)
|
||||
if (target.kind === 'local' || !context.worktreeId) {
|
||||
await window.api.git.abortMerge({
|
||||
worktreePath: context.worktreePath,
|
||||
connectionId: context.connectionId
|
||||
})
|
||||
return
|
||||
}
|
||||
await callRuntimeRpc(
|
||||
target,
|
||||
'git.abortMerge',
|
||||
{ worktree: context.worktreeId },
|
||||
{ timeoutMs: 30_000 }
|
||||
)
|
||||
}
|
||||
|
||||
export async function getRuntimeGitDiff(
|
||||
context: RuntimeGitContext,
|
||||
args: { filePath: string; staged: boolean; compareAgainstHead?: boolean }
|
||||
|
||||
@@ -1063,6 +1063,10 @@ function createGitApi(): NonNullable<Partial<PreloadApi>['git']> {
|
||||
const worktree = await resolveRuntimeWorktreeByPath(worktreePath)
|
||||
return callRuntimeResult('git.conflictOperation', { worktree: worktree.id })
|
||||
},
|
||||
abortMerge: async ({ worktreePath }) => {
|
||||
const worktree = await resolveRuntimeWorktreeByPath(worktreePath)
|
||||
await callRuntimeResult('git.abortMerge', { worktree: worktree.id })
|
||||
},
|
||||
diff: async ({ worktreePath, filePath, staged, compareAgainstHead }) => {
|
||||
const file = await resolveRuntimeFilePath(filePath, worktreePath)
|
||||
return callRuntimeResult('git.diff', {
|
||||
|
||||
Reference in New Issue
Block a user