更新若干功能

This commit is contained in:
李琦
2026-08-14 07:51:46 +08:00
parent d2aeb13a09
commit 153c7ed448
48 changed files with 5567 additions and 2673 deletions

View File

@@ -129,27 +129,46 @@ func (g GitService) Diagnostics(ctx context.Context, dir string, ref string) mod
return d
}
// gitLogMaxCommits 限制单次 git log 解析的提交数量,避免超大仓库拖垮分析。
const gitLogMaxCommits = 20000
// GitAnalyzeOptions 控制 Git 分析的范围与增量起点。
type GitAnalyzeOptions struct {
Ref string // 统计视图引用;为空表示当前分支
AllBranches bool // true 时统计所有分支gitScope=all
SinceHash string // 已入库的最新提交哈希;非空且仍在历史内时执行增量分析
}
// Analyze 分析工作区当前分支。
func (g GitService) Analyze(ctx context.Context, dir string) (model.GitStats, error) {
return g.AnalyzeRef(ctx, dir, "")
stats, _, e := g.AnalyzeWithOptions(ctx, dir, GitAnalyzeOptions{})
return stats, e
}
// AnalyzeRef 只切换统计视图,不修改用户工作区。
func (g GitService) AnalyzeRef(ctx context.Context, dir, ref string) (model.GitStats, error) {
stats, _, e := g.AnalyzeWithOptions(ctx, dir, GitAnalyzeOptions{Ref: ref})
return stats, e
}
// AnalyzeWithOptions 返回统计结果和 incremental 标记:
// incremental=true 时结果只包含 SinceHash 之后的新提交,调用方应执行合并而非整表替换。
func (g GitService) AnalyzeWithOptions(ctx context.Context, dir string, opt GitAnalyzeOptions) (model.GitStats, bool, error) {
if _, e := g.run(ctx, dir, "rev-parse", "--git-dir"); e != nil {
return model.GitStats{Available: false, Error: e.Error()}, e
return model.GitStats{Available: false, Error: e.Error()}, false, e
}
branch, _ := g.run(ctx, dir, "branch", "--show-current")
branch = strings.TrimSpace(branch)
ref := opt.Ref
if ref == "" {
ref = branch
}
out := model.GitStats{Available: true, CurrentBranch: branch, WorkspaceBranch: branch, ViewRef: ref, Commits: []model.GitCommit{}, Refs: []model.GitRef{}, Contributors: []model.Contributor{}, Heatmap: []model.HeatDay{}}
refs, e := g.run(ctx, dir, "for-each-ref", "--format=%(refname:short)%x1f%(objectname)%x1f%(refname)", "refs/heads", "refs/remotes")
out := model.GitStats{Available: true, CurrentBranch: branch, WorkspaceBranch: branch, ViewRef: ref, Commits: []model.GitCommit{}, Refs: []model.GitRef{}, Contributors: []model.Contributor{}, Heatmap: []model.HeatDay{}, Hotspots: []model.GitFileHotspot{}}
refs, e := g.run(ctx, dir, "for-each-ref", "--format=%(refname:short)%x1f%(objectname)%x1f%(refname)", "refs/heads", "refs/remotes", "refs/tags")
if e != nil {
out.Available = false
out.Error = e.Error()
return out, e
return out, false, e
}
for _, line := range strings.Split(refs, "\n") {
p := strings.Split(line, "\x1f")
@@ -162,21 +181,43 @@ func (g GitService) AnalyzeRef(ctx context.Context, dir, ref string) (model.GitS
kind := "local"
if strings.HasPrefix(p[2], "refs/remotes/") {
kind = "remote"
} else if strings.HasPrefix(p[2], "refs/tags/") {
kind = "tag"
}
out.Refs = append(out.Refs, model.GitRef{Name: p[0], Hash: ShortHash(p[1]), Kind: kind, Current: p[0] == branch})
}
args := []string{"log", "--use-mailmap", "--date=iso-strict", "--pretty=format:@@CC@@%H%x1f%aN%x1f%aE%x1f%aI%x1f%s", "--numstat"}
if strings.TrimSpace(ref) != "" {
incremental := opt.SinceHash != "" && g.canIncrement(ctx, dir, opt.SinceHash, ref, opt.AllBranches)
args := []string{"log", "--use-mailmap", "--date=iso-strict", "--pretty=format:@@CC@@%H%x1f%aN%x1f%aE%x1f%aI%x1f%s", "--numstat", "--max-count", strconv.Itoa(gitLogMaxCommits)}
if opt.AllBranches {
args = append(args, "--all")
}
if incremental {
args = append(args, "^"+opt.SinceHash)
}
if !opt.AllBranches && strings.TrimSpace(ref) != "" {
args = append(args, ref)
}
log, e := g.run(ctx, dir, args...)
if e != nil {
out.Available = false
out.Error = e.Error()
return out, e
return out, false, e
}
parseLog(log, &out)
return out, nil
return out, incremental, nil
}
// canIncrement 校验增量起点仍然存在且属于目标历史,否则回退全量分析。
func (g GitService) canIncrement(ctx context.Context, dir, since, ref string, all bool) bool {
if _, e := g.run(ctx, dir, "rev-parse", "--verify", "--quiet", since+"^{commit}"); e != nil {
return false
}
target := strings.TrimSpace(ref)
if all || target == "" {
target = "HEAD"
}
_, e := g.run(ctx, dir, "merge-base", "--is-ancestor", since, target)
return e == nil
}
// CommitDetail 按需读取单次提交和逐文件增删行,避免 Git 首页一次传输过多数据。
@@ -253,11 +294,15 @@ func (g GitService) CheckoutBranch(ctx context.Context, dir, ref string) (model.
func parseLog(log string, out *model.GitStats) {
var cur *model.GitCommit
fm := map[string]*model.GitFileHotspot{}
flush := func() {
if cur != nil {
out.Commits = append(out.Commits, *cur)
}
}
for _, line := range strings.Split(log, "\n") {
if strings.HasPrefix(line, "@@CC@@") {
if cur != nil {
out.Commits = append(out.Commits, *cur)
}
flush()
p := strings.Split(strings.TrimPrefix(line, "@@CC@@"), "\x1f")
if len(p) == 5 {
cur = &model.GitCommit{Hash: p[0], Author: p[1], Email: strings.ToLower(p[2]), Date: p[3], Message: p[4]}
@@ -268,23 +313,34 @@ func parseLog(log string, out *model.GitStats) {
}
if cur != nil {
p := strings.Split(line, "\t")
if len(p) >= 2 {
if len(p) >= 3 {
a, e1 := strconv.ParseInt(p[0], 10, 64)
d, e2 := strconv.ParseInt(p[1], 10, 64)
if e1 == nil {
cur.Added += a
if e1 != nil {
a = 0
}
if e2 == nil {
cur.Deleted += d
if e2 != nil {
d = 0
}
cur.Added += a
cur.Deleted += d
path := strings.TrimSpace(p[2])
if path != "" {
h := fm[path]
if h == nil {
h = &model.GitFileHotspot{Path: path}
fm[path] = h
}
h.Changes++
h.Added += a
h.Deleted += d
}
}
}
}
if cur != nil {
out.Commits = append(out.Commits, *cur)
}
flush()
cm := map[string]*model.Contributor{}
hm := map[string]int64{}
hm := map[string]*model.HeatDay{}
cut := time.Now().AddDate(-1, 0, 0)
for _, c := range out.Commits {
out.Added += c.Added
@@ -298,7 +354,15 @@ func parseLog(log string, out *model.GitStats) {
x.Added += c.Added
x.Deleted += c.Deleted
if t, e := time.Parse(time.RFC3339, c.Date); e == nil && t.After(cut) {
hm[t.Local().Format("2006-01-02")]++
key := t.Local().Format("2006-01-02")
d := hm[key]
if d == nil {
d = &model.HeatDay{Date: key}
hm[key] = d
}
d.Count++
d.Added += c.Added
d.Deleted += c.Deleted
}
}
out.CommitCount = int64(len(out.Commits))
@@ -307,10 +371,22 @@ func parseLog(log string, out *model.GitStats) {
}
out.ContributorCount = int64(len(out.Contributors))
sort.Slice(out.Contributors, func(i, j int) bool { return out.Contributors[i].Commits > out.Contributors[j].Commits })
for d, c := range hm {
out.Heatmap = append(out.Heatmap, model.HeatDay{Date: d, Count: c})
for _, d := range hm {
out.Heatmap = append(out.Heatmap, *d)
}
sort.Slice(out.Heatmap, func(i, j int) bool { return out.Heatmap[i].Date < out.Heatmap[j].Date })
for _, h := range fm {
out.Hotspots = append(out.Hotspots, *h)
}
sort.Slice(out.Hotspots, func(i, j int) bool {
if out.Hotspots[i].Changes != out.Hotspots[j].Changes {
return out.Hotspots[i].Changes > out.Hotspots[j].Changes
}
return out.Hotspots[i].Added+out.Hotspots[i].Deleted > out.Hotspots[j].Added+out.Hotspots[j].Deleted
})
if len(out.Hotspots) > 50 {
out.Hotspots = out.Hotspots[:50]
}
}
func ShortHash(s string) string {