更新若干功能
This commit is contained in:
124
service/git.go
124
service/git.go
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user