From 6e0becb748ebc9bae1042ee649fd20fcd3cb65bc Mon Sep 17 00:00:00 2001 From: zyj Date: Tue, 10 Mar 2026 10:25:45 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=BF=9C=E7=A8=8B=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E5=85=8B=E9=9A=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/components/Sidebar.vue | 109 ++++++++++++++++++ .../GitPilot/internal/app/appservice.js | 13 +++ internal/app/service.go | 25 ++++ internal/git/client.go | 23 +++- 4 files changed, 169 insertions(+), 1 deletion(-) diff --git a/frontend/app/components/Sidebar.vue b/frontend/app/components/Sidebar.vue index d7b586a..561c922 100644 --- a/frontend/app/components/Sidebar.vue +++ b/frontend/app/components/Sidebar.vue @@ -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(() => { + + + + + + + + + + + + + + + + + + + + +
+ 目标路径: + {{ cloneForm.parentDir }}/{{ cloneForm.name }} +
+
+
+ } + */ +export function CloneProject(platform, username, repoURL, parentDir, name) { + return $Call.ByID(2347019414, platform, username, repoURL, parentDir, name); +} + /** * CommitChanges 提交已暂存的更改 * @param {string} path diff --git a/internal/app/service.go b/internal/app/service.go index 2bc52b8..5ef3c6f 100644 --- a/internal/app/service.go +++ b/internal/app/service.go @@ -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] diff --git a/internal/git/client.go b/internal/git/client.go index d276a30..9447dab 100644 --- a/internal/git/client.go +++ b/internal/git/client.go @@ -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) {