Skip to content
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
128 changes: 128 additions & 0 deletions models/badges/badge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package badges

import (
"context"

"gitea.dev/models/db"
"gitea.dev/modules/util"

"xorm.io/builder"
)

// Badge represents a reputation badge
type Badge struct {
ID int64 `xorm:"pk autoincr"`
Slug string `xorm:"UNIQUE"`
Description string
ImageURL string
}

func init() {
db.RegisterModel(new(Badge))
}

// CreateBadge creates a new badge.
func CreateBadge(ctx context.Context, badge *Badge) error {
exists, err := db.GetEngine(ctx).Where("slug = ?", badge.Slug).Exist(new(Badge))
if err != nil {
return err
}
if exists {
return util.NewAlreadyExistErrorf("badge already exists [slug: %s]", badge.Slug)
}

if _, err := db.GetEngine(ctx).Insert(badge); err != nil {
// Handle race between existence check and insert.
exists, existErr := db.GetEngine(ctx).Where("slug = ?", badge.Slug).Exist(new(Badge))
if existErr == nil && exists {
return util.NewAlreadyExistErrorf("badge already exists [slug: %s]", badge.Slug)
}
return err
}
return nil
}

// GetBadge returns a specific badge
func GetBadge(ctx context.Context, slug string) (*Badge, error) {
badge := new(Badge)
has, err := db.GetEngine(ctx).Where("slug=?", slug).Get(badge)
if err != nil {
return nil, err
}
if !has {
return nil, util.NewNotExistErrorf("badge does not exist [slug: %s]", slug)
}
return badge, nil
}

// UpdateBadge updates a badge based on its slug.
func UpdateBadge(ctx context.Context, badge *Badge) error {
_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Cols("description", "image_url").Update(badge)
return err
}

// DeleteBadge deletes a badge and all associated entries.
func DeleteBadge(ctx context.Context, badge *Badge) error {
return db.WithTx(ctx, func(ctx context.Context) error {
// First delete all mapping entries for this badge
if _, err := db.GetEngine(ctx).Exec("DELETE FROM user_badge WHERE badge_id = ?", badge.ID); err != nil {
return err
}
if _, err := db.GetEngine(ctx).Exec("DELETE FROM org_badge WHERE badge_id = ?", badge.ID); err != nil {
return err
}
if _, err := db.GetEngine(ctx).Exec("DELETE FROM repo_badge WHERE badge_id = ?", badge.ID); err != nil {
return err
}

// Then delete the badge itself
if _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge); err != nil {
return err
}
return nil
})
}

// SearchBadgeOptions represents the options when finding badges
type SearchBadgeOptions struct {
db.ListOptions

Keyword string
Slug string
ID int64
OrderBy db.SearchOrderBy
}

func (opts *SearchBadgeOptions) ToConds() builder.Cond {
cond := builder.NewCond()

if opts.Keyword != "" {
keywordCond := builder.Or(
db.BuildCaseInsensitiveLike("badge.slug", opts.Keyword),
db.BuildCaseInsensitiveLike("badge.description", opts.Keyword),
)
cond = cond.And(keywordCond)
}

if opts.ID > 0 {
cond = cond.And(builder.Eq{"badge.id": opts.ID})
}

if len(opts.Slug) > 0 {
cond = cond.And(builder.Eq{"badge.slug": opts.Slug})
}

return cond
}

func (opts *SearchBadgeOptions) ToOrders() string {
return opts.OrderBy.String()
}

// SearchBadges returns badges based on the provided SearchBadgeOptions options
func SearchBadges(ctx context.Context, opts *SearchBadgeOptions) ([]*Badge, int64, error) {
return db.FindAndCount[Badge](ctx, opts)
}
126 changes: 126 additions & 0 deletions models/organization/badge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package organization

import (
"context"

"gitea.dev/models/badges"
"gitea.dev/models/db"
"gitea.dev/modules/util"

"xorm.io/xorm/schemas"
)

// OrgBadge represents an organization badge
type OrgBadge struct {
ID int64 `xorm:"pk autoincr"`
BadgeID int64
OrgID int64
}

