feat: 新增冲突处理、提交搜索、批量操作功能\n\n冲突处理:\n- 检测合并状态并显示冲突提示栏\n- 冲突文件列表展示和内容编辑器\n- 支持保存冲突文件、标记已解决、中止合并、完成合并\n\n提交搜索:\n- 支持按提交信息关键字和作者搜索\n- 搜索结果与完整历史切换显示\n\n批量操作:\n- 侧边栏新增批量操作入口\n- 展示所有项目概览(分支/变更/未推送状态)\n- 支持批量 Pull/Push 并显示执行结果"
This commit is contained in:
@@ -31,6 +31,9 @@ import {
|
|||||||
GlobalOutlined,
|
GlobalOutlined,
|
||||||
SaveOutlined,
|
SaveOutlined,
|
||||||
DownloadOutlined,
|
DownloadOutlined,
|
||||||
|
SearchOutlined,
|
||||||
|
WarningOutlined,
|
||||||
|
StopOutlined,
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import { Modal, message } from 'ant-design-vue'
|
import { Modal, message } from 'ant-design-vue'
|
||||||
import { AppService } from '../../bindings/github.com/zhuy1228/GitPilot/internal/app'
|
import { AppService } from '../../bindings/github.com/zhuy1228/GitPilot/internal/app'
|
||||||
@@ -100,6 +103,21 @@ const showSettings = ref(false)
|
|||||||
const settingsLoading = ref(false)
|
const settingsLoading = ref(false)
|
||||||
const gitConfig = ref({ userName: '', userEmail: '' })
|
const gitConfig = ref({ userName: '', userEmail: '' })
|
||||||
|
|
||||||
|
// ---- 冲突处理 ----
|
||||||
|
const isMerging = ref(false)
|
||||||
|
const conflictFiles = ref([])
|
||||||
|
const selectedConflictFile = ref(null)
|
||||||
|
const conflictContent = ref('')
|
||||||
|
const conflictSaving = ref(false)
|
||||||
|
|
||||||
|
// ---- 提交搜索 ----
|
||||||
|
const searchKeyword = ref('')
|
||||||
|
const searchAuthor = ref('')
|
||||||
|
const searchLoading = ref(false)
|
||||||
|
const isSearchMode = ref(false)
|
||||||
|
const searchResults = ref([])
|
||||||
|
const displayLogs = computed(() => isSearchMode.value ? searchResults.value : commitLogs.value)
|
||||||
|
|
||||||
// ---- 文件列表拖拽调整宽度 ----
|
// ---- 文件列表拖拽调整宽度 ----
|
||||||
const fileListWidth = ref(300)
|
const fileListWidth = ref(300)
|
||||||
const MIN_FILELIST = 180
|
const MIN_FILELIST = 180
|
||||||
@@ -256,6 +274,20 @@ async function loadStatus() {
|
|||||||
loadingFiles.value = false
|
loadingFiles.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查合并冲突状态
|
||||||
|
try {
|
||||||
|
isMerging.value = await AppService.IsMerging(props.project.path)
|
||||||
|
if (isMerging.value) {
|
||||||
|
const cFiles = await AppService.GetConflictFiles(props.project.path)
|
||||||
|
conflictFiles.value = Array.isArray(cFiles) ? cFiles : []
|
||||||
|
} else {
|
||||||
|
conflictFiles.value = []
|
||||||
|
selectedConflictFile.value = null
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('检查合并状态失败:', e)
|
||||||
|
}
|
||||||
|
|
||||||
// 第三步:并行加载分支列表 + 提交历史
|
// 第三步:并行加载分支列表 + 提交历史
|
||||||
loadBranches()
|
loadBranches()
|
||||||
if (activeTab.value === 'history') {
|
if (activeTab.value === 'history') {
|
||||||
@@ -794,6 +826,122 @@ async function saveSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 冲突处理 ----
|
||||||
|
async function selectConflictFile(file) {
|
||||||
|
selectedConflictFile.value = file
|
||||||
|
conflictContent.value = ''
|
||||||
|
try {
|
||||||
|
const content = await AppService.GetConflictFileContent(props.project.path, file.filePath)
|
||||||
|
conflictContent.value = content
|
||||||
|
} catch (e) {
|
||||||
|
conflictContent.value = '读取冲突文件失败: ' + e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveConflictContent() {
|
||||||
|
if (!selectedConflictFile.value) return
|
||||||
|
conflictSaving.value = true
|
||||||
|
try {
|
||||||
|
await AppService.SaveConflictFile(props.project.path, selectedConflictFile.value.filePath, conflictContent.value)
|
||||||
|
message.success('冲突文件已保存')
|
||||||
|
} catch (e) {
|
||||||
|
Modal.error({ title: '保存失败', content: String(e) })
|
||||||
|
} finally {
|
||||||
|
conflictSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveConflictFiles(files) {
|
||||||
|
if (!props.project?.path) return
|
||||||
|
try {
|
||||||
|
const paths = files.map(f => f.filePath)
|
||||||
|
await AppService.ResolveConflictFile(props.project.path, paths)
|
||||||
|
message.success('已标记为已解决')
|
||||||
|
isMerging.value = await AppService.IsMerging(props.project.path)
|
||||||
|
if (isMerging.value) {
|
||||||
|
const cFiles = await AppService.GetConflictFiles(props.project.path)
|
||||||
|
conflictFiles.value = Array.isArray(cFiles) ? cFiles : []
|
||||||
|
} else {
|
||||||
|
conflictFiles.value = []
|
||||||
|
}
|
||||||
|
selectedConflictFile.value = null
|
||||||
|
await refreshFiles()
|
||||||
|
} catch (e) {
|
||||||
|
Modal.error({ title: '解决冲突失败', content: String(e) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeMerge() {
|
||||||
|
if (!props.project?.path) return
|
||||||
|
const msg = commitMessage.value.trim() || 'Merge completed'
|
||||||
|
commitLoading.value = true
|
||||||
|
try {
|
||||||
|
await AppService.CommitChanges(props.project.path, msg)
|
||||||
|
commitMessage.value = ''
|
||||||
|
isMerging.value = false
|
||||||
|
conflictFiles.value = []
|
||||||
|
selectedConflictFile.value = null
|
||||||
|
await loadStatus()
|
||||||
|
} catch (e) {
|
||||||
|
Modal.error({ title: '完成合并失败', content: String(e) })
|
||||||
|
} finally {
|
||||||
|
commitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAbortMerge() {
|
||||||
|
if (!props.project?.path) return
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认中止合并',
|
||||||
|
icon: h(ExclamationCircleOutlined),
|
||||||
|
content: '中止合并将丢弃所有合并更改,回到合并之前的状态。确定继续?',
|
||||||
|
okText: '中止合并',
|
||||||
|
cancelText: '取消',
|
||||||
|
okButtonProps: { danger: true },
|
||||||
|
async onOk() {
|
||||||
|
try {
|
||||||
|
await AppService.AbortMerge(props.project.path)
|
||||||
|
message.success('已中止合并')
|
||||||
|
isMerging.value = false
|
||||||
|
conflictFiles.value = []
|
||||||
|
selectedConflictFile.value = null
|
||||||
|
await loadStatus()
|
||||||
|
} catch (e) {
|
||||||
|
Modal.error({ title: '中止合并失败', content: String(e) })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 提交搜索 ----
|
||||||
|
async function searchCommits() {
|
||||||
|
if (!props.project?.path) return
|
||||||
|
if (!searchKeyword.value.trim() && !searchAuthor.value.trim()) return
|
||||||
|
searchLoading.value = true
|
||||||
|
isSearchMode.value = true
|
||||||
|
try {
|
||||||
|
const results = await AppService.SearchCommitLog(
|
||||||
|
props.project.path,
|
||||||
|
searchKeyword.value.trim(),
|
||||||
|
searchAuthor.value.trim(),
|
||||||
|
100
|
||||||
|
)
|
||||||
|
searchResults.value = Array.isArray(results) ? results : []
|
||||||
|
} catch (e) {
|
||||||
|
console.error('搜索提交失败:', e)
|
||||||
|
searchResults.value = []
|
||||||
|
} finally {
|
||||||
|
searchLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSearch() {
|
||||||
|
isSearchMode.value = false
|
||||||
|
searchKeyword.value = ''
|
||||||
|
searchAuthor.value = ''
|
||||||
|
searchResults.value = []
|
||||||
|
}
|
||||||
|
|
||||||
// ---- 提交历史 ----
|
// ---- 提交历史 ----
|
||||||
async function loadCommitLog() {
|
async function loadCommitLog() {
|
||||||
if (!props.project?.path) return
|
if (!props.project?.path) return
|
||||||
@@ -1237,6 +1385,57 @@ watch(activeTab, (tab) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 合并冲突提示栏 -->
|
||||||
|
<div v-if="isMerging" class="conflict-banner">
|
||||||
|
<div class="conflict-banner-header">
|
||||||
|
<WarningOutlined style="color: #fab387;" />
|
||||||
|
<span style="font-weight: 600;">合并冲突</span>
|
||||||
|
<span v-if="conflictFiles.length" style="color: var(--text-muted); font-size: 12px;">
|
||||||
|
{{ conflictFiles.length }} 个文件需要解决
|
||||||
|
</span>
|
||||||
|
<span style="flex:1"></span>
|
||||||
|
<a-space :size="4">
|
||||||
|
<a-button size="small" danger @click="handleAbortMerge">
|
||||||
|
<template #icon><StopOutlined /></template>
|
||||||
|
中止
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
:disabled="conflictFiles.length > 0"
|
||||||
|
:loading="commitLoading"
|
||||||
|
@click="completeMerge"
|
||||||
|
>
|
||||||
|
完成合并
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
<div v-if="conflictFiles.length" class="conflict-file-list">
|
||||||
|
<div
|
||||||
|
v-for="cf in conflictFiles"
|
||||||
|
:key="cf.filePath"
|
||||||
|
class="conflict-file-item"
|
||||||
|
:class="{ active: selectedConflictFile?.filePath === cf.filePath }"
|
||||||
|
@click="selectConflictFile(cf)"
|
||||||
|
>
|
||||||
|
<WarningOutlined style="color: #fab387; font-size: 12px;" />
|
||||||
|
<span class="conflict-file-name">{{ cf.filePath }}</span>
|
||||||
|
<div class="conflict-file-actions" @click.stop>
|
||||||
|
<a-tooltip title="标记为已解决">
|
||||||
|
<span class="conflict-resolve-btn" @click="resolveConflictFiles([cf])">
|
||||||
|
<CheckCircleOutlined />
|
||||||
|
</span>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="conflictFiles.length > 1" style="padding: 4px 8px; text-align: right; border-top: 1px solid var(--border-color);">
|
||||||
|
<a-button size="small" type="link" @click="resolveConflictFiles(conflictFiles)">
|
||||||
|
<CheckCircleOutlined /> 全部标记为已解决
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="file-list-content">
|
<div class="file-list-content">
|
||||||
<!-- 骨架屏 -->
|
<!-- 骨架屏 -->
|
||||||
<template v-if="loadingBase || loadingFiles">
|
<template v-if="loadingBase || loadingFiles">
|
||||||
@@ -1316,20 +1515,46 @@ watch(activeTab, (tab) => {
|
|||||||
<!-- ===== 提交历史面板 ===== -->
|
<!-- ===== 提交历史面板 ===== -->
|
||||||
<template v-else-if="activeTab === 'history'">
|
<template v-else-if="activeTab === 'history'">
|
||||||
<div class="file-list-content history-panel">
|
<div class="file-list-content history-panel">
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<div class="search-bar">
|
||||||
|
<a-input
|
||||||
|
v-model:value="searchKeyword"
|
||||||
|
placeholder="搜索提交信息..."
|
||||||
|
size="small"
|
||||||
|
allow-clear
|
||||||
|
@pressEnter="searchCommits"
|
||||||
|
>
|
||||||
|
<template #prefix><SearchOutlined style="color: var(--text-muted);" /></template>
|
||||||
|
</a-input>
|
||||||
|
<a-input
|
||||||
|
v-model:value="searchAuthor"
|
||||||
|
placeholder="作者"
|
||||||
|
size="small"
|
||||||
|
style="width: 100px;"
|
||||||
|
allow-clear
|
||||||
|
@pressEnter="searchCommits"
|
||||||
|
>
|
||||||
|
<template #prefix><UserOutlined style="color: var(--text-muted);" /></template>
|
||||||
|
</a-input>
|
||||||
|
<a-button size="small" type="primary" :loading="searchLoading" @click="searchCommits">
|
||||||
|
<template #icon><SearchOutlined /></template>
|
||||||
|
</a-button>
|
||||||
|
<a-button v-if="isSearchMode" size="small" @click="clearSearch">清除</a-button>
|
||||||
|
</div>
|
||||||
<!-- 提交列表区域 -->
|
<!-- 提交列表区域 -->
|
||||||
<div class="commit-list-section" :class="{ 'has-selected': selectedCommit }">
|
<div class="commit-list-section" :class="{ 'has-selected': selectedCommit }">
|
||||||
<template v-if="commitLogsLoading">
|
<template v-if="commitLogsLoading || searchLoading">
|
||||||
<div v-for="i in 8" :key="i" style="padding: 10px 12px;">
|
<div v-for="i in 8" :key="i" style="padding: 10px 12px;">
|
||||||
<div style="height: 14px; background: var(--bg-hover, #333); border-radius: 4px; animation: pulse 1.5s infinite;" :style="{ width: (40 + i * 7) + '%' }"></div>
|
<div style="height: 14px; background: var(--bg-hover, #333); border-radius: 4px; animation: pulse 1.5s infinite;" :style="{ width: (40 + i * 7) + '%' }"></div>
|
||||||
<div style="height: 10px; background: var(--bg-hover, #333); border-radius: 4px; animation: pulse 1.5s infinite; margin-top: 4px; width: 40%;"></div>
|
<div style="height: 10px; background: var(--bg-hover, #333); border-radius: 4px; animation: pulse 1.5s infinite; margin-top: 4px; width: 40%;"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else-if="!commitLogs.length" style="padding: 24px; text-align: center; color: var(--text-muted);">
|
<div v-else-if="!displayLogs.length" style="padding: 24px; text-align: center; color: var(--text-muted);">
|
||||||
暂无提交历史
|
{{ isSearchMode ? '未找到匹配的提交' : '暂无提交历史' }}
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div
|
<div
|
||||||
v-for="log in commitLogs"
|
v-for="log in displayLogs"
|
||||||
:key="log.hash"
|
:key="log.hash"
|
||||||
class="commit-item"
|
class="commit-item"
|
||||||
:class="{ active: selectedCommit?.hash === log.hash, unpushed: !log.pushed }"
|
:class="{ active: selectedCommit?.hash === log.hash, unpushed: !log.pushed }"
|
||||||
@@ -1503,7 +1728,34 @@ watch(activeTab, (tab) => {
|
|||||||
<div class="file-viewer">
|
<div class="file-viewer">
|
||||||
<!-- 变更模式的文件查看器 -->
|
<!-- 变更模式的文件查看器 -->
|
||||||
<template v-if="activeTab === 'changes'">
|
<template v-if="activeTab === 'changes'">
|
||||||
<div v-if="!selectedFile" class="empty-state small">
|
<!-- 冲突文件编辑器 -->
|
||||||
|
<template v-if="selectedConflictFile">
|
||||||
|
<div class="viewer-header">
|
||||||
|
<WarningOutlined style="color: #fab387; margin-right: 6px;" />
|
||||||
|
<span class="viewer-path">{{ selectedConflictFile.filePath }}</span>
|
||||||
|
<a-tag color="warning" size="small" style="margin-left: 8px;">冲突</a-tag>
|
||||||
|
<span style="flex:1"></span>
|
||||||
|
<a-space :size="6">
|
||||||
|
<a-button size="small" :loading="conflictSaving" @click="saveConflictContent">
|
||||||
|
<template #icon><SaveOutlined /></template>
|
||||||
|
保存
|
||||||
|
</a-button>
|
||||||
|
<a-button size="small" type="primary" @click="resolveConflictFiles([selectedConflictFile])">
|
||||||
|
<template #icon><CheckCircleOutlined /></template>
|
||||||
|
标记已解决
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
<div class="viewer-content conflict-editor">
|
||||||
|
<textarea
|
||||||
|
v-model="conflictContent"
|
||||||
|
class="conflict-textarea"
|
||||||
|
spellcheck="false"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- 普通文件查看器 -->
|
||||||
|
<div v-else-if="!selectedFile" class="empty-state small">
|
||||||
<span style="color: var(--text-muted)">点击左侧文件查看详情</span>
|
<span style="color: var(--text-muted)">点击左侧文件查看详情</span>
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -2375,4 +2627,108 @@ watch(activeTab, (tab) => {
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 冲突处理 */
|
||||||
|
.conflict-banner {
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
background: rgba(250, 179, 135, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-banner-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-file-list {
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-file-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-file-item:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-file-item.active {
|
||||||
|
background: var(--bg-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-file-name {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-family: 'Consolas', 'Courier New', monospace;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-file-actions {
|
||||||
|
visibility: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-file-item:hover .conflict-file-actions {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-resolve-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-resolve-btn:hover {
|
||||||
|
background: rgba(166, 227, 161, 0.2);
|
||||||
|
color: var(--success, #a6e3a1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-textarea {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
background: var(--bg-primary, #1e1e2e);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
padding: 12px;
|
||||||
|
font-family: 'Consolas', 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
tab-size: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 提交搜索 */
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,8 +9,14 @@ import {
|
|||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
CloudDownloadOutlined,
|
CloudDownloadOutlined,
|
||||||
LoadingOutlined,
|
LoadingOutlined,
|
||||||
|
AppstoreOutlined,
|
||||||
|
CloudUploadOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
CloseCircleOutlined,
|
||||||
|
BranchesOutlined,
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message, Modal } from 'ant-design-vue'
|
||||||
import { AppService } from '../../bindings/github.com/zhuy1228/GitPilot/internal/app'
|
import { AppService } from '../../bindings/github.com/zhuy1228/GitPilot/internal/app'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -32,6 +38,19 @@ const showCloneDialog = ref(false)
|
|||||||
const cloneForm = ref({ platform: '', username: '', repoURL: '', parentDir: '', name: '' })
|
const cloneForm = ref({ platform: '', username: '', repoURL: '', parentDir: '', name: '' })
|
||||||
const cloneLoading = ref(false)
|
const cloneLoading = ref(false)
|
||||||
|
|
||||||
|
// ---- 批量操作 ----
|
||||||
|
const showBatchModal = ref(false)
|
||||||
|
const batchOverviews = ref([])
|
||||||
|
const batchLoading = ref(false)
|
||||||
|
const selectedBatchPaths = ref([])
|
||||||
|
const batchResults = ref([])
|
||||||
|
const batchActionLoading = ref('')
|
||||||
|
const batchResultMap = computed(() => {
|
||||||
|
const map = {}
|
||||||
|
batchResults.value.forEach(r => { map[r.path] = r })
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
|
||||||
// 右键菜单
|
// 右键菜单
|
||||||
const contextMenu = ref({ visible: false, x: 0, y: 0, type: '', data: {} })
|
const contextMenu = ref({ visible: false, x: 0, y: 0, type: '', data: {} })
|
||||||
|
|
||||||
@@ -393,6 +412,79 @@ async function removeProject(platform, username, name) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 批量操作 ----
|
||||||
|
async function openBatchModal() {
|
||||||
|
showBatchModal.value = true
|
||||||
|
batchResults.value = []
|
||||||
|
await loadBatchOverview()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBatchOverview() {
|
||||||
|
batchLoading.value = true
|
||||||
|
try {
|
||||||
|
const overviews = await AppService.GetAllProjectOverview()
|
||||||
|
batchOverviews.value = Array.isArray(overviews) ? overviews : []
|
||||||
|
selectedBatchPaths.value = batchOverviews.value
|
||||||
|
.filter(p => !p.error)
|
||||||
|
.map(p => p.path)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取项目概览失败:', e)
|
||||||
|
} finally {
|
||||||
|
batchLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleBatchPath(path) {
|
||||||
|
const idx = selectedBatchPaths.value.indexOf(path)
|
||||||
|
if (idx >= 0) {
|
||||||
|
selectedBatchPaths.value = selectedBatchPaths.value.filter(p => p !== path)
|
||||||
|
} else {
|
||||||
|
selectedBatchPaths.value = [...selectedBatchPaths.value, path]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAllBatchPaths(e) {
|
||||||
|
if (e.target.checked) {
|
||||||
|
selectedBatchPaths.value = batchOverviews.value
|
||||||
|
.filter(p => !p.error)
|
||||||
|
.map(p => p.path)
|
||||||
|
} else {
|
||||||
|
selectedBatchPaths.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doBatchPull() {
|
||||||
|
if (!selectedBatchPaths.value.length) return
|
||||||
|
batchActionLoading.value = 'pull'
|
||||||
|
try {
|
||||||
|
const results = await AppService.BatchPull(selectedBatchPaths.value)
|
||||||
|
batchResults.value = Array.isArray(results) ? results : []
|
||||||
|
const successCount = batchResults.value.filter(r => r.success).length
|
||||||
|
message.info(`批量 Pull 完成:${successCount}/${batchResults.value.length} 成功`)
|
||||||
|
await loadBatchOverview()
|
||||||
|
} catch (e) {
|
||||||
|
Modal.error({ title: '批量 Pull 失败', content: String(e) })
|
||||||
|
} finally {
|
||||||
|
batchActionLoading.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doBatchPush() {
|
||||||
|
if (!selectedBatchPaths.value.length) return
|
||||||
|
batchActionLoading.value = 'push'
|
||||||
|
try {
|
||||||
|
const results = await AppService.BatchPush(selectedBatchPaths.value)
|
||||||
|
batchResults.value = Array.isArray(results) ? results : []
|
||||||
|
const successCount = batchResults.value.filter(r => r.success).length
|
||||||
|
message.info(`批量 Push 完成:${successCount}/${batchResults.value.length} 成功`)
|
||||||
|
await loadBatchOverview()
|
||||||
|
} catch (e) {
|
||||||
|
Modal.error({ title: '批量 Push 失败', content: String(e) })
|
||||||
|
} finally {
|
||||||
|
batchActionLoading.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 平台 SVG Logo
|
// 平台 SVG Logo
|
||||||
const platformLogos = {
|
const platformLogos = {
|
||||||
github: `<svg viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>`,
|
github: `<svg viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>`,
|
||||||
@@ -415,9 +507,14 @@ onMounted(() => {
|
|||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<span class="logo">GitPilot</span>
|
<span class="logo">GitPilot</span>
|
||||||
|
<a-space :size="0">
|
||||||
|
<a-button type="text" size="small" @click="openBatchModal" title="批量操作">
|
||||||
|
<template #icon><AppstoreOutlined /></template>
|
||||||
|
</a-button>
|
||||||
<a-button type="text" size="small" @click="openAddPlatformDialog" title="添加平台">
|
<a-button type="text" size="small" @click="openAddPlatformDialog" title="添加平台">
|
||||||
<template #icon><PlusOutlined /></template>
|
<template #icon><PlusOutlined /></template>
|
||||||
</a-button>
|
</a-button>
|
||||||
|
</a-space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-content" @contextmenu="onSidebarContextMenu">
|
<div class="sidebar-content" @contextmenu="onSidebarContextMenu">
|
||||||
@@ -600,6 +697,82 @@ onMounted(() => {
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 批量操作弹窗 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="showBatchModal"
|
||||||
|
title="批量操作"
|
||||||
|
:width="600"
|
||||||
|
:footer="null"
|
||||||
|
>
|
||||||
|
<div class="batch-panel">
|
||||||
|
<div class="batch-header">
|
||||||
|
<a-checkbox
|
||||||
|
:checked="selectedBatchPaths.length === batchOverviews.filter(p => !p.error).length && batchOverviews.length > 0"
|
||||||
|
:indeterminate="selectedBatchPaths.length > 0 && selectedBatchPaths.length < batchOverviews.filter(p => !p.error).length"
|
||||||
|
@change="toggleAllBatchPaths"
|
||||||
|
>
|
||||||
|
全选
|
||||||
|
</a-checkbox>
|
||||||
|
<span style="flex:1"></span>
|
||||||
|
<a-space :size="4">
|
||||||
|
<a-button size="small" :loading="batchActionLoading === 'pull'" :disabled="!selectedBatchPaths.length" @click="doBatchPull">
|
||||||
|
<template #icon><CloudDownloadOutlined /></template>
|
||||||
|
Pull
|
||||||
|
</a-button>
|
||||||
|
<a-button size="small" :loading="batchActionLoading === 'push'" :disabled="!selectedBatchPaths.length" @click="doBatchPush">
|
||||||
|
<template #icon><CloudUploadOutlined /></template>
|
||||||
|
Push
|
||||||
|
</a-button>
|
||||||
|
<a-button size="small" :loading="batchLoading" @click="loadBatchOverview">
|
||||||
|
<template #icon><SyncOutlined /></template>
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
<div class="batch-list">
|
||||||
|
<template v-if="batchLoading">
|
||||||
|
<div v-for="i in 4" :key="i" style="padding: 10px 12px;">
|
||||||
|
<div style="height: 14px; background: var(--bg-hover, #333); border-radius: 4px; animation: pulse 1.5s infinite;" :style="{ width: (50 + i * 8) + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else-if="!batchOverviews.length" style="padding: 24px; text-align: center; color: var(--text-muted);">
|
||||||
|
暂无项目
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-for="proj in batchOverviews"
|
||||||
|
:key="proj.key"
|
||||||
|
class="batch-item"
|
||||||
|
:class="{ error: !!proj.error }"
|
||||||
|
>
|
||||||
|
<a-checkbox
|
||||||
|
:checked="selectedBatchPaths.includes(proj.path)"
|
||||||
|
:disabled="!!proj.error"
|
||||||
|
@change="toggleBatchPath(proj.path)"
|
||||||
|
style="flex-shrink: 0;"
|
||||||
|
/>
|
||||||
|
<div class="batch-item-info">
|
||||||
|
<div class="batch-item-name">{{ proj.name }}</div>
|
||||||
|
<div class="batch-item-meta">
|
||||||
|
<a-tag v-if="proj.branch" size="small" color="green">
|
||||||
|
<BranchesOutlined /> {{ proj.branch }}
|
||||||
|
</a-tag>
|
||||||
|
<a-tag v-if="proj.hasChanges" size="small" color="orange">有变更</a-tag>
|
||||||
|
<a-tag v-if="proj.unpushed > 0" size="small" color="blue">{{ proj.unpushed }} 未推送</a-tag>
|
||||||
|
<span v-if="proj.error" style="color: #f38ba8; font-size: 11px;">{{ proj.error }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="batchResultMap[proj.path]" class="batch-result-icon">
|
||||||
|
<CheckCircleOutlined v-if="batchResultMap[proj.path].success" style="color: #a6e3a1;" />
|
||||||
|
<a-tooltip v-else :title="batchResultMap[proj.path].message">
|
||||||
|
<CloseCircleOutlined style="color: #f38ba8;" />
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -726,4 +899,72 @@ onMounted(() => {
|
|||||||
:deep(.ant-tree .ant-tree-icon__customize) {
|
:deep(.ant-tree .ant-tree-icon__customize) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 批量操作 */
|
||||||
|
.batch-panel {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid var(--border-color, #313244);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 4px;
|
||||||
|
border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.04));
|
||||||
|
transition: background 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-item:hover {
|
||||||
|
background: var(--bg-hover, rgba(255,255,255,0.04));
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-item.error {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-item-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-item-name {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-item-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 2px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-result-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 0.4; }
|
||||||
|
50% { opacity: 0.8; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ import * as config$0 from "../../config/models.js";
|
|||||||
// @ts-ignore: Unused imports
|
// @ts-ignore: Unused imports
|
||||||
import * as $models from "./models.js";
|
import * as $models from "./models.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AbortMerge 中止合并
|
||||||
|
* @param {string} path
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
|
*/
|
||||||
|
export function AbortMerge(path) {
|
||||||
|
return $Call.ByID(3035150106, path);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AddPlatform 添加新平台
|
* AddPlatform 添加新平台
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
@@ -55,6 +64,28 @@ export function AddUser(platform, username, token) {
|
|||||||
return $Call.ByID(2264426704, platform, username, token);
|
return $Call.ByID(2264426704, platform, username, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BatchPull 批量拉取指定项目
|
||||||
|
* @param {string[]} paths
|
||||||
|
* @returns {$CancellablePromise<$models.BatchPullResult[]>}
|
||||||
|
*/
|
||||||
|
export function BatchPull(paths) {
|
||||||
|
return $Call.ByID(758996647, paths).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType1($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BatchPush 批量推送指定项目
|
||||||
|
* @param {string[]} paths
|
||||||
|
* @returns {$CancellablePromise<$models.BatchPullResult[]>}
|
||||||
|
*/
|
||||||
|
export function BatchPush(paths) {
|
||||||
|
return $Call.ByID(794082076, paths).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType1($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CheckoutRemoteBranch 检出远程分支到本地
|
* CheckoutRemoteBranch 检出远程分支到本地
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
@@ -159,13 +190,23 @@ export function FetchProject(path) {
|
|||||||
return $Call.ByID(3541106829, path);
|
return $Call.ByID(3541106829, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetAllProjectOverview 获取所有项目的概览状态
|
||||||
|
* @returns {$CancellablePromise<$models.ProjectOverview[]>}
|
||||||
|
*/
|
||||||
|
export function GetAllProjectOverview() {
|
||||||
|
return $Call.ByID(2453130151).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType3($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GetAppSettings 获取应用设置
|
* GetAppSettings 获取应用设置
|
||||||
* @returns {$CancellablePromise<config$0.Settings | null>}
|
* @returns {$CancellablePromise<config$0.Settings | null>}
|
||||||
*/
|
*/
|
||||||
export function GetAppSettings() {
|
export function GetAppSettings() {
|
||||||
return $Call.ByID(428589026).then(/** @type {($result: any) => any} */(($result) => {
|
return $Call.ByID(428589026).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
return $$createType1($result);
|
return $$createType5($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +217,7 @@ export function GetAppSettings() {
|
|||||||
*/
|
*/
|
||||||
export function GetBranches(path) {
|
export function GetBranches(path) {
|
||||||
return $Call.ByID(1686190192, path).then(/** @type {($result: any) => any} */(($result) => {
|
return $Call.ByID(1686190192, path).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
return $$createType3($result);
|
return $$createType7($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +250,7 @@ export function GetCommitFileDiff(path, hash, filePath) {
|
|||||||
*/
|
*/
|
||||||
export function GetCommitFiles(path, hash) {
|
export function GetCommitFiles(path, hash) {
|
||||||
return $Call.ByID(2420707876, path, hash).then(/** @type {($result: any) => any} */(($result) => {
|
return $Call.ByID(2420707876, path, hash).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
return $$createType5($result);
|
return $$createType9($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +262,28 @@ export function GetCommitFiles(path, hash) {
|
|||||||
*/
|
*/
|
||||||
export function GetCommitLog(path, count) {
|
export function GetCommitLog(path, count) {
|
||||||
return $Call.ByID(1281870789, path, count).then(/** @type {($result: any) => any} */(($result) => {
|
return $Call.ByID(1281870789, path, count).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
return $$createType7($result);
|
return $$createType11($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetConflictFileContent 获取冲突文件内容(包含冲突标记)
|
||||||
|
* @param {string} projectPath
|
||||||
|
* @param {string} filePath
|
||||||
|
* @returns {$CancellablePromise<string>}
|
||||||
|
*/
|
||||||
|
export function GetConflictFileContent(projectPath, filePath) {
|
||||||
|
return $Call.ByID(298498517, projectPath, filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetConflictFiles 获取冲突文件列表
|
||||||
|
* @param {string} path
|
||||||
|
* @returns {$CancellablePromise<$models.ConflictFileInfo[]>}
|
||||||
|
*/
|
||||||
|
export function GetConflictFiles(path) {
|
||||||
|
return $Call.ByID(2446806137, path).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType13($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,7 +323,7 @@ export function GetFileDiffStaged(projectPath, filePath) {
|
|||||||
*/
|
*/
|
||||||
export function GetGitGlobalConfig() {
|
export function GetGitGlobalConfig() {
|
||||||
return $Call.ByID(154811497).then(/** @type {($result: any) => any} */(($result) => {
|
return $Call.ByID(154811497).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
return $$createType9($result);
|
return $$createType15($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +334,7 @@ export function GetGitGlobalConfig() {
|
|||||||
*/
|
*/
|
||||||
export function GetPlatformInfo(name) {
|
export function GetPlatformInfo(name) {
|
||||||
return $Call.ByID(2668095547, name).then(/** @type {($result: any) => any} */(($result) => {
|
return $Call.ByID(2668095547, name).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
return $$createType11($result);
|
return $$createType17($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +345,7 @@ export function GetPlatformInfo(name) {
|
|||||||
*/
|
*/
|
||||||
export function GetProjectChangedFiles(path) {
|
export function GetProjectChangedFiles(path) {
|
||||||
return $Call.ByID(2302591462, path).then(/** @type {($result: any) => any} */(($result) => {
|
return $Call.ByID(2302591462, path).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
return $$createType13($result);
|
return $$createType19($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,7 +356,7 @@ export function GetProjectChangedFiles(path) {
|
|||||||
*/
|
*/
|
||||||
export function GetProjectStatus(path) {
|
export function GetProjectStatus(path) {
|
||||||
return $Call.ByID(3451089027, path).then(/** @type {($result: any) => any} */(($result) => {
|
return $Call.ByID(3451089027, path).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
return $$createType15($result);
|
return $$createType21($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +366,7 @@ export function GetProjectStatus(path) {
|
|||||||
*/
|
*/
|
||||||
export function GetProjectTree() {
|
export function GetProjectTree() {
|
||||||
return $Call.ByID(651189689).then(/** @type {($result: any) => any} */(($result) => {
|
return $Call.ByID(651189689).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
return $$createType17($result);
|
return $$createType23($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +377,7 @@ export function GetProjectTree() {
|
|||||||
*/
|
*/
|
||||||
export function GetRemoteBranches(path) {
|
export function GetRemoteBranches(path) {
|
||||||
return $Call.ByID(994796370, path).then(/** @type {($result: any) => any} */(($result) => {
|
return $Call.ByID(994796370, path).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
return $$createType3($result);
|
return $$createType7($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +388,7 @@ export function GetRemoteBranches(path) {
|
|||||||
*/
|
*/
|
||||||
export function GetStashList(path) {
|
export function GetStashList(path) {
|
||||||
return $Call.ByID(1948257945, path).then(/** @type {($result: any) => any} */(($result) => {
|
return $Call.ByID(1948257945, path).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
return $$createType19($result);
|
return $$createType25($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,7 +399,7 @@ export function GetStashList(path) {
|
|||||||
*/
|
*/
|
||||||
export function GetTags(path) {
|
export function GetTags(path) {
|
||||||
return $Call.ByID(3979169621, path).then(/** @type {($result: any) => any} */(($result) => {
|
return $Call.ByID(3979169621, path).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
return $$createType21($result);
|
return $$createType27($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,10 +411,19 @@ export function GetTags(path) {
|
|||||||
*/
|
*/
|
||||||
export function GetUserInfo(platform, username) {
|
export function GetUserInfo(platform, username) {
|
||||||
return $Call.ByID(2674655707, platform, username).then(/** @type {($result: any) => any} */(($result) => {
|
return $Call.ByID(2674655707, platform, username).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
return $$createType23($result);
|
return $$createType29($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IsMerging 检查是否处于合并状态
|
||||||
|
* @param {string} path
|
||||||
|
* @returns {$CancellablePromise<boolean>}
|
||||||
|
*/
|
||||||
|
export function IsMerging(path) {
|
||||||
|
return $Call.ByID(693809703, path);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MergeBranch 合并指定分支到当前分支
|
* MergeBranch 合并指定分支到当前分支
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
@@ -433,6 +504,16 @@ export function ResetProject(path, hash, mode) {
|
|||||||
return $Call.ByID(2061148684, path, hash, mode);
|
return $Call.ByID(2061148684, path, hash, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ResolveConflictFile 将冲突文件标记为已解决
|
||||||
|
* @param {string} path
|
||||||
|
* @param {string[]} files
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
|
*/
|
||||||
|
export function ResolveConflictFile(path, files) {
|
||||||
|
return $Call.ByID(1893119072, path, files);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RevertCommit 撤回指定提交(生成一个反向提交)
|
* RevertCommit 撤回指定提交(生成一个反向提交)
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
@@ -443,6 +524,31 @@ export function RevertCommit(path, hash) {
|
|||||||
return $Call.ByID(1334788539, path, hash);
|
return $Call.ByID(1334788539, path, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SaveConflictFile 保存冲突文件内容(手动解决冲突后保存)
|
||||||
|
* @param {string} projectPath
|
||||||
|
* @param {string} filePath
|
||||||
|
* @param {string} content
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
|
*/
|
||||||
|
export function SaveConflictFile(projectPath, filePath, content) {
|
||||||
|
return $Call.ByID(3340289161, projectPath, filePath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SearchCommitLog 搜索提交历史
|
||||||
|
* @param {string} path
|
||||||
|
* @param {string} keyword
|
||||||
|
* @param {string} author
|
||||||
|
* @param {number} maxCount
|
||||||
|
* @returns {$CancellablePromise<$models.CommitLog[]>}
|
||||||
|
*/
|
||||||
|
export function SearchCommitLog(path, keyword, author, maxCount) {
|
||||||
|
return $Call.ByID(2874450917, path, keyword, author, maxCount).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType11($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SelectDirectory 打开系统文件夹选择器,返回选中的路径
|
* SelectDirectory 打开系统文件夹选择器,返回选中的路径
|
||||||
* @returns {$CancellablePromise<string>}
|
* @returns {$CancellablePromise<string>}
|
||||||
@@ -589,27 +695,33 @@ export function UpdateUser(platform, oldUsername, newUsername, token) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Private type creation functions
|
// Private type creation functions
|
||||||
const $$createType0 = config$0.Settings.createFrom;
|
const $$createType0 = $models.BatchPullResult.createFrom;
|
||||||
const $$createType1 = $Create.Nullable($$createType0);
|
const $$createType1 = $Create.Array($$createType0);
|
||||||
const $$createType2 = $models.BranchInfo.createFrom;
|
const $$createType2 = $models.ProjectOverview.createFrom;
|
||||||
const $$createType3 = $Create.Array($$createType2);
|
const $$createType3 = $Create.Array($$createType2);
|
||||||
const $$createType4 = $models.CommitFileInfo.createFrom;
|
const $$createType4 = config$0.Settings.createFrom;
|
||||||
const $$createType5 = $Create.Array($$createType4);
|
const $$createType5 = $Create.Nullable($$createType4);
|
||||||
const $$createType6 = $models.CommitLog.createFrom;
|
const $$createType6 = $models.BranchInfo.createFrom;
|
||||||
const $$createType7 = $Create.Array($$createType6);
|
const $$createType7 = $Create.Array($$createType6);
|
||||||
const $$createType8 = $models.GitConfig.createFrom;
|
const $$createType8 = $models.CommitFileInfo.createFrom;
|
||||||
const $$createType9 = $Create.Nullable($$createType8);
|
const $$createType9 = $Create.Array($$createType8);
|
||||||
const $$createType10 = $models.PlatformInfo.createFrom;
|
const $$createType10 = $models.CommitLog.createFrom;
|
||||||
const $$createType11 = $Create.Nullable($$createType10);
|
const $$createType11 = $Create.Array($$createType10);
|
||||||
const $$createType12 = $models.FileInfo.createFrom;
|
const $$createType12 = $models.ConflictFileInfo.createFrom;
|
||||||
const $$createType13 = $Create.Array($$createType12);
|
const $$createType13 = $Create.Array($$createType12);
|
||||||
const $$createType14 = $models.ProjectStatus.createFrom;
|
const $$createType14 = $models.GitConfig.createFrom;
|
||||||
const $$createType15 = $Create.Nullable($$createType14);
|
const $$createType15 = $Create.Nullable($$createType14);
|
||||||
const $$createType16 = $models.TreeNode.createFrom;
|
const $$createType16 = $models.PlatformInfo.createFrom;
|
||||||
const $$createType17 = $Create.Array($$createType16);
|
const $$createType17 = $Create.Nullable($$createType16);
|
||||||
const $$createType18 = $models.StashInfo.createFrom;
|
const $$createType18 = $models.FileInfo.createFrom;
|
||||||
const $$createType19 = $Create.Array($$createType18);
|
const $$createType19 = $Create.Array($$createType18);
|
||||||
const $$createType20 = $models.TagInfo.createFrom;
|
const $$createType20 = $models.ProjectStatus.createFrom;
|
||||||
const $$createType21 = $Create.Array($$createType20);
|
const $$createType21 = $Create.Nullable($$createType20);
|
||||||
const $$createType22 = $models.UserInfo.createFrom;
|
const $$createType22 = $models.TreeNode.createFrom;
|
||||||
const $$createType23 = $Create.Nullable($$createType22);
|
const $$createType23 = $Create.Array($$createType22);
|
||||||
|
const $$createType24 = $models.StashInfo.createFrom;
|
||||||
|
const $$createType25 = $Create.Array($$createType24);
|
||||||
|
const $$createType26 = $models.TagInfo.createFrom;
|
||||||
|
const $$createType27 = $Create.Array($$createType26);
|
||||||
|
const $$createType28 = $models.UserInfo.createFrom;
|
||||||
|
const $$createType29 = $Create.Nullable($$createType28);
|
||||||
|
|||||||
@@ -8,12 +8,15 @@ export {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
BatchPullResult,
|
||||||
BranchInfo,
|
BranchInfo,
|
||||||
CommitFileInfo,
|
CommitFileInfo,
|
||||||
CommitLog,
|
CommitLog,
|
||||||
|
ConflictFileInfo,
|
||||||
FileInfo,
|
FileInfo,
|
||||||
GitConfig,
|
GitConfig,
|
||||||
PlatformInfo,
|
PlatformInfo,
|
||||||
|
ProjectOverview,
|
||||||
ProjectStatus,
|
ProjectStatus,
|
||||||
StashInfo,
|
StashInfo,
|
||||||
TagInfo,
|
TagInfo,
|
||||||
|
|||||||
@@ -6,6 +6,58 @@
|
|||||||
// @ts-ignore: Unused imports
|
// @ts-ignore: Unused imports
|
||||||
import { Create as $Create } from "@wailsio/runtime";
|
import { Create as $Create } from "@wailsio/runtime";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BatchPullResult 批量 pull 结果
|
||||||
|
*/
|
||||||
|
export class BatchPullResult {
|
||||||
|
/**
|
||||||
|
* Creates a new BatchPullResult instance.
|
||||||
|
* @param {Partial<BatchPullResult>} [$$source = {}] - The source object to create the BatchPullResult.
|
||||||
|
*/
|
||||||
|
constructor($$source = {}) {
|
||||||
|
if (!("name" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["name"] = "";
|
||||||
|
}
|
||||||
|
if (!("path" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["path"] = "";
|
||||||
|
}
|
||||||
|
if (!("success" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
this["success"] = false;
|
||||||
|
}
|
||||||
|
if (!("message" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["message"] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new BatchPullResult instance from a string or object.
|
||||||
|
* @param {any} [$$source = {}]
|
||||||
|
* @returns {BatchPullResult}
|
||||||
|
*/
|
||||||
|
static createFrom($$source = {}) {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new BatchPullResult(/** @type {Partial<BatchPullResult>} */($$parsedSource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BranchInfo 分支信息
|
* BranchInfo 分支信息
|
||||||
*/
|
*/
|
||||||
@@ -155,6 +207,37 @@ export class CommitLog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ConflictFileInfo 冲突文件信息
|
||||||
|
*/
|
||||||
|
export class ConflictFileInfo {
|
||||||
|
/**
|
||||||
|
* Creates a new ConflictFileInfo instance.
|
||||||
|
* @param {Partial<ConflictFileInfo>} [$$source = {}] - The source object to create the ConflictFileInfo.
|
||||||
|
*/
|
||||||
|
constructor($$source = {}) {
|
||||||
|
if (!("filePath" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["filePath"] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new ConflictFileInfo instance from a string or object.
|
||||||
|
* @param {any} [$$source = {}]
|
||||||
|
* @returns {ConflictFileInfo}
|
||||||
|
*/
|
||||||
|
static createFrom($$source = {}) {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new ConflictFileInfo(/** @type {Partial<ConflictFileInfo>} */($$parsedSource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FileInfo 文件信息
|
* FileInfo 文件信息
|
||||||
*/
|
*/
|
||||||
@@ -283,6 +366,79 @@ export class PlatformInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ProjectOverview 项目概览信息(轻量级)
|
||||||
|
*/
|
||||||
|
export class ProjectOverview {
|
||||||
|
/**
|
||||||
|
* Creates a new ProjectOverview instance.
|
||||||
|
* @param {Partial<ProjectOverview>} [$$source = {}] - The source object to create the ProjectOverview.
|
||||||
|
*/
|
||||||
|
constructor($$source = {}) {
|
||||||
|
if (!("key" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["key"] = "";
|
||||||
|
}
|
||||||
|
if (!("name" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["name"] = "";
|
||||||
|
}
|
||||||
|
if (!("path" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["path"] = "";
|
||||||
|
}
|
||||||
|
if (!("branch" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["branch"] = "";
|
||||||
|
}
|
||||||
|
if (!("hasChanges" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
this["hasChanges"] = false;
|
||||||
|
}
|
||||||
|
if (!("unpushed" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this["unpushed"] = 0;
|
||||||
|
}
|
||||||
|
if (/** @type {any} */(false)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string | undefined}
|
||||||
|
*/
|
||||||
|
this["error"] = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new ProjectOverview instance from a string or object.
|
||||||
|
* @param {any} [$$source = {}]
|
||||||
|
* @returns {ProjectOverview}
|
||||||
|
*/
|
||||||
|
static createFrom($$source = {}) {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new ProjectOverview(/** @type {Partial<ProjectOverview>} */($$parsedSource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ProjectStatus 项目状态信息
|
* ProjectStatus 项目状态信息
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1049,3 +1049,224 @@ func (s *AppService) UpdateAppSettings(logLevel string) error {
|
|||||||
s.config.Settings.LogLevel = logLevel
|
s.config.Settings.LogLevel = logLevel
|
||||||
return config.SaveConfig(s.config)
|
return config.SaveConfig(s.config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 冲突处理 ---
|
||||||
|
|
||||||
|
// ConflictFileInfo 冲突文件信息
|
||||||
|
type ConflictFileInfo struct {
|
||||||
|
FilePath string `json:"filePath"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConflictFiles 获取冲突文件列表
|
||||||
|
func (s *AppService) GetConflictFiles(path string) ([]ConflictFileInfo, error) {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return nil, fmt.Errorf("项目路径不存在: %s", path)
|
||||||
|
}
|
||||||
|
out, err := s.gitClient.ConflictFiles(path)
|
||||||
|
if err != nil {
|
||||||
|
// 没有冲突文件时命令可能返回错误,返回空列表
|
||||||
|
return []ConflictFileInfo{}, nil
|
||||||
|
}
|
||||||
|
var files []ConflictFileInfo
|
||||||
|
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
files = append(files, ConflictFileInfo{FilePath: line})
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConflictFileContent 获取冲突文件内容(包含冲突标记)
|
||||||
|
func (s *AppService) GetConflictFileContent(projectPath, filePath string) (string, error) {
|
||||||
|
fullPath := filepath.Join(projectPath, filePath)
|
||||||
|
data, err := os.ReadFile(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("读取冲突文件失败: %w", err)
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveConflictFile 将冲突文件标记为已解决
|
||||||
|
func (s *AppService) ResolveConflictFile(path string, files []string) error {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("项目路径不存在: %s", path)
|
||||||
|
}
|
||||||
|
if len(files) == 0 {
|
||||||
|
return fmt.Errorf("未指定文件")
|
||||||
|
}
|
||||||
|
_, err := s.gitClient.MarkResolved(path, files...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveConflictFile 保存冲突文件内容(手动解决冲突后保存)
|
||||||
|
func (s *AppService) SaveConflictFile(projectPath, filePath, content string) error {
|
||||||
|
fullPath := filepath.Join(projectPath, filePath)
|
||||||
|
return os.WriteFile(fullPath, []byte(content), 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AbortMerge 中止合并
|
||||||
|
func (s *AppService) AbortMerge(path string) error {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("项目路径不存在: %s", path)
|
||||||
|
}
|
||||||
|
_, err := s.gitClient.AbortMerge(path)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsMerging 检查是否处于合并状态
|
||||||
|
func (s *AppService) IsMerging(path string) bool {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return s.gitClient.MergeStatus(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 提交搜索 ---
|
||||||
|
|
||||||
|
// SearchCommitLog 搜索提交历史
|
||||||
|
func (s *AppService) SearchCommitLog(path, keyword, author string, maxCount int) ([]CommitLog, error) {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return nil, fmt.Errorf("项目路径不存在: %s", path)
|
||||||
|
}
|
||||||
|
if maxCount <= 0 {
|
||||||
|
maxCount = 100
|
||||||
|
}
|
||||||
|
out, err := s.gitClient.SearchCommits(path, keyword, author, maxCount)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("搜索提交历史失败: %w", err)
|
||||||
|
}
|
||||||
|
logs := parseCommitLog(out)
|
||||||
|
// 标记推送状态
|
||||||
|
branch, brErr := s.gitClient.Branch(path)
|
||||||
|
if brErr == nil {
|
||||||
|
branch = strings.TrimSpace(branch)
|
||||||
|
unpushedOut, upErr := s.gitClient.UnpushedCommits(path, branch)
|
||||||
|
unpushedSet := make(map[string]bool)
|
||||||
|
if upErr == nil {
|
||||||
|
for _, h := range strings.Split(strings.TrimSpace(unpushedOut), "\n") {
|
||||||
|
if h != "" {
|
||||||
|
unpushedSet[h] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range logs {
|
||||||
|
logs[i].Pushed = !unpushedSet[logs[i].Hash]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return logs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 批量操作 ---
|
||||||
|
|
||||||
|
// ProjectOverview 项目概览信息(轻量级)
|
||||||
|
type ProjectOverview struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Branch string `json:"branch"`
|
||||||
|
HasChanges bool `json:"hasChanges"`
|
||||||
|
Unpushed int `json:"unpushed"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllProjectOverview 获取所有项目的概览状态
|
||||||
|
func (s *AppService) GetAllProjectOverview() []ProjectOverview {
|
||||||
|
var results []ProjectOverview
|
||||||
|
for platformName, platform := range s.config.Platforms {
|
||||||
|
for _, user := range platform.Users {
|
||||||
|
for _, proj := range user.Projects {
|
||||||
|
overview := ProjectOverview{
|
||||||
|
Key: platformName + "/" + user.Username + "/" + proj.Name,
|
||||||
|
Name: proj.Name,
|
||||||
|
Path: proj.Path,
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(proj.Path); os.IsNotExist(err) {
|
||||||
|
overview.Error = "路径不存在"
|
||||||
|
results = append(results, overview)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
branch, hasChanges, unpushed, err := s.gitClient.QuickStatus(proj.Path)
|
||||||
|
if err != nil {
|
||||||
|
overview.Error = err.Error()
|
||||||
|
} else {
|
||||||
|
overview.Branch = branch
|
||||||
|
overview.HasChanges = hasChanges
|
||||||
|
overview.Unpushed = unpushed
|
||||||
|
}
|
||||||
|
results = append(results, overview)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchPullResult 批量 pull 结果
|
||||||
|
type BatchPullResult struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchPull 批量拉取指定项目
|
||||||
|
func (s *AppService) BatchPull(paths []string) []BatchPullResult {
|
||||||
|
var results []BatchPullResult
|
||||||
|
for _, path := range paths {
|
||||||
|
result := BatchPullResult{Path: path}
|
||||||
|
// 从路径推断名称
|
||||||
|
result.Name = filepath.Base(path)
|
||||||
|
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
result.Message = "路径不存在"
|
||||||
|
results = append(results, result)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
branch, err := s.gitClient.Branch(path)
|
||||||
|
if err != nil {
|
||||||
|
result.Message = "获取分支失败: " + err.Error()
|
||||||
|
results = append(results, result)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, err = s.gitClient.Run(path, "pull", "origin", strings.TrimSpace(branch))
|
||||||
|
if err != nil {
|
||||||
|
result.Message = err.Error()
|
||||||
|
} else {
|
||||||
|
result.Success = true
|
||||||
|
result.Message = "拉取成功"
|
||||||
|
}
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchPush 批量推送指定项目
|
||||||
|
func (s *AppService) BatchPush(paths []string) []BatchPullResult {
|
||||||
|
var results []BatchPullResult
|
||||||
|
for _, path := range paths {
|
||||||
|
result := BatchPullResult{Path: path}
|
||||||
|
result.Name = filepath.Base(path)
|
||||||
|
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
result.Message = "路径不存在"
|
||||||
|
results = append(results, result)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
branch, err := s.gitClient.Branch(path)
|
||||||
|
if err != nil {
|
||||||
|
result.Message = "获取分支失败: " + err.Error()
|
||||||
|
results = append(results, result)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, err = s.gitClient.Run(path, "push", "origin", strings.TrimSpace(branch))
|
||||||
|
if err != nil {
|
||||||
|
result.Message = err.Error()
|
||||||
|
} else {
|
||||||
|
result.Success = true
|
||||||
|
result.Message = "推送成功"
|
||||||
|
}
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|||||||
@@ -443,6 +443,79 @@ func (g *GitClient) SetGitGlobalConfig(key, value string) (string, error) {
|
|||||||
return strings.TrimSpace(stdout.String()), nil
|
return strings.TrimSpace(stdout.String()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 冲突处理 ---
|
||||||
|
|
||||||
|
// ConflictFiles 获取冲突文件列表 (git diff --name-only --diff-filter=U)
|
||||||
|
func (g *GitClient) ConflictFiles(path string) (string, error) {
|
||||||
|
return g.Run(path, "diff", "--name-only", "--diff-filter=U")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConflictContent 获取冲突文件的完整内容(含冲突标记)
|
||||||
|
func (g *GitClient) GetConflictContent(path, filePath string) (string, error) {
|
||||||
|
return g.Run(path, "show", ":0:"+filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkResolved 将冲突文件标记为已解决 (git add <file>)
|
||||||
|
func (g *GitClient) MarkResolved(path string, files ...string) (string, error) {
|
||||||
|
args := append([]string{"add"}, files...)
|
||||||
|
return g.Run(path, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AbortMerge 中止合并 (git merge --abort)
|
||||||
|
func (g *GitClient) AbortMerge(path string) (string, error) {
|
||||||
|
return g.Run(path, "merge", "--abort")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeStatus 检查是否处于合并状态
|
||||||
|
func (g *GitClient) MergeStatus(path string) bool {
|
||||||
|
// 检查 .git/MERGE_HEAD 文件是否存在
|
||||||
|
_, err := g.Run(path, "rev-parse", "--verify", "MERGE_HEAD")
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 提交搜索 ---
|
||||||
|
|
||||||
|
// SearchCommits 搜索提交历史 (git log --grep / --author / --after / --before)
|
||||||
|
func (g *GitClient) SearchCommits(path string, keyword, author string, maxCount int) (string, error) {
|
||||||
|
args := []string{"log"}
|
||||||
|
if maxCount > 0 {
|
||||||
|
args = append(args, fmt.Sprintf("--max-count=%d", maxCount))
|
||||||
|
}
|
||||||
|
if keyword != "" {
|
||||||
|
args = append(args, "--grep="+keyword, "-i")
|
||||||
|
}
|
||||||
|
if author != "" {
|
||||||
|
args = append(args, "--author="+author)
|
||||||
|
}
|
||||||
|
args = append(args, "--format=%H%n%h%n%an%n%ae%n%at%n%s%n---END---")
|
||||||
|
return g.Run(path, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 批量操作 ---
|
||||||
|
|
||||||
|
// QuickStatus 快速获取分支名和是否有变更(轻量级)
|
||||||
|
func (g *GitClient) QuickStatus(path string) (branch string, hasChanges bool, unpushed int, err error) {
|
||||||
|
branchOut, err := g.Branch(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, 0, err
|
||||||
|
}
|
||||||
|
branch = strings.TrimSpace(branchOut)
|
||||||
|
|
||||||
|
// 检查是否有变更
|
||||||
|
statusOut, err := g.Run(path, "status", "--porcelain")
|
||||||
|
if err == nil {
|
||||||
|
hasChanges = strings.TrimSpace(statusOut) != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查未推送提交数
|
||||||
|
unpushedOut, err := g.Run(path, "rev-list", "--count", "origin/"+branch+"..HEAD")
|
||||||
|
if err == nil {
|
||||||
|
fmt.Sscanf(strings.TrimSpace(unpushedOut), "%d", &unpushed)
|
||||||
|
}
|
||||||
|
|
||||||
|
return branch, hasChanges, unpushed, nil
|
||||||
|
}
|
||||||
|
|
||||||
// StatusText 返回变更状态的中文描述
|
// StatusText 返回变更状态的中文描述
|
||||||
func (fc FileChange) StatusText() string {
|
func (fc FileChange) StatusText() string {
|
||||||
switch {
|
switch {
|
||||||
|
|||||||
Reference in New Issue
Block a user