607 lines
16 KiB
TypeScript
607 lines
16 KiB
TypeScript
import { ipcMain } from 'electron'
|
|
import { IPC } from '../../shared/constants'
|
|
import { execGit, execGitOrThrow } from '../git/executor'
|
|
import { getRepoStatus, stageFile, unstageFile, stageAll, unstageAll, discardFile, discardAll, applyPatch } from '../git/changes'
|
|
import { getLog, getGraphData, getCommitDetail } from '../git/history'
|
|
import { getFileDiff, getCommitDiff, getCommitsDiff, getBranchDiff } from '../git/diff'
|
|
import { listBranches, createBranch, renameBranch, switchBranch, deleteBranch, mergeBranch, rebaseBranch, listTags, createTag, deleteTag, pushTag, listRemotes, addRemote, removeRemote } from '../git/branches'
|
|
import { listStash, createStash, applyStash, popStash, dropStash } from '../git/stash'
|
|
import { fetchRemote, pullRemote, pushRemote } from '../git/remote'
|
|
import { listConflicts, markResolved, continueOperation, abortOperation } from '../git/conflict'
|
|
import { blameFile } from '../git/blame'
|
|
import { upsertRepository, getAllRepositories, getRepository, removeRepository } from '../persistence/database'
|
|
import { statSync, existsSync } from 'fs'
|
|
import { basename, resolve as resolvePath } from 'path'
|
|
import { randomUUID } from 'crypto'
|
|
import { createLogger } from '../logger'
|
|
import type { IpcResult } from '../../shared/types/ipc.types'
|
|
|
|
const logger = createLogger('ipc:repo')
|
|
|
|
function ok<T>(data: T): IpcResult<T> {
|
|
return { success: true, data }
|
|
}
|
|
|
|
function err(error: string, code?: string): IpcResult<never> {
|
|
return { success: false, error, code }
|
|
}
|
|
|
|
async function isValidGitRepo(path: string): Promise<boolean> {
|
|
try {
|
|
const result = await execGit(['rev-parse', '--git-dir'], { cwd: path, timeout: 5000 })
|
|
return result.exitCode === 0
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
export function registerRepoIpc(): void {
|
|
// ——— Repository management ———
|
|
|
|
ipcMain.handle(IPC.REPO_LIST, () => {
|
|
return ok(getAllRepositories())
|
|
})
|
|
|
|
ipcMain.handle(IPC.REPO_OPEN, async (_, path: string) => {
|
|
try {
|
|
if (!existsSync(path)) return err(`Path does not exist: ${path}`)
|
|
const isRepo = await isValidGitRepo(path)
|
|
if (!isRepo) return err(`Not a Git repository: ${path}`)
|
|
|
|
const id = randomUUID()
|
|
const statusResult = await getRepoStatus(path)
|
|
|
|
const repo = {
|
|
id,
|
|
path,
|
|
name: basename(path),
|
|
lastOpened: new Date().toISOString(),
|
|
isPinned: false,
|
|
currentBranch: statusResult.branch,
|
|
isValid: true,
|
|
}
|
|
|
|
upsertRepository(repo)
|
|
return ok({ ...repo, remotes: [] })
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.REPO_INIT, async (_, path: string) => {
|
|
try {
|
|
await execGitOrThrow(['init', path])
|
|
const id = randomUUID()
|
|
const repo = {
|
|
id,
|
|
path: resolvePath(path),
|
|
name: basename(path),
|
|
lastOpened: new Date().toISOString(),
|
|
isPinned: false,
|
|
currentBranch: 'main',
|
|
isValid: true,
|
|
}
|
|
upsertRepository(repo)
|
|
return ok({ ...repo, remotes: [] })
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.REPO_CLONE, async (_, url: string, targetPath: string) => {
|
|
try {
|
|
await execGitOrThrow(['clone', url, targetPath], { timeout: 300_000 })
|
|
const repoPath = resolvePath(targetPath)
|
|
const id = randomUUID()
|
|
const statusResult = await getRepoStatus(repoPath)
|
|
const repo = {
|
|
id,
|
|
path: repoPath,
|
|
name: basename(repoPath),
|
|
lastOpened: new Date().toISOString(),
|
|
isPinned: false,
|
|
currentBranch: statusResult.branch,
|
|
isValid: true,
|
|
}
|
|
upsertRepository(repo)
|
|
return ok({ ...repo, remotes: [] })
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.REPO_STATUS, async (_, repoPath: string) => {
|
|
try {
|
|
const status = await getRepoStatus(repoPath)
|
|
return ok(status)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.REPO_ADD, async (_, repoPath: string) => {
|
|
try {
|
|
if (!existsSync(repoPath)) return err('Path does not exist')
|
|
const isRepo = await isValidGitRepo(repoPath)
|
|
if (!isRepo) return err('Not a Git repository')
|
|
|
|
const existing = getAllRepositories().find(r => r.path === repoPath)
|
|
if (existing) return ok(existing)
|
|
|
|
const id = randomUUID()
|
|
const statusResult = await getRepoStatus(repoPath)
|
|
const repo = {
|
|
id,
|
|
path: repoPath,
|
|
name: basename(repoPath),
|
|
lastOpened: new Date().toISOString(),
|
|
isPinned: false,
|
|
currentBranch: statusResult.branch,
|
|
isValid: true,
|
|
}
|
|
upsertRepository(repo)
|
|
return ok({ ...repo, remotes: [] })
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.REPO_REMOVE, async (_, id: string) => {
|
|
try {
|
|
removeRepository(id)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.REPO_REFRESH, async (_, repoPath: string) => {
|
|
try {
|
|
const status = await getRepoStatus(repoPath)
|
|
return ok(status)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
// ——— Changes & Staging ———
|
|
|
|
ipcMain.handle(IPC.CHANGES_GET, async (_, repoPath: string) => {
|
|
try {
|
|
const status = await getRepoStatus(repoPath)
|
|
return ok(status)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.CHANGES_STAGE, async (_, repoPath: string, filePath: string) => {
|
|
try {
|
|
await stageFile(repoPath, filePath)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.CHANGES_UNSTAGE, async (_, repoPath: string, filePath: string) => {
|
|
try {
|
|
await unstageFile(repoPath, filePath)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.CHANGES_STAGE_ALL, async (_, repoPath: string) => {
|
|
try {
|
|
await stageAll(repoPath)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.CHANGES_UNSTAGE_ALL, async (_, repoPath: string) => {
|
|
try {
|
|
await unstageAll(repoPath)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.CHANGES_DISCARD, async (_, repoPath: string, filePath: string) => {
|
|
try {
|
|
await discardFile(repoPath, filePath)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.CHANGES_DISCARD_ALL, async (_, repoPath: string) => {
|
|
try {
|
|
await discardAll(repoPath)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
// ——— Commits ———
|
|
|
|
ipcMain.handle(IPC.COMMIT_CREATE, async (_, repoPath: string, message: string, options?: { amend?: boolean }) => {
|
|
try {
|
|
if (!message.trim()) return err('Commit message cannot be empty')
|
|
const args = ['commit', '-m', message]
|
|
if (options?.amend) args.push('--amend')
|
|
await execGitOrThrow(args, { cwd: repoPath })
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.COMMIT_AMEND, async (_, repoPath: string, message: string) => {
|
|
try {
|
|
await execGitOrThrow(['commit', '--amend', '-m', message], { cwd: repoPath })
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.COMMIT_CHERRY_PICK, async (_, repoPath: string, hash: string) => {
|
|
try {
|
|
const result = await execGit(['cherry-pick', hash], { cwd: repoPath })
|
|
if (result.exitCode === 0) return ok({ conflicts: false })
|
|
if (result.stdout.includes('CONFLICT')) return ok({ conflicts: true })
|
|
return err(result.stderr)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.COMMIT_REVERT, async (_, repoPath: string, hash: string) => {
|
|
try {
|
|
await execGitOrThrow(['revert', hash, '--no-edit'], { cwd: repoPath })
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
// ——— Diff ———
|
|
|
|
ipcMain.handle(IPC.DIFF_FILE, async (_, repoPath: string, filePath: string, options?: { staged?: boolean; ref?: string }) => {
|
|
try {
|
|
const diff = await getFileDiff(repoPath, filePath, options)
|
|
return ok(diff)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.DIFF_COMMIT, async (_, repoPath: string, hash: string) => {
|
|
try {
|
|
const diffs = await getCommitDiff(repoPath, hash)
|
|
return ok(diffs)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.DIFF_COMMITS, async (_, repoPath: string, fromHash: string, toHash: string) => {
|
|
try {
|
|
const diffs = await getCommitsDiff(repoPath, fromHash, toHash)
|
|
return ok(diffs)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.DIFF_BRANCH, async (_, repoPath: string, branchA: string, branchB: string) => {
|
|
try {
|
|
const diffs = await getBranchDiff(repoPath, branchA, branchB)
|
|
return ok(diffs)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
// ——— History ———
|
|
|
|
ipcMain.handle(IPC.HISTORY_LOG, async (_, repoPath: string, options?: object) => {
|
|
try {
|
|
const commits = await getLog(repoPath, options ?? {})
|
|
return ok(commits)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.HISTORY_GRAPH, async (_, repoPath: string, options?: { limit?: number }) => {
|
|
try {
|
|
const graph = await getGraphData(repoPath, options?.limit ?? 500)
|
|
return ok(graph)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.HISTORY_COMMIT, async (_, repoPath: string, hash: string) => {
|
|
try {
|
|
const commit = await getCommitDetail(repoPath, hash)
|
|
if (!commit) return err(`Commit not found: ${hash}`)
|
|
return ok(commit)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.HISTORY_COMMIT_FILES, async (_, repoPath: string, hash: string) => {
|
|
try {
|
|
const commit = await getCommitDetail(repoPath, hash)
|
|
return ok(commit?.files ?? [])
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
// ——— Branches ———
|
|
|
|
ipcMain.handle(IPC.BRANCH_LIST, async (_, repoPath: string) => {
|
|
try {
|
|
const branches = await listBranches(repoPath)
|
|
return ok(branches)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.BRANCH_CREATE, async (_, repoPath: string, name: string, from?: string) => {
|
|
try {
|
|
await createBranch(repoPath, name, from)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.BRANCH_RENAME, async (_, repoPath: string, oldName: string, newName: string) => {
|
|
try {
|
|
await renameBranch(repoPath, oldName, newName)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.BRANCH_SWITCH, async (_, repoPath: string, name: string) => {
|
|
try {
|
|
await switchBranch(repoPath, name)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.BRANCH_DELETE, async (_, repoPath: string, name: string, force?: boolean) => {
|
|
try {
|
|
await deleteBranch(repoPath, name, force)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.BRANCH_MERGE, async (_, repoPath: string, source: string, options?: object) => {
|
|
try {
|
|
const result = await mergeBranch(repoPath, source, options ?? {})
|
|
return ok(result)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.BRANCH_REBASE, async (_, repoPath: string, onto: string) => {
|
|
try {
|
|
const result = await rebaseBranch(repoPath, onto)
|
|
return ok(result)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
// ——— Tags ———
|
|
|
|
ipcMain.handle(IPC.TAG_LIST, async (_, repoPath: string) => {
|
|
try {
|
|
const tags = await listTags(repoPath)
|
|
return ok(tags)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.TAG_CREATE, async (_, repoPath: string, name: string, ref: string, message?: string) => {
|
|
try {
|
|
await createTag(repoPath, name, ref, message)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.TAG_DELETE, async (_, repoPath: string, name: string) => {
|
|
try {
|
|
await deleteTag(repoPath, name)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.TAG_PUSH, async (_, repoPath: string, remote: string, tagName: string) => {
|
|
try {
|
|
await pushTag(repoPath, remote, tagName)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
// ——— Stash ———
|
|
|
|
ipcMain.handle(IPC.STASH_LIST, async (_, repoPath: string) => {
|
|
try {
|
|
const stashes = await listStash(repoPath)
|
|
return ok(stashes)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.STASH_CREATE, async (_, repoPath: string, message?: string, includeUntracked?: boolean) => {
|
|
try {
|
|
await createStash(repoPath, message, includeUntracked)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.STASH_APPLY, async (_, repoPath: string, index: number) => {
|
|
try {
|
|
const result = await applyStash(repoPath, index)
|
|
return ok(result)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.STASH_POP, async (_, repoPath: string, index: number) => {
|
|
try {
|
|
const result = await popStash(repoPath, index)
|
|
return ok(result)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.STASH_DROP, async (_, repoPath: string, index: number) => {
|
|
try {
|
|
await dropStash(repoPath, index)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
// ——— Remotes ———
|
|
|
|
ipcMain.handle(IPC.REMOTE_LIST, async (_, repoPath: string) => {
|
|
try {
|
|
const remotes = await listRemotes(repoPath)
|
|
return ok(remotes)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.REMOTE_ADD, async (_, repoPath: string, name: string, url: string) => {
|
|
try {
|
|
await addRemote(repoPath, name, url)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.REMOTE_REMOVE, async (_, repoPath: string, name: string) => {
|
|
try {
|
|
await removeRemote(repoPath, name)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.REMOTE_FETCH, async (_, repoPath: string, remote?: string) => {
|
|
try {
|
|
await fetchRemote(repoPath, remote)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.REMOTE_PULL, async (_, repoPath: string, remote?: string, branch?: string) => {
|
|
try {
|
|
const result = await pullRemote(repoPath, remote, branch)
|
|
return ok(result)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.REMOTE_PUSH, async (_, repoPath: string, remote?: string, branch?: string, force?: boolean) => {
|
|
try {
|
|
await pushRemote(repoPath, remote, branch, { forceWithLease: force })
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
// ——— Conflicts ———
|
|
|
|
ipcMain.handle(IPC.CONFLICT_LIST, async (_, repoPath: string) => {
|
|
try {
|
|
const conflicts = await listConflicts(repoPath)
|
|
return ok(conflicts)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.CONFLICT_MARK_RESOLVED, async (_, repoPath: string, filePath: string) => {
|
|
try {
|
|
await markResolved(repoPath, filePath)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.CONFLICT_CONTINUE, async (_, repoPath: string) => {
|
|
try {
|
|
await continueOperation(repoPath)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC.CONFLICT_ABORT, async (_, repoPath: string) => {
|
|
try {
|
|
await abortOperation(repoPath)
|
|
return ok(null)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
|
|
// ——— Blame ———
|
|
|
|
ipcMain.handle(IPC.BLAME_FILE, async (_, repoPath: string, filePath: string, ref?: string) => {
|
|
try {
|
|
const blame = await blameFile(repoPath, filePath, ref)
|
|
return ok(blame)
|
|
} catch (e: any) {
|
|
return err(e.message)
|
|
}
|
|
})
|
|
}
|