// TableIndices implements xorm's TableIndices interface
func (n *OrgBadge) TableIndices() []*schemas.Index {
indices := make([]*schemas.Index, 0, 1)
ubUnique := schemas.NewIndex("unique_org_badge", schemas.UniqueType)
ubUnique.AddColumn("org_id", "badge_id")
indices = append(indices, ubUnique)
return indices
}

func init() {
db.RegisterModel(new(OrgBadge))
}

// GetOrgBadges returns the org's badges.
func GetOrgBadges(ctx context.Context, org *Organization) ([]*badges.Badge, int64, error) {
sess := db.GetEngine(ctx).
Select("`badge`.*").
Join("INNER", "org_badge", "`org_badge`.badge_id=badge.id").
Where("org_badge.org_id=?", org.ID)

badgesSlice := make([]*badges.Badge, 0, 8)
count, err := sess.FindAndCount(&badgesSlice)
return badgesSlice, count, err
}

// AddOrgBadge adds a badge to an organization.
func AddOrgBadge(ctx context.Context, org *Organization, badge *badges.Badge) error {
return db.WithTx(ctx, func(ctx context.Context) error {
// hydrate badge and check if it exists
has, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Get(badge)
if err != nil {
return err
} else if !has {
return util.NewNotExistErrorf("badge does not exist [slug: %s]", badge.Slug)
}

exists, err := db.GetEngine(ctx).Where("badge_id = ? AND org_id = ?", badge.ID, org.ID).Exist(new(OrgBadge))
if err != nil {
return err
}
if exists {
return util.NewAlreadyExistErrorf("org badge already exists [org_id: %d, badge_id: %d]", org.ID, badge.ID)
}

if err := db.Insert(ctx, &OrgBadge{
BadgeID: badge.ID,
OrgID: org.ID,
}); err != nil {
exists, existErr := db.GetEngine(ctx).Where("badge_id = ? AND org_id = ?", badge.ID, org.ID).Exist(new(OrgBadge))
if existErr == nil && exists {
return util.NewAlreadyExistErrorf("org badge already exists [org_id: %d, badge_id: %d]", org.ID, badge.ID)
}
return err
}
return nil
})
}

// RemoveOrgBadge removes a badge from an organization.
func RemoveOrgBadge(ctx context.Context, org *Organization, badge *badges.Badge) error {
return db.WithTx(ctx, func(ctx context.Context) error {
var userBadges []OrgBadge
if err := db.GetEngine(ctx).Table("org_badge").
Join("INNER", "badge", "badge.id = `org_badge`.badge_id").
Where("`org_badge`.org_id = ?", org.ID).In("`badge`.slug", []string{badge.Slug}).
Find(&userBadges); err != nil {
return err
}
userBadgeIDs := make([]int64, 0, len(userBadges))
for _, ub := range userBadges {
userBadgeIDs = append(userBadgeIDs, ub.ID)
}
if len(userBadgeIDs) == 0 {
return nil
}
if _, err := db.GetEngine(ctx).Table("org_badge").In("id", userBadgeIDs).Delete(); err != nil {
return err
}
return nil
})
}

// GetBadgeOrgsOptions contains options for getting orgs with a specific badge
type GetBadgeOrgsOptions struct {
db.ListOptions
BadgeSlug string
}

// GetBadgeOrgs returns the orgs that have a specific badge with pagination support.
func GetBadgeOrgs(ctx context.Context, opts *GetBadgeOrgsOptions) ([]*Organization, int64, error) {
sess := db.GetEngine(ctx).
Select("`user`.*").
Join("INNER", "org_badge", "`org_badge`.org_id=`user`.id").
Join("INNER", "badge", "`org_badge`.badge_id=badge.id").
Where("badge.slug=?", opts.BadgeSlug)

if opts.Page > 0 {
db.SetSessionPagination(sess, opts)
}

orgs := make([]*Organization, 0, opts.PageSize)
count, err := sess.FindAndCount(&orgs)
return orgs, count, err
}
126 changes: 126 additions & 0 deletions models/repo/badge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package repo

import (
"context"

"gitea.dev/models/badges"
"gitea.dev/models/db"
"gitea.dev/modules/util"

"xorm.io/xorm/schemas"
)

