优化代码冲突解决

This commit is contained in:
xintaofei
2026-03-14 21:17:26 +08:00
parent 4129f02985
commit 2b679b5ba8
19 changed files with 165 additions and 15 deletions

View File

@@ -30,6 +30,7 @@ function MergePageInner() {
const folderId = Number(searchParams.get("folderId") ?? "0")
const operation = searchParams.get("operation") ?? "merge"
const upstreamCommit = searchParams.get("upstreamCommit") ?? undefined
const normalizedFolderId = Number.isFinite(folderId) ? folderId : 0
const hasValidFolderId = normalizedFolderId > 0
const loading = hasValidFolderId && state.loadedId !== normalizedFolderId
@@ -104,6 +105,7 @@ function MergePageInner() {
folderId={normalizedFolderId}
folderPath={folder.path}
operation={operation}
upstreamCommit={upstreamCommit}
onCompleted={closeWindow}
onAborted={closeWindow}
/>

View File

@@ -81,6 +81,7 @@ import {
openCommitWindow,
setFolderParentBranch,
gitListConflicts,
gitHasMergeHead,
} from "@/lib/tauri"
import { RemoteManageDialog } from "@/components/layout/remote-manage-dialog"
import { ConflictDialog } from "@/components/layout/conflict-dialog"
@@ -292,6 +293,9 @@ export function BranchDropdown({
})
}
// Uses operation "merge" intentionally: MERGE_HEAD exists so merge state is
// already active. MergeWorkspace won't call gitStartPullMerge (only for "pull"),
// and ConflictDialog abort correctly runs git merge --abort.
async function showMergeConflictDialog() {
try {
const remaining = await gitListConflicts(folderPath)
@@ -310,6 +314,16 @@ export function BranchDropdown({
}
async function handlePush() {
// Pre-check: if MERGE_HEAD exists, show conflict dialog immediately
try {
if (await gitHasMergeHead(folderPath)) {
await showMergeConflictDialog()
return
}
} catch {
// Pre-check failed, continue with normal push flow
}
const taskId = `git-${++taskSeq.current}-${Date.now()}`
const label = t("tasks.pushCode")
setLoading(true)

View File

@@ -44,6 +44,7 @@ export function ConflictDialog({
const [resolvedFiles, setResolvedFiles] = useState<Set<string>>(new Set())
const [aborting, setAborting] = useState(false)
const [completing, setCompleting] = useState(false)
const [done, setDone] = useState(false)
const open = conflictInfo !== null
const operation = conflictInfo?.operation ?? "merge"
@@ -53,6 +54,7 @@ export function ConflictDialog({
if (conflictInfo) {
setConflictedFiles(conflictInfo.conflicted_files)
setResolvedFiles(new Set())
setDone(false)
}
}, [conflictInfo])
@@ -76,6 +78,7 @@ export function ConflictDialog({
let unlistenResolved: UnlistenFn | null = null
let unlistenCompleted: UnlistenFn | null = null
let unlistenAborted: UnlistenFn | null = null
listen<{ folder_id: number; file: string }>(
"folder://merge-conflict-resolved",
@@ -91,6 +94,7 @@ export function ConflictDialog({
listen<{ folder_id: number }>("folder://merge-completed", (event) => {
if (event.payload.folder_id !== folderId) return
setDone(true)
onResolved()
onClose()
})
@@ -99,12 +103,26 @@ export function ConflictDialog({
})
.catch(() => {})
// Merge was aborted (user clicked abort in merge window, or window closed)
// Reset resolved state since abort reverts all changes
listen<{ folder_id: number }>("folder://merge-aborted", (event) => {
if (event.payload.folder_id !== folderId) return
setDone(true)
setResolvedFiles(new Set())
onClose()
})
.then((fn) => {
unlistenAborted = fn
})
.catch(() => {})
return () => {
disposeTauriListener(
unlistenResolved,
"ConflictDialog.mergeConflictResolved"
)
disposeTauriListener(unlistenCompleted, "ConflictDialog.mergeCompleted")
disposeTauriListener(unlistenAborted, "ConflictDialog.mergeAborted")
}
}, [open, folderId, onResolved, onClose])
@@ -122,7 +140,7 @@ export function ConflictDialog({
async function handleOpenMergeTool() {
try {
await openMergeWindow(folderId, operation)
await openMergeWindow(folderId, operation, conflictInfo?.upstream_commit)
} catch (err) {
toast.error(String(err))
}
@@ -149,6 +167,7 @@ export function ConflictDialog({
}
async function handleComplete() {
if (done) return
setCompleting(true)
try {
await gitContinueOperation(folderPath, operation)
@@ -227,7 +246,7 @@ export function ConflictDialog({
<Button
size="sm"
onClick={handleComplete}
disabled={completing || aborting}
disabled={completing || aborting || done}
>
{completing && (
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />

View File

@@ -29,6 +29,7 @@ interface MergeWorkspaceProps {
folderId: number
folderPath: string
operation: string
upstreamCommit?: string
onCompleted: () => void
onAborted: () => void
}
@@ -37,6 +38,7 @@ export function MergeWorkspace({
folderId,
folderPath,
operation,
upstreamCommit,
onCompleted,
onAborted,
}: MergeWorkspaceProps) {
@@ -51,6 +53,7 @@ export function MergeWorkspace({
const [completing, setCompleting] = useState(false)
const currentContentRef = useRef<string>("")
const [hasUnresolvedConflicts, setHasUnresolvedConflicts] = useState(true)
const [preparing, setPreparing] = useState(false)
// Load conflict files on mount
useEffect(() => {
@@ -62,7 +65,12 @@ export function MergeWorkspace({
// For pull operations, the merge was aborted during detection to keep
// working tree clean. Re-start the merge to create conflict state.
if (operation === "pull") {
await gitStartPullMerge(folderPath)
setPreparing(true)
try {
await gitStartPullMerge(folderPath, upstreamCommit)
} finally {
setPreparing(false)
}
}
const conflictFiles = await gitListConflicts(folderPath)
setFiles(conflictFiles)
@@ -137,7 +145,7 @@ export function MergeWorkspace({
try {
await gitAbortOperation(folderPath, operation)
toast.success(t("abortSuccess"))
await emit("folder://merge-completed", { folder_id: folderId })
await emit("folder://merge-aborted", { folder_id: folderId })
onAborted()
} catch (err) {
toast.error(toErrorMessage(err))
@@ -221,7 +229,12 @@ export function MergeWorkspace({
{/* Main area: three-pane merge editor */}
<ResizablePanel defaultSize={82}>
{loadingVersions ? (
{preparing ? (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t("preparingMerge")}
</div>
) : loadingVersions ? (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t("loadingFile")}

View File

@@ -557,6 +557,7 @@
"allResolved": "تم حل جميع التعارضات",
"conflictFiles": "ملفات متعارضة",
"loadingFile": "جارٍ تحميل الملف...",
"preparingMerge": "جارٍ تحضير الدمج...",
"selectFile": "اختر ملفًا لحله",
"noConflicts": "لا توجد ملفات متعارضة",
"skipFile": "تخطي",

View File

@@ -557,6 +557,7 @@
"allResolved": "Alle Konflikte gelöst",
"conflictFiles": "Konfliktdateien",
"loadingFile": "Datei wird geladen...",
"preparingMerge": "Merge wird vorbereitet...",
"selectFile": "Datei zum Lösen auswählen",
"noConflicts": "Keine Konfliktdateien",
"skipFile": "Überspringen",

View File

@@ -557,6 +557,7 @@
"allResolved": "All conflicts resolved",
"conflictFiles": "Conflict Files",
"loadingFile": "Loading file...",
"preparingMerge": "Preparing merge...",
"selectFile": "Select a file to resolve",
"noConflicts": "No conflict files",
"skipFile": "Skip",

View File

@@ -557,6 +557,7 @@
"allResolved": "Todos los conflictos resueltos",
"conflictFiles": "Archivos en conflicto",
"loadingFile": "Cargando archivo...",
"preparingMerge": "Preparando fusión...",
"selectFile": "Seleccionar un archivo para resolver",
"noConflicts": "No hay archivos en conflicto",
"skipFile": "Omitir",

View File

@@ -557,6 +557,7 @@
"allResolved": "Tous les conflits sont résolus",
"conflictFiles": "Fichiers en conflit",
"loadingFile": "Chargement du fichier...",
"preparingMerge": "Préparation de la fusion...",
"selectFile": "Sélectionner un fichier à résoudre",
"noConflicts": "Aucun fichier en conflit",
"skipFile": "Passer",

View File

@@ -557,6 +557,7 @@
"allResolved": "すべてのコンフリクトが解決されました",
"conflictFiles": "コンフリクトファイル",
"loadingFile": "ファイルを読み込み中...",
"preparingMerge": "マージを準備中...",
"selectFile": "解決するファイルを選択してください",
"noConflicts": "コンフリクトファイルなし",
"skipFile": "スキップ",

View File

@@ -557,6 +557,7 @@
"allResolved": "모든 충돌이 해결되었습니다",
"conflictFiles": "충돌 파일",
"loadingFile": "파일 로딩 중...",
"preparingMerge": "병합 준비 중...",
"selectFile": "해결할 파일을 선택하세요",
"noConflicts": "충돌 파일 없음",
"skipFile": "건너뛰기",

View File

@@ -557,6 +557,7 @@
"allResolved": "Todos os conflitos resolvidos",
"conflictFiles": "Arquivos em conflito",
"loadingFile": "Carregando arquivo...",
"preparingMerge": "Preparando mesclagem...",
"selectFile": "Selecione um arquivo para resolver",
"noConflicts": "Nenhum arquivo em conflito",
"skipFile": "Pular",

View File

@@ -557,6 +557,7 @@
"allResolved": "所有冲突已解决",
"conflictFiles": "冲突文件",
"loadingFile": "正在加载文件...",
"preparingMerge": "正在准备合并...",
"selectFile": "选择一个文件进行解决",
"noConflicts": "无冲突文件",
"skipFile": "跳过",

View File

@@ -557,6 +557,7 @@
"allResolved": "所有衝突已解決",
"conflictFiles": "衝突檔案",
"loadingFile": "正在載入檔案...",
"preparingMerge": "正在準備合併...",
"selectFile": "選擇一個檔案進行解決",
"noConflicts": "無衝突檔案",
"skipFile": "跳過",

View File

@@ -452,8 +452,15 @@ export async function gitPull(path: string): Promise<GitPullResult> {
return invoke("git_pull", { path })
}
export async function gitStartPullMerge(path: string): Promise<void> {
return invoke("git_start_pull_merge", { path })
export async function gitStartPullMerge(
path: string,
upstreamCommit?: string | null
): Promise<void> {
return invoke("git_start_pull_merge", { path, upstreamCommit })
}
export async function gitHasMergeHead(path: string): Promise<boolean> {
return invoke("git_has_merge_head", { path })
}
export async function gitFetch(path: string): Promise<string> {
@@ -556,9 +563,14 @@ export async function gitContinueOperation(
export async function openMergeWindow(
folderId: number,
operation: string
operation: string,
upstreamCommit?: string | null
): Promise<void> {
return invoke("open_merge_window", { folderId, operation })
return invoke("open_merge_window", {
folderId,
operation,
upstreamCommit: upstreamCommit ?? null,
})
}
export async function gitStash(path: string): Promise<string> {

View File

@@ -670,6 +670,7 @@ export interface GitConflictInfo {
has_conflicts: boolean
conflicted_files: string[]
operation: string
upstream_commit?: string | null
}
export interface GitPullResult {