Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Storing branch commits count in db rather than caching them in memory or redis #33954

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions models/git/branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ type Branch struct {
Repo *repo_model.Repository `xorm:"-"`
Name string `xorm:"UNIQUE(s) NOT NULL"` // git's ref-name is case-sensitive internally, however, in some databases (mssql, mysql, by default), it's case-insensitive at the moment
CommitID string
CommitCountID string // the commit id of the commit count
CommitCount int64 // the number of commits in this branch
CommitMessage string `xorm:"TEXT"` // it only stores the message summary (the first line)
PusherID int64
Pusher *user_model.User `xorm:"-"`
Expand Down Expand Up @@ -251,6 +253,16 @@ func UpdateBranch(ctx context.Context, repoID, pusherID int64, branchName string
})
}

func UpdateBranchCommitCount(ctx context.Context, repoID int64, branchName, commitID string, commitCount int64) error {
_, err := db.GetEngine(ctx).Where("repo_id=? AND name=?", repoID, branchName).
Cols("commit_count", "commit_count_id").
Update(&Branch{
CommitCount: commitCount,
CommitCountID: commitID,
})
return err
}

// AddDeletedBranch adds a deleted branch to the database
func AddDeletedBranch(ctx context.Context, repoID int64, branchName string, deletedByID int64) error {
branch, err := GetBranch(ctx, repoID, branchName)
Expand Down
35 changes: 35 additions & 0 deletions models/git/branch_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,38 @@ func FindBranchesByRepoAndBranchName(ctx context.Context, repoBranches map[int64
}
return branchMap, nil
}

func FindCommitsCountOutdatedBranches(ctx context.Context, startID, limit int64) (BranchList, error) {
var branches BranchList
if err := db.GetEngine(ctx).
Join("INNER", "repository", "branch.repo_id = repository.id").
And("repository.is_empty = ?", false).
Where("id > ?", startID).
And(
builder.Expr("commit_count_id IS NULL").
Or(builder.Expr("commit_id <> commit_count_id")),
).
Asc("id").
Limit(int(limit)).Find(&branches); err != nil {
return nil, err
}
return branches, nil
}

func FindRepoCommitsCountOutdatedBranches(ctx context.Context, repoID, startID, limit int64) (BranchList, error) {
var branches BranchList
if err := db.GetEngine(ctx).
Join("INNER", "repository", "branch.repo_id = repository.id").
Where("branch.repo_id = ?", repoID).
And("repository.is_empty = ?", false).
And("id > ?", startID).
And(
builder.Expr("commit_count_id IS NULL").
Or(builder.Expr("commit_id <> commit_count_id")),
).
Asc("id").
Limit(int(limit)).Find(&branches); err != nil {
return nil, err
}
return branches, nil
}
1 change: 1 addition & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ func prepareMigrationTasks() []*migration {
newMigration(315, "Add Ephemeral to ActionRunner", v1_24.AddEphemeralToActionRunner),
newMigration(316, "Add description for secrets and variables", v1_24.AddDescriptionForSecretsAndVariables),
newMigration(317, "Add new index for action for heatmap", v1_24.AddNewIndexForUserDashboard),
newMigration(318, "Add branch commits count for branch table", v1_24.AddBranchCommitsCount),
}
return preparedMigrations
}
Expand Down
16 changes: 16 additions & 0 deletions models/migrations/v1_24/v318.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package v1_24 //nolint

import (
"xorm.io/xorm"
)

func AddBranchCommitsCount(x *xorm.Engine) error {
type Branch struct {
CommitCountID string // the commit id of the commit count
CommitCount int64 // the number of commits in this branch
}
return x.Sync(new(Branch))
}
12 changes: 12 additions & 0 deletions modules/git/ref.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,18 @@ func RefNameFromCommit(shortName string) RefName {
return RefName(shortName)
}

func RefNameFromObjectTypeAndShortName(objectType ObjectType, shortName string) RefName {
switch objectType {
case ObjectBranch:
return RefNameFromBranch(shortName)
case ObjectTag:
return RefNameFromTag(shortName)
case ObjectCommit:
return RefNameFromCommit(shortName)
}
return RefName(shortName)
}

func (ref RefName) String() string {
return string(ref)
}
Expand Down
12 changes: 1 addition & 11 deletions modules/git/repo_commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -502,18 +502,8 @@ func (repo *Repository) IsCommitInBranch(commitID, branch string) (r bool, err e
return len(stdout) > 0, err
}