// RepoBadge represents a repository badge
type RepoBadge struct { //nolint:revive // export stutter
ID int64 `xorm:"pk autoincr"`
BadgeID int64
RepoID int64
}

// TableIndices implements xorm's TableIndices interface
func (n *RepoBadge) TableIndices() []*schemas.Index {
indices := make([]*schemas.Index, 0, 1)
ubUnique := schemas.NewIndex("unique_repo_badge", schemas.UniqueType)
ubUnique.AddColumn("repo_id", "badge_id")
indices = append(indices, ubUnique)
return indices
}

func init() {
db.RegisterModel(new(RepoBadge))
}

// GetRepoBadges returns the repo's badges.
func GetRepoBadges(ctx context.Context, repo *Repository) ([]*badges.Badge, int64, error) {
sess := db.GetEngine(ctx).
Select("`badge`.*").
Join("INNER", "repo_badge", "`repo_badge`.badge_id=badge.id").
Where("repo_badge.repo_id=?", repo.ID)

badgesSlice := make([]*badges.Badge, 0, 8)
count, err := sess.FindAndCount(&badgesSlice)
return badgesSlice, count, err
}

// AddRepoBadge adds a badge to a repository.
func AddRepoBadge(ctx context.Context, repo *Repository, badge *badges.Badge) error {
return db.WithTx(ctx, func(ctx context.Context) error {
// hydrate badge and check if it exists
has, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Get(badge)
if err != nil {
return err
} else if !has {
return util.NewNotExistErrorf("badge does not exist [slug: %s]", badge.Slug)
}

exists, err := db.GetEngine(ctx).Where("badge_id = ? AND repo_id = ?", badge.ID, repo.ID).Exist(new(RepoBadge))
if err != nil {
return err
}
if exists {
return util.NewAlreadyExistErrorf("repo badge already exists [repo_id: %d, badge_id: %d]", repo.ID, badge.ID)
}

if err := db.Insert(ctx, &RepoBadge{
BadgeID: badge.ID,
RepoID: repo.ID,
}); err != nil {
exists, existErr := db.GetEngine(ctx).Where("badge_id = ? AND repo_id = ?", badge.ID, repo.ID).Exist(new(RepoBadge))
if existErr == nil && exists {
return util.NewAlreadyExistErrorf("repo badge already exists [repo_id: %d, badge_id: %d]", repo.ID, badge.ID)
}
return err
}
return nil
})
}

// RemoveRepoBadge removes a badge from a repository.
func RemoveRepoBadge(ctx context.Context, repo *Repository, badge *badges.Badge) error {
return db.WithTx(ctx, func(ctx context.Context) error {
var userBadges []RepoBadge
if err := db.GetEngine(ctx).Table("repo_badge").
Join("INNER", "badge", "badge.id = `repo_badge`.badge_id").
Where("`repo_badge`.repo_id = ?", repo.ID).In("`badge`.slug", []string{badge.Slug}).
Find(&userBadges); err != nil {
return err
}
userBadgeIDs := make([]int64, 0, len(userBadges))
for _, ub := range userBadges {
userBadgeIDs = append(userBadgeIDs, ub.ID)
}
if len(userBadgeIDs) == 0 {
return nil
}
if _, err := db.GetEngine(ctx).Table("repo_badge").In("id", userBadgeIDs).Delete(); err != nil {
return err
}
return nil
})
}

// GetBadgeReposOptions contains options for getting repos with a specific badge
type GetBadgeReposOptions struct {
db.ListOptions
BadgeSlug string
}

// GetBadgeRepos returns the repos that have a specific badge with pagination support.
func GetBadgeRepos(ctx context.Context, opts *GetBadgeReposOptions) ([]*Repository, int64, error) {
sess := db.GetEngine(ctx).
Select("`repository`.*").
Join("INNER", "repo_badge", "`repo_badge`.repo_id=`repository`.id").
Join("INNER", "badge", "`repo_badge`.badge_id=badge.id").
Where("badge.slug=?", opts.BadgeSlug)

if opts.Page > 0 {
db.SetSessionPagination(sess, opts)
}

repos := make([]*Repository, 0, opts.PageSize)
count, err := sess.FindAndCount(&repos)
return repos, count, err
}
Loading
Loading