From 10fd9e9e38555b6caecb7d37ba1638f2b4262c9c Mon Sep 17 00:00:00 2001 From: Joe Chen <jc@unknwon.io> Date: Thu, 24 Aug 2023 01:00:45 -0400 Subject: [PATCH] db: migrate `org.go` to `orgs.go` with GORM --- .github/workflows/go.yml | 10 +- docs/dev/database_schema.md | 19 + internal/context/org.go | 4 +- internal/db/access_tokens.go | 2 +- internal/db/actions.go | 23 +- internal/db/backup_test.go | 19 +- internal/db/db.go | 1 + internal/db/error.go | 20 - internal/db/lfs.go | 3 +- internal/db/login_sources.go | 2 +- internal/db/migrations/migrations.go | 2 +- internal/db/models.go | 4 +- internal/db/org.go | 373 +---------------- internal/db/org_team.go | 143 +++---- internal/db/orgs.go | 390 +++++++++++++++++- internal/db/repo.go | 49 ++- internal/db/repos.go | 7 +- .../db/testdata/backup/OrgUser.golden.json | 2 + internal/db/two_factors.go | 2 +- internal/db/users.go | 49 ++- internal/db/users_test.go | 23 +- internal/route/admin/orgs.go | 6 +- internal/route/api/v1/org/org.go | 2 +- internal/route/api/v1/repo/repo.go | 11 +- internal/route/home.go | 6 +- internal/route/org/members.go | 17 +- internal/route/user/home.go | 34 +- internal/route/user/setting.go | 2 +- 28 files changed, 663 insertions(+), 562 deletions(-) create mode 100644 internal/db/testdata/backup/OrgUser.golden.json diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index c9feee4e2..dae14e837 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -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: diff --git a/docs/dev/database_schema.md b/docs/dev/database_schema.md index b49cca3e6..613f1831e 100644 --- a/docs/dev/database_schema.md +++ b/docs/dev/database_schema.md @@ -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) +``` + diff --git a/internal/context/org.go b/internal/context/org.go index 389677af9..51256358a 100644 --- a/internal/context/org.go +++ b/internal/context/org.go @@ -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. diff --git a/internal/db/access_tokens.go b/internal/db/access_tokens.go index 825cfa87a..f9b63e154 100644 --- a/internal/db/access_tokens.go +++ b/internal/db/access_tokens.go @@ -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 diff --git a/internal/db/actions.go b/internal/db/actions.go index 48d080b3d..a67cb0b8d 100644 --- a/internal/db/actions.go +++ b/internal/db/actions.go @@ -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) { diff --git a/internal/db/backup_test.go b/internal/db/backup_test.go index 9d2d9403a..391af791a 100644 --- a/internal/db/backup_test.go +++ b/internal/db/backup_test.go @@ -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 diff --git a/internal/db/db.go b/internal/db/db.go index 9878032d3..e7dc31119 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -46,6 +46,7 @@ var Tables = []any{ new(Follow), new(LFSObject), new(LoginSource), new(Notice), + new(OrgUser), } // Init initializes the database with given logger. diff --git a/internal/db/error.go b/internal/db/error.go index 8436aa99e..1c831cf86 100644 --- a/internal/db/error.go +++ b/internal/db/error.go @@ -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) -} - // __________ .__ __ // \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__. // | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | | diff --git a/internal/db/lfs.go b/internal/db/lfs.go index bff18efd1..6f9116614 100644 --- a/internal/db/lfs.go +++ b/internal/db/lfs.go @@ -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 diff --git a/internal/db/login_sources.go b/internal/db/login_sources.go index 9469a3f08..c5a7b974d 100644 --- a/internal/db/login_sources.go +++ b/internal/db/login_sources.go @@ -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 diff --git a/internal/db/migrations/migrations.go b/internal/db/migrations/migrations.go index 04c1e3636..d6cfe6f94 100644 --- a/internal/db/migrations/migrations.go +++ b/internal/db/migrations/migrations.go @@ -80,7 +80,7 @@ func Migrate(db *gorm.DB) error { var current Version err := db.Where("id = ?", 1).First(¤t).Error - if err == gorm.ErrRecordNotFound { + if errors.Is(err, gorm.ErrRecordNotFound) { err = db.Create( &Version{ ID: 1, diff --git a/internal/db/models.go b/internal/db/models.go index 1e3b8d9b7..ebbe5136d 100644 --- a/internal/db/models.go +++ b/internal/db/models.go @@ -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)) diff --git a/internal/db/org.go b/internal/db/org.go index 8bc16701e..4b780cdc6 100644 --- a/internal/db/org.go +++ b/internal/db/org.go @@ -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 diff --git a/internal/db/org_team.go b/internal/db/org_team.go index d737167e4..c79a38f33 100644 --- a/internal/db/org_team.go +++ b/internal/db/org_team.go @@ -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) +} diff --git a/internal/db/orgs.go b/internal/db/orgs.go index 753d81209..0550d07a3 100644 --- a/internal/db/orgs.go +++ b/internal/db/orgs.go @@ -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"` +} diff --git a/internal/db/repo.go b/internal/db/repo.go index 8f15ea2b9..08040e5c9 100644 --- a/internal/db/repo.go +++ b/internal/db/repo.go @@ -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 { diff --git a/internal/db/repos.go b/internal/db/repos.go index 28a38148c..d7f53d612 100644 --- a/internal/db/repos.go +++ b/internal/db/repos.go @@ -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, diff --git a/internal/db/testdata/backup/OrgUser.golden.json b/internal/db/testdata/backup/OrgUser.golden.json new file mode 100644 index 000000000..d27c3cee4 --- /dev/null +++ b/internal/db/testdata/backup/OrgUser.golden.json @@ -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} diff --git a/internal/db/two_factors.go b/internal/db/two_factors.go index 741a2ff79..901191576 100644 --- a/internal/db/two_factors.go +++ b/internal/db/two_factors.go @@ -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 diff --git a/internal/db/users.go b/internal/db/users.go index c39f9f39c..d82d8727c 100644 --- a/internal/db/users.go +++ b/internal/db/users.go @@ -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 diff --git a/internal/db/users_test.go b/internal/db/users_test.go index bb0273a58..d31254b74 100644 --- a/internal/db/users_test.go +++ b/internal/db/users_test.go @@ -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) { diff --git a/internal/route/admin/orgs.go b/internal/route/admin/orgs.go index 922d7f863..46a26b422 100644 --- a/internal/route/admin/orgs.go +++ b/internal/route/admin/orgs.go @@ -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) }, diff --git a/internal/route/api/v1/org/org.go b/internal/route/api/v1/org/org.go index 9b99bead7..e6f2a192c 100644 --- a/internal/route/api/v1/org/org.go +++ b/internal/route/api/v1/org/org.go @@ -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 diff --git a/internal/route/api/v1/repo/repo.go b/internal/route/api/v1/repo/repo.go index a4cc36b30..d16ff84bb 100644 --- a/internal/route/api/v1/repo/repo.go +++ b/internal/route/api/v1/repo/repo.go @@ -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 diff --git a/internal/route/home.go b/internal/route/home.go index a389c0801..183222891 100644 --- a/internal/route/home.go +++ b/internal/route/home.go @@ -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) }, diff --git a/internal/route/org/members.go b/internal/route/org/members.go index b53034e43..a8f8417e4 100644 --- a/internal/route/org/members.go +++ b/internal/route/org/members.go @@ -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 } diff --git a/internal/route/user/home.go b/internal/route/user/home.go index 4521f511b..6aa3f9a55 100644 --- a/internal/route/user/home.go +++ b/internal/route/user/home.go @@ -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 diff --git a/internal/route/user/setting.go b/internal/route/user/setting.go index 6ee5fa4d3..b1560c8e6 100644 --- a/internal/route/user/setting.go +++ b/internal/route/user/setting.go @@ -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 {