Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
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/db"
user_model "gitea.dev/models/user"
"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) ([]*user_model.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)

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

// AddOrgBadge adds a badge to an organization.
func AddOrgBadge(ctx context.Context, org *Organization, badge *user_model.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 *user_model.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/db"
user_model "gitea.dev/models/user"
"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) ([]*user_model.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)

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

// AddRepoBadge adds a badge to a repository.
func AddRepoBadge(ctx context.Context, repo *Repository, badge *user_model.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 *user_model.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
}
18 changes: 16 additions & 2 deletions models/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,9 @@ type Repository struct {

commonRenderingMetas map[string]string `xorm:"-"`

Units []*RepoUnit `xorm:"-"`
PrimaryLanguage *LanguageStat `xorm:"-"`
Units []*RepoUnit `xorm:"-"`
PrimaryLanguage *LanguageStat `xorm:"-"`
Badges []*user_model.Badge `xorm:"-"`

IsFork bool `xorm:"INDEX NOT NULL DEFAULT false"`
ForkID int64 `xorm:"INDEX"`
Expand Down Expand Up @@ -365,6 +366,19 @@ func (repo *Repository) HTMLURL(ctxs ...context.Context) string {
return httplib.MakeAbsoluteURL(ctx, repo.Link())
}

// LoadBadges loads the badges of the repository
func (repo *Repository) LoadBadges(ctx context.Context) error {
if repo.Badges != nil {
return nil
}
badges, _, err := GetRepoBadges(ctx, repo)
if err != nil {
return err
}
repo.Badges = badges
return nil
}

// CommitLink make link to by commit full ID
// note: won't check whether it's an right id
func (repo *Repository) CommitLink(commitID string) (result string) {
Expand Down
54 changes: 54 additions & 0 deletions models/repo/repo_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -794,3 +794,57 @@ func GetOwnerRepositoriesByIDs(ctx context.Context, ownerID int64, repoIDs []int
repos := make(RepositoryList, 0, len(repoIDs))
return repos, db.GetEngine(ctx).Where(builder.Eq{"owner_id": ownerID}).In("id", repoIDs).Find(&repos)
}

// LoadBadges loads the badges of the repositories
func (repos RepositoryList) LoadBadges(ctx context.Context) error {
if len(repos) == 0 {
return nil
}

repoIDs := make([]int64, 0, len(repos))
for _, repo := range repos {
if repo.Badges == nil {
repoIDs = append(repoIDs, repo.ID)
}
}
if len(repoIDs) == 0 {
return nil
}

var repoBadges []RepoBadge
if err := db.GetEngine(ctx).Table("repo_badge").In("repo_id", repoIDs).Find(&repoBadges); err != nil {
return err
}

badgeIDs := make([]int64, 0, len(repoBadges))
for _, rb := range repoBadges {
badgeIDs = append(badgeIDs, rb.BadgeID)
}

if len(badgeIDs) == 0 {
return nil
}

badges := make([]*user_model.Badge, 0, len(badgeIDs))
if err := db.GetEngine(ctx).Table("badge").In("id", badgeIDs).Find(&badges); err != nil {
return err
}

badgeMap := make(map[int64]*user_model.Badge, len(badges))
for _, b := range badges {
badgeMap[b.ID] = b
}

for _, rb := range repoBadges {
for _, repo := range repos {
if repo.ID == rb.RepoID {
if badge, ok := badgeMap[rb.BadgeID]; ok {
repo.Badges = append(repo.Badges, badge)
}
break
}
}
}

return nil
}
2 changes: 2 additions & 0 deletions models/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ type User struct {
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
Theme string `xorm:"NOT NULL DEFAULT ''"`
KeepActivityPrivate bool `xorm:"NOT NULL DEFAULT false"`

Badges []*Badge `xorm:"-"`
}

// Meta defines the meta information of a user, to be stored in the K/V table
Expand Down
Empty file added options/bindata.dat
Empty file.
16 changes: 16 additions & 0 deletions options/locale/locale_en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -2924,6 +2924,22 @@
"admin.badges.add_user": "Add User",
"admin.badges.remove_user": "Remove User",
"admin.badges.delete_user_desc": "Are you sure you want to remove this user from the badge?",
"admin.badges.repos_with_badge": "Repositories with badge: %s",
"admin.badges.repo_already_has": "Repository already has this badge.",
"admin.badges.repo_add_success": "Badge assigned to repository successfully.",
"admin.badges.repo_remove_success": "Badge removed from repository successfully.",
"admin.badges.manage_repos": "Manage Repositories",
"admin.badges.add_repo": "Add Repository",
"admin.badges.remove_repo": "Remove Repository",
"admin.badges.delete_repo_desc": "Are you sure you want to remove this repository from the badge?",
"admin.badges.orgs_with_badge": "Organizations with badge: %s",
"admin.badges.org_already_has": "Organization already has this badge.",
"admin.badges.org_add_success": "Badge assigned to organization successfully.",
"admin.badges.org_remove_success": "Badge removed from organization successfully.",
"admin.badges.manage_orgs": "Manage Organizations",
"admin.badges.add_org": "Add Organization",
"admin.badges.remove_org": "Remove Organization",
"admin.badges.delete_org_desc": "Are you sure you want to remove this organization from the badge?",
"admin.emails": "User Email Addresses",
"admin.config": "Configuration",
"admin.config_summary": "Summary",
Expand Down
Loading
Loading