Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ec453cc38 | ||
|
|
3d6874f79e | ||
|
|
d6dbd8834b | ||
|
|
beb40688ce | ||
|
|
5154bb5564 | ||
|
|
af15e86df6 | ||
|
|
6354cbfc94 | ||
|
|
cb6b21ef98 | ||
|
|
6e0becb748 |
@@ -9,7 +9,7 @@ info:
|
|||||||
description: "Git Repository Management Tool"
|
description: "Git Repository Management Tool"
|
||||||
copyright: "(c) 2025, GitPilot"
|
copyright: "(c) 2025, GitPilot"
|
||||||
comments: "GitPilot - Git Repository Manager"
|
comments: "GitPilot - Git Repository Manager"
|
||||||
version: "0.1.0"
|
version: "0.3.1"
|
||||||
|
|
||||||
dev_mode:
|
dev_mode:
|
||||||
root_path: .
|
root_path: .
|
||||||
|
|||||||
@@ -4,17 +4,17 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>Peter Agent</string>
|
<string>GitPilot</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>peter-agent</string>
|
<string>gitpilot</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>com.peteragent.app</string>
|
<string>com.gitpilot.app</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>0.1.0</string>
|
<string>0.2.0</string>
|
||||||
<key>CFBundleGetInfoString</key>
|
<key>CFBundleGetInfoString</key>
|
||||||
<string>AI Agent Application</string>
|
<string>Git Repository Management Tool</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.1.0</string>
|
<string>0.2.0</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>icons</string>
|
<string>icons</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
<string>true</string>
|
<string>true</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>© 2025, Peter Agent</string>
|
<string>© 2025, GitPilot</string>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsLocalNetworking</key>
|
<key>NSAllowsLocalNetworking</key>
|
||||||
|
|||||||
@@ -4,17 +4,17 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>Peter Agent</string>
|
<string>GitPilot</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>peter-agent</string>
|
<string>gitpilot</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>com.peteragent.app</string>
|
<string>com.gitpilot.app</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>0.1.0</string>
|
<string>0.3.1</string>
|
||||||
<key>CFBundleGetInfoString</key>
|
<key>CFBundleGetInfoString</key>
|
||||||
<string>AI Agent Application</string>
|
<string>Git Repository Management Tool</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.1.0</string>
|
<string>0.3.1</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>icons</string>
|
<string>icons</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
@@ -22,6 +22,6 @@
|
|||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
<string>true</string>
|
<string>true</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>© 2025, Peter Agent</string>
|
<string>© 2025, GitPilot</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Version=1.0
|
Version=1.0
|
||||||
Name=Peter Agent
|
Name=GitPilot
|
||||||
Comment=AI Agent Application
|
Comment=Git Repository Management Tool
|
||||||
# The Exec line includes %u to pass the URL to the application
|
# The Exec line includes %u to pass the URL to the application
|
||||||
Exec=/usr/local/bin/peter-agent %u
|
Exec=/usr/local/bin/gitpilot %u
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Icon=peter-agent
|
Icon=gitpilot
|
||||||
Categories=Utility;
|
Categories=Utility;
|
||||||
StartupWMClass=peter-agent
|
StartupWMClass=gitpilot
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"fixed": {
|
"fixed": {
|
||||||
"file_version": "0.1.0"
|
"file_version": "0.3.1"
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"0000": {
|
"0000": {
|
||||||
"ProductVersion": "0.1.0",
|
"ProductVersion": "0.3.1",
|
||||||
"CompanyName": "GitPilot",
|
"CompanyName": "GitPilot",
|
||||||
"FileDescription": "Git Repository Management Tool",
|
"FileDescription": "Git Repository Management Tool",
|
||||||
"LegalCopyright": "© 2025, GitPilot",
|
"LegalCopyright": "© 2025, GitPilot",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
376
frontend/app/components/GitCheck.vue
Normal file
376
frontend/app/components/GitCheck.vue
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import {
|
||||||
|
WarningOutlined,
|
||||||
|
DownloadOutlined,
|
||||||
|
FolderOpenOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
LoadingOutlined,
|
||||||
|
CloseCircleOutlined,
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
import { AppService } from '../../bindings/github.com/zhuy1228/GitPilot/internal/app'
|
||||||
|
import { Events } from '@wailsio/runtime'
|
||||||
|
|
||||||
|
const emit = defineEmits(['ready'])
|
||||||
|
|
||||||
|
const checking = ref(true)
|
||||||
|
const gitInstalled = ref(false)
|
||||||
|
const gitVersion = ref('')
|
||||||
|
const gitPath = ref('')
|
||||||
|
|
||||||
|
// 安装相关
|
||||||
|
const installDir = ref('C:\\Program Files\\Git')
|
||||||
|
const installing = ref(false)
|
||||||
|
const installPhase = ref('') // downloading | installing | done | error
|
||||||
|
const installPercent = ref(0)
|
||||||
|
const installMessage = ref('')
|
||||||
|
|
||||||
|
let unsubscribe = null
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// 监听安装进度事件
|
||||||
|
unsubscribe = Events.On('git-install-progress', (event) => {
|
||||||
|
const data = event.data?.[0] || event.data
|
||||||
|
if (data) {
|
||||||
|
installPhase.value = data.phase
|
||||||
|
installPercent.value = data.percent
|
||||||
|
installMessage.value = data.message
|
||||||
|
if (data.phase === 'done') {
|
||||||
|
installing.value = false
|
||||||
|
gitInstalled.value = true
|
||||||
|
setTimeout(() => emit('ready'), 1500)
|
||||||
|
} else if (data.phase === 'error') {
|
||||||
|
installing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 检查 Git
|
||||||
|
try {
|
||||||
|
const status = await AppService.CheckGitInstalled()
|
||||||
|
gitInstalled.value = status.installed
|
||||||
|
gitVersion.value = status.version || ''
|
||||||
|
gitPath.value = status.path || ''
|
||||||
|
if (status.installed) {
|
||||||
|
emit('ready')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('检查 Git 失败:', e)
|
||||||
|
gitInstalled.value = false
|
||||||
|
} finally {
|
||||||
|
checking.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (unsubscribe) unsubscribe()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function selectInstallDir() {
|
||||||
|
try {
|
||||||
|
const dir = await AppService.SelectGitInstallDir()
|
||||||
|
if (dir) installDir.value = dir
|
||||||
|
} catch (e) {
|
||||||
|
console.error('选择目录失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startInstall() {
|
||||||
|
installing.value = true
|
||||||
|
installPhase.value = 'downloading'
|
||||||
|
installPercent.value = 0
|
||||||
|
installMessage.value = '准备下载...'
|
||||||
|
try {
|
||||||
|
await AppService.InstallGit(installDir.value)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('安装 Git 失败:', e)
|
||||||
|
installPhase.value = 'error'
|
||||||
|
installMessage.value = '安装失败: ' + String(e)
|
||||||
|
installing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- 检查中 -->
|
||||||
|
<div v-if="checking" class="git-check-overlay">
|
||||||
|
<div class="git-check-card">
|
||||||
|
<LoadingOutlined :style="{ fontSize: '32px', color: 'var(--accent, #89b4fa)' }" spin />
|
||||||
|
<div class="git-check-title">正在检查 Git 环境...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Git 未安装 -->
|
||||||
|
<div v-else-if="!gitInstalled" class="git-check-overlay">
|
||||||
|
<div class="git-check-card install-card">
|
||||||
|
<div class="git-check-icon">
|
||||||
|
<WarningOutlined :style="{ fontSize: '48px', color: '#fab387' }" />
|
||||||
|
</div>
|
||||||
|
<div class="git-check-title">未检测到 Git</div>
|
||||||
|
<div class="git-check-desc">GitPilot 需要 Git 才能正常工作,请安装 Git 后使用。</div>
|
||||||
|
|
||||||
|
<!-- 安装进度 -->
|
||||||
|
<template v-if="installing || installPhase === 'done'">
|
||||||
|
<div class="install-progress">
|
||||||
|
<div v-if="installPhase === 'downloading'" class="progress-section">
|
||||||
|
<div class="progress-bar-bg">
|
||||||
|
<div class="progress-bar-fill" :style="{ width: installPercent + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-text">
|
||||||
|
<DownloadOutlined /> {{ installMessage }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="installPhase === 'installing'" class="progress-section">
|
||||||
|
<div class="progress-bar-bg">
|
||||||
|
<div class="progress-bar-fill installing-anim" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-text">
|
||||||
|
<LoadingOutlined spin /> {{ installMessage }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="installPhase === 'done'" class="progress-section done">
|
||||||
|
<CheckCircleOutlined :style="{ fontSize: '24px', color: '#a6e3a1' }" />
|
||||||
|
<div class="progress-text success">{{ installMessage }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="installPhase === 'error'" class="progress-section error">
|
||||||
|
<CloseCircleOutlined :style="{ fontSize: '24px', color: '#f38ba8' }" />
|
||||||
|
<div class="progress-text error-text">{{ installMessage }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 安装表单 -->
|
||||||
|
<template v-else>
|
||||||
|
<div class="install-form">
|
||||||
|
<div class="install-field">
|
||||||
|
<label>安装路径</label>
|
||||||
|
<div class="install-dir-input">
|
||||||
|
<input
|
||||||
|
v-model="installDir"
|
||||||
|
class="dir-input"
|
||||||
|
placeholder="C:\Program Files\Git"
|
||||||
|
/>
|
||||||
|
<button class="dir-btn" @click="selectInstallDir" title="选择目录">
|
||||||
|
<FolderOpenOutlined />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="install-actions">
|
||||||
|
<button class="install-btn primary" @click="startInstall">
|
||||||
|
<DownloadOutlined /> 自动下载并安装 Git
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="install-hint">
|
||||||
|
将从 GitHub 下载 Git for Windows 安装包(约 65MB),需保持网络连接。
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.git-check-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--bg-primary, #1e1e2e);
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-check-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 40px;
|
||||||
|
max-width: 480px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-card {
|
||||||
|
background: var(--bg-secondary, #181825);
|
||||||
|
border: 1px solid var(--border-color, #313244);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 40px 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-check-icon {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-check-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary, #cdd6f4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-check-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-muted, #6c7086);
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-form {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-field label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary, #a6adc8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-dir-input {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--bg-primary, #1e1e2e);
|
||||||
|
border: 1px solid var(--border-color, #313244);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-primary, #cdd6f4);
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: 'Consolas', 'Courier New', monospace;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-input:focus {
|
||||||
|
border-color: var(--accent, #89b4fa);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--bg-primary, #1e1e2e);
|
||||||
|
border: 1px solid var(--border-color, #313244);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--accent, #89b4fa);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-btn:hover {
|
||||||
|
background: var(--bg-hover, #313244);
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-actions {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-color, #313244);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-btn.primary {
|
||||||
|
background: var(--accent, #89b4fa);
|
||||||
|
color: #1e1e2e;
|
||||||
|
border-color: var(--accent, #89b4fa);
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-btn.primary:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted, #6c7086);
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 安装进度 */
|
||||||
|
.install-progress {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-section.done,
|
||||||
|
.progress-section.error {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-bg {
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
background: var(--bg-primary, #1e1e2e);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--accent, #89b4fa);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-fill.installing-anim {
|
||||||
|
background: linear-gradient(90deg, var(--accent, #89b4fa) 0%, #b4befe 50%, var(--accent, #89b4fa) 100%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary, #a6adc8);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text.success {
|
||||||
|
color: #a6e3a1;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text.error-text {
|
||||||
|
color: #f38ba8;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -7,7 +7,16 @@ import {
|
|||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
|
CloudDownloadOutlined,
|
||||||
|
LoadingOutlined,
|
||||||
|
AppstoreOutlined,
|
||||||
|
CloudUploadOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
CloseCircleOutlined,
|
||||||
|
BranchesOutlined,
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-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({
|
||||||
@@ -24,6 +33,24 @@ const selectedKeys = ref([])
|
|||||||
const showAddDialog = ref(false)
|
const showAddDialog = ref(false)
|
||||||
const addForm = ref({ platform: '', username: '', name: '', path: '' })
|
const addForm = ref({ platform: '', username: '', name: '', path: '' })
|
||||||
|
|
||||||
|
// 克隆项目弹窗
|
||||||
|
const showCloneDialog = ref(false)
|
||||||
|
const cloneForm = ref({ platform: '', username: '', repoURL: '', parentDir: '', name: '' })
|
||||||
|
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: {} })
|
||||||
|
|
||||||
@@ -142,6 +169,7 @@ function onContextMenuClick({ key: action }) {
|
|||||||
case 'edit-platform': openEditPlatformDialog(data.key); break
|
case 'edit-platform': openEditPlatformDialog(data.key); break
|
||||||
case 'remove-platform': removePlatform(data.key); break
|
case 'remove-platform': removePlatform(data.key); break
|
||||||
case 'add-project': openAddDialog(data.platformKey, data.username); break
|
case 'add-project': openAddDialog(data.platformKey, data.username); break
|
||||||
|
case 'clone-project': openCloneDialog(data.platformKey, data.username); break
|
||||||
case 'edit-user': openEditUserDialog(data.platformKey, data.username); break
|
case 'edit-user': openEditUserDialog(data.platformKey, data.username); break
|
||||||
case 'remove-user': removeUser(data.platformKey, data.username); break
|
case 'remove-user': removeUser(data.platformKey, data.username); break
|
||||||
case 'remove-project': removeProject(data.platformKey, data.username, data.name); break
|
case 'remove-project': removeProject(data.platformKey, data.username, data.name); break
|
||||||
@@ -166,6 +194,7 @@ const contextMenuItems = computed(() => {
|
|||||||
if (type === 'user') {
|
if (type === 'user') {
|
||||||
return [
|
return [
|
||||||
{ key: 'add-project', label: '添加项目', icon: h(FolderOutlined) },
|
{ key: 'add-project', label: '添加项目', icon: h(FolderOutlined) },
|
||||||
|
{ key: 'clone-project', label: '克隆项目', icon: h(CloudDownloadOutlined) },
|
||||||
{ key: 'edit-user', label: '编辑用户', icon: h(EditOutlined) },
|
{ key: 'edit-user', label: '编辑用户', icon: h(EditOutlined) },
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{ key: 'remove-user', label: '删除用户', danger: true, icon: h(DeleteOutlined) },
|
{ key: 'remove-user', label: '删除用户', danger: true, icon: h(DeleteOutlined) },
|
||||||
@@ -174,6 +203,7 @@ const contextMenuItems = computed(() => {
|
|||||||
if (type === 'project') {
|
if (type === 'project') {
|
||||||
return [
|
return [
|
||||||
{ key: 'add-project', label: '添加项目', icon: h(FolderOutlined) },
|
{ key: 'add-project', label: '添加项目', icon: h(FolderOutlined) },
|
||||||
|
{ key: 'clone-project', label: '克隆项目', icon: h(CloudDownloadOutlined) },
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{ key: 'remove-project', label: '删除项目', danger: true, icon: h(DeleteOutlined) },
|
{ key: 'remove-project', label: '删除项目', danger: true, icon: h(DeleteOutlined) },
|
||||||
]
|
]
|
||||||
@@ -320,6 +350,57 @@ async function addProject() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 克隆项目 ---
|
||||||
|
function openCloneDialog(platformKey, username) {
|
||||||
|
cloneForm.value = { platform: platformKey, username: username, repoURL: '', parentDir: '', name: '' }
|
||||||
|
showCloneDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pickCloneDirectory() {
|
||||||
|
try {
|
||||||
|
const path = await AppService.SelectDirectory()
|
||||||
|
if (path) {
|
||||||
|
cloneForm.value.parentDir = path
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('选择文件夹失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRepoURLChange() {
|
||||||
|
// 从仓库 URL 自动提取项目名称
|
||||||
|
if (!cloneForm.value.name && cloneForm.value.repoURL) {
|
||||||
|
const url = cloneForm.value.repoURL.trim()
|
||||||
|
const match = url.match(/\/([^\/]+?)(\.git)?$/)
|
||||||
|
if (match) {
|
||||||
|
cloneForm.value.name = match[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cloneProject() {
|
||||||
|
if (!cloneForm.value.repoURL || !cloneForm.value.parentDir || !cloneForm.value.name) return
|
||||||
|
cloneLoading.value = true
|
||||||
|
try {
|
||||||
|
await AppService.CloneProject(
|
||||||
|
cloneForm.value.platform,
|
||||||
|
cloneForm.value.username,
|
||||||
|
cloneForm.value.repoURL,
|
||||||
|
cloneForm.value.parentDir,
|
||||||
|
cloneForm.value.name
|
||||||
|
)
|
||||||
|
showCloneDialog.value = false
|
||||||
|
message.success('克隆成功')
|
||||||
|
await loadTree()
|
||||||
|
emit('tree-updated')
|
||||||
|
} catch (e) {
|
||||||
|
console.error('克隆项目失败:', e)
|
||||||
|
message.error('克隆失败: ' + String(e))
|
||||||
|
} finally {
|
||||||
|
cloneLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function removeProject(platform, username, name) {
|
async function removeProject(platform, username, name) {
|
||||||
if (!confirm(`确定删除项目 "${name}" 吗?`)) return
|
if (!confirm(`确定删除项目 "${name}" 吗?`)) return
|
||||||
try {
|
try {
|
||||||
@@ -331,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, 'origin')
|
||||||
|
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, 'origin')
|
||||||
|
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>`,
|
||||||
@@ -353,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">
|
||||||
@@ -441,6 +600,53 @@ onMounted(() => {
|
|||||||
</a-form>
|
</a-form>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 克隆项目弹窗 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="showCloneDialog"
|
||||||
|
title="克隆项目"
|
||||||
|
@ok="cloneProject"
|
||||||
|
:ok-text="cloneLoading ? '克隆中...' : '克隆'"
|
||||||
|
cancel-text="取消"
|
||||||
|
:ok-button-props="{ loading: cloneLoading, disabled: !cloneForm.repoURL || !cloneForm.parentDir || !cloneForm.name }"
|
||||||
|
:closable="!cloneLoading"
|
||||||
|
:maskClosable="!cloneLoading"
|
||||||
|
:width="480"
|
||||||
|
>
|
||||||
|
<a-form layout="vertical" :style="{ marginTop: '16px' }">
|
||||||
|
<a-form-item label="平台">
|
||||||
|
<a-input :value="cloneForm.platform" disabled />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="用户">
|
||||||
|
<a-input :value="cloneForm.username" disabled />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="仓库地址" required>
|
||||||
|
<a-input
|
||||||
|
v-model:value="cloneForm.repoURL"
|
||||||
|
placeholder="https://github.com/user/repo.git"
|
||||||
|
@blur="onRepoURLChange"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="克隆到目录" required>
|
||||||
|
<a-input v-model:value="cloneForm.parentDir" placeholder="选择父目录" read-only>
|
||||||
|
<template #suffix>
|
||||||
|
<FolderOpenOutlined
|
||||||
|
style="cursor: pointer; color: var(--accent, #89b4fa);"
|
||||||
|
title="选择文件夹"
|
||||||
|
@click="pickCloneDirectory"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="项目名称" required>
|
||||||
|
<a-input v-model:value="cloneForm.name" placeholder="自动从仓库地址提取" />
|
||||||
|
</a-form-item>
|
||||||
|
<div v-if="cloneForm.parentDir && cloneForm.name" style="font-size: 12px; color: var(--text-muted); margin-top: -8px; margin-bottom: 8px;">
|
||||||
|
<span>目标路径:</span>
|
||||||
|
<span style="color: var(--accent, #89b4fa);">{{ cloneForm.parentDir }}/{{ cloneForm.name }}</span>
|
||||||
|
</div>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
<!-- 平台弹窗 -->
|
<!-- 平台弹窗 -->
|
||||||
<a-modal
|
<a-modal
|
||||||
v-model:open="showPlatformDialog"
|
v-model:open="showPlatformDialog"
|
||||||
@@ -491,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>
|
||||||
|
|
||||||
@@ -617,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>
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
import { ref, onBeforeUnmount } from 'vue'
|
import { ref, onBeforeUnmount } from 'vue'
|
||||||
|
|
||||||
const selectedProject = ref(null)
|
const selectedProject = ref(null)
|
||||||
|
const gitReady = ref(false)
|
||||||
|
|
||||||
|
function onGitReady() {
|
||||||
|
gitReady.value = true
|
||||||
|
}
|
||||||
|
|
||||||
function onSelectProject(project) {
|
function onSelectProject(project) {
|
||||||
selectedProject.value = project
|
selectedProject.value = project
|
||||||
@@ -43,7 +48,11 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="main-layout">
|
<!-- Git 环境检查 -->
|
||||||
|
<GitCheck v-if="!gitReady" @ready="onGitReady" />
|
||||||
|
|
||||||
|
<!-- 主界面 -->
|
||||||
|
<div v-else class="main-layout">
|
||||||
<Sidebar :selected-project="selectedProject" :style="{ width: sidebarWidth + 'px' }" @select-project="onSelectProject" />
|
<Sidebar :selected-project="selectedProject" :style="{ width: sidebarWidth + 'px' }" @select-project="onSelectProject" />
|
||||||
<div class="resize-handle" @mousedown="startSidebarResize"></div>
|
<div class="resize-handle" @mousedown="startSidebarResize"></div>
|
||||||
<ContentArea :project="selectedProject" />
|
<ContentArea :project="selectedProject" />
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// @ts-check
|
||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
export {
|
||||||
|
Settings
|
||||||
|
} from "./models.js";
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// @ts-check
|
||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import { Create as $Create } from "@wailsio/runtime";
|
||||||
|
|
||||||
|
export class Settings {
|
||||||
|
/**
|
||||||
|
* Creates a new Settings instance.
|
||||||
|
* @param {Partial<Settings>} [$$source = {}] - The source object to create the Settings.
|
||||||
|
*/
|
||||||
|
constructor($$source = {}) {
|
||||||
|
if (!("Concurrency" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this["Concurrency"] = 0;
|
||||||
|
}
|
||||||
|
if (!("NetworkCheck" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
this["NetworkCheck"] = false;
|
||||||
|
}
|
||||||
|
if (!("LogLevel" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["LogLevel"] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Settings instance from a string or object.
|
||||||
|
* @param {any} [$$source = {}]
|
||||||
|
* @returns {Settings}
|
||||||
|
*/
|
||||||
|
static createFrom($$source = {}) {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new Settings(/** @type {Partial<Settings>} */($$parsedSource));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,11 +14,23 @@ import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Cr
|
|||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore: Unused imports
|
// @ts-ignore: Unused imports
|
||||||
import * as application$0 from "../../../../wailsapp/wails/v3/pkg/application/models.js";
|
import * as application$0 from "../../../../wailsapp/wails/v3/pkg/application/models.js";
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import * as config$0 from "../../config/models.js";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @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
|
||||||
@@ -41,6 +53,17 @@ export function AddProject(platform, username, name, path) {
|
|||||||
return $Call.ByID(2299402672, platform, username, name, path);
|
return $Call.ByID(2299402672, platform, username, name, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AddRemote 添加远程仓库
|
||||||
|
* @param {string} path
|
||||||
|
* @param {string} name
|
||||||
|
* @param {string} url
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
|
*/
|
||||||
|
export function AddRemote(path, name, url) {
|
||||||
|
return $Call.ByID(3697916441, path, name, url);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AddUser 添加用户到指定平台
|
* AddUser 添加用户到指定平台
|
||||||
* @param {string} platform
|
* @param {string} platform
|
||||||
@@ -52,6 +75,63 @@ export function AddUser(platform, username, token) {
|
|||||||
return $Call.ByID(2264426704, platform, username, token);
|
return $Call.ByID(2264426704, platform, username, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BatchPull 批量拉取指定项目
|
||||||
|
* @param {string[]} paths
|
||||||
|
* @param {string} remote
|
||||||
|
* @returns {$CancellablePromise<$models.BatchPullResult[]>}
|
||||||
|
*/
|
||||||
|
export function BatchPull(paths, remote) {
|
||||||
|
return $Call.ByID(758996647, paths, remote).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType1($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BatchPush 批量推送指定项目
|
||||||
|
* @param {string[]} paths
|
||||||
|
* @param {string} remote
|
||||||
|
* @returns {$CancellablePromise<$models.BatchPullResult[]>}
|
||||||
|
*/
|
||||||
|
export function BatchPush(paths, remote) {
|
||||||
|
return $Call.ByID(794082076, paths, remote).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType1($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CheckGitInstalled 检查 Git 是否已安装
|
||||||
|
* @returns {$CancellablePromise<$models.GitStatus>}
|
||||||
|
*/
|
||||||
|
export function CheckGitInstalled() {
|
||||||
|
return $Call.ByID(464620426).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType2($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CheckoutRemoteBranch 检出远程分支到本地
|
||||||
|
* @param {string} path
|
||||||
|
* @param {string} remoteBranch
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
|
*/
|
||||||
|
export function CheckoutRemoteBranch(path, remoteBranch) {
|
||||||
|
return $Call.ByID(3682237522, path, remoteBranch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CloneProject 克隆远程仓库到本地目录,并添加到项目树
|
||||||
|
* @param {string} platform
|
||||||
|
* @param {string} username
|
||||||
|
* @param {string} repoURL
|
||||||
|
* @param {string} parentDir
|
||||||
|
* @param {string} name
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
|
*/
|
||||||
|
export function CloneProject(platform, username, repoURL, parentDir, name) {
|
||||||
|
return $Call.ByID(2347019414, platform, username, repoURL, parentDir, name);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CommitChanges 提交已暂存的更改
|
* CommitChanges 提交已暂存的更改
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
@@ -62,6 +142,16 @@ export function CommitChanges(path, message) {
|
|||||||
return $Call.ByID(921984546, path, message);
|
return $Call.ByID(921984546, path, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CreateBranch 创建新分支
|
||||||
|
* @param {string} path
|
||||||
|
* @param {string} name
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
|
*/
|
||||||
|
export function CreateBranch(path, name) {
|
||||||
|
return $Call.ByID(1422152924, path, name);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CreateTag 创建标签
|
* CreateTag 创建标签
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
@@ -73,14 +163,37 @@ export function CreateTag(path, name, message) {
|
|||||||
return $Call.ByID(4209616956, path, name, message);
|
return $Call.ByID(4209616956, path, name, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DeleteBranch 删除本地分支
|
||||||
|
* @param {string} path
|
||||||
|
* @param {string} name
|
||||||
|
* @param {boolean} force
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
|
*/
|
||||||
|
export function DeleteBranch(path, name, force) {
|
||||||
|
return $Call.ByID(580272035, path, name, force);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DeleteRemoteBranch 删除远程分支
|
||||||
|
* @param {string} path
|
||||||
|
* @param {string} branch
|
||||||
|
* @param {string} remote
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
|
*/
|
||||||
|
export function DeleteRemoteBranch(path, branch, remote) {
|
||||||
|
return $Call.ByID(2626609049, path, branch, remote);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DeleteTag 删除标签(本地+远程)
|
* DeleteTag 删除标签(本地+远程)
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
|
* @param {string} remote
|
||||||
* @returns {$CancellablePromise<void>}
|
* @returns {$CancellablePromise<void>}
|
||||||
*/
|
*/
|
||||||
export function DeleteTag(path, name) {
|
export function DeleteTag(path, name, remote) {
|
||||||
return $Call.ByID(873653241, path, name);
|
return $Call.ByID(873653241, path, name, remote);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,12 +207,33 @@ export function DiscardFiles(path, files) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FetchProject 拉取远程信息
|
* FetchProject 拉取远程信息(指定 remote,空则 fetch --all)
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
|
* @param {string} remote
|
||||||
* @returns {$CancellablePromise<string>}
|
* @returns {$CancellablePromise<string>}
|
||||||
*/
|
*/
|
||||||
export function FetchProject(path) {
|
export function FetchProject(path, remote) {
|
||||||
return $Call.ByID(3541106829, path);
|
return $Call.ByID(3541106829, path, remote);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetAllProjectOverview 获取所有项目的概览状态
|
||||||
|
* @returns {$CancellablePromise<$models.ProjectOverview[]>}
|
||||||
|
*/
|
||||||
|
export function GetAllProjectOverview() {
|
||||||
|
return $Call.ByID(2453130151).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType4($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetAppSettings 获取应用设置
|
||||||
|
* @returns {$CancellablePromise<config$0.Settings | null>}
|
||||||
|
*/
|
||||||
|
export function GetAppSettings() {
|
||||||
|
return $Call.ByID(428589026).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType6($result);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,7 +243,7 @@ export function FetchProject(path) {
|
|||||||
*/
|
*/
|
||||||
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 $$createType1($result);
|
return $$createType8($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +276,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 $$createType3($result);
|
return $$createType10($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +288,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 $$createType5($result);
|
return $$createType12($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 $$createType14($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +343,16 @@ export function GetFileDiffStaged(projectPath, filePath) {
|
|||||||
return $Call.ByID(2773256455, projectPath, filePath);
|
return $Call.ByID(2773256455, projectPath, filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetGitGlobalConfig 获取 git 全局配置
|
||||||
|
* @returns {$CancellablePromise<$models.GitConfig | null>}
|
||||||
|
*/
|
||||||
|
export function GetGitGlobalConfig() {
|
||||||
|
return $Call.ByID(154811497).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType16($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GetPlatformInfo 获取平台信息
|
* GetPlatformInfo 获取平台信息
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
@@ -195,7 +360,7 @@ export function GetFileDiffStaged(projectPath, filePath) {
|
|||||||
*/
|
*/
|
||||||
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 $$createType7($result);
|
return $$createType18($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +371,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 $$createType9($result);
|
return $$createType20($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +382,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 $$createType11($result);
|
return $$createType22($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +392,41 @@ 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 $$createType13($result);
|
return $$createType24($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetRemoteBranches 获取远程分支列表
|
||||||
|
* @param {string} path
|
||||||
|
* @param {string} remote
|
||||||
|
* @returns {$CancellablePromise<$models.BranchInfo[]>}
|
||||||
|
*/
|
||||||
|
export function GetRemoteBranches(path, remote) {
|
||||||
|
return $Call.ByID(994796370, path, remote).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType8($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetRemotes 获取项目所有远程仓库列表
|
||||||
|
* @param {string} path
|
||||||
|
* @returns {$CancellablePromise<$models.RemoteItem[]>}
|
||||||
|
*/
|
||||||
|
export function GetRemotes(path) {
|
||||||
|
return $Call.ByID(975520027, path).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType26($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetStashList 获取贮藏列表
|
||||||
|
* @param {string} path
|
||||||
|
* @returns {$CancellablePromise<$models.StashInfo[]>}
|
||||||
|
*/
|
||||||
|
export function GetStashList(path) {
|
||||||
|
return $Call.ByID(1948257945, path).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType28($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +437,7 @@ export function GetProjectTree() {
|
|||||||
*/
|
*/
|
||||||
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 $$createType15($result);
|
return $$createType30($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,36 +449,68 @@ 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 $$createType17($result);
|
return $$createType32($result);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PullProject 拉取项目(当前分支)
|
* InstallGit 下载并安装 Git
|
||||||
* @param {string} path
|
* installDir: 用户选择的安装目录(Windows 有效)
|
||||||
* @returns {$CancellablePromise<string>}
|
* @param {string} installDir
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
*/
|
*/
|
||||||
export function PullProject(path) {
|
export function InstallGit(installDir) {
|
||||||
return $Call.ByID(4145813996, path);
|
return $Call.ByID(1052641569, installDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PushProject 推送项目(当前分支)
|
* IsMerging 检查是否处于合并状态
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
|
* @returns {$CancellablePromise<boolean>}
|
||||||
|
*/
|
||||||
|
export function IsMerging(path) {
|
||||||
|
return $Call.ByID(693809703, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MergeBranch 合并指定分支到当前分支
|
||||||
|
* @param {string} path
|
||||||
|
* @param {string} branch
|
||||||
* @returns {$CancellablePromise<string>}
|
* @returns {$CancellablePromise<string>}
|
||||||
*/
|
*/
|
||||||
export function PushProject(path) {
|
export function MergeBranch(path, branch) {
|
||||||
return $Call.ByID(3887901113, path);
|
return $Call.ByID(278161076, path, branch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PullProject 拉取项目(当前分支,指定 remote)
|
||||||
|
* @param {string} path
|
||||||
|
* @param {string} remote
|
||||||
|
* @returns {$CancellablePromise<string>}
|
||||||
|
*/
|
||||||
|
export function PullProject(path, remote) {
|
||||||
|
return $Call.ByID(4145813996, path, remote);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PushProject 推送项目(当前分支,指定 remote)
|
||||||
|
* @param {string} path
|
||||||
|
* @param {string} remote
|
||||||
|
* @returns {$CancellablePromise<string>}
|
||||||
|
*/
|
||||||
|
export function PushProject(path, remote) {
|
||||||
|
return $Call.ByID(3887901113, path, remote);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PushTag 推送标签到远程
|
* PushTag 推送标签到远程
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
|
* @param {string} remote
|
||||||
* @returns {$CancellablePromise<void>}
|
* @returns {$CancellablePromise<void>}
|
||||||
*/
|
*/
|
||||||
export function PushTag(path, name) {
|
export function PushTag(path, name, remote) {
|
||||||
return $Call.ByID(1424810524, path, name);
|
return $Call.ByID(1424810524, path, name, remote);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -302,6 +533,16 @@ export function RemoveProject(platform, username, name) {
|
|||||||
return $Call.ByID(2748109581, platform, username, name);
|
return $Call.ByID(2748109581, platform, username, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RemoveRemote 删除远程仓库
|
||||||
|
* @param {string} path
|
||||||
|
* @param {string} name
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
|
*/
|
||||||
|
export function RemoveRemote(path, name) {
|
||||||
|
return $Call.ByID(2915623022, path, name);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RemoveUser 从平台删除用户
|
* RemoveUser 从平台删除用户
|
||||||
* @param {string} platform
|
* @param {string} platform
|
||||||
@@ -324,6 +565,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
|
||||||
@@ -334,6 +585,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 $$createType12($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SelectDirectory 打开系统文件夹选择器,返回选中的路径
|
* SelectDirectory 打开系统文件夹选择器,返回选中的路径
|
||||||
* @returns {$CancellablePromise<string>}
|
* @returns {$CancellablePromise<string>}
|
||||||
@@ -342,6 +618,14 @@ export function SelectDirectory() {
|
|||||||
return $Call.ByID(2318416763);
|
return $Call.ByID(2318416763);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SelectGitInstallDir 打开文件夹选择器让用户选择 Git 安装路径
|
||||||
|
* @returns {$CancellablePromise<string>}
|
||||||
|
*/
|
||||||
|
export function SelectGitInstallDir() {
|
||||||
|
return $Call.ByID(2941074074);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {application$0.App | null} app
|
* @param {application$0.App | null} app
|
||||||
* @returns {$CancellablePromise<void>}
|
* @returns {$CancellablePromise<void>}
|
||||||
@@ -350,6 +634,16 @@ export function SetApplication(app) {
|
|||||||
return $Call.ByID(383695022, app);
|
return $Call.ByID(383695022, app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SetGitGlobalConfig 设置 git 全局配置
|
||||||
|
* @param {string} name
|
||||||
|
* @param {string} email
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
|
*/
|
||||||
|
export function SetGitGlobalConfig(name, email) {
|
||||||
|
return $Call.ByID(3875064981, name, email);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* StageAll 暂存所有变更文件
|
* StageAll 暂存所有变更文件
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
@@ -369,6 +663,46 @@ export function StageFiles(path, files) {
|
|||||||
return $Call.ByID(4064506881, path, files);
|
return $Call.ByID(4064506881, path, files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StashApply 应用贮藏(不删除)
|
||||||
|
* @param {string} path
|
||||||
|
* @param {number} index
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
|
*/
|
||||||
|
export function StashApply(path, index) {
|
||||||
|
return $Call.ByID(721583959, path, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StashDrop 删除贮藏
|
||||||
|
* @param {string} path
|
||||||
|
* @param {number} index
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
|
*/
|
||||||
|
export function StashDrop(path, index) {
|
||||||
|
return $Call.ByID(3821556536, path, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StashPop 应用贮藏并删除
|
||||||
|
* @param {string} path
|
||||||
|
* @param {number} index
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
|
*/
|
||||||
|
export function StashPop(path, index) {
|
||||||
|
return $Call.ByID(4117964460, path, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StashSave 保存当前变更到贮藏
|
||||||
|
* @param {string} path
|
||||||
|
* @param {string} message
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
|
*/
|
||||||
|
export function StashSave(path, message) {
|
||||||
|
return $Call.ByID(1178263140, path, message);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SwitchBranch 切换分支
|
* SwitchBranch 切换分支
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
@@ -398,6 +732,15 @@ export function UnstageFiles(path, files) {
|
|||||||
return $Call.ByID(3546473046, path, files);
|
return $Call.ByID(3546473046, path, files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateAppSettings 更新应用设置
|
||||||
|
* @param {string} logLevel
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
|
*/
|
||||||
|
export function UpdateAppSettings(logLevel) {
|
||||||
|
return $Call.ByID(718820989, logLevel);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UpdatePlatform 修改平台信息(base_url)
|
* UpdatePlatform 修改平台信息(base_url)
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
@@ -421,21 +764,36 @@ export function UpdateUser(platform, oldUsername, newUsername, token) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Private type creation functions
|
// Private type creation functions
|
||||||
const $$createType0 = $models.BranchInfo.createFrom;
|
const $$createType0 = $models.BatchPullResult.createFrom;
|
||||||
const $$createType1 = $Create.Array($$createType0);
|
const $$createType1 = $Create.Array($$createType0);
|
||||||
const $$createType2 = $models.CommitFileInfo.createFrom;
|
const $$createType2 = $models.GitStatus.createFrom;
|
||||||
const $$createType3 = $Create.Array($$createType2);
|
const $$createType3 = $models.ProjectOverview.createFrom;
|
||||||
const $$createType4 = $models.CommitLog.createFrom;
|
const $$createType4 = $Create.Array($$createType3);
|
||||||
const $$createType5 = $Create.Array($$createType4);
|
const $$createType5 = config$0.Settings.createFrom;
|
||||||
const $$createType6 = $models.PlatformInfo.createFrom;
|
const $$createType6 = $Create.Nullable($$createType5);
|
||||||
const $$createType7 = $Create.Nullable($$createType6);
|
const $$createType7 = $models.BranchInfo.createFrom;
|
||||||
const $$createType8 = $models.FileInfo.createFrom;
|
const $$createType8 = $Create.Array($$createType7);
|
||||||
const $$createType9 = $Create.Array($$createType8);
|
const $$createType9 = $models.CommitFileInfo.createFrom;
|
||||||
const $$createType10 = $models.ProjectStatus.createFrom;
|
const $$createType10 = $Create.Array($$createType9);
|
||||||
const $$createType11 = $Create.Nullable($$createType10);
|
const $$createType11 = $models.CommitLog.createFrom;
|
||||||
const $$createType12 = $models.TreeNode.createFrom;
|
const $$createType12 = $Create.Array($$createType11);
|
||||||
const $$createType13 = $Create.Array($$createType12);
|
const $$createType13 = $models.ConflictFileInfo.createFrom;
|
||||||
const $$createType14 = $models.TagInfo.createFrom;
|
const $$createType14 = $Create.Array($$createType13);
|
||||||
const $$createType15 = $Create.Array($$createType14);
|
const $$createType15 = $models.GitConfig.createFrom;
|
||||||
const $$createType16 = $models.UserInfo.createFrom;
|
const $$createType16 = $Create.Nullable($$createType15);
|
||||||
const $$createType17 = $Create.Nullable($$createType16);
|
const $$createType17 = $models.PlatformInfo.createFrom;
|
||||||
|
const $$createType18 = $Create.Nullable($$createType17);
|
||||||
|
const $$createType19 = $models.FileInfo.createFrom;
|
||||||
|
const $$createType20 = $Create.Array($$createType19);
|
||||||
|
const $$createType21 = $models.ProjectStatus.createFrom;
|
||||||
|
const $$createType22 = $Create.Nullable($$createType21);
|
||||||
|
const $$createType23 = $models.TreeNode.createFrom;
|
||||||
|
const $$createType24 = $Create.Array($$createType23);
|
||||||
|
const $$createType25 = $models.RemoteItem.createFrom;
|
||||||
|
const $$createType26 = $Create.Array($$createType25);
|
||||||
|
const $$createType27 = $models.StashInfo.createFrom;
|
||||||
|
const $$createType28 = $Create.Array($$createType27);
|
||||||
|
const $$createType29 = $models.TagInfo.createFrom;
|
||||||
|
const $$createType30 = $Create.Array($$createType29);
|
||||||
|
const $$createType31 = $models.UserInfo.createFrom;
|
||||||
|
const $$createType32 = $Create.Nullable($$createType31);
|
||||||
|
|||||||
@@ -8,12 +8,19 @@ export {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
BatchPullResult,
|
||||||
BranchInfo,
|
BranchInfo,
|
||||||
CommitFileInfo,
|
CommitFileInfo,
|
||||||
CommitLog,
|
CommitLog,
|
||||||
|
ConflictFileInfo,
|
||||||
FileInfo,
|
FileInfo,
|
||||||
|
GitConfig,
|
||||||
|
GitStatus,
|
||||||
PlatformInfo,
|
PlatformInfo,
|
||||||
|
ProjectOverview,
|
||||||
ProjectStatus,
|
ProjectStatus,
|
||||||
|
RemoteItem,
|
||||||
|
StashInfo,
|
||||||
TagInfo,
|
TagInfo,
|
||||||
TreeNode,
|
TreeNode,
|
||||||
UserInfo
|
UserInfo
|
||||||
|
|||||||
@@ -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 文件信息
|
||||||
*/
|
*/
|
||||||
@@ -207,6 +290,89 @@ export class FileInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GitConfig Git 全局配置
|
||||||
|
*/
|
||||||
|
export class GitConfig {
|
||||||
|
/**
|
||||||
|
* Creates a new GitConfig instance.
|
||||||
|
* @param {Partial<GitConfig>} [$$source = {}] - The source object to create the GitConfig.
|
||||||
|
*/
|
||||||
|
constructor($$source = {}) {
|
||||||
|
if (!("userName" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["userName"] = "";
|
||||||
|
}
|
||||||
|
if (!("userEmail" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["userEmail"] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new GitConfig instance from a string or object.
|
||||||
|
* @param {any} [$$source = {}]
|
||||||
|
* @returns {GitConfig}
|
||||||
|
*/
|
||||||
|
static createFrom($$source = {}) {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new GitConfig(/** @type {Partial<GitConfig>} */($$parsedSource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GitStatus Git 安装状态
|
||||||
|
*/
|
||||||
|
export class GitStatus {
|
||||||
|
/**
|
||||||
|
* Creates a new GitStatus instance.
|
||||||
|
* @param {Partial<GitStatus>} [$$source = {}] - The source object to create the GitStatus.
|
||||||
|
*/
|
||||||
|
constructor($$source = {}) {
|
||||||
|
if (!("installed" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
this["installed"] = false;
|
||||||
|
}
|
||||||
|
if (!("version" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["version"] = "";
|
||||||
|
}
|
||||||
|
if (!("path" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["path"] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new GitStatus instance from a string or object.
|
||||||
|
* @param {any} [$$source = {}]
|
||||||
|
* @returns {GitStatus}
|
||||||
|
*/
|
||||||
|
static createFrom($$source = {}) {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new GitStatus(/** @type {Partial<GitStatus>} */($$parsedSource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PlatformInfo 平台信息
|
* PlatformInfo 平台信息
|
||||||
*/
|
*/
|
||||||
@@ -245,6 +411,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 项目状态信息
|
||||||
*/
|
*/
|
||||||
@@ -268,6 +507,20 @@ export class ProjectStatus {
|
|||||||
*/
|
*/
|
||||||
this["remoteUrl"] = "";
|
this["remoteUrl"] = "";
|
||||||
}
|
}
|
||||||
|
if (!("remotes" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {RemoteItem[]}
|
||||||
|
*/
|
||||||
|
this["remotes"] = [];
|
||||||
|
}
|
||||||
|
if (!("currentRemote" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["currentRemote"] = "";
|
||||||
|
}
|
||||||
if (!("changedFiles" in $$source)) {
|
if (!("changedFiles" in $$source)) {
|
||||||
/**
|
/**
|
||||||
* @member
|
* @member
|
||||||
@@ -286,14 +539,108 @@ export class ProjectStatus {
|
|||||||
*/
|
*/
|
||||||
static createFrom($$source = {}) {
|
static createFrom($$source = {}) {
|
||||||
const $$createField2_0 = $$createType1;
|
const $$createField2_0 = $$createType1;
|
||||||
|
const $$createField4_0 = $$createType3;
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
if ("remotes" in $$parsedSource) {
|
||||||
|
$$parsedSource["remotes"] = $$createField2_0($$parsedSource["remotes"]);
|
||||||
|
}
|
||||||
if ("changedFiles" in $$parsedSource) {
|
if ("changedFiles" in $$parsedSource) {
|
||||||
$$parsedSource["changedFiles"] = $$createField2_0($$parsedSource["changedFiles"]);
|
$$parsedSource["changedFiles"] = $$createField4_0($$parsedSource["changedFiles"]);
|
||||||
}
|
}
|
||||||
return new ProjectStatus(/** @type {Partial<ProjectStatus>} */($$parsedSource));
|
return new ProjectStatus(/** @type {Partial<ProjectStatus>} */($$parsedSource));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RemoteItem 远程仓库信息
|
||||||
|
*/
|
||||||
|
export class RemoteItem {
|
||||||
|
/**
|
||||||
|
* Creates a new RemoteItem instance.
|
||||||
|
* @param {Partial<RemoteItem>} [$$source = {}] - The source object to create the RemoteItem.
|
||||||
|
*/
|
||||||
|
constructor($$source = {}) {
|
||||||
|
if (!("name" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["name"] = "";
|
||||||
|
}
|
||||||
|
if (!("url" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["url"] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new RemoteItem instance from a string or object.
|
||||||
|
* @param {any} [$$source = {}]
|
||||||
|
* @returns {RemoteItem}
|
||||||
|
*/
|
||||||
|
static createFrom($$source = {}) {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new RemoteItem(/** @type {Partial<RemoteItem>} */($$parsedSource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StashInfo 贮藏信息
|
||||||
|
*/
|
||||||
|
export class StashInfo {
|
||||||
|
/**
|
||||||
|
* Creates a new StashInfo instance.
|
||||||
|
* @param {Partial<StashInfo>} [$$source = {}] - The source object to create the StashInfo.
|
||||||
|
*/
|
||||||
|
constructor($$source = {}) {
|
||||||
|
if (!("index" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this["index"] = 0;
|
||||||
|
}
|
||||||
|
if (!("ref" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["ref"] = "";
|
||||||
|
}
|
||||||
|
if (!("message" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["message"] = "";
|
||||||
|
}
|
||||||
|
if (!("timestamp" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this["timestamp"] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new StashInfo instance from a string or object.
|
||||||
|
* @param {any} [$$source = {}]
|
||||||
|
* @returns {StashInfo}
|
||||||
|
*/
|
||||||
|
static createFrom($$source = {}) {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new StashInfo(/** @type {Partial<StashInfo>} */($$parsedSource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TagInfo 标签信息
|
* TagInfo 标签信息
|
||||||
*/
|
*/
|
||||||
@@ -401,7 +748,7 @@ export class TreeNode {
|
|||||||
* @returns {TreeNode}
|
* @returns {TreeNode}
|
||||||
*/
|
*/
|
||||||
static createFrom($$source = {}) {
|
static createFrom($$source = {}) {
|
||||||
const $$createField4_0 = $$createType3;
|
const $$createField4_0 = $$createType5;
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
if ("children" in $$parsedSource) {
|
if ("children" in $$parsedSource) {
|
||||||
$$parsedSource["children"] = $$createField4_0($$parsedSource["children"]);
|
$$parsedSource["children"] = $$createField4_0($$parsedSource["children"]);
|
||||||
@@ -449,7 +796,9 @@ export class UserInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Private type creation functions
|
// Private type creation functions
|
||||||
const $$createType0 = FileInfo.createFrom;
|
const $$createType0 = RemoteItem.createFrom;
|
||||||
const $$createType1 = $Create.Array($$createType0);
|
const $$createType1 = $Create.Array($$createType0);
|
||||||
const $$createType2 = TreeNode.createFrom;
|
const $$createType2 = FileInfo.createFrom;
|
||||||
const $$createType3 = $Create.Array($$createType2);
|
const $$createType3 = $Create.Array($$createType2);
|
||||||
|
const $$createType4 = TreeNode.createFrom;
|
||||||
|
const $$createType5 = $Create.Array($$createType4);
|
||||||
|
|||||||
241
internal/app/gitsetup.go
Normal file
241
internal/app/gitsetup.go
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zhuy1228/GitPilot/internal/git"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GitStatus Git 安装状态
|
||||||
|
type GitStatus struct {
|
||||||
|
Installed bool `json:"installed"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GitInstallProgress 安装进度
|
||||||
|
type GitInstallProgress struct {
|
||||||
|
Phase string `json:"phase"` // downloading, installing, done, error
|
||||||
|
Percent float64 `json:"percent"` // 下载百分比 0-100
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckGitInstalled 检查 Git 是否已安装
|
||||||
|
func (s *AppService) CheckGitInstalled() GitStatus {
|
||||||
|
path, err := exec.LookPath("git")
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Git 未安装:", err)
|
||||||
|
return GitStatus{Installed: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取版本号
|
||||||
|
cmd := exec.Command("git", "--version")
|
||||||
|
hideWindowCmd(cmd)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("获取 Git 版本失败:", err)
|
||||||
|
return GitStatus{Installed: true, Path: path}
|
||||||
|
}
|
||||||
|
|
||||||
|
version := strings.TrimSpace(string(output))
|
||||||
|
// "git version 2.43.0.windows.1" -> "2.43.0"
|
||||||
|
version = strings.TrimPrefix(version, "git version ")
|
||||||
|
if idx := strings.Index(version, ".windows"); idx > 0 {
|
||||||
|
version = version[:idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
return GitStatus{
|
||||||
|
Installed: true,
|
||||||
|
Version: version,
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectGitInstallDir 打开文件夹选择器让用户选择 Git 安装路径
|
||||||
|
func (s *AppService) SelectGitInstallDir() (string, error) {
|
||||||
|
if s.app == nil {
|
||||||
|
return "", fmt.Errorf("应用未初始化")
|
||||||
|
}
|
||||||
|
path, err := s.app.Dialog.OpenFile().
|
||||||
|
CanChooseDirectories(true).
|
||||||
|
CanChooseFiles(false).
|
||||||
|
SetTitle("选择 Git 安装路径").
|
||||||
|
PromptForSingleSelection()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("打开文件夹选择器失败: %w", err)
|
||||||
|
}
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getGitDownloadURL 根据平台返回 Git 下载地址
|
||||||
|
func getGitDownloadURL() string {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
if runtime.GOARCH == "arm64" {
|
||||||
|
return "https://github.com/git-for-windows/git/releases/download/v2.49.0.windows.1/Git-2.49.0-arm64.exe"
|
||||||
|
}
|
||||||
|
return "https://github.com/git-for-windows/git/releases/download/v2.49.0.windows.1/Git-2.49.0-64-bit.exe"
|
||||||
|
case "darwin":
|
||||||
|
return "" // macOS 建议使用 brew 或 Xcode CLI
|
||||||
|
default:
|
||||||
|
return "" // Linux 建议使用包管理器
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallGit 下载并安装 Git
|
||||||
|
// installDir: 用户选择的安装目录(Windows 有效)
|
||||||
|
func (s *AppService) InstallGit(installDir string) error {
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
return fmt.Errorf("自动安装仅支持 Windows,请使用系统包管理器安装 Git")
|
||||||
|
}
|
||||||
|
|
||||||
|
url := getGitDownloadURL()
|
||||||
|
if url == "" {
|
||||||
|
return fmt.Errorf("无法获取 Git 下载地址")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 下载安装包
|
||||||
|
s.emitInstallProgress("downloading", 0, "正在下载 Git 安装包...")
|
||||||
|
log.Println("开始下载 Git:", url)
|
||||||
|
|
||||||
|
tmpDir := os.TempDir()
|
||||||
|
installerPath := filepath.Join(tmpDir, "Git-Installer.exe")
|
||||||
|
|
||||||
|
err := s.downloadFile(url, installerPath)
|
||||||
|
if err != nil {
|
||||||
|
s.emitInstallProgress("error", 0, "下载失败: "+err.Error())
|
||||||
|
return fmt.Errorf("下载 Git 安装包失败: %w", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(installerPath)
|
||||||
|
|
||||||
|
// 2. 静默安装
|
||||||
|
s.emitInstallProgress("installing", 100, "正在安装 Git,请稍候...")
|
||||||
|
log.Println("开始安装 Git 到:", installDir)
|
||||||
|
|
||||||
|
// Git for Windows 静默安装参数
|
||||||
|
args := []string{
|
||||||
|
"/VERYSILENT",
|
||||||
|
"/NORESTART",
|
||||||
|
"/NOCANCEL",
|
||||||
|
"/SP-",
|
||||||
|
"/CLOSEAPPLICATIONS",
|
||||||
|
"/RESTARTAPPLICATIONS",
|
||||||
|
"/COMPONENTS=icons,ext,ext\\shellhere,ext\\guihere,gitlfs,assoc,assoc_sh,autoupdate",
|
||||||
|
}
|
||||||
|
if installDir != "" {
|
||||||
|
args = append(args, "/DIR="+installDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(installerPath, args...)
|
||||||
|
hideWindowCmd(cmd)
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
s.emitInstallProgress("error", 0, "安装失败: "+err.Error())
|
||||||
|
return fmt.Errorf("安装 Git 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 验证安装
|
||||||
|
time.Sleep(2 * time.Second) // 等待 PATH 更新
|
||||||
|
status := s.CheckGitInstalled()
|
||||||
|
if !status.Installed {
|
||||||
|
// 尝试在指定目录查找
|
||||||
|
gitExe := filepath.Join(installDir, "bin", "git.exe")
|
||||||
|
if _, err := os.Stat(gitExe); err == nil {
|
||||||
|
s.emitInstallProgress("done", 100, "Git 安装成功!路径: "+gitExe)
|
||||||
|
// 重新初始化 gitClient
|
||||||
|
s.reinitGitClient()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.emitInstallProgress("error", 0, "安装完成但未检测到 Git,请重启应用重试")
|
||||||
|
return fmt.Errorf("安装完成但未检测到 Git")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.emitInstallProgress("done", 100, "Git 安装成功!版本: "+status.Version)
|
||||||
|
s.reinitGitClient()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// reinitGitClient 重新初始化 Git 客户端
|
||||||
|
func (s *AppService) reinitGitClient() {
|
||||||
|
s.gitClient = git.NewGitClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
// emitInstallProgress 向前端发送安装进度事件
|
||||||
|
func (s *AppService) emitInstallProgress(phase string, percent float64, message string) {
|
||||||
|
if s.app != nil {
|
||||||
|
s.app.Event.Emit("git-install-progress", GitInstallProgress{
|
||||||
|
Phase: phase,
|
||||||
|
Percent: percent,
|
||||||
|
Message: message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// downloadFile 下载文件并通过事件报告进度
|
||||||
|
func (s *AppService) downloadFile(url, dest string) error {
|
||||||
|
client := &http.Client{Timeout: 10 * time.Minute}
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := os.Create(dest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
totalSize := resp.ContentLength
|
||||||
|
var downloaded int64
|
||||||
|
buf := make([]byte, 32*1024)
|
||||||
|
lastReport := time.Now()
|
||||||
|
|
||||||
|
for {
|
||||||
|
n, readErr := resp.Body.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
_, writeErr := out.Write(buf[:n])
|
||||||
|
if writeErr != nil {
|
||||||
|
return writeErr
|
||||||
|
}
|
||||||
|
downloaded += int64(n)
|
||||||
|
|
||||||
|
// 每 300ms 报告一次进度
|
||||||
|
if time.Since(lastReport) > 300*time.Millisecond {
|
||||||
|
var pct float64
|
||||||
|
if totalSize > 0 {
|
||||||
|
pct = float64(downloaded) / float64(totalSize) * 100
|
||||||
|
}
|
||||||
|
sizeMB := float64(downloaded) / 1024 / 1024
|
||||||
|
msg := fmt.Sprintf("正在下载... %.1f MB", sizeMB)
|
||||||
|
if totalSize > 0 {
|
||||||
|
totalMB := float64(totalSize) / 1024 / 1024
|
||||||
|
msg = fmt.Sprintf("正在下载... %.1f / %.1f MB", sizeMB, totalMB)
|
||||||
|
}
|
||||||
|
s.emitInstallProgress("downloading", pct, msg)
|
||||||
|
lastReport = time.Now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if readErr != nil {
|
||||||
|
if readErr == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return readErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
8
internal/app/hidewindow_other.go
Normal file
8
internal/app/hidewindow_other.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package app
|
||||||
|
|
||||||
|
import "os/exec"
|
||||||
|
|
||||||
|
// hideWindowCmd 非 Windows 平台无需处理
|
||||||
|
func hideWindowCmd(cmd *exec.Cmd) {}
|
||||||
14
internal/app/hidewindow_windows.go
Normal file
14
internal/app/hidewindow_windows.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// hideWindowCmd 在 Windows 上隐藏子进程的控制台窗口
|
||||||
|
func hideWindowCmd(cmd *exec.Cmd) {
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
HideWindow: true,
|
||||||
|
CreationFlags: 0x08000000,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -97,6 +97,31 @@ func (s *AppService) GetProjectTree() []TreeNode {
|
|||||||
|
|
||||||
// --- 项目管理 ---
|
// --- 项目管理 ---
|
||||||
|
|
||||||
|
// CloneProject 克隆远程仓库到本地目录,并添加到项目树
|
||||||
|
func (s *AppService) CloneProject(platform, username, repoURL, parentDir, name string) error {
|
||||||
|
if repoURL == "" {
|
||||||
|
return fmt.Errorf("仓库地址不能为空")
|
||||||
|
}
|
||||||
|
if parentDir == "" {
|
||||||
|
return fmt.Errorf("目标目录不能为空")
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("项目名称不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 目标路径: parentDir/name
|
||||||
|
targetPath := parentDir + "/" + name
|
||||||
|
|
||||||
|
// 执行 git clone
|
||||||
|
_, err := s.gitClient.Clone(repoURL, targetPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("克隆失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 克隆成功后添加到项目树
|
||||||
|
return s.AddProject(platform, username, name, targetPath)
|
||||||
|
}
|
||||||
|
|
||||||
// AddProject 添加项目到指定平台/用户下
|
// AddProject 添加项目到指定平台/用户下
|
||||||
func (s *AppService) AddProject(platform, username, name, path string) error {
|
func (s *AppService) AddProject(platform, username, name, path string) error {
|
||||||
p, ok := s.config.Platforms[platform]
|
p, ok := s.config.Platforms[platform]
|
||||||
@@ -284,9 +309,17 @@ func (s *AppService) GetPlatformInfo(name string) (*PlatformInfo, error) {
|
|||||||
type ProjectStatus struct {
|
type ProjectStatus struct {
|
||||||
Branch string `json:"branch"`
|
Branch string `json:"branch"`
|
||||||
RemoteURL string `json:"remoteUrl"`
|
RemoteURL string `json:"remoteUrl"`
|
||||||
|
Remotes []RemoteItem `json:"remotes"`
|
||||||
|
CurrentRemote string `json:"currentRemote"`
|
||||||
ChangedFiles []FileInfo `json:"changedFiles"`
|
ChangedFiles []FileInfo `json:"changedFiles"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoteItem 远程仓库信息
|
||||||
|
type RemoteItem struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
// FileInfo 文件信息
|
// FileInfo 文件信息
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
@@ -321,9 +354,24 @@ func (s *AppService) GetProjectStatus(path string) (*ProjectStatus, error) {
|
|||||||
|
|
||||||
remoteURL, _ := s.gitClient.RemoteURL(path)
|
remoteURL, _ := s.gitClient.RemoteURL(path)
|
||||||
|
|
||||||
|
// 获取所有 remote 列表
|
||||||
|
var remotes []RemoteItem
|
||||||
|
remoteList, remoteErr := s.gitClient.RemoteList(path)
|
||||||
|
if remoteErr == nil {
|
||||||
|
for _, r := range remoteList {
|
||||||
|
remotes = append(remotes, RemoteItem{Name: r.Name, URL: r.URL})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentRemote := "origin"
|
||||||
|
if len(remotes) > 0 {
|
||||||
|
currentRemote = remotes[0].Name
|
||||||
|
}
|
||||||
|
|
||||||
return &ProjectStatus{
|
return &ProjectStatus{
|
||||||
Branch: strings.TrimSpace(branch),
|
Branch: strings.TrimSpace(branch),
|
||||||
RemoteURL: remoteURL,
|
RemoteURL: remoteURL,
|
||||||
|
Remotes: remotes,
|
||||||
|
CurrentRemote: currentRemote,
|
||||||
ChangedFiles: []FileInfo{},
|
ChangedFiles: []FileInfo{},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -490,22 +538,28 @@ func (s *AppService) GetFileDiffStaged(projectPath, filePath string) (string, er
|
|||||||
return s.gitClient.DiffStagedFile(projectPath, filePath)
|
return s.gitClient.DiffStagedFile(projectPath, filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PullProject 拉取项目(当前分支)
|
// PullProject 拉取项目(当前分支,指定 remote)
|
||||||
func (s *AppService) PullProject(path string) (string, error) {
|
func (s *AppService) PullProject(path, remote string) (string, error) {
|
||||||
|
if remote == "" {
|
||||||
|
remote = "origin"
|
||||||
|
}
|
||||||
branch, err := s.gitClient.Branch(path)
|
branch, err := s.gitClient.Branch(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("获取当前分支失败: %w", err)
|
return "", fmt.Errorf("获取当前分支失败: %w", err)
|
||||||
}
|
}
|
||||||
return s.gitClient.Run(path, "pull", "origin", strings.TrimSpace(branch))
|
return s.gitClient.PullFrom(path, remote, strings.TrimSpace(branch))
|
||||||
}
|
}
|
||||||
|
|
||||||
// PushProject 推送项目(当前分支)
|
// PushProject 推送项目(当前分支,指定 remote)
|
||||||
func (s *AppService) PushProject(path string) (string, error) {
|
func (s *AppService) PushProject(path, remote string) (string, error) {
|
||||||
|
if remote == "" {
|
||||||
|
remote = "origin"
|
||||||
|
}
|
||||||
branch, err := s.gitClient.Branch(path)
|
branch, err := s.gitClient.Branch(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("获取当前分支失败: %w", err)
|
return "", fmt.Errorf("获取当前分支失败: %w", err)
|
||||||
}
|
}
|
||||||
return s.gitClient.Run(path, "push", "origin", strings.TrimSpace(branch))
|
return s.gitClient.PushTo(path, remote, strings.TrimSpace(branch))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCommitDiff 获取指定提交的 diff
|
// GetCommitDiff 获取指定提交的 diff
|
||||||
@@ -556,10 +610,13 @@ func (s *AppService) GetCommitFileDiff(path, hash, filePath string) (string, err
|
|||||||
return s.gitClient.CommitFileDiff(path, hash, filePath)
|
return s.gitClient.CommitFileDiff(path, hash, filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchProject 拉取远程信息
|
// FetchProject 拉取远程信息(指定 remote,空则 fetch --all)
|
||||||
func (s *AppService) FetchProject(path string) (string, error) {
|
func (s *AppService) FetchProject(path, remote string) (string, error) {
|
||||||
|
if remote == "" {
|
||||||
return s.gitClient.Fetch(path)
|
return s.gitClient.Fetch(path)
|
||||||
}
|
}
|
||||||
|
return s.gitClient.FetchRemote(path, remote)
|
||||||
|
}
|
||||||
|
|
||||||
// CommitLog 提交记录
|
// CommitLog 提交记录
|
||||||
type CommitLog struct {
|
type CommitLog struct {
|
||||||
@@ -766,7 +823,7 @@ func (s *AppService) CreateTag(path, name, message string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteTag 删除标签(本地+远程)
|
// DeleteTag 删除标签(本地+远程)
|
||||||
func (s *AppService) DeleteTag(path, name string) error {
|
func (s *AppService) DeleteTag(path, name, remote string) error {
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
return fmt.Errorf("项目路径不存在: %s", path)
|
return fmt.Errorf("项目路径不存在: %s", path)
|
||||||
}
|
}
|
||||||
@@ -774,17 +831,20 @@ func (s *AppService) DeleteTag(path, name string) error {
|
|||||||
if name == "" {
|
if name == "" {
|
||||||
return fmt.Errorf("标签名不能为空")
|
return fmt.Errorf("标签名不能为空")
|
||||||
}
|
}
|
||||||
|
if remote == "" {
|
||||||
|
remote = "origin"
|
||||||
|
}
|
||||||
// 删除本地标签
|
// 删除本地标签
|
||||||
if _, err := s.gitClient.DeleteTag(path, name); err != nil {
|
if _, err := s.gitClient.DeleteTag(path, name); err != nil {
|
||||||
return fmt.Errorf("删除本地标签失败: %w", err)
|
return fmt.Errorf("删除本地标签失败: %w", err)
|
||||||
}
|
}
|
||||||
// 尝试删除远程标签(忽略错误,可能未推送过)
|
// 尝试删除远程标签(忽略错误,可能未推送过)
|
||||||
s.gitClient.DeleteRemoteTag(path, name)
|
s.gitClient.DeleteRemoteTagFrom(path, remote, name)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PushTag 推送标签到远程
|
// PushTag 推送标签到远程
|
||||||
func (s *AppService) PushTag(path, name string) error {
|
func (s *AppService) PushTag(path, name, remote string) error {
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
return fmt.Errorf("项目路径不存在: %s", path)
|
return fmt.Errorf("项目路径不存在: %s", path)
|
||||||
}
|
}
|
||||||
@@ -792,6 +852,522 @@ func (s *AppService) PushTag(path, name string) error {
|
|||||||
if name == "" {
|
if name == "" {
|
||||||
return fmt.Errorf("标签名不能为空")
|
return fmt.Errorf("标签名不能为空")
|
||||||
}
|
}
|
||||||
_, err := s.gitClient.PushTag(path, name)
|
if remote == "" {
|
||||||
|
remote = "origin"
|
||||||
|
}
|
||||||
|
_, err := s.gitClient.PushTagTo(path, remote, name)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 分支管理 ---
|
||||||
|
|
||||||
|
// CreateBranch 创建新分支
|
||||||
|
func (s *AppService) CreateBranch(path, name string) error {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("项目路径不存在: %s", path)
|
||||||
|
}
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("分支名不能为空")
|
||||||
|
}
|
||||||
|
_, err := s.gitClient.CreateBranch(path, name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBranch 删除本地分支
|
||||||
|
func (s *AppService) DeleteBranch(path, name string, force bool) error {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("项目路径不存在: %s", path)
|
||||||
|
}
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("分支名不能为空")
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
if force {
|
||||||
|
_, err = s.gitClient.ForceDeleteBranch(path, name)
|
||||||
|
} else {
|
||||||
|
_, err = s.gitClient.DeleteBranch(path, name)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeBranch 合并指定分支到当前分支
|
||||||
|
func (s *AppService) MergeBranch(path, branch string) (string, error) {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return "", fmt.Errorf("项目路径不存在: %s", path)
|
||||||
|
}
|
||||||
|
branch = strings.TrimSpace(branch)
|
||||||
|
if branch == "" {
|
||||||
|
return "", fmt.Errorf("分支名不能为空")
|
||||||
|
}
|
||||||
|
return s.gitClient.MergeBranch(path, branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 远程分支管理 ---
|
||||||
|
|
||||||
|
// GetRemoteBranches 获取远程分支列表
|
||||||
|
func (s *AppService) GetRemoteBranches(path, remote string) ([]BranchInfo, error) {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return nil, fmt.Errorf("项目路径不存在: %s", path)
|
||||||
|
}
|
||||||
|
var out string
|
||||||
|
var err error
|
||||||
|
if remote == "" {
|
||||||
|
out, err = s.gitClient.RemoteBranchList(path)
|
||||||
|
} else {
|
||||||
|
out, err = s.gitClient.RemoteBranchListByRemote(path, remote)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取远程分支列表失败: %w", err)
|
||||||
|
}
|
||||||
|
var branches []BranchInfo
|
||||||
|
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
|
||||||
|
name := strings.TrimSpace(line)
|
||||||
|
if name == "" || strings.Contains(name, "HEAD") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
branches = append(branches, BranchInfo{Name: name, Current: false})
|
||||||
|
}
|
||||||
|
return branches, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckoutRemoteBranch 检出远程分支到本地
|
||||||
|
func (s *AppService) CheckoutRemoteBranch(path, remoteBranch string) error {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("项目路径不存在: %s", path)
|
||||||
|
}
|
||||||
|
remoteBranch = strings.TrimSpace(remoteBranch)
|
||||||
|
if remoteBranch == "" {
|
||||||
|
return fmt.Errorf("远程分支名不能为空")
|
||||||
|
}
|
||||||
|
// origin/feature -> feature
|
||||||
|
localBranch := remoteBranch
|
||||||
|
if idx := strings.Index(remoteBranch, "/"); idx != -1 {
|
||||||
|
localBranch = remoteBranch[idx+1:]
|
||||||
|
}
|
||||||
|
_, err := s.gitClient.CheckoutNewBranch(path, localBranch, remoteBranch)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRemoteBranch 删除远程分支
|
||||||
|
func (s *AppService) DeleteRemoteBranch(path, branch, remote string) error {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("项目路径不存在: %s", path)
|
||||||
|
}
|
||||||
|
branch = strings.TrimSpace(branch)
|
||||||
|
if branch == "" {
|
||||||
|
return fmt.Errorf("分支名不能为空")
|
||||||
|
}
|
||||||
|
if remote == "" {
|
||||||
|
remote = "origin"
|
||||||
|
}
|
||||||
|
// origin/feature -> feature
|
||||||
|
localName := branch
|
||||||
|
if idx := strings.Index(branch, "/"); idx != -1 {
|
||||||
|
localName = branch[idx+1:]
|
||||||
|
}
|
||||||
|
_, err := s.gitClient.DeleteRemoteBranchFrom(path, remote, localName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Stash 贮藏管理 ---
|
||||||
|
|
||||||
|
// StashInfo 贮藏信息
|
||||||
|
type StashInfo struct {
|
||||||
|
Index int `json:"index"`
|
||||||
|
Ref string `json:"ref"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StashSave 保存当前变更到贮藏
|
||||||
|
func (s *AppService) StashSave(path, message string) error {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("项目路径不存在: %s", path)
|
||||||
|
}
|
||||||
|
_, err := s.gitClient.StashSave(path, message)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStashList 获取贮藏列表
|
||||||
|
func (s *AppService) GetStashList(path string) ([]StashInfo, error) {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return nil, fmt.Errorf("项目路径不存在: %s", path)
|
||||||
|
}
|
||||||
|
out, err := s.gitClient.StashList(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取贮藏列表失败: %w", err)
|
||||||
|
}
|
||||||
|
var stashes []StashInfo
|
||||||
|
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(line, "\t", 3)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ref := parts[0]
|
||||||
|
message := parts[1]
|
||||||
|
var ts int64
|
||||||
|
if len(parts) >= 3 {
|
||||||
|
fmt.Sscanf(parts[2], "%d", &ts)
|
||||||
|
}
|
||||||
|
var index int
|
||||||
|
fmt.Sscanf(ref, "stash@{%d}", &index)
|
||||||
|
stashes = append(stashes, StashInfo{
|
||||||
|
Index: index,
|
||||||
|
Ref: ref,
|
||||||
|
Message: message,
|
||||||
|
Timestamp: ts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return stashes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StashApply 应用贮藏(不删除)
|
||||||
|
func (s *AppService) StashApply(path string, index int) error {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("项目路径不存在: %s", path)
|
||||||
|
}
|
||||||
|
_, err := s.gitClient.StashApply(path, index)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// StashPop 应用贮藏并删除
|
||||||
|
func (s *AppService) StashPop(path string, index int) error {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("项目路径不存在: %s", path)
|
||||||
|
}
|
||||||
|
_, err := s.gitClient.StashPop(path, index)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// StashDrop 删除贮藏
|
||||||
|
func (s *AppService) StashDrop(path string, index int) error {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("项目路径不存在: %s", path)
|
||||||
|
}
|
||||||
|
_, err := s.gitClient.StashDrop(path, index)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 设置管理 ---
|
||||||
|
|
||||||
|
// GitConfig Git 全局配置
|
||||||
|
type GitConfig struct {
|
||||||
|
UserName string `json:"userName"`
|
||||||
|
UserEmail string `json:"userEmail"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGitGlobalConfig 获取 git 全局配置
|
||||||
|
func (s *AppService) GetGitGlobalConfig() (*GitConfig, error) {
|
||||||
|
name, _ := s.gitClient.GetGitGlobalConfig("user.name")
|
||||||
|
email, _ := s.gitClient.GetGitGlobalConfig("user.email")
|
||||||
|
return &GitConfig{
|
||||||
|
UserName: name,
|
||||||
|
UserEmail: email,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGitGlobalConfig 设置 git 全局配置
|
||||||
|
func (s *AppService) SetGitGlobalConfig(name, email string) error {
|
||||||
|
if name != "" {
|
||||||
|
if _, err := s.gitClient.SetGitGlobalConfig("user.name", name); err != nil {
|
||||||
|
return fmt.Errorf("设置 user.name 失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if email != "" {
|
||||||
|
if _, err := s.gitClient.SetGitGlobalConfig("user.email", email); err != nil {
|
||||||
|
return fmt.Errorf("设置 user.email 失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAppSettings 获取应用设置
|
||||||
|
func (s *AppService) GetAppSettings() *config.Settings {
|
||||||
|
return &s.config.Settings
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAppSettings 更新应用设置
|
||||||
|
func (s *AppService) UpdateAppSettings(logLevel string) error {
|
||||||
|
s.config.Settings.LogLevel = logLevel
|
||||||
|
return config.SaveConfig(s.config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 远程仓库管理 ---
|
||||||
|
|
||||||
|
// GetRemotes 获取项目所有远程仓库列表
|
||||||
|
func (s *AppService) GetRemotes(path string) ([]RemoteItem, error) {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return nil, fmt.Errorf("项目路径不存在: %s", path)
|
||||||
|
}
|
||||||
|
remoteList, err := s.gitClient.RemoteList(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取远程仓库列表失败: %w", err)
|
||||||
|
}
|
||||||
|
var remotes []RemoteItem
|
||||||
|
for _, r := range remoteList {
|
||||||
|
remotes = append(remotes, RemoteItem{Name: r.Name, URL: r.URL})
|
||||||
|
}
|
||||||
|
return remotes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRemote 添加远程仓库
|
||||||
|
func (s *AppService) AddRemote(path, name, url string) error {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("项目路径不存在: %s", path)
|
||||||
|
}
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
url = strings.TrimSpace(url)
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("远程名称不能为空")
|
||||||
|
}
|
||||||
|
if url == "" {
|
||||||
|
return fmt.Errorf("远程地址不能为空")
|
||||||
|
}
|
||||||
|
_, err := s.gitClient.AddRemote(path, name, url)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRemote 删除远程仓库
|
||||||
|
func (s *AppService) RemoveRemote(path, name string) error {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("项目路径不存在: %s", path)
|
||||||
|
}
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("远程名称不能为空")
|
||||||
|
}
|
||||||
|
_, err := s.gitClient.RemoveRemote(path, name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 冲突处理 ---
|
||||||
|
|
||||||
|
// 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, remote string) []BatchPullResult {
|
||||||
|
if remote == "" {
|
||||||
|
remote = "origin"
|
||||||
|
}
|
||||||
|
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.PullFrom(path, remote, 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, remote string) []BatchPullResult {
|
||||||
|
if remote == "" {
|
||||||
|
remote = "origin"
|
||||||
|
}
|
||||||
|
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.PushTo(path, remote, strings.TrimSpace(branch))
|
||||||
|
if err != nil {
|
||||||
|
result.Message = err.Error()
|
||||||
|
} else {
|
||||||
|
result.Success = true
|
||||||
|
result.Message = "推送成功"
|
||||||
|
}
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|||||||
@@ -60,18 +60,95 @@ func (g *GitClient) Push(path string) (string, error) {
|
|||||||
return g.Run(path, "push")
|
return g.Run(path, "push")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PushTo 推送到指定远程
|
||||||
|
func (g *GitClient) PushTo(path, remote, branch string) (string, error) {
|
||||||
|
return g.Run(path, "push", remote, branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PullFrom 从指定远程拉取
|
||||||
|
func (g *GitClient) PullFrom(path, remote, branch string) (string, error) {
|
||||||
|
return g.Run(path, "pull", remote, branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchRemote 拉取指定远程信息
|
||||||
|
func (g *GitClient) FetchRemote(path, remote string) (string, error) {
|
||||||
|
return g.Run(path, "fetch", remote)
|
||||||
|
}
|
||||||
|
|
||||||
func (g *GitClient) Status(path string) (string, error) {
|
func (g *GitClient) Status(path string) (string, error) {
|
||||||
return g.Run(path, "status")
|
return g.Run(path, "status")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GitClient) Clone(repoURL, path string) (string, error) {
|
func (g *GitClient) Clone(repoURL, path string) (string, error) {
|
||||||
return g.Run(".", "clone", repoURL, path)
|
// Clone 操作可能需要较长时间,使用独立的超时设置(10 分钟)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
args := []string{"-c", "core.quotePath=false"}
|
||||||
|
if g.Enabled {
|
||||||
|
args = append(args, "-c", "http.proxy="+g.Proxy, "-c", "https.proxy="+g.Proxy)
|
||||||
|
}
|
||||||
|
args = append(args, "clone", "--progress", repoURL, path)
|
||||||
|
log.Println(args)
|
||||||
|
cmd := exec.CommandContext(ctx, "git", args...)
|
||||||
|
hideWindow(cmd)
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
err := cmd.Run()
|
||||||
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
|
return "", fmt.Errorf("git clone timeout")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("git clone error: %v, stderr: %s", err, stderr.String())
|
||||||
|
}
|
||||||
|
return stdout.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GitClient) Fetch(path string) (string, error) {
|
func (g *GitClient) Fetch(path string) (string, error) {
|
||||||
return g.Run(path, "fetch", "--all")
|
return g.Run(path, "fetch", "--all")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoteList 获取所有远程仓库列表 (git remote -v)
|
||||||
|
type RemoteInfo struct {
|
||||||
|
Name string
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GitClient) RemoteList(path string) ([]RemoteInfo, error) {
|
||||||
|
out, err := g.Run(path, "remote", "-v")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
var remotes []RemoteInfo
|
||||||
|
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := parts[0]
|
||||||
|
if seen[name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[name] = true
|
||||||
|
remotes = append(remotes, RemoteInfo{Name: name, URL: parts[1]})
|
||||||
|
}
|
||||||
|
return remotes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRemote 添加远程仓库
|
||||||
|
func (g *GitClient) AddRemote(path, name, url string) (string, error) {
|
||||||
|
return g.Run(path, "remote", "add", name, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRemote 删除远程仓库
|
||||||
|
func (g *GitClient) RemoveRemote(path, name string) (string, error) {
|
||||||
|
return g.Run(path, "remote", "remove", name)
|
||||||
|
}
|
||||||
|
|
||||||
func (g *GitClient) RemoteURL(path string) (string, error) {
|
func (g *GitClient) RemoteURL(path string) (string, error) {
|
||||||
out, err := g.Run(path, "config", "--get", "remote.origin.url")
|
out, err := g.Run(path, "config", "--get", "remote.origin.url")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -120,11 +197,16 @@ func (g *GitClient) Log(path string, count int) (string, error) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnpushedCommits 获取当前分支上未推送到远程的提交哈希列表
|
// UnpushedCommits 获取当前分支上未推送到远程的提交哈希列表(默认 origin)
|
||||||
func (g *GitClient) UnpushedCommits(path, branch string) (string, error) {
|
func (g *GitClient) UnpushedCommits(path, branch string) (string, error) {
|
||||||
return g.Run(path, "rev-list", "origin/"+branch+"..HEAD")
|
return g.Run(path, "rev-list", "origin/"+branch+"..HEAD")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnpushedCommitsTo 获取当前分支上未推送到指定远程的提交哈希列表
|
||||||
|
func (g *GitClient) UnpushedCommitsTo(path, remote, branch string) (string, error) {
|
||||||
|
return g.Run(path, "rev-list", remote+"/"+branch+"..HEAD")
|
||||||
|
}
|
||||||
|
|
||||||
// RevertCommit 撤回指定提交(创建一个反向提交)
|
// RevertCommit 撤回指定提交(创建一个反向提交)
|
||||||
func (g *GitClient) RevertCommit(path, hash string) (string, error) {
|
func (g *GitClient) RevertCommit(path, hash string) (string, error) {
|
||||||
return g.Run(path, "revert", "--no-edit", hash)
|
return g.Run(path, "revert", "--no-edit", hash)
|
||||||
@@ -146,16 +228,26 @@ func (g *GitClient) DeleteTag(path, name string) (string, error) {
|
|||||||
return g.Run(path, "tag", "-d", name)
|
return g.Run(path, "tag", "-d", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PushTag 推送标签到远程
|
// PushTag 推送标签到远程(默认 origin)
|
||||||
func (g *GitClient) PushTag(path, name string) (string, error) {
|
func (g *GitClient) PushTag(path, name string) (string, error) {
|
||||||
return g.Run(path, "push", "origin", name)
|
return g.Run(path, "push", "origin", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteRemoteTag 删除远程标签
|
// PushTagTo 推送标签到指定远程
|
||||||
|
func (g *GitClient) PushTagTo(path, remote, name string) (string, error) {
|
||||||
|
return g.Run(path, "push", remote, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRemoteTag 删除远程标签(默认 origin)
|
||||||
func (g *GitClient) DeleteRemoteTag(path, name string) (string, error) {
|
func (g *GitClient) DeleteRemoteTag(path, name string) (string, error) {
|
||||||
return g.Run(path, "push", "origin", "--delete", name)
|
return g.Run(path, "push", "origin", "--delete", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteRemoteTagFrom 删除指定远程的标签
|
||||||
|
func (g *GitClient) DeleteRemoteTagFrom(path, remote, name string) (string, error) {
|
||||||
|
return g.Run(path, "push", remote, "--delete", name)
|
||||||
|
}
|
||||||
|
|
||||||
// BranchList 获取所有本地分支
|
// BranchList 获取所有本地分支
|
||||||
func (g *GitClient) BranchList(path string) (string, error) {
|
func (g *GitClient) BranchList(path string) (string, error) {
|
||||||
return g.Run(path, "branch", "--format=%(refname:short)\t%(HEAD)")
|
return g.Run(path, "branch", "--format=%(refname:short)\t%(HEAD)")
|
||||||
@@ -327,6 +419,184 @@ func parseNameStatus(output string) []FileChange {
|
|||||||
return changes
|
return changes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateBranch 创建新分支
|
||||||
|
func (g *GitClient) CreateBranch(path, name string) (string, error) {
|
||||||
|
return g.Run(path, "branch", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBranch 删除本地分支 (-d 安全删除)
|
||||||
|
func (g *GitClient) DeleteBranch(path, name string) (string, error) {
|
||||||
|
return g.Run(path, "branch", "-d", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForceDeleteBranch 强制删除本地分支 (-D)
|
||||||
|
func (g *GitClient) ForceDeleteBranch(path, name string) (string, error) {
|
||||||
|
return g.Run(path, "branch", "-D", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeBranch 合并指定分支到当前分支
|
||||||
|
func (g *GitClient) MergeBranch(path, branch string) (string, error) {
|
||||||
|
return g.Run(path, "merge", branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRemoteBranch 删除远程分支(默认 origin)
|
||||||
|
func (g *GitClient) DeleteRemoteBranch(path, branch string) (string, error) {
|
||||||
|
return g.Run(path, "push", "origin", "--delete", branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRemoteBranchFrom 删除指定远程的分支
|
||||||
|
func (g *GitClient) DeleteRemoteBranchFrom(path, remote, branch string) (string, error) {
|
||||||
|
return g.Run(path, "push", remote, "--delete", branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoteBranchList 获取所有远程分支
|
||||||
|
func (g *GitClient) RemoteBranchList(path string) (string, error) {
|
||||||
|
return g.Run(path, "branch", "-r", "--format=%(refname:short)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoteBranchListByRemote 获取指定远程的分支
|
||||||
|
func (g *GitClient) RemoteBranchListByRemote(path, remote string) (string, error) {
|
||||||
|
return g.Run(path, "branch", "-r", "--list", remote+"/*", "--format=%(refname:short)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckoutNewBranch 从远程分支检出新本地分支
|
||||||
|
func (g *GitClient) CheckoutNewBranch(path, localBranch, remoteBranch string) (string, error) {
|
||||||
|
return g.Run(path, "checkout", "-b", localBranch, remoteBranch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StashSave 保存当前工作区变更到贮藏
|
||||||
|
func (g *GitClient) StashSave(path, message string) (string, error) {
|
||||||
|
if message != "" {
|
||||||
|
return g.Run(path, "stash", "push", "-m", message)
|
||||||
|
}
|
||||||
|
return g.Run(path, "stash", "push")
|
||||||
|
}
|
||||||
|
|
||||||
|
// StashList 获取贮藏列表
|
||||||
|
func (g *GitClient) StashList(path string) (string, error) {
|
||||||
|
return g.Run(path, "stash", "list", "--format=%gd\t%s\t%at")
|
||||||
|
}
|
||||||
|
|
||||||
|
// StashApply 应用贮藏(不删除)
|
||||||
|
func (g *GitClient) StashApply(path string, index int) (string, error) {
|
||||||
|
return g.Run(path, "stash", "apply", fmt.Sprintf("stash@{%d}", index))
|
||||||
|
}
|
||||||
|
|
||||||
|
// StashPop 应用贮藏并删除
|
||||||
|
func (g *GitClient) StashPop(path string, index int) (string, error) {
|
||||||
|
return g.Run(path, "stash", "pop", fmt.Sprintf("stash@{%d}", index))
|
||||||
|
}
|
||||||
|
|
||||||
|
// StashDrop 删除贮藏
|
||||||
|
func (g *GitClient) StashDrop(path string, index int) (string, error) {
|
||||||
|
return g.Run(path, "stash", "drop", fmt.Sprintf("stash@{%d}", index))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGitGlobalConfig 获取 git 全局配置
|
||||||
|
func (g *GitClient) GetGitGlobalConfig(key string) (string, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), g.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
cmd := exec.CommandContext(ctx, "git", "config", "--global", "--get", key)
|
||||||
|
hideWindow(cmd)
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil // key 不存在不算错误
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(stdout.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGitGlobalConfig 设置 git 全局配置
|
||||||
|
func (g *GitClient) SetGitGlobalConfig(key, value string) (string, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), g.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
cmd := exec.CommandContext(ctx, "git", "config", "--global", key, value)
|
||||||
|
hideWindow(cmd)
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("git config error: %v, stderr: %s", err, stderr.String())
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/sys/windows/registry"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProxyInfo 保存当前系统代理信息
|
// ProxyInfo 保存当前系统代理信息
|
||||||
@@ -25,8 +23,8 @@ type ProxyInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentProxy 获取本机当前的代理设置
|
// GetCurrentProxy 获取本机当前的代理设置
|
||||||
// 优先读取 Windows 系统代理(注册表),同时也读取环境变量中的代理配置
|
// 优先读取系统代理(Windows 注册表 / macOS scutil / Linux 环境变量),
|
||||||
// 支持检测 Clash、V2Ray 等常见代理工具的配置
|
// 同时也读取环境变量中的代理配置
|
||||||
func GetCurrentProxy() (*ProxyInfo, error) {
|
func GetCurrentProxy() (*ProxyInfo, error) {
|
||||||
info := &ProxyInfo{}
|
info := &ProxyInfo{}
|
||||||
|
|
||||||
@@ -34,9 +32,8 @@ func GetCurrentProxy() (*ProxyInfo, error) {
|
|||||||
info.HTTPProxy = getEnvProxy("HTTP_PROXY", "http_proxy")
|
info.HTTPProxy = getEnvProxy("HTTP_PROXY", "http_proxy")
|
||||||
info.HTTPSProxy = getEnvProxy("HTTPS_PROXY", "https_proxy")
|
info.HTTPSProxy = getEnvProxy("HTTPS_PROXY", "https_proxy")
|
||||||
|
|
||||||
// 2. 从 Windows 注册表读取系统代理设置
|
// 2. 从系统设置读取代理(平台相关)
|
||||||
if err := readWindowsSystemProxy(info); err != nil {
|
if err := readSystemProxy(info); err != nil {
|
||||||
// 注册表读取失败不算致命错误,环境变量中可能仍有代理信息
|
|
||||||
fmt.Printf("读取系统代理设置时出错: %v\n", err)
|
fmt.Printf("读取系统代理设置时出错: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,42 +51,6 @@ func GetCurrentProxy() (*ProxyInfo, error) {
|
|||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readWindowsSystemProxy 从 Windows 注册表读取系统代理设置
|
|
||||||
func readWindowsSystemProxy(info *ProxyInfo) error {
|
|
||||||
key, err := registry.OpenKey(
|
|
||||||
registry.CURRENT_USER,
|
|
||||||
`Software\Microsoft\Windows\CurrentVersion\Internet Settings`,
|
|
||||||
registry.QUERY_VALUE,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("无法打开注册表键: %w", err)
|
|
||||||
}
|
|
||||||
defer key.Close()
|
|
||||||
|
|
||||||
// 读取代理是否启用 (ProxyEnable: 0=关闭, 1=开启)
|
|
||||||
proxyEnable, _, err := key.GetIntegerValue("ProxyEnable")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("无法读取 ProxyEnable: %w", err)
|
|
||||||
}
|
|
||||||
info.Enabled = proxyEnable == 1
|
|
||||||
|
|
||||||
// 读取代理服务器地址
|
|
||||||
proxyServer, _, err := key.GetStringValue("ProxyServer")
|
|
||||||
if err == nil && proxyServer != "" {
|
|
||||||
info.Server = proxyServer
|
|
||||||
// 根据端口推断代理协议
|
|
||||||
info.Protocol = guessProtocol(proxyServer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取代理绕过列表
|
|
||||||
bypass, _, err := key.GetStringValue("ProxyOverride")
|
|
||||||
if err == nil {
|
|
||||||
info.Bypass = bypass
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getEnvProxy 依次检查多个环境变量名,返回第一个非空值
|
// getEnvProxy 依次检查多个环境变量名,返回第一个非空值
|
||||||
func getEnvProxy(names ...string) string {
|
func getEnvProxy(names ...string) string {
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
@@ -104,11 +65,9 @@ func getEnvProxy(names ...string) string {
|
|||||||
// 例如 "http://127.0.0.1:7890" -> "127.0.0.1:7890"
|
// 例如 "http://127.0.0.1:7890" -> "127.0.0.1:7890"
|
||||||
func extractServerFromURL(proxyURL string) string {
|
func extractServerFromURL(proxyURL string) string {
|
||||||
s := proxyURL
|
s := proxyURL
|
||||||
// 去掉协议前缀
|
|
||||||
if idx := strings.Index(s, "://"); idx != -1 {
|
if idx := strings.Index(s, "://"); idx != -1 {
|
||||||
s = s[idx+3:]
|
s = s[idx+3:]
|
||||||
}
|
}
|
||||||
// 去掉尾部斜杠
|
|
||||||
s = strings.TrimRight(s, "/")
|
s = strings.TrimRight(s, "/")
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
@@ -127,14 +86,14 @@ func extractProtocolFromURL(proxyURL string) string {
|
|||||||
// V2Ray 默认: 10808(socks5), 10809(http)
|
// V2Ray 默认: 10808(socks5), 10809(http)
|
||||||
func guessProtocol(server string) string {
|
func guessProtocol(server string) string {
|
||||||
knownPorts := map[string]string{
|
knownPorts := map[string]string{
|
||||||
"7890": "http", // Clash HTTP
|
"7890": "http",
|
||||||
"7891": "socks5", // Clash SOCKS5
|
"7891": "socks5",
|
||||||
"7892": "http", // Clash mixed
|
"7892": "http",
|
||||||
"10808": "socks5", // V2Ray SOCKS5
|
"10808": "socks5",
|
||||||
"10809": "http", // V2Ray HTTP
|
"10809": "http",
|
||||||
"1080": "socks5", // 通用 SOCKS5
|
"1080": "socks5",
|
||||||
"1081": "http", // 通用 HTTP
|
"1081": "http",
|
||||||
"8080": "http", // 通用 HTTP
|
"8080": "http",
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(server, ":")
|
parts := strings.Split(server, ":")
|
||||||
|
|||||||
82
internal/proxy_darwin.go
Normal file
82
internal/proxy_darwin.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// readSystemProxy 从 macOS 系统偏好设置读取代理配置
|
||||||
|
// 使用 scutil --proxy 命令获取系统代理信息
|
||||||
|
func readSystemProxy(info *ProxyInfo) error {
|
||||||
|
out, err := exec.Command("scutil", "--proxy").Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("执行 scutil --proxy 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(out), "\n")
|
||||||
|
settings := make(map[string]string)
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
parts := strings.SplitN(line, " : ", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
settings[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 HTTP 代理
|
||||||
|
if settings["HTTPEnable"] == "1" {
|
||||||
|
host := settings["HTTPProxy"]
|
||||||
|
port := settings["HTTPPort"]
|
||||||
|
if host != "" {
|
||||||
|
info.Enabled = true
|
||||||
|
info.Server = host
|
||||||
|
if port != "" && port != "0" {
|
||||||
|
info.Server = host + ":" + port
|
||||||
|
}
|
||||||
|
info.Protocol = "http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 HTTPS 代理
|
||||||
|
if !info.Enabled && settings["HTTPSEnable"] == "1" {
|
||||||
|
host := settings["HTTPSProxy"]
|
||||||
|
port := settings["HTTPSPort"]
|
||||||
|
if host != "" {
|
||||||
|
info.Enabled = true
|
||||||
|
info.Server = host
|
||||||
|
if port != "" && port != "0" {
|
||||||
|
info.Server = host + ":" + port
|
||||||
|
}
|
||||||
|
info.Protocol = "http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 SOCKS 代理
|
||||||
|
if !info.Enabled && settings["SOCKSEnable"] == "1" {
|
||||||
|
host := settings["SOCKSProxy"]
|
||||||
|
port := settings["SOCKSPort"]
|
||||||
|
if host != "" {
|
||||||
|
info.Enabled = true
|
||||||
|
info.Server = host
|
||||||
|
if port != "" && port != "0" {
|
||||||
|
info.Server = host + ":" + port
|
||||||
|
}
|
||||||
|
info.Protocol = "socks5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果通过以上方式检测到了代理,根据端口再次猜测协议
|
||||||
|
if info.Enabled && info.Server != "" {
|
||||||
|
info.Protocol = guessProtocol(info.Server)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取绕过列表
|
||||||
|
if exceptions, ok := settings["ExceptionsList"]; ok {
|
||||||
|
info.Bypass = exceptions
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
122
internal/proxy_linux.go
Normal file
122
internal/proxy_linux.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// readSystemProxy 从 Linux 系统读取代理配置
|
||||||
|
// 优先尝试 GNOME (gsettings),失败则回退到环境变量
|
||||||
|
func readSystemProxy(info *ProxyInfo) error {
|
||||||
|
// 尝试通过 gsettings 读取 GNOME 系统代理
|
||||||
|
if err := readGnomeProxy(info); err == nil && info.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试通过 KDE 配置读取代理
|
||||||
|
if err := readKDEProxy(info); err == nil && info.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux 上环境变量已由 GetCurrentProxy 中读取,此处无需额外操作
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readGnomeProxy 通过 gsettings 读取 GNOME 桌面环境的代理配置
|
||||||
|
func readGnomeProxy(info *ProxyInfo) error {
|
||||||
|
// 检查 gsettings 是否可用
|
||||||
|
if _, err := exec.LookPath("gsettings"); err != nil {
|
||||||
|
return fmt.Errorf("gsettings 不可用: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取代理模式
|
||||||
|
mode, err := gsettingsGet("org.gnome.system.proxy", "mode")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode != "'manual'" {
|
||||||
|
return nil // 代理未启用
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取 HTTP 代理
|
||||||
|
host, err := gsettingsGet("org.gnome.system.proxy.http", "host")
|
||||||
|
if err == nil && host != "" && host != "''" {
|
||||||
|
host = strings.Trim(host, "'")
|
||||||
|
port, _ := gsettingsGet("org.gnome.system.proxy.http", "port")
|
||||||
|
port = strings.TrimSpace(port)
|
||||||
|
if port != "" && port != "0" {
|
||||||
|
info.Enabled = true
|
||||||
|
info.Server = host + ":" + port
|
||||||
|
info.Protocol = guessProtocol(info.Server)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取 SOCKS 代理
|
||||||
|
host, err = gsettingsGet("org.gnome.system.proxy.socks", "host")
|
||||||
|
if err == nil && host != "" && host != "''" {
|
||||||
|
host = strings.Trim(host, "'")
|
||||||
|
port, _ := gsettingsGet("org.gnome.system.proxy.socks", "port")
|
||||||
|
port = strings.TrimSpace(port)
|
||||||
|
if port != "" && port != "0" {
|
||||||
|
info.Enabled = true
|
||||||
|
info.Server = host + ":" + port
|
||||||
|
info.Protocol = "socks5"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readKDEProxy 通过 kreadconfig5/kreadconfig6 读取 KDE 代理配置
|
||||||
|
func readKDEProxy(info *ProxyInfo) error {
|
||||||
|
// 尝试 kreadconfig5 或 kreadconfig6
|
||||||
|
var cmd string
|
||||||
|
if _, err := exec.LookPath("kreadconfig6"); err == nil {
|
||||||
|
cmd = "kreadconfig6"
|
||||||
|
} else if _, err := exec.LookPath("kreadconfig5"); err == nil {
|
||||||
|
cmd = "kreadconfig5"
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("kreadconfig 不可用")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取代理类型
|
||||||
|
proxyType, err := kdeGet(cmd, "Proxy Settings", "ProxyType")
|
||||||
|
if err != nil || proxyType != "1" {
|
||||||
|
return nil // 0 = 无代理, 1 = 手动
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取 HTTP 代理
|
||||||
|
httpProxy, err := kdeGet(cmd, "Proxy Settings", "httpProxy")
|
||||||
|
if err == nil && httpProxy != "" {
|
||||||
|
info.Enabled = true
|
||||||
|
info.Server = extractServerFromURL(httpProxy)
|
||||||
|
info.Protocol = guessProtocol(info.Server)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// gsettingsGet 执行 gsettings get 命令
|
||||||
|
func gsettingsGet(schema, key string) (string, error) {
|
||||||
|
out, err := exec.Command("gsettings", "get", schema, key).Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(out)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// kdeGet 执行 kreadconfig 命令读取 KDE 配置
|
||||||
|
func kdeGet(cmd, group, key string) (string, error) {
|
||||||
|
out, err := exec.Command(cmd, "--file", "kioslaverc", "--group", group, "--key", key).Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(out)), nil
|
||||||
|
}
|
||||||
43
internal/proxy_windows.go
Normal file
43
internal/proxy_windows.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// readSystemProxy 从 Windows 注册表读取系统代理设置
|
||||||
|
func readSystemProxy(info *ProxyInfo) error {
|
||||||
|
key, err := registry.OpenKey(
|
||||||
|
registry.CURRENT_USER,
|
||||||
|
`Software\Microsoft\Windows\CurrentVersion\Internet Settings`,
|
||||||
|
registry.QUERY_VALUE,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("无法打开注册表键: %w", err)
|
||||||
|
}
|
||||||
|
defer key.Close()
|
||||||
|
|
||||||
|
// 读取代理是否启用 (ProxyEnable: 0=关闭, 1=开启)
|
||||||
|
proxyEnable, _, err := key.GetIntegerValue("ProxyEnable")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("无法读取 ProxyEnable: %w", err)
|
||||||
|
}
|
||||||
|
info.Enabled = proxyEnable == 1
|
||||||
|
|
||||||
|
// 读取代理服务器地址
|
||||||
|
proxyServer, _, err := key.GetStringValue("ProxyServer")
|
||||||
|
if err == nil && proxyServer != "" {
|
||||||
|
info.Server = proxyServer
|
||||||
|
// 根据端口推断代理协议
|
||||||
|
info.Protocol = guessProtocol(proxyServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取代理绕过列表
|
||||||
|
bypass, _, err := key.GetStringValue("ProxyOverride")
|
||||||
|
if err == nil {
|
||||||
|
info.Bypass = bypass
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user