db: migrate `org.go` to `orgs.go` with GORM

jc/db-migrate-orgs
Joe Chen 2023-08-24 01:00:45 -04:00
parent 25fdeaac49
commit 10fd9e9e38
No known key found for this signature in database
GPG Key ID: 0BDE5280C552FF60
28 changed files with 663 additions and 562 deletions

View File

@ -61,7 +61,7 @@ jobs:
name: Test
strategy:
matrix:
go-version: [ 1.20.x, 1.21.x ]
go-version: [ 1.21.x ]
platform: [ ubuntu-latest, macos-latest ]
runs-on: ${{ matrix.platform }}
steps:
@ -101,7 +101,7 @@ jobs:
name: Test Windows
strategy:
matrix:
go-version: [ 1.20.x, 1.21.x ]
go-version: [ 1.21.x ]
platform: [ windows-latest ]
runs-on: ${{ matrix.platform }}
steps:
@ -139,7 +139,7 @@ jobs:
name: Postgres
strategy:
matrix:
go-version: [ 1.20.x, 1.21.x ]
go-version: [ 1.21.x ]
platform: [ ubuntu-latest ]
runs-on: ${{ matrix.platform }}
services:
@ -175,7 +175,7 @@ jobs:
name: MySQL
strategy:
matrix:
go-version: [ 1.20.x, 1.21.x ]
go-version: [ 1.21.x ]
platform: [ ubuntu-20.04 ]
runs-on: ${{ matrix.platform }}
steps:
@ -200,7 +200,7 @@ jobs:
name: SQLite - Go
strategy:
matrix:
go-version: [ 1.20.x, 1.21.x ]
go-version: [ 1.21.x ]
platform: [ ubuntu-latest ]
runs-on: ${{ matrix.platform }}
steps:

View File

