mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08:00 
			
		
		
		
	Git statistics in Activity tab (#4724)
* Initial implementation for git statistics in Activity tab * Create top user by commit count endpoint * Add UI and update src-d/go-git dependency * Add coloring * Fix typo * Move git activity stats data extraction to git module * Fix message * Add git code stats test
This commit is contained in:
		@@ -6,11 +6,22 @@ package models
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
 | 
			
		||||
	"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"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ActivityStats represets issue and pull request information.
 | 
			
		||||
type ActivityStats struct {
 | 
			
		||||
	OpenedPRs                   PullRequestList
 | 
			
		||||
@@ -24,32 +35,97 @@ type ActivityStats struct {
 | 
			
		||||
	UnresolvedIssues            IssueList
 | 
			
		||||
	PublishedReleases           []*Release
 | 
			
		||||
	PublishedReleaseAuthorCount int64
 | 
			
		||||
	Code                        *git.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: &git.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 {
 | 
			
		||||
		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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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)
 | 
			
		||||
	}
 | 
			
		||||
	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
 | 
			
		||||
func (stats *ActivityStats) ActivePRCount() int {
 | 
			
		||||
	return stats.OpenedPRCount() + stats.MergedPRCount()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										108
									
								
								modules/git/repo_stats.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								modules/git/repo_stats.go
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										35
									
								
								modules/git/repo_stats_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								modules/git/repo_stats_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -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[""])
 | 
			
		||||
}
 | 
			
		||||
@@ -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 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
 | 
			
		||||
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
 | 
			
		||||
 
 | 
			
		||||
@@ -44,13 +44,42 @@ 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)); err != nil {
 | 
			
		||||
		ctx.Repo.CanRead(models.UnitTypePullRequests),
 | 
			
		||||
		ctx.Repo.CanRead(models.UnitTypeCode)); err != nil {
 | 
			
		||||
		ctx.ServerError("GetActivityStats", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
	authors, err := models.GetActivityStatsTopAuthors(ctx.Repo.Repository, timeFrom, 10)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("GetActivityStatsTopAuthors", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.JSON(200, authors)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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() {
 | 
			
		||||
 
 | 
			
		||||
@@ -81,6 +81,33 @@
 | 
			
		||||
		</div>
 | 
			
		||||
		{{end}}
 | 
			
		||||
 | 
			
		||||
		{{if .Permission.CanRead $.UnitTypeCode}}
 | 
			
		||||
			{{if eq .Activity.Code.CommitCountInAllBranches 0}}
 | 
			
		||||
				<div class="ui center aligned segment">
 | 
			
		||||
				<h4 class="ui header">{{.i18n.Tr "repo.activity.no_git_activity" }}</h4>
 | 
			
		||||
				</div>
 | 
			
		||||
			{{end}}
 | 
			
		||||
			{{if gt .Activity.Code.CommitCountInAllBranches 0}}
 | 
			
		||||
				<div class="ui attached segment horizontal segments">
 | 
			
		||||
					<div class="ui attached segment text">
 | 
			
		||||
					    {{.i18n.Tr "repo.activity.git_stats_exclude_merges" }}
 | 
			
		||||
						<strong>{{.i18n.Tr (TrN .i18n.Lang .Activity.Code.AuthorCount "repo.activity.git_stats_author_1" "repo.activity.git_stats_author_n") .Activity.Code.AuthorCount }}</strong>
 | 
			
		||||
						{{.i18n.Tr "repo.activity.git_stats_pushed" }}
 | 
			
		||||
						<strong>{{.i18n.Tr (TrN .i18n.Lang .Activity.Code.CommitCount "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n") .Activity.Code.CommitCount }}</strong>
 | 
			
		||||
						{{.i18n.Tr "repo.activity.git_stats_push_to_branch" .Repository.DefaultBranch }}
 | 
			
		||||
						<strong>{{.i18n.Tr (TrN .i18n.Lang .Activity.Code.CommitCountInAllBranches "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n") .Activity.Code.CommitCountInAllBranches }}</strong>
 | 
			
		||||
						{{.i18n.Tr "repo.activity.git_stats_push_to_all_branches" }}
 | 
			
		||||
						{{.i18n.Tr "repo.activity.git_stats_on_default_branch" .Repository.DefaultBranch }}
 | 
			
		||||
						<strong>{{.i18n.Tr (TrN .i18n.Lang .Activity.Code.ChangedFiles "repo.activity.git_stats_file_1" "repo.activity.git_stats_file_n") .Activity.Code.ChangedFiles }}</strong>
 | 
			
		||||
						{{.i18n.Tr "repo.activity.git_stats_files_changed" }}
 | 
			
		||||
						<strong class="text green">{{.i18n.Tr (TrN .i18n.Lang .Activity.Code.Additions "repo.activity.git_stats_addition_1" "repo.activity.git_stats_addition_n") .Activity.Code.Additions }}</strong>
 | 
			
		||||
						{{.i18n.Tr "repo.activity.git_stats_and_deletions" }}
 | 
			
		||||
						<strong class="text red">{{.i18n.Tr (TrN .i18n.Lang .Activity.Code.Deletions "repo.activity.git_stats_deletion_1" "repo.activity.git_stats_deletion_n") .Activity.Code.Deletions }}</strong>.
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			{{end}}
 | 
			
		||||
		{{end}}
 | 
			
		||||
 | 
			
		||||
		{{if gt .Activity.PublishedReleaseCount 0}}
 | 
			
		||||
			<h4 class="ui horizontal divider header" id="published-releases">
 | 
			
		||||
				<i class="text octicon octicon-tag"></i>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user