更新若干功能

This commit is contained in:
李琦
2026-08-14 07:52:01 +08:00
parent 153c7ed448
commit 89bc265f68
196 changed files with 17675 additions and 0 deletions

150
clone.go Normal file
View 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
}