flowgit/electron/git/conflict.ts

94 lines
3.3 KiB
TypeScript

import { execGit, execGitOrThrow } from './executor'
import type { ConflictFile, BlameEntry } from '../../shared/types/git.types'
import { readFile } from 'fs/promises'
import { join } from 'path'
import { createLogger } from '../logger'
const logger = createLogger('git:conflict')
/** List files with unresolved merge conflicts. */
export async function listConflicts(repoPath: string): Promise<ConflictFile[]> {
const result = await execGit(['diff', '--name-only', '--diff-filter=U'], { cwd: repoPath })
if (result.exitCode !== 0 || !result.stdout.trim()) return []
const conflictFiles: ConflictFile[] = []
const paths = result.stdout.trim().split('\n').filter(Boolean)
for (const filePath of paths) {
try {
const content = await readFile(join(repoPath, filePath), 'utf8')
const conflictMarkers = (content.match(/^<<<<<<< /gm) ?? []).length
// Extract ours and theirs versions
const oursResult = await execGit(['show', `:2:${filePath}`], { cwd: repoPath })
const theirsResult = await execGit(['show', `:3:${filePath}`], { cwd: repoPath })
conflictFiles.push({
path: filePath,
ours: oursResult.stdout,
theirs: theirsResult.stdout,
conflictCount: conflictMarkers,
})
} catch {
conflictFiles.push({ path: filePath, ours: '', theirs: '', conflictCount: 0 })
}
}
return conflictFiles
}
/** Mark a file as resolved (git add). */
export async function markResolved(repoPath: string, filePath: string): Promise<void> {
await execGitOrThrow(['add', '--', filePath], { cwd: repoPath })
}
/** Continue a merge/rebase/cherry-pick after resolving conflicts. */
export async function continueOperation(repoPath: string): Promise<void> {
// Detect operation type
const mergeHead = await execGit(['rev-parse', '--verify', 'MERGE_HEAD'], { cwd: repoPath })
if (mergeHead.exitCode === 0) {
await execGitOrThrow(['commit', '--no-edit'], { cwd: repoPath })
return
}
const rebaseHead = await execGit(['rev-parse', '--verify', 'REBASE_HEAD'], { cwd: repoPath })
if (rebaseHead.exitCode === 0) {
await execGitOrThrow(['rebase', '--continue'], {
cwd: repoPath,
env: { GIT_EDITOR: 'true' },
})
return
}
const cherryPickHead = await execGit(['rev-parse', '--verify', 'CHERRY_PICK_HEAD'], { cwd: repoPath })
if (cherryPickHead.exitCode === 0) {
await execGitOrThrow(['cherry-pick', '--continue'], { cwd: repoPath })
return
}
throw new Error('No merge, rebase, or cherry-pick in progress')
}
/** Abort a merge/rebase/cherry-pick. */
export async function abortOperation(repoPath: string): Promise<void> {
const mergeHead = await execGit(['rev-parse', '--verify', 'MERGE_HEAD'], { cwd: repoPath })
if (mergeHead.exitCode === 0) {
await execGitOrThrow(['merge', '--abort'], { cwd: repoPath })
return
}
const rebaseHead = await execGit(['rev-parse', '--verify', 'REBASE_HEAD'], { cwd: repoPath })
if (rebaseHead.exitCode === 0) {
await execGitOrThrow(['rebase', '--abort'], { cwd: repoPath })
return
}
const cherryPickHead = await execGit(['rev-parse', '--verify', 'CHERRY_PICK_HEAD'], { cwd: repoPath })
if (cherryPickHead.exitCode === 0) {
await execGitOrThrow(['cherry-pick', '--abort'], { cwd: repoPath })
return
}
throw new Error('No operation to abort')
}