feat(package): gitea: add heatmap contribution model

This commit is contained in:
2026-06-06 19:20:37 +00:00
parent 3299daf061
commit 53dfd60b4c
5 changed files with 210 additions and 0 deletions
@@ -0,0 +1,73 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package activities
import (
"context"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm/schemas"
)
// HeatmapContribution stores commit contributions counted for profile heatmaps.
type HeatmapContribution struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"NOT NULL"`
RepoID int64 `xorm:"NOT NULL"`
CommitSHA string `xorm:"VARCHAR(64) NOT NULL"`
AuthorEmail string `xorm:"VARCHAR(320) NOT NULL"`
AuthorUnix timeutil.TimeStamp `xorm:"NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
func init() {
db.RegisterModel(new(HeatmapContribution))
}
// TableIndices implements xorm's TableIndices interface.
func (c *HeatmapContribution) TableIndices() []*schemas.Index {
uniqueContribution := schemas.NewIndex("repo_commit_user", schemas.UniqueType)
uniqueContribution.AddColumn("repo_id", "commit_sha", "user_id")
userAuthorRepo := schemas.NewIndex("u_a_r", schemas.IndexType)
userAuthorRepo.AddColumn("user_id", "author_unix", "repo_id")
return []*schemas.Index{uniqueContribution, userAuthorRepo}
}
// UpsertHeatmapContribution creates or updates a commit contribution without duplicating counted rows.
func UpsertHeatmapContribution(ctx context.Context, contribution *HeatmapContribution) error {
return db.WithTx(ctx, func(ctx context.Context) error {
e := db.GetEngine(ctx)
rows, err := e.Where("repo_id=? AND commit_sha=? AND user_id=?", contribution.RepoID, contribution.CommitSHA, contribution.UserID).
Cols("author_email", "author_unix").
Update(contribution)
if err != nil {
return err
}
if rows > 0 {
return nil
}
has, err := e.Exist(&HeatmapContribution{RepoID: contribution.RepoID, CommitSHA: contribution.CommitSHA, UserID: contribution.UserID})
if err != nil {
return err
}
if has {
return nil
}
_, err = e.Insert(contribution)
return err
})
}
// CountHeatmapContributions returns counted heatmap contribution rows for a user.
func CountHeatmapContributions(ctx context.Context, userID int64) (int64, error) {
return db.GetEngine(ctx).Where("user_id=?", userID).Count(new(HeatmapContribution))
}
@@ -0,0 +1,81 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package activities_test
import (
"testing"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHeatmapContributionModel(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
assert.NoError(t, db.TruncateBeans(t.Context(), &activities_model.HeatmapContribution{}))
const (
userID = int64(2)
repoID = int64(1)
commitSHA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
authorUnix = timeutil.TimeStamp(1579089600)
)
contribution := &activities_model.HeatmapContribution{
UserID: userID,
RepoID: repoID,
CommitSHA: commitSHA,
AuthorEmail: "user2@example.com",
AuthorUnix: authorUnix,
}
require.NoError(t, activities_model.UpsertHeatmapContribution(t.Context(), contribution))
count, err := activities_model.CountHeatmapContributions(t.Context(), userID)
require.NoError(t, err)
assert.EqualValues(t, 1, count)
require.NoError(t, activities_model.UpsertHeatmapContribution(t.Context(), &activities_model.HeatmapContribution{
UserID: userID,
RepoID: repoID,
CommitSHA: commitSHA,
AuthorEmail: "changed@example.com",
AuthorUnix: authorUnix + 900,
}))
count, err = activities_model.CountHeatmapContributions(t.Context(), userID)
require.NoError(t, err)
assert.EqualValues(t, 1, count)
stored := &activities_model.HeatmapContribution{UserID: userID, RepoID: repoID, CommitSHA: commitSHA}
has, err := db.GetEngine(t.Context()).Get(stored)
require.NoError(t, err)
require.True(t, has)
assert.Equal(t, "changed@example.com", stored.AuthorEmail)
assert.Equal(t, authorUnix+900, stored.AuthorUnix)
assert.NotZero(t, stored.CreatedUnix)
assert.NotZero(t, stored.UpdatedUnix)
err = db.Insert(t.Context(), &activities_model.HeatmapContribution{
UserID: userID,
RepoID: repoID,
CommitSHA: commitSHA,
AuthorEmail: "duplicate@example.com",
AuthorUnix: authorUnix,
})
assert.Error(t, err)
includePrivate, err := user_model.GetIncludePrivateContributions(t.Context(), userID)
require.NoError(t, err)
assert.False(t, includePrivate)
require.NoError(t, user_model.SetIncludePrivateContributions(t.Context(), userID, true))
includePrivate, err = user_model.GetIncludePrivateContributions(t.Context(), userID)
require.NoError(t, err)
assert.True(t, includePrivate)
}
@@ -405,6 +405,7 @@ func prepareMigrationTasks() []*migration {
newMigration(328, "Add TokenPermissions column to ActionRunJob", v1_26.AddTokenPermissionsToActionRunJob), newMigration(328, "Add TokenPermissions column to ActionRunJob", v1_26.AddTokenPermissionsToActionRunJob),
newMigration(329, "Add unique constraint for user badge", v1_26.AddUniqueIndexForUserBadge), newMigration(329, "Add unique constraint for user badge", v1_26.AddUniqueIndexForUserBadge),
newMigration(330, "Add name column to webhook", v1_26.AddNameToWebhook), newMigration(330, "Add name column to webhook", v1_26.AddNameToWebhook),
newMigration(331, "Create heatmap contribution table", v1_26.CreateHeatmapContributionTable),
} }
return preparedMigrations return preparedMigrations
} }
@@ -0,0 +1,38 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
type HeatmapContribution struct { //revive:disable-line:exported
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"NOT NULL"`
RepoID int64 `xorm:"NOT NULL"`
CommitSHA string `xorm:"VARCHAR(64) NOT NULL"`
AuthorEmail string `xorm:"VARCHAR(320) NOT NULL"`
AuthorUnix timeutil.TimeStamp `xorm:"NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
// TableIndices implements xorm's TableIndices interface.
func (c *HeatmapContribution) TableIndices() []*schemas.Index {
uniqueContribution := schemas.NewIndex("repo_commit_user", schemas.UniqueType)
uniqueContribution.AddColumn("repo_id", "commit_sha", "user_id")
userAuthorRepo := schemas.NewIndex("u_a_r", schemas.IndexType)
userAuthorRepo.AddColumn("user_id", "author_unix", "repo_id")
return []*schemas.Index{uniqueContribution, userAuthorRepo}
}
func CreateHeatmapContributionTable(x *xorm.Engine) error {
_, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(HeatmapContribution))
return err
}
@@ -7,6 +7,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strconv"
"strings" "strings"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@@ -19,6 +20,8 @@ import (
"xorm.io/xorm/convert" "xorm.io/xorm/convert"
) )
const SettingsKeyIncludePrivateContributions = "include_private_contributions"
// Setting is a key value store of user settings // Setting is a key value store of user settings
type Setting struct { type Setting struct {
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
@@ -254,3 +257,17 @@ func SetUserSettingJSON[T any](ctx context.Context, userID int64, key string, va
} }
return SetUserSetting(ctx, userID, key, util.UnsafeBytesToString(bs)) return SetUserSetting(ctx, userID, key, util.UnsafeBytesToString(bs))
} }
// GetIncludePrivateContributions returns whether a user opted in to private heatmap contributions.
func GetIncludePrivateContributions(ctx context.Context, userID int64) (bool, error) {
value, err := GetUserSetting(ctx, userID, SettingsKeyIncludePrivateContributions, "false")
if err != nil {
return false, err
}
return strconv.ParseBool(value)
}
// SetIncludePrivateContributions stores whether a user opted in to private heatmap contributions.
func SetIncludePrivateContributions(ctx context.Context, userID int64, include bool) error {
return SetUserSetting(ctx, userID, SettingsKeyIncludePrivateContributions, strconv.FormatBool(include))
}