新增远程项目克隆功能
This commit is contained in:
@@ -7,7 +7,10 @@ import {
|
|||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
|
CloudDownloadOutlined,
|
||||||
|
LoadingOutlined,
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
import { AppService } from '../../bindings/github.com/zhuy1228/GitPilot/internal/app'
|
import { AppService } from '../../bindings/github.com/zhuy1228/GitPilot/internal/app'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -24,6 +27,11 @@ 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 contextMenu = ref({ visible: false, x: 0, y: 0, type: '', data: {} })
|
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 '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 +175,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 +184,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 +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) {
|
async function removeProject(platform, username, name) {
|
||||||
if (!confirm(`确定删除项目 "${name}" 吗?`)) return
|
if (!confirm(`确定删除项目 "${name}" 吗?`)) return
|
||||||
try {
|
try {
|
||||||
@@ -441,6 +503,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"
|
||||||
|
|||||||
@@ -52,6 +52,19 @@ export function AddUser(platform, username, token) {
|
|||||||
return $Call.ByID(2264426704, 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 提交已暂存的更改
|
* CommitChanges 提交已暂存的更改
|
||||||
* @param {string} path
|
* @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 添加项目到指定平台/用户下
|
// 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]
|
||||||
|
|||||||
@@ -65,7 +65,28 @@ func (g *GitClient) Status(path string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user