Handle clean local merges when hosting provider reports conflicts (#5942)
When the hosting provider (GitHub) reports conflicts but a local merge simulation is clean, we now mark the conflict summary as locally clean. This state is surfaced in both the desktop and mobile sidebars with a clear explanation and a copyable set of commands to trigger a remote mergeability recalculation via an empty commit and push.
This commit is contained in:
parent
8a4f10b35c
commit
16347ab799
|
|
@ -91,6 +91,8 @@ function createMockAgent(index: number, now: number): RuntimeWorktreeAgentRow {
|
|||
state: index % 12 === 0 ? 'waiting' : 'working',
|
||||
agentType: index % 3 === 0 ? 'claude' : 'codex',
|
||||
prompt: `Investigate mobile lag scenario ${index + 1}`,
|
||||
taskTitle: null,
|
||||
displayName: null,
|
||||
lastAssistantMessage: index % 6 === 0 ? 'Running focused checks' : null,
|
||||
toolName: null,
|
||||
toolInput: null,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { useEffect, useRef, useState } from 'react'
|
||||
import { ActivityIndicator, Pressable, ScrollView, Text, View } from 'react-native'
|
||||
import { FileWarning, Sparkles } from 'lucide-react-native'
|
||||
import * as Clipboard from 'expo-clipboard'
|
||||
import { Check, Copy, FileWarning, Sparkles } from 'lucide-react-native'
|
||||
import { colors } from '../../theme/mobile-theme'
|
||||
import type { PRInfo } from '../../../../src/shared/types'
|
||||
import { PRSection } from './PRSection'
|
||||
|
|
@ -27,10 +29,47 @@ type Props = {
|
|||
// list is not yet available. Ports the desktop ConflictingFilesSection +
|
||||
// MergeConflictNotice into the mobile card shell.
|
||||
export function PRConflictingFilesSection({ pr, isRefreshing = false, triage }: Props) {
|
||||
const [commandsCopied, setCommandsCopied] = useState(false)
|
||||
const copiedResetTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const conflict = resolveConflictDisplay(pr)
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (copiedResetTimerRef.current) {
|
||||
clearTimeout(copiedResetTimerRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!conflict) {
|
||||
return null
|
||||
}
|
||||
let noticeBody = 'Conflict file details are unavailable'
|
||||
if (isRefreshing) {
|
||||
noticeBody = 'Refreshing conflict details…'
|
||||
} else if (conflict.localMergeClean) {
|
||||
noticeBody =
|
||||
'GitHub reports conflicts, but local Git did not reproduce them. Refresh the PR or push the branch to recalculate mergeability.'
|
||||
}
|
||||
|
||||
const copyRefreshCommands = async () => {
|
||||
if (!conflict.mergeabilityRefreshCommands) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await Clipboard.setStringAsync(conflict.mergeabilityRefreshCommands)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (copiedResetTimerRef.current) {
|
||||
clearTimeout(copiedResetTimerRef.current)
|
||||
}
|
||||
setCommandsCopied(true)
|
||||
copiedResetTimerRef.current = setTimeout(() => {
|
||||
copiedResetTimerRef.current = null
|
||||
setCommandsCopied(false)
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
return (
|
||||
<PRSection title="Conflicts">
|
||||
|
|
@ -44,11 +83,35 @@ export function PRConflictingFilesSection({ pr, isRefreshing = false, triage }:
|
|||
{conflict.fileDetailsUnavailable ? (
|
||||
<View>
|
||||
<Text style={styles.noticeTitle}>This branch has conflicts that must be resolved</Text>
|
||||
<Text style={styles.noticeBody}>
|
||||
{isRefreshing
|
||||
? 'Refreshing conflict details…'
|
||||
: 'Conflict file details are unavailable'}
|
||||
</Text>
|
||||
<Text style={styles.noticeBody}>{noticeBody}</Text>
|
||||
{conflict.mergeabilityRefreshCommands ? (
|
||||
<View style={styles.commandBox}>
|
||||
<View style={styles.commandHeader}>
|
||||
<Text style={styles.commandLabel}>Run from this worktree</Text>
|
||||
<Pressable
|
||||
style={({ pressed }) => [
|
||||
styles.copyCommandButton,
|
||||
pressed && styles.copyCommandButtonPressed
|
||||
]}
|
||||
onPress={() => void copyRefreshCommands()}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Copy mergeability refresh commands"
|
||||
>
|
||||
{commandsCopied ? (
|
||||
<Check size={13} color={colors.textPrimary} strokeWidth={2.2} />
|
||||
) : (
|
||||
<Copy size={13} color={colors.textPrimary} strokeWidth={2.2} />
|
||||
)}
|
||||
<Text style={styles.copyCommandText}>
|
||||
{commandsCopied ? 'Copied' : 'Copy commands'}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<Text selectable style={styles.commandText}>
|
||||
{conflict.mergeabilityRefreshCommands}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
) : (
|
||||
<View>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import type { PRInfo } from '../../../../src/shared/types'
|
||||
import { hasMergeConflicts, resolveConflictDisplay } from './pr-conflict-presentation'
|
||||
import {
|
||||
buildMergeabilityRefreshCommands,
|
||||
hasMergeConflicts,
|
||||
resolveConflictDisplay
|
||||
} from './pr-conflict-presentation'
|
||||
|
||||
function pr(over: Partial<PRInfo>): PRInfo {
|
||||
return {
|
||||
|
|
@ -24,6 +28,16 @@ describe('hasMergeConflicts', () => {
|
|||
})
|
||||
|
||||
describe('resolveConflictDisplay', () => {
|
||||
it('builds safe mergeability refresh commands', () => {
|
||||
expect(buildMergeabilityRefreshCommands()).toBe(
|
||||
[
|
||||
'git fetch origin',
|
||||
'git commit --allow-empty --only -m "chore: refresh PR mergeability"',
|
||||
'git push'
|
||||
].join('\n')
|
||||
)
|
||||
})
|
||||
|
||||
it('returns null when there are no conflicts', () => {
|
||||
expect(resolveConflictDisplay(pr({ mergeable: 'MERGEABLE' }))).toBeNull()
|
||||
expect(resolveConflictDisplay(pr({ mergeable: 'UNKNOWN' }))).toBeNull()
|
||||
|
|
@ -45,7 +59,9 @@ describe('resolveConflictDisplay', () => {
|
|||
files: ['src/a.ts', 'src/b.ts'],
|
||||
commitsBehind: 3,
|
||||
baseCommit: 'abc1234',
|
||||
fileDetailsUnavailable: false
|
||||
fileDetailsUnavailable: false,
|
||||
localMergeClean: false,
|
||||
mergeabilityRefreshCommands: null
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -55,7 +71,9 @@ describe('resolveConflictDisplay', () => {
|
|||
files: [],
|
||||
commitsBehind: null,
|
||||
baseCommit: null,
|
||||
fileDetailsUnavailable: true
|
||||
fileDetailsUnavailable: true,
|
||||
localMergeClean: false,
|
||||
mergeabilityRefreshCommands: null
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -68,4 +86,41 @@ describe('resolveConflictDisplay', () => {
|
|||
)
|
||||
expect(display?.fileDetailsUnavailable).toBe(true)
|
||||
})
|
||||
|
||||
it('flags locally clean when GitHub reports a conflict that local git does not reproduce', () => {
|
||||
const display = resolveConflictDisplay(
|
||||
pr({
|
||||
mergeable: 'CONFLICTING',
|
||||
conflictSummary: {
|
||||
baseRef: 'main',
|
||||
baseCommit: 'x',
|
||||
commitsBehind: 1,
|
||||
files: [],
|
||||
localMergeState: 'clean'
|
||||
}
|
||||
})
|
||||
)
|
||||
expect(display?.localMergeClean).toBe(true)
|
||||
expect(display?.mergeabilityRefreshCommands).toContain('git fetch origin')
|
||||
expect(display?.mergeabilityRefreshCommands).toContain('git commit --allow-empty --only')
|
||||
})
|
||||
|
||||
it('does not interpolate shell-sensitive base refs into copyable commands', () => {
|
||||
const display = resolveConflictDisplay(
|
||||
pr({
|
||||
mergeable: 'CONFLICTING',
|
||||
conflictSummary: {
|
||||
baseRef: 'release/$USER;echo unsafe',
|
||||
baseCommit: 'x',
|
||||
commitsBehind: 1,
|
||||
files: [],
|
||||
localMergeState: 'clean'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
expect(display?.mergeabilityRefreshCommands).toContain('git fetch origin')
|
||||
expect(display?.mergeabilityRefreshCommands).not.toContain('$USER')
|
||||
expect(display?.mergeabilityRefreshCommands).not.toContain('echo unsafe')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ export type ConflictDisplay = {
|
|||
// True when conflicts exist but no file list is available — desktop shows a
|
||||
// fallback notice instead of the file list in this case.
|
||||
fileDetailsUnavailable: boolean
|
||||
// True when the host reports conflicts but the local merge simulation is clean.
|
||||
localMergeClean: boolean
|
||||
mergeabilityRefreshCommands: string | null
|
||||
}
|
||||
|
||||
// Conflicts exist only when the host reports CONFLICTING. Anything else (MERGEABLE
|
||||
|
|
@ -22,6 +25,14 @@ export function hasMergeConflicts(pr: Pick<PRInfo, 'mergeable'>): boolean {
|
|||
return pr.mergeable === 'CONFLICTING'
|
||||
}
|
||||
|
||||
export function buildMergeabilityRefreshCommands(): string {
|
||||
return [
|
||||
'git fetch origin',
|
||||
'git commit --allow-empty --only -m "chore: refresh PR mergeability"',
|
||||
'git push'
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
// Resolve the conflict view-model, or null when there is nothing to show. Returns
|
||||
// a model both when files are listed AND when conflicts exist without a file list
|
||||
// (so the section can render the fallback notice, matching desktop).
|
||||
|
|
@ -32,10 +43,13 @@ export function resolveConflictDisplay(
|
|||
return null
|
||||
}
|
||||
const files = pr.conflictSummary?.files ?? []
|
||||
const localMergeClean = pr.conflictSummary?.localMergeState === 'clean'
|
||||
return {
|
||||
files,
|
||||
commitsBehind: pr.conflictSummary?.commitsBehind ?? null,
|
||||
baseCommit: pr.conflictSummary?.baseCommit ?? null,
|
||||
fileDetailsUnavailable: files.length === 0
|
||||
fileDetailsUnavailable: files.length === 0,
|
||||
localMergeClean,
|
||||
mergeabilityRefreshCommands: localMergeClean ? buildMergeabilityRefreshCommands() : null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,5 +56,49 @@ export const prConflictStyles = StyleSheet.create({
|
|||
color: colors.textSecondary,
|
||||
fontSize: 11,
|
||||
marginTop: spacing.xs
|
||||
},
|
||||
commandBox: {
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.borderSubtle,
|
||||
backgroundColor: colors.bgRaised,
|
||||
borderRadius: radii.button,
|
||||
marginTop: spacing.sm,
|
||||
padding: spacing.sm
|
||||
},
|
||||
commandHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: spacing.sm
|
||||
},
|
||||
commandLabel: {
|
||||
color: colors.textSecondary,
|
||||
fontSize: 10,
|
||||
fontWeight: '600'
|
||||
},
|
||||
copyCommandButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.xs,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.borderSubtle,
|
||||
borderRadius: radii.button,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: spacing.xs
|
||||
},
|
||||
copyCommandButtonPressed: {
|
||||
backgroundColor: colors.borderSubtle
|
||||
},
|
||||
copyCommandText: {
|
||||
color: colors.textPrimary,
|
||||
fontSize: 11,
|
||||
fontWeight: '600'
|
||||
},
|
||||
commandText: {
|
||||
color: colors.textPrimary,
|
||||
fontFamily: typography.monoFamily,
|
||||
fontSize: 10,
|
||||
lineHeight: 15,
|
||||
marginTop: spacing.sm
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1632,6 +1632,42 @@ describe('getPRForBranch', () => {
|
|||
expect(pr?.conflictSummary).toBeUndefined()
|
||||
})
|
||||
|
||||
it('marks the conflict summary as locally clean when GitHub reports dirty but merge-tree has no conflicted files', async () => {
|
||||
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
|
||||
ghExecFileAsyncMock.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify([
|
||||
{
|
||||
number: 42,
|
||||
title: 'Fix PR discovery',
|
||||
state: 'open',
|
||||
html_url: 'https://github.com/acme/widgets/pull/42',
|
||||
updated_at: '2026-06-20T22:16:43Z',
|
||||
draft: false,
|
||||
mergeable_state: 'dirty',
|
||||
base: { ref: 'main', sha: 'base-oid' },
|
||||
head: { ref: 'feature/test', sha: 'head-oid' }
|
||||
}
|
||||
])
|
||||
})
|
||||
gitExecFileAsyncMock
|
||||
.mockResolvedValueOnce({ stdout: '' })
|
||||
.mockResolvedValueOnce({ stdout: 'latest-base-oid\n' })
|
||||
.mockResolvedValueOnce({ stdout: 'merge-base-oid\n' })
|
||||
.mockResolvedValueOnce({ stdout: '1\n' })
|
||||
.mockResolvedValueOnce({ stdout: 'result-tree-oid\u0000' })
|
||||
|
||||
const pr = await getPRForBranch('/repo-root', 'feature/test')
|
||||
|
||||
expect(pr?.mergeable).toBe('CONFLICTING')
|
||||
expect(pr?.conflictSummary).toEqual({
|
||||
baseRef: 'main',
|
||||
baseCommit: 'latest-',
|
||||
commitsBehind: 1,
|
||||
files: [],
|
||||
localMergeState: 'clean'
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to GitHub baseRefOid when fetching or resolving the base ref fails', async () => {
|
||||
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
|
||||
ghExecFileAsyncMock.mockResolvedValueOnce({
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ export async function getPRConflictSummary(
|
|||
baseRef: baseRefName,
|
||||
baseCommit: latestBaseOid.slice(0, 7),
|
||||
commitsBehind,
|
||||
files
|
||||
files,
|
||||
...(files.length === 0 ? { localMergeState: 'clean' as const } : {})
|
||||
}
|
||||
} catch {
|
||||
return undefined
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest'
|
|||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import type { PRCheckDetail, PRComment, PRInfo } from '../../../../shared/types'
|
||||
import {
|
||||
buildMergeabilityRecalculationCommands,
|
||||
CheckJobLogTail,
|
||||
ChecksList,
|
||||
ConflictTriageStrip,
|
||||
|
|
@ -41,6 +42,16 @@ function renderNotice(pr: PRInfo, isRefreshingConflictDetails = false): string {
|
|||
}
|
||||
|
||||
describe('MergeConflictNotice', () => {
|
||||
it('builds safe mergeability recalculation commands', () => {
|
||||
expect(buildMergeabilityRecalculationCommands()).toBe(
|
||||
[
|
||||
'git fetch origin',
|
||||
'git commit --allow-empty --only -m "chore: refresh PR mergeability"',
|
||||
'git push'
|
||||
].join('\n')
|
||||
)
|
||||
})
|
||||
|
||||
it('does not claim conflict details are refreshing after the refresh has settled', () => {
|
||||
const markup = renderNotice(makePR())
|
||||
|
||||
|
|
@ -54,6 +65,47 @@ describe('MergeConflictNotice', () => {
|
|||
expect(markup).toContain('Refreshing conflict details')
|
||||
})
|
||||
|
||||
it('explains when the hosting provider reports conflicts but local git simulates a clean merge', () => {
|
||||
const markup = renderNotice(
|
||||
makePR({
|
||||
conflictSummary: {
|
||||
baseRef: 'main',
|
||||
baseCommit: 'abc1234',
|
||||
commitsBehind: 1,
|
||||
files: [],
|
||||
localMergeState: 'clean'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
expect(markup).toContain('local Git did not reproduce them')
|
||||
expect(markup).toContain('Run from this worktree')
|
||||
expect(markup).toContain('hosting provider reports conflicts')
|
||||
expect(markup).toContain('git fetch origin')
|
||||
expect(markup).toContain('git commit --allow-empty --only')
|
||||
expect(markup).toContain('git push')
|
||||
expect(markup).toContain('Copy commands')
|
||||
expect(markup).not.toContain('Conflict file details are unavailable')
|
||||
})
|
||||
|
||||
it('does not interpolate shell-sensitive base refs into copyable commands', () => {
|
||||
const markup = renderNotice(
|
||||
makePR({
|
||||
conflictSummary: {
|
||||
baseRef: 'release/$USER;echo unsafe',
|
||||
baseCommit: 'abc1234',
|
||||
commitsBehind: 1,
|
||||
files: [],
|
||||
localMergeState: 'clean'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
expect(markup).toContain('git fetch origin')
|
||||
expect(markup).not.toContain('$USER')
|
||||
expect(markup).not.toContain('echo unsafe')
|
||||
})
|
||||
|
||||
it('hides when the conflicting file list is available', () => {
|
||||
const markup = renderNotice(
|
||||
makePR({
|
||||
|
|
|
|||
|
|
@ -108,6 +108,14 @@ type ConflictReview = {
|
|||
conflictSummary?: PRConflictSummary
|
||||
}
|
||||
|
||||
export function buildMergeabilityRecalculationCommands(): string {
|
||||
return [
|
||||
'git fetch origin',
|
||||
'git commit --allow-empty --only -m "chore: refresh PR mergeability"',
|
||||
'git push'
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export function ConflictingFilesSection({ pr }: { pr: ConflictReview }): React.JSX.Element | null {
|
||||
const files = pr.conflictSummary?.files ?? []
|
||||
if (pr.mergeable !== 'CONFLICTING' || files.length === 0) {
|
||||
|
|
@ -164,6 +172,23 @@ export function MergeConflictNotice({
|
|||
if (pr.mergeable !== 'CONFLICTING' || (pr.conflictSummary?.files.length ?? 0) > 0) {
|
||||
return null
|
||||
}
|
||||
const locallyClean = pr.conflictSummary?.localMergeState === 'clean'
|
||||
let noticeBody = translate(
|
||||
'auto.components.right.sidebar.checks.panel.content.ae8a04ef17',
|
||||
'Conflict file details are unavailable'
|
||||
)
|
||||
if (isRefreshingConflictDetails) {
|
||||
noticeBody = translate(
|
||||
'auto.components.right.sidebar.checks.panel.content.73d0675356',
|
||||
'Refreshing conflict details…'
|
||||
)
|
||||
} else if (locallyClean) {
|
||||
noticeBody = translate(
|
||||
'auto.components.right.sidebar.checks.panel.content.f5bc5c4cf1',
|
||||
'The hosting provider reports conflicts, but local Git did not reproduce them. Refresh the review or push the branch to recalculate mergeability.'
|
||||
)
|
||||
}
|
||||
const refreshCommands = locallyClean ? buildMergeabilityRecalculationCommands() : null
|
||||
|
||||
return (
|
||||
<div className="border-t border-border px-3 py-3">
|
||||
|
|
@ -173,17 +198,89 @@ export function MergeConflictNotice({
|
|||
'This branch has conflicts that must be resolved'
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] text-muted-foreground">
|
||||
{isRefreshingConflictDetails
|
||||
? translate(
|
||||
'auto.components.right.sidebar.checks.panel.content.73d0675356',
|
||||
'Refreshing conflict details…'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.right.sidebar.checks.panel.content.ae8a04ef17',
|
||||
'Conflict file details are unavailable'
|
||||
)}
|
||||
<div className="mt-1 text-[11px] text-muted-foreground">{noticeBody}</div>
|
||||
{refreshCommands ? <MergeabilityRecalculationCommandBox commands={refreshCommands} /> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MergeabilityRecalculationCommandBox({
|
||||
commands
|
||||
}: {
|
||||
commands: string
|
||||
}): React.JSX.Element {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const copiedResetTimerRef = useRef<number | null>(null)
|
||||
const isMountedRef = useRef(false)
|
||||
|
||||
const clearCopiedResetTimer = useCallback((): void => {
|
||||
if (copiedResetTimerRef.current !== null) {
|
||||
window.clearTimeout(copiedResetTimerRef.current)
|
||||
copiedResetTimerRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
const setCopyButtonRef = useCallback(
|
||||
(node: HTMLButtonElement | null) => {
|
||||
isMountedRef.current = node !== null
|
||||
if (node === null) {
|
||||
clearCopiedResetTimer()
|
||||
}
|
||||
},
|
||||
[clearCopiedResetTimer]
|
||||
)
|
||||
|
||||
const copyCommands = useCallback((): void => {
|
||||
void window.api.ui
|
||||
.writeClipboardText(commands)
|
||||
.then(() => {
|
||||
if (!isMountedRef.current) {
|
||||
return
|
||||
}
|
||||
clearCopiedResetTimer()
|
||||
setCopied(true)
|
||||
copiedResetTimerRef.current = window.setTimeout(() => {
|
||||
copiedResetTimerRef.current = null
|
||||
setCopied(false)
|
||||
}, 1500)
|
||||
})
|
||||
.catch(() => {
|
||||
/* best-effort */
|
||||
})
|
||||
}, [clearCopiedResetTimer, commands])
|
||||
|
||||
return (
|
||||
<div className="mt-3 rounded-md border border-border bg-accent/20 p-2.5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="text-[10px] font-medium text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.right.sidebar.checks.panel.content.5bc9bda2af',
|
||||
'Run from this worktree'
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
ref={setCopyButtonRef}
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={copyCommands}
|
||||
aria-label={translate(
|
||||
'auto.components.right.sidebar.checks.panel.content.e87fb3d929',
|
||||
'Copy mergeability refresh commands'
|
||||
)}
|
||||
>
|
||||
{copied ? <Check className="size-3" /> : <Copy className="size-3" />}
|
||||
{copied
|
||||
? translate('auto.components.right.sidebar.checks.panel.content.1e53e45072', 'Copied')
|
||||
: translate(
|
||||
'auto.components.right.sidebar.checks.panel.content.084c516efb',
|
||||
'Copy commands'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<pre className="scrollbar-sleek mt-2 max-h-28 overflow-auto whitespace-pre-wrap break-all rounded-md border border-border bg-background px-2 py-1.5 font-mono text-[10px] leading-4 text-foreground">
|
||||
{commands}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8795,6 +8795,11 @@
|
|||
"066fedd446": "Failed jobs",
|
||||
"ae8a04ef17": "Conflict file details are unavailable",
|
||||
"73d0675356": "Refreshing conflict details…",
|
||||
"f5bc5c4cf1": "The hosting provider reports conflicts, but local Git did not reproduce them. Refresh the review or push the branch to recalculate mergeability.",
|
||||
"5bc9bda2af": "Run from this worktree",
|
||||
"e87fb3d929": "Copy mergeability refresh commands",
|
||||
"1e53e45072": "Copied",
|
||||
"084c516efb": "Copy commands",
|
||||
"5dc3af25c0": "Select comment",
|
||||
"d7a2f9c401": "Send unresolved {{value0}} comments",
|
||||
"d91f2a6c39": "Send {{value0}} queued comments",
|
||||
|
|
|
|||
|
|
@ -2307,7 +2307,7 @@
|
|||
"TerminalContextMenu": {
|
||||
"b4cdd9314e": "Borrar pantalla",
|
||||
"8c17d6786d": "Cerrar panel",
|
||||
"copyTerminalId": "Copiar ID de terminal",
|
||||
"copyTerminalId": "Copy Terminal ID",
|
||||
"2cf85a6a55": "Copiar ID del panel",
|
||||
"39809d152f": "Establecer título…",
|
||||
"06c2b0f043": "Igualar tamaños de paneles",
|
||||
|
|
@ -8794,6 +8794,11 @@
|
|||
"066fedd446": "Trabajos fallidos",
|
||||
"ae8a04ef17": "Los detalles del archivo de conflicto no están disponibles",
|
||||
"73d0675356": "Detalles refrescantes del conflicto...",
|
||||
"f5bc5c4cf1": "The hosting provider reports conflicts, but local Git did not reproduce them. Refresh the review or push the branch to recalculate mergeability.",
|
||||
"5bc9bda2af": "Run from this worktree",
|
||||
"e87fb3d929": "Copy mergeability refresh commands",
|
||||
"1e53e45072": "Copied",
|
||||
"084c516efb": "Copy commands",
|
||||
"5dc3af25c0": "Seleccionar comentario",
|
||||
"d7a2f9c401": "Send unresolved {{value0}} comments",
|
||||
"d91f2a6c39": "Enviar {{value0}} comentarios en cola",
|
||||
|
|
|
|||
|
|
@ -8794,6 +8794,11 @@
|
|||
"066fedd446": "失敗したジョブ",
|
||||
"ae8a04ef17": "競合ファイルの詳細は利用できません",
|
||||
"73d0675356": "競合の詳細を更新しています…",
|
||||
"f5bc5c4cf1": "The hosting provider reports conflicts, but local Git did not reproduce them. Refresh the review or push the branch to recalculate mergeability.",
|
||||
"5bc9bda2af": "Run from this worktree",
|
||||
"e87fb3d929": "Copy mergeability refresh commands",
|
||||
"1e53e45072": "Copied",
|
||||
"084c516efb": "Copy commands",
|
||||
"5dc3af25c0": "コメントを選択",
|
||||
"d7a2f9c401": "Send unresolved {{value0}} comments",
|
||||
"d91f2a6c39": "キュー内の {{value0}} 件のコメントを送信",
|
||||
|
|
|
|||
|
|
@ -8794,6 +8794,11 @@
|
|||
"066fedd446": "실패한 작업",
|
||||
"ae8a04ef17": "충돌 파일 세부정보를 사용할 수 없습니다.",
|
||||
"73d0675356": "충돌 세부정보 새로고침 중…",
|
||||
"f5bc5c4cf1": "The hosting provider reports conflicts, but local Git did not reproduce them. Refresh the review or push the branch to recalculate mergeability.",
|
||||
"5bc9bda2af": "Run from this worktree",
|
||||
"e87fb3d929": "Copy mergeability refresh commands",
|
||||
"1e53e45072": "Copied",
|
||||
"084c516efb": "Copy commands",
|
||||
"5dc3af25c0": "댓글 선택",
|
||||
"d7a2f9c401": "해결되지 않은 댓글 {{value0}}개 보내기",
|
||||
"d91f2a6c39": "대기 중인 댓글 {{value0}}개 보내기",
|
||||
|
|
|
|||
|
|
@ -2307,7 +2307,7 @@
|
|||
"TerminalContextMenu": {
|
||||
"b4cdd9314e": "清晰的屏幕",
|
||||
"8c17d6786d": "关闭窗格",
|
||||
"copyTerminalId": "复制终端 ID",
|
||||
"copyTerminalId": "Copy Terminal ID",
|
||||
"2cf85a6a55": "复制窗格 ID",
|
||||
"39809d152f": "设置标题...",
|
||||
"06c2b0f043": "均衡窗格大小",
|
||||
|
|
@ -8794,6 +8794,11 @@
|
|||
"066fedd446": "失败的工作",
|
||||
"ae8a04ef17": "冲突文件详细信息不可用",
|
||||
"73d0675356": "刷新冲突细节...",
|
||||
"f5bc5c4cf1": "The hosting provider reports conflicts, but local Git did not reproduce them. Refresh the review or push the branch to recalculate mergeability.",
|
||||
"5bc9bda2af": "Run from this worktree",
|
||||
"e87fb3d929": "Copy mergeability refresh commands",
|
||||
"1e53e45072": "Copied",
|
||||
"084c516efb": "Copy commands",
|
||||
"5dc3af25c0": "选择评论",
|
||||
"d7a2f9c401": "Send unresolved {{value0}} comments",
|
||||
"d91f2a6c39": "发送 {{value0}} 条已排队评论",
|
||||
|
|
|
|||
|
|
@ -1067,6 +1067,7 @@ export type PRConflictSummary = {
|
|||
baseCommit: string
|
||||
commitsBehind: number
|
||||
files: string[]
|
||||
localMergeState?: 'clean'
|
||||
}
|
||||
|
||||
export type GitHubRepositoryIdentity = { owner: string; repo: string }
|
||||
|
|
|
|||
Loading…
Reference in New Issue