新增远程项目克隆功能
This commit is contained in:
@@ -7,7 +7,10 @@ import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
CloudDownloadOutlined,
|
||||
LoadingOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { AppService } from '../../bindings/github.com/zhuy1228/GitPilot/internal/app'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -24,6 +27,11 @@ const selectedKeys = ref([])
|
||||
const showAddDialog = ref(false)
|
||||
const addForm = ref({ platform: '', username: '', name: '', path: '' })
|
||||
|
||||
// 克隆项目弹窗
|
||||
const showCloneDialog = ref(false)
|
||||
const cloneForm = ref({ platform: '', username: '', repoURL: '', parentDir: '', name: '' })
|
||||
const cloneLoading = ref(false)
|
||||
|
||||
// 右键菜单
|
||||
const contextMenu = ref({ visible: false, x: 0, y: 0, type: '', data: {} })
|
||||
|
||||
@@ -142,6 +150,7 @@ function onContextMenuClick({ key: action }) {
|
||||
case 'edit-platform': openEditPlatformDialog(data.key); break
|
||||
case 'remove-platform': removePlatform(data.key); 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 'remove-user': removeUser(data.platformKey, data.username); break
|
||||
case 'remove-project': removeProject(data.platformKey, data.username, data.name); break
|
||||
@@ -166,6 +175,7 @@ const contextMenuItems = computed(() => {
|
||||
if (type === 'user') {
|
||||
return [
|
||||
{ key: 'add-project', label: '添加项目', icon: h(FolderOutlined) },
|
||||
{ key: 'clone-project', label: '克隆项目', icon: h(CloudDownloadOutlined) },
|
||||
{ key: 'edit-user', label: '编辑用户', icon: h(EditOutlined) },
|
||||
{ type: 'divider' },
|
||||
{ key: 'remove-user', label: '删除用户', danger: true, icon: h(DeleteOutlined) },
|
||||
@@ -174,6 +184,7 @@ const contextMenuItems = computed(() => {
|
||||
if (type === 'project') {
|
||||
return [
|
||||
{ key: 'add-project', label: '添加项目', icon: h(FolderOutlined) },
|
||||
{ key: 'clone-project', label: '克隆项目', icon: h(CloudDownloadOutlined) },
|
||||
{ type: 'divider' },
|
||||
{ key: 'remove-project', label: '删除项目', danger: true, icon: h(DeleteOutlined) },
|
||||
]
|
||||
@@ -320,6 +331,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) {
|
||||
if (!confirm(`确定删除项目 "${name}" 吗?`)) return
|
||||
try {
|
||||
@@ -441,6 +503,53 @@ onMounted(() => {
|
||||
</a-form>
|
||||
</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
|
||||
v-model:open="showPlatformDialog"
|
||||
|
||||
@@ -52,6 +52,19 @@ export function AddUser(platform, username, token) {
|
||||
return $Call.ByID(2264426704, platform, username, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 提交已暂存的更改
|
||||
* @param {string} path
|
||||
|
||||
@@ -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 添加项目到指定平台/用户下
|
||||
func (s *AppService) AddProject(platform, username, name, path string) error {
|
||||
p, ok := s.config.Platforms[platform]
|
||||
|
||||
@@ -65,7 +65,28 @@ func (g *GitClient) Status(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) {
|
||||
|
||||
Reference in New Issue
Block a user