From 36dc4fe82590120bda35e3be60276d8242f6e63a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauris=20Buk=C5=A1is-Haberkorns?= Date: Wed, 15 Aug 2018 03:18:31 +0300 Subject: [PATCH 1/8] Initial implementation for git statistics in Activity tab --- models/repo_activity.go | 112 +++++++++++++++++++++++++++++++++-- routers/repo/activity.go | 3 +- templates/repo/activity.tmpl | 9 +++ 3 files changed, 117 insertions(+), 7 deletions(-) diff --git a/models/repo_activity.go b/models/repo_activity.go index c3017e8e397fd..79a70a6bbed29 100644 --- a/models/repo_activity.go +++ b/models/repo_activity.go @@ -5,12 +5,26 @@ package models import ( + "bufio" "fmt" + "strconv" + "strings" "time" + "code.gitea.io/gitea/modules/process" + "github.com/go-xorm/xorm" ) +type CodeActivityStats struct { + AuthorCount int64 + CommitCount int64 + ChangedFiles int64 + Additions int64 + Deletions int64 + CommitCountInAllBranches int64 +} + // ActivityStats represets issue and pull request information. type ActivityStats struct { OpenedPRs PullRequestList @@ -24,29 +38,35 @@ type ActivityStats struct { UnresolvedIssues IssueList PublishedReleases []*Release PublishedReleaseAuthorCount int64 + Code *CodeActivityStats } // GetActivityStats return stats for repository at given time range -func GetActivityStats(repoID int64, timeFrom time.Time, releases, issues, prs bool) (*ActivityStats, error) { - stats := &ActivityStats{} +func GetActivityStats(repo *Repository, timeFrom time.Time, releases, issues, prs, code bool) (*ActivityStats, error) { + stats := &ActivityStats{Code: &CodeActivityStats{}} if releases { - if err := stats.FillReleases(repoID, timeFrom); err != nil { + if err := stats.FillReleases(repo.ID, timeFrom); err != nil { return nil, fmt.Errorf("FillReleases: %v", err) } } if prs { - if err := stats.FillPullRequests(repoID, timeFrom); err != nil { + if err := stats.FillPullRequests(repo.ID, timeFrom); err != nil { return nil, fmt.Errorf("FillPullRequests: %v", err) } } if issues { - if err := stats.FillIssues(repoID, timeFrom); err != nil { + if err := stats.FillIssues(repo.ID, timeFrom); err != nil { return nil, fmt.Errorf("FillIssues: %v", err) } } - if err := stats.FillUnresolvedIssues(repoID, timeFrom, issues, prs); err != nil { + if err := stats.FillUnresolvedIssues(repo.ID, timeFrom, issues, prs); err != nil { return nil, fmt.Errorf("FillUnresolvedIssues: %v", err) } + if code { + if err := stats.Code.FillFromGit(repo, timeFrom); err != nil { + return nil, fmt.Errorf("FillFromGit: %v", err) + } + } return stats, nil } @@ -269,3 +289,83 @@ func releasesForActivityStatement(repoID int64, fromTime time.Time) *xorm.Sessio And("release.is_draft = ?", false). And("release.created_unix >= ?", fromTime.Unix()) } + +// FillFromGit returns code statistics for acitivity page +func (stats *CodeActivityStats) FillFromGit(repo *Repository, fromTime time.Time) error { + gitPath := repo.RepoPath() + since := fromTime.Format(time.RFC3339) + + if stdout, stderr, err := process.GetManager().ExecDir(-1, gitPath, + fmt.Sprintf("FillFromGit.RevList (git rev-list): %s", gitPath), + "git", "rev-list", "--count", "--no-merges", "--branches=*", "--date=iso", fmt.Sprintf("--since='%s'", since)); err != nil { + return fmt.Errorf("git rev-list --count --branch [%s]: %s", gitPath, stderr) + } else { + if c, err := strconv.ParseInt(strings.TrimSpace(stdout), 10, 64); err != nil { + return err + } else { + stats.CommitCountInAllBranches = c + } + } + + if stdout, stderr, err := process.GetManager().ExecDir(-1, gitPath, + fmt.Sprintf("FillFromGit.RevList (git rev-list): %s", gitPath), + "git", "log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%an%n%ae%n", "--first-parent", "--date=iso", fmt.Sprintf("--since='%s'", since), repo.DefaultBranch); err != nil { + return fmt.Errorf("git log --numstat --first-parent [%s -> %s]: %s", repo.DefaultBranch, gitPath, stderr) + } else { + scanner := bufio.NewScanner(strings.NewReader(stdout)) + scanner.Split(bufio.ScanLines) + stats.CommitCount = 0 + stats.Additions = 0 + stats.Deletions = 0 + authors := make(map[string]int64) + files := make(map[string]bool) + p := 0 + for scanner.Scan() { + l := strings.TrimSpace(scanner.Text()) + if l == "---" { + p = 1 + } else if p == 0 { + continue + } else { + p++ + } + if p > 4 && len(l) == 0 { + continue + } + switch p { + case 1: // Seperator + case 2: // Commit sha-1 + stats.CommitCount++ + case 3: // Author + //fmt.Println("Author: " + l) + case 4: // E-mail + email := strings.ToLower(l) + i := authors[email] + authors[email] = i + 1 + default: // Changed fileB + fmt.Println("L:" + l) + if parts := strings.Fields(l); len(parts) >= 3 { + if parts[0] != "-" { + if c, err := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 64); err == nil { + stats.Additions += c + } + } + if parts[1] != "-" { + if c, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64); err == nil { + stats.Deletions += c + } + } + if _, ok := files[parts[2]]; !ok { + files[parts[2]] = true + } + } else { + fmt.Println("err fields") + } + } + } + stats.AuthorCount = int64(len(authors)) + stats.ChangedFiles = int64(len(files)) + } + + return nil +} diff --git a/routers/repo/activity.go b/routers/repo/activity.go index 5d90d73506db2..c89bb41483797 100644 --- a/routers/repo/activity.go +++ b/routers/repo/activity.go @@ -47,7 +47,8 @@ func Activity(ctx *context.Context) { if ctx.Data["Activity"], err = models.GetActivityStats(ctx.Repo.Repository.ID, timeFrom, ctx.Repo.CanRead(models.UnitTypeReleases), ctx.Repo.CanRead(models.UnitTypeIssues), - ctx.Repo.CanRead(models.UnitTypePullRequests)); err != nil { + ctx.Repo.CanRead(models.UnitTypePullRequests), + ctx.Repo.CanRead(models.UnitTypeCode)); err != nil { ctx.ServerError("GetActivityStats", err) return } diff --git a/templates/repo/activity.tmpl b/templates/repo/activity.tmpl index 2b8fbc6c1c872..935f9c60fbe94 100644 --- a/templates/repo/activity.tmpl +++ b/templates/repo/activity.tmpl @@ -81,6 +81,15 @@ {{end}} + + Excluding merges, {{.Activity.Code.AuthorCount}} authors + has pushed {{.Activity.Code.CommitCount}} commits to {{.Repository.DefaultBranch}} + and {{.Activity.Code.CommitCountInAllBranches}} commits to all branches. + On {{.Repository.DefaultBranch}}, {{.Activity.Code.ChangedFiles}} files have changed + and there have been {{.Activity.Code.Additions}} additions + and {{.Activity.Code.Deletions}} deletions. + + {{if gt .Activity.PublishedReleaseCount 0}}