@ -129,3 +129,22 @@ Primary keys: id
Primary keys: id
```
# Table "org_user"
```
FIELD | COLUMN | POSTGRESQL | MYSQL | SQLITE3
-----------+-----------+--------------------------------+--------------------------------+---------------------------------
ID | id | BIGSERIAL | BIGINT AUTO_INCREMENT | INTEGER
UserID | uid | BIGINT NOT NULL | BIGINT NOT NULL | INTEGER NOT NULL
OrgID | org_id | BIGINT NOT NULL | BIGINT NOT NULL | INTEGER NOT NULL
IsPublic | is_public | BOOLEAN NOT NULL DEFAULT FALSE | BOOLEAN NOT NULL DEFAULT FALSE | NUMERIC NOT NULL DEFAULT FALSE
IsOwner | is_owner | BOOLEAN NOT NULL DEFAULT FALSE | BOOLEAN NOT NULL DEFAULT FALSE | NUMERIC NOT NULL DEFAULT FALSE
NumTeams | num_teams | BIGINT NOT NULL DEFAULT 0 | BIGINT NOT NULL DEFAULT 0 | INTEGER NOT NULL DEFAULT 0
Primary keys: id
Indexes:
"idx_org_user_org_id" (org_id)
"idx_org_user_user_id" (uid)
"org_user_user_org_unique" UNIQUE (uid, org_id)
```

View File

@ -73,8 +73,8 @@ func HandleOrgAssignment(c *Context, args ...bool) {
c.Org.IsMember = true
c.Org.IsTeamMember = true
c.Org.IsTeamAdmin = true
} else if org.IsOrgMember(c.User.ID) {
c.Org.IsMember = true
} else {
c.Org.IsMember, _ = db.Orgs.HasMember(c.Req.Context(), org.ID, c.User.ID)
}
} else {
// Fake data.

View File

@ -154,7 +154,7 @@ func (db *accessTokens) GetBySHA1(ctx context.Context, sha1 string) (*AccessToke
token := new(AccessToken)
err := db.WithContext(ctx).Where("sha256 = ?", sha256).First(token).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrAccessTokenNotExist{args: errutil.Args{"sha": sha1}}
}
return nil, err

View File

@ -86,19 +86,20 @@ func (db *actions) listByOrganization(ctx context.Context, orgID, actorID, after
/*
Equivalent SQL for PostgreSQL:
SELECT * FROM "action"
<SELECT * FROM "action">
WHERE
user_id = @userID
AND (@skipAfter OR id < @afterID)
AND repo_id IN (
SELECT repository.id FROM "repository"
JOIN team_repo ON repository.id = team_repo.repo_id
WHERE team_repo.team_id IN (
WHERE
team_repo.team_id IN (
SELECT team_id FROM "team_user"
WHERE
team_user.org_id = @orgID AND uid = @actorID)
OR (repository.is_private = FALSE AND repository.is_unlisted = FALSE)
)
WHERE team_user.org_id = @orgID AND uid = @actorID)
)
OR (repository.is_private = FALSE AND repository.is_unlisted = FALSE)
)
ORDER BY id DESC
LIMIT @limit
*/
@ -120,8 +121,8 @@ func (db *actions) listByOrganization(ctx context.Context, orgID, actorID, after
).
Or("repository.is_private = ? AND repository.is_unlisted = ?", false, false),
).
Limit(conf.UI.User.NewsFeedPagingNum).
Order("id DESC")
Order("id DESC").
Limit(conf.UI.User.NewsFeedPagingNum)
}
func (db *actions) ListByOrganization(ctx context.Context, orgID, actorID, afterID int64) ([]*Action, error) {
@ -133,7 +134,7 @@ func (db *actions) listByUser(ctx context.Context, userID, actorID, afterID int6
/*
Equivalent SQL for PostgreSQL:
SELECT * FROM "action"
<SELECT * FROM "action">
WHERE
user_id = @userID
AND (@skipAfter OR id < @afterID)
@ -153,8 +154,8 @@ func (db *actions) listByUser(ctx context.Context, userID, actorID, afterID int6
Where("?", !isProfile || actorID == userID).
Or("is_private = ? AND act_user_id = ?", false, userID),
).
Limit(conf.UI.User.NewsFeedPagingNum).
Order("id DESC")
Order("id DESC").
Limit(conf.UI.User.NewsFeedPagingNum)
}
func (db *actions) ListByUser(ctx context.Context, userID, actorID, afterID int64, isProfile bool) ([]*Action, error) {

View File

@ -31,7 +31,7 @@ func TestDumpAndImport(t *testing.T) {
}
t.Parallel()
const wantTables = 8
const wantTables = 9
if len(Tables) != wantTables {
t.Fatalf("New table has added (want %d got %d), please add new tests for the table and update this check", wantTables, len(Tables))
}
@ -197,6 +197,23 @@ func setupDBToDump(t *testing.T, db *gorm.DB) {
Description: "This is a notice",
CreatedUnix: 1588568886,
},
&OrgUser{
ID: 1,
UserID: 1,
OrgID: 11,
IsPublic: true,
IsOwner: true,
NumTeams: 3,
},
&OrgUser{
ID: 2,
UserID: 2,
OrgID: 11,
IsPublic: false,
IsOwner: false,
NumTeams: 0,
},
}
for _, val := range vals {
err := db.Create(val).Error

View File

@ -46,6 +46,7 @@ var Tables = []any{
new(Follow),
new(LFSObject), new(LoginSource),
new(Notice),
new(OrgUser),
}
// Init initializes the database with given logger.

View File

@ -133,26 +133,6 @@ func (err ErrDeployKeyNameAlreadyUsed) Error() string {
return fmt.Sprintf("public key already exists [repo_id: %d, name: %s]", err.RepoID, err.Name)
}
// ________ .__ __ .__
// \_____ \_______ _________ ____ |__|____________ _/ |_|__| ____ ____
// / | \_ __ \/ ___\__ \ / \| \___ /\__ \\ __\ |/ _ \ / \
// / | \ | \/ /_/ > __ \| | \ |/ / / __ \| | | ( <_> ) | \
// \_______ /__| \___ (____ /___| /__/_____ \(____ /__| |__|\____/|___| /
// \/ /_____/ \/ \/ \/ \/ \/
type ErrLastOrgOwner struct {
UID int64
}
func IsErrLastOrgOwner(err error) bool {
_, ok := err.(ErrLastOrgOwner)
return ok
}
func (err ErrLastOrgOwner) Error() string {
return fmt.Sprintf("user is the last member of owner team [uid: %d]", err.UID)
}
// __________ .__ __
// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__.
// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | |

View File

@ -9,6 +9,7 @@ import (
"fmt"
"time"
"github.com/pkg/errors"
"gorm.io/gorm"
"gogs.io/gogs/internal/errutil"
@ -75,7 +76,7 @@ func (db *lfs) GetObjectByOID(ctx context.Context, repoID int64, oid lfsutil.OID
object := new(LFSObject)
err := db.WithContext(ctx).Where("repo_id = ? AND oid = ?", repoID, oid).First(object).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrLFSObjectNotExist{args: errutil.Args{"repoID": repoID, "oid": oid}}
}
return nil, err

View File

@ -264,7 +264,7 @@ func (db *loginSources) GetByID(ctx context.Context, id int64) (*LoginSource, er
source := new(LoginSource)
err := db.WithContext(ctx).Where("id = ?", id).First(source).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
if errors.Is(err, gorm.ErrRecordNotFound) {
return db.files.GetByID(id)
}
return nil, err

View File

@ -80,7 +80,7 @@ func Migrate(db *gorm.DB) error {
var current Version
err := db.Where("id = ?", 1).First(&current).Error
if err == gorm.ErrRecordNotFound {
if errors.Is(err, gorm.ErrRecordNotFound) {
err = db.Create(
&Version{
ID: 1,

View File

@ -57,7 +57,7 @@ func init() {
new(Label), new(IssueLabel), new(Milestone),
new(Mirror), new(Release), new(Webhook), new(HookTask),
new(ProtectBranch), new(ProtectBranchWhitelist),
new(Team), new(OrgUser), new(TeamUser), new(TeamRepo),
new(Team), new(TeamUser), new(TeamRepo),
)
gonicNames := []string{"SSL"}
@ -211,7 +211,7 @@ type Statistic struct {
func GetStatistic(ctx context.Context) (stats Statistic) {
stats.Counter.User = Users.Count(ctx)
stats.Counter.Org = CountOrganizations()
stats.Counter.Org = Orgs.Count(ctx)
stats.Counter.PublicKey, _ = x.Count(new(PublicKey))
stats.Counter.Repo = CountRepositories(true)
stats.Counter.Watch, _ = x.Count(new(Watch))

View File

@ -6,7 +6,6 @@ package db
import (
"context"
"errors"
"fmt"
"os"
"strings"
@ -19,87 +18,6 @@ import (
"gogs.io/gogs/internal/userutil"
)
var ErrOrgNotExist = errors.New("Organization does not exist")
// IsOwnedBy returns true if given user is in the owner team.
func (org *User) IsOwnedBy(userID int64) bool {
return IsOrganizationOwner(org.ID, userID)
}
// IsOrgMember returns true if given user is member of organization.
func (org *User) IsOrgMember(uid int64) bool {
return org.IsOrganization() && IsOrganizationMember(org.ID, uid)
}
func (org *User) getTeam(e Engine, name string) (*Team, error) {
return getTeamOfOrgByName(e, org.ID, name)
}
// GetTeamOfOrgByName returns named team of organization.
func (org *User) GetTeam(name string) (*Team, error) {
return org.getTeam(x, name)
}
func (org *User) getOwnerTeam(e Engine) (*Team, error) {
return org.getTeam(e, OWNER_TEAM)
}
// GetOwnerTeam returns owner team of organization.
func (org *User) GetOwnerTeam() (*Team, error) {
return org.getOwnerTeam(x)
}
func (org *User) getTeams(e Engine) (err error) {
org.Teams, err = getTeamsByOrgID(e, org.ID)
return err
}
// GetTeams returns all teams that belong to organization.
func (org *User) GetTeams() error {
return org.getTeams(x)
}
// TeamsHaveAccessToRepo returns all teams that have given access level to the repository.
func (org *User) TeamsHaveAccessToRepo(repoID int64, mode AccessMode) ([]*Team, error) {
return GetTeamsHaveAccessToRepo(org.ID, repoID, mode)
}
// GetMembers returns all members of organization.
func (org *User) GetMembers(limit int) error {
ous, err := GetOrgUsersByOrgID(org.ID, limit)
if err != nil {
return err
}
org.Members = make([]*User, len(ous))
for i, ou := range ous {
org.Members[i], err = Users.GetByID(context.TODO(), ou.Uid)
if err != nil {
return err
}
}
return nil
}
// AddMember adds new member to organization.
func (org *User) AddMember(uid int64) error {
return AddOrgUser(org.ID, uid)
}
// RemoveMember removes member from organization.
func (org *User) RemoveMember(uid int64) error {
return RemoveOrgUser(org.ID, uid)
}
func (org *User) removeOrgRepo(e Engine, repoID int64) error {
return removeOrgRepo(e, org.ID, repoID)
}
// RemoveOrgRepo removes all team-repository relations of organization.
func (org *User) RemoveOrgRepo(repoID int64) error {
return org.removeOrgRepo(x, repoID)
}
// CreateOrganization creates record of a new organization.
func CreateOrganization(org, owner *User) (err error) {
if err = isUsernameAllowed(org.Name); err != nil {
@ -139,7 +57,7 @@ func CreateOrganization(org, owner *User) (err error) {
// Add initial creator to organization and owner team.
if _, err = sess.Insert(&OrgUser{
Uid: owner.ID,
UserID: owner.ID,
OrgID: org.ID,
IsOwner: true,
NumTeams: 1,
@ -150,8 +68,8 @@ func CreateOrganization(org, owner *User) (err error) {
// Create default owner team.
t := &Team{
OrgID: org.ID,
LowerName: strings.ToLower(OWNER_TEAM),
Name: OWNER_TEAM,
LowerName: strings.ToLower(TeamNameOwners),
Name: TeamNameOwners,
Authorize: AccessModeOwner,
NumMembers: 1,
}
@ -174,30 +92,6 @@ func CreateOrganization(org, owner *User) (err error) {
return sess.Commit()
}
// GetOrgByName returns organization by given name.
func GetOrgByName(name string) (*User, error) {
if name == "" {
return nil, ErrOrgNotExist
}
u := &User{
LowerName: strings.ToLower(name),
Type: UserTypeOrganization,
}
has, err := x.Get(u)
if err != nil {
return nil, err
} else if !has {
return nil, ErrOrgNotExist
}
return u, nil
}
// CountOrganizations returns number of organizations.
func CountOrganizations() int64 {
count, _ := x.Where("type=1").Count(new(User))
return count
}
// Organizations returns number of organizations in given page.
func Organizations(page, pageSize int) ([]*User, error) {
orgs := make([]*User, 0, pageSize)
@ -237,41 +131,6 @@ func DeleteOrganization(org *User) error {
return sess.Commit()
}
// ________ ____ ___
// \_____ \_______ ____ | | \______ ___________
// / | \_ __ \/ ___\| | / ___// __ \_ __ \
// / | \ | \/ /_/ > | /\___ \\ ___/| | \/
// \_______ /__| \___ /|______//____ >\___ >__|
// \/ /_____/ \/ \/
// OrgUser represents relations of organizations and their members.
type OrgUser struct {
ID int64 `gorm:"primaryKey"`
Uid int64 `xorm:"INDEX UNIQUE(s)" gorm:"uniqueIndex:org_user_user_org_unique;index;not null"`
OrgID int64 `xorm:"INDEX UNIQUE(s)" gorm:"uniqueIndex:org_user_user_org_unique;index;not null"`
IsPublic bool `gorm:"not null;default:FALSE"`
IsOwner bool `gorm:"not null;default:FALSE"`
NumTeams int `gorm:"not null;default:0"`
}
// IsOrganizationOwner returns true if given user is in the owner team.
func IsOrganizationOwner(orgID, userID int64) bool {
has, _ := x.Where("is_owner = ?", true).And("uid = ?", userID).And("org_id = ?", orgID).Get(new(OrgUser))
return has
}
// IsOrganizationMember returns true if given user is member of organization.
func IsOrganizationMember(orgId, uid int64) bool {
has, _ := x.Where("uid=?", uid).And("org_id=?", orgId).Get(new(OrgUser))
return has
}
// IsPublicMembership returns true if given user public his/her membership.
func IsPublicMembership(orgId, uid int64) bool {
has, _ := x.Where("uid=?", uid).And("org_id=?", orgId).And("is_public=?", true).Get(new(OrgUser))
return has
}
func getOrgsByUserID(sess *xorm.Session, userID int64, showAll bool) ([]*User, error) {
orgs := make([]*User, 0, 10)
if !showAll {
@ -287,18 +146,13 @@ func GetOrgsByUserID(userID int64, showAll bool) ([]*User, error) {
return getOrgsByUserID(x.NewSession(), userID, showAll)
}
// getOwnedOrgsByUserID returns a list of organizations are owned by given user ID.
func getOwnedOrgsByUserID(sess *xorm.Session, userID int64) ([]*User, error) {
orgs := make([]*User, 0, 10)
return orgs, sess.Where("`org_user`.uid=?", userID).And("`org_user`.is_owner=?", true).
Join("INNER", "`org_user`", "`org_user`.org_id=`user`.id").Find(&orgs)
}
// GetOwnedOrgsByUserID returns a list of organizations are owned by given user ID.
func GetOwnedOrgsByUserID(userID int64) ([]*User, error) {
sess := x.NewSession()
return getOwnedOrgsByUserID(sess, userID)
}
// GetOwnedOrganizationsByUserIDDesc returns a list of organizations are owned by
// given user ID, ordered descending by the given condition.
func GetOwnedOrgsByUserIDDesc(userID int64, desc string) ([]*User, error) {
@ -306,6 +160,7 @@ func GetOwnedOrgsByUserIDDesc(userID int64, desc string) ([]*User, error) {
return getOwnedOrgsByUserID(sess.Desc(desc), userID)
}
// getOrgUsersByOrgID returns all organization-user relations by organization ID.
func getOrgUsersByOrgID(e Engine, orgID int64, limit int) ([]*OrgUser, error) {
orgUsers := make([]*OrgUser, 0, 10)
@ -316,221 +171,9 @@ func getOrgUsersByOrgID(e Engine, orgID int64, limit int) ([]*OrgUser, error) {
return orgUsers, sess.Find(&orgUsers)
}
// GetOrgUsersByOrgID returns all organization-user relations by organization ID.
func GetOrgUsersByOrgID(orgID int64, limit int) ([]*OrgUser, error) {
return getOrgUsersByOrgID(x, orgID, limit)
}
// ChangeOrgUserStatus changes public or private membership status.
func ChangeOrgUserStatus(orgID, uid int64, public bool) error {
ou := new(OrgUser)
has, err := x.Where("uid=?", uid).And("org_id=?", orgID).Get(ou)
if err != nil {
return err
} else if !has {
return nil
}
ou.IsPublic = public
_, err = x.Id(ou.ID).AllCols().Update(ou)
return err
}
// AddOrgUser adds new user to given organization.
func AddOrgUser(orgID, uid int64) error {
if IsOrganizationMember(orgID, uid) {
return nil
}
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
ou := &OrgUser{
Uid: uid,
OrgID: orgID,
}
if _, err := sess.Insert(ou); err != nil {
return err
} else if _, err = sess.Exec("UPDATE `user` SET num_members = num_members + 1 WHERE id = ?", orgID); err != nil {
return err
}
return sess.Commit()
}
// RemoveOrgUser removes user from given organization.
func RemoveOrgUser(orgID, userID int64) error {
ou := new(OrgUser)
has, err := x.Where("uid=?", userID).And("org_id=?", orgID).Get(ou)
if err != nil {
return fmt.Errorf("get org-user: %v", err)
} else if !has {
return nil
}
user, err := Users.GetByID(context.TODO(), userID)
if err != nil {
return fmt.Errorf("GetUserByID [%d]: %v", userID, err)
}
org, err := Users.GetByID(context.TODO(), orgID)
if err != nil {
return fmt.Errorf("GetUserByID [%d]: %v", orgID, err)
}
// FIXME: only need to get IDs here, not all fields of repository.
repos, _, err := org.GetUserRepositories(user.ID, 1, org.NumRepos)
if err != nil {
return fmt.Errorf("GetUserRepositories [%d]: %v", user.ID, err)
}
// Check if the user to delete is the last member in owner team.
if IsOrganizationOwner(orgID, userID) {
t, err := org.GetOwnerTeam()
if err != nil {
return err
}
if t.NumMembers == 1 {
return ErrLastOrgOwner{UID: userID}
}
}
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if _, err := sess.ID(ou.ID).Delete(ou); err != nil {
return err
} else if _, err = sess.Exec("UPDATE `user` SET num_members=num_members-1 WHERE id=?", orgID); err != nil {
return err
}
// Delete all repository accesses and unwatch them.
repoIDs := make([]int64, len(repos))
for i := range repos {
repoIDs = append(repoIDs, repos[i].ID)
if err = watchRepo(sess, user.ID, repos[i].ID, false); err != nil {
return err
}
}
if len(repoIDs) > 0 {
if _, err = sess.Where("user_id = ?", user.ID).In("repo_id", repoIDs).Delete(new(Access)); err != nil {
return err
}
}
// Delete member in his/her teams.
teams, err := getUserTeams(sess, org.ID, user.ID)
if err != nil {
return err
}
for _, t := range teams {
if err = removeTeamMember(sess, org.ID, t.ID, user.ID); err != nil {
return err
}
}
return sess.Commit()
}
func removeOrgRepo(e Engine, orgID, repoID int64) error {
_, err := e.Delete(&TeamRepo{
OrgID: orgID,
RepoID: repoID,
})
return err
}
// RemoveOrgRepo removes all team-repository relations of given organization.
func RemoveOrgRepo(orgID, repoID int64) error {
return removeOrgRepo(x, orgID, repoID)
}
func (org *User) getUserTeams(e Engine, userID int64, cols ...string) ([]*Team, error) {
teams := make([]*Team, 0, org.NumTeams)
return teams, e.Where("team_user.org_id = ?", org.ID).
And("team_user.uid = ?", userID).
Join("INNER", "team_user", "team_user.team_id = team.id").
Cols(cols...).Find(&teams)
}
// GetUserTeamIDs returns of all team IDs of the organization that user is member of.
func (org *User) GetUserTeamIDs(userID int64) ([]int64, error) {
teams, err := org.getUserTeams(x, userID, "team.id")
if err != nil {
return nil, fmt.Errorf("getUserTeams [%d]: %v", userID, err)
}
teamIDs := make([]int64, len(teams))
for i := range teams {
teamIDs[i] = teams[i].ID
}
return teamIDs, nil
}
// GetTeams returns all teams that belong to organization,
// and that the user has joined.
func (org *User) GetUserTeams(userID int64) ([]*Team, error) {
return org.getUserTeams(x, userID)
}
// GetUserRepositories returns a range of repositories in organization which the user has access to,
// and total number of records based on given condition.
func (org *User) GetUserRepositories(userID int64, page, pageSize int) ([]*Repository, int64, error) {
teamIDs, err := org.GetUserTeamIDs(userID)
if err != nil {
return nil, 0, fmt.Errorf("GetUserTeamIDs: %v", err)
}
if len(teamIDs) == 0 {
// user has no team but "IN ()" is invalid SQL
teamIDs = []int64{-1} // there is no team with id=-1
}
var teamRepoIDs []int64
if err = x.Table("team_repo").In("team_id", teamIDs).Distinct("repo_id").Find(&teamRepoIDs); err != nil {
return nil, 0, fmt.Errorf("get team repository IDs: %v", err)
}
if len(teamRepoIDs) == 0 {
// team has no repo but "IN ()" is invalid SQL
teamRepoIDs = []int64{-1} // there is no repo with id=-1
}
if page <= 0 {
page = 1
}
repos := make([]*Repository, 0, pageSize)
if err = x.Where("owner_id = ?", org.ID).
And(builder.Or(
builder.And(builder.Expr("is_private = ?", false), builder.Expr("is_unlisted = ?", false)),
builder.In("id", teamRepoIDs))).
Desc("updated_unix").
Limit(pageSize, (page-1)*pageSize).
Find(&repos); err != nil {
return nil, 0, fmt.Errorf("get user repositories: %v", err)
}
repoCount, err := x.Where("owner_id = ?", org.ID).
And(builder.Or(
builder.Expr("is_private = ?", false),
builder.In("id", teamRepoIDs))).
Count(new(Repository))
if err != nil {
return nil, 0, fmt.Errorf("count user repositories: %v", err)
}
return repos, repoCount, nil
}
// GetUserMirrorRepositories returns mirror repositories of the organization which the user has access to.
func (org *User) GetUserMirrorRepositories(userID int64) ([]*Repository, error) {
teamIDs, err := org.GetUserTeamIDs(userID)
func (u *User) GetUserMirrorRepositories(userID int64) ([]*Repository, error) {
teamIDs, err := u.GetUserTeamIDs(userID)
if err != nil {
return nil, fmt.Errorf("GetUserTeamIDs: %v", err)
}
@ -549,7 +192,7 @@ func (org *User) GetUserMirrorRepositories(userID int64) ([]*Repository, error)
}
repos := make([]*Repository, 0, 10)
if err = x.Where("owner_id = ?", org.ID).
if err = x.Where("owner_id = ?", u.ID).
And("is_private = ?", false).
Or(builder.In("id", teamRepoIDs)).
And("is_mirror = ?", true). // Don't move up because it's an independent condition

View File

@ -9,24 +9,24 @@ import (
"fmt"
"strings"
"github.com/pkg/errors"
"xorm.io/xorm"
"gogs.io/gogs/internal/db/errors"
"gogs.io/gogs/internal/errutil"
dberrors "gogs.io/gogs/internal/db/errors"
)
const OWNER_TEAM = "Owners"
const TeamNameOwners = "Owners"
// Team represents a organization team.
type Team struct {
ID int64
OrgID int64 `xorm:"INDEX"`
ID int64 `gorm:"primaryKey"`
OrgID int64 `xorm:"INDEX" gorm:"index"`
LowerName string
Name string
Description string
Authorize AccessMode
Repos []*Repository `xorm:"-" json:"-"`
Members []*User `xorm:"-" json:"-"`
Repos []*Repository `xorm:"-" json:"-" gorm:"-"`
Members []*User `xorm:"-" json:"-" gorm:"-"`
NumRepos int
NumMembers int
}
@ -43,7 +43,7 @@ func (t *Team) AfterSet(colName string, _ xorm.Cell) {
// IsOwnerTeam returns true if team is owner team.
func (t *Team) IsOwnerTeam() bool {
return t.Name == OWNER_TEAM
return t.Name == TeamNameOwners
}
// HasWriteAccess returns true if team has at least write level access mode.
@ -136,7 +136,7 @@ func (t *Team) addRepository(e Engine, repo *Repository) (err error) {
// AddRepository adds new repository to team of organization.
func (t *Team) AddRepository(repo *Repository) (err error) {
if repo.OwnerID != t.OrgID {
return errors.New("Repository does not belong to organization")
return dberrors.New("Repository does not belong to organization")
} else if t.HasRepository(repo.ID) {
return nil
}
@ -259,9 +259,9 @@ func IsUsableTeamName(name string) error {
// It's caller's responsibility to assign organization ID.
func NewTeam(t *Team) error {
if t.Name == "" {
return errors.New("empty team name")
return dberrors.New("empty team name")
} else if t.OrgID == 0 {
return errors.New("OrgID is not assigned")
return dberrors.New("OrgID is not assigned")
}
if err := IsUsableTeamName(t.Name); err != nil {
@ -272,7 +272,7 @@ func NewTeam(t *Team) error {
if err != nil {
return err
} else if !has {
return ErrOrgNotExist
return errors.New("organization does not exist")
}
t.LowerName = strings.ToLower(t.Name)
@ -301,44 +301,6 @@ func NewTeam(t *Team) error {
return sess.Commit()
}
var _ errutil.NotFound = (*ErrTeamNotExist)(nil)
type ErrTeamNotExist struct {
args map[string]any
}
func IsErrTeamNotExist(err error) bool {
_, ok := err.(ErrTeamNotExist)
return ok
}
func (err ErrTeamNotExist) Error() string {
return fmt.Sprintf("team does not exist: %v", err.args)
}
func (ErrTeamNotExist) NotFound() bool {
return true
}
func getTeamOfOrgByName(e Engine, orgID int64, name string) (*Team, error) {
t := &Team{
OrgID: orgID,
LowerName: strings.ToLower(name),
}
has, err := e.Get(t)
if err != nil {
return nil, err
} else if !has {
return nil, ErrTeamNotExist{args: map[string]any{"orgID": orgID, "name": name}}
}
return t, nil
}
// GetTeamOfOrgByName returns team by given team name and organization.
func GetTeamOfOrgByName(orgID int64, name string) (*Team, error) {
return getTeamOfOrgByName(x, orgID, name)
}
func getTeamByID(e Engine, teamID int64) (*Team, error) {
t := new(Team)
has, err := e.ID(teamID).Get(t)
@ -355,20 +317,16 @@ func GetTeamByID(teamID int64) (*Team, error) {
return getTeamByID(x, teamID)
}
// getTeamsByOrgID returns all teams belong to given organization.
func getTeamsByOrgID(e Engine, orgID int64) ([]*Team, error) {
teams := make([]*Team, 0, 3)
return teams, e.Where("org_id = ?", orgID).Find(&teams)
}
// GetTeamsByOrgID returns all teams belong to given organization.
func GetTeamsByOrgID(orgID int64) ([]*Team, error) {
return getTeamsByOrgID(x, orgID)
}
// UpdateTeam updates information of team.
func UpdateTeam(t *Team, authChanged bool) (err error) {
if t.Name == "" {
return errors.New("empty team name")
return dberrors.New("empty team name")
}
if len(t.Description) > 255 {
@ -528,7 +486,7 @@ func AddTeamMember(orgID, teamID, userID int64) error {
return nil
}
if err := AddOrgUser(orgID, userID); err != nil {
if err := Orgs.AddMember(context.TODO(), orgID, userID); err != nil {
return err
}
@ -583,8 +541,8 @@ func AddTeamMember(orgID, teamID, userID int64) error {
return sess.Commit()
}
func removeTeamMember(e Engine, orgID, teamID, uid int64) error {
if !isTeamMember(e, orgID, teamID, uid) {
func removeTeamMember(e Engine, orgID, teamID, userID int64) error {
if !isTeamMember(e, orgID, teamID, userID) {
return nil
}
@ -596,7 +554,7 @@ func removeTeamMember(e Engine, orgID, teamID, uid int64) error {
// Check if the user to delete is the last member in owner team.
if t.IsOwnerTeam() && t.NumMembers == 1 {
return ErrLastOrgOwner{UID: uid}
return ErrLastOrgOwner{args: map[string]any{"orgID": orgID, "userID": userID}}
}
t.NumMembers--
@ -612,7 +570,7 @@ func removeTeamMember(e Engine, orgID, teamID, uid int64) error {
}
tu := &TeamUser{
UID: uid,
UID: userID,
OrgID: orgID,
TeamID: teamID,
}
@ -631,7 +589,7 @@ func removeTeamMember(e Engine, orgID, teamID, uid int64) error {
// This must exist.
ou := new(OrgUser)
_, err = e.Where("uid = ?", uid).And("org_id = ?", org.ID).Get(ou)
_, err = e.Where("uid = ?", userID).And("org_id = ?", org.ID).Get(ou)
if err != nil {
return err
}
@ -673,16 +631,13 @@ type TeamRepo struct {
RepoID int64 `xorm:"UNIQUE(s)"`
}
// hasTeamRepo returns true if given team has access to the repository of the organization.
func hasTeamRepo(e Engine, orgID, teamID, repoID int64) bool {
has, _ := e.Where("org_id = ?", orgID).And("team_id = ?", teamID).And("repo_id = ?", repoID).Get(new(TeamRepo))
return has
}
// HasTeamRepo returns true if given team has access to the repository of the organization.
func HasTeamRepo(orgID, teamID, repoID int64) bool {
return hasTeamRepo(x, orgID, teamID, repoID)
}
// addTeamRepo adds new repository relation to team.
func addTeamRepo(e Engine, orgID, teamID, repoID int64) error {
_, err := e.InsertOne(&TeamRepo{
OrgID: orgID,
@ -692,11 +647,7 @@ func addTeamRepo(e Engine, orgID, teamID, repoID int64) error {
return err
}
// AddTeamRepo adds new repository relation to team.
func AddTeamRepo(orgID, teamID, repoID int64) error {
return addTeamRepo(x, orgID, teamID, repoID)
}
// removeTeamRepo deletes repository relation to team.
func removeTeamRepo(e Engine, teamID, repoID int64) error {
_, err := e.Delete(&TeamRepo{
TeamID: teamID,
@ -705,11 +656,6 @@ func removeTeamRepo(e Engine, teamID, repoID int64) error {
return err
}
// RemoveTeamRepo deletes repository relation to team.
func RemoveTeamRepo(teamID, repoID int64) error {
return removeTeamRepo(x, teamID, repoID)
}
// GetTeamsHaveAccessToRepo returns all teams in an organization that have given access level to the repository.
func GetTeamsHaveAccessToRepo(orgID, repoID int64, mode AccessMode) ([]*Team, error) {
teams := make([]*Team, 0, 5)
@ -719,3 +665,46 @@ func GetTeamsHaveAccessToRepo(orgID, repoID int64, mode AccessMode) ([]*Team, er
And("team_repo.repo_id = ?", repoID).
Find(&teams)
}
func (u *User) getTeams(e Engine) (err error) {
u.Teams, err = getTeamsByOrgID(e, u.ID)
return err
}
// GetTeams returns all teams that belong to organization.
func (u *User) GetTeams() error {
return u.getTeams(x)
}
// TeamsHaveAccessToRepo returns all teams that have given access level to the repository.
func (u *User) TeamsHaveAccessToRepo(repoID int64, mode AccessMode) ([]*Team, error) {
return GetTeamsHaveAccessToRepo(u.ID, repoID, mode)
}
func (u *User) getUserTeams(e Engine, userID int64, cols ...string) ([]*Team, error) {
teams := make([]*Team, 0, u.NumTeams)
return teams, e.Where("team_user.org_id = ?", u.ID).
And("team_user.uid = ?", userID).
Join("INNER", "team_user", "team_user.team_id = team.id").
Cols(cols...).Find(&teams)
}
// GetUserTeamIDs returns of all team IDs of the organization that user is member of.
func (u *User) GetUserTeamIDs(userID int64) ([]int64, error) {
teams, err := u.getUserTeams(x, userID, "team.id")
if err != nil {
return nil, fmt.Errorf("getUserTeams [%d]: %v", userID, err)
}
teamIDs := make([]int64, len(teams))
for i := range teams {
teamIDs[i] = teams[i].ID
}
return teamIDs, nil
}
// GetTeams returns all teams that belong to organization,
// and that the user has joined.
func (u *User) GetUserTeams(userID int64) ([]*Team, error) {
return u.getUserTeams(x, userID)
}

View File

@ -6,26 +6,57 @@ package db
import (
"context"
"fmt"
"strings"
"github.com/pkg/errors"
"gorm.io/gorm"
"gogs.io/gogs/internal/dbutil"
"gogs.io/gogs/internal/errutil"
)
// OrgsStore is the persistent interface for organizations.
type OrgsStore interface {
// List returns a list of organizations filtered by options.
List(ctx context.Context, opts ListOrgsOptions) ([]*Organization, error)
// AddMember adds a new member to the given organization.
AddMember(ctx context.Context, orgID, userID int64) error
// RemoveMember removes a member from the given organization.
RemoveMember(ctx context.Context, orgID, userID int64) error
// HasMember returns whether the given user is a member of the organization
// (first), and whether the organization membership is public (second).
HasMember(ctx context.Context, orgID, userID int64) (bool, bool)
// ListMembers returns all members of the given organization, and sorted by the
// given order (e.g. "id ASC").
ListMembers(ctx context.Context, orgID int64, opts ListOrgMembersOptions) ([]*User, error)
// IsOwnedBy returns true if the given user is an owner of the organization.
IsOwnedBy(ctx context.Context, orgID, userID int64) bool
// SetMemberVisibility sets the visibility of the given user in the organization.
SetMemberVisibility(ctx context.Context, orgID, userID int64, public bool) error
// GetByName returns the organization with given name.
GetByName(ctx context.Context, name string) (*Organization, error)
// SearchByName returns a list of organizations whose username or full name
// matches the given keyword case-insensitively. Results are paginated by given
// page and page size, and sorted by the given order (e.g. "id DESC"). A total
// count of all results is also returned. If the order is not given, it's up to
// the database to decide.
SearchByName(ctx context.Context, keyword string, page, pageSize int, orderBy string) ([]*Organization, int64, error)
// List returns a list of organizations filtered by options.
List(ctx context.Context, opts ListOrgsOptions) ([]*Organization, error)
// CountByUser returns the number of organizations the user is a member of.
CountByUser(ctx context.Context, userID int64) (int64, error)
// Count returns the total number of organizations.
Count(ctx context.Context) int64
// GetTeamByName returns the team with given name under the given organization.
// It returns ErrTeamNotExist whe not found.
GetTeamByName(ctx context.Context, orgID int64, name string) (*Team, error)
// AccessibleRepositoriesByUser returns a range of repositories in the
// organization that the user has access to and the total number of it. Results
// are paginated by given page and page size, and sorted by the given order
// (e.g. "updated_unix DESC").
AccessibleRepositoriesByUser(ctx context.Context, orgID, userID int64, page, pageSize int, opts AccessibleRepositoriesByUserOptions) ([]*Repository, int64, error)
}
var Orgs OrgsStore
@ -42,6 +73,260 @@ func NewOrgsStore(db *gorm.DB) OrgsStore {
return &orgs{DB: db}
}
func (*orgs) recountMembers(tx *gorm.DB, orgID int64) error {
/*
Equivalent SQL for PostgreSQL:
UPDATE "user"
SET num_members = (
SELECT COUNT(*) FROM org_user WHERE org_id = @orgID
)
WHERE id = @orgID
*/
err := tx.Model(&User{}).
Where("id = ?", orgID).
Update(
"num_members",
tx.Model(&OrgUser{}).Select("COUNT(*)").Where("org_id = ?", orgID),
).
Error
if err != nil {
return errors.Wrap(err, `update "user.num_members"`)
}
return nil
}
func (db *orgs) AddMember(ctx context.Context, orgID, userID int64) error {
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
ou := &OrgUser{
UserID: userID,
OrgID: orgID,
}
result := tx.FirstOrCreate(ou, ou)
if result.Error != nil {
return errors.Wrap(result.Error, "upsert")
} else if result.RowsAffected <= 0 {
return nil // Relation already exists
}
return db.recountMembers(tx, orgID)
})
}
type ErrLastOrgOwner struct {
args map[string]any
}
func IsErrLastOrgOwner(err error) bool {
return errors.As(err, &ErrLastOrgOwner{})
}
func (err ErrLastOrgOwner) Error() string {
return fmt.Sprintf("user is the last owner of the organization: %v", err.args)
}
func (db *orgs) RemoveMember(ctx context.Context, orgID, userID int64) error {
ou, err := db.getOrgUser(ctx, orgID, userID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil // Not a member
}
return errors.Wrap(err, "check organization membership")
}
// Check if the member to remove is the last owner.
if ou.IsOwner {
t, err := db.GetTeamByName(ctx, orgID, TeamNameOwners)
if err != nil {
return errors.Wrap(err, "get owners team")
} else if t.NumMembers == 1 {
return ErrLastOrgOwner{args: map[string]any{"orgID": orgID, "userID": userID}}
}
}
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
repoIDsConds := db.accessibleRepositoriesByUser(tx, orgID, userID, accessibleRepositoriesByUserOptions{}).Select("repository.id")
err := tx.Where("user_id = ? AND repo_id IN (?)", userID, repoIDsConds).Delete(&Watch{}).Error
if err != nil {
return errors.Wrap(err, "unwatch repositories")
}
err = tx.Table("repository").
Where("id IN (?)", repoIDsConds).
UpdateColumn("num_watches", gorm.Expr("num_watches - 1")).
Error
if err != nil {
return errors.Wrap(err, `decrease "repository.num_watches"`)
}
err = tx.Where("user_id = ? AND repo_id IN (?)", userID, repoIDsConds).Delete(&Access{}).Error
if err != nil {
return errors.Wrap(err, "delete repository accesses")
}
err = tx.Where("user_id = ? AND repo_id IN (?)", userID, repoIDsConds).Delete(&Collaboration{}).Error
if err != nil {
return errors.Wrap(err, "delete repository collaborations")
}
/*
Equivalent SQL for PostgreSQL:
UPDATE "team"
SET num_members = num_members - 1
WHERE id IN (
SELECT team_id FROM "team_user"
WHERE team_user.org_id = @orgID AND uid = @userID)
)
*/
err = tx.Table("team").
Where(`id IN (?)`, tx.
Select("team_id").
Table("team_user").
Where("org_id = ? AND uid = ?", orgID, userID),
).
UpdateColumn("num_members", gorm.Expr("num_members - 1")).
Error
if err != nil {
return errors.Wrap(err, `decrease "team.num_members"`)
}
err = tx.Where("uid = ? AND org_id = ?", userID, orgID).Delete(&TeamUser{}).Error
if err != nil {
return errors.Wrap(err, "delete team membership")
}
err = tx.Where("uid = ? AND org_id = ?", userID, orgID).Delete(&OrgUser{}).Error
if err != nil {
return errors.Wrap(err, "delete organization membership")
}
return db.recountMembers(tx, orgID)
})
}
type accessibleRepositoriesByUserOptions struct {
orderBy string
page int
pageSize int
}
func (*orgs) accessibleRepositoriesByUser(tx *gorm.DB, orgID, userID int64, opts accessibleRepositoriesByUserOptions) *gorm.DB {
/*
Equivalent SQL for PostgreSQL:
<SELECT * FROM "repository">
JOIN team_repo ON repository.id = team_repo.repo_id
WHERE
owner_id = @orgID
AND (
team_repo.team_id IN (
SELECT team_id FROM "team_user"
WHERE team_user.org_id = @orgID AND uid = @userID)
)
OR (repository.is_private = FALSE AND repository.is_unlisted = FALSE)
)
[ORDER BY updated_unix DESC]
[LIMIT @limit OFFSET @offset]
*/
conds := tx.
Joins("JOIN team_repo ON repository.id = team_repo.repo_id").
Where("owner_id = ? AND (?)", orgID, tx.
Where("team_repo.team_id IN (?)", tx.
Select("team_id").
Table("team_user").
Where("team_user.org_id = ? AND uid = ?", orgID, userID),
).
Or("repository.is_private = ? AND repository.is_unlisted = ?", false, false),
)
if opts.orderBy != "" {
conds.Order(opts.orderBy)
}
if opts.page > 0 && opts.pageSize > 0 {
conds.Limit(opts.pageSize).Offset((opts.page - 1) * opts.pageSize)
}
return conds
}
type AccessibleRepositoriesByUserOptions struct {
// Whether to skip counting the total number of repositories.
SkipCount bool
}
func (db *orgs) AccessibleRepositoriesByUser(ctx context.Context, orgID, userID int64, page, pageSize int, opts AccessibleRepositoriesByUserOptions) ([]*Repository, int64, error) {
conds := db.accessibleRepositoriesByUser(
db.DB,
orgID,
userID,
accessibleRepositoriesByUserOptions{
orderBy: "updated_unix DESC",
page: page,
pageSize: pageSize,
},
).WithContext(ctx)
repos := make([]*Repository, 0, pageSize)
err := conds.Find(&repos).Error
if err != nil {
return nil, 0, errors.Wrap(err, "list repositories")
}
if opts.SkipCount {
return repos, 0, nil
}
var count int64
err = conds.Model(&Repository{}).Count(&count).Error
if err != nil {
return nil, 0, errors.Wrap(err, "count repositories")
}
return repos, count, nil
}
func (db *orgs) getOrgUser(ctx context.Context, orgID, userID int64) (*OrgUser, error) {
var ou OrgUser
return &ou, db.WithContext(ctx).Where("org_id = ? AND uid = ?", orgID, userID).First(&ou).Error
}
func (db *orgs) IsOwnedBy(ctx context.Context, orgID, userID int64) bool {
ou, err := db.getOrgUser(ctx, orgID, userID)
return err == nil && ou.IsOwner
}
func (db *orgs) SetMemberVisibility(ctx context.Context, orgID, userID int64, public bool) error {
return db.Table("org_user").Where("org_id = ? AND uid = ?", orgID, userID).UpdateColumn("is_public", public).Error
}
func (db *orgs) HasMember(ctx context.Context, orgID, userID int64) (bool, bool) {
ou, err := db.getOrgUser(ctx, orgID, userID)
return err == nil, ou != nil && ou.IsPublic
}
type ListOrgMembersOptions struct {
// The maximum number of members to return.
Limit int
}
func (db *orgs) ListMembers(ctx context.Context, orgID int64, opts ListOrgMembersOptions) ([]*User, error) {
/*
Equivalent SQL for PostgreSQL:
SELECT * FROM "user"
JOIN org_user ON org_user.uid = user.id
WHERE
org_user.org_id = @orgID
ORDER BY user.id ASC
[LIMIT @limit]
*/
conds := db.WithContext(ctx).
Joins(dbutil.Quote("JOIN org_user ON org_user.uid = %s.id", "user")).
Where("org_user.org_id = ?", orgID).
Order(dbutil.Quote("%s.id ASC", "user"))
if opts.Limit > 0 {
conds.Limit(opts.Limit)
}
var users []*User
return users, conds.Find(&users).Error
}
type ListOrgsOptions struct {
// Filter by the membership with the given user ID.
MemberID int64
@ -57,23 +342,54 @@ func (db *orgs) List(ctx context.Context, opts ListOrgsOptions) ([]*Organization
/*
Equivalent SQL for PostgreSQL:
SELECT * FROM "org"
JOIN org_user ON org_user.org_id = org.id
SELECT * FROM "user"
JOIN org_user ON org_user.org_id = user.id
WHERE
org_user.uid = @memberID
[AND org_user.is_public = @includePrivateMembers]
ORDER BY org.id ASC
ORDER BY user.id ASC
*/
tx := db.WithContext(ctx).
conds := db.WithContext(ctx).
Joins(dbutil.Quote("JOIN org_user ON org_user.org_id = %s.id", "user")).
Where("org_user.uid = ?", opts.MemberID).
Order(dbutil.Quote("%s.id ASC", "user"))
if !opts.IncludePrivateMembers {
tx = tx.Where("org_user.is_public = ?", true)
conds.Where("org_user.is_public = ?", true)
}
var orgs []*Organization
return orgs, tx.Find(&orgs).Error
return orgs, conds.Find(&orgs).Error
}
var _ errutil.NotFound = (*ErrUserNotExist)(nil)
type ErrOrganizationNotExist struct {
args errutil.Args
}
// IsErrOrganizationNotExist returns true if the underlying error has the type
// ErrOrganizationNotExist.
func IsErrOrganizationNotExist(err error) bool {
return errors.As(err, &ErrOrganizationNotExist{})
}
func (err ErrOrganizationNotExist) Error() string {
return fmt.Sprintf("organization does not exist: %v", err.args)
}
func (ErrOrganizationNotExist) NotFound() bool {
return true
}
func (db *orgs) GetByName(ctx context.Context, name string) (*Organization, error) {
org, err := getUserByUsername(ctx, db.DB, UserTypeOrganization, name)
if err != nil {
if IsErrUserNotExist(err) {
return nil, ErrOrganizationNotExist{args: map[string]any{"name": name}}
}
return nil, errors.Wrap(err, "get organization by name")
}
return org, nil
}
func (db *orgs) SearchByName(ctx context.Context, keyword string, page, pageSize int, orderBy string) ([]*Organization, int64, error) {
@ -85,8 +401,62 @@ func (db *orgs) CountByUser(ctx context.Context, userID int64) (int64, error) {
return count, db.WithContext(ctx).Model(&OrgUser{}).Where("uid = ?", userID).Count(&count).Error
}
func (db *orgs) Count(ctx context.Context) int64 {
var count int64
db.WithContext(ctx).Model(&User{}).Where("type = ?", UserTypeOrganization).Count(&count)
return count
}
var _ errutil.NotFound = (*ErrTeamNotExist)(nil)
type ErrTeamNotExist struct {
args map[string]any
}
func IsErrTeamNotExist(err error) bool {
return errors.As(err, &ErrTeamNotExist{})
}
func (err ErrTeamNotExist) Error() string {
return fmt.Sprintf("team does not exist: %v", err.args)
}
func (ErrTeamNotExist) NotFound() bool {
return true
}
func (db *orgs) GetTeamByName(ctx context.Context, orgID int64, name string) (*Team, error) {
var team Team
err := db.WithContext(ctx).Where("org_id = ? AND lower_name = ?", orgID, strings.ToLower(name)).First(&team).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrTeamNotExist{args: map[string]any{"orgID": orgID, "name": name}}
}
return nil, errors.Wrap(err, "get team by name")
}
return &team, nil
}
type Organization = User
func (o *Organization) TableName() string {
func (u *Organization) TableName() string {
return "user"
}
// IsOwnedBy returns true if the given user is an owner of the organization.
//
// TODO(unknwon): This is also used in templates, which should be fixed by
// having a dedicated type `template.Organization`.
func (u *Organization) IsOwnedBy(userID int64) bool {
return Orgs.IsOwnedBy(context.TODO(), u.ID, userID)
}
// OrgUser represents relations of organizations and their members.
type OrgUser struct {
ID int64 `gorm:"primaryKey"`
UserID int64 `xorm:"uid INDEX UNIQUE(s)" gorm:"column:uid;uniqueIndex:org_user_user_org_unique;index;not null" json:"Uid"`
OrgID int64 `xorm:"INDEX UNIQUE(s)" gorm:"uniqueIndex:org_user_user_org_unique;index;not null"`
IsPublic bool `gorm:"not null;default:FALSE"`
IsOwner bool `gorm:"not null;default:FALSE"`
NumTeams int `gorm:"not null;default:0"`
}

View File

@ -798,7 +798,7 @@ func MigrateRepository(doer, owner *User, opts MigrateRepoOptions) (*Repository,
wikiPath := WikiPath(owner.Name, opts.Name)
if owner.IsOrganization() {
t, err := owner.GetOwnerTeam()
t, err := Orgs.GetTeamByName(context.TODO(), owner.ID, TeamNameOwners)
if err != nil {
return nil, err
}
@ -1149,7 +1149,24 @@ func createRepository(e *xorm.Session, doer, owner *User, repo *Repository) (err
// Give access to all members in owner team.
if owner.IsOrganization() {
t, err := owner.getOwnerTeam(e)
// FIXME: This is identical to Orgs.GetTeamByName but we are not yet able to
// wrap transaction with different ORM objects, should delete this once migrated
// to GORM for this part of logic.
getTeamOfOrgByName := func(e Engine, orgID int64, name string) (*Team, error) {
t := &Team{
OrgID: orgID,
LowerName: strings.ToLower(name),
}
has, err := e.Get(t)
if err != nil {
return nil, err
} else if !has {
return nil, ErrTeamNotExist{args: map[string]any{"orgID": orgID, "name": name}}
}
return t, nil
}
t, err := getTeamOfOrgByName(e, owner.ID, TeamNameOwners)
if err != nil {
return fmt.Errorf("getOwnerTeam: %v", err)
} else if err = t.addRepository(e, repo); err != nil {
@ -1393,7 +1410,8 @@ func TransferOwnership(doer *User, newOwnerName string, repo *Repository) error
collaboration := &Collaboration{RepoID: repo.ID}
for _, c := range collaborators {
collaboration.UserID = c.ID
if c.ID == newOwner.ID || newOwner.IsOrgMember(c.ID) {
if c.ID == newOwner.ID ||
(newOwner.IsOrganization() && func() bool { member, _ := Orgs.HasMember(context.TODO(), newOwner.ID, c.ID); return member }()) {
if _, err = sess.Delete(collaboration); err != nil {
return fmt.Errorf("remove collaborator '%d': %v", c.ID, err)
}
@ -1416,13 +1434,34 @@ func TransferOwnership(doer *User, newOwnerName string, repo *Repository) error
}
}
if err = owner.removeOrgRepo(sess, repo.ID); err != nil {
_, err := sess.Delete(&TeamRepo{
OrgID: owner.ID,
RepoID: repo.ID,
})
if err != nil {
return fmt.Errorf("removeOrgRepo: %v", err)
}
}
if newOwner.IsOrganization() {
t, err := newOwner.getOwnerTeam(sess)
// FIXME: This is identical to Orgs.GetTeamByName but we are not yet able to
// wrap transaction with different ORM objects, should delete this once migrated
// to GORM for this part of logic.
getTeamOfOrgByName := func(e Engine, orgID int64, name string) (*Team, error) {
t := &Team{
OrgID: orgID,
LowerName: strings.ToLower(name),
}
has, err := e.Get(t)
if err != nil {
return nil, err
} else if !has {
return nil, ErrTeamNotExist{args: map[string]any{"orgID": orgID, "name": name}}
}
return t, nil
}
t, err := getTeamOfOrgByName(sess, owner.ID, TeamNameOwners)
if err != nil {
return fmt.Errorf("getOwnerTeam: %v", err)
} else if err = t.addRepository(sess, repo); err != nil {

View File

@ -263,8 +263,7 @@ type ErrRepoNotExist struct {
}
func IsErrRepoNotExist(err error) bool {
_, ok := err.(ErrRepoNotExist)
return ok
return errors.As(err, &ErrRepoNotExist{})
}
func (err ErrRepoNotExist) Error() string {
@ -279,7 +278,7 @@ func (db *repos) GetByID(ctx context.Context, id int64) (*Repository, error) {
repo := new(Repository)
err := db.WithContext(ctx).Where("id = ?", id).First(repo).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrRepoNotExist{errutil.Args{"repoID": id}}
}
return nil, err
@ -294,7 +293,7 @@ func (db *repos) GetByName(ctx context.Context, ownerID int64, name string) (*Re
First(repo).
Error
if err != nil {
if err == gorm.ErrRecordNotFound {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrRepoNotExist{
args: errutil.Args{
"ownerID": ownerID,

View File

@ -0,0 +1,2 @@
{"ID":1,"Uid":1,"OrgID":11,"IsPublic":true,"IsOwner":true,"NumTeams":3}
{"ID":2,"Uid":2,"OrgID":11,"IsPublic":false,"IsOwner":false,"NumTeams":0}

View File

@ -104,7 +104,7 @@ func (db *twoFactors) GetByUserID(ctx context.Context, userID int64) (*TwoFactor
tf := new(TwoFactor)
err := db.WithContext(ctx).Where("user_id = ?", userID).First(tf).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrTwoFactorNotFound{args: errutil.Args{"userID": userID}}
}
return nil, err

View File

@ -738,7 +738,6 @@ func (db *users) Follow(ctx context.Context, userID, followID int64) error {
} else if result.RowsAffected <= 0 {
return nil // Relation already exists
}
return db.recountFollows(tx, userID, followID)
})
}
@ -770,8 +769,7 @@ type ErrUserNotExist struct {
// IsErrUserNotExist returns true if the underlying error has the type
// ErrUserNotExist.
func IsErrUserNotExist(err error) bool {
_, ok := errors.Cause(err).(ErrUserNotExist)
return ok
return errors.As(err, &ErrUserNotExist{})
}
func (err ErrUserNotExist) Error() string {
@ -811,7 +809,7 @@ func (db *users) GetByEmail(ctx context.Context, email string) (*User, error) {
First(&user).
Error
if err != nil {
if err == gorm.ErrRecordNotFound {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotExist{args: errutil.Args{"email": email}}
}
return nil, err
@ -823,7 +821,7 @@ func (db *users) GetByID(ctx context.Context, id int64) (*User, error) {
user := new(User)
err := db.WithContext(ctx).Where("id = ?", id).First(user).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotExist{args: errutil.Args{"userID": id}}
}
return nil, err
@ -831,11 +829,17 @@ func (db *users) GetByID(ctx context.Context, id int64) (*User, error) {
return user, nil
}
func (db *users) GetByUsername(ctx context.Context, username string) (*User, error) {
func getUserByUsername(ctx context.Context, db *gorm.DB, userType UserType, username string) (*User, error) {
if username == "" {
return nil, ErrUserNotExist{args: errutil.Args{"name": username}}
}
user := new(User)
err := db.WithContext(ctx).Where("lower_name = ?", strings.ToLower(username)).First(user).Error
err := db.WithContext(ctx).
Where("type = ? AND lower_name = ?", userType, strings.ToLower(username)).
First(user).
Error
if err != nil {
if err == gorm.ErrRecordNotFound {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotExist{args: errutil.Args{"name": username}}
}
return nil, err
@ -843,6 +847,10 @@ func (db *users) GetByUsername(ctx context.Context, username string) (*User, err
return user, nil
}
func (db *users) GetByUsername(ctx context.Context, username string) (*User, error) {
return getUserByUsername(ctx, db.DB, UserTypeIndividual, username)
}
func (db *users) GetByKeyID(ctx context.Context, keyID int64) (*User, error) {
user := new(User)
err := db.WithContext(ctx).
@ -851,7 +859,7 @@ func (db *users) GetByKeyID(ctx context.Context, keyID int64) (*User, error) {
First(user).
Error
if err != nil {
if err == gorm.ErrRecordNotFound {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotExist{args: errutil.Args{"keyID": keyID}}
}
return nil, err
@ -1126,15 +1134,15 @@ func (ErrEmailNotExist) NotFound() bool {
}
func (db *users) GetEmail(ctx context.Context, userID int64, email string, needsActivated bool) (*EmailAddress, error) {
tx := db.WithContext(ctx).Where("uid = ? AND email = ?", userID, email)
conds := db.WithContext(ctx).Where("uid = ? AND email = ?", userID, email)
if needsActivated {
tx = tx.Where("is_activated = ?", true)
conds.Where("is_activated = ?", true)
}
emailAddress := new(EmailAddress)
err := tx.First(emailAddress).Error
err := conds.First(emailAddress).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrEmailNotExist{
args: errutil.Args{
"email": email,
@ -1213,7 +1221,7 @@ func (db *users) MarkEmailPrimary(ctx context.Context, userID int64, email strin
var emailAddress EmailAddress
err := db.WithContext(ctx).Where("uid = ? AND email = ?", userID, email).First(&emailAddress).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrEmailNotExist{args: errutil.Args{"email": email}}
}
return errors.Wrap(err, "get email address")
@ -1317,7 +1325,6 @@ type User struct {
NumTeams int
NumMembers int
Teams []*Team `xorm:"-" gorm:"-" json:"-"`
Members []*User `xorm:"-" gorm:"-" json:"-"`
}
// BeforeCreate implements the GORM create hook.
@ -1467,13 +1474,12 @@ func (u *User) IsFollowing(followID int64) bool {
return Users.IsFollowing(context.TODO(), u.ID, followID)
}
// IsUserOrgOwner returns true if the user is in the owner team of the given
// organization.
// IsUserOrgOwner returns true if the user is an owner of the organization.
//
// TODO(unknwon): This is also used in templates, which should be fixed by
// having a dedicated type `template.User`.
func (u *User) IsUserOrgOwner(orgId int64) bool {
return IsOrganizationOwner(orgId, u.ID)
func (u *User) IsUserOrgOwner(orgID int64) bool {
return Orgs.IsOwnedBy(context.TODO(), orgID, u.ID)
}
// IsPublicMember returns true if the user has public membership of the given
@ -1481,8 +1487,9 @@ func (u *User) IsUserOrgOwner(orgId int64) bool {
//
// TODO(unknwon): This is also used in templates, which should be fixed by
// having a dedicated type `template.User`.
func (u *User) IsPublicMember(orgId int64) bool {
return IsPublicMembership(orgId, u.ID)
func (u *User) IsPublicMember(orgID int64) bool {
_, public := Orgs.HasMember(context.TODO(), orgID, u.ID)
return public
}
// GetOrganizationCount returns the count of organization membership that the

View File

@ -801,16 +801,23 @@ func usersGetByID(t *testing.T, ctx context.Context, db *users) {
}
func usersGetByUsername(t *testing.T, ctx context.Context, db *users) {
alice, err := db.Create(ctx, "alice", "alice@exmaple.com", CreateUserOptions{})
require.NoError(t, err)
t.Run("correct user type", func(t *testing.T) {
alice, err := db.Create(ctx, "alice", "alice@exmaple.com", CreateUserOptions{})
require.NoError(t, err)
user, err := db.GetByUsername(ctx, alice.Name)
require.NoError(t, err)
assert.Equal(t, alice.Name, user.Name)
user, err := db.GetByUsername(ctx, alice.Name)
require.NoError(t, err)
assert.Equal(t, alice.Name, user.Name)
_, err = db.GetByUsername(ctx, "bad_username")
wantErr := ErrUserNotExist{args: errutil.Args{"name": "bad_username"}}
assert.Equal(t, wantErr, err)
})
t.Run("wrong user type", func(t *testing.T) {
// org1,err:=NewOrgsStore(db.DB).Create(ctx,"org1","// TODO: Use Orgs.Create
})
_, err = db.GetByUsername(ctx, "bad_username")
wantErr := ErrUserNotExist{args: errutil.Args{"name": "bad_username"}}
assert.Equal(t, wantErr, err)
}
func usersGetByKeyID(t *testing.T, ctx context.Context, db *users) {

View File

@ -23,10 +23,8 @@ func Organizations(c *context.Context) {
c.Data["PageIsAdminOrganizations"] = true
route.RenderUserSearch(c, &route.UserSearchOptions{
Type: db.UserTypeOrganization,
Counter: func(gocontext.Context) int64 {
return db.CountOrganizations()
},
Type: db.UserTypeOrganization,
Counter: db.Orgs.Count,
Ranger: func(_ gocontext.Context, page, pageSize int) ([]*db.User, error) {
return db.Organizations(page, pageSize)
},

View File

@ -104,7 +104,7 @@ func Edit(c *context.APIContext, form api.EditOrgOption) {
return
}
org, err = db.GetOrgByName(org.Name)
org, err = db.Orgs.GetByName(c.Req.Context(), org.Name)
if err != nil {
c.Error(err, "get organization")
return

View File

@ -87,7 +87,14 @@ func listUserRepositories(c *context.APIContext, username string) {
// or an organization isn't a member of.
var ownRepos []*db.Repository
if user.IsOrganization() {
ownRepos, _, err = user.GetUserRepositories(c.User.ID, 1, user.NumRepos)
ownRepos, _, err = db.Orgs.AccessibleRepositoriesByUser(
c.Req.Context(),
user.ID,
c.User.ID,
1,
user.NumRepos,
db.AccessibleRepositoriesByUserOptions{},
)
} else {
ownRepos, err = db.GetUserRepositories(&db.UserRepoOptions{
UserID: user.ID,
@ -191,7 +198,7 @@ func Create(c *context.APIContext, opt api.CreateRepoOption) {
}
func CreateOrgRepo(c *context.APIContext, opt api.CreateRepoOption) {
org, err := db.GetOrgByName(c.Params(":org"))
org, err := db.Orgs.GetByName(c.Req.Context(), c.Params(":org"))
if err != nil {
c.NotFoundOrError(err, "get organization by name")
return

View File

@ -152,10 +152,8 @@ func ExploreOrganizations(c *context.Context) {
c.Data["PageIsExploreOrganizations"] = true
RenderUserSearch(c, &UserSearchOptions{
Type: db.UserTypeOrganization,
Counter: func(gocontext.Context) int64 {
return db.CountOrganizations()
},
Type: db.UserTypeOrganization,
Counter: db.Orgs.Count,
Ranger: func(_ gocontext.Context, page, pageSize int) ([]*db.User, error) {
return db.Organizations(page, pageSize)
},

View File

@ -23,11 +23,12 @@ func Members(c *context.Context) {
c.Data["Title"] = org.FullName
c.Data["PageIsOrgMembers"] = true
if err := org.GetMembers(0); err != nil {
c.Error(err, "get members")
members, err := db.Orgs.ListMembers(c.Req.Context(), org.ID, db.ListOrgMembersOptions{})
if err != nil {
c.Error(err, "list members")
return
}
c.Data["Members"] = org.Members
c.Data["Members"] = members
c.Success(MEMBERS)
}
@ -47,26 +48,26 @@ func MembersAction(c *context.Context) {
c.NotFound()
return
}
err = db.ChangeOrgUserStatus(org.ID, uid, false)
err = db.Orgs.SetMemberVisibility(c.Req.Context(), org.ID, uid, false)
case "public":
if c.User.ID != uid && !c.Org.IsOwner {
c.NotFound()
return
}
err = db.ChangeOrgUserStatus(org.ID, uid, true)
err = db.Orgs.SetMemberVisibility(c.Req.Context(), org.ID, uid, true)
case "remove":
if !c.Org.IsOwner {
c.NotFound()
return
}
err = org.RemoveMember(uid)
err = db.Orgs.RemoveMember(c.Req.Context(), org.ID, uid)
if db.IsErrLastOrgOwner(err) {
c.Flash.Error(c.Tr("form.last_org_owner"))
c.Redirect(c.Org.OrgLink + "/members")
return
}
case "leave":
err = org.RemoveMember(c.User.ID)
err = db.Orgs.RemoveMember(c.Req.Context(), org.ID, c.User.ID)
if db.IsErrLastOrgOwner(err) {
c.Flash.Error(c.Tr("form.last_org_owner"))
c.Redirect(c.Org.OrgLink + "/members")
@ -108,7 +109,7 @@ func Invitation(c *context.Context) {
return
}
if err = org.AddMember(u.ID); err != nil {
if err = db.Orgs.AddMember(c.Req.Context(), org.ID, u.ID); err != nil {
c.Error(err, "add member")
return
}

View File

@ -140,7 +140,14 @@ func Dashboard(c *context.Context) {
var repos, mirrors []*db.Repository
var repoCount int64
if ctxUser.IsOrganization() {
repos, repoCount, err = ctxUser.GetUserRepositories(c.User.ID, 1, conf.UI.User.RepoPagingNum)
repos, repoCount, err = db.Orgs.AccessibleRepositoriesByUser(
c.Req.Context(),
ctxUser.ID,
c.User.ID,
1,
conf.UI.User.RepoPagingNum,
db.AccessibleRepositoriesByUserOptions{},
)
if err != nil {
c.Error(err, "get user repositories")
return
@ -236,7 +243,14 @@ func Issues(c *context.Context) {
showRepos = make([]*db.Repository, 0, 10)
)
if ctxUser.IsOrganization() {
repos, _, err = ctxUser.GetUserRepositories(c.User.ID, 1, ctxUser.NumRepos)
repos, _, err = db.Orgs.AccessibleRepositoriesByUser(
c.Req.Context(),
ctxUser.ID,
c.User.ID,
1,
ctxUser.NumRepos,
db.AccessibleRepositoriesByUserOptions{SkipCount: true},
)
if err != nil {
c.Error(err, "get repositories")
return
@ -409,7 +423,14 @@ func showOrgProfile(c *context.Context) {
err error
)
if c.IsLogged && !c.User.IsAdmin {
repos, count, err = org.GetUserRepositories(c.User.ID, page, conf.UI.User.RepoPagingNum)
repos, count, err = db.Orgs.AccessibleRepositoriesByUser(
c.Req.Context(),
org.ID,
c.User.ID,
page,
conf.UI.User.RepoPagingNum,
db.AccessibleRepositoriesByUserOptions{},
)
if err != nil {
c.Error(err, "get user repositories")
return
@ -432,11 +453,12 @@ func showOrgProfile(c *context.Context) {
}
c.Data["Page"] = paginater.New(int(count), conf.UI.User.RepoPagingNum, page, 5)
if err := org.GetMembers(12); err != nil {
c.Error(err, "get members")
members, err := db.Orgs.ListMembers(c.Req.Context(), org.ID, db.ListOrgMembersOptions{Limit: 12})
if err != nil {
c.Error(err, "list members")
return
}
c.Data["Members"] = org.Members
c.Data["Members"] = members
c.Data["Teams"] = org.Teams

View File

@ -566,7 +566,7 @@ func SettingsOrganizations(c *context.Context) {
}
func SettingsLeaveOrganization(c *context.Context) {
if err := db.RemoveOrgUser(c.QueryInt64("id"), c.User.ID); err != nil {
if err := db.Orgs.RemoveMember(c.Req.Context(), c.QueryInt64("id"), c.User.ID); err != nil {
if db.IsErrLastOrgOwner(err) {
c.Flash.Error(c.Tr("form.last_org_owner"))
} else {