func (repo *Repository) AddLastCommitCache(cacheKey, fullName, sha string) error {
func (repo *Repository) AddLastCommitCache(commitsCount int64, fullName, sha string) error {
if repo.LastCommitCache == nil {
commitsCount, err := cache.GetInt64(cacheKey, func() (int64, error) {
commit, err := repo.GetCommit(sha)
if err != nil {
return 0, err
}
return commit.CommitsCount()
})
if err != nil {
return err
}
repo.LastCommitCache = NewLastCommitCache(commitsCount, fullName, repo, cache.GetCache())
}
return nil
Expand Down
17 changes: 17 additions & 0 deletions modules/gitrepo/commit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package gitrepo

import (
"context"

"code.gitea.io/gitea/modules/git"
)

func CommitsCount(ctx context.Context, repo Repository, ref string) (int64, error) {
return git.CommitsCount(ctx, git.CommitsCountOptions{
RepoPath: repoPath(repo),
Revision: []string{ref},
})
}
1 change: 1 addition & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3016,6 +3016,7 @@ dashboard.sync_branch.started = Branches Sync started
dashboard.sync_tag.started = Tags Sync started
dashboard.rebuild_issue_indexer = Rebuild issue indexer
dashboard.sync_repo_licenses = Sync repo licenses
dashboard.sync_branch_commits_count = Sync Branch commits counts

users.user_manage_panel = User Account Management
users.new_account = Create User Account
Expand Down
3 changes: 2 additions & 1 deletion routers/api/v1/utils/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ func ResolveRefOrSha(ctx *context.APIContext, ref string) string {
sha = MustConvertToSHA1(ctx, ctx.Repo, sha)

if ctx.Repo.GitRepo != nil {
err := ctx.Repo.GitRepo.AddLastCommitCache(ctx.Repo.Repository.GetCommitsCountCacheKey(ref, ref != sha), ctx.Repo.Repository.FullName(), sha)
commitsCount, _ := context.GetRefCommitsCount(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, git.RefName(ref))
err := ctx.Repo.GitRepo.AddLastCommitCache(commitsCount, ctx.Repo.Repository.FullName(), sha)
if err != nil {
log.Error("Unable to get commits count for %s in %s. Error: %v", sha, ctx.Repo.Repository.FullName(), err)
}
Expand Down
2 changes: 1 addition & 1 deletion routers/web/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func CommitInfoCache(ctx *context.Context) {
ctx.ServerError("GetBranchCommit", err)
return
}
ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount()
ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount(ctx)
if err != nil {
ctx.ServerError("GetCommitsCount", err)
return
Expand Down
45 changes: 37 additions & 8 deletions services/context/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,16 +163,45 @@ func (r *Repository) CanCreateIssueDependencies(ctx context.Context, user *user_
return r.Repository.IsDependenciesEnabled(ctx) && r.Permission.CanWriteIssuesOrPulls(isPull)
}

// getCommitsCountCacheKey returns cache key used for commits count caching.
func getCommitsCountCacheKey(contextName string, repoID int64) string {
return fmt.Sprintf("commits-count-%d-commit-%s", repoID, contextName)
}

func GetRefCommitsCount(ctx context.Context, repoID int64, gitRepo *git.Repository, refFullName git.RefName) (int64, error) {
// Get the commit count of the branch or the tag
switch {
case refFullName.IsBranch():
branch, err := git_model.GetBranch(ctx, repoID, refFullName.BranchName())
if err != nil {
return 0, err
}
return branch.CommitCount, nil
case refFullName.IsTag():
tag, err := repo_model.GetRelease(ctx, repoID, refFullName.TagName())
if err != nil {
return 0, err
}
return tag.NumCommits, nil
case refFullName.RefType() == git.RefTypeCommit:
return cache.GetInt64(getCommitsCountCacheKey(string(refFullName), repoID), func() (int64, error) {
commit, err := gitRepo.GetCommit(string(refFullName))
if err != nil {
return 0, err
}
return commit.CommitsCount()
})
default:
return 0, nil
}
}

// GetCommitsCount returns cached commit count for current view
func (r *Repository) GetCommitsCount() (int64, error) {
func (r *Repository) GetCommitsCount(ctx context.Context) (int64, error) {
if r.Commit == nil {
return 0, nil
}
contextName := r.RefFullName.ShortName()
isRef := r.RefFullName.IsBranch() || r.RefFullName.IsTag()
return cache.GetInt64(r.Repository.GetCommitsCountCacheKey(contextName, isRef), func() (int64, error) {
return r.Commit.CommitsCount()
})
return GetRefCommitsCount(ctx, r.Repository.ID, r.GitRepo, r.RefFullName)
}

// GetCommitGraphsCount returns cached commit count for current view
Expand Down Expand Up @@ -782,7 +811,7 @@ func RepoRefByDefaultBranch() func(*Context) {
ctx.Repo.RefFullName = git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch)
ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch
ctx.Repo.Commit, _ = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
ctx.Repo.CommitsCount, _ = ctx.Repo.GetCommitsCount()
ctx.Repo.CommitsCount, _ = ctx.Repo.GetCommitsCount(ctx)
ctx.Data["RefFullName"] = ctx.Repo.RefFullName
ctx.Data["BranchName"] = ctx.Repo.BranchName
ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
Expand Down Expand Up @@ -931,7 +960,7 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) {

ctx.Data["CanCreateBranch"] = ctx.Repo.CanCreateBranch() // only used by the branch selector dropdown: AllowCreateNewRef

ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount()
ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount(ctx)
if err != nil {
ctx.ServerError("GetCommitsCount", err)
return
Expand Down
11 changes: 11 additions & 0 deletions services/cron/tasks_basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,16 @@ func registerSyncRepoLicenses() {
})
}

func registerSyncBranchCommitsCount() {
RegisterTaskFatal("sync_branch_commits_count", &BaseConfig{
Enabled: true,
RunAtStart: true,
Schedule: "@midnight",
}, func(ctx context.Context, _ *user_model.User, _ Config) error {
return repo_service.SyncBranchCommitsCount(ctx)
})
}

func initBasicTasks() {
if setting.Mirror.Enabled {
registerUpdateMirrorTask()
Expand All @@ -183,4 +193,5 @@ func initBasicTasks() {
registerCleanupPackages()
}
registerSyncRepoLicenses()
registerSyncBranchCommitsCount()
}
12 changes: 2 additions & 10 deletions services/mirror/mirror_pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (

repo_model "code.gitea.io/gitea/models/repo"
system_model "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git"
giturl "code.gitea.io/gitea/modules/git/url"
"code.gitea.io/gitea/modules/gitrepo"
Expand Down Expand Up @@ -411,15 +410,8 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
log.Trace("SyncMirrors [repo: %-v Wiki]: git remote update complete", m.Repo)
}

log.Trace("SyncMirrors [repo: %-v]: invalidating mirror branch caches...", m.Repo)
branches, _, err := gitrepo.GetBranchesByPath(ctx, m.Repo, 0, 0)
if err != nil {
log.Error("SyncMirrors [repo: %-v]: failed to GetBranches: %v", m.Repo, err)
return nil, false
}

for _, branch := range branches {
cache.Remove(m.Repo.GetCommitsCountCacheKey(branch.Name, true))
if err := repo_service.SyncRepoBranchesCommitsCount(ctx, m.Repo); err != nil {
log.Error("SyncMirrors [repo: %-v]: failed to sync branches commits count: %v", m.Repo, err)
}

m.UpdatedUnix = timeutil.TimeStampNow()
Expand Down
4 changes: 0 additions & 4 deletions services/pull/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/globallock"
"code.gitea.io/gitea/modules/httplib"
Expand Down Expand Up @@ -251,9 +250,6 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U
notify_service.MergePullRequest(ctx, doer, pr)
}

// Reset cached commit count
cache.Remove(pr.Issue.Repo.GetCommitsCountCacheKey(pr.BaseBranch, true))

return handleCloseCrossReferences(ctx, pr, doer)
}

Expand Down
76 changes: 76 additions & 0 deletions services/repository/branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -736,3 +736,79 @@ func GetBranchDivergingInfo(ctx reqctx.RequestContext, baseRepo *repo_model.Repo
info.BaseHasNewCommits = info.HeadCommitsBehind > 0
return info, nil
}

func SyncRepoBranchesCommitsCount(ctx context.Context, repo *repo_model.Repository) error {
startID := int64(0)
for {
select {
case <-ctx.Done():
return nil
default:
}

// search all branches commits count are not synced
branches, err := git_model.FindRepoCommitsCountOutdatedBranches(ctx, repo.ID, startID, 100)
if err != nil {
return err
}
if len(branches) == 0 {
return nil
}

for _, branch := range branches {
branch.Repo = repo
if branch.ID > startID {
startID = branch.ID
}
if err := syncBranchCommitsCount(ctx, branch); err != nil {
log.Error("syncBranchCommitsCount: %v", err)
}
}
}
}

func SyncBranchCommitsCount(ctx context.Context) error {
startID := int64(0)
for {
select {
case <-ctx.Done():
return nil
default:
}

// search all branches commits count are not synced
branches, err := git_model.FindCommitsCountOutdatedBranches(ctx, startID, 100)
if err != nil {
return err
}
if len(branches) == 0 {
return nil
}

if err := branches.LoadRepo(ctx); err != nil {
return err
}

for _, branch := range branches {
if branch.ID > startID {
startID = branch.ID
}
if err := syncBranchCommitsCount(ctx, branch); err != nil {
log.Error("syncBranchCommitsCount: %v", err)
}
}
}
}

func syncBranchCommitsCount(ctx context.Context, branch *git_model.Branch) error {
if branch.CommitID == "" {
return nil
}

commitsCount, err := gitrepo.CommitsCount(ctx, branch.Repo, branch.CommitID)
if err != nil {
return err
}

return git_model.UpdateBranchCommitCount(ctx, branch.RepoID, branch.Name, branch.CommitID, commitsCount)
}
Loading
Loading