From 842277ad2ac155109a0fde1724d4269b6ca8db8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauris=20Buk=C5=A1is-Haberkorns?= Date: Thu, 16 Aug 2018 02:06:21 +0300 Subject: [PATCH 2/8] Create top user by commit count endpoint --- models/repo_activity.go | 194 +++++++++++++++++++++++++++------------ routers/repo/activity.go | 34 +++++++ routers/routes/routes.go | 5 + 3 files changed, 172 insertions(+), 61 deletions(-) diff --git a/models/repo_activity.go b/models/repo_activity.go index 79a70a6bbed29..521983aae9829 100644 --- a/models/repo_activity.go +++ b/models/repo_activity.go @@ -7,6 +7,7 @@ package models import ( "bufio" "fmt" + "sort" "strconv" "strings" "time" @@ -16,6 +17,15 @@ import ( "github.com/go-xorm/xorm" ) +// ActivityAuthorData represents statistical git commit count data +type ActivityAuthorData struct { + Name string `json:"name"` + Login string `json:"login"` + AvatarLink string `json:"avatar_link"` + Commits int64 `json:"commits"` +} + +// CodeActivityStats represents git statistics data type CodeActivityStats struct { AuthorCount int64 CommitCount int64 @@ -23,6 +33,7 @@ type CodeActivityStats struct { Additions int64 Deletions int64 CommitCountInAllBranches int64 + Authors map[string]int64 } // ActivityStats represets issue and pull request information. @@ -63,13 +74,22 @@ func GetActivityStats(repo *Repository, timeFrom time.Time, releases, issues, pr return nil, fmt.Errorf("FillUnresolvedIssues: %v", err) } if code { - if err := stats.Code.FillFromGit(repo, timeFrom); err != nil { + if err := stats.Code.FillFromGit(repo, timeFrom, false); err != nil { return nil, fmt.Errorf("FillFromGit: %v", err) } } return stats, nil } +// GetActivityStatsAuthors returns stats for git commits for all branches +func GetActivityStatsAuthors(repo *Repository, timeFrom time.Time) (*CodeActivityStats, error) { + code := &CodeActivityStats{} + if err := code.FillFromGit(repo, timeFrom, true); err != nil { + return nil, fmt.Errorf("FillFromGit: %v", err) + } + return code, nil +} + // ActivePRCount returns total active pull request count func (stats *ActivityStats) ActivePRCount() int { return stats.OpenedPRCount() + stats.MergedPRCount() @@ -291,81 +311,133 @@ func releasesForActivityStatement(repoID int64, fromTime time.Time) *xorm.Sessio } // FillFromGit returns code statistics for acitivity page -func (stats *CodeActivityStats) FillFromGit(repo *Repository, fromTime time.Time) error { +func (stats *CodeActivityStats) FillFromGit(repo *Repository, fromTime time.Time, allBranches bool) error { gitPath := repo.RepoPath() since := fromTime.Format(time.RFC3339) - if stdout, stderr, err := process.GetManager().ExecDir(-1, gitPath, + stdout, stderr, err := process.GetManager().ExecDir(-1, gitPath, fmt.Sprintf("FillFromGit.RevList (git rev-list): %s", gitPath), - "git", "rev-list", "--count", "--no-merges", "--branches=*", "--date=iso", fmt.Sprintf("--since='%s'", since)); err != nil { + "git", "rev-list", "--count", "--no-merges", "--branches=*", "--date=iso", fmt.Sprintf("--since='%s'", since)) + if err != nil { return fmt.Errorf("git rev-list --count --branch [%s]: %s", gitPath, stderr) + } + + c, err := strconv.ParseInt(strings.TrimSpace(stdout), 10, 64) + if err != nil { + return err + } + stats.CommitCountInAllBranches = c + + args := []string{"log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%an%n%ae%n", "--date=iso", fmt.Sprintf("--since='%s'", since)} + if allBranches { + args = append(args, "--branches=*") } else { - if c, err := strconv.ParseInt(strings.TrimSpace(stdout), 10, 64); err != nil { - return err - } else { - stats.CommitCountInAllBranches = c - } + args = append(args, "--first-parent", repo.DefaultBranch) } - if stdout, stderr, err := process.GetManager().ExecDir(-1, gitPath, + stdout, stderr, err = process.GetManager().ExecDir(-1, gitPath, fmt.Sprintf("FillFromGit.RevList (git rev-list): %s", gitPath), - "git", "log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%an%n%ae%n", "--first-parent", "--date=iso", fmt.Sprintf("--since='%s'", since), repo.DefaultBranch); err != nil { - return fmt.Errorf("git log --numstat --first-parent [%s -> %s]: %s", repo.DefaultBranch, gitPath, stderr) - } else { - scanner := bufio.NewScanner(strings.NewReader(stdout)) - scanner.Split(bufio.ScanLines) - stats.CommitCount = 0 - stats.Additions = 0 - stats.Deletions = 0 - authors := make(map[string]int64) - files := make(map[string]bool) - p := 0 - for scanner.Scan() { - l := strings.TrimSpace(scanner.Text()) - if l == "---" { - p = 1 - } else if p == 0 { - continue - } else { - p++ - } - if p > 4 && len(l) == 0 { - continue - } - switch p { - case 1: // Seperator - case 2: // Commit sha-1 - stats.CommitCount++ - case 3: // Author - //fmt.Println("Author: " + l) - case 4: // E-mail - email := strings.ToLower(l) - i := authors[email] - authors[email] = i + 1 - default: // Changed fileB - fmt.Println("L:" + l) - if parts := strings.Fields(l); len(parts) >= 3 { - if parts[0] != "-" { - if c, err := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 64); err == nil { - stats.Additions += c - } - } - if parts[1] != "-" { - if c, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64); err == nil { - stats.Deletions += c - } + "git", args...) + if err != nil { + return fmt.Errorf("git log --numstat [%s]: %s", gitPath, stderr) + } + + scanner := bufio.NewScanner(strings.NewReader(stdout)) + scanner.Split(bufio.ScanLines) + stats.CommitCount = 0 + stats.Additions = 0 + stats.Deletions = 0 + authors := make(map[string]int64) + files := make(map[string]bool) + p := 0 + for scanner.Scan() { + l := strings.TrimSpace(scanner.Text()) + if l == "---" { + p = 1 + } else if p == 0 { + continue + } else { + p++ + } + if p > 4 && len(l) == 0 { + continue + } + switch p { + case 1: // Seperator + case 2: // Commit sha-1 + stats.CommitCount++ + case 3: // Author + //fmt.Println("Author: " + l) + case 4: // E-mail + email := strings.ToLower(l) + i := authors[email] + authors[email] = i + 1 + default: // Changed file + if parts := strings.Fields(l); len(parts) >= 3 { + if parts[0] != "-" { + if c, err := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 64); err == nil { + stats.Additions += c } - if _, ok := files[parts[2]]; !ok { - files[parts[2]] = true + } + if parts[1] != "-" { + if c, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64); err == nil { + stats.Deletions += c } - } else { - fmt.Println("err fields") + } + if _, ok := files[parts[2]]; !ok { + files[parts[2]] = true } } } - stats.AuthorCount = int64(len(authors)) - stats.ChangedFiles = int64(len(files)) } + stats.AuthorCount = int64(len(authors)) + stats.ChangedFiles = int64(len(files)) + stats.Authors = authors return nil } + +// GetTopAuthors get top users with most commit count based on already loaded data from git +func (stats *CodeActivityStats) GetTopAuthors(count int) ([]*ActivityAuthorData, error) { + if stats.Authors == nil { + return nil, nil + } + users := make(map[int64]*ActivityAuthorData) + for k, v := range stats.Authors { + if len(k) == 0 { + continue + } + u, err := GetUserByEmail(k) + if u == nil || IsErrUserNotExist(err) { + continue + } + if err != nil { + return nil, err + } + if user, ok := users[u.ID]; !ok { + users[u.ID] = &ActivityAuthorData{ + Name: u.DisplayName(), + Login: u.LowerName, + AvatarLink: u.AvatarLink(), + Commits: v, + } + } else { + user.Commits += v + } + } + v := make([]*ActivityAuthorData, 0) + for _, u := range users { + v = append(v, u) + } + + sort.Slice(v[:], func(i, j int) bool { + return v[i].Commits < v[j].Commits + }) + + cnt := count + if cnt > len(v) { + cnt = len(v) + } + + return v[:cnt], nil +} diff --git a/routers/repo/activity.go b/routers/repo/activity.go index c89bb41483797..d569b7a0c6d03 100644 --- a/routers/repo/activity.go +++ b/routers/repo/activity.go @@ -55,3 +55,37 @@ func Activity(ctx *context.Context) { ctx.HTML(200, tplActivity) } + +// ActivityAuthors renders JSON with top commit authors for given time period over all branches +func ActivityAuthors(ctx *context.Context) { + timeUntil := time.Now() + var timeFrom time.Time + + switch ctx.Params("period") { + case "daily": + timeFrom = timeUntil.Add(-time.Hour * 24) + case "halfweekly": + timeFrom = timeUntil.Add(-time.Hour * 72) + case "weekly": + timeFrom = timeUntil.Add(-time.Hour * 168) + case "monthly": + timeFrom = timeUntil.AddDate(0, -1, 0) + default: + timeFrom = timeUntil.Add(-time.Hour * 168) + } + + var err error + code, err := models.GetActivityStatsAuthors(ctx.Repo.Repository, timeFrom) + if err != nil { + ctx.ServerError("GetActivityStatsAuthors", err) + return + } + + authors, err := code.GetTopAuthors(10) + if err != nil { + ctx.ServerError("GetTopAuthors", err) + return + } + + ctx.JSON(200, authors) +} diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 5fa37a841765c..938afcab791a9 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -802,6 +802,11 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get("/:period", repo.Activity) }, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(models.UnitTypePullRequests, models.UnitTypeIssues, models.UnitTypeReleases)) + m.Group("/activity_author_data", func() { + m.Get("", repo.ActivityAuthors) + m.Get("/:period", repo.ActivityAuthors) + }, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(models.UnitTypeCode)) + m.Get("/archive/*", repo.MustBeNotEmpty, reqRepoCodeReader, repo.Download) m.Group("/branches", func() { From 251e168a1dcb30c362d83efdfa14e82615b4dcba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauris=20Buk=C5=A1is-Haberkorns?= Date: Sat, 23 Feb 2019 23:17:35 +0200 Subject: [PATCH 3/8] Add UI and update src-d/go-git dependency --- options/locale/locale_en-US.ini | 18 +++++++++++++++++ routers/repo/activity.go | 2 +- templates/repo/activity.tmpl | 34 +++++++++++++++++++++++++-------- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index eedede2a05859..b9b0f6b70bec2 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1061,6 +1061,24 @@ activity.title.releases_1 = %d Release activity.title.releases_n = %d Releases activity.title.releases_published_by = %s published by %s activity.published_release_label = Published +activity.no_git_activity = There has been any commit activity in this period. +activity.git_stats_exclude_merges = Excluding merges, +activity.git_stats_author_1 = %d author +activity.git_stats_author_n = %d authors +activity.git_stats_pushed = has pushed +activity.git_stats_commit_1 = %d commit +activity.git_stats_commit_n = %d commits +activity.git_stats_push_to_branch = to %s and +activity.git_stats_push_to_all_branches = to all branches. +activity.git_stats_on_default_branch = On %s, +activity.git_stats_file_1 = %d file +activity.git_stats_file_n = %d files +activity.git_stats_files_changed = have changed and there have been +activity.git_stats_addition_1 = %d addition +activity.git_stats_addition_n = %d additions +activity.git_stats_and_deletions = and +activity.git_stats_deletion_1 = %d deletion +activity.git_stats_deletion_n = %d deletions search = Search search.search_repo = Search repository diff --git a/routers/repo/activity.go b/routers/repo/activity.go index d569b7a0c6d03..5889cf140261a 100644 --- a/routers/repo/activity.go +++ b/routers/repo/activity.go @@ -44,7 +44,7 @@ func Activity(ctx *context.Context) { ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string)) var err error - if ctx.Data["Activity"], err = models.GetActivityStats(ctx.Repo.Repository.ID, timeFrom, + if ctx.Data["Activity"], err = models.GetActivityStats(ctx.Repo.Repository, timeFrom, ctx.Repo.CanRead(models.UnitTypeReleases), ctx.Repo.CanRead(models.UnitTypeIssues), ctx.Repo.CanRead(models.UnitTypePullRequests), diff --git a/templates/repo/activity.tmpl b/templates/repo/activity.tmpl index 935f9c60fbe94..9970838560c27 100644 --- a/templates/repo/activity.tmpl +++ b/templates/repo/activity.tmpl @@ -81,14 +81,32 @@ {{end}} - - Excluding merges, {{.Activity.Code.AuthorCount}} authors - has pushed {{.Activity.Code.CommitCount}} commits to {{.Repository.DefaultBranch}} - and {{.Activity.Code.CommitCountInAllBranches}} commits to all branches. - On {{.Repository.DefaultBranch}}, {{.Activity.Code.ChangedFiles}} files have changed - and there have been {{.Activity.Code.Additions}} additions - and {{.Activity.Code.Deletions}} deletions. - + {{if .Permission.CanRead $.UnitTypeCode}} + {{if eq .Activity.Code.CommitCountInAllBranches 0}} +
+

{{.i18n.Tr "repo.activity.no_git_activity" }}

+
+ {{end}} + {{if gt .Activity.Code.CommitCountInAllBranches 0}} +
+
+ {{.i18n.Tr "repo.activity.git_stats_exclude_merges" }} + {{.i18n.Tr (TrN .i18n.Lang .Activity.Code.AuthorCount "repo.activity.git_stats_author_1" "repo.activity.git_stats_author_n") .Activity.Code.AuthorCount }} + {{.i18n.Tr "repo.activity.git_stats_pushed" }} + {{.i18n.Tr (TrN .i18n.Lang .Activity.Code.CommitCount "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n") .Activity.Code.CommitCount }} + {{.i18n.Tr "repo.activity.git_stats_push_to_branch" .Repository.DefaultBranch }} + {{.i18n.Tr (TrN .i18n.Lang .Activity.Code.CommitCountInAllBranches "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n") .Activity.Code.CommitCountInAllBranches }} + {{.i18n.Tr "repo.activity.git_stats_push_to_all_branches" }} + {{.i18n.Tr "repo.activity.git_stats_on_default_branch" .Repository.DefaultBranch }} + {{.i18n.Tr (TrN .i18n.Lang .Activity.Code.ChangedFiles "repo.activity.git_stats_file_1" "repo.activity.git_stats_file_n") .Activity.Code.ChangedFiles }} + {{.i18n.Tr "repo.activity.git_stats_files_changed" }} + {{.i18n.Tr (TrN .i18n.Lang .Activity.Code.Additions "repo.activity.git_stats_addition_1" "repo.activity.git_stats_addition_n") .Activity.Code.Additions }} + {{.i18n.Tr "repo.activity.git_stats_and_deletions" }} + {{.i18n.Tr (TrN .i18n.Lang .Activity.Code.Deletions "repo.activity.git_stats_deletion_1" "repo.activity.git_stats_deletion_n") .Activity.Code.Deletions }}. +
+
+ {{end}} + {{end}} {{if gt .Activity.PublishedReleaseCount 0}}

From 070c8c17be2b543e3f721059e5579b5fba03f841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauris=20Buk=C5=A1is-Haberkorns?= Date: Sat, 23 Feb 2019 23:28:33 +0200 Subject: [PATCH 4/8] Add coloring --- templates/repo/activity.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/repo/activity.tmpl b/templates/repo/activity.tmpl index 9970838560c27..5b6559c8bf03a 100644 --- a/templates/repo/activity.tmpl +++ b/templates/repo/activity.tmpl @@ -100,9 +100,9 @@ {{.i18n.Tr "repo.activity.git_stats_on_default_branch" .Repository.DefaultBranch }} {{.i18n.Tr (TrN .i18n.Lang .Activity.Code.ChangedFiles "repo.activity.git_stats_file_1" "repo.activity.git_stats_file_n") .Activity.Code.ChangedFiles }} {{.i18n.Tr "repo.activity.git_stats_files_changed" }} - {{.i18n.Tr (TrN .i18n.Lang .Activity.Code.Additions "repo.activity.git_stats_addition_1" "repo.activity.git_stats_addition_n") .Activity.Code.Additions }} + {{.i18n.Tr (TrN .i18n.Lang .Activity.Code.Additions "repo.activity.git_stats_addition_1" "repo.activity.git_stats_addition_n") .Activity.Code.Additions }} {{.i18n.Tr "repo.activity.git_stats_and_deletions" }} - {{.i18n.Tr (TrN .i18n.Lang .Activity.Code.Deletions "repo.activity.git_stats_deletion_1" "repo.activity.git_stats_deletion_n") .Activity.Code.Deletions }}. + {{.i18n.Tr (TrN .i18n.Lang .Activity.Code.Deletions "repo.activity.git_stats_deletion_1" "repo.activity.git_stats_deletion_n") .Activity.Code.Deletions }}. {{end}} From 5e96ab1752bbdc823ab6cafd0a0df48650cf7364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauris=20Buk=C5=A1is-Haberkorns?= Date: Sat, 23 Feb 2019 23:36:05 +0200 Subject: [PATCH 5/8] Fix typo --- models/repo_activity.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/repo_activity.go b/models/repo_activity.go index 521983aae9829..c1dce72a98ca8 100644 --- a/models/repo_activity.go +++ b/models/repo_activity.go @@ -363,7 +363,7 @@ func (stats *CodeActivityStats) FillFromGit(repo *Repository, fromTime time.Time continue } switch p { - case 1: // Seperator + case 1: // Separator case 2: // Commit sha-1 stats.CommitCount++ case 3: // Author From 3336bda09c7e53d455fa1e3d660cfd61d176dd21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauris=20Buk=C5=A1is-Haberkorns?= Date: Wed, 1 May 2019 00:29:43 +0300 Subject: [PATCH 6/8] Move git activity stats data extraction to git module --- models/repo_activity.go | 214 +++++++++++--------------------------- modules/git/repo_stats.go | 108 +++++++++++++++++++ routers/repo/activity.go | 10 +- 3 files changed, 169 insertions(+), 163 deletions(-) create mode 100644 modules/git/repo_stats.go diff --git a/models/repo_activity.go b/models/repo_activity.go index c1dce72a98ca8..fb1385a54b12e 100644 --- a/models/repo_activity.go +++ b/models/repo_activity.go @@ -5,14 +5,11 @@ package models import ( - "bufio" "fmt" "sort" - "strconv" - "strings" "time" - "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/modules/git" "github.com/go-xorm/xorm" ) @@ -25,17 +22,6 @@ type ActivityAuthorData struct { Commits int64 `json:"commits"` } -// CodeActivityStats represents git statistics data -type CodeActivityStats struct { - AuthorCount int64 - CommitCount int64 - ChangedFiles int64 - Additions int64 - Deletions int64 - CommitCountInAllBranches int64 - Authors map[string]int64 -} - // ActivityStats represets issue and pull request information. type ActivityStats struct { OpenedPRs PullRequestList @@ -49,12 +35,12 @@ type ActivityStats struct { UnresolvedIssues IssueList PublishedReleases []*Release PublishedReleaseAuthorCount int64 - Code *CodeActivityStats + Code *git.CodeActivityStats } // GetActivityStats return stats for repository at given time range func GetActivityStats(repo *Repository, timeFrom time.Time, releases, issues, prs, code bool) (*ActivityStats, error) { - stats := &ActivityStats{Code: &CodeActivityStats{}} + stats := &ActivityStats{Code: &git.CodeActivityStats{}} if releases { if err := stats.FillReleases(repo.ID, timeFrom); err != nil { return nil, fmt.Errorf("FillReleases: %v", err) @@ -74,20 +60,70 @@ func GetActivityStats(repo *Repository, timeFrom time.Time, releases, issues, pr return nil, fmt.Errorf("FillUnresolvedIssues: %v", err) } if code { - if err := stats.Code.FillFromGit(repo, timeFrom, false); err != nil { + gitRepo, err := git.OpenRepository(repo.RepoPath()) + if err != nil { + return nil, fmt.Errorf("OpenRepository: %v", err) + } + code, err := gitRepo.GetCodeActivityStats(timeFrom, repo.DefaultBranch) + if err != nil { return nil, fmt.Errorf("FillFromGit: %v", err) } + stats.Code = code } return stats, nil } -// GetActivityStatsAuthors returns stats for git commits for all branches -func GetActivityStatsAuthors(repo *Repository, timeFrom time.Time) (*CodeActivityStats, error) { - code := &CodeActivityStats{} - if err := code.FillFromGit(repo, timeFrom, true); err != nil { +// GetActivityStatsTopAuthors returns top author stats for git commits for all branches +func GetActivityStatsTopAuthors(repo *Repository, timeFrom time.Time, count int) ([]*ActivityAuthorData, error) { + gitRepo, err := git.OpenRepository(repo.RepoPath()) + if err != nil { + return nil, fmt.Errorf("OpenRepository: %v", err) + } + code, err := gitRepo.GetCodeActivityStats(timeFrom, "") + if err != nil { return nil, fmt.Errorf("FillFromGit: %v", err) } - return code, nil + if code.Authors == nil { + return nil, nil + } + users := make(map[int64]*ActivityAuthorData) + for k, v := range code.Authors { + if len(k) == 0 { + continue + } + u, err := GetUserByEmail(k) + if u == nil || IsErrUserNotExist(err) { + continue + } + if err != nil { + return nil, err + } + if user, ok := users[u.ID]; !ok { + users[u.ID] = &ActivityAuthorData{ + Name: u.DisplayName(), + Login: u.LowerName, + AvatarLink: u.AvatarLink(), + Commits: v, + } + } else { + user.Commits += v + } + } + v := make([]*ActivityAuthorData, 0) + for _, u := range users { + v = append(v, u) + } + + sort.Slice(v[:], func(i, j int) bool { + return v[i].Commits < v[j].Commits + }) + + cnt := count + if cnt > len(v) { + cnt = len(v) + } + + return v[:cnt], nil } // ActivePRCount returns total active pull request count @@ -309,135 +345,3 @@ func releasesForActivityStatement(repoID int64, fromTime time.Time) *xorm.Sessio And("release.is_draft = ?", false). And("release.created_unix >= ?", fromTime.Unix()) } - -// FillFromGit returns code statistics for acitivity page -func (stats *CodeActivityStats) FillFromGit(repo *Repository, fromTime time.Time, allBranches bool) error { - gitPath := repo.RepoPath() - since := fromTime.Format(time.RFC3339) - - stdout, stderr, err := process.GetManager().ExecDir(-1, gitPath, - fmt.Sprintf("FillFromGit.RevList (git rev-list): %s", gitPath), - "git", "rev-list", "--count", "--no-merges", "--branches=*", "--date=iso", fmt.Sprintf("--since='%s'", since)) - if err != nil { - return fmt.Errorf("git rev-list --count --branch [%s]: %s", gitPath, stderr) - } - - c, err := strconv.ParseInt(strings.TrimSpace(stdout), 10, 64) - if err != nil { - return err - } - stats.CommitCountInAllBranches = c - - args := []string{"log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%an%n%ae%n", "--date=iso", fmt.Sprintf("--since='%s'", since)} - if allBranches { - args = append(args, "--branches=*") - } else { - args = append(args, "--first-parent", repo.DefaultBranch) - } - - stdout, stderr, err = process.GetManager().ExecDir(-1, gitPath, - fmt.Sprintf("FillFromGit.RevList (git rev-list): %s", gitPath), - "git", args...) - if err != nil { - return fmt.Errorf("git log --numstat [%s]: %s", gitPath, stderr) - } - - scanner := bufio.NewScanner(strings.NewReader(stdout)) - scanner.Split(bufio.ScanLines) - stats.CommitCount = 0 - stats.Additions = 0 - stats.Deletions = 0 - authors := make(map[string]int64) - files := make(map[string]bool) - p := 0 - for scanner.Scan() { - l := strings.TrimSpace(scanner.Text()) - if l == "---" { - p = 1 - } else if p == 0 { - continue - } else { - p++ - } - if p > 4 && len(l) == 0 { - continue - } - switch p { - case 1: // Separator - case 2: // Commit sha-1 - stats.CommitCount++ - case 3: // Author - //fmt.Println("Author: " + l) - case 4: // E-mail - email := strings.ToLower(l) - i := authors[email] - authors[email] = i + 1 - default: // Changed file - if parts := strings.Fields(l); len(parts) >= 3 { - if parts[0] != "-" { - if c, err := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 64); err == nil { - stats.Additions += c - } - } - if parts[1] != "-" { - if c, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64); err == nil { - stats.Deletions += c - } - } - if _, ok := files[parts[2]]; !ok { - files[parts[2]] = true - } - } - } - } - stats.AuthorCount = int64(len(authors)) - stats.ChangedFiles = int64(len(files)) - stats.Authors = authors - - return nil -} - -// GetTopAuthors get top users with most commit count based on already loaded data from git -func (stats *CodeActivityStats) GetTopAuthors(count int) ([]*ActivityAuthorData, error) { - if stats.Authors == nil { - return nil, nil - } - users := make(map[int64]*ActivityAuthorData) - for k, v := range stats.Authors { - if len(k) == 0 { - continue - } - u, err := GetUserByEmail(k) - if u == nil || IsErrUserNotExist(err) { - continue - } - if err != nil { - return nil, err - } - if user, ok := users[u.ID]; !ok { - users[u.ID] = &ActivityAuthorData{ - Name: u.DisplayName(), - Login: u.LowerName, - AvatarLink: u.AvatarLink(), - Commits: v, - } - } else { - user.Commits += v - } - } - v := make([]*ActivityAuthorData, 0) - for _, u := range users { - v = append(v, u) - } - - sort.Slice(v[:], func(i, j int) bool { - return v[i].Commits < v[j].Commits - }) - - cnt := count - if cnt > len(v) { - cnt = len(v) - } - - return v[:cnt], nil -} diff --git a/modules/git/repo_stats.go b/modules/git/repo_stats.go new file mode 100644 index 0000000000000..aa62e7420342a --- /dev/null +++ b/modules/git/repo_stats.go @@ -0,0 +1,108 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package git + +import ( + "bufio" + "bytes" + "fmt" + "strconv" + "strings" + "time" +) + +// CodeActivityStats represents git statistics data +type CodeActivityStats struct { + AuthorCount int64 + CommitCount int64 + ChangedFiles int64 + Additions int64 + Deletions int64 + CommitCountInAllBranches int64 + Authors map[string]int64 +} + +// GetCodeActivityStats returns code statistics for acitivity page +func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) (*CodeActivityStats, error) { + stats := &CodeActivityStats{} + + since := fromTime.Format(time.RFC3339) + + stdout, err := NewCommand("rev-list", "--count", "--no-merges", "--branches=*", "--date=iso", fmt.Sprintf("--since='%s'", since)).RunInDirBytes(repo.Path) + if err != nil { + return nil, err + } + + c, err := strconv.ParseInt(strings.TrimSpace(string(stdout)), 10, 64) + if err != nil { + return nil, err + } + stats.CommitCountInAllBranches = c + + args := []string{"log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%an%n%ae%n", "--date=iso", fmt.Sprintf("--since='%s'", since)} + if len(branch) == 0 { + args = append(args, "--branches=*") + } else { + args = append(args, "--first-parent", branch) + } + + stdout, err = NewCommand(args...).RunInDirBytes(repo.Path) + if err != nil { + return nil, err + } + + scanner := bufio.NewScanner(bytes.NewReader(stdout)) + scanner.Split(bufio.ScanLines) + stats.CommitCount = 0 + stats.Additions = 0 + stats.Deletions = 0 + authors := make(map[string]int64) + files := make(map[string]bool) + p := 0 + for scanner.Scan() { + l := strings.TrimSpace(scanner.Text()) + if l == "---" { + p = 1 + } else if p == 0 { + continue + } else { + p++ + } + if p > 4 && len(l) == 0 { + continue + } + switch p { + case 1: // Separator + case 2: // Commit sha-1 + stats.CommitCount++ + case 3: // Author + case 4: // E-mail + email := strings.ToLower(l) + i := authors[email] + authors[email] = i + 1 + default: // Changed file + if parts := strings.Fields(l); len(parts) >= 3 { + if parts[0] != "-" { + if c, err := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 64); err == nil { + stats.Additions += c + } + } + if parts[1] != "-" { + if c, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64); err == nil { + stats.Deletions += c + } + } + if _, ok := files[parts[2]]; !ok { + files[parts[2]] = true + } + } + } + } + stats.AuthorCount = int64(len(authors)) + stats.ChangedFiles = int64(len(files)) + stats.Authors = authors + + return stats, nil +} diff --git a/routers/repo/activity.go b/routers/repo/activity.go index 5889cf140261a..e170a91299915 100644 --- a/routers/repo/activity.go +++ b/routers/repo/activity.go @@ -75,15 +75,9 @@ func ActivityAuthors(ctx *context.Context) { } var err error - code, err := models.GetActivityStatsAuthors(ctx.Repo.Repository, timeFrom) + authors, err := models.GetActivityStatsTopAuthors(ctx.Repo.Repository, timeFrom, 10) if err != nil { - ctx.ServerError("GetActivityStatsAuthors", err) - return - } - - authors, err := code.GetTopAuthors(10) - if err != nil { - ctx.ServerError("GetTopAuthors", err) + ctx.ServerError("GetActivityStatsTopAuthors", err) return } From 5c15007ea24bf06997c99706e2cb81e4fee78952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauris=20Buk=C5=A1is-Haberkorns?= Date: Wed, 1 May 2019 00:34:32 +0300 Subject: [PATCH 7/8] Fix message --- options/locale/locale_en-US.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index b9b0f6b70bec2..fe90d65451e48 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1061,7 +1061,7 @@ activity.title.releases_1 = %d Release activity.title.releases_n = %d Releases activity.title.releases_published_by = %s published by %s activity.published_release_label = Published -activity.no_git_activity = There has been any commit activity in this period. +activity.no_git_activity = There has not been any commit activity in this period. activity.git_stats_exclude_merges = Excluding merges, activity.git_stats_author_1 = %d author activity.git_stats_author_n = %d authors From 5d4e114dcad62f3aab5dd65b63c769b76c3e1346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauris=20Buk=C5=A1is-Haberkorns?= Date: Wed, 1 May 2019 13:01:51 +0300 Subject: [PATCH 8/8] Add git code stats test --- modules/git/repo_stats_test.go | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 modules/git/repo_stats_test.go diff --git a/modules/git/repo_stats_test.go b/modules/git/repo_stats_test.go new file mode 100644 index 0000000000000..2e8565b9e28c2 --- /dev/null +++ b/modules/git/repo_stats_test.go @@ -0,0 +1,35 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package git + +import ( + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestRepository_GetCodeActivityStats(t *testing.T) { + bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") + bareRepo1, err := OpenRepository(bareRepo1Path) + assert.NoError(t, err) + + timeFrom, err := time.Parse(time.RFC3339, "2016-01-01T00:00:00+00:00") + + code, err := bareRepo1.GetCodeActivityStats(timeFrom, "") + assert.NoError(t, err) + assert.NotNil(t, code) + + assert.EqualValues(t, 8, code.CommitCount) + assert.EqualValues(t, 2, code.AuthorCount) + assert.EqualValues(t, 8, code.CommitCountInAllBranches) + assert.EqualValues(t, 10, code.Additions) + assert.EqualValues(t, 1, code.Deletions) + assert.Len(t, code.Authors, 2) + assert.Contains(t, code.Authors, "tris.git@shoddynet.org") + assert.EqualValues(t, 3, code.Authors["tris.git@shoddynet.org"]) + assert.EqualValues(t, 5, code.Authors[""]) +}