更新若干功能
This commit is contained in:
150
clone.go
Normal file
150
clone.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"view/platform"
|
||||
)
|
||||
|
||||
// ---------------- 从 Git 克隆并添加项目 ----------------
|
||||
|
||||
// repoNameFromURL 从 Git 地址推导目录名(支持 https / ssh / 本地路径形式)。
|
||||
func repoNameFromURL(u string) string {
|
||||
s := strings.TrimSpace(u)
|
||||
s = strings.TrimRight(s, "/\\")
|
||||
s = strings.TrimSuffix(s, ".git")
|
||||
if i := strings.LastIndexAny(s, "/:\\"); i >= 0 {
|
||||
s = s[i+1:]
|
||||
}
|
||||
s = regexp.MustCompile(`[<>:"/\\|?*\s]+`).ReplaceAllString(s, "-")
|
||||
return strings.Trim(s, "-.")
|
||||
}
|
||||
|
||||
var cloneProgressRe = regexp.MustCompile(`(Counting objects|Compressing objects|Receiving objects|Resolving deltas):\s+(\d+)%`)
|
||||
|
||||
// cloneProgress 把 git 各阶段百分比映射到 0-98 的总体进度。
|
||||
func cloneProgress(line string) (int, bool) {
|
||||
m := cloneProgressRe.FindStringSubmatch(line)
|
||||
if m == nil {
|
||||
return 0, false
|
||||
}
|
||||
pct, _ := strconv.Atoi(m[2])
|
||||
switch m[1] {
|
||||
case "Counting objects":
|
||||
return 2 + pct*3/100, true
|
||||
case "Compressing objects":
|
||||
return 5 + pct*5/100, true
|
||||
case "Receiving objects":
|
||||
return 10 + pct*75/100, true
|
||||
default: // Resolving deltas
|
||||
return 85 + pct*13/100, true
|
||||
}
|
||||
}
|
||||
|
||||
// scanCRLines 以 \r 或 \n 切行:git 进度用回车原地刷新,普通行以换行结束。
|
||||
func scanCRLines(data []byte, atEOF bool) (int, []byte, error) {
|
||||
if atEOF && len(data) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
if i := bytes.IndexAny(data, "\r\n"); i >= 0 {
|
||||
return i + 1, data[:i], nil
|
||||
}
|
||||
if atEOF {
|
||||
return len(data), data, nil
|
||||
}
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
// CloneProject 把远程仓库克隆到指定父目录后直接加入项目列表。
|
||||
// 进度通过 analysis:progress 事件复用任务条 / 全屏加载展示。
|
||||
func (a *App) CloneProject(in CloneInput) (Project, error) {
|
||||
if e := a.ready(); e != nil {
|
||||
return Project{}, e
|
||||
}
|
||||
url := strings.TrimSpace(in.URL)
|
||||
if url == "" {
|
||||
return Project{}, errors.New("URL_REQUIRED")
|
||||
}
|
||||
parent := strings.TrimSpace(in.ParentDir)
|
||||
if st, e := os.Stat(parent); e != nil || !st.IsDir() {
|
||||
return Project{}, errors.New("DIR_NOT_FOUND")
|
||||
}
|
||||
name := strings.TrimSpace(in.Name)
|
||||
if name == "" {
|
||||
name = repoNameFromURL(url)
|
||||
}
|
||||
if name == "" {
|
||||
return Project{}, errors.New("NAME_REQUIRED")
|
||||
}
|
||||
target := filepath.Join(parent, name)
|
||||
if ents, e := os.ReadDir(target); e == nil && len(ents) > 0 {
|
||||
return Project{}, errors.New("TARGET_NOT_EMPTY")
|
||||
}
|
||||
|
||||
taskID := "clone-" + name
|
||||
emitP := func(stage string, progress int, key string) {
|
||||
a.emit("analysis:progress", TaskEvent{TaskID: taskID, Stage: stage, Progress: progress, MessageKey: key, Params: map[string]any{"project": name}})
|
||||
}
|
||||
emitP("cloning", 1, "task.cloning")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Minute)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, "git", "clone", "--progress", url, target)
|
||||
// 禁止 git 弹出交互式凭据询问,私有仓库直接快速失败
|
||||
cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0", "GCM_INTERACTIVE=never")
|
||||
platform.ConfigureHidden(cmd)
|
||||
stderr, e := cmd.StderrPipe()
|
||||
if e != nil {
|
||||
emitP("error", 100, "task.cloneFailed")
|
||||
return Project{}, e
|
||||
}
|
||||
if e := cmd.Start(); e != nil {
|
||||
emitP("error", 100, "task.cloneFailed")
|
||||
a.store.Log("error", "项目", "Git 克隆失败", url+" | "+e.Error())
|
||||
return Project{}, e
|
||||
}
|
||||
sc := bufio.NewScanner(stderr)
|
||||
sc.Split(scanCRLines)
|
||||
tail := []string{}
|
||||
for sc.Scan() {
|
||||
line := strings.TrimSpace(sc.Text())
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if len(tail) >= 6 {
|
||||
tail = tail[1:]
|
||||
}
|
||||
tail = append(tail, line)
|
||||
if p, ok := cloneProgress(line); ok {
|
||||
emitP("cloning", p, "task.cloning")
|
||||
}
|
||||
}
|
||||
if e := cmd.Wait(); e != nil {
|
||||
_ = os.RemoveAll(target) // 清掉半成品目录(克隆前已确认为空/不存在)
|
||||
msg := strings.Join(tail, "\n")
|
||||
a.store.Log("error", "项目", "Git 克隆失败", url+" | "+msg)
|
||||
emitP("error", 100, "task.cloneFailed")
|
||||
if msg != "" {
|
||||
return Project{}, errors.New(msg)
|
||||
}
|
||||
return Project{}, e
|
||||
}
|
||||
p, e := a.SaveProject(0, ProjectInput{Name: name, Path: target, Description: in.Description, GroupID: in.GroupID})
|
||||
if e != nil {
|
||||
emitP("error", 100, "task.cloneFailed")
|
||||
return p, e
|
||||
}
|
||||
a.store.Log("info", "项目", "Git 克隆成功", url+" -> "+target)
|
||||
emitP("completed", 100, "task.cloneDone")
|
||||
return p, nil
|
||||
}
|
||||
Reference in New Issue
Block a user