Files
code-utils/clone.go
2026-08-14 07:52:01 +08:00

151 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}